Darragh Burke 5 سال پیش
والد
کامیت
21e103c3ef

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

@@ -191,6 +191,7 @@ export class TexturePropertyGridComponent extends React.Component<ITextureProper
                         texture={this.props.texture}
                         url={textureUrl}
                         window={this.popoutWindowRef}
+                        onUpdate={() => this.forceRefresh()}
                     />
                 </PopupComponent>)}
                 <CustomPropertyGridComponent globalState={this.props.globalState} target={texture}

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

@@ -2,12 +2,15 @@ import * as React from 'react';
 
 interface BottomBarProps {
     name: string;
+    mipLevel: number;
+    hasMips: boolean;
 }
 
 export class BottomBar extends React.Component<BottomBarProps> {
     render() {
         return <div id='bottom-bar'>
             <span id='file-url'>{this.props.name}</span>
+            {this.props.hasMips && <span id='mip-level'>MIP Preview: {this.props.mipLevel}</span>}
         </div>;
     }
 }

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

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

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

@@ -19,7 +19,7 @@ export const Eyedropper : IToolData = {
             const pixel = ctx!.getImageData(x, y, 1, 1).data;
             setMetadata({
                 color: '#' + ('000000' + this.rgbToHex(pixel[0], pixel[1], pixel[2])).slice(-6),
-                opacity: pixel[3] / 255
+                alpha: pixel[3] / 255
             });
         }
         

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

