Преглед на файлове

Texture inspector: allows tools to use post processes (#8694)

* 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

* upload and resize

* color picker UI

* add external tools

* add default tools to codebase

* remove comments

* remove extra flipping code

* linting and tool fixes

* fix import

* allow tools to support dynamic parameters

* rename defaultTools index for tree shaking

* initial PP commit

* fixes to contrast/exposure tool

* minor fixes

* cleanup

* fixes

* Rename Paintbrush.ts to paintbrush.ts

* Rename Floodfill.ts to floodfill.ts

* Rename Eyedropper.ts to eyedropper.ts

* add typing and comments to tool API
DarraghBurkeMS преди 5 години
родител
ревизия
8cf09e4d9f
променени са 17 файла, в които са добавени 962 реда и са изтрити 570 реда
  1. 1 1
      dist/preview release/what's new.md
  2. 4 3
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/texturePropertyGridComponent.tsx
  3. 5 5
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/channelsBar.tsx
  4. 0 59
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/Paintbrush.ts
  5. 100 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/contrast.ts
  6. 6 5
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/defaultTools.ts
  7. 59 60
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/Eyedropper.ts
  8. 44 44
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/Floodfill.ts
  9. 97 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/paintbrush.ts
  10. 10 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/rectangleSelect.ts
  11. 143 143
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/propertiesBar.tsx
  12. 5 5
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasComponent.tsx
  13. 293 107
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasManager.ts
  14. 8 2
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditor.scss
  15. 83 35
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditorComponent.tsx
  16. 100 101
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/toolBar.tsx
  17. 4 0
      inspector/src/components/popupComponent.tsx

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

@@ -79,7 +79,7 @@
 - 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))
 - Hex Component for Hex inputs on layer masks. ([msDestiny14](https://github.com/msDestiny14))
-- View & edit textures in pop out inspector with zoom & pan and individual channels, and save to local machine. ([DarraghBurkeMS](https://github.com/DarraghBurkeMS))
+- View & edit textures in pop out inspector using canvas and postprocesses. ([DarraghBurkeMS](https://github.com/DarraghBurkeMS))
 
 ### Cameras
 

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

@@ -45,6 +45,7 @@ interface ITexturePropertyGridComponentState {
 export class TexturePropertyGridComponent extends React.Component<ITexturePropertyGridComponentProps,ITexturePropertyGridComponentState> {
 
     private _adtInstrumentation: Nullable<AdvancedDynamicTextureInstrumentation>;
+    private popoutWindowRef : React.RefObject<PopupComponent>;
     private textureLineRef: React.RefObject<TextureLineComponent>;
 
 
@@ -58,6 +59,7 @@ export class TexturePropertyGridComponent extends React.Component<ITextureProper
         const texture = this.props.texture;
 
         this.textureLineRef = React.createRef();
+        this.popoutWindowRef = React.createRef();
 
         if (!texture || !(texture as any).rootContainer) {
             return;
@@ -120,9 +122,6 @@ export class TexturePropertyGridComponent extends React.Component<ITextureProper
             isTextureEditorOpen: false,
             textureEditing: null
         }, callback);
-        if (window !== null) {
-            window.close();
-        }
     }
 
     forceRefresh() {
@@ -185,11 +184,13 @@ export class TexturePropertyGridComponent extends React.Component<ITextureProper
                   onClose={(window: Window) =>
                     this.onCloseTextureEditor(window)
                   }
+                  ref={this.popoutWindowRef}
                 >
                     <TextureEditorComponent
                         globalState={this.props.globalState}
                         texture={this.props.texture}
                         url={textureUrl}
+                        window={this.popoutWindowRef}
                     />
                 </PopupComponent>)}
                 <CustomPropertyGridComponent globalState={this.props.globalState} target={texture}

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

@@ -1,6 +1,6 @@
 import * as React from 'react';
 
-export interface Channel {
+export interface IChannel {
     visible: boolean;
     editable: boolean;
     name: string;
@@ -8,15 +8,15 @@ export interface Channel {
     icon: any;
 }
 
-interface ChannelsBarProps {
-    channels: Channel[];
-    setChannels(channelState : Channel[]) : void;
+interface IChannelsBarProps {
+    channels: IChannel[];
+    setChannels(channelState : IChannel[]) : void;
 }
 
 const eyeOpen = require('./assets/eyeOpen.svg');
 const eyeClosed = require('./assets/eyeClosed.svg');
 
-export class ChannelsBar extends React.Component<ChannelsBarProps> {
+export class ChannelsBar extends React.Component<IChannelsBarProps> {
     render() {
         return <div id='channels-bar'>
             {this.props.channels.map(

+ 0 - 59
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/Paintbrush.ts

@@ -1,59 +0,0 @@
-import { ToolParameters, ToolData } from '../textureEditorComponent';
-import { PointerEventTypes, PointerInfo } from 'babylonjs/Events/pointerEvents';
-
-export const Paintbrush : ToolData = {
-    name: "Paintbrush",
-    type: class {
-        getParameters: () => ToolParameters;
-        pointerObservable: any;
-        isPainting: boolean;
-
-        constructor(getParameters: () => ToolParameters) {
-            this.getParameters = getParameters;
-        }
-
-        paint(pointerInfo : PointerInfo) {
-            const p = this.getParameters();
-            const ctx = p.canvas2D.getContext('2d')!;
-            const x = pointerInfo.pickInfo!.getTextureCoordinates()!.x * p.size.width;
-            const y = (1 - pointerInfo.pickInfo!.getTextureCoordinates()!.y) * p.size.height;
-            ctx.globalCompositeOperation = 'source-over';
-            ctx.fillStyle = p.getMetadata().color;
-            ctx.globalAlpha = p.getMetadata().opacity;
-            ctx.beginPath();
-            ctx.ellipse(x, y, 15, 15, 0, 0, Math.PI * 2);
-            ctx.fill();
-            p.updateTexture();
-        }
-        
-        setup () {
-            this.pointerObservable = this.getParameters().scene.onPointerObservable.add((pointerInfo) => {
-                if (pointerInfo.pickInfo?.hit) {
-                    if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
-                        if (pointerInfo.event.button == 0) {
-                            this.isPainting = true;
-                          }
-                    }
-                    if (pointerInfo.type === PointerEventTypes.POINTERMOVE && this.isPainting) {
-                        this.paint(pointerInfo);
-                    }
-                }
-                if (pointerInfo.type === PointerEventTypes.POINTERUP) {
-                    if (pointerInfo.event.button == 0) {
-                        this.isPainting = false;
-                      }
-                }
-            });
-            this.isPainting = false;
-        }
-        cleanup () {
-            this.isPainting = false;
-            if (this.pointerObservable) {
-                this.getParameters().scene.onPointerObservable.remove(this.pointerObservable);
-            }
-        }
-    },
-    icon: `PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHN0eWxlPSJmaWxsOm5vbmUiLz48cGF0aCBkPSJNMjksMTFhMy41
-    NywzLjU3LDAsMCwxLDAsNS4wNkwxNywyOGEyLjM0LDIuMzQsMCwwLDEtMSwuNThMMTAuOTEsMzBhLjc1Ljc1LDAsMCwxLS45Mi0uOTJMMTEuMzgsMjRBMi4zNCwyLjM0LDAsMCwxLDEyLDIzbDEyLTEyQTMuNTcsMy41NywwLDAsMSwyOSwxMVpNMjMsMTQuMSwxMywy
-    NGEuNjkuNjksMCwwLDAtLjE5LjMzbC0xLjA1LDMuODUsMy44NS0xQS42OS42OSwwLDAsMCwxNiwyN0wyNS45LDE3Wm0yLTItMSwxTDI3LDE2bDEtMUEyLjA4LDIuMDgsMCwxLDAsMjUsMTIuMDdaIiBzdHlsZT0iZmlsbDojZmZmIi8+PC9zdmc+`
-};

+ 100 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/contrast.ts

@@ -0,0 +1,100 @@
+import { IToolData, IToolParameters, IToolType } from '../textureEditorComponent';
+import { TextBlock } from 'babylonjs-gui/2D/controls/textBlock';
+import { Slider } from 'babylonjs-gui/2D/controls/sliders/slider';
+
+export const Contrast : IToolData = {
+    name: 'Contrast/Exposure',
+    type: class implements IToolType {
+        getParameters: () => IToolParameters;
+        contrast : number = 1.0;
+        exposure : number = 1.0;
+        GUI: {
+            contrastLabel : TextBlock;
+            contrastSlider : Slider;
+            exposureLabel : TextBlock;
+            exposureSlider : Slider;
+        }
+        constructor(getParameters: () => IToolParameters) {
+            this.getParameters = getParameters;
+        }
+        setExposure(exposure : number) {
+            this.exposure = exposure;
+            this.GUI.exposureLabel.text = `Exposure: ${this.exposure}`;
+            const {scene3D, updateTexture} = this.getParameters();
+            scene3D.imageProcessingConfiguration.isEnabled = true;
+            scene3D.imageProcessingConfiguration.exposure = this.computeExposure(this.exposure);
+            updateTexture();
+        }
+        setContrast(contrast : number) {
+            this.contrast = contrast;
+            this.GUI.contrastLabel.text = `Contrast: ${this.contrast}`;
+            const {scene3D, updateTexture} = this.getParameters();
+            scene3D.imageProcessingConfiguration.isEnabled = true;
+            scene3D.imageProcessingConfiguration.contrast = this.computeContrast(contrast);
+            updateTexture();
+        }
+        /** Maps slider values to post processing values using an exponential regression */
+        computeExposure(sliderValue : number) {
+            return Math.pow(1.05698, sliderValue) + 0.0000392163 * sliderValue;
+        }
+        /** Maps slider values to post processing values using an exponential regression */
+        computeContrast(sliderValue : number) {
+            return Math.pow(1.05698, sliderValue) + 0.0000392163 * sliderValue;
+        }
+        setup() {
+            this.contrast = 0;
+            this.exposure = 0;
+            const {GUI} = this.getParameters();
+            const contrastLabel = new TextBlock();
+            contrastLabel.style = GUI.style;
+            contrastLabel.height = '20px';
+            contrastLabel.color = '#ffffff';
+            const contrastSlider = new Slider();
+            contrastSlider.value = this.contrast;
+            contrastSlider.minimum = -100;
+            contrastSlider.maximum = 100;
+            contrastSlider.height = '20px';
+            contrastSlider.isThumbCircle = true;
+            contrastSlider.background = '#a3a3a3';
+            contrastSlider.color = '#33648f';
+            contrastSlider.borderColor = '#33648f';
+            contrastSlider.onValueChangedObservable.add(evt => this.setContrast(evt.valueOf()));
+            const exposureLabel = new TextBlock();
+            exposureLabel.style = GUI.style;
+            exposureLabel.height = '20px';
+            exposureLabel.color = '#ffffff';
+            const exposureSlider = new Slider();
+            exposureSlider.value = this.exposure;
+            exposureSlider.minimum = -100;
+            exposureSlider.maximum = 100;
+            exposureSlider.height = '20px';
+            exposureSlider.isThumbCircle = true;
+            exposureSlider.background = '#a3a3a3';
+            exposureSlider.color = '#33648f';
+            exposureSlider.borderColor = '#33648f';
+            exposureSlider.onValueChangedObservable.add(evt => this.setExposure(evt.valueOf()));
+            GUI.toolWindow.addControl(contrastLabel);
+            GUI.toolWindow.addControl(contrastSlider);
+            GUI.toolWindow.addControl(exposureLabel);
+            GUI.toolWindow.addControl(exposureSlider);
+            this.GUI = {contrastLabel, contrastSlider, exposureLabel, exposureSlider};
+            this.setExposure(this.exposure);
+            this.setContrast(this.contrast);
+        }
+        cleanup() {
+            Object.entries(this.GUI).forEach(([key, value]) => value.dispose());
+        }
+        onReset() {
+            this.GUI.contrastSlider.value = 0;
+            this.GUI.exposureSlider.value = 0;
+        }
+    },
+    usesWindow: true,
+    is3D: true,
+    icon: `PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj48cmVjdCB3aWR0aD0i
+    NDAiIGhlaWdodD0iNDAiIHN0eWxlPSJmaWxsOm5vbmUiLz48cGF0aCBkPSJNMTcuNTUsMjYuNTVsOC41OS0zLjIxQTYuODYsNi44NiwwLDAsMSwyNCwyNS43NWwtMy4x
+    OSwxLjE5QTcsNywwLDAsMSwxNy41NSwyNi41NVpNMjAsMTEuNUE4LjUsOC41LDAsMSwwLDI4LjUsMjAsOC41MSw4LjUxLDAsMCwwLDIwLDExLjVNMjAsMTBBMTAsMTAs
+    MCwxLDEsMTAsMjAsMTAsMTAsMCwwLDEsMjAsMTBabS0yLjQ1LDUuMzQsNS0xLjg2QTcsNywwLDAsMCwxOS40NCwxM2wtMS44OS43MVptMCwzLjIsNy44OC0yLjk0YTYu
+    ODgsNi44OCwwLDAsMC0xLjE5LTEuMTZsLTYuNjksMi41Wm0wLDMuMiw5LjIzLTMuNDRhNy42OCw3LjY4LDAsMCwwLS41Mi0xLjQxbC04LjcxLDMuMjVabTAsMS42djEu
+    Nmw5LjI4LTMuNDZBNi42Nyw2LjY3LDAsMCwwLDI3LDE5LjgyWiIgc3R5bGU9ImZpbGw6I2ZmZiIvPjwvc3ZnPg==`
+}

+ 6 - 5
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/defaultTools.ts

@@ -1,5 +1,6 @@
-import { Paintbrush } from './Paintbrush';
-import { Eyedropper } from './Eyedropper';
-import { Floodfill } from './Floodfill';
-
-export default [Paintbrush, Eyedropper, Floodfill];
+import { Paintbrush } from './paintbrush';
+import { Eyedropper } from './eyedropper';
+import { Floodfill } from './floodfill';
+import { Contrast } from './contrast';
+
+export default [Paintbrush, Eyedropper, Floodfill, Contrast];

+ 59 - 60
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/Eyedropper.ts

@@ -1,60 +1,59 @@
-import { ToolParameters, ToolData } from '../textureEditorComponent';
-import { PointerEventTypes, PointerInfo } from 'babylonjs/Events/pointerEvents';
-
-export const Eyedropper : ToolData = {
-    name: "Eyedropper",
-    type: class {
-        getParameters: () => ToolParameters;
-        pointerObservable: any;
-        isPicking: boolean;
-
-        constructor(getParameters: () => ToolParameters) {
-            this.getParameters = getParameters;
-        }
-
-        pick(pointerInfo : PointerInfo) {
-            const p = this.getParameters();
-            const ctx = p.canvas2D.getContext('2d');
-            const x = pointerInfo.pickInfo!.getTextureCoordinates()!.x * p.size.width;
-            const y = (1 - pointerInfo.pickInfo!.getTextureCoordinates()!.y) * p.size.height;
-            const pixel = ctx!.getImageData(x, y, 1, 1).data;
-            p.setMetadata({
-                color: "#" + ("000000" + this.rgbToHex(pixel[0], pixel[1], pixel[2])).slice(-6),
-                opacity: pixel[3] / 255
-            });
-        }
-        
-        setup () {
-            this.pointerObservable = this.getParameters().scene.onPointerObservable.add((pointerInfo) => {
-                if (pointerInfo.pickInfo?.hit) {
-                    if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
-                        this.isPicking = true;
-                        this.pick(pointerInfo);
-                    }
-                    if (pointerInfo.type === PointerEventTypes.POINTERMOVE && this.isPicking) {
-                        this.pick(pointerInfo);
-                    }
-                    if (pointerInfo.type === PointerEventTypes.POINTERUP) {
-                        this.isPicking = false;
-                    }
-                }
-            });
-            this.isPicking = false;
-        }
-        cleanup () {
-            if (this.pointerObservable) {
-                this.getParameters().scene.onPointerObservable.remove(this.pointerObservable);
-            }
-        }
-        rgbToHex(r: number, g:number, b: number) {
-            return ((r << 16) | (g << 8) | b).toString(16);
-        }
-    },
-    icon: `PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHN0eWxlPSJmaWxsOm5vbmUiLz48cGF0aCBkPSJNMjkuMzIsMTAu
-    NjhjLTEuNjYtMS42Ni00LjA2LTEtNS41Ni41YTExLjg5LDExLjg5LDAsMCwwLTEuNjYsMi4zMUwyMiwxMy40MWExLjg5LDEuODksMCwwLDAtMi42NiwwbC0uOS45YTEuODksMS44OSwwLDAsMC0uMjIsMi4zOWwtNi4wOSw2LjA5YTIuNzUsMi43NSwwLDAsMC0uNzMs
-    MS4yOGwtLjgxLDMuMjYtLjU2LjU2YTEuMTYsMS4xNiwwLDAsMCwwLDEuNjVsLjQxLjQxYTEuMTcsMS4xNywwLDAsMCwxLjY1LDBsLjU2LS41NiwzLjI2LS44MWEyLjc1LDIuNzUsMCwwLDAsMS4yOC0uNzNsNi4xNC02LjE0YTEuODcsMS44NywwLDAsMCwuODQuMjEs
-    MS44MywxLjgzLDAsMCwwLDEuMzMtLjU1bC45LS45YTEuODcsMS44NywwLDAsMCwuMDgtMi41NywxMS41NCwxMS41NCwwLDAsMCwyLjMyLTEuNjZDMzAuMzIsMTQuNzQsMzEsMTIuMzUsMjkuMzIsMTAuNjhaTTE2LjE1LDI2Ljc5YTEuMjEsMS4yMSwwLDAsMS0uNTgu
-    MzNMMTIsMjhsLjktMy41OWExLjIxLDEuMjEsMCwwLDEsLjMzLS41OGw2LjA3LTYuMDcsMi45NCwyLjk0Wm05LjIxLTcuMzgtLjkuOWMtLjE5LjItLjM0LjItLjU0LDBsLTQuNC00LjRhLjQuNCwwLDAsMSwwLS41NGwuOS0uOWEuNDMuNDMsMCwwLDEsLjI3LS4xMS4z
-    OS4zOSwwLDAsMSwuMjcuMTFsNC40LDQuNEEuMzguMzgsMCwwLDEsMjUuMzYsMTkuNDFabTMuMzgtNS45M2EzLjcsMy43LDAsMCwxLTEsMS43LDExLjY3LDExLjY3LDAsMCwxLTIuMzUsMS42MkwyMy4yLDE0LjU5YTExLjY3LDExLjY3LDAsMCwxLDEuNjItMi4zNSwz
-    LjcsMy43LDAsMCwxLDEuNy0xLDEuODMsMS44MywwLDAsMSwyLjIyLDIuMjJaIiBzdHlsZT0iZmlsbDojZmZmIi8+PC9zdmc+`
-};
+import { IToolParameters, IToolData } from '../textureEditorComponent';
+import { PointerEventTypes, PointerInfo } from 'babylonjs/Events/pointerEvents';
+
+export const Eyedropper : IToolData = {
+    name: 'Eyedropper',
+    type: class {
+        getParameters: () => IToolParameters;
+        pointerObservable: any;
+        isPicking: boolean;
+
+        constructor(getParameters: () => IToolParameters) {
+            this.getParameters = getParameters;
+        }
+
+        pick(pointerInfo : PointerInfo) {
+            const p = this.getParameters();
+            const ctx = p.canvas2D.getContext('2d');
+            const {x, y} = p.getMouseCoordinates(pointerInfo);
+            const pixel = ctx!.getImageData(x, y, 1, 1).data;
+            p.setMetadata({
+                color: '#' + ('000000' + this.rgbToHex(pixel[0], pixel[1], pixel[2])).slice(-6),
+                opacity: pixel[3] / 255
+            });
+        }
+        
+        setup () {
+            this.pointerObservable = this.getParameters().scene.onPointerObservable.add((pointerInfo) => {
+                if (pointerInfo.pickInfo?.hit) {
+                    if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
+                        this.isPicking = true;
+                        this.pick(pointerInfo);
+                    }
+                    if (pointerInfo.type === PointerEventTypes.POINTERMOVE && this.isPicking) {
+                        this.pick(pointerInfo);
+                    }
+                    if (pointerInfo.type === PointerEventTypes.POINTERUP) {
+                        this.isPicking = false;
+                    }
+                }
+            });
+            this.isPicking = false;
+        }
+        cleanup () {
+            if (this.pointerObservable) {
+                this.getParameters().scene.onPointerObservable.remove(this.pointerObservable);
+            }
+        }
+        rgbToHex(r: number, g:number, b: number) {
+            return ((r << 16) | (g << 8) | b).toString(16);
+        }
+    },
+    icon: `PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHN0eWxlPSJmaWxsOm5vbmUiLz48cGF0aCBkPSJNMjkuMzIsMTAu
+    NjhjLTEuNjYtMS42Ni00LjA2LTEtNS41Ni41YTExLjg5LDExLjg5LDAsMCwwLTEuNjYsMi4zMUwyMiwxMy40MWExLjg5LDEuODksMCwwLDAtMi42NiwwbC0uOS45YTEuODksMS44OSwwLDAsMC0uMjIsMi4zOWwtNi4wOSw2LjA5YTIuNzUsMi43NSwwLDAsMC0uNzMs
+    MS4yOGwtLjgxLDMuMjYtLjU2LjU2YTEuMTYsMS4xNiwwLDAsMCwwLDEuNjVsLjQxLjQxYTEuMTcsMS4xNywwLDAsMCwxLjY1LDBsLjU2LS41NiwzLjI2LS44MWEyLjc1LDIuNzUsMCwwLDAsMS4yOC0uNzNsNi4xNC02LjE0YTEuODcsMS44NywwLDAsMCwuODQuMjEs
+    MS44MywxLjgzLDAsMCwwLDEuMzMtLjU1bC45LS45YTEuODcsMS44NywwLDAsMCwuMDgtMi41NywxMS41NCwxMS41NCwwLDAsMCwyLjMyLTEuNjZDMzAuMzIsMTQuNzQsMzEsMTIuMzUsMjkuMzIsMTAuNjhaTTE2LjE1LDI2Ljc5YTEuMjEsMS4yMSwwLDAsMS0uNTgu
+    MzNMMTIsMjhsLjktMy41OWExLjIxLDEuMjEsMCwwLDEsLjMzLS41OGw2LjA3LTYuMDcsMi45NCwyLjk0Wm05LjIxLTcuMzgtLjkuOWMtLjE5LjItLjM0LjItLjU0LDBsLTQuNC00LjRhLjQuNCwwLDAsMSwwLS41NGwuOS0uOWEuNDMuNDMsMCwwLDEsLjI3LS4xMS4z
+    OS4zOSwwLDAsMSwuMjcuMTFsNC40LDQuNEEuMzguMzgsMCwwLDEsMjUuMzYsMTkuNDFabTMuMzgtNS45M2EzLjcsMy43LDAsMCwxLTEsMS43LDExLjY3LDExLjY3LDAsMCwxLTIuMzUsMS42MkwyMy4yLDE0LjU5YTExLjY3LDExLjY3LDAsMCwxLDEuNjItMi4zNSwz
+    LjcsMy43LDAsMCwxLDEuNy0xLDEuODMsMS44MywwLDAsMSwyLjIyLDIuMjJaIiBzdHlsZT0iZmlsbDojZmZmIi8+PC9zdmc+`
+};

+ 44 - 44
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/Floodfill.ts

@@ -1,44 +1,44 @@
-import { ToolParameters, ToolData } from '../textureEditorComponent';
-import { PointerEventTypes } from 'babylonjs/Events/pointerEvents';
-
-export const Floodfill : ToolData = {
-    name: "Floodfill",
-    type: class {
-        getParameters: () => ToolParameters;
-        pointerObservable: any;
-
-        constructor(getParameters: () => ToolParameters) {
-            this.getParameters = getParameters;
-        }
-
-        fill() {
-            const p = this.getParameters();
-            const ctx = p.canvas2D.getContext('2d')!;
-            ctx.fillStyle = p.getMetadata().color;
-            ctx.globalAlpha = p.getMetadata().opacity;
-            ctx.globalCompositeOperation = 'source-over';
-            ctx.fillRect(0,0, p.size.width, p.size.height);
-            p.updateTexture();
-        }
-        
-        setup () {
-            this.pointerObservable = this.getParameters().scene.onPointerObservable.add((pointerInfo) => {
-                if (pointerInfo.pickInfo?.hit) {
-                    if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
-                        this.fill();
-                    }
-                }
-            });
-        }
-        cleanup () {
-            if (this.pointerObservable) {
-                this.getParameters().scene.onPointerObservable.remove(this.pointerObservable);
-            }
-        }
-    },
-    icon: `PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHN0eWxlPSJmaWxsOm5vbmUiLz48cGF0aCBkPSJNMjAsMTAuNWEu
-    NzUuNzUsMCwwLDAtMS41LDB2MS4yNWEyLjE0LDIuMTQsMCwwLDAtLjg0LjUzbC02Ljg4LDYuODhhMi4yNSwyLjI1LDAsMCwwLDAsMy4xOGw0Ljg4LDQuODhhMi4yNSwyLjI1LDAsMCwwLDMuMTgsMGw2Ljg4LTYuODhhMi4yNSwyLjI1LDAsMCwwLDAtMy4xOGwtNC44
-    OC00Ljg4YTIuMjksMi4yOSwwLDAsMC0uODQtLjUzWm0tOC4xNiw5LjcyLDYuNjYtNi42NlYxNUEuNzUuNzUsMCwwLDAsMjAsMTVWMTMuNTZsNC42Niw0LjY2YS43NS43NSwwLDAsMSwwLDEuMDZsLTEsMUgxMS44Wm0uNDcsMS41M2g5Ljg4bC00LjQxLDQuNDFhLjc1
-    Ljc1LDAsMCwxLTEuMDYsMFoiIHN0eWxlPSJmaWxsOiNmZmYiLz48cGF0aCBkPSJNMjcuNTEsMjEuODVhLjg4Ljg4LDAsMCwwLTEuNTQsMGwtMiwzLjc3YTMuMTUsMy4xNSwwLDEsMCw1LjU2LDBabS0yLjIzLDQuNDcsMS40Ni0yLjczLDEuNDUsMi43M2ExLjY1LDEu
-    NjUsMCwxLDEtMi45MSwwWiIgc3R5bGU9ImZpbGw6I2ZmZiIvPjwvc3ZnPg==`
-};
+import { IToolParameters, IToolData } from '../textureEditorComponent';
+import { PointerEventTypes } from 'babylonjs/Events/pointerEvents';
+
+export const Floodfill : IToolData = {
+    name: 'Floodfill',
+    type: class {
+        getParameters: () => IToolParameters;
+        pointerObservable: any;
+
+        constructor(getParameters: () => IToolParameters) {
+            this.getParameters = getParameters;
+        }
+
+        fill() {
+            const p = this.getParameters();
+            const ctx = p.canvas2D.getContext('2d')!;
+            ctx.fillStyle = p.metadata.color;
+            ctx.globalAlpha = p.metadata.opacity;
+            ctx.globalCompositeOperation = 'source-over';
+            ctx.fillRect(0,0, p.size.width, p.size.height);
+            p.updateTexture();
+        }
+        
+        setup () {
+            this.pointerObservable = this.getParameters().scene.onPointerObservable.add((pointerInfo) => {
+                if (pointerInfo.pickInfo?.hit) {
+                    if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
+                        this.fill();
+                    }
+                }
+            });
+        }
+        cleanup () {
+            if (this.pointerObservable) {
+                this.getParameters().scene.onPointerObservable.remove(this.pointerObservable);
+            }
+        }
+    },
+    icon: `PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHN0eWxlPSJmaWxsOm5vbmUiLz48cGF0aCBkPSJNMjAsMTAuNWEu
+    NzUuNzUsMCwwLDAtMS41LDB2MS4yNWEyLjE0LDIuMTQsMCwwLDAtLjg0LjUzbC02Ljg4LDYuODhhMi4yNSwyLjI1LDAsMCwwLDAsMy4xOGw0Ljg4LDQuODhhMi4yNSwyLjI1LDAsMCwwLDMuMTgsMGw2Ljg4LTYuODhhMi4yNSwyLjI1LDAsMCwwLDAtMy4xOGwtNC44
+    OC00Ljg4YTIuMjksMi4yOSwwLDAsMC0uODQtLjUzWm0tOC4xNiw5LjcyLDYuNjYtNi42NlYxNUEuNzUuNzUsMCwwLDAsMjAsMTVWMTMuNTZsNC42Niw0LjY2YS43NS43NSwwLDAsMSwwLDEuMDZsLTEsMUgxMS44Wm0uNDcsMS41M2g5Ljg4bC00LjQxLDQuNDFhLjc1
+    Ljc1LDAsMCwxLTEuMDYsMFoiIHN0eWxlPSJmaWxsOiNmZmYiLz48cGF0aCBkPSJNMjcuNTEsMjEuODVhLjg4Ljg4LDAsMCwwLTEuNTQsMGwtMiwzLjc3YTMuMTUsMy4xNSwwLDEsMCw1LjU2LDBabS0yLjIzLDQuNDcsMS40Ni0yLjczLDEuNDUsMi43M2ExLjY1LDEu
+    NjUsMCwxLDEtMi45MSwwWiIgc3R5bGU9ImZpbGw6I2ZmZiIvPjwvc3ZnPg==`
+};

+ 97 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/paintbrush.ts

@@ -0,0 +1,97 @@
+import { IToolParameters, IToolData, IToolType } from '../textureEditorComponent';
+import { PointerEventTypes, PointerInfo } from 'babylonjs/Events/pointerEvents';
+import { TextBlock } from 'babylonjs-gui/2D/controls/textBlock';
+import { Slider } from 'babylonjs-gui/2D/controls/sliders/slider';
+
+export const Paintbrush : IToolData = {
+    name: 'Paintbrush',
+    type: class implements IToolType {
+        getParameters: () => IToolParameters;
+        pointerObservable: any;
+        isPainting: boolean;
+        GUI: {
+            radiusLabel : TextBlock;
+            radiusSlider : Slider;
+        };
+        radius = 15;
+
+        constructor(getParameters: () => IToolParameters) {
+            this.getParameters = getParameters;
+        }
+
+        paint(pointerInfo : PointerInfo) {
+            const {canvas2D, getMouseCoordinates, metadata, updateTexture } = this.getParameters();
+            const ctx = canvas2D.getContext('2d')!;
+            const {x, y} = getMouseCoordinates(pointerInfo);
+            ctx.globalCompositeOperation = 'source-over';
+            ctx.strokeStyle = metadata.color;
+            ctx.globalAlpha = metadata.opacity;
+            ctx.lineWidth = this.radius;
+            ctx.lineCap = 'round';
+            ctx.lineJoin = 'round';
+            ctx.lineTo(x, y);
+            ctx.stroke();
+            updateTexture();
+        }
+        
+        setup () {
+            const {scene, getMouseCoordinates, GUI, canvas2D} = this.getParameters();
+            const radiusLabel = new TextBlock();
+            radiusLabel.text = `Brush Width: ${this.radius}`;
+            radiusLabel.color = 'white';
+            radiusLabel.height = '20px';
+            radiusLabel.style = GUI.style;
+            GUI.toolWindow.addControl(radiusLabel);
+            const radiusSlider = new Slider();
+            radiusSlider.height = '20px';
+            radiusSlider.value = this.radius;
+            radiusSlider.minimum = 1;
+            radiusSlider.maximum = 100;
+            radiusSlider.step = 1;
+            radiusSlider.isThumbCircle = true;
+            radiusSlider.background = '#a3a3a3';
+            radiusSlider.color = '#33648f';
+            radiusSlider.borderColor = '#33648f';
+            radiusSlider.onValueChangedObservable.add(value => {
+                this.radius = value;
+                this.GUI.radiusLabel.text = `Brush Width: ${this.radius}`;
+            });
+            GUI.toolWindow.addControl(radiusSlider);
+            this.GUI = {radiusLabel, radiusSlider};
+
+            this.pointerObservable = scene.onPointerObservable.add((pointerInfo) => {
+                if (pointerInfo.pickInfo?.hit) {
+                    if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
+                        if (pointerInfo.event.button == 0) {
+                            this.isPainting = true;
+                            const {x, y} = getMouseCoordinates(pointerInfo);
+                            const ctx = canvas2D.getContext('2d')!;
+                            ctx.beginPath();
+                            ctx.moveTo(x, y);
+                          }
+                    }
+                    if (pointerInfo.type === PointerEventTypes.POINTERMOVE && this.isPainting) {
+                        this.paint(pointerInfo);
+                    }
+                }
+                if (pointerInfo.type === PointerEventTypes.POINTERUP) {
+                    if (pointerInfo.event.button == 0) {
+                        this.isPainting = false;
+                      }
+                }
+            });
+            this.isPainting = false;
+        }
+        cleanup () {
+            Object.entries(this.GUI).forEach(([key, value]) => value.dispose());
+            this.isPainting = false;
+            if (this.pointerObservable) {
+                this.getParameters().scene.onPointerObservable.remove(this.pointerObservable);
+            }
+        }
+    },
+    usesWindow: true,
+    icon: `PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHN0eWxlPSJmaWxsOm5vbmUiLz48cGF0aCBkPSJNMjksMTFhMy41
+    NywzLjU3LDAsMCwxLDAsNS4wNkwxNywyOGEyLjM0LDIuMzQsMCwwLDEtMSwuNThMMTAuOTEsMzBhLjc1Ljc1LDAsMCwxLS45Mi0uOTJMMTEuMzgsMjRBMi4zNCwyLjM0LDAsMCwxLDEyLDIzbDEyLTEyQTMuNTcsMy41NywwLDAsMSwyOSwxMVpNMjMsMTQuMSwxMywy
+    NGEuNjkuNjksMCwwLDAtLjE5LjMzbC0xLjA1LDMuODUsMy44NS0xQS42OS42OSwwLDAsMCwxNiwyN0wyNS45LDE3Wm0yLTItMSwxTDI3LDE2bDEtMUEyLjA4LDIuMDgsMCwxLDAsMjUsMTIuMDdaIiBzdHlsZT0iZmlsbDojZmZmIi8+PC9zdmc+`
+};

Файловите разлики са ограничени, защото са твърде много
+ 10 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/rectangleSelect.ts


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

@@ -1,144 +1,144 @@
-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;
-    resizeTexture(width: number, height: number) : void;
-    uploadTexture(file : File) : void;
-}
-
-interface PropertiesBarState {
-    width: number;
-    height: number;
-}
-
-interface PixelDataProps {
-    name : string;
-    data?: number;
-}
-
-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
-]
-
-function PixelData(props : PixelDataProps) {
-    return <span className='pixel-data'>{props.name}: <span className='value'>{props.data || '-'}</span></span>
-}
-
-function getNewDimension(oldDim : number, newDim : any) {
-    if (!isNaN(newDim)) {
-        if (parseInt(newDim) > 0) {
-            if (Number.isInteger(parseInt(newDim)))
-                return parseInt(newDim);
-        }
-    }
-    return oldDim;
-}
-
-export class PropertiesBar extends React.Component<PropertiesBarProps,PropertiesBarState> {
-    constructor(props : PropertiesBarProps) {
-        super(props);
-
-        this.state = {
-            width: props.texture.getSize().width,
-            height: props.texture.getSize().height
-        }
-    }
-    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' value={this.state.width} onChange={(evt) => this.setState({width: getNewDimension(this.state.width, evt.target.value)})}/>
-                        </label>
-                    <label className='dimensions'>
-                        H: <input type='text' value={this.state.height} onChange={(evt) => this.setState({height: getNewDimension(this.state.height, evt.target.value)})}/>
-                        </label>
-                    <img id='resize' className='icon button' title='Resize' alt='Resize' src={resizeButton} onClick={() => this.props.resizeTexture(this.state.width, this.state.height)}/>
-                </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()}/>
-                        <label>
-                            <input
-                                accept='.jpg, .png, .tga, .dds, .env'
-                                type='file'
-                                onChange={
-                                    (evt : React.ChangeEvent<HTMLInputElement>) => {
-                                        const files = evt.target.files;
-                                        if (files && files.length) {
-                                            this.props.uploadTexture(files[0]);
-                                        }
-                                
-                                        evt.target.value = "";
-                                    }
-                                }
-                            />
-                            <img
-                                title='Upload'
-                                className='icon button'
-                                src={uploadButton}
-                            />
-                        </label>
-                        <img title='Save' className='icon button' src={saveButton} onClick={() => this.props.saveTexture()}/>
-                    </div>
-                </div>
-        </div>;
-    }
+import * as React from 'react';
+import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
+import { IPixelData } from './textureCanvasManager';
+
+interface IPropertiesBarProps {
+    texture: BaseTexture;
+    saveTexture(): void;
+    pixelData: IPixelData;
+    face: number;
+    setFace(face : number): void;
+    resetTexture() : void;
+    resizeTexture(width: number, height: number) : void;
+    uploadTexture(file : File) : void;
+}
+
+interface IPropertiesBarState {
+    width: number;
+    height: number;
+}
+
+interface IPixelDataProps {
+    name : string;
+    data?: number;
+}
+
+export class PropertiesBar extends React.Component<IPropertiesBarProps,IPropertiesBarState> {
+    private _resetButton = require('./assets/reset.svg');
+    private _uploadButton = require('./assets/upload.svg');
+    private _saveButton = require('./assets/save.svg');
+    private _babylonLogo = require('./assets/babylonLogo.svg');
+    
+    private _resizeButton = require('./assets/resizeTool.svg');
+    
+    private _mipUp = require('./assets/mipUp.svg');
+    private _mipDown = require('./assets/mipDown.svg');
+    
+
+    private _faces = [
+        require('./assets/posX.svg'),
+        require('./assets/posY.svg'),
+        require('./assets/posZ.svg'),
+        require('./assets/negX.svg'),
+        require('./assets/negY.svg'),
+        require('./assets/negZ.svg')
+    ]
+
+    constructor(props : IPropertiesBarProps) {
+        super(props);
+
+        this.state = {
+            width: props.texture.getSize().width,
+            height: props.texture.getSize().height
+        }
+    }
+
+    private pixelData(props: IPixelDataProps) {
+        return <span className='pixel-data'>{props.name}: <span className='value'>{props.data || '-'}</span></span>;
+    }
+
+    private getNewDimension(oldDim : number, newDim : any) {
+        if (!isNaN(newDim)) {
+            if (parseInt(newDim) > 0) {
+                if (Number.isInteger(parseInt(newDim)))
+                    return parseInt(newDim);
+            }
+        }
+        return oldDim;
+    }
+
+    render() {
+        return <div id='properties'>
+                <div className='tab' id='logo-tab'>
+                    <img className='icon' src={this._babylonLogo}/>
+                </div>
+                <div className='tab' id='dimensions-tab'>
+                    <form onSubmit={evt => {
+                        this.props.resizeTexture(this.state.width, this.state.height);
+                        evt.preventDefault();
+                    }}>
+                        <label className='dimensions'>
+                            W: <input type='text' value={this.state.width} onChange={(evt) => this.setState({width: this.getNewDimension(this.state.width, evt.target.value)})}/>
+                            </label>
+                        <label className='dimensions'>
+                            H: <input type='text' value={this.state.height} onChange={(evt) => this.setState({height: this.getNewDimension(this.state.height, evt.target.value)})}/>
+                            </label>
+                        <img id='resize' className='icon button' title='Resize' alt='Resize' src={this._resizeButton} onClick={() => this.props.resizeTexture(this.state.width, this.state.height)}/> 
+                    </form>
+                </div>
+                <div className='tab' id='pixel-coords-tab'>
+                    <this.pixelData name='X' data={this.props.pixelData.x}/>
+                    <this.pixelData name='Y' data={this.props.pixelData.y}/>
+                </div>
+                <div className='tab' id='pixel-color-tab'>
+                    <this.pixelData name='R' data={this.props.pixelData.r}/>
+                    <this.pixelData name='G' data={this.props.pixelData.g}/>
+                    <this.pixelData name='B' data={this.props.pixelData.b}/>
+                    <this.pixelData name='A' data={this.props.pixelData.a}/>
+                </div>
+                {this.props.texture.isCube &&
+                <>
+                    <div className='tab' id='face-tab'>
+                        {this._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={this._mipUp} />
+                        <img title='Mip Preview Down' className='icon button' src={this._mipDown} />
+                    </div>
+                </>}
+                <div className='tab' id='right-tab'>
+                    <div className='content'>
+                        <img title='Reset' className='icon button' src={this._resetButton} onClick={() => this.props.resetTexture()}/>
+                        <label>
+                            <input
+                                accept='.jpg, .png, .tga, .dds, .env'
+                                type='file'
+                                onChange={
+                                    (evt : React.ChangeEvent<HTMLInputElement>) => {
+                                        const files = evt.target.files;
+                                        if (files && files.length) {
+                                            this.props.uploadTexture(files[0]);
+                                        }
+                                
+                                        evt.target.value = "";
+                                    }
+                                }
+                            />
+                            <img
+                                title='Upload'
+                                className='icon button'
+                                src={this._uploadButton}
+                            />
+                        </label>
+                        <img title='Save' className='icon button' src={this._saveButton} onClick={() => this.props.saveTexture()}/>
+                    </div>
+                </div>
+        </div>;
+    }
 }

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

@@ -1,23 +1,23 @@
 import * as React from 'react';
 import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
 
-interface TextureCanvasComponentProps {
+interface ITextureCanvasComponentProps {
     canvasUI : React.RefObject<HTMLCanvasElement>;
     canvas2D : React.RefObject<HTMLCanvasElement>;
-    canvasDisplay : React.RefObject<HTMLCanvasElement>;
+    canvas3D : React.RefObject<HTMLCanvasElement>;
     texture : BaseTexture;
 }
 
-export class TextureCanvasComponent extends React.Component<TextureCanvasComponentProps> {
-    shouldComponentUpdate(nextProps : TextureCanvasComponentProps) {
+export class TextureCanvasComponent extends React.Component<ITextureCanvasComponentProps> {
+    shouldComponentUpdate(nextProps : ITextureCanvasComponentProps) {
         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>
+            <canvas id="canvas-3D" ref={this.props.canvas3D} width={this.props.texture.getSize().width} height={this.props.texture.getSize().height} hidden={true}></canvas>
         </div>
     }
 }

+ 293 - 107
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasManager.ts

@@ -1,7 +1,7 @@
 import { Engine } from 'babylonjs/Engines/engine';
 import { Scene } from 'babylonjs/scene';
 import { Vector3, Vector2 } from 'babylonjs/Maths/math.vector';
-import { Color4 } from 'babylonjs/Maths/math.color';
+import { Color4, Color3 } from 'babylonjs/Maths/math.color';
 import { FreeCamera } from 'babylonjs/Cameras/freeCamera';
 import { Nullable } from 'babylonjs/types'
 
@@ -13,11 +13,10 @@ import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
 import { HtmlElementTexture } from 'babylonjs/Materials/Textures/htmlElementTexture';
 import { InternalTexture } from 'babylonjs/Materials/Textures/internalTexture';
 import { Texture } from 'babylonjs/Materials/Textures/texture';
-import { NodeMaterial } from 'babylonjs/Materials/Node/nodeMaterial';
-import { PBRMaterial } from 'babylonjs/Materials/PBR/pbrMaterial';
 import { RawCubeTexture } from 'babylonjs/Materials/Textures/rawCubeTexture';
 import { CubeTexture } from 'babylonjs/Materials/Textures/cubeTexture';
-
+import { ShaderMaterial } from 'babylonjs/Materials/shaderMaterial';
+import { StandardMaterial } from 'babylonjs/Materials/standardMaterial';
 
 import { ISize } from 'babylonjs/Maths/math.size';
 import { Tools } from 'babylonjs/Misc/tools';
@@ -25,12 +24,19 @@ import { Tools } from 'babylonjs/Misc/tools';
 import { PointerEventTypes, PointerInfo } from 'babylonjs/Events/pointerEvents';
 import { KeyboardEventTypes } from 'babylonjs/Events/keyboardEvents';
 
-import { TextureHelper, TextureChannelsToDisplay } from '../../../../../../textureHelper';
+import { TextureHelper } from '../../../../../../textureHelper';
+
+import { ITool } from './toolBar';
+import { IChannel } from './channelsBar';
+import { TextBlock } from 'babylonjs-gui/2D/controls/textBlock';
+import { Rectangle } from 'babylonjs-gui/2D/controls/rectangle';
+import { StackPanel } from 'babylonjs-gui/2D/controls/stackPanel';
+import { Control } from 'babylonjs-gui/2D/controls/control';
+import { Style } from 'babylonjs-gui/2D/style';
+import { AdvancedDynamicTexture } from 'babylonjs-gui/2D/advancedDynamicTexture';
 
-import { Tool } from './toolBar';
-import { Channel } from './channelsBar';
 
-export interface PixelData {
+export interface IPixelData {
     x? : number;
     y? : number;
     r? : number;
@@ -39,6 +45,14 @@ export interface PixelData {
     a? : number;
 }
 
+export interface IToolGUI {
+    adt: AdvancedDynamicTexture;
+    toolWindow: StackPanel;
+    isDragging: boolean;
+    dragCoords: Nullable<Vector2>;
+    style: Style;
+}
+
 export class TextureCanvasManager {
     private _engine: Engine;
     private _scene: Scene;
@@ -55,12 +69,18 @@ export class TextureCanvasManager {
 
     /* The canvas we paint onto using the canvas API */
     private _2DCanvas : HTMLCanvasElement;
+    /* The canvas we apply post processes to */
+    private _3DCanvas : HTMLCanvasElement;
+    /* The canvas which handles channel filtering */
+    private _channelsTexture : HtmlElementTexture;
 
-    private _displayCanvas : HTMLCanvasElement;
-    private _channels : Channel[] = [];
+    private _3DEngine : Engine;
+    private _3DPlane : Mesh;
+    private _3DCanvasTexture : HtmlElementTexture;
+    private _3DScene : Scene;
+
+    private _channels : IChannel[] = [];
     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;
@@ -72,8 +92,7 @@ export class TextureCanvasManager {
     private _didEdit : boolean = false;
 
     private _plane : Mesh;
-    private _planeMaterial : NodeMaterial;
-    private _planeFallbackMaterial : PBRMaterial;
+    private _planeMaterial : ShaderMaterial;
 
     /* Tracks which keys are currently pressed */
     private _keyMap : any = {};
@@ -84,31 +103,36 @@ export class TextureCanvasManager {
     private static ZOOM_OUT_KEY : string = '-';
 
     private static PAN_SPEED : number = 0.002;
-    private static PAN_MOUSE_BUTTON : number = 0; // LMB
-    private static PAN_KEY : string = ' ';
+    private static PAN_MOUSE_BUTTON : number = 1; // MMB
 
     private static MIN_SCALE : number = 0.01;
     private static MAX_SCALE : number = 10;
 
-    private _tool : Nullable<Tool>;
+    private _tool : Nullable<ITool>;
+
+    private _setPixelData : (pixelData : IPixelData) => void;
+
+    private _GUI : IToolGUI;
 
-    private _setPixelData : (pixelData : PixelData) => void;
+    private _window : Window;
 
-    public metadata : any = {
-        color: '#ffffff',
-        opacity: 1.0
-    };
+    public metadata : any = {};
+
+    private _editing3D : boolean = false;
 
     public constructor(
         texture: BaseTexture,
+        window: Window,
         canvasUI: HTMLCanvasElement,
         canvas2D: HTMLCanvasElement,
-        canvasDisplay: HTMLCanvasElement,
-        setPixelData : (pixelData : PixelData) => void
+        canvas3D: HTMLCanvasElement,
+        setPixelData: (pixelData : IPixelData) => void
     ) {
+        this._window = window;
+
         this._UICanvas = canvasUI;
         this._2DCanvas = canvas2D;
-        this._displayCanvas = canvasDisplay;
+        this._3DCanvas = canvas3D;
         this._setPixelData = setPixelData;
 
         this._size = texture.getSize();
@@ -118,34 +142,178 @@ export class TextureCanvasManager {
         this._scene = new Scene(this._engine);
         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 = new FreeCamera('camera', new Vector3(0, 0, -1), this._scene);
         this._camera.mode = Camera.ORTHOGRAPHIC_CAMERA;
 
-        this._planeFallbackMaterial = new PBRMaterial('fallback_material', this._scene);
-        this._planeFallbackMaterial.albedoTexture = this._displayTexture;
-        this._planeFallbackMaterial.disableLighting = true;
-        this._planeFallbackMaterial.unlit = true;
+        this._channelsTexture = new HtmlElementTexture('ct', this._2DCanvas, {engine: this._engine, scene: null, samplingMode: Engine.TEXTURE_NEAREST_LINEAR});
+
+        this._3DEngine = new Engine(this._3DCanvas);
+        this._3DScene = new Scene(this._3DEngine);
+        this._3DScene.clearColor = new Color4(0,0,0,0);
+        this._3DCanvasTexture = new HtmlElementTexture('canvas', this._2DCanvas, {engine: this._3DEngine, scene: this._3DScene});
+        this._3DCanvasTexture.hasAlpha = true;
+        const cam = new FreeCamera('camera', new Vector3(0,0,-1), this._3DScene);
+        cam.mode = Camera.ORTHOGRAPHIC_CAMERA;
+        [cam.orthoBottom, cam.orthoLeft, cam.orthoTop, cam.orthoRight] = [-0.5, -0.5, 0.5, 0.5];
+        this._3DPlane = PlaneBuilder.CreatePlane('texture', {width: 1, height: 1}, this._3DScene);
+        const mat = new StandardMaterial('material', this._3DScene);
+        mat.diffuseTexture = this._3DCanvasTexture;
+        mat.disableLighting = true;
+        mat.emissiveColor = Color3.White();
+        this._3DPlane.material = mat;
+        
 
-        this._displayTexture = new HtmlElementTexture("display", this._displayCanvas, {engine: this._engine, scene: this._scene});
-        this._displayTexture.updateSamplingMode(Engine.TEXTURE_NEAREST_LINEAR);
         this.grabOriginalTexture();
 
-        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._planeMaterial = new ShaderMaterial(
+            'shader',
+            this._scene,
+            {
+                vertexSource: `
+                    precision highp float;
+
+                    attribute vec3 position;
+                    attribute vec2 uv;
+
+                    uniform mat4 worldViewProjection;
+
+                    varying vec2 vUV;
+
+                    void main(void) {
+                        gl_Position = worldViewProjection * vec4(position, 1.0);
+                        vUV = uv;
+                    }
+                `,
+                fragmentSource: `
+                    precision highp float;
+            
+                    uniform sampler2D textureSampler;
+            
+                    uniform bool r;
+                    uniform bool g;
+                    uniform bool b;
+                    uniform bool a;
+            
+                    varying vec2 vUV;
+            
+                    void main(void) {
+                        float size = 20.0;
+                        vec2 pos2 = vec2(gl_FragCoord.x, gl_FragCoord.y);
+                        vec2 pos = floor(pos2 * 0.05);
+                        float pattern = mod(pos.x + pos.y, 2.0); 
+                        if (pattern == 0.0) {
+                            pattern = 0.7;
+                        }
+                        vec4 bg = vec4(pattern, pattern, pattern, 1.0);
+                        vec4 col = texture(textureSampler, vUV);
+                        if (!r && !g && !b) {
+                            if (a) {
+                                col = vec4(col.a, col.a, col.a, 1.0);
+                            } else {
+                                col = vec4(0.0,0.0,0.0,0.0);
+                            }
+                        } else {
+                            if (!r) {
+                                col.r = 0.0;
+                                if (!b) {
+                                    col.r = col.g;
+                                }
+                                else if (!g) {
+                                    col.r = col.b;
+                                }
+                            }
+                            if (!g) {
+                                col.g = 0.0;
+                                if (!b) {
+                                    col.g = col.r;
+                                }
+                                else if (!r) {
+                                    col.g = col.b;
+                                }
+                            }
+                            if (!b) {
+                                col.b = 0.0;
+                                if (!r) {
+                                    col.b = col.g;
+                                } else if (!g) {
+                                    col.b = col.r;
+                                }
+                            }
+                            if (!a) {
+                                col.a = 1.0;
+                            }
+                        }
+                        gl_FragColor = col;
+                        gl_FragColor = col * (col.a) + bg * (1.0 - col.a);
+                    }`
+            },
+        {
+            attributes: ['position', 'uv'],
+            uniforms: ['worldViewProjection', 'textureSampler', 'r', 'g', 'b', 'a']
+        });
+
+        this._planeMaterial.setTexture('textureSampler', this._channelsTexture);
+        this._planeMaterial.setFloat('r', 1.0);
+        this._planeMaterial.setFloat('g', 1.0);
+        this._planeMaterial.setFloat('b', 1.0);
+        this._planeMaterial.setFloat('a', 1.0);
+        this._plane.material = this._planeMaterial;
+        
+        const adt = AdvancedDynamicTexture.CreateFullscreenUI('gui', true, this._scene);
+        const style = adt.createStyle();
+        style.fontFamily = 'acumin-pro-condensed';
+        style.fontSize = '15px';
+
+        const toolWindow = new StackPanel();
+        toolWindow.background = '#333333';
+        toolWindow.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT;
+        toolWindow.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
+        toolWindow.left = '0px';
+        toolWindow.top = '-30px';
+        toolWindow.width = '200px';
+        toolWindow.isVisible = false;
+        toolWindow.isPointerBlocker = true;
+        adt.addControl(toolWindow);
+
+        this._GUI = {adt, style, toolWindow, isDragging: false, dragCoords: null};
+
+        const topBar = new Rectangle();
+        topBar.width = '100%';
+        topBar.height = '20px';
+        topBar.background = '#666666';
+        topBar.thickness = 0;
+        topBar.hoverCursor = 'grab';
+        topBar.onPointerDownObservable.add(evt => {this._GUI.isDragging = true; topBar.hoverCursor = 'grabbing';});
+        topBar.onPointerUpObservable.add(() => {this._GUI.isDragging = false; this._GUI.dragCoords = null; topBar.hoverCursor = 'grab';});
+
+        const title = new TextBlock();
+        title.text = 'Tool Settings';
+        title.color = 'white';
+        title.height = '20px';
+        title.style = this._GUI.style;
+        topBar.addControl(title);
+        this._GUI.toolWindow.addControl(topBar);
+
+        this._window.addEventListener('pointermove',  (evt : PointerEvent) => {
+            if (!this._GUI.isDragging) return;
+            if (!this._GUI.dragCoords) {
+                this._GUI.dragCoords = new Vector2(evt.x, evt.y);
+                return;
+            }
+            let x = parseInt(this._GUI.toolWindow.left.toString().replace('px', ''));
+            let y = parseInt(this._GUI.toolWindow.top.toString().replace('px', ''));
+            x += evt.x - this._GUI.dragCoords.x;
+            y += evt.y - this._GUI.dragCoords.y;
+            this._GUI.toolWindow.left = `${x}px`;
+            this._GUI.toolWindow.top = `${y}px`;
+            this._GUI.dragCoords.x = evt.x;
+            this._GUI.dragCoords.y = evt.y;
+        });
 
         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.5;
@@ -167,7 +335,7 @@ export class TextureCanvasManager {
                     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._isPanning = true;
                         this._mouseX = pointerInfo.event.x;
                         this._mouseY = pointerInfo.event.y;
@@ -211,67 +379,47 @@ export class TextureCanvasManager {
                     break;
                 case KeyboardEventTypes.KEYUP:
                     this._keyMap[kbInfo.event.key] = false;
-                    if (kbInfo.event.key == TextureCanvasManager.PAN_KEY) {
-                        this._isPanning = false;
-                    }
                 break;
             }
-        })
-
-        this._scene.debugLayer.show();
-
+        });
     }
 
     public async updateTexture() {
         this._didEdit = true;
+        const element = this._editing3D ? this._3DCanvas : this._2DCanvas;
+        if (this._editing3D) {
+            this._3DCanvasTexture.update();
+            this._3DScene.render();
+        }
         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,
+                    element,
                     {
                         engine: this._originalTexture.getScene()?.getEngine()!,
                         scene: null,
                         samplingMode: (this._originalTexture as Texture).samplingMode
                     }
                 );
+            } else {
+                (this._target as HtmlElementTexture).element = element;
             }
             (this._target as HtmlElementTexture).update((this._originalTexture as Texture).invertY);
         }
         this._originalTexture._texture = this._target._texture;
-        this.copyTextureToDisplayTexture();
+        this._channelsTexture.element = element;
+        this.updateDisplay();
     }
 
-    private async copyTextureToDisplayTexture() {
-        let channelsToDisplay : TextureChannelsToDisplay = {
-            R: true,
-            G: true,
-            B: true,
-            A: true
-        }
-        this._channels.forEach(channel => channelsToDisplay[channel.id] = channel.visible);
-        const pixels = await TextureHelper.GetTextureDataAsync(this._originalTexture, this._size.width, this._size.height, this._face, channelsToDisplay);
-        TextureCanvasManager.paintPixelsOnCanvas(pixels, this._displayCanvas);
-        this._displayTexture.update();
+    private updateDisplay() {
+        this._3DScene.render()
+        this._channelsTexture.update();
     }
 
-    public set channels(channels: Channel[]) {
+    public set channels(channels: IChannel[]) {
         // 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) {
@@ -282,13 +430,14 @@ export class TextureCanvasManager {
                 (channel,index) => {
                     if (channel.visible !== this._channels[index].visible) {
                         needsRender = true;
+                        this._planeMaterial.setFloat(channel.id.toLowerCase(), channel.visible ? 1.0 : 0.0);
                     }
                 }
             );
         }
         this._channels = channels;
         if (needsRender) {
-            this.copyTextureToDisplayTexture();
+            this.updateDisplay();
         }
     }
 
@@ -299,10 +448,9 @@ export class TextureCanvasManager {
         ctx.putImageData(imgData, 0, 0);
     }
 
-    public grabOriginalTexture() {
+    public grabOriginalTexture(adjustZoom = true) {
         // Grab image data from original texture and paint it onto the context of a DynamicTexture
-        this._size = this._originalTexture.getSize();
-        this.updateSize();
+        this.setSize(this._originalTexture.getSize(), adjustZoom);
         TextureHelper.GetTextureDataAsync(
             this._originalTexture,
             this._size.width,
@@ -311,11 +459,12 @@ export class TextureCanvasManager {
             {R:true ,G:true ,B:true ,A:true}
         ).then(data => {
             TextureCanvasManager.paintPixelsOnCanvas(data, this._2DCanvas);
-            this.copyTextureToDisplayTexture();
+            this._3DCanvasTexture.update();
+            this.updateDisplay();
         })
     }
 
-    public getMouseCoordinates(pointerInfo : PointerInfo) : Vector2 {
+    public getMouseCoordinates(pointerInfo : PointerInfo) {
         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);
@@ -325,39 +474,64 @@ export class TextureCanvasManager {
         }
     }
 
-    public get scene() : Scene {
+    public get scene() {
         return this._scene;
     }
 
-    public get canvas2D() : HTMLCanvasElement {
+    public get canvas2D() {
         return this._2DCanvas;
     }
 
-    public get size() : ISize {
+    public get size() {
         return this._size;
     }
 
-    public set tool(tool: Nullable<Tool>) {
+    public set tool(tool: Nullable<ITool>) {
         if (this._tool) {
             this._tool.instance.cleanup();
         }
         this._tool = tool;
         if (this._tool) {
             this._tool.instance.setup();
+            if (this._tool.usesWindow) {
+                this._GUI.toolWindow.isVisible = true;
+            } else {
+                this._GUI.toolWindow.isVisible = false;
+            }
+            if (this._editing3D && !this._tool.is3D) {
+                this._editing3D = false;
+                this._2DCanvas.getContext('2d')?.drawImage(this._3DCanvas, 0, 0);
+            }
+            else if (!this._editing3D && this._tool.is3D) {
+                this._editing3D = true;
+                this.updateTexture();
+            }
         }
     }
 
-    public get tool(): Nullable<Tool> {
+    public get tool() {
         return this._tool;
     }
 
+    // BROKEN : FIX THIS
     public set face(face: number) {
         if (this._face !== face) {
             this._face = face;
-            this.copyTextureToDisplayTexture();
+            this.grabOriginalTexture(false);
+            this.updateDisplay();
         }
     }
 
+    /** Returns the tool GUI object, allowing tools to access the GUI */
+    public get GUI() {
+        return this._GUI;
+    }
+
+    /** Returns the 3D scene used for postprocesses */
+    public get scene3D() {
+        return this._3DScene;
+    }
+
     private makePlane() {
         const textureRatio = this._size.width / this._size.height;
         if (this._plane) this._plane.dispose();
@@ -366,10 +540,13 @@ export class TextureCanvasManager {
         this._plane.edgesWidth = 4.0;
         this._plane.edgesColor = new Color4(1,1,1,1);
         this._plane.enablePointerMoveEvents = true;
-        if (this._planeMaterial) this._plane.material = this._planeMaterial; else this._plane.material = this._planeFallbackMaterial;
+        this._plane.material = this._planeMaterial;
     }
 
     public reset() : void {
+        if (this._tool && this._tool.instance.onReset) {
+            this._tool.instance.onReset();
+        }
         this._originalTexture._texture = this._originalInternalTexture;
         this.grabOriginalTexture();
         this.makePlane();
@@ -378,18 +555,23 @@ export class TextureCanvasManager {
 
     public async resize(newSize : ISize) {
         const data = await TextureHelper.GetTextureDataAsync(this._originalTexture, newSize.width, newSize.height, this._face, {R: true,G: true,B: true,A: true});
-        this._size = newSize;
-        this.updateSize();
+        this.setSize(newSize);
         TextureCanvasManager.paintPixelsOnCanvas(data, this._2DCanvas);
         this.updateTexture();
         this._didEdit = true;
     }
 
-    private updateSize() {
+    public setSize(size: ISize, adjustZoom = true) {
+        this._size = size;
         this._2DCanvas.width = this._size.width;
         this._2DCanvas.height = this._size.height;
-        this._displayCanvas.width = this._size.width;
-        this._displayCanvas.height = this._size.height;
+        this._3DCanvas.width = this._size.width;
+        this._3DCanvas.height = this._size.height;
+        if (adjustZoom) {
+            this._camera.position.x = 0;
+            this._camera.position.y = 0;
+            this._scale = 1.5 / (this._size.width/this._size.height);
+        }
         this.makePlane();
     }
 
@@ -429,14 +611,16 @@ export class TextureCanvasManager {
                         () => {
                             TextureHelper.GetTextureDataAsync(texture, texture.getSize().width, texture.getSize().height, 0, {R: true, G: true, B: true, A: true})
                                 .then((pixels) => {
-                                    this._size = texture.getSize();
-                                    this.updateSize();
+                                    if (this._tool && this._tool.instance.onReset) {
+                                        this._tool.instance.onReset();
+                                    }
+                                    this.setSize(texture.getSize());
                                     TextureCanvasManager.paintPixelsOnCanvas(pixels, this._2DCanvas);
                                     this.updateTexture();
                                     texture.dispose();
                                 });
-                        });
-                    
+                        }
+                    );
                 }
             };
 
@@ -444,17 +628,19 @@ export class TextureCanvasManager {
     }
 
     public dispose() {
-        if (this._planeMaterial) {
-            this._planeMaterial.dispose();
-        }
         if (this._didEdit) {
             this._originalInternalTexture?.dispose();
         }
         if (this._tool) {
             this._tool.instance.cleanup();
-        }
-        this._displayTexture.dispose();
+        }        
+        this._3DPlane.dispose();
+        this._3DCanvasTexture.dispose();
+        this._3DScene.dispose();
+        this._3DEngine.dispose();
         this._plane.dispose();
+        this._channelsTexture.dispose();
+        this._planeMaterial.dispose();
         this._camera.dispose();
         this._scene.dispose();
         this._engine.dispose();

+ 8 - 2
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditor.scss

@@ -3,6 +3,7 @@
     width: 100%;
     color: white;
     background-color: #1e1e1e;
+    font-family: 'acumin-pro-condensed';
 
     .icon {
         width: 40px;
@@ -13,11 +14,13 @@
                 background-color: #4a4a4a;
                 cursor: pointer;
             }
+            // When the button is selected - className='active'
             &.active {
                 background-color: #666666;
             }
+            // When the button is clicked
             &:active {
-                background-color: #888888;
+                background-color: #837c7c;
             }
         }
     }
@@ -66,6 +69,9 @@
             background-color: #333333;
         }
         #dimensions-tab {
+            form {
+                display: flex;
+            }
             label {
                 margin-left: 15px;
                 color: #afafaf;
@@ -146,7 +152,7 @@
             position: relative;
             #add-tool-popup {
                 background-color: #333333;
-                width: 340px;
+                width: 348px;
                 margin-left: 40px;
                 position: absolute;
                 top: 0px;

+ 83 - 35
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditorComponent.tsx

@@ -1,9 +1,9 @@
 import * as React from 'react';
 import { GlobalState } from '../../../../../globalState';
-import { TextureCanvasManager, PixelData } from './textureCanvasManager';
-import { Tool, ToolBar } from './toolBar';
+import { TextureCanvasManager, IPixelData, IToolGUI } from './textureCanvasManager';
+import { ITool, ToolBar } from './toolBar';
 import { PropertiesBar } from './propertiesBar';
-import { Channel, ChannelsBar } from './channelsBar';
+import { IChannel, ChannelsBar } from './channelsBar';
 import { BottomBar } from './bottomBar';
 import { TextureCanvasComponent } from './textureCanvasComponent';
 import defaultTools from './defaultTools/defaultTools';
@@ -12,52 +12,94 @@ import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
 import { Tools } from 'babylonjs/Misc/tools';
 import { Scene } from 'babylonjs/scene';
 import { ISize } from 'babylonjs/Maths/math.size';
+import { Vector2 } from 'babylonjs/Maths/math.vector';
+import { PointerInfo } from 'babylonjs/Events/pointerEvents';
+
+import { PopupComponent } from '../../../../../popupComponent';
 
 require('./textureEditor.scss');
 
-interface TextureEditorComponentProps {
+interface ITextureEditorComponentProps {
     globalState: GlobalState;
     texture: BaseTexture;
     url: string;
+    window: React.RefObject<PopupComponent>;
 }
 
-interface TextureEditorComponentState {
-    tools: Tool[];
+interface ITextureEditorComponentState {
+    tools: ITool[];
     activeToolIndex: number;
     metadata: any;
-    channels: Channel[];
-    pixelData : PixelData;
+    channels: IChannel[];
+    pixelData : IPixelData;
     face: number;
 }
 
-export interface ToolParameters {
+export interface IToolParameters {
+    /** The visible scene in the editor. Useful for adding pointer and keyboard events. */
     scene: Scene;
+    /** The 2D canvas which tools can paint on using the canvas API. */
     canvas2D: HTMLCanvasElement;
+    /** The 3D scene which tools can add post processes to. */
+    scene3D: Scene;
+    /** The size of the texture. */
     size: ISize;
+    /** Pushes the editor texture back to the original scene. This should be called every time a tool makes any modification to a texture. */
     updateTexture: () => void;
-    getMetadata: () => any;
+    /** The metadata object which is shared between all tools. Feel free to store any information here. Do not set this directly: instead call setMetadata. */
+    metadata: any;
+    /** Call this when you want to mutate the metadata. */
     setMetadata: (data : any) => void;
+    /** Returns the texture coordinates under the cursor */
+    getMouseCoordinates: (pointerInfo : PointerInfo) => Vector2;
+    /** An object which holds the GUI's ADT as well as the tool window. */
+    GUI: IToolGUI;
+    /** Provides access to the BABYLON namespace */
+    BABYLON: any;
 }
 
-export interface ToolData {
+
+/** An interface representing the definition of a tool */
+export interface IToolData {
+    /** Name to display on the toolbar */
     name: string;
-    type: any;
+    /** A class definition for the tool including setup and cleanup methods */
+    type: IToolConstructable;
+    /**  An SVG icon encoded in Base64 */
     icon: string;
+    /** Whether the tool uses the draggable GUI window */
+    usesWindow? : boolean;
+    /** Whether the tool uses postprocesses */
+    is3D? : boolean;
+}
+
+export interface IToolType {
+    /** Called when the tool is selected. */
+    setup: () => void;
+    /** Called when the tool is deselected. */
+    cleanup: () => void;
+    /** Optional. Called when the user resets the texture or uploads a new texture. Tools may want to reset their state when this happens. */
+    onReset?: () => void;
+}
+
+/** For constructable types, TS requires that you define a seperate interface which constructs your actual interface */
+interface IToolConstructable {
+    new (getParameters: () => IToolParameters) : IToolType;
 }
 
 declare global {
-    var _TOOL_DATA_ : ToolData;
+    var _TOOL_DATA_ : IToolData;
 }
 
-export class TextureEditorComponent extends React.Component<TextureEditorComponentProps, TextureEditorComponentState> {
+export class TextureEditorComponent extends React.Component<ITextureEditorComponentProps, ITextureEditorComponentState> {
     private _textureCanvasManager: TextureCanvasManager;
-    private canvasUI = React.createRef<HTMLCanvasElement>();
-    private canvas2D = React.createRef<HTMLCanvasElement>();
-    private canvasDisplay = React.createRef<HTMLCanvasElement>();
+    private _UICanvas = React.createRef<HTMLCanvasElement>();
+    private _2DCanvas = React.createRef<HTMLCanvasElement>();
+    private _3DCanvas = React.createRef<HTMLCanvasElement>();
 
-    constructor(props : TextureEditorComponentProps) {
+    constructor(props : ITextureEditorComponentProps) {
         super(props);
-        let channels : Channel[] = [
+        let channels : IChannel[] = [
             {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')},
@@ -92,16 +134,17 @@ export class TextureEditorComponent extends React.Component<TextureEditorCompone
     componentDidMount() {
         this._textureCanvasManager = new TextureCanvasManager(
             this.props.texture,
-            this.canvasUI.current!,
-            this.canvas2D.current!,
-            this.canvasDisplay.current!,
-            (data : PixelData) => {this.setState({pixelData: data})}
+            this.props.window.current!.getWindow()!,
+            this._UICanvas.current!,
+            this._2DCanvas.current!,
+            this._3DCanvas.current!,
+            (data : IPixelData) => {this.setState({pixelData: data})}
         );
         this.addTools(defaultTools);
     }
 
     componentDidUpdate() {
-        let channelsClone : Channel[] = [];
+        let channelsClone : IChannel[] = [];
         this.state.channels.forEach(channel => channelsClone.push({...channel}));
         this._textureCanvasManager.channels = channelsClone;
         this._textureCanvasManager.metadata = {...this.state.metadata};
@@ -118,12 +161,13 @@ export class TextureEditorComponent extends React.Component<TextureEditorCompone
         });
     }
     
-    addTools(tools : ToolData[]) {
-        let newTools : Tool[] = [];
+    addTools(tools : IToolData[]) {
+        let newTools : ITool[] = [];
         tools.forEach(toolData => {
-            const tool : Tool = {
+            const tool : ITool = {
                 ...toolData,
-                instance: new toolData.type(() => this.getToolParameters())};
+                instance: new toolData.type(() => this.getToolParameters())
+            };
             newTools = newTools.concat(tool);
         });
         newTools = this.state.tools.concat(newTools);
@@ -131,14 +175,18 @@ export class TextureEditorComponent extends React.Component<TextureEditorCompone
         console.log(newTools);
     }
 
-    getToolParameters() : ToolParameters {
+    getToolParameters() : IToolParameters {
         return {
             scene: this._textureCanvasManager.scene,
             canvas2D: this._textureCanvasManager.canvas2D,
+            scene3D: this._textureCanvasManager.scene3D,
             size: this._textureCanvasManager.size,
             updateTexture: () => this._textureCanvasManager.updateTexture(),
-            getMetadata: () => this.state.metadata,
-            setMetadata: (data : any) => this.setMetadata(data)
+            metadata: this.state.metadata,
+            setMetadata: (data : any) => this.setMetadata(data),
+            getMouseCoordinates: (pointerInfo : PointerInfo) => this._textureCanvasManager.getMouseCoordinates(pointerInfo),
+            GUI: this._textureCanvasManager.GUI,
+            BABYLON: BABYLON,
         };
     }
 
@@ -164,7 +212,7 @@ export class TextureEditorComponent extends React.Component<TextureEditorCompone
     }
 
     saveTexture() {
-        Tools.ToBlob(this.canvas2D.current!, (blob) => {
+        Tools.ToBlob(this._2DCanvas.current!, (blob) => {
             Tools.Download(blob!, this.props.url);
         });
     }
@@ -193,16 +241,16 @@ export class TextureEditorComponent extends React.Component<TextureEditorCompone
                 resizeTexture={this.resizeTexture}
                 uploadTexture={this.uploadTexture}
             />
-            <ToolBar
+            {!this.props.texture.isCube && <ToolBar
                 tools={this.state.tools}
                 activeToolIndex={this.state.activeToolIndex}
                 addTool={this.loadToolFromURL}
                 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}/>
+            <TextureCanvasComponent canvas2D={this._2DCanvas} canvas3D={this._3DCanvas} canvasUI={this._UICanvas} texture={this.props.texture}/>
             <BottomBar name={this.props.url}/>
         </div>
     }

+ 100 - 101
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/toolBar.tsx

@@ -1,102 +1,101 @@
-import * as React from 'react';
-import { SketchPicker } from 'react-color';
-
-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;
-    pickerOpen : boolean;
-    addOpen : boolean;
-}
-
-const addTool = require('./assets/addTool.svg');
-
-export class ToolBar extends React.Component<ToolBarProps, ToolBarState> {
-    private pickerRef : React.RefObject<HTMLDivElement>;
-    constructor(props : ToolBarProps) {
-        super(props);
-        this.state = {
-            toolURL: "",
-            pickerOpen: false,
-            addOpen: false
-        };
-        this.pickerRef = React.createRef();
-    }
-
-    computeRGBAColor() {
-        const opacityInt = Math.floor(this.props.metadata.opacity * 255);
-        const opacityHex = opacityInt.toString(16).padStart(2, '0');
-        return `${this.props.metadata.color}${opacityHex}`;
-    }
-
-    render() {
-        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={evt => {
-                                if (evt.button === 0) {
-                                    this.props.changeTool(index)
-                                }
-                            }}
-                            key={index}
-                        />
-                    }
-                )}
-                <div id='add-tool'>
-                    <img src={addTool} className='icon button' title='Add Tool' alt='Add Tool' onClick={() => this.setState({addOpen: !this.state.addOpen})}/>
-                    { this.state.addOpen && 
-                    <div id='add-tool-popup'>
-                        <form onSubmit={event => {
-                            event.preventDefault();
-                            this.props.addTool(this.state.toolURL);
-                            this.setState({toolURL: '', addOpen: false})
-                        }}>
-                            <label>
-                                Enter tool URL: <input value={this.state.toolURL} onChange={evt => this.setState({toolURL: evt.target.value})} type='text'/>
-                            </label>
-                            <button>Add</button>
-                        </form>
-                    </div> }
-                </div>
-            </div>
-            <div id='color' onClick={() => this.setState({pickerOpen: !this.state.pickerOpen})} title='Color' className={`icon button${this.state.pickerOpen ? ` active` : ``}`}>
-                <div id='activeColor' style={{backgroundColor: this.props.metadata.color}}></div>
-            </div>
-            {
-                this.state.pickerOpen &&
-                <>
-                    <div className='color-picker-cover' onClick={evt => {
-                        if (evt.target !== this.pickerRef.current?.ownerDocument.querySelector('.color-picker-cover')) {
-                            return;
-                        }
-                        this.setState({pickerOpen: false});
-                    }}>
-                    </div>
-                    <div className='color-picker' ref={this.pickerRef}>
-                            <SketchPicker color={this.computeRGBAColor()}  onChange={color => this.props.setMetadata({color: color.hex, opacity: color.rgb.a})}/>
-                    </div>
-                </>
-            }
-        </div>;
-    }
+import * as React from 'react';
+import { SketchPicker } from 'react-color';
+import { IToolData, IToolType } from './textureEditorComponent';
+
+export interface ITool extends IToolData {
+    instance: IToolType;
+}
+
+interface IToolBarProps {
+    tools: ITool[];
+    addTool(url: string): void;
+    changeTool(toolIndex : number): void;
+    activeToolIndex : number;
+    metadata: any;
+    setMetadata(data : any): void;
+}
+
+interface IToolBarState {
+    toolURL : string;
+    pickerOpen : boolean;
+    addOpen : boolean;
+}
+
+
+export class ToolBar extends React.Component<IToolBarProps, IToolBarState> {
+    private _addTool = require('./assets/addTool.svg');
+
+    private _pickerRef : React.RefObject<HTMLDivElement>;
+    constructor(props : IToolBarProps) {
+        super(props);
+        this.state = {
+            toolURL: "",
+            pickerOpen: false,
+            addOpen: false
+        };
+        this._pickerRef = React.createRef();
+    }
+
+    computeRGBAColor() {
+        const opacityInt = Math.floor(this.props.metadata.opacity * 255);
+        const opacityHex = opacityInt.toString(16).padStart(2, '0');
+        return `${this.props.metadata.color}${opacityHex}`;
+    }
+
+    render() {
+        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={evt => {
+                                if (evt.button === 0) {
+                                    this.props.changeTool(index)
+                                }
+                            }}
+                            key={index}
+                        />
+                    }
+                )}
+                <div id='add-tool'>
+                    <img src={this._addTool} className='icon button' title='Add Tool' alt='Add Tool' onClick={() => this.setState({addOpen: !this.state.addOpen})}/>
+                    { this.state.addOpen && 
+                    <div id='add-tool-popup'>
+                        <form onSubmit={event => {
+                            event.preventDefault();
+                            this.props.addTool(this.state.toolURL);
+                            this.setState({toolURL: '', addOpen: false})
+                        }}>
+                            <label>
+                                Enter tool URL: <input value={this.state.toolURL} onChange={evt => this.setState({toolURL: evt.target.value})} type='text'/>
+                            </label>
+                            <button>Add</button>
+                        </form>
+                    </div> }
+                </div>
+            </div>
+            <div id='color' onClick={() => this.setState({pickerOpen: !this.state.pickerOpen})} title='Color' className={`icon button${this.state.pickerOpen ? ` active` : ``}`}>
+                <div id='activeColor' style={{backgroundColor: this.props.metadata.color}}></div>
+            </div>
+            {
+                this.state.pickerOpen &&
+                <>
+                    <div className='color-picker-cover' onClick={evt => {
+                        if (evt.target !== this._pickerRef.current?.ownerDocument.querySelector('.color-picker-cover')) {
+                            return;
+                        }
+                        this.setState({pickerOpen: false});
+                    }}>
+                    </div>
+                    <div className='color-picker' ref={this._pickerRef}>
+                            <SketchPicker color={this.computeRGBAColor()}  onChange={color => this.props.setMetadata({color: color.hex, opacity: color.rgb.a})}/>
+                    </div>
+                </>
+            }
+        </div>;
+    }
 }

+ 4 - 0
inspector/src/components/popupComponent.tsx

@@ -70,6 +70,10 @@ export class PopupComponent extends React.Component<IPopupComponentProps, { isCo
         }
     }
 
+    getWindow() {
+        return this._window;
+    }
+
     render() {
         if (!this.state.isComponentMounted || this._container === null) {
             return null;