diff --git a/.vscode/settings.json b/.vscode/settings.json index 305652651..ff45a1acc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "workbench.colorTheme": "Solarized Light" + "workbench.colorTheme": "Solarized Dark" } \ No newline at end of file diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 05bd76d6f..b3b0b9f04 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,5 +1,17 @@ # Change Log +## v3.61.0 - May 18, 2023 + +* Checks if a scene file was generated by a newer and incompatible version of the editor. +* Shows Object List items in the Outline view. Allows ordering the items with the Up, Down, Top, Down, commands. +* A new game object scope: LOCAL. The LOCAL scope is now the default scope for objects and has the same meaning of METHOD scope before. Now the METHOD scope forces the creation of a variable for the object. +* Auto computes the middle-private nested prefabs. It doesn't require to declare a parent of a nested prefab as nested prefab. +* Improves Outline elements tagging. It uses tahs like `#prefab_inst` `#nested_prefab_inst` `#scope_local` `#scope_nested_prefab`... So you can search for it in the Outline filter box. +* Allows enabling input in objects. +* Adds the Name parameter to the Variable section of a nested prefab instance. +* [#282](https://github.com/PhaserEditor2D/PhaserEditor2D-v3/issues/282) Use full nested prefab path in user object variable properties. +* Excludes script node prefabs from the Blocks view when editing a non-script node scene. + ## v3.60.3 - Apr 27, 2023 * Fixes regression bug with the Play command. diff --git a/scripts/make-all-help-files.js b/scripts/make-all-help-files.js index 019f6ea36..2192f1fb3 100755 --- a/scripts/make-all-help-files.js +++ b/scripts/make-all-help-files.js @@ -209,6 +209,8 @@ utils.makeHelpFile([ "Phaser.GameObjects.Ellipse", "Phaser.GameObjects.Ellipse.smoothness", + "Phaser.GameObjects.Polygon", + "Phaser.GameObjects.Triangle", "Phaser.Geom.Triangle.x1", "Phaser.Geom.Triangle.y1", @@ -217,7 +219,21 @@ utils.makeHelpFile([ "Phaser.Geom.Triangle.x3", "Phaser.Geom.Triangle.y3", - "Phaser.GameObjects.Polygon", + "Phaser.Geom.Rectangle.x", + "Phaser.Geom.Rectangle.y", + "Phaser.Geom.Rectangle.width", + "Phaser.Geom.Rectangle.height", + + "Phaser.Geom.Ellipse.x", + "Phaser.Geom.Ellipse.y", + "Phaser.Geom.Ellipse.width", + "Phaser.Geom.Ellipse.height", + + "Phaser.Geom.Circle.x", + "Phaser.Geom.Circle.y", + "Phaser.Geom.Circle.radius", + + "Phaser.Input.InputPlugin.makePixelPerfect(alphaTolerance)", "Phaser.GameObjects.Layer", diff --git a/source/editor/plugins/colibri/src/ui/controls/Menu.ts b/source/editor/plugins/colibri/src/ui/controls/Menu.ts index 343bdff4f..29bc7c699 100644 --- a/source/editor/plugins/colibri/src/ui/controls/Menu.ts +++ b/source/editor/plugins/colibri/src/ui/controls/Menu.ts @@ -345,7 +345,7 @@ namespace colibri.ui.controls { if (menuRect.width > window.innerWidth - x || openLeft) { - this._element.style.left = targetRect.right - menuRect.width + "px"; + this._element.style.left = Math.max(0, targetRect.right - menuRect.width) + "px"; } if (menuRect.height > window.innerHeight - y) { diff --git a/source/editor/plugins/colibri/src/ui/controls/viewers/RenderCellArgs.ts b/source/editor/plugins/colibri/src/ui/controls/viewers/RenderCellArgs.ts index fbadc6e42..40c798d7e 100644 --- a/source/editor/plugins/colibri/src/ui/controls/viewers/RenderCellArgs.ts +++ b/source/editor/plugins/colibri/src/ui/controls/viewers/RenderCellArgs.ts @@ -1,20 +1,20 @@ namespace colibri.ui.controls.viewers { - export class RenderCellArgs { + export class RenderCellArgs { - constructor( - public canvasContext: CanvasRenderingContext2D, - public x: number, - public y: number, - public w: number, - public h: number, - public obj: any, - public viewer: Viewer, - public center = false) { - } + constructor( + public canvasContext: CanvasRenderingContext2D, + public x: number, + public y: number, + public w: number, + public h: number, + public obj: any, + public viewer: Viewer, + public center = false) { + } - clone() { - return new RenderCellArgs(this.canvasContext, this.x, this.y, this.w, this.h, this.obj, this.viewer, this.center); - } - } + clone() { + return new RenderCellArgs(this.canvasContext, this.x, this.y, this.w, this.h, this.obj, this.viewer, this.center); + } + } } \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.ide/src/IDEPlugin.ts b/source/editor/plugins/phasereditor2d.ide/src/IDEPlugin.ts index f09134110..d2ba894aa 100644 --- a/source/editor/plugins/phasereditor2d.ide/src/IDEPlugin.ts +++ b/source/editor/plugins/phasereditor2d.ide/src/IDEPlugin.ts @@ -133,6 +133,8 @@ namespace phasereditor2d.ide { openBrowser(url: string) { + console.log("Opening browser for: " + url); + colibri.Platform.onElectron(electron => { colibri.core.io.apiRequest("OpenBrowser", { url }); @@ -149,17 +151,20 @@ namespace phasereditor2d.ide { const search = startScene ? `?start=${startScene}` : ""; - const url = (config.playUrl || colibri.ui.ide.FileUtils.getRoot().getExternalUrl()) - + search; + let url: string; - colibri.Platform.onElectron(electron => { + if (config.playUrl) { - colibri.core.io.apiRequest("OpenBrowser", { url: config.playUrl }); + url = config.playUrl + search; - }, () => { + } else { - controls.Controls.openUrlInNewPage(url); - }); + const {protocol, host} = window.location; + + url = `${protocol}//${host}/editor/external/${search}`; + } + + this.openBrowser(url); } async requestUpdateAvailable() { @@ -317,7 +322,7 @@ namespace phasereditor2d.ide { /* program entry point */ - export const VER = "3.60.3"; + export const VER = "3.61.0"; async function main() { diff --git a/source/editor/plugins/phasereditor2d.scene/data/phaser-docs.json b/source/editor/plugins/phasereditor2d.scene/data/phaser-docs.json index 17bdf2dd9..ca6d2b0d3 100644 --- a/source/editor/plugins/phasereditor2d.scene/data/phaser-docs.json +++ b/source/editor/plugins/phasereditor2d.scene/data/phaser-docs.json @@ -80,11 +80,11 @@ "Phaser.GameObjects.BitmapText.dropShadowY": "The vertical offset of the drop shadow.\n\nYou can set this directly, or use `Phaser.GameObjects.BitmapText#setDropShadow`.", "Phaser.GameObjects.BitmapText.dropShadowColor": "The color of the drop shadow.\n\nYou can set this directly, or use `Phaser.GameObjects.BitmapText#setDropShadow`.", "Phaser.GameObjects.BitmapText.dropShadowAlpha": "The alpha value of the drop shadow.\n\nYou can set this directly, or use `Phaser.GameObjects.BitmapText#setDropShadow`.", - "Phaser.Tilemaps.Tilemap": "A Tilemap is a container for Tilemap data. This isn't a display object, rather, it holds data\nabout the map and allows you to add tilesets and tilemap layers to it. A map can have one or\nmore tilemap layers, which are the display objects that actually render the tiles.\n\nThe Tilemap data can be parsed from a Tiled JSON file, a CSV file or a 2D array. Tiled is a free\nsoftware package specifically for creating tile maps, and is available from:\nhttp://www.mapeditor.org\n\nAs of Phaser 3.50.0 the Tilemap API now supports the following types of map:\n\n1) Orthogonal\n2) Isometric\n3) Hexagonal\n4) Staggered\n\nPrior to this release, only orthogonal maps were supported.\n\nAnother large change in 3.50 was the consolidation of Tilemap Layers. Previously, you created\neither a Static or Dynamic Tilemap Layer. However, as of 3.50 the features of both have been\nmerged and the API simplified, so now there is just the single `TilemapLayer` class.\n\nA Tilemap has handy methods for getting and manipulating the tiles within a layer, allowing\nyou to build or modify the tilemap data at runtime.\n\nNote that all Tilemaps use a base tile size to calculate dimensions from, but that a\nTilemapLayer may have its own unique tile size that overrides this.\n\nAs of Phaser 3.21.0, if your tilemap includes layer groups (a feature of Tiled 1.2.0+) these\nwill be traversed and the following properties will impact children:\n\n- Opacity (blended with parent) and visibility (parent overrides child)\n- Vertical and horizontal offset\n\nThe grouping hierarchy is not preserved and all layers will be flattened into a single array.\n\nGroup layers are parsed during Tilemap construction but are discarded after parsing so dynamic\nlayers will NOT continue to be affected by a parent.\n\nTo avoid duplicate layer names, a layer that is a child of a group layer will have its parent\ngroup name prepended with a '/'. For example, consider a group called 'ParentGroup' with a\nchild called 'Layer 1'. In the Tilemap object, 'Layer 1' will have the name\n'ParentGroup/Layer 1'.", + "Phaser.Tilemaps.Tilemap": "A Tilemap is a container for Tilemap data. This isn't a display object, rather, it holds data\nabout the map and allows you to add tilesets and tilemap layers to it. A map can have one or\nmore tilemap layers, which are the display objects that actually render the tiles.\n\nThe Tilemap data can be parsed from a Tiled JSON file, a CSV file or a 2D array. Tiled is a free\nsoftware package specifically for creating tile maps, and is available from:\nhttp://www.mapeditor.org\n\nAs of Phaser 3.50.0 the Tilemap API now supports the following types of map:\n\n1) Orthogonal\n2) Isometric\n3) Hexagonal\n4) Staggered\n\nPrior to this release, only orthogonal maps were supported.\n\nAnother large change in 3.50 was the consolidation of Tilemap Layers. Previously, you created\neither a Static or Dynamic Tilemap Layer. However, as of 3.50 the features of both have been\nmerged and the API simplified, so now there is just the single `TilemapLayer` class.\n\nA Tilemap has handy methods for getting and manipulating the tiles within a layer, allowing\nyou to build or modify the tilemap data at runtime.\n\nNote that all Tilemaps use a base tile size to calculate dimensions from, but that a\nTilemapLayer may have its own unique tile size that overrides this.\n\nAs of Phaser 3.21.0, if your tilemap includes layer groups (a feature of Tiled 1.2.0+) these\nwill be traversed and the following properties will impact children:\n\n- Opacity (blended with parent) and visibility (parent overrides child)\n- Vertical and horizontal offset\n\nThe grouping hierarchy is not preserved and all layers will be flattened into a single array.\n\nGroup layers are parsed during Tilemap construction but are discarded after parsing so dynamic\nlayers will NOT continue to be affected by a parent.\n\nTo avoid duplicate layer names, a layer that is a child of a group layer will have its parent\ngroup name prepended with a '/'. For example, consider a group called 'ParentGroup' with a\nchild called 'Layer 1'. In the Tilemap object, 'Layer 1' will have the name\n'ParentGroup/Layer 1'.\n\nThe Phaser Tiled Parser does **not** support the 'Collection of Images' feature for a Tileset.\nYou must ensure all of your tiles are contained in a single tileset image file (per layer)\nand have this 'embedded' in the exported Tiled JSON map data.", "Phaser.Tilemaps.Tilemap.tileWidth": "The base width of a tile in pixels. Note that individual layers may have a different tile\nwidth.", "Phaser.Tilemaps.Tilemap.tileHeight": "The base height of a tile in pixels. Note that individual layers may have a different\ntile height.", "Phaser.GameObjects.GameObjectFactory.tilemap(key)": "The key in the Phaser cache that corresponds to the loaded tilemap data.", - "Phaser.Tilemaps.Tileset": "A Tileset is a combination of an image containing the tiles and a container for data about\neach tile.", + "Phaser.Tilemaps.Tileset": "A Tileset is a combination of a single image containing the tiles and a container for data about\neach tile.", "Phaser.Tilemaps.Tileset.name": "The name of the Tileset.", "Phaser.Tilemaps.Tileset.image": "The cached image that contains the individual tiles. Use setImage to set.", "Phaser.Tilemaps.Tileset.tileWidth": "The width of each tile (in pixels). Use setTileSize to change.", @@ -111,6 +111,7 @@ "Phaser.GameObjects.Rectangle": "The Rectangle Shape is a Game Object that can be added to a Scene, Group or Container. You can\ntreat it like any other Game Object in your game, such as tweening it, scaling it, or enabling\nit for input or physics. It provides a quick and easy way for you to render this shape in your\ngame without using a texture, while still taking advantage of being fully batched in WebGL.\n\nThis shape supports both fill and stroke colors.\n\nYou can change the size of the rectangle by changing the `width` and `height` properties.", "Phaser.GameObjects.Ellipse": "The Ellipse Shape is a Game Object that can be added to a Scene, Group or Container. You can\ntreat it like any other Game Object in your game, such as tweening it, scaling it, or enabling\nit for input or physics. It provides a quick and easy way for you to render this shape in your\ngame without using a texture, while still taking advantage of being fully batched in WebGL.\n\nThis shape supports both fill and stroke colors.\n\nWhen it renders it displays an ellipse shape. You can control the width and height of the ellipse.\nIf the width and height match it will render as a circle. If the width is less than the height,\nit will look more like an egg shape.\n\nThe Ellipse shape also has a `smoothness` property and corresponding `setSmoothness` method.\nThis allows you to control how smooth the shape renders in WebGL, by controlling the number of iterations\nthat take place during construction. Increase and decrease the default value for smoother, or more\njagged, shapes.", "Phaser.GameObjects.Ellipse.smoothness": "The smoothness of the ellipse. The number of points used when rendering it.\nIncrease this value for a smoother ellipse, at the cost of more polygons being rendered.", + "Phaser.GameObjects.Polygon": "The Polygon Shape is a Game Object that can be added to a Scene, Group or Container. You can\ntreat it like any other Game Object in your game, such as tweening it, scaling it, or enabling\nit for input or physics. It provides a quick and easy way for you to render this shape in your\ngame without using a texture, while still taking advantage of being fully batched in WebGL.\n\nThis shape supports both fill and stroke colors.\n\nThe Polygon Shape is created by providing a list of points, which are then used to create an\ninternal Polygon geometry object. The points can be set from a variety of formats:\n\n- A string containing paired values separated by a single space: `'40 0 40 20 100 20 100 80 40 80 40 100 0 50'`\n- An array of Point or Vector2 objects: `[new Phaser.Math.Vector2(x1, y1), ...]`\n- An array of objects with public x/y properties: `[obj1, obj2, ...]`\n- An array of paired numbers that represent point coordinates: `[x1,y1, x2,y2, ...]`\n- An array of arrays with two elements representing x/y coordinates: `[[x1, y1], [x2, y2], ...]`\n\nBy default the `x` and `y` coordinates of this Shape refer to the center of it. However, depending\non the coordinates of the points provided, the final shape may be rendered offset from its origin.", "Phaser.GameObjects.Triangle": "The Triangle Shape is a Game Object that can be added to a Scene, Group or Container. You can\ntreat it like any other Game Object in your game, such as tweening it, scaling it, or enabling\nit for input or physics. It provides a quick and easy way for you to render this shape in your\ngame without using a texture, while still taking advantage of being fully batched in WebGL.\n\nThis shape supports both fill and stroke colors.\n\nThe Triangle consists of 3 lines, joining up to form a triangular shape. You can control the\nposition of each point of these lines. The triangle is always closed and cannot have an open\nface. If you require that, consider using a Polygon instead.", "Phaser.Geom.Triangle.x1": "`x` coordinate of the first point.", "Phaser.Geom.Triangle.y1": "`y` coordinate of the first point.", @@ -118,7 +119,18 @@ "Phaser.Geom.Triangle.y2": "`y` coordinate of the second point.", "Phaser.Geom.Triangle.x3": "`x` coordinate of the third point.", "Phaser.Geom.Triangle.y3": "`y` coordinate of the third point.", - "Phaser.GameObjects.Polygon": "The Polygon Shape is a Game Object that can be added to a Scene, Group or Container. You can\ntreat it like any other Game Object in your game, such as tweening it, scaling it, or enabling\nit for input or physics. It provides a quick and easy way for you to render this shape in your\ngame without using a texture, while still taking advantage of being fully batched in WebGL.\n\nThis shape supports both fill and stroke colors.\n\nThe Polygon Shape is created by providing a list of points, which are then used to create an\ninternal Polygon geometry object. The points can be set from a variety of formats:\n\n- A string containing paired values separated by a single space: `'40 0 40 20 100 20 100 80 40 80 40 100 0 50'`\n- An array of Point or Vector2 objects: `[new Phaser.Math.Vector2(x1, y1), ...]`\n- An array of objects with public x/y properties: `[obj1, obj2, ...]`\n- An array of paired numbers that represent point coordinates: `[x1,y1, x2,y2, ...]`\n- An array of arrays with two elements representing x/y coordinates: `[[x1, y1], [x2, y2], ...]`\n\nBy default the `x` and `y` coordinates of this Shape refer to the center of it. However, depending\non the coordinates of the points provided, the final shape may be rendered offset from its origin.", + "Phaser.Geom.Rectangle.x": "The X coordinate of the top left corner of the Rectangle.", + "Phaser.Geom.Rectangle.y": "The Y coordinate of the top left corner of the Rectangle.", + "Phaser.Geom.Rectangle.width": "The width of the Rectangle, i.e. the distance between its left side (defined by `x`) and its right side.", + "Phaser.Geom.Rectangle.height": "The height of the Rectangle, i.e. the distance between its top side (defined by `y`) and its bottom side.", + "Phaser.Geom.Ellipse.x": "The x position of the center of the ellipse.", + "Phaser.Geom.Ellipse.y": "The y position of the center of the ellipse.", + "Phaser.Geom.Ellipse.width": "The width of the ellipse.", + "Phaser.Geom.Ellipse.height": "The height of the ellipse.", + "Phaser.Geom.Circle.x": "The x position of the center of the circle.", + "Phaser.Geom.Circle.y": "The y position of the center of the circle.", + "Phaser.Geom.Circle.radius": "The radius of the Circle.", + "Phaser.Input.InputPlugin.makePixelPerfect(alphaTolerance)": "The alpha level that the pixel should be above to be included as a successful interaction.", "Phaser.GameObjects.Layer": "A Layer Game Object.\n\nA Layer is a special type of Game Object that acts as a Display List. You can add any type of Game Object\nto a Layer, just as you would to a Scene. Layers can be used to visually group together 'layers' of Game\nObjects:\n\n```javascript\nconst spaceman = this.add.sprite(150, 300, 'spaceman');\nconst bunny = this.add.sprite(400, 300, 'bunny');\nconst elephant = this.add.sprite(650, 300, 'elephant');\n\nconst layer = this.add.layer();\n\nlayer.add([ spaceman, bunny, elephant ]);\n```\n\nThe 3 sprites in the example above will now be managed by the Layer they were added to. Therefore,\nif you then set `layer.setVisible(false)` they would all vanish from the display.\n\nYou can also control the depth of the Game Objects within the Layer. For example, calling the\n`setDepth` method of a child of a Layer will allow you to adjust the depth of that child _within the\nLayer itself_, rather than the whole Scene. The Layer, too, can have its depth set as well.\n\nThe Layer class also offers many different methods for manipulating the list, such as the\nmethods `moveUp`, `moveDown`, `sendToBack`, `bringToTop` and so on. These allow you to change the\ndisplay list position of the Layers children, causing it to adjust the order in which they are\nrendered. Using `setDepth` on a child allows you to override this.\n\nLayers can have Post FX Pipelines set, which allows you to easily enable a post pipeline across\na whole range of children, which, depending on the effect, can often be far more efficient that doing so\non a per-child basis.\n\nLayers have no position or size within the Scene. This means you cannot enable a Layer for\nphysics or input, or change the position, rotation or scale of a Layer. They also have no scroll\nfactor, texture, tint, origin, crop or bounds.\n\nIf you need those kind of features then you should use a Container instead. Containers can be added\nto Layers, but Layers cannot be added to Containers.\n\nHowever, you can set the Alpha, Blend Mode, Depth, Mask and Visible state of a Layer. These settings\nwill impact all children being rendered by the Layer.", "Phaser.Physics.Arcade.Collider": "An Arcade Physics Collider will automatically check for collision, or overlaps, between two objects\nevery step. If a collision, or overlap, occurs it will invoke the given callbacks.", "Phaser.Physics.Arcade.Image": "An Arcade Physics Image is an Image with an Arcade Physics body and related components.\nThe body can be dynamic or static.\n\nThe main difference between an Arcade Image and an Arcade Sprite is that you cannot animate an Arcade Image.", @@ -129,7 +141,7 @@ "Phaser.Physics.Arcade.Body.radius": "If this Body is circular, this is the unscaled radius of the Body, as set by setCircle(), in source pixels.\nThe true radius is equal to `halfWidth`.", "Phaser.Physics.Arcade.Body.moves": "Whether the Body's position and rotation are affected by its velocity, acceleration, drag, and gravity.", "Phaser.Physics.Arcade.Body.velocity": "The Body's velocity, in pixels per second.", - "Phaser.Physics.Arcade.Body.maxVelocity": "The Body's absolute maximum velocity, in pixels per second.\nThe horizontal and vertical components are applied separately.", + "Phaser.Physics.Arcade.Body.maxVelocity": "The absolute maximum velocity of this body, in pixels per second.\nThe horizontal and vertical components are applied separately.", "Phaser.Physics.Arcade.Body.maxSpeed": "The maximum speed this Body is allowed to reach, in pixels per second.\n\nIf not negative it limits the scalar value of speed.\n\nAny negative value means no maximum is being applied (the default).", "Phaser.Physics.Arcade.Body.allowGravity": "Whether this Body's position is affected by gravity (local or world).", "Phaser.Physics.Arcade.Body.gravity": "Acceleration due to gravity (specific to this Body), in pixels per second squared.\nTotal gravity is the sum of this vector and the simulation's `gravity`.", diff --git a/source/editor/plugins/phasereditor2d.scene/src/ScenePlugin.ts b/source/editor/plugins/phasereditor2d.scene/src/ScenePlugin.ts index bb55933dd..7f42def3e 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ScenePlugin.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ScenePlugin.ts @@ -374,6 +374,12 @@ namespace phasereditor2d.scene { page => new ui.sceneobjects.TileSpriteSection(page), page => new ui.sceneobjects.NineSliceSection(page), page => new ui.sceneobjects.ThreeSliceSection(page), + page => new ui.sceneobjects.HitAreaSection(page), + page => new ui.sceneobjects.RectangleHitAreaSection(page), + page => new ui.sceneobjects.CircleHitAreaSection(page), + page => new ui.sceneobjects.EllipseHitAreaSection(page), + page => new ui.sceneobjects.PolygonHitAreaSection(page), + page => new ui.sceneobjects.PixelPerfectHitAreaSection(page), page => new ui.sceneobjects.ArcadeBodySection(page), page => new ui.sceneobjects.ArcadeGeometrySection(page), page => new ui.sceneobjects.ArcadeBodyMovementSection(page), @@ -382,7 +388,7 @@ namespace phasereditor2d.scene { page => new ui.sceneobjects.TextContentSection(page), page => new ui.sceneobjects.TextSection(page), page => new ui.sceneobjects.BitmapTextSection(page), - page => new ui.sceneobjects.ListSection(page), + page => new ui.sceneobjects.ObjectListItemSection(page), page => new ui.sceneobjects.ScenePlainObjectVariableSection(page), page => new ui.sceneobjects.TilemapSection(page), page => new ui.sceneobjects.TilesetSection(page), @@ -404,11 +410,12 @@ namespace phasereditor2d.scene { new ui.sceneobjects.ScaleTool(), new ui.sceneobjects.OriginTool(), new ui.sceneobjects.SizeTool(), + new ui.sceneobjects.EditHitAreaTool(), new ui.sceneobjects.ArcadeBodyTool(), new ui.sceneobjects.SliceTool(), new ui.sceneobjects.PolygonTool(), new ui.sceneobjects.SelectionRegionTool(), - new ui.sceneobjects.PanTool(), + new ui.sceneobjects.PanTool() )); // files view sections @@ -489,7 +496,7 @@ namespace phasereditor2d.scene { getPrefabColor() { - return colibri.ui.controls.Controls.getTheme().dark? "lightGreen" : "darkGreen"; + return colibri.ui.controls.Controls.getTheme().dark ? "lightGreen" : "darkGreen"; } getNestedPrefabColor() { @@ -638,8 +645,32 @@ namespace phasereditor2d.scene { dlg.close(); } + private _showIncompatibilityMessage = true; + runSceneDataMigrations(sceneData: core.json.ISceneData) { + // check scene data min supported version + + if (this._showIncompatibilityMessage) { + + const version = sceneData.meta.version; + + if (version) { + + if (version > ui.Scene.CURRENT_VERSION) { + + alert(` + The project contains scene files created by newer versions of the editor. + You should update the editor. + `); + } + } + + this._showIncompatibilityMessage = false; + } + + // check migrations + const migrations = colibri.Platform.getExtensionRegistry() .getExtensions(ui.SceneDataMigrationExtension.POINT_ID); diff --git a/source/editor/plugins/phasereditor2d.scene/src/core/code/SceneCodeDOMBuilder.ts b/source/editor/plugins/phasereditor2d.scene/src/core/code/SceneCodeDOMBuilder.ts index d54e5d20a..e24e970e7 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/core/code/SceneCodeDOMBuilder.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/core/code/SceneCodeDOMBuilder.ts @@ -223,17 +223,16 @@ namespace phasereditor2d.scene.core.code { for (const obj of this._scene.getPlainObjects()) { - const editorSupport = obj.getEditorSupport(); - const scope = editorSupport.getScope(); + const objES = obj.getEditorSupport(); - if (scope !== ui.sceneobjects.ObjectScope.METHOD) { + if (objES.isClassOrPublicScope()) { - const objType = editorSupport.getPhaserType(); + const objType = objES.getPhaserType(); const dom = new FieldDeclCodeDOM( - formatToValidVarName(editorSupport.getLabel()), + formatToValidVarName(objES.getLabel()), objType, - scope === ui.sceneobjects.ObjectScope.PUBLIC); + objES.isPublicScope()); dom.setAllowUndefined(!this._scene.isPrefabSceneType()); @@ -248,14 +247,14 @@ namespace phasereditor2d.scene.core.code { for (const list of this._scene.getObjectLists().getLists()) { - if (list.getScope() !== ui.sceneobjects.ObjectScope.METHOD) { + if (ui.sceneobjects.isClassOrPublicScope(list.getScope())) { const listType = list.inferType(objMap); const dom = new FieldDeclCodeDOM( formatToValidVarName(list.getLabel()), listType, - list.getScope() === ui.sceneobjects.ObjectScope.PUBLIC); + ui.sceneobjects.isPublicScope(list.getScope())); dom.setAllowUndefined(!this._scene.isPrefabSceneType()); @@ -282,7 +281,7 @@ namespace phasereditor2d.scene.core.code { ? objES.getPrefabName() : (explicitType ? explicitType : objES.getPhaserType()); - const isPublic = objES.isPublic(); + const isPublic = objES.isPublicScope(); const field = new FieldDeclCodeDOM(varName, type, isPublic); // Allow undefined if the object is part of a scene. @@ -456,7 +455,8 @@ namespace phasereditor2d.scene.core.code { if (obj.getEditorSupport().isMutableNestedPrefabInstance()) { - this.addCreateObjectCodeOfNestedPrefab(obj, createMethodDecl, lazyStatements); + // this.addCreateObjectCodeOfNestedPrefab(obj, createMethodDecl, lazyStatements); + throw new Error("Assert: this code should not be reached."); } else { @@ -490,11 +490,11 @@ namespace phasereditor2d.scene.core.code { private addCreatePlainObjectCode( obj: ui.sceneobjects.IScenePlainObject, firstStatements: CodeDOM[], lazyStatements: CodeDOM[]) { - const objSupport = obj.getEditorSupport(); + const objES = obj.getEditorSupport(); - const varname = formatToValidVarName(objSupport.getLabel()); + const varname = formatToValidVarName(objES.getLabel()); - const result = objSupport.getExtension().buildCreateObjectWithFactoryCodeDOM({ + const result = objES.getExtension().buildCreateObjectWithFactoryCodeDOM({ gameObjectFactoryExpr: this._scene.isPrefabSceneType() ? "scene" : "this", obj: obj, varname @@ -519,7 +519,7 @@ namespace phasereditor2d.scene.core.code { const objectFactoryMethodCall = result.objectFactoryMethodCall; - if (!objSupport.isMethodScope()) { + if (objES.isClassOrPublicScope() || objES.isMethodScope()) { objectFactoryMethodCall.setDeclareReturnToVar(true); } @@ -590,28 +590,6 @@ namespace phasereditor2d.scene.core.code { fields.push(dom); } } - - // for (const obj of children) { - - // const objES = obj.getEditorSupport(); - - // if (!objES.isMethodScope() && prefabObj !== obj) { - - // const varname = formatToValidVarName(objES.getLabel()); - - // const dom = new AssignPropertyCodeDOM(varname, "this"); - // dom.value(varname); - - // fields.push(dom); - // } - - // const walkingChildren = this.getWalkingChildren(obj); - - // if (walkingChildren) { - - // this.addFieldInitCode_GameObjects(fields, prefabObj, walkingChildren); - // } - // } } private addFieldInitCode(body: CodeDOM[]) { @@ -624,11 +602,11 @@ namespace phasereditor2d.scene.core.code { for (const obj of this._scene.getPlainObjects()) { - const editorSupport = obj.getEditorSupport(); + const objES = obj.getEditorSupport(); - if (editorSupport.getScope() !== ui.sceneobjects.ObjectScope.METHOD) { + if (objES.isClassOrPublicScope()) { - const varname = formatToValidVarName(editorSupport.getLabel()); + const varname = formatToValidVarName(objES.getLabel()); const dom = new AssignPropertyCodeDOM(varname, "this"); @@ -640,7 +618,7 @@ namespace phasereditor2d.scene.core.code { for (const list of this._scene.getObjectLists().getLists()) { - if (list.getScope() !== ui.sceneobjects.ObjectScope.METHOD) { + if (ui.sceneobjects.isClassOrPublicScope(list.getScope())) { const varname = formatToValidVarName(list.getLabel()); @@ -661,7 +639,7 @@ namespace phasereditor2d.scene.core.code { private addCreateObjectCodeOfNestedPrefab(obj: ISceneGameObject, createMethodDecl: MethodDeclCodeDOM, lazyStatements: CodeDOM[]) { - const varname = this.getPrefabInstanceVarName(obj); + const varname = SceneCodeDOMBuilder.getPrefabInstanceVarName(obj); const result = this.buildSetObjectProperties({ obj, @@ -709,7 +687,7 @@ namespace phasereditor2d.scene.core.code { && objParent === this._scene.getPrefabObject(); parentVarName = parentIsPrefabObject ? "this" - : this.getPrefabInstanceVarName(objParent); + : SceneCodeDOMBuilder.getPrefabInstanceVarName(objParent); } // the script nodes require using the varname of the parents @@ -813,6 +791,11 @@ namespace phasereditor2d.scene.core.code { createObjectMethodCall.setDeclareReturnToVar(true); } + if (objES.isMethodScope()) { + // it is method scope... the user wants a variable! + createObjectMethodCall.setDeclareReturnToVar(true); + } + lazyStatements.push(...result.lazyStatements); createMethodDecl.getBody().push(...result.statements); @@ -872,9 +855,10 @@ namespace phasereditor2d.scene.core.code { // set var flags - if (!objES.isMethodScope()) { + if (objES.isClassOrPublicScope()) { createObjectMethodCall.setDeclareReturnToVar(true); + this._objectsToFieldList.push(obj); } @@ -897,7 +881,7 @@ namespace phasereditor2d.scene.core.code { .join(" & "); } - private getPrefabInstanceVarName(obj: ISceneGameObject): string { + static getPrefabInstanceVarName(obj: ISceneGameObject): string { const objES = obj.getEditorSupport(); @@ -920,7 +904,7 @@ namespace phasereditor2d.scene.core.code { return varName; } - private findPrefabInstanceWhereTheGivenObjectIsDefined(obj: ui.sceneobjects.ISceneGameObject): ui.sceneobjects.ISceneGameObject { + private static findPrefabInstanceWhereTheGivenObjectIsDefined(obj: ui.sceneobjects.ISceneGameObject): ui.sceneobjects.ISceneGameObject { const objES = obj.getEditorSupport(); @@ -935,7 +919,7 @@ namespace phasereditor2d.scene.core.code { return this.findPrefabInstanceOfFile(parent, objPrefabFile); } - private findPrefabInstanceOfFile(obj: ISceneGameObject, targetPrefaFile: io.FilePath): ISceneGameObject { + private static findPrefabInstanceOfFile(obj: ISceneGameObject, targetPrefaFile: io.FilePath): ISceneGameObject { const finder = ScenePlugin.getInstance().getSceneFinder(); diff --git a/source/editor/plugins/phasereditor2d.scene/src/core/json/IObjectData.ts b/source/editor/plugins/phasereditor2d.scene/src/core/json/IObjectData.ts index e8bd9fd1a..cbe2dea5c 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/core/json/IObjectData.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/core/json/IObjectData.ts @@ -8,6 +8,7 @@ namespace phasereditor2d.scene.core.json { label: string; unlock?: string[]; scope?: ui.sceneobjects.ObjectScope; + private_np?: boolean; list?: IObjectData[]; nestedPrefabs?: IObjectData[]; } diff --git a/source/editor/plugins/phasereditor2d.scene/src/core/json/SceneFinder.ts b/source/editor/plugins/phasereditor2d.scene/src/core/json/SceneFinder.ts index ebce30ae2..b5ba65213 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/core/json/SceneFinder.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/core/json/SceneFinder.ts @@ -24,8 +24,6 @@ namespace phasereditor2d.scene.core.json { async preload(monitor: controls.IProgressMonitor) { await this._finder.preload(monitor); - - this._finder.runMigrations(); } } @@ -73,7 +71,6 @@ namespace phasereditor2d.scene.core.json { colibri.ui.ide.FileUtils.getFileStorage().addChangeListener(async (e) => { await this.handleStorageChange(e); - }); } @@ -135,14 +132,6 @@ namespace phasereditor2d.scene.core.json { monitor.step(); } - runMigrations() { - - for (const data of this._sceneFilename_Data_Map.values()) { - - ScenePlugin.getInstance().runSceneDataMigrations(data); - } - } - private async preloadComponentsFiles(monitor: controls.IProgressMonitor): Promise { const compFiles = []; @@ -214,7 +203,7 @@ namespace phasereditor2d.scene.core.json { const data = JSON.parse(content) as ISceneData; - // await ScenePlugin.getInstance().runSceneDataMigrations(data); + ScenePlugin.getInstance().runSceneDataMigrations(data); sceneFilename_Data_Map.set(file.getFullName(), data); { @@ -225,7 +214,6 @@ namespace phasereditor2d.scene.core.json { if (data.id) { - if (sceneIdSet.has(data.id)) { const mappedFile = prefabId_File_Map.get(data.id); @@ -257,6 +245,7 @@ namespace phasereditor2d.scene.core.json { sceneFiles.push(file); } catch (e) { + console.error(`SceneDataTable: parsing file ${file.getFullName()}. Error: ${(e as Error).message}`); } @@ -283,7 +272,7 @@ namespace phasereditor2d.scene.core.json { for (const c of objData.list) { - if (c.scope === ui.sceneobjects.ObjectScope.NESTED_PREFAB) { + if (c.private_np || ui.sceneobjects.isNestedPrefabScope(c.scope)) { prefabObjectId_ObjectData_Map.set(c.id, c); prefabId_File_Map.set(c.id, file); diff --git a/source/editor/plugins/phasereditor2d.scene/src/core/json/SceneWriter.ts b/source/editor/plugins/phasereditor2d.scene/src/core/json/SceneWriter.ts index 8325fe47c..14cd95380 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/core/json/SceneWriter.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/core/json/SceneWriter.ts @@ -41,10 +41,32 @@ namespace phasereditor2d.scene.core.json { // display list + for (const obj of this._scene.getGameObjects()) { + + this.computeDataField_private_np(obj); + } + for (const obj of this._scene.getGameObjects()) { const objData = {} as IObjectData; - obj.getEditorSupport().writeJSON(objData); + + const objES = obj.getEditorSupport(); + + // write the `private_np` field + + const private_np = objES._private_np; + + if (private_np) { + + objData.private_np = true; + } + + // serialize all the other obj data + + objES.writeJSON(objData); + + // add the data to the list + sceneData.displayList.push(objData); } @@ -62,6 +84,42 @@ namespace phasereditor2d.scene.core.json { return sceneData; } + private computeDataField_private_np(obj: ui.sceneobjects.ISceneGameObject) { + + const objES = obj.getEditorSupport(); + + for (const child of objES.getObjectChildren()) { + + this.computeDataField_private_np(child); + } + + if (!objES.isPrefabInstancePart() + && !objES.isNestedPrefabScope() + && !objES.isScenePrefabObject()) { + + // ok, it is an object in the scene which + // I don't know if it is a private nested prefab (`private_np`) + // let's investigate with the kids + + for (const child of objES.getObjectChildren()) { + + const childES = child.getEditorSupport(); + + if ( + // the child is flagged as private_np + childES._private_np + // or the child is is a common object with a NESTED_PREFAB scope + || (childES.isNestedPrefabScope() && !childES.isPrefabInstancePart())) { + + // flag the object and stop the search + objES._private_np = true; + + break; + } + } + } + } + toString(): string { const json = this.toJSON(); diff --git a/source/editor/plugins/phasereditor2d.scene/src/core/migrations/TextAlignMigration.ts b/source/editor/plugins/phasereditor2d.scene/src/core/migrations/TextAlignMigration.ts index 5565dc830..0e1a67ffa 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/core/migrations/TextAlignMigration.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/core/migrations/TextAlignMigration.ts @@ -5,8 +5,6 @@ namespace phasereditor2d.scene.core.migrations { migrate(data: json.ISceneData) { - console.log("Migrating: Text align"); - this.migrateList(data.displayList); } @@ -23,6 +21,8 @@ namespace phasereditor2d.scene.core.migrations { const alignProp = ui.sceneobjects.TextComponent.align; objData.align = alignProp.values[objData.align] ?? alignProp.defValue; + + console.log(`Migrate Text align to ${objData.align} [${objData.id}]`); } } diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/Scene.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/Scene.ts index 6e983b934..2e91883c5 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/Scene.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/Scene.ts @@ -3,7 +3,7 @@ namespace phasereditor2d.scene.ui { export class Scene extends BaseScene { - static CURRENT_VERSION = 3; + static CURRENT_VERSION = 4; private _id: string; private _sceneType: core.json.SceneType; diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/blocks/SceneEditorBlocksContentProvider.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/blocks/SceneEditorBlocksContentProvider.ts index f660991e9..0b19af699 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/blocks/SceneEditorBlocksContentProvider.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/blocks/SceneEditorBlocksContentProvider.ts @@ -16,8 +16,6 @@ namespace phasereditor2d.scene.ui.blocks { const grouping = pack.ui.viewers.AssetPackGrouping; - const SCRIPTS_CATEGORY = "Scripts"; - export class SceneEditorBlocksContentProvider extends pack.ui.viewers.AssetPackContentProvider { private _getPacks: () => pack.core.AssetPack[]; @@ -45,12 +43,9 @@ namespace phasereditor2d.scene.ui.blocks { if (this._editor.getScene().isScriptNodePrefabScene()) { - const sceneFinder = ScenePlugin.getInstance().getSceneFinder(); - return [ sceneobjects.ScriptNodeExtension.getInstance(), - ...this.getSceneFiles("prefabs") - .filter(f => sceneFinder.isScriptPrefabFile(f)) + ...this.getPrefabFiles("scripts") ]; } @@ -68,7 +63,7 @@ namespace phasereditor2d.scene.ui.blocks { if (groupingType === grouping.GROUP_ASSETS_BY_LOCATION) { const files = colibri.ui.ide.FileUtils.distinct( - this.getSceneFiles("prefabs").map(f => f.getParent())); + this.getPrefabFiles("prefabs").map(f => f.getParent())); return files; } @@ -113,7 +108,7 @@ namespace phasereditor2d.scene.ui.blocks { return [ BUILTIN_SECTION, ...colibri.ui.ide.FileUtils.distinct([ - ...this.getSceneFiles().map(f => f.getParent()), + ...this.getPrefabFiles().map(f => f.getParent()), ...packFolders]) ] } @@ -121,13 +116,28 @@ namespace phasereditor2d.scene.ui.blocks { return []; } - private getSceneFiles(sceneType: "prefabs" | "all" = "all") { + private getPrefabFiles(sceneType: "prefabs" | "scripts" = "prefabs") { const finder = ScenePlugin.getInstance().getSceneFinder(); - const files = (sceneType === "prefabs" ? finder.getPrefabFiles() : finder.getSceneFiles()); + let files: io.FilePath[] = []; + + switch (sceneType) { + + case "prefabs": + + files = finder.getPrefabFiles().filter(f => !finder.isScriptPrefabFile(f)); + break; + + case "scripts": + + files = finder.getPrefabFiles().filter(f => finder.isScriptPrefabFile(f)); + break; + } + + files = files.filter(file => SceneMaker.acceptDropFile(file, this._editor.getInput())); - return files.filter(file => SceneMaker.acceptDropFile(file, this._editor.getInput())); + return files; } getChildren(parent: any): any[] { @@ -136,7 +146,7 @@ namespace phasereditor2d.scene.ui.blocks { const finder = ScenePlugin.getInstance().getSceneFinder(); - return this.getSceneFiles().filter(file => finder.getScenePhaserType(file) === parent.getPhaserType()); + return this.getPrefabFiles().filter(file => finder.getScenePhaserType(file) === parent.getPhaserType()); } if (parent instanceof pack.core.AssetPack) { @@ -182,7 +192,7 @@ namespace phasereditor2d.scene.ui.blocks { case PREFAB_SECTION: - const files = this.getSceneFiles(); + const files = this.getPrefabFiles(); return files; } @@ -197,10 +207,10 @@ namespace phasereditor2d.scene.ui.blocks { if (tabSection === TAB_SECTION_PREFABS) { - return this.getSceneFiles("prefabs").filter(f => f.getParent() === parent); + return this.getPrefabFiles("prefabs").filter(f => f.getParent() === parent); } - const scenes = this.getSceneFiles().filter(f => f.getParent() === parent); + const scenes = this.getPrefabFiles().filter(f => f.getParent() === parent); const items = this.getPackItems().filter(item => grouping.getItemFolder(item) === parent); return [...scenes, ...items]; diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SceneEditor.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SceneEditor.ts index bd78ff7d3..3835b2465 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SceneEditor.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SceneEditor.ts @@ -383,7 +383,7 @@ namespace phasereditor2d.scene.ui.editor { if (file) { if (ScenePlugin.getInstance().getSceneFinder().isScriptPrefabFile(file)) { - + return ScenePlugin.getInstance().getIcon(ICON_BUILD); } @@ -568,14 +568,21 @@ namespace phasereditor2d.scene.ui.editor { return this.getSelection() - .filter(obj => sceneobjects.isGameObject(obj)) as any; + .filter(obj => sceneobjects.isGameObject(obj)); } getSelectedLists(): sceneobjects.ObjectList[] { return this.getSelection() - .filter(obj => obj instanceof sceneobjects.ObjectList) as any; + .filter(obj => obj instanceof sceneobjects.ObjectList); + } + + getSelectedListItems(): sceneobjects.ObjectListItem[] { + + return this.getSelection() + + .filter(obj => obj instanceof sceneobjects.ObjectListItem); } getSelectedPlainObjects(): sceneobjects.IScenePlainObject[] { @@ -901,7 +908,7 @@ namespace phasereditor2d.scene.ui.editor { } catch (e) { console.log(e); - + alert(e.message); } } diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SceneEditorMenuCreator.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SceneEditorMenuCreator.ts index ef0d6a1b9..4c4b68a56 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SceneEditorMenuCreator.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SceneEditorMenuCreator.ts @@ -41,6 +41,8 @@ namespace phasereditor2d.scene.ui.editor { menu.addMenu(this.createDepthMenu()); + menu.addMenu(this.createListMenu()); + menu.addSeparator(); menu.addMenu(this.createSnappingMenu()); @@ -168,7 +170,6 @@ namespace phasereditor2d.scene.ui.editor { return menu; } - private createDepthMenu(): controls.Menu { const menu = new controls.Menu("Depth"); @@ -183,6 +184,20 @@ namespace phasereditor2d.scene.ui.editor { return menu; } + private createListMenu(): controls.Menu { + + const menu = new controls.Menu("Object List"); + + for (const move of ["Up", "Down", "Top", "Bottom"]) { + + const id = "phasereditor2d.scene.ui.editor.commands.ListOrder" + move; + + menu.addCommand(id); + } + + return menu; + } + private createEditMenu() { const menu = new controls.Menu("Edit"); diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SelectionManager.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SelectionManager.ts index 0bd3e0e49..af2280d1a 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SelectionManager.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/SelectionManager.ts @@ -29,11 +29,14 @@ namespace phasereditor2d.scene.ui.editor { .filter(obj => obj instanceof sceneobjects.ObjectList) .map(obj => (obj as sceneobjects.ObjectList).getId())); + list.push(...selection + .filter(obj => obj instanceof sceneobjects.ObjectListItem) + .map(obj => (obj as sceneobjects.ObjectListItem).getId())) + list.push(...selection .filter(i => i instanceof sceneobjects.UserComponentNode) .map((i: sceneobjects.UserComponentNode) => i.getId())); - list.push(...selection .filter(obj => obj instanceof sceneobjects.UserProperty) .map((p: sceneobjects.UserProperty) => `prefabProperty#${p.getName()}`)) @@ -66,6 +69,11 @@ namespace phasereditor2d.scene.ui.editor { for (const list of this._editor.getScene().getObjectLists().getLists()) { map.set(list.getId(), list); + + for(const item of list.getItems()) { + + map.set(item.getId(), item); + } } for(const prop of scene.getPrefabUserProperties().getProperties()) { diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/commands/SceneEditorCommands.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/commands/SceneEditorCommands.ts index 21f013803..eb27e94de 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/commands/SceneEditorCommands.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/commands/SceneEditorCommands.ts @@ -59,6 +59,7 @@ namespace phasereditor2d.scene.ui.editor.commands { export const CMD_OPEN_SCRIPT_DIALOG = "phasereditor2d.scene.ui.editor.commands.OpenScriptDialog"; export const CMD_OPEN_ADD_SCRIPT_DIALOG = "phasereditor2d.scene.ui.editor.commands.OpenAddScriptDialog"; export const CMD_PREVIEW_SCENE = "phasereditor2d.scene.ui.editor.commands.PreviewScene"; + export const CMD_EDIT_HIT_AREA = "phasereditor2d.scene.ui.editor.commands.ResizeHitArea"; function isSceneScope(args: colibri.ui.ide.commands.HandlerArgs) { @@ -139,6 +140,8 @@ namespace phasereditor2d.scene.ui.editor.commands { this.registerDepthCommands(manager); + this.registerListCommands(manager); + this.registerTypeCommands(manager); this.registerMoveObjectCommands(manager); @@ -1898,6 +1901,23 @@ namespace phasereditor2d.scene.ui.editor.commands { .getToolsManager().swapTool(ui.sceneobjects.SliceTool.ID) } }); + + manager.add({ + command: { + id: CMD_EDIT_HIT_AREA, + name: "Hit Area Tool", + tooltip: "Resize the hit area of the selected objects.", + category: CAT_SCENE_EDITOR + }, + handler: { + testFunc: isSceneScope, + executeFunc: args => (args.activeEditor as SceneEditor) + .getToolsManager().swapTool(ui.sceneobjects.EditHitAreaTool.ID) + }, + keys: { + key: "KeyI" + } + }); } private static registerVisibilityCommands(manager: colibri.ui.ide.commands.CommandManager) { @@ -1998,6 +2018,41 @@ namespace phasereditor2d.scene.ui.editor.commands { } } + private static registerListCommands(manager: colibri.ui.ide.commands.CommandManager) { + + // order commands + + const moves: [undo.DepthMove, string][] = [["Up", "PageUp"], ["Down", "PageDown"], ["Top", "Home"], ["Bottom", "End"]]; + + for (const tuple of moves) { + + const move = tuple[0]; + const key = tuple[1]; + + manager.add({ + + command: { + id: "phasereditor2d.scene.ui.editor.commands.ListOrder" + move, + name: "Move " + move, + category: CAT_SCENE_EDITOR, + tooltip: "Move the object in its list to " + move + "." + }, + + handler: { + testFunc: args => isSceneScope(args) && args.activeEditor.getSelection().length > 0 + && sceneobjects.ListOrderOperation.allow(args.activeEditor as any, move), + + executeFunc: args => args.activeEditor.getUndoManager().add( + new sceneobjects.ListOrderOperation(args.activeEditor as editor.SceneEditor, move)) + }, + + keys: { + key + } + }); + } + } + static computeOriginCommandData(): Array<{ command: string, name: string, diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineContentProvider.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineContentProvider.ts index 0bab7dcf3..c96c9b86e 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineContentProvider.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineContentProvider.ts @@ -71,7 +71,9 @@ namespace phasereditor2d.scene.ui.editor.outline { } else if (parentES.isPrefabInstance()) { - const prefabChildren = parentES.getMutableNestedPrefabChildren(); + const prefabChildren: sceneobjects.ISceneGameObject[] = []; + + this.getPublicMutableNestedPrefabChildren(parent, prefabChildren); const appendedChildren = parentES.getAppendedChildren(); @@ -108,6 +110,12 @@ namespace phasereditor2d.scene.ui.editor.outline { return parent.getLists(); + } else if (parent instanceof sceneobjects.ObjectList) { + + const scene = this._editor.getScene(); + + return parent.getItemsWithObjects(scene); + } else if (typeof parent === "string") { return this._editor.getScene().getPlainObjectsByCategory(parent); @@ -125,5 +133,27 @@ namespace phasereditor2d.scene.ui.editor.outline { return []; } + + private getPublicMutableNestedPrefabChildren(parent: sceneobjects.ISceneGameObject, list: sceneobjects.ISceneGameObject[]) { + + const parentES = parent.getEditorSupport(); + + for (const child of parentES.getMutableNestedPrefabChildren()) { + + const childES = child.getEditorSupport(); + + if (childES.isMutableNestedPrefabInstance()) { + + if (childES.isPrivateNestedPrefabInstance()) { + + this.getPublicMutableNestedPrefabChildren(child, list); + + } else { + + list.push(child); + } + } + } + } } } \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineRendererProvider.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineRendererProvider.ts index 17c55c2b6..74afa787f 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineRendererProvider.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineRendererProvider.ts @@ -23,6 +23,11 @@ namespace phasereditor2d.scene.ui.editor.outline { return new controls.viewers.IconImageCellRenderer(ScenePlugin.getInstance().getIcon(ICON_LIST)); + } else if (element instanceof sceneobjects.ObjectListItem) { + + return new sceneobjects.ObjectListItemCellRenderer( + this.getCellRenderer(element.getObject())); + } else if (element instanceof sceneobjects.UserComponentNode) { return new controls.viewers.IconImageCellRenderer(ScenePlugin.getInstance().getIcon(ICON_USER_COMPONENT)); diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineStyledLabelProvider.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineStyledLabelProvider.ts index dc8eacc87..6a4d4c346 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineStyledLabelProvider.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/outline/SceneEditorOutlineStyledLabelProvider.ts @@ -37,6 +37,10 @@ namespace phasereditor2d.scene.ui.editor.outline { return obj.getLabel(); + } else if (obj instanceof sceneobjects.ObjectListItem) { + + return this.getLabel(obj.getObject()); + } else if (obj instanceof sceneobjects.UserComponentNode) { return obj.getComponentName(); @@ -106,20 +110,30 @@ namespace phasereditor2d.scene.ui.editor.outline { if (sceneobjects.isGameObject(obj)) { - const support = (obj as sceneobjects.ISceneGameObject).getEditorSupport(); + const objES = (obj as sceneobjects.ISceneGameObject).getEditorSupport(); - if (support.isMutableNestedPrefabInstance()) { + if (obj instanceof sceneobjects.ScriptNode) { + + hintText += " #script"; + } - hintText += "- nested prefab"; + if (objES.isMutableNestedPrefabInstance()) { + + hintText += " #nested_prefab_inst"; color = ScenePlugin.getInstance().getNestedPrefabColor(); - } else if (support.isPrefabInstance()) { + } else if (objES.isPrefabInstance()) { - hintText += "- prefab" + hintText += " #prefab_inst" color = ScenePlugin.getInstance().getPrefabColor(); } + + if (!objES.isNestedPrefabInstance()) { + + hintText += ` #scope_${objES.getScope().toLocaleLowerCase()}`; + } } if (hintText === "") { @@ -138,7 +152,7 @@ namespace phasereditor2d.scene.ui.editor.outline { }, { text: " " + hintText, - color: theme.viewerForeground + "90" + color: theme.viewerForeground + "45" } ]; } diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/undo/DeleteOperation.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/undo/DeleteOperation.ts index e3341ea7f..eaf453c2e 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/undo/DeleteOperation.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/undo/DeleteOperation.ts @@ -30,6 +30,14 @@ namespace phasereditor2d.scene.ui.editor.undo { lists.removeListById(obj.getId()); } + for(const obj of editor.getSelectedListItems()) { + + for(const list of lists.getLists()) { + + list.removeItem(obj.getId()); + } + } + for(const obj of editor.getSelectedPrefabProperties()) { obj.getAllProperties().deleteProperty(obj.getName()); diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/undo/DepthOperation.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/undo/DepthOperation.ts index f6c3d1173..78d59750f 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/editor/undo/DepthOperation.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/editor/undo/DepthOperation.ts @@ -14,8 +14,15 @@ namespace phasereditor2d.scene.ui.editor.undo { static allow(editor: SceneEditor, move: DepthMove) { + // sort the selection and filter off non-game-objects let sel = this.sortedSelection(editor); + // if the sorted selection contains all the selected objects + if (sel.length !== editor.getSelection().length) { + + return false; + } + for (const obj of sel) { const parent = obj.getEditorSupport().getObjectParent(); diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/Component.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/Component.ts index b84e67bc4..0a7db2c38 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/Component.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/Component.ts @@ -22,7 +22,6 @@ namespace phasereditor2d.scene.ui.sceneobjects { export abstract class Component implements core.json.ISerializable { - private _obj: T; private _properties: Set>; private _active: boolean; diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/EditorSupport.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/EditorSupport.ts index 0d5393573..6d9e18016 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/EditorSupport.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/EditorSupport.ts @@ -2,14 +2,6 @@ namespace phasereditor2d.scene.ui.sceneobjects { import controls = colibri.ui.controls; - export enum ObjectScope { - - METHOD = "METHOD", - CLASS = "CLASS", - PUBLIC = "PUBLIC", - NESTED_PREFAB = "NESTED_PREFAB" - } - export abstract class EditorSupport { private _object: T; @@ -24,7 +16,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { this._scene = scene; this._label = label; this._useGameObjectName = false; - this._scope = ObjectScope.METHOD; + this._scope = ObjectScope.LOCAL; this.setId(Phaser.Utils.String.UUID()); } @@ -60,9 +52,34 @@ namespace phasereditor2d.scene.ui.sceneobjects { return ""; } + isLocalScope() { + + return isLocalScope(this._scope); + } + isMethodScope() { - return this._scope === ObjectScope.METHOD; + return isMethodScope(this._scope); + } + + isClassScope() { + + return isClassScope(this._scope); + } + + isPublicScope() { + + return isPublicScope(this._scope); + } + + isNestedPrefabScope() { + + return isNestedPrefabScope(this._scope); + } + + isClassOrPublicScope() { + + return isClassOrPublicScope(this._scope); } getObject() { @@ -95,17 +112,6 @@ namespace phasereditor2d.scene.ui.sceneobjects { return this._scope; } - isNestedPrefabScope() { - - return this._scope === ObjectScope.NESTED_PREFAB; - } - - isPublic() { - - return this._scope === ObjectScope.PUBLIC - || this._scope === ObjectScope.NESTED_PREFAB; - } - setScope(scope: ObjectScope) { this._scope = scope; diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/GameObjectEditorSupport.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/GameObjectEditorSupport.ts index 1ee9fb474..ec9fc74ff 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/GameObjectEditorSupport.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/GameObjectEditorSupport.ts @@ -2,6 +2,12 @@ namespace phasereditor2d.scene.ui.sceneobjects { import json = core.json; + import controls = colibri.ui.controls; + + interface IUnlockListenerArg { + property: IProperty, + unlock: boolean; + } export abstract class GameObjectEditorSupport extends EditorSupport { @@ -12,6 +18,11 @@ namespace phasereditor2d.scene.ui.sceneobjects { private _componentMap: Map>; private _unlockedProperties: Set; private _isNestedPrefabInstance: boolean; + private _isPrivateNestedPrefabInstance: boolean; + private _isPrefabInstancePart: boolean; + // a temporal variable used for serialization + public _private_np: boolean; + public unlockEvent: controls.ListenerList>; // parent private _allowPickChildren: boolean; @@ -25,10 +36,14 @@ namespace phasereditor2d.scene.ui.sceneobjects { super(obj, extension.getTypeName().toLowerCase(), scene); this._extension = extension; + this.unlockEvent = new controls.ListenerList(); this._unlockedProperties = new Set(); this._serializables = []; this._componentMap = new Map(); this._isNestedPrefabInstance = false; + this._isPrivateNestedPrefabInstance = false; + this._isPrefabInstancePart = false; + this._private_np = false; this._allowPickChildren = true; this._showChildrenInOutline = true; @@ -45,6 +60,18 @@ namespace phasereditor2d.scene.ui.sceneobjects { this.addComponent(new PrefabUserPropertyComponent(obj)); this.addComponent(new UserComponentsEditorComponent(obj)); + if (this.isDisplayObject()) { + + this.addComponent( + new HitAreaComponent(obj), + new RectangleHitAreaComponent(obj), + new CircleHitAreaComponent(obj), + new EllipseHitAreaComponent(obj), + new PolygonHitAreaComponent(obj), + new PixelPerfectHitAreaComponent(obj) + ); + } + this.setInteractive(); scene.addGameObject(obj); @@ -329,6 +356,8 @@ namespace phasereditor2d.scene.ui.sceneobjects { this._unlockedProperties.delete(property.name); } + + this.unlockEvent.fire({ property, unlock }); } setUnlockedPropertyXY(property: IPropertyXY, unlock: boolean) { @@ -613,6 +642,9 @@ namespace phasereditor2d.scene.ui.sceneobjects { */ isMutableNestedPrefabInstance() { + // TODO: the next line is a better implementation we should test: + // return this.isNestedPrefabInstance() && !this.isPrivateNestedPrefabInstance(); + if (this.isNestedPrefabInstance()) { const parent = this.getObjectParent(); @@ -660,7 +692,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { const parent = this.getObjectParent(); const parentES = parent?.getEditorSupport(); - if (!parent + if (!parent || !parentES.isPrefabInstance() || this.isPrefeabInstanceAppendedChild()) { @@ -671,14 +703,34 @@ namespace phasereditor2d.scene.ui.sceneobjects { return false; } + isNestedPrefabInstanceParent() { + + for (const obj of this.getObjectChildren()) { + + const objES = obj.getEditorSupport(); + + if (objES.isNestedPrefabInstance() || objES.isNestedPrefabInstanceParent()) { + + return true; + } + } + + return false; + } + isNestedPrefabInstance() { return this._isNestedPrefabInstance; } - _setNestedPrefabInstance(isNestedPrefabInstace: boolean) { + isPrivateNestedPrefabInstance() { + + return this._isPrivateNestedPrefabInstance; + } + + isPrefabInstancePart() { - this._isNestedPrefabInstance = isNestedPrefabInstace; + return this._isPrefabInstancePart; } isPrefabInstance() { @@ -816,7 +868,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { const result = []; - for(const obj of children) { + for (const obj of children) { const objES = obj.getEditorSupport(); @@ -1008,6 +1060,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { } data.id = this.getId(); + data.private_np = this._private_np ? true : undefined; if (this._prefabId && this._unlockedProperties.size > 0) { @@ -1140,6 +1193,18 @@ namespace phasereditor2d.scene.ui.sceneobjects { return objData as json.IObjectData; }); } + + // if the container has an empty list, remove it from the file + if (containerData.list && containerData.list.length === 0) { + + delete containerData.list; + } + + // if the container has an empty nestedPrefabs array, remove it from the file + if (containerData.nestedPrefabs && containerData.nestedPrefabs.length === 0) { + + delete containerData.nestedPrefabs; + } } private static readPrefabChildren(serializer: core.json.Serializer, list: json.IObjectData[]) { @@ -1235,21 +1300,27 @@ namespace phasereditor2d.scene.ui.sceneobjects { if (sprite) { - parent.getEditorSupport().addObjectChild(sprite); + parentES.addObjectChild(sprite); + + const spriteES = sprite.getEditorSupport(); // if it is not an appended child if (i < prefabChildren.length) { const prefabData = prefabChildren[i]; + const { private_np, scope } = prefabData; - if (prefabData.scope === sceneobjects.ObjectScope.NESTED_PREFAB) { + if (private_np || ui.sceneobjects.isNestedPrefabScope(scope)) { - sprite.getEditorSupport()._setNestedPrefabInstance(true); + spriteES._isNestedPrefabInstance = true; + spriteES._isPrivateNestedPrefabInstance = private_np; } + + this._isPrefabInstancePart = true; } // updates the object with the final data - sprite.getEditorSupport().readJSON(childData); + spriteES.readJSON(childData); } i++; @@ -1271,11 +1342,10 @@ namespace phasereditor2d.scene.ui.sceneobjects { for (const originalChild of originalPrefabChildren) { - if (originalChild.scope !== sceneobjects.ObjectScope.NESTED_PREFAB) { + const isNestedPrefab = originalChild.private_np + || sceneobjects.isNestedPrefabScope(originalChild.scope); - result.push(originalChild); - - } else { + if (isNestedPrefab) { // find a local nested prefab @@ -1303,13 +1373,31 @@ namespace phasereditor2d.scene.ui.sceneobjects { } } + let createFreshObject = true; + let newNestedPrefabs: json.IObjectData[]; + if (localNestedPrefab) { - result.push(localNestedPrefab); + if (isPublicScope(originalChild.scope)) { + // it is ok, the original child is public + // add the local nested prefab as final version of the object + result.push(localNestedPrefab); + + createFreshObject = false; + + } else { - } else { + // the original object is not public any more, + // we will create a link-object, but keeping the same nested prefabs + newNestedPrefabs = localNestedPrefab.nestedPrefabs; + } + } - // we don't have a local prefab, find one remote and create a pointer to it + if (createFreshObject) { + + // we don't have a local nested prefab, + // or the original nested prefab is not public any more + // so find one remote and create a pointer to it const remoteNestedPrefab = this.findRemoteNestedPrefab(objData.prefabId, originalChild.id); @@ -1321,6 +1409,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { id: Phaser.Utils.String.UUID(), prefabId: remoteNestedPrefab.id, label: remoteNestedPrefab.label, + nestedPrefabs: newNestedPrefabs }; result.push(nestedPrefab); @@ -1333,11 +1422,16 @@ namespace phasereditor2d.scene.ui.sceneobjects { id: Phaser.Utils.String.UUID(), prefabId: originalChild.id, label: originalChild.label, + nestedPrefabs: newNestedPrefabs }; result.push(nestedPrefab); } } + + } else { + + result.push(originalChild); } } diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/ObjectScope.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/ObjectScope.ts new file mode 100644 index 000000000..dfe43ef63 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/ObjectScope.ts @@ -0,0 +1,56 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export enum ObjectScope { + LOCAL = "LOCAL", + METHOD = "METHOD", + CLASS = "CLASS", + PUBLIC = "PUBLIC", + NESTED_PREFAB = "NESTED_PREFAB", + } + + export const OBJECT_SCOPES = [ + ObjectScope.LOCAL, + ObjectScope.METHOD, + ObjectScope.CLASS, + ObjectScope.PUBLIC, + ObjectScope.NESTED_PREFAB + ]; + + export function isNestedPrefabScope(scope: ObjectScope) { + + return scope === ObjectScope.NESTED_PREFAB; + } + + export function isClassOrPublicScope(scope: ObjectScope) { + + return isClassScope(scope) || isPublicScope(scope); + } + + export function isPublicScope(scope: ObjectScope) { + + switch (scope) { + + case ObjectScope.PUBLIC: + case ObjectScope.NESTED_PREFAB: + + return true; + } + + return false; + } + + export function isLocalScope(scope: ObjectScope) { + + return scope === ObjectScope.LOCAL; + } + + export function isMethodScope(scope: ObjectScope) { + + return scope === ObjectScope.METHOD; + } + + export function isClassScope(scope: ObjectScope) { + + return scope === ObjectScope.CLASS; + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/ScenePlainObjectVariableSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/ScenePlainObjectVariableSection.ts index 273515148..96909260d 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/ScenePlainObjectVariableSection.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/ScenePlainObjectVariableSection.ts @@ -56,16 +56,21 @@ namespace phasereditor2d.scene.ui.sceneobjects { this.createLabel(comp, "Scope", "The lexical scope of the object."); - const items = [{ - name: "Method", - value: ObjectScope.METHOD - }, { - name: "Class", - value: ObjectScope.CLASS - }, { - name: "Public", - value: ObjectScope.PUBLIC - }]; + const items = [ + { + name: "LOCAL", + value: ObjectScope.LOCAL + }, + { + name: "METHOD", + value: ObjectScope.METHOD + }, { + name: "CLASS", + value: ObjectScope.CLASS + }, { + name: "PUBLIC", + value: ObjectScope.PUBLIC + }]; const btn = this.createMenuButton(comp, "", () => items, scope => { @@ -73,7 +78,9 @@ namespace phasereditor2d.scene.ui.sceneobjects { for (const obj of objects) { - obj.getEditorSupport().setScope(scope); + const objES = obj.getEditorSupport(); + + objES.setScope(scope); } }); }); diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaComponent.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaComponent.ts new file mode 100644 index 000000000..368d775d2 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaComponent.ts @@ -0,0 +1,95 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export abstract class BaseHitAreaComponent extends Component { + + private _shape: HitAreaShape; + + constructor(obj: ISceneGameObject, shape: HitAreaShape, properties: IProperty[]) { + super(obj, properties); + + this._shape = shape; + } + + private initUnlockListener() { + + const objES = this.getEditorSupport(); + + const unlockEvent = objES.unlockEvent; + + unlockEvent.addListener(args => { + + if (args.property.name === HitAreaComponent.hitAreaShape.name) { + + for (const prop of this.getProperties()) { + + objES.setUnlockedProperty(prop, args.unlock); + } + } + }); + } + + readJSON(ser: core.json.Serializer): void { + + this.initUnlockListener(); + + super.readJSON(ser); + } + + protected abstract _setDefaultValues(width: number, height: number): void; + + setDefaultValues() { + + const obj = this.getObject() as Image; + const objES = this.getEditorSupport(); + + let width = 0, height = 0; + + let [widthProp, heightProp] = objES.getSizeProperties(); + + if (widthProp && heightProp) { + + width = widthProp.getValue(obj); + height = heightProp.getValue(obj); + + } else if (obj instanceof Container) { + + const c = obj as Container; + + const b = c.getBounds(); + + width = b.width; + height = b.height; + + } else if (obj.width && obj.height) { + + width = obj.width; + height = obj.height; + } + + this._setDefaultValues(width, height); + } + + buildSetObjectPropertiesCodeDOM(args: ISetObjectPropertiesCodeDOMArgs): void { + + const obj = this.getObject(); + const objES = obj.getEditorSupport(); + + if (objES.getComponent(HitAreaComponent)) { + + if (objES.isUnlockedProperty(HitAreaComponent.hitAreaShape)) { + + if (HitAreaComponent.hitAreaShape.getValue(obj) === this._shape) { + + const code = new core.code.MethodCallCodeDOM("setInteractive", args.objectVarName); + + this.buildSetInteractiveCodeCOM(args, obj, code); + + args.statements.push(code); + } + } + } + } + + protected abstract buildSetInteractiveCodeCOM(args: ISetObjectPropertiesCodeDOMArgs, obj: ISceneGameObject, code: core.code.MethodCallCodeDOM): void; + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaOffsetToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaOffsetToolItem.ts new file mode 100644 index 000000000..507ec8b17 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaOffsetToolItem.ts @@ -0,0 +1,220 @@ +/// +namespace phasereditor2d.scene.ui.sceneobjects { + + export abstract class BaseHitAreaOffsetToolItem + extends BaseHitAreaToolItem implements editor.tools.ISceneToolItemXY { + + protected _x: IAxisFactor; + protected _y: IAxisFactor; + private _dragging: boolean; + + constructor(shape: HitAreaShape, x: IAxisFactor, y: IAxisFactor) { + super(shape); + + this._x = x; + this._y = y; + } + + protected abstract getToolOrigin(obj: ISceneGameObject): { originX: number, originY: number }; + + getPoint(args: editor.tools.ISceneToolContextArgs): { x: number; y: number; } { + + return this.getAvgScreenPointOfObjects(args, + + (sprite: sceneobjects.Image) => this._x - this.getToolOrigin(sprite).originX, + + (sprite: sceneobjects.Image) => this._y - this.getToolOrigin(sprite).originY, + ); + } + + protected getScreenPointOfObject(args: ui.editor.tools.ISceneToolContextArgs, sprite: Sprite, fx: number, fy: number) { + + const worldPoint = new Phaser.Geom.Point(0, 0); + + let { displayOriginX, displayOriginY } = sprite.getEditorSupport().computeDisplayOrigin(); + + if (sprite instanceof Container) { + + displayOriginX = 0; + displayOriginY = 0; + } + + const offset = this.getOffsetProperties(sprite); + const size = this.getSizeProperties(sprite); + + let x = offset.x.getValue(sprite) as number; + let y = offset.y.getValue(sprite) as number; + let width = size.width.getValue(sprite) as number; + let height = size.height.getValue(sprite) as number; + + x = x - displayOriginX + fx * width; + y = y - displayOriginY + fy * height; + + const tx = sprite.getWorldTransformMatrix(); + + tx.transformPoint(x, y, worldPoint); + + return args.camera.getScreenPoint(worldPoint.x, worldPoint.y); + } + + protected computeSize(obj: ISceneGameObject) { + + const size = this.getSizeProperties(obj); + + return { + width: size.width.getValue(obj) as number, + height: size.height.getValue(obj) as number + } + } + + private computeOffset(obj: ISceneGameObject) { + + const offset = this.getOffsetProperties(obj); + + return { + x: offset.x.getValue(obj), + y: offset.y.getValue(obj) + } + }; + + protected abstract getOffsetProperties(obj: ISceneGameObject): { + x: IProperty, + y: IProperty + }; + + protected abstract getSizeProperties(obj: ISceneGameObject): { + width: IProperty, + height: IProperty + }; + + render(args: editor.tools.ISceneToolRenderArgs) { + + const point = this.getPoint(args); + + const ctx = args.canvasContext; + + ctx.save(); + + ctx.translate(point.x, point.y); + + const angle = this.globalAngle(args.objects[0] as any); + ctx.rotate(Phaser.Math.DegToRad(angle)); + + this.drawRect(ctx, args.canEdit ? + EditHitAreaTool.TOOL_COLOR : editor.tools.SceneTool.COLOR_CANNOT_EDIT); + + ctx.restore(); + } + + containsPoint(args: editor.tools.ISceneToolDragEventArgs): boolean { + + const point = this.getPoint(args); + + return Phaser.Math.Distance.Between(args.x, args.y, point.x, point.y) < 20; + } + + onStartDrag(args: editor.tools.ISceneToolDragEventArgs): void { + + if (!this.containsPoint(args)) { + + return; + } + + this._dragging = true; + + const point = this.getPoint(args); + + for (const obj of args.objects) { + + const sprite = obj as unknown as Image; + + const worldTx = new Phaser.GameObjects.Components.TransformMatrix(); + + const initLocalPos = new Phaser.Math.Vector2(); + + sprite.getWorldTransformMatrix(worldTx); + + worldTx.applyInverse(point.x, point.y, initLocalPos); + + const offset = this.computeOffset(sprite); + + sprite.setData(this.getKeyData(), { + initLocalPos: initLocalPos, + initLocalOffset: offset, + initWorldTx: worldTx + }); + } + } + + protected getInitialValue(obj: ISceneGameObject) { + + const { initLocalOffset } = obj.getData(this.getKeyData()); + + return initLocalOffset; + } + + protected abstract getKeyData(): string; + + onDrag(args: editor.tools.ISceneToolDragEventArgs): void { + + if (!this._dragging) { + return; + } + + const camera = args.camera; + + for (const obj of args.objects) { + + const sprite = obj as Sprite; + const data = sprite.data.get(this.getKeyData()); + const initLocalPos: Phaser.Math.Vector2 = data.initLocalPos; + const worldTx: Phaser.GameObjects.Components.TransformMatrix = data.initWorldTx; + + const localPos = new Phaser.Math.Vector2(); + + worldTx.applyInverse(args.x, args.y, localPos); + + const flipX = sprite.flipX ? -1 : 1; + const flipY = sprite.flipY ? -1 : 1; + + const dx = (localPos.x - initLocalPos.x) * flipX / camera.zoom; + const dy = (localPos.y - initLocalPos.y) * flipY / camera.zoom; + + const x = data.initLocalOffset.x + dx; + const y = data.initLocalOffset.y + dy; + + const changeAll = this._x === 0 && this._y === 0 || this._x === 0.5 && this._y === 0.5; + const changeX = this._x === 0 && this._y === 0.5 || changeAll; + const changeY = this._x === 0.5 && this._y === 0 || changeAll; + + const offset = this.getOffsetProperties(obj); + + if (changeX) { + + offset.x.setValue(sprite, Math.floor(x)); + } + + if (changeY) { + + offset.y.setValue(sprite, Math.floor(y)); + } + + args.editor.updateInspectorViewSection(this.getOffsetSectionId()); + } + } + + protected abstract getOffsetSectionId(): string; + + onStopDrag(args: editor.tools.ISceneToolDragEventArgs): void { + + if (this._dragging) { + + args.editor.getUndoManager().add(this.createStopDragOperation(args)); + + this._dragging = false; + } + } + + protected abstract createStopDragOperation(args: editor.tools.ISceneToolDragEventArgs): colibri.ui.ide.undo.Operation; + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaSizeToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaSizeToolItem.ts new file mode 100644 index 000000000..3de3c207d --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaSizeToolItem.ts @@ -0,0 +1,189 @@ +/// +namespace phasereditor2d.scene.ui.sceneobjects { + + export abstract class BaseHitAreaSizeToolItem + extends BaseHitAreaToolItem implements editor.tools.ISceneToolItemXY { + + protected _x: IAxisFactor; + protected _y: IAxisFactor; + private _dragging: boolean; + + constructor(shape: HitAreaShape, x: IAxisFactor, y: IAxisFactor) { + super(shape); + + this._x = x; + this._y = y; + } + + protected abstract computeSize(obj: ISceneGameObject): { width: number, height: number }; + + protected abstract getHitAreaOffset(obj: ISceneGameObject): { x: number, y: number }; + + protected abstract getDataKey(): string; + + protected abstract getHitAreaSectionId(): string; + + protected abstract onDragValues(obj: ISceneGameObject, changeX: boolean, changeY: boolean, width: number, height: number): void; + + protected abstract createStopDragOperation(args: editor.tools.ISceneToolDragEventArgs): colibri.ui.ide.undo.Operation; + + getPoint(args: editor.tools.ISceneToolContextArgs): { x: number; y: number; } { + + return this.getAvgScreenPointOfObjects(args, + + (sprite: sceneobjects.Image) => this._x - this.getToolOrigin(sprite).originX, + + (sprite: sceneobjects.Image) => this._y - this.getToolOrigin(sprite).originY, + ); + } + + protected getScreenPointOfObject(args: ui.editor.tools.ISceneToolContextArgs, sprite: Sprite, fx: number, fy: number) { + + const worldPoint = new Phaser.Geom.Point(0, 0); + + const { width, height } = this.computeSize(sprite); + + let { displayOriginX, displayOriginY } = sprite.getEditorSupport().computeDisplayOrigin(); + + if (sprite instanceof Container) { + + displayOriginX = 0; + displayOriginY = 0; + } + + const offset = this.getHitAreaOffset(sprite); + const x = offset.x - displayOriginX + fx * width; + const y = offset.y - displayOriginY + fy * height; + + const tx = sprite.getWorldTransformMatrix(); + + tx.transformPoint(x, y, worldPoint); + + return args.camera.getScreenPoint(worldPoint.x, worldPoint.y); + } + + render(args: editor.tools.ISceneToolRenderArgs) { + + const point = this.getPoint(args); + + const ctx = args.canvasContext; + + ctx.save(); + + ctx.translate(point.x, point.y); + + const angle = this.globalAngle(args.objects[0] as any); + ctx.rotate(Phaser.Math.DegToRad(angle)); + + this.drawRect(ctx, args.canEdit ? + EditHitAreaTool.TOOL_COLOR : editor.tools.SceneTool.COLOR_CANNOT_EDIT); + + ctx.restore(); + } + + containsPoint(args: editor.tools.ISceneToolDragEventArgs): boolean { + + const point = this.getPoint(args); + + return Phaser.Math.Distance.Between(args.x, args.y, point.x, point.y) < 20; + } + + onStartDrag(args: editor.tools.ISceneToolDragEventArgs): void { + + if (!this.containsPoint(args)) { + return; + } + + this._dragging = true; + + const point = this.getPoint(args); + + for (const obj of args.objects) { + + const sprite = obj as unknown as Image; + + const worldTx = new Phaser.GameObjects.Components.TransformMatrix(); + + const initLocalPos = new Phaser.Math.Vector2(); + + sprite.getWorldTransformMatrix(worldTx); + + worldTx.applyInverse(point.x, point.y, initLocalPos); + + const { width, height } = this.computeSize(sprite); + + sprite.setData(this.getDataKey(), { + initWidth: width, + initHeight: height, + initLocalPos: initLocalPos, + initWorldTx: worldTx + }); + } + } + + protected getInitialSize(obj: any): { x: number, y: number } { + + const data = obj.getData(this.getDataKey()); + + return { x: data.initWidth, y: data.initHeight }; + } + + protected abstract getToolOrigin(obj: ISceneGameObject): { originX: number, originY: number }; + + onDrag(args: editor.tools.ISceneToolDragEventArgs): void { + + if (!this._dragging) { + + return; + } + + const camera = args.camera; + + for (const obj of args.objects) { + + const sprite = obj as Sprite; + const data = sprite.data.get(this.getDataKey()); + const initLocalPos: Phaser.Math.Vector2 = data.initLocalPos; + const worldTx: Phaser.GameObjects.Components.TransformMatrix = data.initWorldTx; + + const localPos = new Phaser.Math.Vector2(); + + worldTx.applyInverse(args.x, args.y, localPos); + + const flipX = sprite.flipX ? -1 : 1; + const flipY = sprite.flipY ? -1 : 1; + + const { originX, originY } = this.getToolOrigin(obj); + + const dx = (localPos.x - initLocalPos.x) * flipX / camera.zoom; + const dy = (localPos.y - initLocalPos.y) * flipY / camera.zoom; + + const dw = dx / (1 - (originX === 1 ? 0 : originX)); + const dh = dy / (1 - (originY === 1 ? 0 : originY)); + + const { x: width, y: height } = args.editor.getScene().snapPoint( + data.initWidth + dw, + data.initHeight + dh + ); + + const changeAll = this._x === 1 && this._y === 1; + const changeX = this._x === 1 && this._y === 0.5 || changeAll; + const changeY = this._x === 0.5 && this._y === 1 || changeAll; + + this.onDragValues(sprite, changeX, changeY, width, height); + + args.editor.updateInspectorViewSection(this.getHitAreaSectionId()); + } + } + + onStopDrag(args: editor.tools.ISceneToolDragEventArgs): void { + + if (this._dragging) { + + args.editor.getUndoManager().add(this.createStopDragOperation(args)); + + this._dragging = false; + } + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaToolItem.ts new file mode 100644 index 000000000..cd81ce1de --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/BaseHitAreaToolItem.ts @@ -0,0 +1,29 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export abstract class BaseHitAreaToolItem extends editor.tools.SceneToolItem { + + private _shape: HitAreaShape; + + constructor(shape: HitAreaShape) { + super(); + + this._shape = shape; + } + + isValidFor(objects: ISceneGameObject[]): boolean { + + if (EditHitAreaTool.isValidFor(...objects)) { + + for (const obj of objects) { + + if (HitAreaComponent.getShape(obj) === this._shape) { + + return true; + } + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaComponent.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaComponent.ts new file mode 100644 index 000000000..1121d070f --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaComponent.ts @@ -0,0 +1,57 @@ +/// +/// +namespace phasereditor2d.scene.ui.sceneobjects { + + export class CircleHitAreaComponent extends BaseHitAreaComponent { + + static x = HitAreaProperty(CircleHitAreaComponent, "x", "X", "phaser:Phaser.Geom.Circle.x", 0); + static y = HitAreaProperty(CircleHitAreaComponent, "y", "Y", "phaser:Phaser.Geom.Circle.y", 0); + static radius = HitAreaProperty(CircleHitAreaComponent, "radius", "Radius", "phaser:Phaser.Geom.Circle.radius", 0); + + static position: IPropertyXY = { + label: "Offset", + x: this.x, + y: this.y + }; + + public x = 0; + public y = 0; + public radius = 0; + + constructor(obj: ISceneGameObject) { + super(obj, HitAreaShape.CIRCLE, [ + CircleHitAreaComponent.x, + CircleHitAreaComponent.y, + CircleHitAreaComponent.radius, + ]); + } + + static getCircleComponent(obj: ISceneGameObject) { + + const objES = obj.getEditorSupport(); + + const comp = objES.getComponent(CircleHitAreaComponent) as CircleHitAreaComponent; + + return comp; + } + + protected _setDefaultValues(width: number, height: number): void { + + this.x = width / 2; + this.y = height / 2; + this.radius = Math.min(width, height) / 2; + } + + protected override buildSetInteractiveCodeCOM( + args: ISetObjectPropertiesCodeDOMArgs, + obj: ISceneGameObject, + code: core.code.MethodCallCodeDOM): void { + + const { x, y, radius } = this; + + code.arg(`new Phaser.Geom.Circle(${x}, ${y}, ${radius})`); + + code.arg("Phaser.Geom.Circle.Contains"); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaOffsetToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaOffsetToolItem.ts new file mode 100644 index 000000000..d2de5b424 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaOffsetToolItem.ts @@ -0,0 +1,47 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class CircleHitAreaOffsetToolItem extends BaseHitAreaOffsetToolItem { + + constructor() { + super(HitAreaShape.CIRCLE, 0.5, 0.5); + } + + protected getToolOrigin(obj: ISceneGameObject): { originX: number; originY: number; } { + + return { originX: 0.5, originY: 0.5 }; + } + + protected getOffsetProperties(obj: ISceneGameObject): { x: IProperty; y: IProperty; } { + + const { x, y } = CircleHitAreaComponent; + + return { x, y }; + } + + protected getSizeProperties(obj: ISceneGameObject): { width: IProperty; height: IProperty; } { + + const { radius } = CircleHitAreaComponent; + + const prop = {...radius}; + + prop.getValue = obj => radius.getValue(obj) * 2; + + return { width: prop, height: prop }; + } + + protected getKeyData(): string { + + return "CircleHitAreaOffsetToolItem"; + } + + protected getOffsetSectionId(): string { + + return CircleHitAreaSection.ID; + } + + protected createStopDragOperation(args: editor.tools.ISceneToolDragEventArgs): colibri.ui.ide.undo.Operation { + + return new CircleOffsetOperation(args, obj => this.getInitialValue(obj)); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaSection.ts new file mode 100644 index 000000000..528f9b455 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaSection.ts @@ -0,0 +1,40 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + import controls = colibri.ui.controls; + + export class CircleHitAreaSection extends SceneGameObjectSection { + + static ID = "phasereditor2d.scene.ui.sceneobjects.CircleHitAreaSection"; + + constructor(page: controls.properties.PropertyPage) { + super(page, CircleHitAreaSection.ID, "Hit Area (Circle)"); + } + + createMenu(menu: controls.Menu) { + + this.createToolMenuItem(menu, EditHitAreaTool.ID); + + super.createMenu(menu); + } + + createForm(parent: HTMLDivElement) { + + const comp = this.createGridElementWithPropertiesXY(parent); + + this.createPropertyXYRow(comp, CircleHitAreaComponent.position, false); + + this.createPropertyFloatRow(comp, CircleHitAreaComponent.radius, false) + .style.gridColumn = "span 4"; + } + + canEdit(obj: any, n: number): boolean { + + return HitAreaComponent.hasHitAreaShape(obj, HitAreaShape.CIRCLE); + } + + canEditNumber(n: number): boolean { + + return n > 0; + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaSizeOperation.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaSizeOperation.ts new file mode 100644 index 000000000..bcb1ddbe8 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaSizeOperation.ts @@ -0,0 +1,30 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class CircleHitAreaSizeOperation extends editor.tools.SceneToolOperation<{ x: number, y: number }> { + + constructor( + toolArgs: editor.tools.ISceneToolContextArgs, + private getInitialSize: (obj: ISceneGameObject) => { x: number, y: number }) { + + super(toolArgs); + } + + getInitialValue(obj: ISceneGameObject): { x: number; y: number; } { + + return this.getInitialSize(obj); + } + + getFinalValue(obj: ISceneGameObject): { x: number; y: number; } { + + return { + x: CircleHitAreaComponent.radius.getValue(obj), + y: CircleHitAreaComponent.radius.getValue(obj) + }; + } + + setValue(obj: ISceneGameObject, value: { x: number; y: number; }) { + + CircleHitAreaComponent.radius.setValue(obj, value.x / 2); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaSizeToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaSizeToolItem.ts new file mode 100644 index 000000000..05f20b973 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleHitAreaSizeToolItem.ts @@ -0,0 +1,60 @@ +/// + +namespace phasereditor2d.scene.ui.sceneobjects { + + export class CircleHitAreaSizeToolItem extends BaseHitAreaSizeToolItem { + + constructor() { + super(HitAreaShape.CIRCLE, 1, 0.5); + } + + protected getToolOrigin(obj: ISceneGameObject): { originX: number; originY: number; } { + + return { originX: 0.5, originY: 0.5 }; + } + + protected computeSize(obj: ISceneGameObject): { width: number; height: number; } { + + const { radius } = CircleHitAreaComponent.getCircleComponent(obj); + + return { width: radius * 2, height: radius * 2 }; + } + + protected getHitAreaOffset(obj: ISceneGameObject): { x: number; y: number; } { + + const { x, y } = CircleHitAreaComponent.getCircleComponent(obj); + + return { x, y }; + } + + protected getDataKey(): string { + + return "CircleHitAreaSizeToolItem"; + } + + protected getHitAreaSectionId(): string { + + return CircleHitAreaSection.ID; + } + + protected onDragValues(obj: ISceneGameObject, changeX: boolean, changeY: boolean, width: number, height: number) { + + const comp = CircleHitAreaComponent.getCircleComponent(obj); + + if (changeX) { + + comp.radius = width / 2; + } + + if (changeY) { + + comp.radius = height / 2; + } + } + + protected createStopDragOperation(args: editor.tools.ISceneToolDragEventArgs): colibri.ui.ide.undo.Operation { + + return new CircleHitAreaSizeOperation(args, obj => this.getInitialSize(obj)); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleOffsetOperation.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleOffsetOperation.ts new file mode 100644 index 000000000..2c78968b2 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/CircleOffsetOperation.ts @@ -0,0 +1,31 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class CircleOffsetOperation extends editor.tools.SceneToolOperation<{ x: number, y: number }> { + + constructor( + toolArgs: editor.tools.ISceneToolContextArgs, + private getInitialOffset: (obj: ISceneGameObject) => { x: number, y: number }) { + + super(toolArgs); + } + + getInitialValue(obj: ISceneGameObject): { x: number; y: number; } { + + return this.getInitialOffset(obj); + } + + getFinalValue(obj: ISceneGameObject): { x: number; y: number; } { + + return { + x: CircleHitAreaComponent.x.getValue(obj), + y: CircleHitAreaComponent.y.getValue(obj) + }; + } + + setValue(obj: ISceneGameObject, value: { x: number; y: number; }) { + + CircleHitAreaComponent.x.setValue(obj, value.x); + CircleHitAreaComponent.y.setValue(obj, value.y); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EditHitAreaTool.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EditHitAreaTool.ts new file mode 100644 index 000000000..83eab5abb --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EditHitAreaTool.ts @@ -0,0 +1,390 @@ +/// +namespace phasereditor2d.scene.ui.sceneobjects { + + export class EditHitAreaTool extends BaseObjectTool { + + static ID = "phasereditor2d.scene.ui.sceneobjects.EditHitAreaTool"; + static TOOL_COLOR = "orange"; + private _polygonToolItem: PolygonHitAreaToolItem; + + constructor() { + super({ + id: EditHitAreaTool.ID, + command: editor.commands.CMD_EDIT_HIT_AREA, + }, + RectangleHitAreaComponent.x, + RectangleHitAreaComponent.y, + RectangleHitAreaComponent.width, + RectangleHitAreaComponent.height, + EllipseHitAreaComponent.x, + EllipseHitAreaComponent.y, + EllipseHitAreaComponent.width, + EllipseHitAreaComponent.height, + CircleHitAreaComponent.x, + CircleHitAreaComponent.y, + CircleHitAreaComponent.radius + ); + + this.addItems( + new RectangleHitAreaSizeToolItem(1, 0.5), + new RectangleHitAreaSizeToolItem(1, 1), + new RectangleHitAreaSizeToolItem(0.5, 1), + new RectangleHitAreaOffsetToolItem(0, 0), + new RectangleHitAreaOffsetToolItem(0.5, 0), + new RectangleHitAreaOffsetToolItem(0, 0.5), + + new EllipseHitAreaSizeToolItem(1, 0.5), + new EllipseHitAreaSizeToolItem(1, 1), + new EllipseHitAreaSizeToolItem(0.5, 1), + new EllipseHitAreaOffsetToolItem(0, 0), + new EllipseHitAreaOffsetToolItem(0.5, 0), + new EllipseHitAreaOffsetToolItem(0, 0.5), + + new CircleHitAreaSizeToolItem(), + new CircleHitAreaOffsetToolItem(), + + this._polygonToolItem = new PolygonHitAreaToolItem() + ); + } + + handleDoubleClick(args: editor.tools.ISceneToolContextArgs): boolean { + + if (this._polygonToolItem.isValidFor(args.objects)) { + + return this._polygonToolItem.handleDoubleClick(args); + } + + return super.handleDoubleClick(args); + } + + handleDeleteCommand(args: editor.tools.ISceneToolContextArgs): boolean { + + if (this._polygonToolItem.isValidFor(args.objects)) { + + return this._polygonToolItem.handleDeleteCommand(args); + } + + return super.handleDeleteCommand(args); + } + + requiresRepaintOnMouseMove() { + + return true; + } + + protected getProperties(obj?: any): IProperty[] { + + if (HitAreaComponent.hasHitArea(obj)) { + + return [ + RectangleHitAreaComponent.x, + RectangleHitAreaComponent.x, + RectangleHitAreaComponent.width, + RectangleHitAreaComponent.height + ]; + } + + return []; + } + + async onActivated(args: editor.tools.ISceneToolContextArgs) { + + super.onActivated(args); + + for (const obj of args.objects) { + + if (!HitAreaComponent.hasHitArea(obj)) { + + return; + } + } + + const sections = [RectangleHitAreaSection.ID]; + + const props: Set> = new Set(); + + for (const obj of args.objects) { + + const objProps = this.getProperties(obj); + + for (const prop of objProps) { + + props.add(prop); + } + } + + await this.confirmUnlockProperty(args, [...props], "hit area", ...sections); + } + + render(args: editor.tools.ISceneToolRenderArgs): void { + + for (const obj of args.objects) { + + if (EditHitAreaTool.isValidFor(obj)) { + + this.renderObj(args, obj as Sprite); + } + } + + super.render(args); + } + + static isValidFor(...objects: ISceneGameObject[]): boolean { + + for (const obj of objects) { + + if (!HitAreaComponent.hasHitArea(obj) + || HitAreaComponent.getShape(obj) === HitAreaShape.NONE) { + + return false + } + } + + return true; + } + + private renderObj(args: editor.tools.ISceneToolRenderArgs, obj: Sprite) { + + const objES = obj.getEditorSupport(); + + const comp = objES.getComponent(HitAreaComponent) as HitAreaComponent; + + const shape = comp.getHitAreaShape(); + + if (shape === HitAreaShape.PIXEL_PERFECT) { + + // pixel perfect doesn't have any tool item + + return; + } + + const ctx = args.canvasContext; + + ctx.save(); + + if (shape === HitAreaShape.ELLIPSE) { + + this.renderEllipse(obj, args, ctx); + + } else if (shape === HitAreaShape.CIRCLE) { + + this.renderCircle(obj, args, ctx); + + } else if (shape === HitAreaShape.POLYGON) { + + this.renderPolygon(obj, args, ctx); + + } else { + + this.renderRect(obj, args, ctx); + } + + ctx.restore(); + } + + private renderPolygon(obj: Sprite, args: editor.tools.ISceneToolRenderArgs, ctx: CanvasRenderingContext2D) { + + const origin = obj.getEditorSupport().computeDisplayOrigin(); + + if (obj instanceof Container) { + + origin.displayOriginX = 0; + origin.displayOriginY = 0; + } + + const comp = PolygonHitAreaComponent.getPolygonComponent(obj); + + const tx = obj.getWorldTransformMatrix(); + + const points: Array = comp.vectors + .map(p => new Phaser.Math.Vector2( + p.x - origin.displayOriginX, + p.y - origin.displayOriginY)) + + .map(p => tx.transformPoint(p.x, p.y)) + + .map(p => args.camera.getScreenPoint(p.x, p.y)); + + // close the path + points.push(points[0]); + + ctx.save(); + + ctx.strokeStyle = "black"; + ctx.lineWidth = 3; + this.drawPath(ctx, points); + + ctx.strokeStyle = EditHitAreaTool.TOOL_COLOR; + ctx.lineWidth = 1; + this.drawPath(ctx, points); + + ctx.restore(); + } + + private renderRect(obj: Sprite, args: editor.tools.ISceneToolRenderArgs, ctx: CanvasRenderingContext2D) { + + const origin = obj.getEditorSupport().computeDisplayOrigin(); + + if (obj instanceof Container) { + + origin.displayOriginX = 0; + origin.displayOriginY = 0; + } + + const comp = RectangleHitAreaComponent.getRectangleComponent(obj); + + const { x, y, width, height } = comp; + + let x1 = x - origin.displayOriginX; + let y1 = y - origin.displayOriginY; + let x2 = x1 + width; + let y2 = y1 + height; + + const tx = obj.getWorldTransformMatrix(); + + const points = [ + [x1, y1], + [x2, y1], + [x2, y2], + [x1, y2], + [x1, y1] + ].map(([x, y]) => { + + return tx.transformPoint(x, y); + }).map(p => { + + return args.camera.getScreenPoint(p.x, p.y); + }); + + ctx.save(); + + ctx.strokeStyle = "black"; + ctx.lineWidth = 3; + this.drawPath(ctx, points); + + ctx.strokeStyle = EditHitAreaTool.TOOL_COLOR; + ctx.lineWidth = 1; + this.drawPath(ctx, points); + + ctx.restore(); + } + + private drawPath(ctx: CanvasRenderingContext2D, points: Phaser.Math.Vector2[]) { + + ctx.beginPath(); + + ctx.moveTo(points[0].x, points[0].y); + + for (const p of points) { + + ctx.lineTo(p.x, p.y); + } + + ctx.stroke(); + ctx.closePath(); + } + + private renderEllipse(obj: Sprite, args: editor.tools.ISceneToolRenderArgs, ctx: CanvasRenderingContext2D) { + + const comp = EllipseHitAreaComponent.getEllipseComponent(obj); + + const { x, y, width, height } = comp; + + this.renderEllipseHelper(args, ctx, obj, x, y, width, height, true); + } + + private drawEllipse(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, angle: number) { + + const rx = w / 2; + const ry = h / 2; + + ctx.ellipse(x, y, rx, ry, Phaser.Math.DegToRad(angle), 0, Math.PI * 2); + } + + private renderCircle(obj: Sprite, args: editor.tools.ISceneToolRenderArgs, ctx: CanvasRenderingContext2D) { + + const comp = CircleHitAreaComponent.getCircleComponent(obj); + + const { x, y, radius } = comp; + + const width = radius * 2; + const height = width; + + this.renderEllipseHelper(args, ctx, obj, x, y, width, height, false); + } + + private renderEllipseHelper(args: editor.tools.ISceneToolRenderArgs, ctx: CanvasRenderingContext2D, obj: Sprite, x: number, y: number, width: number, height: number, dashedRect: boolean) { + + const origin = obj.getEditorSupport().computeDisplayOrigin(); + + if (obj instanceof Container) { + + origin.displayOriginX = 0; + origin.displayOriginY = 0; + } + + let x1 = x - origin.displayOriginX; + let y1 = y - origin.displayOriginY; + let x2 = x1 + width; + let y2 = y1 + height; + const tx = obj.getWorldTransformMatrix(); + + let points = [ + [x1, y1], + [x2, y1], + [x2, y2], + ] + .map(([x, y]) => tx.transformPoint(x, y)) + + .map(p => args.camera.getScreenPoint(p.x, p.y)); + + const [p1, p2, p3] = points; + + const screenWidth = Phaser.Math.Distance.BetweenPoints(p1, p2); + const screenHeight = Phaser.Math.Distance.BetweenPoints(p2, p3); + const angle = ui.editor.tools.SceneToolItem.getGlobalAngle(obj); + + ctx.save(); + + ctx.beginPath(); + ctx.strokeStyle = "black"; + ctx.lineWidth = 3; + this.drawEllipse(ctx, p1.x, p1.y, screenWidth, screenHeight, angle); + ctx.stroke(); + ctx.closePath(); + + ctx.beginPath(); + ctx.strokeStyle = EditHitAreaTool.TOOL_COLOR; + ctx.lineWidth = 1; + this.drawEllipse(ctx, p1.x, p1.y, screenWidth, screenHeight, angle); + ctx.stroke(); + ctx.closePath(); + + if (dashedRect) { + + // dashed rect + + x1 = x - origin.displayOriginX - width / 2; + y1 = y - origin.displayOriginY - height / 2; + x2 = x1 + width; + y2 = y1 + height; + + points = [ + [x1, y1], + [x2, y1], + [x2, y2], + [x1, y2], + [x1, y1] + ] + .map(([x, y]) => tx.transformPoint(x, y)) + + .map(p => args.camera.getScreenPoint(p.x, p.y)); + + ctx.setLineDash([1, 1]); + this.drawPath(ctx, points); + + ctx.restore(); + } + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaComponent.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaComponent.ts new file mode 100644 index 000000000..e8d5d0b08 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaComponent.ts @@ -0,0 +1,67 @@ +/// +/// +namespace phasereditor2d.scene.ui.sceneobjects { + + export class EllipseHitAreaComponent extends BaseHitAreaComponent { + + static x = HitAreaProperty(EllipseHitAreaComponent, "x", "X", "phaser:Phaser.Geom.Ellipse.x", 0); + static y = HitAreaProperty(EllipseHitAreaComponent, "y", "Y", "phaser:Phaser.Geom.Ellipse.y", 0); + static width = HitAreaProperty(EllipseHitAreaComponent, "width", "W", "phaser:Phaser.Geom.Ellipse.width", 0); + static height = HitAreaProperty(EllipseHitAreaComponent, "height", "H", "phaser:Phaser.Geom.Ellipse.height", 0); + + static position: IPropertyXY = { + label: "Offset", + x: this.x, + y: this.y + }; + + static size: IPropertyXY = { + label: "Size", + x: this.width, + y: this.height + }; + + public x = 0; + public y = 0; + public width = 0; + public height = 0; + + constructor(obj: ISceneGameObject) { + super(obj, HitAreaShape.ELLIPSE, [ + EllipseHitAreaComponent.x, + EllipseHitAreaComponent.y, + EllipseHitAreaComponent.width, + EllipseHitAreaComponent.height, + ]); + } + + static getEllipseComponent(obj: ISceneGameObject) { + + const objES = obj.getEditorSupport(); + + const comp = objES.getComponent(EllipseHitAreaComponent) as EllipseHitAreaComponent; + + return comp; + } + + protected _setDefaultValues(width: number, height: number): void { + + this.x = width / 2; + this.y = height / 2; + this.width = width; + this.height = height; + } + + protected override buildSetInteractiveCodeCOM( + args: ISetObjectPropertiesCodeDOMArgs, + obj: ISceneGameObject, + code: core.code.MethodCallCodeDOM): void { + + const { x, y, width, height } = this; + + code.arg(`new Phaser.Geom.Ellipse(${x}, ${y}, ${width}, ${height})`); + + code.arg("Phaser.Geom.Ellipse.Contains"); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaOffsetToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaOffsetToolItem.ts new file mode 100644 index 000000000..c43b11763 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaOffsetToolItem.ts @@ -0,0 +1,43 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class EllipseHitAreaOffsetToolItem extends BaseHitAreaOffsetToolItem { + + constructor(x: IAxisFactor, y: IAxisFactor) { + super(HitAreaShape.ELLIPSE, x, y); + } + + protected getToolOrigin(obj: ISceneGameObject): { originX: number; originY: number; } { + + return { originX: 0.5, originY: 0.5 }; + } + + protected getOffsetProperties(obj: ISceneGameObject): { x: IProperty; y: IProperty; } { + + const { x, y } = EllipseHitAreaComponent; + + return { x, y }; + } + + protected getSizeProperties(obj: ISceneGameObject): { width: IProperty; height: IProperty; } { + + const { width, height } = EllipseHitAreaComponent; + + return { width, height }; + } + + protected getKeyData(): string { + + return "EllipseHitAreaOffsetToolItem"; + } + + protected getOffsetSectionId(): string { + + return EllipseHitAreaSection.ID; + } + + protected createStopDragOperation(args: editor.tools.ISceneToolDragEventArgs): colibri.ui.ide.undo.Operation { + + return new EllipseOffsetOperation(args, obj => this.getInitialValue(obj)); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaSection.ts new file mode 100644 index 000000000..1f39f4a1d --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaSection.ts @@ -0,0 +1,38 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + import controls = colibri.ui.controls; + + export class EllipseHitAreaSection extends SceneGameObjectSection { + + static ID = "phasereditor2d.scene.ui.sceneobjects.EllipseHitAreaSection"; + + constructor(page: controls.properties.PropertyPage) { + super(page, EllipseHitAreaSection.ID, "Hit Area (Ellipse)"); + } + + createMenu(menu: controls.Menu) { + + this.createToolMenuItem(menu, EditHitAreaTool.ID); + + super.createMenu(menu); + } + + createForm(parent: HTMLDivElement) { + + const comp = this.createGridElementWithPropertiesXY(parent); + + this.createPropertyXYRow(comp, EllipseHitAreaComponent.position, false); + this.createPropertyXYRow(comp, EllipseHitAreaComponent.size, false); + } + + canEdit(obj: any, n: number): boolean { + + return HitAreaComponent.hasHitAreaShape(obj, HitAreaShape.ELLIPSE); + } + + canEditNumber(n: number): boolean { + + return n > 0; + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaSizeOperation.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaSizeOperation.ts new file mode 100644 index 000000000..d3f35ca3d --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaSizeOperation.ts @@ -0,0 +1,31 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class EllipseHitAreaSizeOperation extends editor.tools.SceneToolOperation<{ x: number, y: number }> { + + constructor( + toolArgs: editor.tools.ISceneToolContextArgs, + private getInitialSize: (obj: ISceneGameObject) => { x: number, y: number }) { + + super(toolArgs); + } + + getInitialValue(obj: ISceneGameObject): { x: number; y: number; } { + + return this.getInitialSize(obj); + } + + getFinalValue(obj: ISceneGameObject): { x: number; y: number; } { + + return { + x: EllipseHitAreaComponent.width.getValue(obj), + y: EllipseHitAreaComponent.height.getValue(obj) + }; + } + + setValue(obj: ISceneGameObject, value: { x: number; y: number; }) { + + EllipseHitAreaComponent.width.setValue(obj, value.x); + EllipseHitAreaComponent.height.setValue(obj, value.y); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaSizeToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaSizeToolItem.ts new file mode 100644 index 000000000..3574a28db --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseHitAreaSizeToolItem.ts @@ -0,0 +1,69 @@ +/// + +namespace phasereditor2d.scene.ui.sceneobjects { + + export class EllipseHitAreaSizeToolItem extends BaseHitAreaSizeToolItem { + + constructor(x: IAxisFactor, y: IAxisFactor) { + super(HitAreaShape.ELLIPSE, x, y); + } + + protected getToolOrigin(obj: ISceneGameObject): { originX: number; originY: number; } { + + return { originX: 0.5, originY: 0.5 }; + } + + private getHitAreaComp(obj: ISceneGameObject) { + + const objEs = obj.getEditorSupport(); + + const comp = objEs.getComponent(EllipseHitAreaComponent) as EllipseHitAreaComponent; + + return comp; + } + + protected computeSize(obj: ISceneGameObject): { width: number; height: number; } { + + const { width, height } = this.getHitAreaComp(obj); + + return { width, height }; + } + + protected getHitAreaOffset(obj: ISceneGameObject): { x: number; y: number; } { + + const { x, y } = this.getHitAreaComp(obj); + + return { x, y }; + } + + protected getDataKey(): string { + + return "EllipseHitAreaSizeToolItem"; + } + + protected getHitAreaSectionId(): string { + + return EllipseHitAreaSection.ID; + } + + protected onDragValues(obj: ISceneGameObject, changeX: boolean, changeY: boolean, width: number, height: number) { + + const comp = this.getHitAreaComp(obj); + + if (changeX) { + + comp.width = width; + } + + if (changeY) { + + comp.height = height; + } + } + + protected createStopDragOperation(args: editor.tools.ISceneToolDragEventArgs): colibri.ui.ide.undo.Operation { + + return new EllipseHitAreaSizeOperation(args, obj => this.getInitialSize(obj)); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseOffsetOperation.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseOffsetOperation.ts new file mode 100644 index 000000000..fab55ed46 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/EllipseOffsetOperation.ts @@ -0,0 +1,31 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class EllipseOffsetOperation extends editor.tools.SceneToolOperation<{ x: number, y: number }> { + + constructor( + toolArgs: editor.tools.ISceneToolContextArgs, + private getInitialOffset: (obj: ISceneGameObject) => { x: number, y: number }) { + + super(toolArgs); + } + + getInitialValue(obj: ISceneGameObject): { x: number; y: number; } { + + return this.getInitialOffset(obj); + } + + getFinalValue(obj: ISceneGameObject): { x: number; y: number; } { + + return { + x: EllipseHitAreaComponent.x.getValue(obj), + y: EllipseHitAreaComponent.y.getValue(obj) + }; + } + + setValue(obj: ISceneGameObject, value: { x: number; y: number; }) { + + EllipseHitAreaComponent.x.setValue(obj, value.x); + EllipseHitAreaComponent.y.setValue(obj, value.y); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/HitAreaComponent.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/HitAreaComponent.ts new file mode 100644 index 000000000..9c7c2337b --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/HitAreaComponent.ts @@ -0,0 +1,120 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export enum HitAreaShape { + NONE = "NONE", + RECTANGLE = "RECTANGLE", + CIRCLE = "CIRCLE", + ELLIPSE = "ELLIPSE", + POLYGON = "POLYGON", + PIXEL_PERFECT = "PIXEL_PERFECT" + } + + function getComp(obj: ISceneGameObject) { + + return obj.getEditorSupport().getComponent(HitAreaComponent) as HitAreaComponent; + } + + export class HitAreaComponent extends Component { + + static hitAreaShape: IEnumProperty = { + name: "hitArea.shape", + label: "Shape", + defValue: HitAreaShape.NONE, + getValue: obj => getComp(obj).getHitAreaShape(), + setValue: (obj, value) => getComp(obj).setHitAreaShape(value), + getValueLabel: value => value.toString(), + values: [ + HitAreaShape.NONE, + HitAreaShape.RECTANGLE, + HitAreaShape.CIRCLE, + HitAreaShape.ELLIPSE, + HitAreaShape.POLYGON, + HitAreaShape.PIXEL_PERFECT + ] + }; + + private _hitAreaShape: HitAreaShape; + + constructor(obj: ISceneGameObject) { + super(obj, [ + HitAreaComponent.hitAreaShape + ]); + + this._hitAreaShape = HitAreaShape.NONE; + } + + static getShapeComponent(obj: ISceneGameObject) { + + const shape = this.getShape(obj); + + switch (shape) { + + case HitAreaShape.RECTANGLE: + + return RectangleHitAreaComponent.getRectangleComponent(obj); + + case HitAreaShape.CIRCLE: + + return CircleHitAreaComponent.getCircleComponent(obj); + + case HitAreaShape.ELLIPSE: + + return EllipseHitAreaComponent.getEllipseComponent(obj); + + case HitAreaShape.POLYGON: + + return PolygonHitAreaComponent.getPolygonComponent(obj); + + case HitAreaShape.PIXEL_PERFECT: + + return PixelPerfectHitAreaComponent.getPixelPerfectComponent(obj) + } + + return undefined; + } + + static hasHitAreaShape(obj: ISceneGameObject, shape: HitAreaShape) { + + if (this.hasHitArea(obj)) { + + return this.getShape(obj) === shape; + } + + return false; + } + + static hasHitArea(obj: ISceneGameObject) { + + return GameObjectEditorSupport.hasObjectComponent(obj, HitAreaComponent); + } + + static getShape(obj: ISceneGameObject): HitAreaShape { + + return this.hitAreaShape.getValue(obj) + } + + getHitAreaShape() { + + return this._hitAreaShape; + } + + setHitAreaShape(hitAreaShape: HitAreaShape) { + + this._hitAreaShape = hitAreaShape; + } + + buildSetObjectPropertiesCodeDOM(args: ISetObjectPropertiesCodeDOMArgs): void { + + const objES = this.getEditorSupport(); + + if (objES.isPrefabInstance()) { + + if (objES.isUnlockedProperty(HitAreaComponent.hitAreaShape)) { + + const code = new core.code.MethodCallCodeDOM("removeInteractive", args.objectVarName); + args.statements.push(code); + } + } + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/HitAreaProperty.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/HitAreaProperty.ts new file mode 100644 index 000000000..9275c6fdd --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/HitAreaProperty.ts @@ -0,0 +1,42 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export function HitAreaProperty( + component: Function, + name: string, + label: string, + tooltip: string, + defValue: any, + ): IProperty { + + return { + name: `hitArea.${name}`, + label, + tooltip, + defValue, + getValue: (obj:ISceneGameObject) => { + + const objES = obj.getEditorSupport(); + + const comp = objES.getComponent(component); + + if (comp) { + + return comp[name]; + } + + return undefined; + }, + setValue: (obj:ISceneGameObject, value: any) => { + + const objES = obj.getEditorSupport(); + + const comp = objES.getComponent(component); + + if (comp) { + + comp[name] = value; + } + }, + }; + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/HitAreaSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/HitAreaSection.ts new file mode 100644 index 000000000..e783014d3 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/HitAreaSection.ts @@ -0,0 +1,58 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + import controls = colibri.ui.controls; + + export class HitAreaSection extends SceneGameObjectSection { + + constructor(page: controls.properties.PropertyPage) { + super(page, "phasereditor2d.scene.ui.sceneobjects.HitAreaSection", "Hit Area"); + } + + createForm(parent: HTMLDivElement) { + + const comp = this.createGridElement(parent, 3); + + const { hitAreaShape } = HitAreaComponent; + + const prop = { ...hitAreaShape }; + + prop.setValue = (obj, value) => { + + hitAreaShape.setValue(obj, value); + + if (value !== HitAreaShape.NONE) { + + const comp = HitAreaComponent.getShapeComponent(obj); + + comp.setDefaultValues(); + } + } + + this.createPropertyEnumRow(comp, prop, undefined, value => { + + if (value === HitAreaShape.PIXEL_PERFECT) { + + for(const obj of this.getSelection()) { + + if (!(obj instanceof Sprite) && !(obj instanceof Image)) { + + return false; + } + } + } + + return true; + }); + } + + canEdit(obj: any, n: number): boolean { + + return GameObjectEditorSupport.hasObjectComponent(obj, HitAreaComponent); + } + + canEditNumber(n: number): boolean { + + return n > 0; + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PixelPerfectHitAreaComponent.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PixelPerfectHitAreaComponent.ts new file mode 100644 index 000000000..1cbe18fb6 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PixelPerfectHitAreaComponent.ts @@ -0,0 +1,47 @@ +/// +/// +namespace phasereditor2d.scene.ui.sceneobjects { + + export class PixelPerfectHitAreaComponent extends BaseHitAreaComponent { + + static alphaTolerance = HitAreaProperty(PixelPerfectHitAreaComponent, "alphaTolerance", "Alpha Tolerance", "phaser:Phaser.Input.InputPlugin.makePixelPerfect(alphaTolerance)", 1); + + public alphaTolerance = 1; + + constructor(obj: ISceneGameObject) { + super(obj, HitAreaShape.PIXEL_PERFECT, [ + PixelPerfectHitAreaComponent.alphaTolerance + ]); + } + + static getPixelPerfectComponent(obj: ISceneGameObject) { + + const objES = obj.getEditorSupport(); + + const comp = objES.getComponent(PixelPerfectHitAreaComponent) as PixelPerfectHitAreaComponent; + + return comp; + } + + protected _setDefaultValues(width: number, height: number): void { + // nothing + } + + protected buildSetInteractiveCodeCOM( + args: ISetObjectPropertiesCodeDOMArgs, + obj: ISceneGameObject, + code: core.code.MethodCallCodeDOM): void { + + const objES = this.getEditorSupport(); + + const scene = objES.getScene(); + + const sceneVar = scene.isPrefabSceneType() ? + "this.scene" : "this"; + + const alpha = this.alphaTolerance === 1 ? "" : this.alphaTolerance; + + code.arg(`${sceneVar}.input.makePixelPerfect(${alpha})`); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PixelPerfectHitAreaSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PixelPerfectHitAreaSection.ts new file mode 100644 index 000000000..356fba033 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PixelPerfectHitAreaSection.ts @@ -0,0 +1,37 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + import controls = colibri.ui.controls; + + export class PixelPerfectHitAreaSection extends SceneGameObjectSection { + + static ID = "phasereditor2d.scene.ui.sceneobjects.PixelPerfectHitAreaSection"; + + constructor(page: controls.properties.PropertyPage) { + super(page, PixelPerfectHitAreaSection.ID, "Hit Area (Pixel Perfect)"); + } + + createMenu(menu: controls.Menu) { + + this.createToolMenuItem(menu, EditHitAreaTool.ID); + + super.createMenu(menu); + } + + createForm(parent: HTMLDivElement) { + + const comp = this.createGridElement(parent, 3); + + this.createPropertyFloatRow(comp, PixelPerfectHitAreaComponent.alphaTolerance, false); + } + + canEdit(obj: any, n: number): boolean { + + return HitAreaComponent.hasHitAreaShape(obj, HitAreaShape.PIXEL_PERFECT); + } + + canEditNumber(n: number): boolean { + + return n > 0; + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PolygonHitAreaComponent.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PolygonHitAreaComponent.ts new file mode 100644 index 000000000..6b79d76f1 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PolygonHitAreaComponent.ts @@ -0,0 +1,60 @@ +/// +/// +namespace phasereditor2d.scene.ui.sceneobjects { + + export class PolygonHitAreaComponent extends BaseHitAreaComponent { + + static points = HitAreaProperty(PolygonHitAreaComponent, "points", "Points", "The polygon's points, in a string format `X1 Y1 Y2 X2...`", ""); + + public points: string; + + constructor(obj: ISceneGameObject) { + super(obj, HitAreaShape.POLYGON, [ + PolygonHitAreaComponent.points + ]); + + this.points = ""; + } + + get vectors() { + + const vectors: Phaser.Math.Vector2[] = []; + + const chunks = this.points.split(" ").map(s => s.trim()).filter(s => s.length > 0); + + for (let i = 0; i < chunks.length - 1; i += 2) { + + const x = Number.parseFloat(chunks[i]); + const y = Number.parseFloat(chunks[i + 1]); + + vectors.push(new Phaser.Math.Vector2(x, y)) + } + + return vectors; + } + + static getPolygonComponent(obj: ISceneGameObject) { + + const objES = obj.getEditorSupport(); + + const comp = objES.getComponent(PolygonHitAreaComponent) as PolygonHitAreaComponent; + + return comp; + } + + protected _setDefaultValues(w: number, h: number): void { + + this.points = `0 ${h * 0.25} ${w/2} 0 ${w} ${h * 0.25} ${w} ${h} 0 ${h}`; + } + + protected override buildSetInteractiveCodeCOM( + args: ISetObjectPropertiesCodeDOMArgs, + obj: ISceneGameObject, + code: core.code.MethodCallCodeDOM): void { + + code.arg(`new Phaser.Geom.Polygon("${this.points}")`); + + code.arg("Phaser.Geom.Polygon.Contains"); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PolygonHitAreaSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PolygonHitAreaSection.ts new file mode 100644 index 000000000..5e93459f7 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PolygonHitAreaSection.ts @@ -0,0 +1,37 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + import controls = colibri.ui.controls; + + export class PolygonHitAreaSection extends SceneGameObjectSection { + + static ID = "phasereditor2d.scene.ui.sceneobjects.PolygonHitAreaSection"; + + constructor(page: controls.properties.PropertyPage) { + super(page, PolygonHitAreaSection.ID, "Hit Area (Polygon)"); + } + + createMenu(menu: controls.Menu) { + + this.createToolMenuItem(menu, EditHitAreaTool.ID); + + super.createMenu(menu); + } + + createForm(parent: HTMLDivElement) { + + const comp = this.createGridElement(parent, 3); + + this.createPropertyStringDialogRow(comp, PolygonHitAreaComponent.points, false); + } + + canEdit(obj: any, n: number): boolean { + + return HitAreaComponent.hasHitAreaShape(obj, HitAreaShape.POLYGON); + } + + canEditNumber(n: number): boolean { + + return n > 0; + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PolygonHitAreaToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PolygonHitAreaToolItem.ts new file mode 100644 index 000000000..6b55224ce --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/PolygonHitAreaToolItem.ts @@ -0,0 +1,395 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class PolygonHitAreaToolItem + extends editor.tools.SceneToolItem implements editor.tools.ISceneToolItemXY { + private _dragging: boolean; + private _draggingIndex: number; + private _newPoint: Phaser.Math.Vector2; + private _newPointIndex: number; + private _highlightPointIndex = -1; + + constructor() { + super(); + } + + handleDoubleClick(args: editor.tools.ISceneToolContextArgs): boolean { + + if (this._highlightPointIndex >= 0) { + + const op = new editor.undo.SceneSnapshotOperation(args.editor, async () => { + + this.handleDeleteCommand(args); + }); + + args.editor.getUndoManager().add(op); + + return true; + } + + return false; + } + + handleDeleteCommand(args: editor.tools.ISceneToolContextArgs): boolean { + + if (this._highlightPointIndex >= 0) { + + const comp = PolygonHitAreaComponent.getPolygonComponent(args.objects[0]); + + const points = comp.vectors; + + if (points.length <= 3) { + + return true; + } + + const newPoints = []; + + for (let i = 0; i < points.length; i++) { + + if (i !== this._highlightPointIndex) { + + newPoints.push(points[i]); + } + } + + comp.points = newPoints.map(p => `${p.x} ${p.y}`).join(" "); + + return true; + } + + return false; + } + + getPoint(args: editor.tools.ISceneToolContextArgs): { x: number; y: number; } { + + return { x: 0, y: 0 }; + } + + render(args: editor.tools.ISceneToolRenderArgs) { + + if (args.objects.length !== 1) { + + return; + } + + let nearPoint: Phaser.Geom.Point; + let nearPointIndex: number; + + const comp = PolygonHitAreaComponent.getPolygonComponent(args.objects[0]); + + const sprite = comp.getObject() as Sprite; + + const points = this.getPolygonScreenPoints(comp); + + const ctx = args.canvasContext; + + const cursor = args.editor.getMouseManager().getMousePosition(); + + // find highlihting point + + let highlightPoint: Phaser.Math.Vector2; + let highlightPointIndex = -1; + + for (let i = 0; i < points.length; i++) { + + const point = points[i]; + + if (this.isCursorOnPoint(cursor.x, cursor.y, point)) { + + highlightPoint = point; + highlightPointIndex = i; + + break; + } + } + + + if (!highlightPoint) { + + // paint near line + + let nearLine: Phaser.Geom.Line; + let nearLineDistance = Number.MAX_VALUE; + + const line = new Phaser.Geom.Line(); + const tempPoint = new Phaser.Geom.Point(); + + for (let i = 0; i < points.length; i++) { + + const p1 = points[i]; + const p2 = points[(i + 1) % points.length]; + + line.setTo(p1.x, p1.y, p2.x, p2.y); + + Phaser.Geom.Line.GetNearestPoint(line, new Phaser.Geom.Point(cursor.x, cursor.y), tempPoint); + + const d = Phaser.Math.Distance.BetweenPoints(cursor, tempPoint); + + if (d < 10 && d < nearLineDistance) { + + const lineLength = Phaser.Geom.Line.Length(line); + const length1 = Phaser.Math.Distance.BetweenPoints(p1, tempPoint); + const length2 = Phaser.Math.Distance.BetweenPoints(p2, tempPoint); + + // check the point is inside the segment + if (length1 <= lineLength && length2 <= lineLength) { + + nearLineDistance = d; + nearPointIndex = i; + + if (nearLine) { + + nearLine.setTo(line.x1, line.y1, line.x2, line.y2); + nearPoint.setTo(tempPoint.x, tempPoint.y); + + } else { + + nearLine = new Phaser.Geom.Line(line.x1, line.y1, line.x2, line.y2); + nearPoint = new Phaser.Geom.Point(tempPoint.x, tempPoint.y); + } + } + } + } + + if (nearLine) { + + const color = args.canEdit ? "#fff" : editor.tools.SceneTool.COLOR_CANNOT_EDIT; + + // draw near line + + ctx.save(); + + ctx.translate(nearLine.x1, nearLine.y1); + + const angle = this.globalAngle(sprite); + + ctx.rotate(Phaser.Math.DegToRad(angle)); + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(nearLine.x2 - nearLine.x1, nearLine.y2 - nearLine.y1); + ctx.strokeStyle = color; + ctx.lineWidth = 3; + ctx.stroke(); + + ctx.restore(); + + // draw near point + + ctx.save(); + + ctx.translate(nearPoint.x, nearPoint.y); + ctx.rotate(Phaser.Math.DegToRad(this.globalAngle(sprite))); + + this.drawRect(ctx, args.canEdit ? "#faa" : editor.tools.SceneTool.COLOR_CANNOT_EDIT); + + ctx.restore(); + } + } + + // paint highlight point + + for (const point of points) { + + ctx.save(); + + ctx.translate(point.x, point.y); + + const angle = this.globalAngle(sprite); + + ctx.rotate(Phaser.Math.DegToRad(angle)); + + const color = point === highlightPoint ? "#f00" : EditHitAreaTool.TOOL_COLOR; + + this.drawRect(ctx, args.canEdit ? color : editor.tools.SceneTool.COLOR_CANNOT_EDIT); + + ctx.restore(); + } + + this._newPoint = nearPoint ? this.getPolygonLocalPoint(sprite, nearPoint) : undefined; + this._newPointIndex = nearPointIndex; + this._highlightPointIndex = highlightPointIndex; + } + + private getPolygonScreenPoints(comp: PolygonHitAreaComponent) { + + const points: Phaser.Math.Vector2[] = []; + + const worldPoint = new Phaser.Math.Vector2(0, 0); + + const obj = comp.getObject() as Sprite; + + const { displayOriginX, displayOriginY } = obj.getEditorSupport().computeDisplayOrigin(); + + const tx = obj.getWorldTransformMatrix(); + + for (const point of comp.vectors) { + + tx.transformPoint( + point.x - displayOriginX, + point.y - displayOriginY, + worldPoint); + + const screenPoint = obj.scene.cameras.main.getScreenPoint(worldPoint.x, worldPoint.y); + + points.push(screenPoint); + } + + return points; + } + + getPolygonLocalPoint(polygon: Sprite, point: { x: number, y: number }) { + + const camera = polygon.scene.cameras.main; + point = camera.getWorldPoint2(point.x, point.y); + + const localPoint = polygon.getWorldTransformMatrix().applyInverse(point.x, point.y); + localPoint.x += polygon.displayOriginX; + localPoint.y += polygon.displayOriginY; + + return localPoint; + } + + containsPoint(args: editor.tools.ISceneToolDragEventArgs): boolean { + + if (this._newPoint) { + + return true; + } + + const points = this.getPolygonScreenPoints(PolygonHitAreaComponent.getPolygonComponent(args.objects[0])); + + for (const point of points) { + + if (this.isCursorOnPoint(args.x, args.y, point)) { + + return true; + } + } + + return false; + } + + private isCursorOnPoint(cursorX: number, cursorY: number, point: { x: number, y: number }) { + + return cursorX >= point.x - 5 && cursorX <= point.x + 5 + && cursorY >= point.y - 5 && cursorY <= point.y + 5; + } + + private _startDragTime = 0; + + onStartDrag(args: editor.tools.ISceneToolDragEventArgs): void { + + this._startDragTime = Date.now(); + + const comp = PolygonHitAreaComponent.getPolygonComponent(args.objects[0]); + + if (this._newPoint) { + + const points = comp.vectors; + + let newPoints: { x: number, y: number }[] = []; + + for (let i = 0; i < points.length; i++) { + + const point = points[i]; + + newPoints.push(point); + + if (this._newPointIndex === i) { + + newPoints.push(this._newPoint); + } + } + + comp.points = newPoints.map(p => `${p.x} ${p.y}`).join(" "); + } + + const cursor = args.editor.getMouseManager().getMousePosition(); + + const points = this.getPolygonScreenPoints(comp); + + for (let i = 0; i < points.length; i++) { + + const point = points[i]; + + if (this.isCursorOnPoint(cursor.x, cursor.y, point)) { + + comp.getObject().setData("PolygonHitAreaToolItem", { + initPoints: comp.points + }); + + this._draggingIndex = i; + this._dragging = true; + + break; + } + } + } + + static getInitialPoints(obj: ISceneGameObject) { + + return obj.getData("PolygonHitAreaToolItem").initPoints; + } + + isValidFor(objects: ISceneGameObject[]): boolean { + + const obj = objects[0]; + + if (obj) { + + if (HitAreaComponent.hasHitArea(obj) && HitAreaComponent.hasHitAreaShape(obj, HitAreaShape.POLYGON)) { + + return true; + } + } + + return false; + } + + onDrag(args: editor.tools.ISceneToolDragEventArgs): void { + + if (!this._dragging) { + + return; + } + + const comp = PolygonHitAreaComponent.getPolygonComponent(args.objects[0]); + + const points = comp.vectors; + + const point = points[this._draggingIndex]; + + const newPoint = args.editor.getScene().getCamera().getWorldPoint(args.x, args.y); + + const sprite = comp.getObject() as Sprite; + + sprite.getWorldTransformMatrix().applyInverse(newPoint.x, newPoint.y, newPoint); + + const {displayOriginX, displayOriginY} = sprite.getEditorSupport().computeDisplayOrigin(); + + point.x = newPoint.x + displayOriginX; + point.y = newPoint.y + displayOriginY; + + comp.points = points.map(p => `${p.x} ${p.y}`).join(" "); + + args.editor.updateInspectorViewSection(PolygonHitAreaSection.ID); + } + + onStopDrag(args: editor.tools.ISceneToolDragEventArgs): void { + + this._newPoint = undefined; + + if (this._dragging) { + + if (Date.now() - this._startDragTime > 300) { + + args.editor.getUndoManager().add(new PolygonHitAreaOperation(args)); + + } + + this._dragging = false; + } + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaComponent.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaComponent.ts new file mode 100644 index 000000000..f10ddb978 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaComponent.ts @@ -0,0 +1,64 @@ +/// +namespace phasereditor2d.scene.ui.sceneobjects { + + export class RectangleHitAreaComponent extends BaseHitAreaComponent { + + static x = HitAreaProperty(RectangleHitAreaComponent, "x", "X", "phaser:Phaser.Geom.Rectangle.x", 0); + static y = HitAreaProperty(RectangleHitAreaComponent, "y", "Y", "phaser:Phaser.Geom.Rectangle.y", 0); + static width = HitAreaProperty(RectangleHitAreaComponent, "width", "W", "phaser:Phaser.Geom.Rectangle.width", 0); + static height = HitAreaProperty(RectangleHitAreaComponent, "height", "H", "phaser:Phaser.Geom.Rectangle.height", 0); + static position: IPropertyXY = { + label: "Offset", + x: this.x, + y: this.y + }; + static size: IPropertyXY = { + label: "Size", + x: this.width, + y: this.height + }; + + public x = 0; + public y = 0; + public width = 0; + public height = 0; + + constructor(obj: ISceneGameObject) { + super(obj, HitAreaShape.RECTANGLE, [ + RectangleHitAreaComponent.x, + RectangleHitAreaComponent.y, + RectangleHitAreaComponent.width, + RectangleHitAreaComponent.height, + ]); + } + + static getRectangleComponent(obj: ISceneGameObject) { + + const objES = obj.getEditorSupport(); + + const comp = objES.getComponent(RectangleHitAreaComponent) as RectangleHitAreaComponent; + + return comp; + } + + protected _setDefaultValues(width: number, height: number): void { + + this.x = 0; + this.y = 0; + this.width = width; + this.height = height; + } + + protected override buildSetInteractiveCodeCOM( + args: ISetObjectPropertiesCodeDOMArgs, + obj: ISceneGameObject, + code: core.code.MethodCallCodeDOM): void { + + const { x, y, width, height } = this; + + code.arg(`new Phaser.Geom.Rectangle(${x}, ${y}, ${width}, ${height})`); + + code.arg("Phaser.Geom.Rectangle.Contains"); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaOffsetToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaOffsetToolItem.ts new file mode 100644 index 000000000..bfc2c535e --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaOffsetToolItem.ts @@ -0,0 +1,43 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class RectangleHitAreaOffsetToolItem extends BaseHitAreaOffsetToolItem { + + constructor(x: IAxisFactor, y: IAxisFactor) { + super(HitAreaShape.RECTANGLE, x, y); + } + + protected getToolOrigin(obj: ISceneGameObject): { originX: number; originY: number; } { + + return { originX: 0, originY: 0 }; + } + + protected getOffsetProperties(obj: ISceneGameObject): { x: IProperty; y: IProperty; } { + + const { x, y } = RectangleHitAreaComponent; + + return { x, y }; + } + + protected getSizeProperties(obj: ISceneGameObject): { width: IProperty; height: IProperty; } { + + const { width, height } = RectangleHitAreaComponent; + + return { width, height }; + } + + protected getKeyData(): string { + + return "RectangleHitAreaOffsetToolItem"; + } + + protected getOffsetSectionId(): string { + + return RectangleHitAreaSection.ID; + } + + protected createStopDragOperation(args: editor.tools.ISceneToolDragEventArgs): colibri.ui.ide.undo.Operation { + + return new RectangleOffsetOperation(args, obj => this.getInitialValue(obj)) + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaSection.ts new file mode 100644 index 000000000..211660d20 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaSection.ts @@ -0,0 +1,38 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + import controls = colibri.ui.controls; + + export class RectangleHitAreaSection extends SceneGameObjectSection { + + static ID = "phasereditor2d.scene.ui.sceneobjects.RectangleHitAreaSection"; + + constructor(page: controls.properties.PropertyPage) { + super(page, RectangleHitAreaSection.ID, "Hit Area (Rectangle)"); + } + + createMenu(menu: controls.Menu) { + + this.createToolMenuItem(menu, EditHitAreaTool.ID); + + super.createMenu(menu); + } + + createForm(parent: HTMLDivElement) { + + const comp = this.createGridElementWithPropertiesXY(parent); + + this.createPropertyXYRow(comp, RectangleHitAreaComponent.position, false); + this.createPropertyXYRow(comp, RectangleHitAreaComponent.size, false); + } + + canEdit(obj: any, n: number): boolean { + + return HitAreaComponent.hasHitAreaShape(obj, HitAreaShape.RECTANGLE); + } + + canEditNumber(n: number): boolean { + + return n > 0; + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaSizeOperation.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaSizeOperation.ts new file mode 100644 index 000000000..1d7b845fd --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaSizeOperation.ts @@ -0,0 +1,31 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class RectangleHitAreaSizeOperation extends editor.tools.SceneToolOperation<{ x: number, y: number }> { + + constructor( + toolArgs: editor.tools.ISceneToolContextArgs, + private getInitialSize: (obj: ISceneGameObject) => { x: number, y: number }) { + + super(toolArgs); + } + + getInitialValue(obj: ISceneGameObject): { x: number; y: number; } { + + return this.getInitialSize(obj); + } + + getFinalValue(obj: ISceneGameObject): { x: number; y: number; } { + + return { + x: RectangleHitAreaComponent.width.getValue(obj), + y: RectangleHitAreaComponent.height.getValue(obj) + }; + } + + setValue(obj: ISceneGameObject, value: { x: number; y: number; }) { + + RectangleHitAreaComponent.width.setValue(obj, value.x); + RectangleHitAreaComponent.height.setValue(obj, value.y); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaSizeToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaSizeToolItem.ts new file mode 100644 index 000000000..0db8afa7c --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleHitAreaSizeToolItem.ts @@ -0,0 +1,69 @@ +/// + +namespace phasereditor2d.scene.ui.sceneobjects { + + export class RectangleHitAreaSizeToolItem extends BaseHitAreaSizeToolItem { + + constructor(x: IAxisFactor, y: IAxisFactor) { + super(HitAreaShape.RECTANGLE, x, y); + } + + protected getToolOrigin(): { originX: number; originY: number; } { + + return { originX: 0, originY: 0 }; + } + + private getHitAreaComp(obj: ISceneGameObject) { + + const objEs = obj.getEditorSupport(); + + const comp = objEs.getComponent(RectangleHitAreaComponent) as RectangleHitAreaComponent; + + return comp; + } + + protected computeSize(obj: ISceneGameObject): { width: number; height: number; } { + + const { width, height } = this.getHitAreaComp(obj); + + return { width, height }; + } + + protected getHitAreaOffset(obj: ISceneGameObject): { x: number; y: number; } { + + const { x, y } = this.getHitAreaComp(obj); + + return { x, y }; + } + + protected getDataKey(): string { + + return "RectangleHitAreaSizeToolItem"; + } + + protected getHitAreaSectionId(): string { + + return RectangleHitAreaSection.ID; + } + + protected onDragValues(obj: ISceneGameObject, changeX: boolean, changeY: boolean, width: number, height: number) { + + const comp = this.getHitAreaComp(obj); + + if (changeX) { + + comp.width = width; + } + + if (changeY) { + + comp.height = height; + } + } + + protected createStopDragOperation(args: editor.tools.ISceneToolDragEventArgs): colibri.ui.ide.undo.Operation { + + return new RectangleHitAreaSizeOperation(args, obj => this.getInitialSize(obj)); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleOffsetOperation.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleOffsetOperation.ts new file mode 100644 index 000000000..bed82602e --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/hitArea/RectangleOffsetOperation.ts @@ -0,0 +1,31 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class RectangleOffsetOperation extends editor.tools.SceneToolOperation<{ x: number, y: number }> { + + constructor( + toolArgs: editor.tools.ISceneToolContextArgs, + private getInitialOffset: (obj: ISceneGameObject) => { x: number, y: number }) { + + super(toolArgs); + } + + getInitialValue(obj: ISceneGameObject): { x: number; y: number; } { + + return this.getInitialOffset(obj); + } + + getFinalValue(obj: ISceneGameObject): { x: number; y: number; } { + + return { + x: RectangleHitAreaComponent.x.getValue(obj), + y: RectangleHitAreaComponent.y.getValue(obj) + }; + } + + setValue(obj: ISceneGameObject, value: { x: number; y: number; }) { + + RectangleHitAreaComponent.x.setValue(obj, value.x); + RectangleHitAreaComponent.y.setValue(obj, value.y); + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ListSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ListSection.ts deleted file mode 100644 index 6505d9310..000000000 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ListSection.ts +++ /dev/null @@ -1,82 +0,0 @@ -namespace phasereditor2d.scene.ui.sceneobjects { - - import controls = colibri.ui.controls; - - export class ListSection extends editor.properties.BaseSceneSection { - - constructor(page: controls.properties.PropertyPage) { - super(page, "phasereditor2d.scene.ui.sceneobjects.ListSection", "List", true); - } - - createForm(parent: HTMLDivElement) { - - const comp = this.createGridElement(parent); - comp.style.gridTemplateColumns = "1fr"; - comp.style.gridTemplateRows = "1fr auto"; - - const viewer = new controls.viewers.TreeViewer("phasereditor2d.scene.ui.sceneobjects.ListSection"); - viewer.setCellSize(64, true); - viewer.setLabelProvider(new editor.outline.SceneEditorOutlineLabelProvider()); - viewer.setCellRendererProvider(new editor.outline.SceneEditorOutlineRendererProvider()); - viewer.setContentProvider(new controls.viewers.ArrayTreeContentProvider()); - - const filteredViewer = new colibri.ui.ide.properties - .FilteredViewerInPropertySection(this.getPage(), viewer, true); - comp.appendChild(filteredViewer.getElement()); - - this.addUpdater(() => { - - const list = this.getSelectionFirstElement(); - - const map = this.getEditor().getScene().buildObjectIdMap(); - - const input = list.getObjectIds() - .map(id => map.get(id)) - .filter(obj => obj !== undefined); - - viewer.setInput(input); - viewer.setSelection([]); - }); - - const btnRow = document.createElement("div"); - - comp.appendChild(btnRow); - - const selectBtn = this.createButton(btnRow, "Select In Scene", () => { - - this.getEditor().setSelection(viewer.getSelection()); - }); - selectBtn.style.float = "right"; - - const removeBtn = this.createButton(btnRow, "Remove From List", () => { - - this.getUndoManager().add( - new RemoveObjectsFromListOperation( - this.getEditor(), - this.getSelectionFirstElement(), - viewer.getSelection()) - ); - - }); - - removeBtn.style.float = "right"; - removeBtn.style.marginRight = "5px"; - - viewer.eventSelectionChanged.addListener(() => { - - selectBtn.disabled = removeBtn.disabled = viewer.getSelection().length === 0; - }); - } - - canEdit(obj: any, n: number): boolean { - - return obj instanceof ObjectList; - } - - canEditNumber(n: number): boolean { - - return n === 1; - } - - } -} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ListVariableSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ListVariableSection.ts index 0a68e51c1..ba880480e 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ListVariableSection.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ListVariableSection.ts @@ -52,14 +52,16 @@ namespace phasereditor2d.scene.ui.sceneobjects { this.createLabel(comp, "Scope", "The lexical scope of the object."); + // I skip the LOCAL scope here because a List without a variable + // has no sense const items = [{ - name: "Method", + name: "METHOD", value: ObjectScope.METHOD }, { - name: "Class", + name: "CLASS", value: ObjectScope.CLASS }, { - name: "Public", + name: "PUBLIC", value: ObjectScope.PUBLIC }]; diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectList.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectList.ts index 3a859f10a..1860119ee 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectList.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectList.ts @@ -8,6 +8,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { private _label: string; private _scope: ObjectScope; private _objectIds: string[]; + private _items: ObjectListItem[]; constructor() { @@ -15,37 +16,77 @@ namespace phasereditor2d.scene.ui.sceneobjects { this._label = "list"; this._scope = ObjectScope.CLASS; this._objectIds = []; + this._items = []; + } + + getItemsWithObjects(scene: Scene) { + + const map = scene.buildObjectIdMap(); + + for(const item of this._items) { + + item.setObject(map.get(item.getObjectId())); + } + + return this._items.filter(item => Boolean(item.getObject())); + } + + updateOrderIdsFromItems() { + + this._objectIds = this._items.map(i => i.getObjectId()); + } + + removeItem(id: string) { + + this._items = this._items.filter(i => i.getId() !== id); + + this._objectIds = this._items.map(i => i.getObjectId()); + } + + getItems() { + + return this._items; } getObjectIds() { + return this._objectIds; } setObjectsIds(ids: string[]) { + this._objectIds = ids; + + this._items = this._objectIds.map(id => new ObjectListItem(this, id)); } getId() { + return this._id; } setId(id: string) { + this._id = id; } getLabel() { + return this._label; } setLabel(label: string) { + this._label = label; } getScope() { + return this._scope; } setScope(scope: ObjectScope) { + this._scope = scope; } @@ -91,8 +132,8 @@ namespace phasereditor2d.scene.ui.sceneobjects { this._id = data.id; this._label = data.label; - this._objectIds = data.objectIds || []; this._scope = data.scope || sceneobjects.ObjectScope.CLASS; + this.setObjectsIds(data.objectIds || []); } writeJSON(data: json.IObjectListData) { diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectListItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectListItem.ts new file mode 100644 index 000000000..5647e68f3 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectListItem.ts @@ -0,0 +1,41 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class ObjectListItem { + + private _parent: ObjectList; + private _objectId: string; + private _obj: ISceneGameObject; + + constructor(parent: ObjectList, objectId: string) { + + this._parent = parent; + this._objectId = objectId; + } + + getParent() { + + return this._parent; + } + + getId() { + + return `ListItem#${this._objectId}`; + } + + getObjectId() { + + return this._objectId; + } + + getObject(): ISceneGameObject { + + return this._obj; + } + + setObject(obj: ISceneGameObject) { + + this._obj = obj; + } + } + +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectListItemCellRenderer.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectListItemCellRenderer.ts new file mode 100644 index 000000000..ce40413bb --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectListItemCellRenderer.ts @@ -0,0 +1,47 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + import controls = colibri.ui.controls; + + export class ObjectListItemCellRenderer implements controls.viewers.ICellRenderer { + + private _objRenderer: controls.viewers.ICellRenderer; + + constructor(objRenderer: controls.viewers.ICellRenderer) { + + this._objRenderer = objRenderer; + } + + private adaptArgs(args: controls.viewers.RenderCellArgs) { + + const args2 = args.clone(); + + args2.obj = (args2.obj as ObjectListItem).getObject(); + + return args2; + } + + renderCell(args: controls.viewers.RenderCellArgs): void { + + this._objRenderer.renderCell(this.adaptArgs(args)); + } + + cellHeight(args: controls.viewers.RenderCellArgs) { + + return this._objRenderer.cellHeight(this.adaptArgs(args)); + } + + preload(args: controls.viewers.PreloadCellArgs): Promise { + + const clone = args.clone(); + + clone.obj = (clone.obj as ObjectListItem).getObject(); + + return this._objRenderer.preload(clone); + } + + get layout() { + + return this._objRenderer.layout; + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectListItemSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectListItemSection.ts new file mode 100644 index 000000000..09dd3c1b0 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/ObjectListItemSection.ts @@ -0,0 +1,63 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + import controls = colibri.ui.controls; + + export class ObjectListItemSection extends editor.properties.BaseSceneSection { + + constructor(page: controls.properties.PropertyPage) { + super(page, "phasereditor2d.scene.ui.sceneobjects.ObjectListItemSection", "List Item"); + } + + createForm(parent: HTMLDivElement) { + + const comp = this.createGridElement(parent, 2); + + { + this.createLabel(comp, "Name", "The object's name."); + + const field = this.createText(comp, true); + + this.addUpdater(() => { + + const labels = this.getSelectedGameObjects() + + .map(obj => obj.getEditorSupport().getLabel()); + + field.value = this.flatValues_StringOneOrNothing(labels); + }); + } + + { + const btn = this.createButton(comp, "Select Game Object", () => { + + this.getEditor().setSelection(this.getSelectedGameObjects()); + }); + + btn.style.gridColumn = "1 / span 2"; + } + } + + private getSelectedGameObjects() { + + const map = this.getEditor().getScene().buildObjectIdMap(); + + return this.getSelection() + + .map(item => item.getObjectId()) + + .map(id => map.get(id)) + + .filter(obj => Boolean(obj)); + } + + canEdit(obj: any, n: number): boolean { + + return obj instanceof ObjectListItem; + } + + canEditNumber(n: number): boolean { + + return n > 0; + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/undo/ListOrderOperation.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/undo/ListOrderOperation.ts new file mode 100644 index 000000000..57989f437 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/undo/ListOrderOperation.ts @@ -0,0 +1,136 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + import DepthMove = scene.ui.editor.undo.DepthMove; + import SceneEditor = scene.ui.editor.SceneEditor; + + export class ListOrderOperation extends ListsSnapshotOperation { + + private _depthMove: DepthMove; + + constructor(editor: SceneEditor, depthMove: DepthMove) { + super(editor); + + this._depthMove = depthMove; + } + + static allow(editor: SceneEditor, move: DepthMove) { + + // sort the selection and filter off non-game-objects + let sel = this.sortedSelection(editor); + + // if the sorted selection contains all the selected objects + if (sel.length !== editor.getSelection().length) { + + return false; + } + + for (const obj of sel) { + + //const parent = obj.getEditorSupport().getObjectParent(); + const siblings = obj.getParent().getItems(); + + const index = siblings.indexOf(obj); + + const len = siblings.length; + + if (move === "Bottom" || move === "Down") { + + if (index === len - 1) { + + return false; + } + + } else { // Top || Up + + if (index === 0) { + + return false; + } + } + } + + return true; + } + + private static sortedSelection(editor: SceneEditor) { + + const sel = editor.getSelectedListItems(); + + sel.sort((a, b) => { + + const aa = a.getParent().getItems().indexOf(a); + const bb = a.getParent().getItems().indexOf(b); + + return aa - bb; + }); + + return sel; + } + + performChange(lists: ObjectLists): void { + + const editor = this.getEditor(); + + const sel = ListOrderOperation.sortedSelection(editor); + + switch (this._depthMove) { + + case "Bottom": + + for (const obj of sel) { + + const siblings = obj.getParent().getItems(); + + Phaser.Utils.Array.BringToTop(siblings, obj); + + obj.getParent().updateOrderIdsFromItems(); + } + + break; + + case "Top": + + for (let i = 0; i < sel.length; i++) { + + const obj = sel[sel.length - i - 1]; + + const siblings = obj.getParent().getItems(); + + Phaser.Utils.Array.SendToBack(siblings, obj) + + obj.getParent().updateOrderIdsFromItems(); + } + + break; + + case "Down": + + for (let i = 0; i < sel.length; i++) { + + const obj = sel[sel.length - i - 1]; + + const siblings = obj.getParent().getItems(); + + Phaser.Utils.Array.MoveUp(siblings, obj); + + obj.getParent().updateOrderIdsFromItems(); + } + + break; + + case "Up": + + for (const obj of sel) { + + const siblings = obj.getParent().getItems(); + + Phaser.Utils.Array.MoveDown(siblings, obj); + + obj.getParent().updateOrderIdsFromItems(); + } + + break; + } + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/undo/ListsSnapshotOperation.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/undo/ListsSnapshotOperation.ts index fe9a5e594..b07ff1593 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/undo/ListsSnapshotOperation.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/list/undo/ListsSnapshotOperation.ts @@ -6,6 +6,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { private _before: json.IObjectListData[]; private _after: json.IObjectListData[]; + private _selection: string[]; constructor(editor: editor.SceneEditor) { super(editor); @@ -15,9 +16,12 @@ namespace phasereditor2d.scene.ui.sceneobjects { async execute() { - const lists = this._editor.getScene().getObjectLists(); + const scene = this._editor.getScene(); + + const lists = scene.getObjectLists(); this._before = lists.toJSON_lists(); + this._selection = this._editor.getSelectionManager().getSelectionIds(); this.performChange(lists); @@ -36,7 +40,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { this._editor.refreshOutline(); - this._editor.getSelectionManager().refreshSelection(); + this._editor.getSelectionManager().setSelectionByIds(this._selection); } undo(): void { diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/UserComponentsEditorComponent.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/UserComponentsEditorComponent.ts index f4f843c1c..8bee025f5 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/UserComponentsEditorComponent.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/UserComponentsEditorComponent.ts @@ -23,7 +23,16 @@ namespace phasereditor2d.scene.ui.sceneobjects { writeJSON(ser: core.json.Serializer) { - ser.getData()["components"] = [...this._compNames]; + const data = ser.getData() as core.json.IObjectData; + + data.components = [...this._compNames]; + + // we don't want to serialize an empty components array, + // if it is the case, we exclude it from the file + if (data.components.length === 0) { + + delete data.components; + } for (const compName of this._compNames) { @@ -39,7 +48,9 @@ namespace phasereditor2d.scene.ui.sceneobjects { readJSON(ser: core.json.Serializer) { - this._compNames = ser.getData()["components"] || []; + const data = ser.getData() as core.json.IObjectData; + + this._compNames = data.components || []; for (const compName of this._compNames) { diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/VariableComponent.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/VariableComponent.ts index 7ac25f298..795ab3c72 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/VariableComponent.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/VariableComponent.ts @@ -1,3 +1,4 @@ +/// namespace phasereditor2d.scene.ui.sceneobjects { import code = scene.core.code; @@ -26,12 +27,12 @@ namespace phasereditor2d.scene.ui.sceneobjects { static scope: IEnumProperty = { name: "scope", tooltip: "The variable lexical scope.", - defValue: ObjectScope.METHOD, + defValue: ObjectScope.LOCAL, local: true, getValue: obj => obj.getEditorSupport().getScope(), setValue: (obj, value) => obj.getEditorSupport().setScope(value), - values: [ObjectScope.METHOD, ObjectScope.CLASS, ObjectScope.PUBLIC, ObjectScope.NESTED_PREFAB], - getValueLabel: value => value.split("_").map(v => v[0] + v.substring(1).toLowerCase()).join(" ") + values: OBJECT_SCOPES, + getValueLabel: value => value.replaceAll("_", " ") // value.split("_").map(v => v[0] + v.substring(1).toLowerCase()).join(" ") }; constructor(obj: ISceneGameObject) { @@ -47,7 +48,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { if (this.getEditorSupport().isUseGameObjectName()) { const dom = new code.AssignPropertyCodeDOM("name", args.objectVarName); - + dom.valueLiteral(this.getEditorSupport().getLabel()); args.statements.push(dom); diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/GameObjectVariableSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/GameObjectVariableSection.ts index 7b2f9ccec..b542d97f5 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/GameObjectVariableSection.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/GameObjectVariableSection.ts @@ -61,7 +61,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { return true; } - return scope !== ObjectScope.NESTED_PREFAB; + return !isNestedPrefabScope(scope); }); } } diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/NestedPrefabObjectVariableSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/NestedPrefabObjectVariableSection.ts index c99fa52c8..2abbdc15d 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/NestedPrefabObjectVariableSection.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/NestedPrefabObjectVariableSection.ts @@ -19,8 +19,34 @@ namespace phasereditor2d.scene.ui.sceneobjects { const comp = this.createGridElement(parent, 2); - const { btn } = GameObjectVariableSection.createTypeEditor(this, comp); - btn.disabled = true; + { + // Name + + this.createLabel(comp, "Name", "The name of the variable associated to this object. This name is used by the compiler."); + + const text = this.createText(comp, true); + + this.addUpdater(() => { + + text.value = this.flatValues_StringOneOrNothing( + this.getSelection() + .map(obj => this.getVarName(obj))); + }) + } + + { + // Type + + const { btn } = GameObjectVariableSection.createTypeEditor(this, comp); + btn.disabled = true; + } + } + + private getVarName(obj: ISceneGameObject) { + + const varName = core.code.SceneCodeDOMBuilder.getPrefabInstanceVarName(obj); + + return varName; } canEdit(obj: any, n: number): boolean { diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/SceneGameObjectSection.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/SceneGameObjectSection.ts index 8dab6e833..6ed9555ea 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/SceneGameObjectSection.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/properties/SceneGameObjectSection.ts @@ -204,7 +204,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { return text; } - createPropertyEnumRow(parent: HTMLElement, prop: IEnumProperty, lockIcon: boolean = true) { + createPropertyEnumRow(parent: HTMLElement, prop: IEnumProperty, lockIcon: boolean = true, filter?: (v: any) => boolean) { if (lockIcon) { @@ -214,7 +214,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { const label = this.createLabel(parent, prop.label, PhaserHelp(prop.tooltip)); label.style.gridColumn = "2"; - const btn = this.createEnumField(parent, prop); + const btn = this.createEnumField(parent, prop, undefined, filter); return btn; } diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/IAxisFactor.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/IAxisFactor.ts new file mode 100644 index 000000000..31c698f63 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/IAxisFactor.ts @@ -0,0 +1,4 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export declare type IAxisFactor = 0 | 0.5 | 1; +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/PolygonHitAreaOperation.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/PolygonHitAreaOperation.ts new file mode 100644 index 000000000..6b7fc3008 --- /dev/null +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/PolygonHitAreaOperation.ts @@ -0,0 +1,24 @@ +namespace phasereditor2d.scene.ui.sceneobjects { + + export class PolygonHitAreaOperation extends editor.tools.SceneToolOperation { + + getInitialValue(obj: any) { + + return PolygonHitAreaToolItem.getInitialPoints(obj); + } + + getFinalValue(obj: any) { + + const comp = PolygonHitAreaComponent.getPolygonComponent(obj); + + return comp.points; + } + + setValue(obj: any, value: string) { + + const comp = PolygonHitAreaComponent.getPolygonComponent(obj); + + comp.points = value; + } + } +} \ No newline at end of file diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/ScaleToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/ScaleToolItem.ts index 6238071e3..0982eaf7a 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/ScaleToolItem.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/ScaleToolItem.ts @@ -1,15 +1,13 @@ namespace phasereditor2d.scene.ui.sceneobjects { - export declare type IScaleAxis = 0 | 0.5 | 1; - export class ScaleToolItem extends editor.tools.SceneToolItem implements editor.tools.ISceneToolItemXY { - private _x: IScaleAxis; - private _y: IScaleAxis; + private _x: IAxisFactor; + private _y: IAxisFactor; private _dragging: boolean; - constructor(x: IScaleAxis, y: IScaleAxis) { + constructor(x: IAxisFactor, y: IAxisFactor) { super(); this._x = x; diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/SizeToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/SizeToolItem.ts index 44a63924d..90657015b 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/SizeToolItem.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/object/tools/SizeToolItem.ts @@ -3,11 +3,11 @@ namespace phasereditor2d.scene.ui.sceneobjects { export class SizeToolItem extends editor.tools.SceneToolItem implements editor.tools.ISceneToolItemXY { - private _x: IScaleAxis; - private _y: IScaleAxis; + private _x: IAxisFactor; + private _y: IAxisFactor; private _dragging: boolean; - constructor(x: IScaleAxis, y: IScaleAxis) { + constructor(x: IAxisFactor, y: IAxisFactor) { super(); this._x = x; diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/ArcadeBodyOffsetToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/ArcadeBodyOffsetToolItem.ts index bc94a0c33..5f891a704 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/ArcadeBodyOffsetToolItem.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/ArcadeBodyOffsetToolItem.ts @@ -5,7 +5,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { extends BaseArcadeBodyOffsetToolItem implements editor.tools.ISceneToolItemXY { - constructor(x: IScaleAxis, y: IScaleAxis) { + constructor(x: IAxisFactor, y: IAxisFactor) { super(x, y); } diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/BaseArcadeBodyOffsetToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/BaseArcadeBodyOffsetToolItem.ts index 561cf4928..e9fcdce6d 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/BaseArcadeBodyOffsetToolItem.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/BaseArcadeBodyOffsetToolItem.ts @@ -3,11 +3,11 @@ namespace phasereditor2d.scene.ui.sceneobjects { export class BaseArcadeBodyOffsetToolItem extends editor.tools.SceneToolItem implements editor.tools.ISceneToolItemXY { - private _x: IScaleAxis; - private _y: IScaleAxis; + private _x: IAxisFactor; + private _y: IAxisFactor; private _dragging: boolean; - constructor(x: IScaleAxis, y: IScaleAxis) { + constructor(x: IAxisFactor, y: IAxisFactor) { super(); this._x = x; diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/BaseArcadeBodySizeToolItem.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/BaseArcadeBodySizeToolItem.ts index 40cbf7bd2..99cc61769 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/BaseArcadeBodySizeToolItem.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/BaseArcadeBodySizeToolItem.ts @@ -3,11 +3,11 @@ namespace phasereditor2d.scene.ui.sceneobjects { export abstract class BaseArcadeBodySizeToolItem extends editor.tools.SceneToolItem implements editor.tools.ISceneToolItemXY { - private _x: IScaleAxis; - private _y: IScaleAxis; + private _x: IAxisFactor; + private _y: IAxisFactor; private _dragging: boolean; - constructor(x: IScaleAxis, y: IScaleAxis) { + constructor(x: IAxisFactor, y: IAxisFactor) { super(); this._x = x; @@ -27,7 +27,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { ); } - protected getScreenPointOfObject(args: ui.editor.tools.ISceneToolContextArgs, sprite: Sprite, fx: number, fy: number, removeRotation = false) { + protected getScreenPointOfObject(args: ui.editor.tools.ISceneToolContextArgs, sprite: Sprite, fx: number, fy: number) { const worldPoint = new Phaser.Geom.Point(0, 0); @@ -47,11 +47,6 @@ namespace phasereditor2d.scene.ui.sceneobjects { const tx = sprite.getWorldTransformMatrix(); - if (removeRotation) { - - tx.rotate(-tx.rotation); - } - tx.transformPoint(x, y, worldPoint); return args.camera.getScreenPoint(worldPoint.x, worldPoint.y); diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/ColliderEditorSupport.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/ColliderEditorSupport.ts index 628fe1c0d..1dcfa153a 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/ColliderEditorSupport.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/physics/ColliderEditorSupport.ts @@ -8,7 +8,7 @@ namespace phasereditor2d.scene.ui.sceneobjects { new ColliderComponent(obj) ); - this.setScope(ObjectScope.METHOD); + this.setScope(ObjectScope.LOCAL); } destroy() { diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/ObjectConstructorPropertyType.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/ObjectConstructorPropertyType.ts index 6940accd0..d00e3d041 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/ObjectConstructorPropertyType.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/ObjectConstructorPropertyType.ts @@ -50,8 +50,6 @@ namespace phasereditor2d.scene.ui.sceneobjects { const finder = ScenePlugin.getInstance().getSceneFinder(); - await finder.preload(controls.EMPTY_PROGRESS_MONITOR); - const file = finder.getSceneFiles() .find(f => this.valueToString(null, f) === value); diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/ObjectVarPropertyType.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/ObjectVarPropertyType.ts index 398158fc1..5c0c3023c 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/ObjectVarPropertyType.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/ObjectVarPropertyType.ts @@ -55,11 +55,16 @@ namespace phasereditor2d.scene.ui.sceneobjects { protected valueToString(viewer: colibri.ui.controls.viewers.TreeViewer, value: any): string { - const support = EditorSupport.getEditorSupport(value); + const objES = EditorSupport.getEditorSupport(value); - if (support) { + if (objES) { - return support.getLabel(); + if (objES instanceof GameObjectEditorSupport && objES.isNestedPrefabInstance()) { + + return core.code.SceneCodeDOMBuilder.getPrefabInstanceVarName(value); + } + + return objES.getLabel(); } return viewer.getLabelProvider().getLabel(value); @@ -76,14 +81,31 @@ namespace phasereditor2d.scene.ui.sceneobjects { const foundElement = [undefined]; - scene.visitAll(obj => { + scene.visitAllAskChildren(obj => { if (!foundElement[0]) { - if (obj.getEditorSupport().getLabel() === value) { + const objES = obj.getEditorSupport(); + + if (objES.isNestedPrefabInstance()) { + + const objVarName = core.code.SceneCodeDOMBuilder.getPrefabInstanceVarName(obj); + + if (objVarName === value) { + + foundElement[0] = obj; + + return false; + } + + } else if (core.code.formatToValidVarName(objES.getLabel()) === value) { foundElement[0] = obj; + + return false; } + + return true; } }); diff --git a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/SceneKeyPropertyType.ts b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/SceneKeyPropertyType.ts index 5457469ee..ea73ce649 100644 --- a/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/SceneKeyPropertyType.ts +++ b/source/editor/plugins/phasereditor2d.scene/src/ui/sceneobjects/userProperties/SceneKeyPropertyType.ts @@ -19,8 +19,6 @@ namespace phasereditor2d.scene.ui.sceneobjects { const finder = ScenePlugin.getInstance().getSceneFinder(); - await finder.preload(controls.EMPTY_PROGRESS_MONITOR); - const file = finder.getSceneFiles() .find(f => this.valueToString(null, f) === value);