@@ -11,13 +11,13 @@ export const Floodfill : IToolData = {
         }
 
         fill() {
-            const {canvas2D, metadata, size, updateTexture} = this.getParameters();
-            const ctx = canvas2D.getContext('2d')!;
+            const {metadata, startPainting, stopPainting} = this.getParameters();
+            const ctx = startPainting();
             ctx.fillStyle = metadata.color;
-            ctx.globalAlpha = metadata.opacity;
-            ctx.globalCompositeOperation = 'source-over';
-            ctx.fillRect(0,0, size.width, size.height);
-            updateTexture();
+            ctx.globalAlpha = metadata.alpha;
+            ctx.globalCompositeOperation = 'copy';
+            ctx.fillRect(0,0, ctx.canvas.width, ctx.canvas.height);
+            stopPainting(ctx);
         }
         
         setup () {

+ 20 - 15
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/paintbrush.ts

@@ -21,18 +21,27 @@ export const Paintbrush : IToolData = {
         }
 
         paint(pointerInfo : PointerInfo) {
-            const {canvas2D, getMouseCoordinates, metadata, updateTexture } = this.getParameters();
-            const ctx = canvas2D.getContext('2d')!;
-            const {x, y} = getMouseCoordinates(pointerInfo);
+            const {canvas2D, getMouseCoordinates, metadata, updateTexture, startPainting, stopPainting } = this.getParameters();
+            const ctx = startPainting();//canvas2D.getContext('2d')!;
+            let {x, y} = getMouseCoordinates(pointerInfo);
+            if (metadata.select.x1 != -1) {
+                x -= metadata.select.x1;
+                y -= metadata.select.y1;
+            }
             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();
+            // ctx.strokeStyle = metadata.color;
+            ctx.globalAlpha = metadata.alpha;
+            // ctx.lineWidth = this.radius;
+            // ctx.lineCap = 'round';
+            // ctx.lineJoin = 'round';
+            // ctx.lineTo(x, y);
+            // ctx.stroke();
+            ctx.beginPath();
+            ctx.arc(x, y, this.radius, 0, 360);
+            ctx.fillStyle = metadata.color;
+            ctx.fill();
+            stopPainting(ctx);
+            // updateTexture();
         }
         
         setup () {
@@ -65,10 +74,6 @@ export const Paintbrush : IToolData = {
                     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) {

+ 15 - 9
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/rectangleSelect.ts

@@ -14,19 +14,18 @@ export const RectangleSelect : IToolData = {
         setup() {
             const {scene} = this.getParameters();
             this.pointerObserver = scene.onPointerObservable.add((pointerInfo) => {
-                const {getMouseCoordinates, setMetadata, metadata, canvas2D} = this.getParameters();
+                const {getMouseCoordinates, setMetadata, metadata} = this.getParameters();
                 if (pointerInfo.pickInfo?.hit) {
                     if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
                         if (pointerInfo.event.button == 0) {
                             this.isSelecting = true;
                             const {x, y} = getMouseCoordinates(pointerInfo);
-                            const {select} = metadata;
                             setMetadata({
                                 select: {
                                     x1: x,
                                     y1: y,
-                                    x2: select.x2,
-                                    y2: select.y2
+                                    x2: x,
+                                    y2: y
                                 }
                             })
                           }
@@ -36,10 +35,10 @@ export const RectangleSelect : IToolData = {
                         const {select} = metadata;
                         setMetadata({
                             select: {
-                                x1: select.x1,
-                                y1: select.y1,
-                                x2: x,
-                                y2: y
+                                x1: Math.min(x, select.x1),
+                                y1: Math.min(y, select.y1),
+                                x2: Math.max(x, select.x1),
+                                y2: Math.max(y, select.y1)
                             }
                         })
                     }
@@ -47,7 +46,14 @@ export const RectangleSelect : IToolData = {
                 if (pointerInfo.type === PointerEventTypes.POINTERUP) {
                     if (pointerInfo.event.button == 0) {
                         this.isSelecting = false;
-                      }
+                        if (metadata.select.x1 === metadata.select.x2 || metadata.select.y1 === metadata.select.y2) {
+                            setMetadata({
+                                select: {
+                                    x1: -1, y1: -1, x2: -1, y2: -1
+                                }
+                            })
+                        }
+                    }
                 }
             });
         }

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

@@ -11,6 +11,8 @@ interface IPropertiesBarProps {
     resetTexture() : void;
     resizeTexture(width: number, height: number) : void;
     uploadTexture(file : File) : void;
+    mipLevel: number;
+    setMipLevel: (mipLevel : number) => void;
 }
 
 interface IPropertiesBarState {
@@ -68,6 +70,7 @@ export class PropertiesBar extends React.Component<IPropertiesBarProps,IProperti
     }
 
     render() {
+        const {mipLevel, setMipLevel, pixelData, resizeTexture, texture, face, setFace, saveTexture, resetTexture, uploadTexture} = this.props;
         return <div id='properties'>
                 <div className='tab' id='logo-tab'>
                     <img className='icon' src={this._babylonLogo}/>
@@ -83,38 +86,39 @@ export class PropertiesBar extends React.Component<IPropertiesBarProps,IProperti
                         <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)}/> 
+                        <img id='resize' className='icon button' title='Resize' alt='Resize' src={this._resizeButton} onClick={() => 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}/>
+                    <this.pixelData name='X' data={pixelData.x}/>
+                    <this.pixelData name='Y' data={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}/>
+                    <this.pixelData name='R' data={pixelData.r}/>
+                    <this.pixelData name='G' data={pixelData.g}/>
+                    <this.pixelData name='B' data={pixelData.b}/>
+                    <this.pixelData name='A' data={pixelData.a}/>
                 </div>
-                {this.props.texture.isCube &&
-                <>
+                {texture.isCube &&
                     <div className='tab' id='face-tab'>
-                        {this._faces.map((face, index) =>
+                        {this._faces.map((value, index) =>
                         <img
                             key={index}
-                            className={this.props.face == index ? 'icon face button active' : 'icon face button'}
-                            src={face}
-                            onClick={() => this.props.setFace(index)}
+                            className={face == index ? 'icon face button active' : 'icon face button'}
+                            src={value}
+                            onClick={() => setFace(index)}
                         />)}
                     </div>
+                }
+                {!texture.noMipmap &&
                     <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} />
+                        <img title='Mip Preview Up' className='icon button' src={this._mipUp} onClick={() => mipLevel > 1 && setMipLevel(mipLevel - 1)} />
+                        <img title='Mip Preview Down' className='icon button' src={this._mipDown} onClick={() => mipLevel < 12 && setMipLevel(mipLevel + 1)} />
                     </div>
-                </>}
+                }
                 <div className='tab' id='right-tab'>
                     <div className='content'>
-                        <img title='Reset' className='icon button' src={this._resetButton} onClick={() => this.props.resetTexture()}/>
+                        <img title='Reset' className='icon button' src={this._resetButton} onClick={() => resetTexture()}/>
                         <label>
                             <input
                                 accept='.jpg, .png, .tga, .dds, .env'
@@ -123,7 +127,7 @@ export class PropertiesBar extends React.Component<IPropertiesBarProps,IProperti
                                     (evt : React.ChangeEvent<HTMLInputElement>) => {
                                         const files = evt.target.files;
                                         if (files && files.length) {
-                                            this.props.uploadTexture(files[0]);
+                                            uploadTexture(files[0]);
                                         }
                                 
                                         evt.target.value = "";
@@ -136,7 +140,7 @@ export class PropertiesBar extends React.Component<IPropertiesBarProps,IProperti
                                 src={this._uploadButton}
                             />
                         </label>
-                        <img title='Save' className='icon button' src={this._saveButton} onClick={() => this.props.saveTexture()}/>
+                        <img title='Save' className='icon button' src={this._saveButton} onClick={() => saveTexture()}/>
                     </div>
                 </div>
         </div>;

+ 181 - 28
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasManager.ts

@@ -34,6 +34,7 @@ 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 { IMetadata } from './textureEditorComponent';
 
 
 export interface IPixelData {
@@ -57,6 +58,7 @@ export class TextureCanvasManager {
     private _engine: Engine;
     private _scene: Scene;
     private _camera: FreeCamera;
+    private _cameraPos: Vector2;
 
     private _scale : number;
     private _isPanning : boolean = false;
@@ -81,6 +83,7 @@ export class TextureCanvasManager {
 
     private _channels : IChannel[] = [];
     private _face : number = 0;
+    private _mipLevel : number = 1;
 
     /* The texture from the original engine that we invoked the editor on */
     private _originalTexture: BaseTexture;
@@ -108,6 +111,9 @@ export class TextureCanvasManager {
     private static MIN_SCALE : number = 0.01;
     private static MAX_SCALE : number = 10;
 
+    private static SELECT_ALL_KEY = 'KeyA';
+    private static DESELECT_KEY = 'Escape'
+
     private _tool : Nullable<ITool>;
 
     private _setPixelData : (pixelData : IPixelData) => void;
@@ -116,17 +122,23 @@ export class TextureCanvasManager {
 
     private _window : Window;
 
-    public metadata : any = {};
+    private _metadata : IMetadata;
 
     private _editing3D : boolean = false;
 
+    private _onUpdate : () => void;
+    private _setMetadata : (metadata: any) => void;
+
     public constructor(
         texture: BaseTexture,
         window: Window,
         canvasUI: HTMLCanvasElement,
         canvas2D: HTMLCanvasElement,
         canvas3D: HTMLCanvasElement,
-        setPixelData: (pixelData : IPixelData) => void
+        setPixelData: (pixelData : IPixelData) => void,
+        metadata: IMetadata,
+        onUpdate: () => void,
+        setMetadata: (metadata: any) => void
     ) {
         this._window = window;
 
@@ -134,6 +146,9 @@ export class TextureCanvasManager {
         this._2DCanvas = canvas2D;
         this._3DCanvas = canvas3D;
         this._setPixelData = setPixelData;
+        this._metadata = metadata;
+        this._onUpdate = onUpdate;
+        this._setMetadata = setMetadata;
 
         this._size = texture.getSize();
         this._originalTexture = texture;
@@ -144,6 +159,7 @@ export class TextureCanvasManager {
 
         this._camera = new FreeCamera('camera', new Vector3(0, 0, -1), this._scene);
         this._camera.mode = Camera.ORTHOGRAPHIC_CAMERA;
+        this._cameraPos = new Vector2();
 
         this._channelsTexture = new HtmlElementTexture('ct', this._2DCanvas, {engine: this._engine, scene: null, samplingMode: Engine.TEXTURE_NEAREST_LINEAR});
 
@@ -195,10 +211,15 @@ export class TextureCanvasManager {
                     uniform bool b;
                     uniform bool a;
 
-                    uniform float x1;
-                    uniform float y1;
-                    uniform float x2;
-                    uniform float y2;
+                    uniform int x1;
+                    uniform int y1;
+                    uniform int x2;
+                    uniform int y2;
+                    uniform int w;
+                    uniform int h;
+
+                    uniform int time;
+                    uniform int mipLevel;
             
                     varying vec2 vUV;
             
@@ -211,7 +232,7 @@ export class TextureCanvasManager {
                             pattern = 0.7;
                         }
                         vec4 bg = vec4(pattern, pattern, pattern, 1.0);
-                        vec4 col = texture(textureSampler, vUV);
+                        vec4 col = textureLod(textureSampler, vUV, 6.0);
                         if (!r && !g && !b) {
                             if (a) {
                                 col = vec4(col.a, col.a, col.a, 1.0);
@@ -250,11 +271,35 @@ export class TextureCanvasManager {
                             }
                         }
                         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 xPixe= int(gl_FragCoord.y);
+                        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 t = mod(float(time), 500.0);
+                                // float amt = mod(floor((gl_FragCoord.x - gl_FragCoord.y + t) * 0.1), 2.0);
+                                gl_FragColor = vec4(1.0,1.0,1.0,1.0);
+                            }
+                        }
+                        // if (xPixel >= x1 && yPixel >= y1 && xPixel <= x2 && yPixel <= y2) {
+                        //     if (xPixel == x1 || yPixel == y1 || xPixel == x2 || yPixel == y2) {
+                        //         float dots = mod(gl_FragCoord.x + gl_FragCoord.y, 2.0); 
+                        //         if (dots == 0.0) {
+                        //             gl_FragColor = vec4(0.0,0.0,0.0,1.0);
+                        //         }
+                        //     } else {
+                        //         gl_FragColor = gl_FragColor * 0.8 + vec4(0.0,0.0,1.0,1.0) * 0.2;
+                        //     }
+                        // }
                     }`
             },
         {
             attributes: ['position', 'uv'],
-            uniforms: ['worldViewProjection', 'textureSampler', 'r', 'g', 'b', 'a', 'x1', 'y1', 'x2', 'y2']
+            uniforms: ['worldViewProjection', 'textureSampler', 'r', 'g', 'b', 'a', 'x1', 'y1', 'x2', 'y2', 'w', 'h', 'time']
         });
 
         this._planeMaterial.setTexture('textureSampler', this._channelsTexture);
@@ -262,10 +307,14 @@ export class TextureCanvasManager {
         this._planeMaterial.setFloat('g', 1.0);
         this._planeMaterial.setFloat('b', 1.0);
         this._planeMaterial.setFloat('a', 1.0);
-        this._planeMaterial.setFloat('x1', -1.0);
-        this._planeMaterial.setFloat('y1', -1.0);
-        this._planeMaterial.setFloat('x2', -1.0);
-        this._planeMaterial.setFloat('y2', -1.0);
+        this._planeMaterial.setInt('x1', -1);
+        this._planeMaterial.setInt('y1', -1);
+        this._planeMaterial.setInt('x2', -1);
+        this._planeMaterial.setInt('y2', -1);
+        this._planeMaterial.setInt('w', this._size.width);
+        this._planeMaterial.setInt('h', this._size.height);
+        this._planeMaterial.setInt('time', 0);
+        this._planeMaterial.setInt('mipLevel', 1);
         this._plane.material = this._planeMaterial;
         
         const adt = AdvancedDynamicTexture.CreateFullscreenUI('gui', true, this._scene);
@@ -303,7 +352,7 @@ export class TextureCanvasManager {
         topBar.addControl(title);
         this._GUI.toolWindow.addControl(topBar);
 
-        this._window.addEventListener('pointermove',  (evt : PointerEvent) => {
+        this._window.addEventListener('pointermove', evt => {
             if (!this._GUI.isDragging) return;
             if (!this._GUI.dragCoords) {
                 this._GUI.dragCoords = new Vector2(evt.x, evt.y);
@@ -319,9 +368,44 @@ export class TextureCanvasManager {
             this._GUI.dragCoords.y = evt.y;
         });
 
+        this._window.addEventListener('keydown', evt => {
+            this._keyMap[evt.code] = true;
+            if (evt.code === TextureCanvasManager.SELECT_ALL_KEY && evt.ctrlKey) {
+                this._setMetadata({
+                    select: {
+                        x1: 0,
+                        y1: 0,
+                        x2: this._size.width,
+                        y2: this._size.height
+                    }
+                });
+                evt.preventDefault();
+            }
+            if (evt.code === TextureCanvasManager.DESELECT_KEY) {
+                this._setMetadata({
+                    select: {
+                        x1: -1,
+                        y1: -1,
+                        x2: -1,
+                        y2: -1
+                    }
+                })
+            }
+        });
+        
+        this._window.addEventListener('keyup', evt => {
+            this._keyMap[evt.code] = false;
+        });
+
         this._engine.runRenderLoop(() => {
             this._engine.resize();
             this._scene.render();
+            let cursor = 'initial';
+            if (this._tool) {
+                cursor = `url(data:image/svg+xml;base64,${this._tool.icon})`;
+            }
+            this._UICanvas.parentElement!.style.cursor = cursor;
+            this._planeMaterial.setInt('time', new Date().getTime());
         });
 
         this._scale = 1.5;
@@ -330,10 +414,11 @@ export class TextureCanvasManager {
         this._scene.onBeforeRenderObservable.add(() => {
             this._scale = Math.min(Math.max(this._scale, TextureCanvasManager.MIN_SCALE), TextureCanvasManager.MAX_SCALE);
             const ratio = this._UICanvas?.width / this._UICanvas?.height;
-            this._camera.orthoBottom = -1 / this._scale;
-            this._camera.orthoTop = 1 / this._scale;
-            this._camera.orthoLeft =  ratio / -this._scale;
-            this._camera.orthoRight = ratio / this._scale;
+            const {x,y} = this._cameraPos;
+            this._camera.orthoBottom = y - 1 / this._scale;
+            this._camera.orthoTop = y + 1 / this._scale;
+            this._camera.orthoLeft =  x - ratio / this._scale;
+            this._camera.orthoRight = x + ratio / this._scale;
         })
 
         this._scene.onPointerObservable.add((pointerInfo) => {
@@ -357,8 +442,8 @@ export class TextureCanvasManager {
                     break;
                 case PointerEventTypes.POINTERMOVE:
                     if (this._isPanning) {
-                        this._camera.position.x -= (pointerInfo.event.x - this._mouseX) / this._scale * TextureCanvasManager.PAN_SPEED;
-                        this._camera.position.y += (pointerInfo.event.y - this._mouseY) / this._scale * TextureCanvasManager.PAN_SPEED;
+                        this._cameraPos.x -= (pointerInfo.event.x - this._mouseX) / this._scale * TextureCanvasManager.PAN_SPEED;
+                        this._cameraPos.y += (pointerInfo.event.y - this._mouseY) / this._scale * TextureCanvasManager.PAN_SPEED;
                         this._mouseX = pointerInfo.event.x;
                         this._mouseY = pointerInfo.event.y;
                     }
@@ -378,11 +463,13 @@ export class TextureCanvasManager {
             switch(kbInfo.type) {
                 case KeyboardEventTypes.KEYDOWN:
                     this._keyMap[kbInfo.event.key] = true;
-                    if (kbInfo.event.key === TextureCanvasManager.ZOOM_IN_KEY) {
-                        this._scale += TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
-                    }
-                    if (kbInfo.event.key === TextureCanvasManager.ZOOM_OUT_KEY) {
-                        this._scale -= TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
+                    switch (kbInfo.event.key) {
+                        case TextureCanvasManager.ZOOM_IN_KEY:
+                            this._scale += TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
+                            break;
+                        case TextureCanvasManager.ZOOM_OUT_KEY:
+                            this._scale -= TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
+                            break;
                     }
                     break;
                 case KeyboardEventTypes.KEYUP:
@@ -420,10 +507,56 @@ export class TextureCanvasManager {
         this._originalTexture._texture = this._target._texture;
         this._channelsTexture.element = element;
         this.updateDisplay();
+        this._onUpdate();
+    }
+
+    public startPainting() : CanvasRenderingContext2D {
+        if (this._metadata.select.x1 == -1) {
+            return this._2DCanvas.getContext('2d')!;
+        }
+        const canvas = document.createElement('canvas');
+        canvas.width = this._metadata.select.x2 - this._metadata.select.x1;
+        canvas.height = this._metadata.select.y2 - this._metadata.select.y1;
+        const ctx = canvas.getContext('2d')!;
+        ctx.putImageData(this._2DCanvas.getContext('2d')!.getImageData(this._metadata.select.x1, this._metadata.select.y1, canvas.width, canvas.height), 0, 0);
+        return ctx;
+    }
+
+    public stopPainting(ctx: CanvasRenderingContext2D) : void {
+        if (this._metadata.select.x1 == -1) {
+            
+        } else {
+            
+            const pixelData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
+            let editingAllChannels = true;
+            this._channels.forEach(channel => {
+                if (!channel.editable) editingAllChannels = false;
+            })
+            if (!editingAllChannels) {
+                const xStart = this._metadata.select.x1;
+                const yStart = this._metadata.select.y1;
+                const oldData = this._2DCanvas.getContext('2d')!.getImageData(xStart, yStart, ctx.canvas.width, ctx.canvas.height);
+                for(let x = 0; x < pixelData.width; x += 1) {
+                    for(let y = 0; y < pixelData.height; y += 1) {
+                        const i = (x + y * pixelData.width) * 4;
+                        this._channels.forEach((channel, index) => {
+                            if (!channel.editable) {
+                                pixelData.data[i + index] = oldData.data[i + index];
+                            }
+                        })
+                    }
+                }
+            }
+            ctx.globalAlpha = 1.0;
+            ctx.globalCompositeOperation = 'source-over';
+            this.canvas2D.getContext('2d')?.putImageData(pixelData, this._metadata.select.x1, this._metadata.select.y1);
+            ctx.canvas.parentNode?.removeChild(ctx.canvas);
+        }
+        this.updateTexture();
     }
 
     private updateDisplay() {
-        this._3DScene.render()
+        this._3DScene.render();
         this._channelsTexture.update();
     }
 
@@ -521,7 +654,6 @@ export class TextureCanvasManager {
         return this._tool;
     }
 
-    // BROKEN : FIX THIS
     public set face(face: number) {
         if (this._face !== face) {
             this._face = face;
@@ -530,6 +662,11 @@ export class TextureCanvasManager {
         }
     }
 
+    public set mipLevel(mipLevel : number) {
+        this._mipLevel = mipLevel;
+        this._planeMaterial.setInt('mipLevel', mipLevel);
+    }
+
     /** Returns the tool GUI object, allowing tools to access the GUI */
     public get GUI() {
         return this._GUI;
@@ -540,6 +677,21 @@ export class TextureCanvasManager {
         return this._3DScene;
     }
 
+    public set metadata(metadata: IMetadata) {
+        this._metadata = metadata;
+        const {x1,y1,x2,y2} = metadata.select;
+        // x1 = x1/this._size.width * this._UICanvas.width;
+        // y1 = 1-(y1/this._size.height) * this._UICanvas.height;
+        // x2 = x2/this._size.width * this._UICanvas.width;
+        // y2 = (1-y2/this._size.height) * this._UICanvas.height;
+
+        this._planeMaterial.setInt('x1', x1);
+        this._planeMaterial.setInt('y1', y1);
+        this._planeMaterial.setInt('x2', x2);
+        this._planeMaterial.setInt('y2', y2);
+        console.log(this._planeMaterial.getEffect()?.getUniformNames());
+    }
+
     private makePlane() {
         const textureRatio = this._size.width / this._size.height;
         if (this._plane) this._plane.dispose();
@@ -559,6 +711,7 @@ export class TextureCanvasManager {
         this.grabOriginalTexture();
         this.makePlane();
         this._didEdit = false;
+        this._onUpdate();
     }
 
     public async resize(newSize : ISize) {
@@ -576,8 +729,8 @@ export class TextureCanvasManager {
         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._cameraPos.x = 0;
+            this._cameraPos.y = 0;
             this._scale = 1.5 / (this._size.width/this._size.height);
         }
         this.makePlane();

+ 44 - 13
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditorComponent.tsx

@@ -24,15 +24,17 @@ interface ITextureEditorComponentProps {
     texture: BaseTexture;
     url: string;
     window: React.RefObject<PopupComponent>;
+    onUpdate: () => void;
 }
 
 interface ITextureEditorComponentState {
     tools: ITool[];
     activeToolIndex: number;
-    metadata: any;
+    metadata: IMetadata;
     channels: IChannel[];
     pixelData : IPixelData;
     face: number;
+    mipLevel: number;
 }
 
 export interface IToolParameters {
@@ -47,7 +49,7 @@ export interface IToolParameters {
     /** 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;
     /** 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;
+    metadata: IMetadata;
     /** Call this when you want to mutate the metadata. */
     setMetadata: (data : any) => void;
     /** Returns the texture coordinates under the cursor */
@@ -56,6 +58,8 @@ export interface IToolParameters {
     GUI: IToolGUI;
     /** Provides access to the BABYLON namespace */
     BABYLON: any;
+    startPainting: () => CanvasRenderingContext2D;
+    stopPainting: (ctx: CanvasRenderingContext2D) => void;
 }
 
 
@@ -87,6 +91,18 @@ interface IToolConstructable {
     new (getParameters: () => IToolParameters) : IToolType;
 }
 
+export interface IMetadata {
+    color: string;
+    alpha: number;
+    select: {
+        x1: number,
+        y1: number,
+        x2: number,
+        y2: number
+    }
+    [key: string] : any;
+}
+
 declare global {
     var _TOOL_DATA_ : IToolData;
 }
@@ -96,6 +112,7 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
     private _UICanvas = React.createRef<HTMLCanvasElement>();
     private _2DCanvas = React.createRef<HTMLCanvasElement>();
     private _3DCanvas = React.createRef<HTMLCanvasElement>();
+    private _timer : number | null;
 
     constructor(props : ITextureEditorComponentProps) {
         super(props);
@@ -114,7 +131,7 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             activeToolIndex: -1,
             metadata: {
                 color: '#ffffff',
-                opacity: 1,
+                alpha: 1,
                 select: {
                     x1: -1,
                     y1: -1,
@@ -124,13 +141,13 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             },
             channels,
             pixelData: {},
-            face: 0
+            face: 0,
+            mipLevel: 1
         }
         this.loadToolFromURL = this.loadToolFromURL.bind(this);
         this.changeTool = this.changeTool.bind(this);
         this.setMetadata = this.setMetadata.bind(this);
         this.saveTexture = this.saveTexture.bind(this);
-        this.setFace = this.setFace.bind(this);
         this.resetTexture = this.resetTexture.bind(this);
         this.resizeTexture = this.resizeTexture.bind(this);
         this.uploadTexture = this.uploadTexture.bind(this);
@@ -144,7 +161,10 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             this._UICanvas.current!,
             this._2DCanvas.current!,
             this._3DCanvas.current!,
-            (data : IPixelData) => {this.setState({pixelData: data})}
+            (data : IPixelData) => {this.setState({pixelData: data})},
+            this.state.metadata,
+            () => this.textureDidUpdate(),
+            data => this.setMetadata(data)
         );
         this.addTools(defaultTools);
     }
@@ -153,14 +173,24 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
         let channelsClone : IChannel[] = [];
         this.state.channels.forEach(channel => channelsClone.push({...channel}));
         this._textureCanvasManager.channels = channelsClone;
-        this._textureCanvasManager.metadata = {...this.state.metadata};
         this._textureCanvasManager.face = this.state.face;
+        this._textureCanvasManager.mipLevel = this.state.mipLevel;
     }
 
     componentWillUnmount() {
         this._textureCanvasManager.dispose();
     }
 
+    textureDidUpdate() {
+        if (this._timer != null) {
+            clearTimeout(this._timer);
+        }
+        this._timer = window.setTimeout(() => {
+            this.props.onUpdate();
+            this._timer = null;
+        }, 300);
+    }
+
     loadToolFromURL(url : string) {
         Tools.LoadScript(url, () => {
             this.addTools([_TOOL_DATA_]);
@@ -188,6 +218,8 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             scene3D: this._textureCanvasManager.scene3D,
             size: this._textureCanvasManager.size,
             updateTexture: () => this._textureCanvasManager.updateTexture(),
+            startPainting: () => this._textureCanvasManager.startPainting(),
+            stopPainting: (ctx : CanvasRenderingContext2D) => this._textureCanvasManager.stopPainting(ctx),
             metadata: this.state.metadata,
             setMetadata: (data : any) => this.setMetadata(data),
             getMouseCoordinates: (pointerInfo : PointerInfo) => this._textureCanvasManager.getMouseCoordinates(pointerInfo),
@@ -211,10 +243,7 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             ...newMetadata
         }
         this.setState({metadata: data});
-    }
-
-    setFace(face: number) {
-        this.setState({face});
+        this._textureCanvasManager.metadata = data;
     }
 
     saveTexture() {
@@ -242,10 +271,12 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
                 saveTexture={this.saveTexture}
                 pixelData={this.state.pixelData}
                 face={this.state.face}
-                setFace={this.setFace}
+                setFace={face => this.setState({face})}
                 resetTexture={this.resetTexture}
                 resizeTexture={this.resizeTexture}
                 uploadTexture={this.uploadTexture}
+                mipLevel={this.state.mipLevel}
+                setMipLevel={mipLevel => this.setState({mipLevel})}
             />
             {!this.props.texture.isCube && <ToolBar
                 tools={this.state.tools}
@@ -257,7 +288,7 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             />}
             <ChannelsBar channels={this.state.channels} setChannels={(channels) => {this.setState({channels})}}/>
             <TextureCanvasComponent canvas2D={this._2DCanvas} canvas3D={this._3DCanvas} canvasUI={this._UICanvas} texture={this.props.texture}/>
-            <BottomBar name={this.props.url}/>
+            <BottomBar name={this.props.url} mipLevel={this.state.mipLevel} hasMips={!this.props.texture.noMipmap}/>
         </div>
     }
 }

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

@@ -1,6 +1,6 @@
 import * as React from 'react';
 import { SketchPicker } from 'react-color';
-import { IToolData, IToolType } from './textureEditorComponent';
+import { IToolData, IToolType, IMetadata } from './textureEditorComponent';
 
 export interface ITool extends IToolData {
     instance: IToolType;
@@ -11,7 +11,7 @@ interface IToolBarProps {
     addTool(url: string): void;
     changeTool(toolIndex : number): void;
     activeToolIndex : number;
-    metadata: any;
+    metadata: IMetadata;
     setMetadata(data : any): void;
 }
 
@@ -37,7 +37,7 @@ export class ToolBar extends React.Component<IToolBarProps, IToolBarState> {
     }
 
     computeRGBAColor() {
-        const opacityInt = Math.floor(this.props.metadata.opacity * 255);
+        const opacityInt = Math.floor(this.props.metadata.alpha * 255);
         const opacityHex = opacityInt.toString(16).padStart(2, '0');
         return `${this.props.metadata.color}${opacityHex}`;
     }
@@ -80,7 +80,7 @@ export class ToolBar extends React.Component<IToolBarProps, IToolBarState> {
             </div>
             <div id='color' onClick={() => this.setState({pickerOpen: !this.state.pickerOpen})} title='Color' className={`icon button${this.state.pickerOpen ? ` active` : ``}`}>
                 <div id='active-color-bg'>
-                    <div id='active-color' style={{backgroundColor: this.props.metadata.color, opacity: this.props.metadata.opacity}}></div>
+                    <div id='active-color' style={{backgroundColor: this.props.metadata.color, opacity: this.props.metadata.alpha}}></div>
                 </div>
             </div>
             {
@@ -94,7 +94,7 @@ export class ToolBar extends React.Component<IToolBarProps, IToolBarState> {
                     }}>
                     </div>
                     <div className='color-picker' ref={this._pickerRef}>
-                            <SketchPicker color={this.computeRGBAColor()}  onChange={color => this.props.setMetadata({color: color.hex, opacity: color.rgb.a})}/>
+                            <SketchPicker color={this.computeRGBAColor()}  onChange={color => this.props.setMetadata({color: color.hex, alpha: color.rgb.a})}/>
                     </div>
                 </>
             }