Browse Source

Merge pull request #8803 from DarraghBurkeMS/master

Texture Inspector QOL Changes
David Catuhe 5 năm trước cách đây
mục cha
commit
4a5bf9559a

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

@@ -85,7 +85,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 using 5 tools. Supports region selection and individual channel editing. ([DarraghBurkeMS](https://github.com/DarraghBurkeMS))
+- View & edit textures in pop out inspector using tools such as brush and floodfill. Supports region selection, individual channel editing, and resizing. ([DarraghBurkeMS](https://github.com/DarraghBurkeMS))
 
 ### Cameras
 

+ 118 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/canvasShader.ts

@@ -0,0 +1,118 @@
+export const canvasShader = {
+    path: {
+        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;
+
+            uniform int x1;
+            uniform int y1;
+            uniform int x2;
+            uniform int y2;
+            uniform int w;
+            uniform int h;
+
+            uniform int time;
+            uniform bool showGrid;
+    
+            varying vec2 vUV;
+
+            float scl = 200.0;
+            float speed = 10.0 / 1000.0;
+            float smoothing = 0.2;
+    
+            void main(void) {
+                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 * (col.a) + bg * (1.0 - col.a);
+                float wF = float(w);
+                float hF = float(h);
+                int xPixel = int(floor(vUV.x * wF));
+                int yPixel = int(floor((1.0 - vUV.y) * hF));
+                int xDis = min(abs(xPixel - x1), abs(xPixel - x2));
+                int yDis = min(abs(yPixel - y1), abs(yPixel - y2));
+                if (showGrid) {
+                    vec2 frac = fract(vUV * vec2(wF,hF));
+                    float thickness = 0.1;
+                    if (abs(frac.x) < thickness || abs (frac.y) < thickness) {
+                        gl_FragColor = vec4(0.75,0.75,0.75,1.0);
+                    }
+                }
+                if (xPixel >= x1 && yPixel >= y1 && xPixel <= x2 && yPixel <= y2) {
+                    if (xDis <= 4 || yDis <= 4) {
+                        float c = sin(vUV.x * scl + vUV.y * scl + float(time) * speed);
+                        c = smoothstep(-smoothing,smoothing,c);
+                        float val = 1.0 - c;
+                        gl_FragColor = vec4(val, val, val, 1.0) * 0.7 + gl_FragColor * 0.3;
+                    }
+                }
+            }`
+    },
+    options: {
+        attributes: ['position', 'uv'],
+        uniforms: ['worldViewProjection', 'textureSampler', 'r', 'g', 'b', 'a', 'x1', 'y1', 'x2', 'y2', 'w', 'h', 'time', 'showGrid']
+    }
+}

+ 2 - 2
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/contrast.tsx

@@ -48,8 +48,8 @@ class contrastTool implements IToolType {
     cleanup() {
     }
     onReset() {
-        this.contrast = 0;
-        this.exposure = 0;
+        this.setExposure(0);
+        this.setContrast(0);
     }
 };
 

+ 1 - 1
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/floodfill.ts

@@ -26,7 +26,7 @@ export const Floodfill : IToolData = {
         setup () {
             this.pointerObserver = this.getParameters().scene.onPointerObservable.add((pointerInfo) => {
                 if (pointerInfo.pickInfo?.hit) {
-                    if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
+                    if (pointerInfo.type === PointerEventTypes.POINTERDOWN && pointerInfo.event.button === 0) {
                         this.fill();
                     }
                 }

+ 17 - 19
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/paintbrush.tsx

@@ -26,30 +26,28 @@ class paintbrushTool implements IToolType {
             x -= metadata.select.x1;
             y -= metadata.select.y1;
         }
+        const {ctx} = this;
+        let numSteps, stepVector;
+        stepVector = new Vector2();
         if (this.mousePos == null) {
             this.mousePos = new Vector2(x, y);
+            numSteps = 1;
+        } else {
+            const maxDistance = this.width / 4;
+            const diffVector = new Vector2(x - this.mousePos.x, y - this.mousePos.y);
+            numSteps = Math.ceil(diffVector.length() / maxDistance);
+            const trueDistance = diffVector.length() / numSteps;
+            stepVector = diffVector.normalize().multiplyByFloats(trueDistance, trueDistance);
         }
-        const {ctx} = this;
-        let xx = this.mousePos.x;
-        let yy = this.mousePos.y;
-        let stepCount = 0;
-        const distance = this.width / 4;
-        const step = new Vector2(x - this.mousePos.x, y - this.mousePos.y).normalize().multiplyByFloats(distance, distance);
-        const numSteps = new Vector2(x - this.mousePos.x, y - this.mousePos.y).length() / distance;
-        while(stepCount < numSteps) {
+        let paintVector = this.mousePos.clone();
+        for(let stepCount = 0; stepCount < numSteps; stepCount++) {
             ctx.globalAlpha = 1.0;
             ctx.globalCompositeOperation = 'destination-out';
-            ctx.drawImage(this.circleCanvas, Math.floor(xx - this.width / 2), Math.floor(yy - this.width / 2));
+            ctx.drawImage(this.circleCanvas, Math.ceil(paintVector.x - this.width / 2), Math.ceil(paintVector.y - this.width / 2));
             ctx.globalAlpha = metadata.alpha;
             ctx.globalCompositeOperation = 'source-over';
-            ctx.drawImage(this.circleCanvas, Math.floor(xx - this.width / 2), Math.floor(yy - this.width / 2));
-            xx += step.x;
-            yy += step.y;
-            stepCount++;
-            if (numSteps - stepCount < 1) {
-                xx = x;
-                yy = y;
-            }
+            ctx.drawImage(this.circleCanvas, Math.ceil(paintVector.x - this.width / 2), Math.ceil(paintVector.y - this.width / 2));
+            paintVector.addInPlace(stepVector);
         }
         updatePainting();
         this.mousePos = new Vector2(x,y);
@@ -80,8 +78,8 @@ class paintbrushTool implements IToolType {
                         const g = Math.floor(rgb.g * 255);
                         const b = Math.floor(rgb.b * 255);
                         let idx = 0;
-                        for(let y = -Math.ceil(this.width / 2); y < Math.floor(this.width / 2); y++) {
-                            for (let x = -Math.ceil(this.width / 2); x < Math.floor(this.width / 2); x++) {
+                        for(let y = -Math.floor(this.width / 2); y < Math.ceil(this.width / 2); y++) {
+                            for (let x = -Math.floor(this.width / 2); x < Math.ceil(this.width / 2); x++) {
                                 pixels[idx++] = r;
                                 pixels[idx++] = g;
                                 pixels[idx++] = b;

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

@@ -1,9 +1,11 @@
 import * as React from 'react';
 import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
 import { IPixelData } from './textureCanvasManager';
+import { ISize } from 'babylonjs/Maths/math.size';
 
 interface IPropertiesBarProps {
     texture: BaseTexture;
+    size: ISize;
     saveTexture(): void;
     pixelData: IPixelData;
     face: number;
@@ -21,7 +23,7 @@ interface IPropertiesBarState {
 }
 
 interface IPixelDataProps {
-    name : string;
+    name: string;
     data: number | undefined;
 }
 
@@ -50,8 +52,8 @@ export class PropertiesBar extends React.PureComponent<IPropertiesBarProps,IProp
         super(props);
 
         this.state = {
-            width: props.texture.getSize().width,
-            height: props.texture.getSize().height
+            width: props.size.width,
+            height: props.size.height
         }
     }
 
@@ -69,8 +71,18 @@ export class PropertiesBar extends React.PureComponent<IPropertiesBarProps,IProp
         return oldDim;
     }
 
+    componentWillUpdate(nextProps: IPropertiesBarProps) {
+        if (nextProps.size.width != this.props.size.width || nextProps.size.height != this.props.size.height) {
+            this.setState({
+                width: nextProps.size.width,
+                height: nextProps.size.height
+            })
+        }
+    }
+
     render() {
         const {mipLevel, setMipLevel, pixelData, resizeTexture, texture, face, setFace, saveTexture, resetTexture, uploadTexture} = this.props;
+        const maxLevels = 1 + Math.floor(Math.log2(Math.max(texture.getSize().width, texture.getSize().height)));
         return <div id='properties'>
                 <div className='tab' id='logo-tab'>
                     <img className='icon' src={this._babylonLogo}/>
@@ -114,7 +126,7 @@ export class PropertiesBar extends React.PureComponent<IPropertiesBarProps,IProp
                     {!texture.noMipmap &&
                         <div className='tab' id='mip-tab'>
                             <img title='Mip Preview Up' className='icon button' src={this._mipUp} onClick={() => mipLevel > 0 && setMipLevel(mipLevel - 1)} />
-                            <img title='Mip Preview Down' className='icon button' src={this._mipDown} onClick={() => mipLevel < 12 && setMipLevel(mipLevel + 1)} />
+                            <img title='Mip Preview Down' className='icon button' src={this._mipDown} onClick={() => mipLevel < maxLevels && setMipLevel(mipLevel + 1)} />
                         </div>
                     }
                 </div>

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

@@ -2,10 +2,10 @@ import * as React from 'react';
 import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
 
 interface ITextureCanvasComponentProps {
-    canvasUI : React.RefObject<HTMLCanvasElement>;
-    canvas2D : React.RefObject<HTMLCanvasElement>;
-    canvas3D : React.RefObject<HTMLCanvasElement>;
-    texture : BaseTexture;
+    canvasUI: React.RefObject<HTMLCanvasElement>;
+    canvas2D: React.RefObject<HTMLCanvasElement>;
+    canvas3D: React.RefObject<HTMLCanvasElement>;
+    texture: BaseTexture;
 }
 
 export class TextureCanvasComponent extends React.PureComponent<ITextureCanvasComponentProps> {

+ 51 - 136
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasManager.ts

@@ -4,7 +4,6 @@ import { Vector3, Vector2 } from 'babylonjs/Maths/math.vector';
 import { Color4, Color3 } from 'babylonjs/Maths/math.color';
 import { FreeCamera } from 'babylonjs/Cameras/freeCamera';
 import { Nullable } from 'babylonjs/types'
-
 import { PlaneBuilder } from 'babylonjs/Meshes/Builders/planeBuilder';
 import { Mesh } from 'babylonjs/Meshes/mesh';
 import { Camera } from 'babylonjs/Cameras/camera';
@@ -30,6 +29,8 @@ import { ITool } from './toolBar';
 import { IChannel } from './channelsBar';
 import { IMetadata } from './textureEditorComponent';
 
+import { canvasShader } from './canvasShader';
+
 
 export interface IPixelData {
     x? : number;
@@ -86,8 +87,8 @@ export class TextureCanvasManager {
     /* Tracks which keys are currently pressed */
     private _keyMap : any = {};
 
-    private static ZOOM_MOUSE_SPEED : number = 0.0005;
-    private static ZOOM_KEYBOARD_SPEED : number = 0.2;
+    private static ZOOM_MOUSE_SPEED : number = 0.001;
+    private static ZOOM_KEYBOARD_SPEED : number = 0.4;
     private static ZOOM_IN_KEY : string = '+';
     private static ZOOM_OUT_KEY : string = '-';
 
@@ -95,9 +96,12 @@ export class TextureCanvasManager {
     private static PAN_MOUSE_BUTTON : number = 1; // MMB
 
     private static MIN_SCALE : number = 0.01;
+    private static GRID_SCALE : number = 0.047;
     private static MAX_SCALE : number = 10;
 
     private static SELECT_ALL_KEY = 'KeyA';
+    private static SAVE_KEY ='KeyS';
+    private static RESET_KEY = 'KeyR';
     private static DESELECT_KEY = 'Escape'
 
     private _tool : Nullable<ITool>;
@@ -144,7 +148,7 @@ export class TextureCanvasManager {
         this._originalTexture = texture;
         this._originalInternalTexture = this._originalTexture._texture;
         this._engine = new Engine(this._UICanvas, true);
-        this._scene = new Scene(this._engine);
+        this._scene = new Scene(this._engine, {virtual: true});
         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);
@@ -154,7 +158,7 @@ export class TextureCanvasManager {
         this._channelsTexture = new HtmlElementTexture('ct', this._2DCanvas, {engine: this._engine, scene: null, samplingMode: Texture.NEAREST_SAMPLINGMODE, generateMipMaps: true});
 
         this._3DEngine = new Engine(this._3DCanvas);
-        this._3DScene = new Scene(this._3DEngine);
+        this._3DScene = new Scene(this._3DEngine, {virtual: true});
         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;
@@ -170,117 +174,13 @@ export class TextureCanvasManager {
         mat.emissiveColor = Color3.White();
         this._3DPlane.material = mat;
 
+
         this._planeMaterial = new ShaderMaterial(
-            'shader',
+            'canvasShader',
             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;
-
-                    uniform int x1;
-                    uniform int y1;
-                    uniform int x2;
-                    uniform int y2;
-                    uniform int w;
-                    uniform int h;
-
-                    uniform int time;
-            
-                    varying vec2 vUV;
-
-                    float scl = 200.0;
-                    float speed = 10.0 / 1000.0;
-                    float smoothing = 0.2;
-            
-                    void main(void) {
-                        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 * (col.a) + bg * (1.0 - col.a);
-                        float wF = float(w);
-                        float hF = float(h);
-                        int xPixel = int(floor(vUV.x * wF));
-                        int yPixel = int(floor((1.0 - vUV.y) * hF));
-                        int xDis = min(abs(xPixel - x1), abs(xPixel - x2));
-                        int yDis = min(abs(yPixel - y1), abs(yPixel - y2));
-                        if (xPixel >= x1 && yPixel >= y1 && xPixel <= x2 && yPixel <= y2) {
-                            if (xDis <= 4 || yDis <= 4) {
-                                float c = sin(vUV.x * scl + vUV.y * scl + float(time) * speed);
-                                c = smoothstep(-smoothing,smoothing,c);
-                                float val = 1.0 - c;
-                                gl_FragColor = vec4(val, val, val, 1.0) * 0.7 + gl_FragColor * 0.3;
-                            }
-                        }
-                    }`
-            },
-        {
-            attributes: ['position', 'uv'],
-            uniforms: ['worldViewProjection', 'textureSampler', 'r', 'g', 'b', 'a', 'x1', 'y1', 'x2', 'y2', 'w', 'h', 'time']
-        });
+            canvasShader.path,
+            canvasShader.options
+        );
         
         this.grabOriginalTexture();
 
@@ -296,6 +196,7 @@ export class TextureCanvasManager {
         this._planeMaterial.setInt('w', this._size.width);
         this._planeMaterial.setInt('h', this._size.height);
         this._planeMaterial.setInt('time', 0);
+        this._planeMaterial.setFloat('showGrid', 0.0);
         this._plane.material = this._planeMaterial;
         
         this._window.addEventListener('keydown', evt => {
@@ -311,6 +212,14 @@ export class TextureCanvasManager {
                 });
                 evt.preventDefault();
             }
+            if (evt.code === TextureCanvasManager.SAVE_KEY && evt.ctrlKey) {
+                this.saveTexture();
+                evt.preventDefault();
+            }
+            if (evt.code === TextureCanvasManager.RESET_KEY && evt.ctrlKey) {
+                this.reset();
+                evt.preventDefault();
+            }
             if (evt.code === TextureCanvasManager.DESELECT_KEY) {
                 this._setMetadata({
                     select: {
@@ -331,13 +240,19 @@ export class TextureCanvasManager {
             this._engine.resize();
             this._scene.render();
             this._planeMaterial.setInt('time', new Date().getTime());
+            
         });
 
-        this._scale = 1.5;
+        this._scale =  1.5 / Math.max(this._size.width, this._size.height);
         this._isPanning = false;
 
         this._scene.onBeforeRenderObservable.add(() => {
-            this._scale = Math.min(Math.max(this._scale, TextureCanvasManager.MIN_SCALE), TextureCanvasManager.MAX_SCALE);
+            this._scale = Math.min(Math.max(this._scale, TextureCanvasManager.MIN_SCALE / Math.log2(Math.min(this._size.width, this._size.height))), TextureCanvasManager.MAX_SCALE);
+            if (this._scale > TextureCanvasManager.GRID_SCALE) {
+                this._planeMaterial.setFloat('showGrid', 1.0);
+            } else {
+                this._planeMaterial.setFloat('showGrid', 0.0);
+            }
             const ratio = this._UICanvas?.width / this._UICanvas?.height;
             const {x,y} = this._cameraPos;
             this._camera.orthoBottom = y - 1 / this._scale;
@@ -438,6 +353,7 @@ export class TextureCanvasManager {
     private queueTextureUpdate() {
         if (this._canUpdate) {
             (this._target as HtmlElementTexture).update((this._originalTexture as Texture).invertY);
+            this._target._texture?.updateSize(this._size.width, this._size.height);
             if (this._editing3D) {
                 this._imageData = this._3DEngine.readPixels(0, 0, this._size.width, this._size.height);
             } else {
@@ -495,6 +411,7 @@ export class TextureCanvasManager {
         ctx2D.fillStyle = 'white';
         ctx2D.fillRect(x,y,w,h);
         ctx2D.imageSmoothingEnabled = false;
+        // If we're not editing all channels, we must process the pixel data
         if (!editingAllChannels) {
             const newData = ctx.getImageData(0, 0, w, h);
             const nd = newData.data;
@@ -511,6 +428,7 @@ export class TextureCanvasManager {
         } else {
             ctx2D.globalCompositeOperation = 'source-over';
             ctx2D.globalAlpha = 1.0;
+            // We want to use drawImage wherever possible since it is much faster than putImageData
             ctx2D.drawImage(ctx.canvas, x, y);
         }
         this.updateTexture();
@@ -554,9 +472,9 @@ export class TextureCanvasManager {
         ctx.putImageData(imgData, 0, 0);
     }
 
-    public grabOriginalTexture(adjustZoom = true) {
+    public grabOriginalTexture() {
         // Grab image data from original texture and paint it onto the context of a DynamicTexture
-        this.setSize(this._originalTexture.getSize(), adjustZoom);
+        this.setSize(this._originalTexture.getSize());
         TextureHelper.GetTextureDataAsync(
             this._originalTexture,
             this._size.width,
@@ -620,7 +538,7 @@ export class TextureCanvasManager {
     public set face(face: number) {
         if (this._face !== face) {
             this._face = face;
-            this.grabOriginalTexture(false);
+            this.grabOriginalTexture();
             this.updateDisplay();
         }
     }
@@ -628,7 +546,7 @@ export class TextureCanvasManager {
     public set mipLevel(mipLevel : number) {
         if (this._mipLevel === mipLevel) return;
         this._mipLevel = mipLevel;
-        this.grabOriginalTexture(false);
+        this.grabOriginalTexture();
     }
 
     /** Returns the 3D scene used for postprocesses */
@@ -646,9 +564,8 @@ export class TextureCanvasManager {
     }
 
     private makePlane() {
-        const textureRatio = this._size.width / this._size.height;
         if (this._plane) this._plane.dispose();
-        this._plane = PlaneBuilder.CreatePlane("plane", {width: textureRatio, height: 1}, this._scene);
+        this._plane = PlaneBuilder.CreatePlane("plane", {width: this._size.width, height: this._size.height}, this._scene);
         this._plane.enableEdgesRendering();
         this._plane.edgesWidth = 4.0;
         this._plane.edgesColor = new Color4(1,1,1,1);
@@ -675,7 +592,8 @@ export class TextureCanvasManager {
         this._didEdit = true;
     }
 
-    public setSize(size: ISize, adjustZoom = true) {
+    public setSize(size: ISize) {
+        const oldSize = this._size;
         this._size = size;
         this._2DCanvas.width = this._size.width;
         this._2DCanvas.height = this._size.height;
@@ -683,10 +601,10 @@ export class TextureCanvasManager {
         this._3DCanvas.height = this._size.height;
         this._planeMaterial.setInt('w', this._size.width);
         this._planeMaterial.setInt('h', this._size.height);
-        if (adjustZoom) {
+        if (oldSize.width != size.width || oldSize.height != size.height) {
             this._cameraPos.x = 0;
             this._cameraPos.y = 0;
-            this._scale = 1.5 / (this._size.width/this._size.height);
+            this._scale = 1.5 / Math.max(this._size.width, this._size.height);
         }
         this.makePlane();
     }
@@ -706,17 +624,7 @@ export class TextureCanvasManager {
                 let base64data = reader.result as string;     
 
                 if (extension === '.dds' || extension === '.env') {
-                    const texture = new CubeTexture(
-                        base64data,
-                        this._scene,
-                        [extension],
-                        this._originalTexture.noMipmap,                        
-                        null,
-                        () => {
-                            // TO-DO: implement cube loading
-                            texture.dispose();
-                        }
-                    );
+                    (this._originalTexture as CubeTexture).updateURL(base64data, extension, () => this.grabOriginalTexture());
                 } else {
                     const texture = new Texture(
                         base64data,
@@ -743,6 +651,13 @@ export class TextureCanvasManager {
         }, undefined, true);
     }
 
+    public saveTexture() {
+        const canvas = this._editing3D ? this._3DCanvas : this._2DCanvas;
+        Tools.ToBlob(canvas, (blob) => {
+            Tools.Download(blob!, this._originalTexture.name);
+        });
+    }
+
     public dispose() {
         if (this._didEdit) {
             this._originalInternalTexture?.dispose();

+ 13 - 14
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditor.scss

@@ -79,6 +79,7 @@
                 }
                 label {
                     margin-left: 15px;
+                    font-size: 15px;
                     color: #afafaf;
                     input {
                         width: 40px;
@@ -86,8 +87,10 @@
                         background-color: #000000;
                         color: #ffffff;
                         border: 0;
-                        padding-left: 4px;
                         font-size: 12px;
+                        text-align: 'left';
+                        font-family: 'acumin-pro-condensed';
+                        font-size: 15px;
                     }
         
                     &:last-of-type {
@@ -110,6 +113,9 @@
             &:first-of-type {
                 margin-left: 15px;
             }
+            &:last-of-type {
+                padding-right: 15px;
+            }
             width: 45px;
             color: #afafaf;
             display: flex;
@@ -119,6 +125,7 @@
                 width: 30px;
                 color: white;
             }
+            font-size: 15px;
         }
     }
     
@@ -161,11 +168,6 @@
             }
         }
 
-        #color-picker {
-            position: absolute;
-            margin-left: 40px;
-        }
-
         #color {
             margin-top: 8px;
             #active-color-bg {
@@ -187,14 +189,7 @@
                 border-radius: 50%;
             }
         }
-        .color-picker-cover {
-            position: fixed;
-            top: 0;
-            left: 0;
-            right: 0;
-            bottom: 0;
-        }
-        .color-picker {
+        #color-picker {
             position: absolute;
             margin-left: 40px;
         }
@@ -222,6 +217,10 @@
             &:hover {
                 cursor: pointer;
             }
+
+            &:last-of-type {
+                border-bottom: none;
+            }
         }
         user-select: none;
     }

+ 24 - 6
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditorComponent.tsx

@@ -34,6 +34,7 @@ interface ITextureEditorComponentState {
     pixelData : IPixelData;
     face: number;
     mipLevel: number;
+    pickerOpen: boolean;
 }
 
 export interface IToolParameters {
@@ -117,6 +118,7 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
     private _UICanvas = React.createRef<HTMLCanvasElement>();
     private _2DCanvas = React.createRef<HTMLCanvasElement>();
     private _3DCanvas = React.createRef<HTMLCanvasElement>();
+    private _pickerRef = React.createRef<HTMLDivElement>();
     private _timer : number | null;
     private static PREVIEW_UPDATE_DELAY_MS = 160;
 
@@ -148,7 +150,8 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             channels,
             pixelData: {},
             face: 0,
-            mipLevel: 0
+            mipLevel: 0,
+            pickerOpen: false
         }
         this.loadToolFromURL = this.loadToolFromURL.bind(this);
         this.changeTool = this.changeTool.bind(this);
@@ -157,7 +160,8 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
         this.resetTexture = this.resetTexture.bind(this);
         this.resizeTexture = this.resizeTexture.bind(this);
         this.uploadTexture = this.uploadTexture.bind(this);
-
+        this.setPickerOpen = this.setPickerOpen.bind(this);
+        this.onPointerDown = this.onPointerDown.bind(this);
     }
 
     componentDidMount() {
@@ -252,10 +256,18 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
         this._textureCanvasManager.metadata = data;
     }
 
+    setPickerOpen(open: boolean) {
+        this.setState({pickerOpen: open});
+    }
+
+    onPointerDown(evt: React.PointerEvent) {
+        if (!this._pickerRef.current?.contains(evt.target as Node)) {
+            this.setPickerOpen(false);
+        }
+    }
+
     saveTexture() {
-        Tools.ToBlob(this._2DCanvas.current!, (blob) => {
-            Tools.Download(blob!, this.props.url);
-        });
+        this._textureCanvasManager.saveTexture();
     }
 
     resetTexture() {
@@ -272,7 +284,8 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
 
     render() {
         const currentTool : ITool | undefined = this.state.tools[this.state.activeToolIndex];
-        return <div id="texture-editor">
+
+        return <div id="texture-editor" onPointerDown={this.onPointerDown}>
             <PropertiesBar
                 texture={this.props.texture}
                 saveTexture={this.saveTexture}
@@ -284,6 +297,7 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
                 uploadTexture={this.uploadTexture}
                 mipLevel={this.state.mipLevel}
                 setMipLevel={mipLevel => this.setState({mipLevel})}
+                size={this._textureCanvasManager?.size || this.props.texture.getSize()}
             />
             {!this.props.texture.isCube && <ToolBar
                 tools={this.state.tools}
@@ -292,6 +306,10 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
                 changeTool={this.changeTool}
                 metadata={this.state.metadata}
                 setMetadata={this.setMetadata}
+                pickerOpen={this.state.pickerOpen}
+                setPickerOpen={this.setPickerOpen}
+                pickerRef={this._pickerRef}
+                hasAlpha={this.props.texture.hasAlpha}
             />}
             <ChannelsBar channels={this.state.channels} setChannels={(channels) => {this.setState({channels})}}/>
             <TextureCanvasComponent canvas2D={this._2DCanvas} canvas3D={this._3DCanvas} canvasUI={this._UICanvas} texture={this.props.texture}/>

+ 18 - 37
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/toolBar.tsx

@@ -13,27 +13,24 @@ interface IToolBarProps {
     activeToolIndex : number;
     metadata: IMetadata;
     setMetadata(data : any): void;
+    pickerOpen: boolean;
+    setPickerOpen(open: boolean): void;
+    pickerRef: React.RefObject<HTMLDivElement>;
+    hasAlpha: boolean;
 }
 
 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() {
@@ -41,6 +38,10 @@ export class ToolBar extends React.Component<IToolBarProps, IToolBarState> {
         const opacityHex = opacityInt.toString(16).padStart(2, '0');
         return `${this.props.metadata.color}${opacityHex}`;
     }
+    
+    shouldComponentUpdate(nextProps: IToolBarProps) {
+        return (nextProps.tools != this.props.tools || nextProps.activeToolIndex !== this.props.activeToolIndex || nextProps.metadata != this.props.metadata || nextProps.pickerOpen != this.props.pickerOpen);
+    }
 
     render() {
         return <div id='toolbar'>
@@ -61,42 +62,22 @@ export class ToolBar extends React.Component<IToolBarProps, IToolBarState> {
                         />
                     }
                 )}
-                <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='color'
+                onClick={() => {if (!this.props.pickerOpen) this.props.setPickerOpen(true);}}
+                title='Color'
+                className={`icon button${this.props.pickerOpen ? ` active` : ``}`}
+            >
                 <div id='active-color-bg'>
                     <div id='active-color' style={{backgroundColor: this.props.metadata.color, opacity: this.props.metadata.alpha}}></div>
                 </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, alpha: color.rgb.a})}/>
-                    </div>
-                </>
+                this.props.pickerOpen &&
+                <div id='color-picker' ref={this.props.pickerRef}>
+                    <SketchPicker disableAlpha={!this.props.hasAlpha} color={this.computeRGBAColor()}  onChange={color => this.props.setMetadata({color: color.hex, alpha: color.rgb.a})}/>
+                </div>
             }
         </div>;
     }