From 3117c60fee6de74ddbcbaf8e36a88dab7c930191 Mon Sep 17 00:00:00 2001 From: IronMonk88 Date: Sun, 21 Feb 2021 21:51:37 -0800 Subject: [PATCH] 1.0.1 updates --- classes/TerrainLayer.js | 901 ++++++++++++++---------------------- classes/terrain.js | 361 +++++++++++++++ classes/terrainconfig.js | 51 ++ classes/terraincontrols.js | 2 +- classes/terrainhud.js | 91 ++++ classes/terrainlayer.old.js | 641 +++++++++++++++++++++++++ js/controls.js | 68 ++- js/settings.js | 56 +++ module.json | 9 +- templates/terrain-hud.html | 2 +- terrain-main.js | 64 +++ 11 files changed, 1640 insertions(+), 606 deletions(-) create mode 100644 classes/terrain.js create mode 100644 classes/terrainconfig.js create mode 100644 classes/terrainhud.js create mode 100644 classes/terrainlayer.old.js create mode 100644 js/settings.js create mode 100644 terrain-main.js diff --git a/classes/TerrainLayer.js b/classes/TerrainLayer.js index 5cb96c1..5cf28fe 100644 --- a/classes/TerrainLayer.js +++ b/classes/TerrainLayer.js @@ -1,583 +1,366 @@ -//TERRAIN LAYER -let TLControlPress = false; -class TerrainSquare extends PIXI.Graphics { - constructor(coord,...args){ - super(...args); - this.coord = coord; - let topLeft = canvas.grid.grid.getPixelsFromGridPosition(coord.x,coord.y) - this.thePosition = `${topLeft[0]}.${topLeft[1]}`; - - } -} -export class TerrainHighlight extends PIXI.Graphics { - constructor(name,...args){ - super(...args); +import { Terrain } from './terrain.js'; +import { TerrainConfig } from './terrainconfig.js'; +import { TerrainHUD } from './terrainhud.js'; + +export let debug = (...args) => { + if (debugEnabled > 1) console.log("DEBUG: terrainlayer | ", ...args); +}; +export let log = (...args) => console.log("terrainlayer | ", ...args); +export let warn = (...args) => { + if (debugEnabled > 0) console.warn("terrainlayer | ", ...args); +}; +export let error = (...args) => console.error("terrainlayer | ", ...args); +export let i18n = key => { + return game.i18n.localize(key); +}; + +export let setting = key => { + return game.settings.get("TerrainLayer", key); +}; + +export class TerrainLayer extends PlaceablesLayer { + constructor() { + super(); + this.showterrain = game.settings.get("TerrainLayer", "showterrain"); + this.defaultmultiple = 2; + } - /** - * Track the Grid Highlight name - * @type {String} - */ - this.name = name; + /** @override */ + static get layerOptions() { + return mergeObject(super.layerOptions, { + zIndex: 15, + controllableObjects: true, + //objectClass: Note, + //sheetClass: NoteConfig, + objectClass: Terrain, + sheetClass: TerrainConfig, + rotatableObjects: false + }); + } + + static get multipleOptions() { + return [0.5, 2, 3, 4]; + } + + static multipleText(multiple) { + return (parseInt(multiple) == 1 || parseInt(multiple) == 0.5 ? '1/2' : multiple); + } + +/* -------------------------------------------- */ + + get costGrid() { + log('costGrid is deprecated, please use the cost function instead'); + if (this._costGrid == undefined) { + this.buildCostGrid(canvas.scene.data.terrain); + } + return this._costGrid; + } /** - * Track distinct positions which have already been highlighted - * @type {Set} + * Tile objects on this layer utilize the TileHUD + * @type {TerrainHUD} */ - this.visible = canvas.scene.getFlag('TerrainLayer','sceneVisibility') || true; - this.positions = new Set(); - } - - /* -------------------------------------------- */ - - /** - * Record a position that is highlighted and return whether or not it should be rendered - * @param {Number} x The x-coordinate to highlight - * @param {Number} y The y-coordinate to highlight - * @return {Boolean} Whether or not to draw the highlight for this location - */ - highlight(pxX, pxY) { - let key = `${pxX}.${pxY}`; - if ( this.positions.has(key) ) return false; - this.positions.add(key); - return true; - } - - /* -------------------------------------------- */ - - /** - * Extend the Graphics clear logic to also reset the highlighted positions - * @param args - */ - clear(...args) { - super.clear(...args); - this.positions = new Set(); - } - - - /* -------------------------------------------- */ - - /** - * Extend how this Graphics container is destroyed to also remove parent layer references - * @param args - */ - destroy(...args) { - delete canvas.terrain.highlightLayers[this.name]; - super.destroy(...args); - } -} -export class TerrainLayer extends CanvasLayer{ - constructor(){ - super(); - this.scene = null; - // this.sceneId = this.scene._id; - //this.layerName = `DifficultTerrain.${this.scene._id}`; - this.highlight = null; - this.mouseInteractionManager = null; - this.dragging = false; - // this._addListeners(); - - } - async draw(){ - - this._deRegisterMouseListeners() - await super.draw(); - console.log('draw') - this.highlightLayers = {}; - this.scene = canvas.scene; - this.sceneId = this.scene._id; - this.layerName = `DifficultTerrain.${this.scene._id}`; - this.highlight = this.addChild(new PIXI.Container()); - this.addHighlightLayer(this.layerName); - this.costGrid = this.scene.getFlag('TerrainLayer','costGrid') ?? {}; - Hooks.once('canvasReady',this.buildFromCostGrid.bind(this)) - this._addListeners(); - return this; - } - async tearDown(){ - console.log('tearDown') - super.tearDown(); - this._deRegisterMouseListeners() - this._deRegisterKeyboardListeners(); - } - async toggle(emit = false){ - this.highlight.children[0].visible = !this.highlight.children[0].visible; - if(game.user.isGM && emit){ - await canvas.scene.setFlag('TerrainLayer','sceneVisibility', this.highlight.children[0].visible ) - game.socket.emit('module.TerrainLayer',{action:'toggle',arguments:[]}) + get hud() { + return canvas.hud.terrain; } - } - _addListeners() { - - // Define callback functions for mouse interaction events - const callbacks = { - dragLeftStart: this._onDragLeftStart.bind(this), - dragLeftMove: this._onDragLeftMove.bind(this), - clickRight: this._onClickRight.bind(this), - dragRightMove: this._onDragRightMove.bind(this), - dragLeftDrop:this._onDragLeftDrop.bind(this) - }; - - // Create and activate the interaction manager - const permissions = {}; - const mgr = new MouseInteractionManager(this, this, permissions, callbacks); - this.mouseInteractionManager = mgr.activate(); - - } - addHighlightLayer(name) { - const layer = this.highlightLayers[name]; - if ( !layer || layer._destroyed ) { - this.highlightLayers[name] = this.highlight.addChild(new TerrainHighlight(name)); - - canvas.terrain.highlight.children[0].visible = (typeof canvas.scene.getFlag('TerrainLayer','sceneVisibility') !='undefined') ? canvas.scene.getFlag('TerrainLayer','sceneVisibility'):true; - } - return this.highlightLayers[name]; - } - getHighlightLayer(name) { - return this.highlightLayers[name]; - } - /** - * Clear a specific Highlight graphic - * @param name - */ - clearHighlightLayer(name) { - const layer = this.highlightLayers[name]; - if ( layer ) layer.clear(); - } - /* -------------------------------------------- */ - - /** - * Destroy a specific Highlight graphic - * @param name - */ - destroyHighlightLayer(name) { - const layer = this.highlightLayers[name]; - this.highlight.removeChild(layer); - layer.destroy(); - } - highlightPosition(name, options) { - const layer = this.highlightLayers[name]; - if ( !layer ) return false; - if(canvas.grid.type == 1) - this.highlightGridPosition(layer, options); - else if(canvas.grid.type == 2 || canvas.grid.type == 3 || canvas.grid.type == 4 || canvas.grid.type == 5) - this.highlightHexPosition(layer,options); - } - /** @override */ - highlightGridPosition(layer , {gridX, gridY, multiple=2,type='ground'}={}) { - //GRID ALREADY HIGHLIGHTED - let gsW = canvas.grid.grid.w; - let gsH = canvas.grid.grid.h; - - let px = canvas.grid.grid.getPixelsFromGridPosition(gridX,gridY) - - const key = `${px[0]}.${px[1]}`; - - layer.highlight(px[0],px[1]); - let s = canvas.dimensions.size; - let terrainSquare = new TerrainSquare({x:gridX,y:gridY}) - let offset = 15; - terrainSquare.x = px[0]; - terrainSquare.y = px[1]; - terrainSquare.width = gsW; - terrainSquare.height = gsH; - terrainSquare.lineStyle(7, 0xffffff, 0.5); - terrainSquare.moveTo((gsW/2), offset); - terrainSquare.lineTo(offset, gsH-offset); - terrainSquare.lineTo(gsW-offset, gsH-offset); - terrainSquare.lineTo((gsW/2), offset); - terrainSquare.closePath(); - terrainSquare.blendMode = PIXI.BLEND_MODES.OVERLAY; - - let text = new PIXI.Text('x'+multiple,{fontFamily : 'Arial', fontSize: 12, fill : 0xffffff,opacity:game.settings.get('TerrainLayer','opacity'), align : 'center'}) - text.blendMode = PIXI.BLEND_MODES.OVERLAY; - text.anchor.set(0.5,0.5); - text.x = gsW/2; - text.y = (gsH/2)+7; - - terrainSquare.addChild(text); - terrainSquare.scale.x = game.settings.get('TerrainLayer','scale'); - terrainSquare.scale.y = game.settings.get('TerrainLayer','scale'); - terrainSquare.alpha = game.settings.get('TerrainLayer','opacity') - // console.log(terrainSquare) - layer.addChild(terrainSquare); - - // this.addToCostGrid(gridX,gridY); - - } - highlightHexPosition(layer,{gridX,gridY,multiple=2,type='ground'}={}){ - - let gsW = Math.floor(canvas.grid.grid.w); - let gsH = Math.round(canvas.grid.grid.h); - - let topLeft = canvas.grid.grid.getPixelsFromGridPosition(gridX,gridY) - let pxX = topLeft[0]; - let pxY = topLeft[1]; - - const points = canvas.grid.grid.options.columns ? canvas.grid.grid.constructor.flatHexPoints : canvas.grid.grid.constructor.pointyHexPoints; - const coords = points.reduce((arr, p) => { - arr.push(topLeft[0] + (p[0]*gsW)); - arr.push(topLeft[1] + (p[1]*gsH)); - return arr; - }, []); - - - let col = canvas.grid.grid.columns; - let even = canvas.grid.grid.even; - - let terrainSquare = new TerrainSquare({x:gridX,y:gridY}) - - layer.highlight(pxX,pxY); - - let offset = gsH* 0.16; - - const halfW = (gsW/2) - - terrainSquare.y = topLeft[1]; - terrainSquare.x = topLeft[0]; - - terrainSquare.width = gsW; - terrainSquare.height = gsH; - - let text = new PIXI.Text('x'+multiple,{fontFamily : 'Arial', fontSize: 12, fill : 0xffffff,opacity:0.5, align : 'center'}) - - - if(canvas.grid.type == 4 || canvas.grid.type == 5){ - terrainSquare.lineStyle(4, 0xffffff, 0.5); - terrainSquare.moveTo(halfW,offset*2); - terrainSquare.lineTo(offset*2, gsW-(offset*3)); - terrainSquare.lineTo(gsW-(offset*2), gsW-(offset*3)); - terrainSquare.lineTo(halfW, offset*2); - text.y = (gsH/2)+3; - }else{ - terrainSquare.lineStyle(7, 0xffffff, 0.5); - terrainSquare.moveTo(halfW,offset); - terrainSquare.lineTo(offset, gsW-offset); - terrainSquare.lineTo(gsW-offset, gsW-offset); - terrainSquare.lineTo(halfW, offset); - text.y = (gsH/2)+7; - } - - terrainSquare.closePath(); - terrainSquare.blendMode = PIXI.BLEND_MODES.OVERLAY; - - - text.blendMode = PIXI.BLEND_MODES.OVERLAY; - text.anchor.set(0.5,0.5); - text.x = gsW/2; - - - terrainSquare.addChild(text); - - - layer.addChild(terrainSquare); - // this.addToCostGrid(gridX,gridY,multiple); - - } - _registerMouseListeners() { - this.addListener('pointerup', this._pointerUp); - this.dragging = false; - } - _registerKeyboardListeners() { - $(document).keydown((event) => { - - //if (ui.controls.activeControl !== this.layername) return; - switch(event.which){ - case 27: - event.stopPropagation(); - ui.menu.toggle(); - break; - case 17: - TLControlPress = true; - break; - default: - break; - } - }); - $(document).keyup((event)=>{ - switch(event.which){ - case 17: - TLControlPress = false; - break; - default: - break; - } - }) - } - _deRegisterMouseListeners(){ - this.removeListener('pointerup', this._pointerUp); - } - _deRegisterKeyboardListeners(){ - $(document).off('keydown') - $(document).off('keyup'); - } - async addTerrain(x,y,emit=false,batch=true){ - - this.highlightPosition(this.layerName,{gridX:x,gridY:y}) - this.addToCostGrid(x,y); - if(game.user.isGM && emit){ - if(!batch) await this.updateCostGridFlag(); - const data = { - action:'addTerrain', - arguments:[x,y] - } - game.socket.emit('module.TerrainLayer', data) + + /* + async draw() { + canvas.scene._data.terrain = canvas.scene.data.terrain = (canvas.scene.data.flags?.TerrainLayer?.data || []); + super.draw(); + }*/ + + async draw() { + canvas.scene.data.terrain = []; + + if (canvas.scene.data.flags?.TerrainLayer) { + for (let [k, v] of Object.entries(canvas.scene.data.flags?.TerrainLayer)) { + if (k.startsWith('terrain')) { + if (k != 'terrainundefined') + canvas.scene.data.terrain.push(v); + else + canvas.scene.unsetFlag('TerrainLayer', k); + } + }; + } + + const d = canvas.dimensions; + this.width = d.width; + this.height = d.height; + this.hitArea = d.rect; + this.zIndex = this.constructor.layerOptions.zIndex; + + // Create objects container which can be sorted + this.objects = this.addChild(new PIXI.Container()); + this.objects.sortableChildren = true; + this.objects.visible = false; + + + // Create preview container which is always above objects + this.preview = this.addChild(new PIXI.Container()); + + // Create and draw objects + const promises = canvas.scene.data.terrain.map(data => { + const obj = this.createObject(data); + return obj.draw(); + }); + + // Wait for all objects to draw + this.visible = true; + return Promise.all(promises || []); } - } - async updateTerrain(x,y,emit=false,batch=true){ - const layer = canvas.terrain.getHighlightLayer(this.layerName); - let [pxX,pxY] = canvas.grid.grid.getPixelsFromGridPosition(x,y) - const key = `${pxX}.${pxY}`; - let square = this.getSquare(layer,key) + async buildCostGrid(data) { + this._costGrid = {}; + for (let grid of data) { + let multiple = grid.multiple; + let type = 'ground'; + if (typeof this._costGrid[grid.y] === 'undefined') + this._costGrid[grid.y] = {}; + this._costGrid[grid.y][grid.x] = { multiple, type }; - let cost = this.costGrid[x][y]; - if(cost.multiple < game.settings.get('TerrainLayer','maxMultiple')){ - this.costGrid[x][y].multiple+=1; + } + } - }else{ - this.costGrid[x][y].multiple=2; + async toggle(show, emit = false) { + //this.highlight.children[0].visible = !this.highlight.children[0].visible; + if (show == undefined) + show = !this.showterrain; + this.showterrain = show; + game.settings.set("TerrainLayer", "showterrain", this.showterrain); + if (game.user.isGM && emit) { + //await canvas.scene.setFlag('TerrainLayer','sceneVisibility', this.highlight.children[0].visible ) + game.socket.emit('module.TerrainLayer', { action: 'toggle', arguments: [this.showterrain] }) + } } - square.getChildAt(0).text = `x${cost.multiple}`; - if(game.user.isGM && emit){ - if(!batch) await this.updateCostGridFlag(); - const data = { - action:'updateTerrain', - arguments:[x,y] - } - game.socket.emit('module.TerrainLayer', data) + + deactivate() { + super.deactivate(); + if (this.objects) this.objects.visible = true; + } + + async updateMany(data, options = {}) { + const user = game.user; + + const pending = new Map(); + data = data instanceof Array ? data : [data]; + for (let d of data) { + if (!d._id) throw new Error("You must provide an id for every Embedded Entity in an update operation"); + pending.set(d._id, d); + } + + // Difference each update against existing data + const updates = canvas.scene.data.terrain.reduce((arr, d) => { + if (!pending.has(d._id)) return arr; + let update = pending.get(d._id); + + // Diff the update against current data + if (options.diff) { + update = diffObject(d, expandObject(update)); + if (isObjectEmpty(update)) return arr; + update["_id"] = d._id; + } + + // Call pre-update hooks to ensure the update is allowed to proceed + if (!options.noHook) { + const allowed = Hooks.call(`preUpdateTerrain`, this, d, update, options, user._id); + if (allowed === false) { + console.debug(`TerrainLayer | Terrain update prevented by preUpdate hook`); + return arr; + } + } + + // Stage the update + arr.push(update); + return arr; + }, []); + if (!updates.length) return []; + + let flags = {}; + for (let u of updates) { + let key = `flags.TerrainLayer.terrain${u._id}`; + flags[key] = u; + } + + canvas.scene.update(flags); } - } - _pointerUp(e) { - let pos = e.data.getLocalPosition(canvas.app.stage); - let gridPt = canvas.grid.grid.getGridPositionFromPixels(pos.x,pos.y); - let [pxX,pxY] = canvas.grid.grid.getPixelsFromGridPosition(gridPt[0],gridPt[1]) - let [x,y] = gridPt; //Normalize the returned data because it's in [y,x] format - let gsW = Math.round(canvas.grid.grid.w); - let gsH = Math.floor(canvas.grid.grid.h); - let gs = Math.min(gsW,gsH) - let gridPX = {x:Math.round(x*gsH),y:Math.round(y*gsW)} - - const layer = canvas.terrain.getHighlightLayer(this.layerName); - if(canvas.grid.type == 0) { - alert('Difficult Terrain does not work with gridless maps.'); - return false; + + async deleteMany(ids, options = {}) { + //+++ need to update this to only respond to actual deletions + + let updates = {}; + let originals = []; + for (let id of ids) { + const object = this.get(id); + log('Removing terrain', object.data.x, object.data.y); + originals.push(object); + this.objects.removeChild(object); + delete this._controlled[id]; + object._onDelete(options, game.user.id); + object.destroy({ children: true }); + let key = `flags.TerrainLayer.-=terrain${id}`; + updates[key] = null; + } + + this.storeHistory("delete", originals); + + canvas.scene.update(updates); } - switch(e.data.button){ - case 0: - - if(game.activeTool == 'addterrain' && !this.dragging){ - - if(this.terrainExists(pxX,pxY)){ - this.updateTerrain(x,y,true,false); - }else{ - this.addTerrain(x,y,true,false) - } - }else if(game.activeTool == 'subtractterrain'){ - if(this.terrainExists(pxX,pxY)){ - this.removeTerrain(x,y,true,false); - } - + + _onClickLeft(event) { + super._onClickLeft(event); + if (game.activeTool == 'addterrain') { + this.createTerrain(event.data.getLocalPosition(canvas.app.stage)); + //make sure there isn't a terrain already there + /* + let pos = event.data.getLocalPosition(canvas.app.stage); + let gridPt = canvas.grid.grid.getGridPositionFromPixels(pos.x, pos.y); + let [y, x] = gridPt; //Normalize the returned data because it's in [y,x] format + log('Adding terrain', x, y); + if (!this.terrainExists(x, y)) { + //const terrain = new Terrain({ x: x, y: y }); + //this.constructor.placeableClass.create(terrain.data); + //terrain.draw(); + this.constructor.placeableClass.create({ x: x, y: y, multiple: 2 }); + } + this._costGrid = null;*/ } - break; - default: - break; } - this.dragging = false; - } - terrainExists(pxX,pxY){ - const layer = canvas.terrain.getHighlightLayer(this.layerName); - const key = `${pxX}.${pxY}`; - if(layer.positions.has(key)) return true; - return false - } - - addToCostGrid(x,y,multiple=2,type='ground'){ - - if(typeof this.costGrid[x] === 'undefined') - this.costGrid[x] = {} - this.costGrid[x][y]={multiple,type}; - } - async updateCostGridFlag(){ - let x = duplicate(this.costGrid); - await canvas.scene.unsetFlag('TerrainLayer','costGrid'); - await canvas.scene.setFlag('TerrainLayer','costGrid',x) - } - buildFromCostGrid(update=true){ - canvas.terrain.highlight.children[0].removeChildren() - for(let x in this.costGrid){ - for(let y in this.costGrid[x]){ - - this.highlightPosition(this.layerName,{gridX:parseInt(x),gridY:parseInt(y),multiple:this.costGrid[x][y].multiple,update:update}) - } + + _onDragLeftStart(e) { + if (game.activeTool == "select") + this.dragging = true; } - } - async resetGrid(emit=false){ - this.getHighlightLayer(this.layerName).clear(); - this.getHighlightLayer(this.layerName).removeChildren(); - this.costGrid = {} - //only the GM who fired the event can set flag and emit, otherwise a game with two DM's might fire recursively. - if(game.user.isGM && emit){ - await this.scene.unsetFlag('TerrainLayer','costGrid'); - - game.socket.emit('module.TerrainLayer',{action:'resetGrid',arguments:[]}) + + _onDragLeftMove(event) { + if (game.activeTool == "select") + return this._onDragSelect(event); + else if (game.activeTool == 'addterrain') { + this.createTerrain(event.data.getLocalPosition(canvas.app.stage)); + /*let pos = event.data.getLocalPosition(canvas.app.stage); + let gridPt = canvas.grid.grid.getGridPositionFromPixels(pos.x, pos.y); + let [y, x] = gridPt; //Normalize the returned data because it's in [y,x] format + + if (!this.terrainExists(x, y)) { + //const terrain = new Terrain({ x: x, y: y }); + //terrain.draw(); + //this.constructor.placeableClass.create(terrain.data); + this.constructor.placeableClass.create({ x: x, y: y, multiple: 2 }); + } + this._costGrid = null;*/ + } + } + + _onDragSelect(event) { + // Extract event data + const { origin, destination } = event.data; + + // Determine rectangle coordinates + let coords = { + x: Math.min(origin.x, destination.x), + y: Math.min(origin.y, destination.y), + width: Math.abs(destination.x - origin.x), + height: Math.abs(destination.y - origin.y) + }; + + // Draw the select rectangle + canvas.controls.drawSelect(coords); + event.data.coords = coords; } - - } - selectSquares(coords){ - const startPx = canvas.grid.grid.getCenter(coords.x,coords.y) - const startGrid = canvas.grid.grid.getGridPositionFromPixels(startPx[0],startPx[1]) - - const endPx = canvas.grid.grid.getCenter(coords.x+coords.width,coords.y+coords.height) - const endGrid = canvas.grid.grid.getGridPositionFromPixels(endPx[0],endPx[1]) - - for(let x = startGrid[0];x<=endGrid[0];x++){ - for(let y = startGrid[1];y<=endGrid[1];y++){ - - if(game.activeTool == 'addterrain' && TLControlPress == false){ - //this.highlightPosition(this.layerName,{gridX:y,gridY:x}) - //this.addToCostGrid(x,y); - if(!this.terrainExists(y*canvas.dimensions.size,x*canvas.dimensions.size)) - this.addTerrain(x,y,true,true) - }else if(game.activeTool == 'subtractterrain' || TLControlPress){ - - - this.removeTerrain(x,y,true,true) - + + _onDragLeftDrop(e) { + if (game.activeTool == "select") { + canvas._onDragLeftDrop(event); + } + else if (game.activeTool != 'addterrain') { + super._onDragLeftDrop(event); } - } } - this.updateCostGridFlag(); - - } - - async removeTerrain(x,y,emit=false,batch=true){ - console.log('removeTerrain') - const [pxX,pxY] = canvas.grid.grid.getPixelsFromGridPosition(x,y) - const layer = canvas.terrain.getHighlightLayer(this.layerName); - const key = `${pxX}.${pxY}`; - if(!layer.positions.has(key)) return false; - let square = this.getSquare(layer,key); - square.destroy(); - layer.positions.delete(key); - this.removeFromCostGrid(x,y) - if(game.user.isGM && emit){ - if(!batch) await this.updateCostGridFlag(); - const data = { - action:'removeTerrain', - arguments:[x,y] - } - game.socket.emit('module.TerrainLayer', data) + + selectObjects({ x, y, width, height, releaseOptions = {}, controlOptions = {} } = {}) { + const oldSet = Object.values(this._controlled); + + let sPt = canvas.grid.grid.getGridPositionFromPixels(x, y); + let [y1, x1] = sPt; //Normalize the returned data because it's in [y,x] format + let dPt = canvas.grid.grid.getGridPositionFromPixels(x + width, y + height); + let [y2, x2] = dPt; //Normalize the returned data because it's in [y,x] format + + // Identify controllable objects + const controllable = this.placeables.filter(obj => obj.visible && (obj.control instanceof Function)); + const newSet = controllable.filter(obj => { + return !(obj.data.x < x1 || obj.data.x > x2 || obj.data.y < y1 || obj.data.y > y2); + }); + + // Release objects no longer controlled + const toRelease = oldSet.filter(obj => !newSet.includes(obj)); + toRelease.forEach(obj => obj.release(releaseOptions)); + + // Control new objects + if (isObjectEmpty(controlOptions)) controlOptions.releaseOthers = false; + const toControl = newSet.filter(obj => !oldSet.includes(obj)); + toControl.forEach(obj => obj.control(controlOptions)); + + // Return a boolean for whether the control set was changed + const changed = (toRelease.length > 0) || (toControl.length > 0); + if (changed) canvas.initializeSources(); + return changed; } - } - removeFromCostGrid(x,y,emit=false){ - if(typeof this.costGrid[x] == 'undefined') return false; - if(typeof this.costGrid[x][y] == 'undefined') return false; - - delete this.costGrid[x][y]; - - } - getSquare(layer,key){ - let square = layer.children.find((x)=>{ - return x.thePosition == key - }) - return square || false; - } - _onDragLeftStart(e){ - this.dragging = true; - } - - _onDragLeftMove(e){ - const isSelect = ["addterrain","subtractterrain"].includes(game.activeTool); - if ( isSelect ) return this._onDragSelect(e); - } - _onDragSelect(event) { - - // Extract event data - const {origin, destination} = event.data; - - // Determine rectangle coordinates - let coords = { - x: Math.min(origin.x, destination.x), - y: Math.min(origin.y, destination.y), - width: Math.abs(destination.x - origin.x), - height: Math.abs(destination.y - origin.y) - }; - - // Draw the select rectangle - canvas.controls.drawSelect(coords); - event.data.coords = coords; - - } - _onDragLeftDrop(e){ - const tool = game.activeTool; - // Conclude a select event - - const isSelect = ["addterrain","subtractterrain"].includes(tool); - if ( isSelect ) { - canvas.controls.select.clear(); - canvas.controls.select.active = false; - if ( tool === "addterrain" || tool === "subtractterrain") return this.selectSquares(e.data.coords); + + createTerrain(pos) { + let gridPt = canvas.grid.grid.getGridPositionFromPixels(pos.x, pos.y); + let [y, x] = gridPt; //Normalize the returned data because it's in [y,x] format + + if (!this.terrainExists(x, y)) { + //const terrain = new Terrain({ x: x, y: y }); + //terrain.draw(); + //this.constructor.placeableClass.create(terrain.data); + this.constructor.placeableClass.create({ x: x, y: y, multiple: this.defaultmultiple }); + } + this._costGrid = null; } - canvas.controls.select.clear(); - } - _onClickRight(e){ - /* DELETE TERRAIN SQUARE */ - let pos = e.data.getLocalPosition(canvas.app.stage); - let gridPt = canvas.grid.grid.getGridPositionFromPixels(pos.x,pos.y); - let px = canvas.grid.grid.getPixelsFromGridPosition(gridPt[0],gridPt[1]) - //Normalize the returned data because it's in [y,x] format - let [x,y] = gridPt; - - let key = `${px[0]}.${px[1]}`; - const layer = canvas.terrain.getHighlightLayer(this.layerName); - let square = this.getSquare(layer,key) - if(game.activeTool == 'addterrain' && square){ - - this.removeTerrain(x,y,true,false); - + + + /* -------------------------------------------- */ + + /** + * Handle drop events for Tile data on the Tiles Layer + * @param {DragEvent} event The concluding drag event + * @param {object} data The extracted Tile data + * @private + */ + async _onDropTerrainData(event, data) { + if (!data.img) return; + if (!this._active) this.activate(); + + // Determine the tile size + const tex = await loadTexture(data.img); + const ratio = canvas.dimensions.size / (data.terrainSize || canvas.dimensions.size); + data.width = tex.baseTexture.width * ratio; + data.height = tex.baseTexture.height * ratio; + + // Validate that the drop position is in-bounds and snap to grid + if (!canvas.grid.hitArea.contains(data.x, data.y)) return false; + data.x = data.x - (data.width / 2); + data.y = data.y - (data.height / 2); + if (!event.shiftKey) mergeObject(data, canvas.grid.getSnappedPosition(data.x, data.y)); + + // Create the tile as hidden if the ALT key is pressed + if (event.altKey) data.hidden = true; + + // Create the Tile + return this.constructor.placeableClass.create(data); } - } - - _onDragRightMove(event) { - - // Extract event data - const DRAG_SPEED_MODIFIER = 0.8; - const {cursorTime, origin, destination} = event.data; - const dx = destination.x - origin.x; - const dy = destination.y - origin.y; - - // Update the client's cursor position every 100ms - const now = Date.now(); - if ( (now - (cursorTime || 0)) > 100 ) { - if ( canvas.controls ) canvas.controls._onMoveCursor(event, destination); - event.data.cursorTime = now; + + terrainExists(pxX, pxY) { + return canvas.scene.data.terrain.find(t => { return t.x == pxX && t.y == pxY }) != undefined; } +} - // Pan the canvas - canvas.pan({ - x: canvas.stage.pivot.x - (dx * DRAG_SPEED_MODIFIER), - y: canvas.stage.pivot.y - (dy * DRAG_SPEED_MODIFIER) - }); - - - } - activate() { - - super.activate(); - const options = this.constructor.layerOptions; - this.interactive = true; - this._registerMouseListeners(); - this._registerKeyboardListeners(); - //canvas.activeLayer = canvas.terrain; - } - /** - * Actions upon layer becoming inactive - */ - deactivate() { - super.deactivate(); - this.interactive = false; - this._deRegisterMouseListeners(); - this._deRegisterKeyboardListeners(); - - } +class TerrainSquare extends PIXI.Graphics { + constructor(coord, ...args) { + super(...args); + this.coord = coord; + let topLeft = canvas.grid.grid.getPixelsFromGridPosition(coord.x, coord.y) + this.thePosition = `${topLeft[0]}.${topLeft[1]}`; + } } \ No newline at end of file diff --git a/classes/terrain.js b/classes/terrain.js new file mode 100644 index 0000000..44de9f9 --- /dev/null +++ b/classes/terrain.js @@ -0,0 +1,361 @@ +import { log, setting } from "./terrainlayer.js"; + +export class Terrain extends PlaceableObject { + constructor(...args) { + super(...args); + + // Clean initial data + this._cleanData(); + /** + * The Tile border frame + * @type {PIXI.Container|null} + */ + this.frame = null; + + /** + * The Tile image container + * @type {PIXI.Container|null} + */ + this.terrain = null; + } + + /* -------------------------------------------- */ + + /** @override */ + static get embeddedName() { + return "Terrain"; + } + + static get layer() { + return canvas.terrain; + } + + static async create(data, options) { + + //super.create(data, options); + //canvas.scene._data.terrain + + let userId = game.user._id; + + data = data instanceof Array ? data : [data]; + for (let d of data) { + const allowed = Hooks.call(`preCreateTerrain`, this, d, options, userId); + if (allowed === false) { + debug(`Terrain creation prevented by preCreate hook`); + return null; + } + } + + let embedded = data.map(d => { + let object = canvas.terrain.createObject(d); + object._onCreate(options, userId); + canvas.scene.data.terrain.push(d); + canvas.scene.setFlag('TerrainLayer', 'terrain' + d._id, d); + Hooks.callAll(`createTerrain`, canvas.terrain, d, options, userId); + return d; + }); + + //+++layer.storeHistory("create", result); + + return data.length === 1 ? embedded[0] : embedded; + + /* + const created = await canvas.scene.createEmbeddedEntity(this.embeddedName, data, options); + if (!created) return; + if (created instanceof Array) { + return created.map(c => this.layer.get(c._id)); + } else { + return this.layer.get(created._id); + }*/ + + //canvas.scene.data.terrain.push(data); + //await canvas.scene.setFlag('TerrainLayer', 'terrain' + data._id, data); + + //return this; + } + + _onDelete() { + //+++delete this.layer._controlled[this.id]; + //+++if ( layer._hover === this ) layer._hover = null; + } + + /* -------------------------------------------- */ + + /** + * Apply initial sanitizations to the provided input data to ensure that a Tile has valid required attributes. + * @private + */ + _cleanData() { + let makeid = function () { + var result = ''; + var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + var charactersLength = characters.length; + for (var i = 0; i < 16; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; + } + + if (this.data._id == undefined) + this.data._id = makeid(); + + if (isNaN(parseInt(this.data.multiple))) + this.data.multiple = 2; + this.data.multiple = parseInt(this.data.multiple); + + // Constrain canvas coordinates + if (!canvas || !this.scene?.active) return; + const d = canvas.dimensions; + const minX = d.paddingX / d.size; + const minY = d.paddingY / d.size; + const maxX = (d.width / d.size) + minX; + const maxY = (d.height / d.size) + minY; + this.data.x = Math.clamped(parseInt(this.data.x), minX, maxX); + this.data.y = Math.clamped(parseInt(this.data.y), minY, maxY); + + this.data.flags = this.data.flags || {}; + } + + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ + + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ + + /** @override */ + async draw() { + this.clear(); + + let s = canvas.dimensions.size; + let mid = (s / 2); + + // Create the outer frame for the border and interaction handles + this.frame = this.addChild(new PIXI.Container()); + this.frame.border = this.frame.addChild(new PIXI.Graphics()); + + // Create the tile container and it's child elements + let mult = Math.clamped(this.data.multiple, 2, 4); + this.terrain = this.addChild(new PIXI.Container()); + this.texture = await loadTexture('modules/TerrainLayer/img/square' + mult + 'x.svg'); + this.terrain.img = this.terrain.addChild(this._drawPrimarySprite(this.texture)); + this.terrain.img.blendMode = PIXI.BLEND_MODES.OVERLAY; + //this.terrain.img = this.addChild(new PIXI.Graphics);//new TerrainSquare()); + + let fontsize = (s / 3); + this.terrain.text = new PIXI.Text('x' + mult, { fontFamily: 'Arial', fontSize: fontsize, fill: 0xffffff, opacity: 1, align: 'center' }); + this.terrain.text.blendMode = PIXI.BLEND_MODES.OVERLAY; + this.terrain.text.anchor.set(0.5, 0.5); + this.terrain.text.x = this.terrain.text.y = mid; + this.terrain.addChild(this.terrain.text); + + // Refresh the current display + this.refresh(); + + // Enable interactivity, only if the Tile has a true ID + if (this.id) this.activateListeners(); + return this; + } + + /* -------------------------------------------- */ + + /** @override */ + refresh() { + let s = canvas.dimensions.size; + //let bit = (s / 16) * (Math.clamped(this.data.multiple, 2, 4) - 1); + let mid = (s / 2); + + let terrainSquare = this.terrain.img; + + let gsW = canvas.grid.grid.w; + let gsH = canvas.grid.grid.h; + + let bounds = null; + if (this.terrain.img) { + const img = this.terrain.img; + + // Set the tile dimensions and mirroring + img.width = s; + img.height = s; + + bounds = this.terrain.getLocalBounds(undefined, true); + } else { + bounds = new NormalizedRectangle(0, 0, s, s); + } + + /* + terrainSquare.width = gsW; + terrainSquare.height = gsH; + terrainSquare.beginFill(0xffffff, 0.5); + terrainSquare.lineStyle(1, 0xffffff, 0.5); + terrainSquare.drawPolygon([0, 0, bit, 0, 0, bit]); + terrainSquare.drawPolygon([mid - bit, 0, mid + bit, 0, 0, mid + bit, 0, mid - bit]); + terrainSquare.drawPolygon([s, 0, s, bit, bit, s, 0, s, 0, s - bit, s - bit, 0]); + terrainSquare.drawPolygon([s, mid - bit, s, mid + bit, mid + bit, s, mid - bit, s]); + terrainSquare.drawPolygon([s, s, s - bit, s, s, s - bit]); + terrainSquare.endFill(); + + terrainSquare.closePath(); + terrainSquare.blendMode = PIXI.BLEND_MODES.OVERLAY;*/ + + this.terrain.text.visible = setting('showText'); + + this.terrain.img.alpha = 0.5; //setting('opacity'); + this.terrain.alpha = 1; + + // Set Tile position + let px = canvas.grid.grid.getPixelsFromGridPosition(this.data.y, this.data.x); + this.position.set(px[0], px[1]); + + this.terrain.width = this.terrain.img.width; + this.terrain.height = this.terrain.img.height; + + // Allow some extra padding to detect handle hover interactions + this.hitArea = this._controlled ? bounds.clone().pad(20) : bounds; + + // Update border frame + this._refreshBorder(bounds); + + this.visible = !this.data.hidden || game.user.isGM; + return this; + } + + /* -------------------------------------------- */ + + /** + * Refresh the display of the Tile border + * @private + */ + _refreshBorder(b) { + const border = this.frame.border; + + // Determine border color + const colors = CONFIG.Canvas.dispositionColors; + let bc = colors.INACTIVE; + if (this._controlled) { + bc = colors.CONTROLLED; + } + + // Draw the tile border + const t = CONFIG.Canvas.objectBorderThickness; + const h = Math.round(t / 2); + const o = Math.round(h / 2); + + let s = canvas.dimensions.size; + //let [x,y] = canvas.grid.grid.getPixelsFromGridPosition(this.data.y, this.data.x); + let x = 0; + let y = 0; + border.clear() + .lineStyle(t, 0x000000, 1.0).drawRoundedRect(x - o, y - o, s + h, s + h, 3) + .lineStyle(h, bc, 1.0).drawRoundedRect(x - o, y - o, s + h, s + h, 3); + border.visible = this._hover || this._controlled; + } + + /* -------------------------------------------- */ + + /** @override */ + activateListeners() { + super.activateListeners(); + /* + this.frame.handle.off("mouseover").off("mouseout").off("mousedown") + .on("mouseover", this._onHandleHoverIn.bind(this)) + .on("mouseout", this._onHandleHoverOut.bind(this)) + .on("mousedown", this._onHandleMouseDown.bind(this)); + this.frame.handle.interactive = true;*/ + } + + /* -------------------------------------------- */ + /* Database Operations */ + /* -------------------------------------------- */ + + /** @override */ + _onUpdate(data) { + const changed = new Set(Object.keys(data)); + if (changed.has("z")) { + this.zIndex = parseInt(data.z) || 0; + } + + // Release control if the Tile was locked + if (data.locked) this.release(); + + // Full re-draw or partial refresh + if (changed.has("multiple")) return this.draw(); + this.refresh(); + + // Update the sheet, if it's visible + if (this._sheet && this._sheet.rendered) this.sheet.render(); + } + + /* -------------------------------------------- */ + /* Interactivity */ + /* -------------------------------------------- */ + + /** @override */ + _canHUD(user, event) { + return this._controlled; + } + + /* -------------------------------------------- */ + + /** @override */ + _canConfigure(user, event) { + if (this.data.locked && !this._controlled) return false; + return super._canConfigure(user); + } + + _canDrag(user, event) { + return false; + } + + /* -------------------------------------------- */ + + /** + * Create a preview tile with a background texture instead of an image + * @return {Tile} + */ + static createPreview(data) { + const terrain = new Terrain(mergeObject({ + x: 0, + y: 0, + rotation: 0, + z: 0, + width: 0, + height: 0 + }, data)); + terrain._controlled = true; + + // Swap the tile and the frame + terrain.draw().then(t => { + terrain.removeChild(terrain.frame); + terrain.addChild(terrain.frame); + }); + return terrain; + } + + async update(data, options) { + let objectdata = duplicate(canvas.scene.getFlag("TerrainLayer", "terrain" + this.data._id)); + //update this object + mergeObject(this.data, data); + delete this.data.id; //remove the id if I've accidentally added it. We should be using _id + //update the data and save it to the scene + mergeObject(objectdata, this.data); + await canvas.scene.setFlag("TerrainLayer", "terrain" + this.data._id, objectdata); + //if the multiple has changed then update the image + if (data.multiple != undefined) { + this.texture = await loadTexture('modules/TerrainLayer/img/square' + this.data.multiple + 'x.svg'); + this.terrain.removeChild(this.terrain.img); + this.terrain.img = this.terrain.addChild(this._drawPrimarySprite(this.texture)); + } + this.refresh(); + return this; + } + + async delete(options) { + let layerdata = duplicate(this.scene.getFlag("TerrainLayer", "data")); + let idx = layerdata.findIndex(t => { return t._id == this.id }); + layerdata.splice(idx, 1); + await this.scene.setFlag("TerrainLayer", "data", layerdata); + return this; + } +} \ No newline at end of file diff --git a/classes/terrainconfig.js b/classes/terrainconfig.js new file mode 100644 index 0000000..8ecf773 --- /dev/null +++ b/classes/terrainconfig.js @@ -0,0 +1,51 @@ +import { TerrainLayer } from './terrainlayer.js'; + +export class TerrainConfig extends FormApplication { + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + id: "terrain-config", + classes: ["sheet", "terrain-sheet"], + title: "Terrain Configuration", + template: "modules/TerrainLayer/templates/terrain-config.html", + width: 400, + submitOnChange: true + }); + } + + /* -------------------------------------------- */ + + /** @override */ + getData(options) { + return { + object: duplicate(this.object.data), + options: this.options, + submitText: this.options.preview ? "Create" : "Update" + } + } + + /* -------------------------------------------- */ + + /** @override */ + _onChangeInput(event) { + if ($(event.target).attr('name') == 'multiple') { + let val = $(event.target).val(); + $(event.target).next().html(TerrainLayer.multipleText(val)); + } + } + + /* -------------------------------------------- */ + + /** @override */ + async _updateObject(event, formData) { + if (!game.user.isGM) throw "You do not have the ability to configure a Terrain object."; + if (this.object.id) { + let data = duplicate(formData); + data.id = this.object.id; + data.multiple = (data.multiple == 1 ? 0.5 : parseInt(data.multiple)); + return this.object.update(data, { diff: false }); + } + return this.object.constructor.create(formData); + } +} \ No newline at end of file diff --git a/classes/terraincontrols.js b/classes/terraincontrols.js index cec681e..3ecae7a 100644 --- a/classes/terraincontrols.js +++ b/classes/terraincontrols.js @@ -1,4 +1,4 @@ -import { TerrainLayer } from './terrainlayer2.js'; +import { TerrainLayer } from './terrainlayer.js'; export class TerrainLayerToolBar extends FormApplication { constructor() { diff --git a/classes/terrainhud.js b/classes/terrainhud.js new file mode 100644 index 0000000..54e9165 --- /dev/null +++ b/classes/terrainhud.js @@ -0,0 +1,91 @@ +import { TerrainLayer } from './terrainlayer.js'; + +export class TerrainHUD extends BasePlaceableHUD { + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + id: "terrain-hud", + template: "modules/TerrainLayer/templates/terrain-hud.html" + }); + } + + /* -------------------------------------------- */ + + /** @override */ + getData() { + const data = super.getData(); + return mergeObject(data, { + visibilityClass: data.hidden ? "active" : "", + }); + } + + activateListeners(html) { + super.activateListeners(html); + + $('.inc-multiple', this.element).on("click", this._onHandleClick.bind(this, true)); + $('.dec-multiple', this.element).on("click", this._onHandleClick.bind(this, false)); + /* + this.frame.handle.off("mouseover").off("mouseout").off("mousedown") + .on("mouseover", this._onHandleHoverIn.bind(this)) + .on("mouseout", this._onHandleHoverOut.bind(this)) + .on("mousedown", this._onHandleMouseDown.bind(this)); + this.frame.handle.interactive = true;*/ + } + + /* + * async _onToggleVisibility(event) { + event.preventDefault(); + + // Toggle the visible state + const isHidden = this.object.data.hidden; + const updates = this.layer.controlled.map(o => { + return {_id: o.id, hidden: !isHidden}; + }); + + // Update all objects + await this.layer.updateMany(updates); + event.currentTarget.classList.toggle("active", !isHidden); + } + */ + + _onHandleClick(increase, event) { + let mult = this.object.data.multiple; + let idx = TerrainLayer.multipleOptions.indexOf(mult); + idx = Math.clamped((increase ? idx + 1 : idx - 1), 0, TerrainLayer.multipleOptions.length - 1); + this.object.update({ multiple: TerrainLayer.multipleOptions[idx] }); + this.object.refresh(); + } + + async _onToggleVisibility(event) { + event.preventDefault(); + + // Toggle the visible state + const isHidden = this.object.data.hidden; + const updates = this.layer.controlled.map(o => { + return { _id: o.id, hidden: !isHidden }; + }); + + // Update all objects + await this.layer.updateMany(updates); + event.currentTarget.classList.toggle("active", !isHidden); + } + + /* -------------------------------------------- */ + + /** @override */ + setPosition() { + $('#hud').append(this.element); + let { x, y, width, height } = this.object.hitArea; + const c = 70; + const p = -10; + let px = canvas.grid.grid.getPixelsFromGridPosition(this.object.data.y, this.object.data.x); + const position = { + width: width + (c * 2) + (p * 2), + height: height + (p * 2), + left: x + px[0] - c - p, + top: y + px[1] - p + }; + this.element.css(position); + } +} \ No newline at end of file diff --git a/classes/terrainlayer.old.js b/classes/terrainlayer.old.js new file mode 100644 index 0000000..0599c33 --- /dev/null +++ b/classes/terrainlayer.old.js @@ -0,0 +1,641 @@ +export let log = (...args) => console.log("terrainlayer | ", ...args); +//TERRAIN LAYER +let TLControlPress = false; +class TerrainSquare extends PIXI.Graphics { + constructor(coord,...args){ + super(...args); + this.coord = coord; + let topLeft = canvas.grid.grid.getPixelsFromGridPosition(coord.x,coord.y) + this.thePosition = `${topLeft[0]}.${topLeft[1]}`; + } +} + +export class TerrainHighlight extends PIXI.Graphics { + constructor(name,...args){ + super(...args); + + /** + * Track the Grid Highlight name + * @type {String} + */ + this.name = name; + + /** + * Track distinct positions which have already been highlighted + * @type {Set} + */ + this.visible = canvas.scene.getFlag('TerrainLayer', 'sceneVisibility') || true; + log('TerrainHighlight:', this.visible); + this.positions = new Set(); + } + + /* -------------------------------------------- */ + + /** + * Record a position that is highlighted and return whether or not it should be rendered + * @param {Number} x The x-coordinate to highlight + * @param {Number} y The y-coordinate to highlight + * @return {Boolean} Whether or not to draw the highlight for this location + */ + highlight(pxX, pxY) { + let key = `${pxX}.${pxY}`; + if ( this.positions.has(key) ) return false; + this.positions.add(key); + return true; + } + + /* -------------------------------------------- */ + + /** + * Extend the Graphics clear logic to also reset the highlighted positions + * @param args + */ + clear(...args) { + super.clear(...args); + this.positions = new Set(); + } + + + /* -------------------------------------------- */ + + /** + * Extend how this Graphics container is destroyed to also remove parent layer references + * @param args + */ + destroy(...args) { + delete canvas.terrain.highlightLayers[this.name]; + super.destroy(...args); + } +} + +export class TerrainLayer extends CanvasLayer{ + constructor(){ + super(); + this.scene = null; + // this.sceneId = this.scene._id; + //this.layerName = `DifficultTerrain.${this.scene._id}`; + this.highlight = null; + this.mouseInteractionManager = null; + this.dragging = false; + // this._addListeners(); + this.showterrain = false; + + this.terrainhud = new TerrainHUD(); + } + + get hud() { + return this.terrainhud; + } + + static get layerOptions() { + return mergeObject(super.layerOptions, { + zIndex: 20 + }); + } + + async draw(){ + this._deRegisterMouseListeners() + await super.draw(); + log('draw') + this.highlightLayers = {}; + this.scene = canvas.scene; + this.sceneId = this.scene._id; + this.layerName = `DifficultTerrain.${this.scene._id}`; + this.highlight = this.addChild(new PIXI.Container()); + this.addHighlightLayer(this.layerName); + this.costGrid = this.scene.getFlag('TerrainLayer','costGrid') ?? {}; + Hooks.once('canvasReady',this.buildFromCostGrid.bind(this)) + this._addListeners(); + return this; + } + + async tearDown(){ + log('tearDown') + super.tearDown(); + this._deRegisterMouseListeners() + this._deRegisterKeyboardListeners(); + } + + async toggle(show, emit = false){ + //this.highlight.children[0].visible = !this.highlight.children[0].visible; + if (show == undefined) + show = !this.showterrain; + this.showterrain = show; + if(game.user.isGM && emit){ + //await canvas.scene.setFlag('TerrainLayer','sceneVisibility', this.highlight.children[0].visible ) + game.socket.emit('module.TerrainLayer', { action: 'toggle', arguments: [this.showterrain]}) + } + } + + _addListeners() { + // Define callback functions for mouse interaction events + const callbacks = { + dragLeftStart: this._onDragLeftStart.bind(this), + dragLeftMove: this._onDragLeftMove.bind(this), + clickRight: this._onClickRight.bind(this), + dragRightMove: this._onDragRightMove.bind(this), + dragLeftDrop:this._onDragLeftDrop.bind(this) + }; + + // Create and activate the interaction manager + const permissions = {}; + const mgr = new MouseInteractionManager(this, this, permissions, callbacks); + this.mouseInteractionManager = mgr.activate(); + } + + addHighlightLayer(name) { + const layer = this.highlightLayers[name]; + if ( !layer || layer._destroyed ) { + this.highlightLayers[name] = this.highlight.addChild(new TerrainHighlight(name)); + + canvas.terrain.highlight.children[0].visible = (typeof canvas.scene.getFlag('TerrainLayer', 'sceneVisibility') != 'undefined') ? canvas.scene.getFlag('TerrainLayer', 'sceneVisibility') : true; + } + return this.highlightLayers[name]; + } + + getHighlightLayer(name) { + return this.highlightLayers[name]; + } + + /** + * Clear a specific Highlight graphic + * @param name + */ + clearHighlightLayer(name) { + const layer = this.highlightLayers[name]; + if ( layer ) layer.clear(); + } + /* -------------------------------------------- */ + + /** + * Destroy a specific Highlight graphic + * @param name + */ + destroyHighlightLayer(name) { + const layer = this.highlightLayers[name]; + this.highlight.removeChild(layer); + layer.destroy(); + } + + highlightPosition(name, options) { + const layer = this.highlightLayers[name]; + if ( !layer ) return false; + if(canvas.grid.type == 1) + this.highlightGridPosition(layer, options); + else if(canvas.grid.type == 2 || canvas.grid.type == 3 || canvas.grid.type == 4 || canvas.grid.type == 5) + this.highlightHexPosition(layer,options); + } + + /** @override */ + highlightGridPosition(layer , {gridX, gridY, multiple=2,type='ground'}={}) { + //GRID ALREADY HIGHLIGHTED + let terrainSquare = new TerrainSquare({ x: gridX, y: gridY }); + + let gsW = canvas.grid.grid.w; + let gsH = canvas.grid.grid.h; + + let px = canvas.grid.grid.getPixelsFromGridPosition(gridX, gridY); + + layer.highlight(px[0],px[1]); + + terrainSquare.x = px[0]; + terrainSquare.y = px[1]; + terrainSquare.width = gsW; + terrainSquare.height = gsH; + + let s = canvas.dimensions.size; + let bit = (s / 16) * (Math.clamped(multiple, 2, 4) - 1); + let mid = (s / 2); + + terrainSquare.beginFill(0xffffff, 0.5); + terrainSquare.lineStyle(1, 0xffffff, 0.5); + terrainSquare.drawPolygon([0, 0, bit, 0, 0, bit]); + terrainSquare.drawPolygon([mid - bit, 0, mid + bit, 0, 0, mid + bit, 0, mid - bit]); + terrainSquare.drawPolygon([s, 0, s, bit, bit, s, 0, s, 0, s - bit, s - bit, 0]); + terrainSquare.drawPolygon([s, mid - bit, s, mid + bit, mid + bit, s, mid - bit, s]); + terrainSquare.drawPolygon([s, s, s - bit, s, s, s - bit]); + terrainSquare.endFill(); + + terrainSquare.closePath(); + terrainSquare.blendMode = PIXI.BLEND_MODES.OVERLAY; + + if (game.settings.get('TerrainLayer', 'showText')) { + let fontsize = (s / 3); + let text = new PIXI.Text('x' + multiple, { fontFamily: 'Arial', fontSize: fontsize, fill: 0xffffff, opacity: 0.6, align: 'center' }); + text.blendMode = PIXI.BLEND_MODES.OVERLAY; + text.anchor.set(0.5, 0.5); + text.x = text.y = mid; + terrainSquare.addChild(text); + } + + terrainSquare.alpha = game.settings.get('TerrainLayer', 'opacity'); + layer.addChild(terrainSquare); + + // this.addToCostGrid(gridX,gridY); + } + + highlightHexPosition(layer,{gridX,gridY,multiple=2,type='ground'}={}){ + let gsW = Math.floor(canvas.grid.grid.w); + let gsH = Math.round(canvas.grid.grid.h); + + let topLeft = canvas.grid.grid.getPixelsFromGridPosition(gridX,gridY) + let pxX = topLeft[0]; + let pxY = topLeft[1]; + + const points = canvas.grid.grid.options.columns ? canvas.grid.grid.constructor.flatHexPoints : canvas.grid.grid.constructor.pointyHexPoints; + const coords = points.reduce((arr, p) => { + arr.push(topLeft[0] + (p[0]*gsW)); + arr.push(topLeft[1] + (p[1]*gsH)); + return arr; + }, []); + + let col = canvas.grid.grid.columns; + let even = canvas.grid.grid.even; + + let terrainSquare = new TerrainSquare({x:gridX,y:gridY}) + + layer.highlight(pxX,pxY); + + let offset = gsH* 0.16; + + const halfW = (gsW/2) + + terrainSquare.y = topLeft[1]; + terrainSquare.x = topLeft[0]; + + terrainSquare.width = gsW; + terrainSquare.height = gsH; + + //let text = new PIXI.Text('x'+multiple,{fontFamily : 'Arial', fontSize: 12, fill : 0xffffff,opacity:0.5, align : 'center'}) + + if(canvas.grid.type == 4 || canvas.grid.type == 5){ + terrainSquare.lineStyle(4, 0xffffff, 0.5); + terrainSquare.moveTo(halfW,offset*2); + terrainSquare.lineTo(offset*2, gsW-(offset*3)); + terrainSquare.lineTo(gsW-(offset*2), gsW-(offset*3)); + terrainSquare.lineTo(halfW, offset*2); + //text.y = (gsH/2)+3; + }else{ + terrainSquare.lineStyle(7, 0xffffff, 0.5); + terrainSquare.moveTo(halfW,offset); + terrainSquare.lineTo(offset, gsW-offset); + terrainSquare.lineTo(gsW-offset, gsW-offset); + terrainSquare.lineTo(halfW, offset); + //text.y = (gsH/2)+7; + } + + terrainSquare.closePath(); + terrainSquare.blendMode = PIXI.BLEND_MODES.OVERLAY; + + /* + text.blendMode = PIXI.BLEND_MODES.OVERLAY; + text.anchor.set(0.5,0.5); + text.x = gsW/2; + + + terrainSquare.addChild(text); + */ + + layer.addChild(terrainSquare); + // this.addToCostGrid(gridX,gridY,multiple); + + } + _registerMouseListeners() { + this.addListener('pointerup', this._pointerUp); + this.dragging = false; + } + _registerKeyboardListeners() { + $(document).keydown((event) => { + + //if (ui.controls.activeControl !== this.layername) return; + switch(event.which){ + case 27: + event.stopPropagation(); + ui.menu.toggle(); + break; + case 17: + TLControlPress = true; + break; + default: + break; + } + }); + $(document).keyup((event) => { + switch (event.which) { + case 17: + TLControlPress = false; + break; + default: + break; + } + }); + } + + _deRegisterMouseListeners(){ + this.removeListener('pointerup', this._pointerUp); + } + + _deRegisterKeyboardListeners(){ + $(document).off('keydown') + $(document).off('keyup'); + } + + async addTerrain(x,y,emit=false,batch=true){ + this.highlightPosition(this.layerName,{gridX:x,gridY:y}) + this.addToCostGrid(x,y); + if(game.user.isGM && emit){ + if(!batch) await this.updateCostGridFlag(); + const data = { + action:'addTerrain', + arguments:[x,y] + } + game.socket.emit('module.TerrainLayer', data) + } + } + async updateTerrain(x,y,emit=false,batch=true){ + const layer = canvas.terrain.getHighlightLayer(this.layerName); + let [pxX,pxY] = canvas.grid.grid.getPixelsFromGridPosition(x,y) + const key = `${pxX}.${pxY}`; + let square = this.getSquare(layer,key) + + let cost = this.costGrid[x][y]; + if (cost.multiple < game.settings.get('TerrainLayer','maxMultiple')){ + this.costGrid[x][y].multiple+=1; + + }else{ + this.costGrid[x][y].multiple=2; + } + //square.getChildAt(0).text = `x${cost.multiple}`; + if(game.user.isGM && emit){ + if(!batch) await this.updateCostGridFlag(); + const data = { + action:'updateTerrain', + arguments:[x,y] + } + game.socket.emit('module.TerrainLayer', data) + } + } + + _pointerUp(e) { + let pos = e.data.getLocalPosition(canvas.app.stage); + let gridPt = canvas.grid.grid.getGridPositionFromPixels(pos.x,pos.y); + let [pxX,pxY] = canvas.grid.grid.getPixelsFromGridPosition(gridPt[0],gridPt[1]) + let [x,y] = gridPt; //Normalize the returned data because it's in [y,x] format + let gsW = Math.round(canvas.grid.grid.w); + let gsH = Math.floor(canvas.grid.grid.h); + let gs = Math.min(gsW,gsH) + let gridPX = {x:Math.round(x*gsH),y:Math.round(y*gsW)} + + const layer = canvas.terrain.getHighlightLayer(this.layerName); + if(canvas.grid.type == 0) { + alert('Difficult Terrain does not work with gridless maps.'); + return false; + } + switch(e.data.button){ + case 0: + if(game.activeTool == 'addterrain' && !this.dragging){ + if(this.terrainExists(pxX,pxY)){ + this.updateTerrain(x,y,true,false); + }else{ + this.addTerrain(x,y,true,false) + } + }else if(game.activeTool == 'subtractterrain'){ + if(this.terrainExists(pxX,pxY)){ + this.removeTerrain(x,y,true,false); + } + } + break; + default: + break; + } + this.dragging = false; + } + + terrainExists(pxX,pxY){ + const layer = canvas.terrain.getHighlightLayer(this.layerName); + const key = `${pxX}.${pxY}`; + if(layer.positions.has(key)) return true; + return false + } + + addToCostGrid(x,y,multiple=2,type='ground'){ + if (typeof this.costGrid[x] === 'undefined') + this.costGrid[x] = {}; + this.costGrid[x][y]={multiple,type}; + } + + async updateCostGridFlag(){ + let x = duplicate(this.costGrid); + await canvas.scene.unsetFlag('TerrainLayer','costGrid'); + await canvas.scene.setFlag('TerrainLayer', 'costGrid', x); + } + + buildFromCostGrid(update=true){ + canvas.terrain.highlight.children[0].removeChildren() + for(let x in this.costGrid){ + for(let y in this.costGrid[x]){ + this.highlightPosition(this.layerName,{gridX:parseInt(x),gridY:parseInt(y),multiple:this.costGrid[x][y].multiple,update:update}) + } + } + } + + async resetGrid(emit=false){ + this.getHighlightLayer(this.layerName).clear(); + this.getHighlightLayer(this.layerName).removeChildren(); + this.costGrid = {} + //only the GM who fired the event can set flag and emit, otherwise a game with two DM's might fire recursively. + if(game.user.isGM && emit){ + await this.scene.unsetFlag('TerrainLayer','costGrid'); + + game.socket.emit('module.TerrainLayer',{action:'resetGrid',arguments:[]}) + } + } + + selectSquares(coords){ + const startPx = canvas.grid.grid.getCenter(coords.x,coords.y) + const startGrid = canvas.grid.grid.getGridPositionFromPixels(startPx[0],startPx[1]) + + const endPx = canvas.grid.grid.getCenter(coords.x+coords.width,coords.y+coords.height) + const endGrid = canvas.grid.grid.getGridPositionFromPixels(endPx[0],endPx[1]) + + for(let x = startGrid[0];x<=endGrid[0];x++){ + for(let y = startGrid[1];y<=endGrid[1];y++){ + if(game.activeTool == 'addterrain' && TLControlPress == false){ + //this.highlightPosition(this.layerName,{gridX:y,gridY:x}) + //this.addToCostGrid(x,y); + if(!this.terrainExists(y*canvas.dimensions.size,x*canvas.dimensions.size)) + this.addTerrain(x,y,true,true) + }else if(game.activeTool == 'subtractterrain' || TLControlPress){ + this.removeTerrain(x,y,true,true) + } + } + } + this.updateCostGridFlag(); + + } + + async removeTerrain(x,y,emit=false,batch=true){ + console.log('removeTerrain') + const [pxX,pxY] = canvas.grid.grid.getPixelsFromGridPosition(x,y) + const layer = canvas.terrain.getHighlightLayer(this.layerName); + const key = `${pxX}.${pxY}`; + if(!layer.positions.has(key)) return false; + let square = this.getSquare(layer,key); + square.destroy(); + layer.positions.delete(key); + this.removeFromCostGrid(x,y) + if(game.user.isGM && emit){ + if(!batch) await this.updateCostGridFlag(); + const data = { + action:'removeTerrain', + arguments:[x,y] + } + game.socket.emit('module.TerrainLayer', data) + } + } + + removeFromCostGrid(x,y,emit=false){ + if(typeof this.costGrid[x] == 'undefined') return false; + if(typeof this.costGrid[x][y] == 'undefined') return false; + + delete this.costGrid[x][y]; + } + + getSquare(layer,key){ + let square = layer.children.find((x) => { + return x.thePosition == key + }); + return square || false; + } + + _onDragLeftStart(e){ + this.dragging = true; + } + + _onDragLeftMove(e){ + if (game.activeTool == "select" ) return this._onDragSelect(e); + } + + _onDragSelect(event) { + // Extract event data + const {origin, destination} = event.data; + + // Determine rectangle coordinates + let coords = { + x: Math.min(origin.x, destination.x), + y: Math.min(origin.y, destination.y), + width: Math.abs(destination.x - origin.x), + height: Math.abs(destination.y - origin.y) + }; + + // Draw the select rectangle + canvas.controls.drawSelect(coords); + event.data.coords = coords; + } + + _onDragLeftDrop(e) { + const tool = game.activeTool; + // Conclude a select event + + const isSelect = ["select"].includes(tool); + if (isSelect) { + canvas.controls.select.clear(); + canvas.controls.select.active = false; + if (tool === "addterrain") return this.selectSquares(e.data.coords); + } + canvas.controls.select.clear(); + } + + _onClickRight(e){ + /* DELETE TERRAIN SQUARE */ + let pos = e.data.getLocalPosition(canvas.app.stage); + let gridPt = canvas.grid.grid.getGridPositionFromPixels(pos.x,pos.y); + let px = canvas.grid.grid.getPixelsFromGridPosition(gridPt[0],gridPt[1]) + //Normalize the returned data because it's in [y,x] format + let [x,y] = gridPt; + + let key = `${px[0]}.${px[1]}`; + const layer = canvas.terrain.getHighlightLayer(this.layerName); + let square = this.getSquare(layer,key) + if(game.activeTool == 'addterrain' && square){ + this.removeTerrain(x,y,true,false); + } + } + + _onDragRightMove(event) { + // Extract event data + const DRAG_SPEED_MODIFIER = 0.8; + const {cursorTime, origin, destination} = event.data; + const dx = destination.x - origin.x; + const dy = destination.y - origin.y; + + // Update the client's cursor position every 100ms + const now = Date.now(); + if ( (now - (cursorTime || 0)) > 100 ) { + if ( canvas.controls ) canvas.controls._onMoveCursor(event, destination); + event.data.cursorTime = now; + } + + // Pan the canvas + canvas.pan({ + x: canvas.stage.pivot.x - (dx * DRAG_SPEED_MODIFIER), + y: canvas.stage.pivot.y - (dy * DRAG_SPEED_MODIFIER) + }); + } + + activate() { + super.activate(); + const options = this.constructor.layerOptions; + this.interactive = true; + this._registerMouseListeners(); + this._registerKeyboardListeners(); + //canvas.activeLayer = canvas.terrain; + } + /** + * Actions upon layer becoming inactive + */ + deactivate() { + super.deactivate(); + this.interactive = false; + this._deRegisterMouseListeners(); + this._deRegisterKeyboardListeners(); + } +} + +class TerrainHUD extends BasePlaceableHUD { + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + id: "terrain-hud", + template: "templates/hud/drawing-hud.html" + }); + } + + /* -------------------------------------------- */ + + /** @override */ + getData() { + const data = super.getData(); + return mergeObject(data, { + lockedClass: data.locked ? "active" : "", + visibilityClass: data.hidden ? "active" : "", + }); + } + + /* -------------------------------------------- */ + + /** @override */ + setPosition() { + let { x, y, width, height } = this.object.drawing.hitArea; + const c = 70; + const p = 10; + const position = { + width: width + (c * 2) + (p * 2), + height: height + (p * 2), + left: x + this.object.data.x - c - p, + top: y + this.object.data.y - p + }; + this.element.css(position); + } +} diff --git a/js/controls.js b/js/controls.js index cf4a31c..7756cd5 100644 --- a/js/controls.js +++ b/js/controls.js @@ -57,45 +57,31 @@ Hooks.on('getSceneControlButtons', (controls) => { }) } }); -Hooks.on('init',()=>{ - game.settings.register('TerrainLayer', 'scale', { - name: "TerrainLayer.scale-s", - hint: "TerrainLayer.scale-l", - scope: "world", - config: true, - default: 1, - type: Number, - range:{ - min:0.4, - max:1, - step:0.1 - }, - onChange: () => { - canvas.terrain.buildFromCostGrid(); - } - }); - game.settings.register('TerrainLayer', 'opacity', { - name: "TerrainLayer.opacity-s", - hint: "TerrainLayer.opacity-l", - scope: "world", - config: true, - default: 1, - type: Number, - range:{ - min:0.3, - max:1, - step:0.1 - }, - onChange: () => { - canvas.terrain.buildFromCostGrid(); - } - }); - game.settings.register('TerrainLayer', 'maxMultiple', { - name: "TerrainLayer.opacity-s", - hint: "TerrainLayer.opacity-l", - scope: "world", - config: true, - default: 3, - type: Number - }); +Hooks.on('renderSceneControls', (controls) => { + if (canvas != null) { + canvas.terrain.visible = (canvas.terrain.showterrain || controls.activeControl == 'terrain'); + + if (controls.activeControl == 'terrain') { + if (canvas.terrain.toolbar == undefined) + canvas.terrain.toolbar = new TerrainLayerToolBar(); + canvas.terrain.toolbar.render(true); + //$('#terrainlayer-tools').toggle(controls.activeTool == 'addterrain'); + } else { + if (!canvas.terrain.toolbar) + return; + canvas.terrain.toolbar.close(); + } + } +}); +Hooks.on('renderTerrainLayerToolBar', () => { + const tools = $(canvas.terrain.toolbar.form).parent(); + if (!tools) + return; + const controltools = $('li[data-control="terrain"] ol.control-tools'); + const offset = controltools.offset(); + tools.css({ top: `${offset.top}px`, left: `${offset.left + controltools.width() + 6}px` }); +}); + +Hooks.on('init', () => { + }) \ No newline at end of file diff --git a/js/settings.js b/js/settings.js new file mode 100644 index 0000000..6339c7e --- /dev/null +++ b/js/settings.js @@ -0,0 +1,56 @@ +export const registerSettings = function () { + game.settings.register('TerrainLayer', 'scale', { + name: "TerrainLayer.scale-s", + hint: "TerrainLayer.scale-l", + scope: "world", + config: true, + default: 1, + type: Number, + range: { + min: 0.4, + max: 1, + step: 0.1 + }, + onChange: () => { + canvas.terrain.buildFromCostGrid(); + } + }); + game.settings.register('TerrainLayer', 'opacity', { + name: "TerrainLayer.opacity-s", + hint: "TerrainLayer.opacity-l", + scope: "world", + config: true, + default: 1, + type: Number, + range: { + min: 0.3, + max: 1, + step: 0.1 + }, + onChange: () => { + canvas.terrain.buildFromCostGrid(); + } + }); + game.settings.register('TerrainLayer', 'maxMultiple', { + name: "TerrainLayer.multiple-s", + hint: "TerrainLayer.multiple-l", + scope: "world", + config: true, + default: 3, + type: Number + }); + game.settings.register('TerrainLayer', 'showText', { + name: "TerrainLayer.showtext-s", + hint: "TerrainLayer.showtext-l", + scope: "world", + config: true, + default: false, + type: Boolean + }); + game.settings.register('TerrainLayer', 'showterrain', { + scope: "world", + config: false, + default: false, + type: Boolean + }); +}; \ No newline at end of file diff --git a/module.json b/module.json index 14f068b..051d0ac 100644 --- a/module.json +++ b/module.json @@ -3,14 +3,15 @@ "title": "Terrain Layer", "description": "A base module that adds a Terrain Layer to Foundry to paint difficult terrain squares and to be used as a dependency for other mods who might integrate it with their functionality.", "authors": [{"name":"Will Saunders","email":"willsaunders1014@gmail.com" }, {"name":"IronMonk, ironmonk88#4075" }], - "version": "1.0.0", + "version": "1.0.1", "minimumCoreVersion": "0.6.6", "compatibleCoreVersion":"0.7.5", "scripts":[ ], "esmodules": [ - "terrain.js", - "js/controls.js" + "terrain-main.js", + "js/controls.js", + "js/settings.js" ], "dependencies":[ ], @@ -31,5 +32,5 @@ "url":"https://github.com/ironmonk88/TerrainLayer", "changelog":"https://raw.githubusercontent.com/ironmonk88/TerrainLayer/main/README.md", "manifest" : "https://raw.githubusercontent.com/ironmonk88/TerrainLayer/main/module.json", - "download" : "https://github.com/ironmonk88/TerrainLayer/archive/1.0.0.zip" + "download" : "https://github.com/ironmonk88/TerrainLayer/archive/1.0.1.zip" } diff --git a/templates/terrain-hud.html b/templates/terrain-hud.html index 80795a1..c9d4f16 100644 --- a/templates/terrain-hud.html +++ b/templates/terrain-hud.html @@ -7,7 +7,7 @@
{{#if isGM}} -
+
diff --git a/terrain-main.js b/terrain-main.js new file mode 100644 index 0000000..a214f44 --- /dev/null +++ b/terrain-main.js @@ -0,0 +1,64 @@ +import { TerrainLayer } from './classes/terrainlayer.js'; +import { TerrainHUD } from './classes/terrainhud.js'; +import { registerSettings } from "./js/settings.js"; + +let theLayers = Canvas.layers; +theLayers.terrain = TerrainLayer; + +/* +let oldConfig = Scene.prototype.constructor.config; +Scene.prototype.constructor.config = function () { + let result = oldConfig.call(this); + result.embeddedEntities.Terrain = "terrains"; + return result; +}*/ + +Hooks.on('canvasInit', () => { + canvas.hud.terrain = new TerrainHUD(); + //Scene.constructor.config.embeddedEntities.Terrain = "terrain"; +}); + +Hooks.on('init', () => { + game.socket.on('module.TerrainLayer', async (data) => { + console.log(data) + canvas.terrain[data.action].apply(canvas.terrain, data.arguments); + }); + + registerSettings(); + + let oldOnDragLeftStart = Token.prototype._onDragLeftStart; + Token.prototype._onDragLeftStart = function (event) { + oldOnDragLeftStart.apply(this, [event]) + if (canvas != null) + canvas.terrain.visible = true; + } + + let oldOnDragLeftDrop = Token.prototype._onDragLeftDrop; + Token.prototype._onDragLeftDrop = function (event) { + if (canvas != null) + canvas.terrain.visible = (canvas.terrain.showterrain || ui.controls.activeControl == 'terrain'); + oldOnDragLeftDrop.apply(this, [event]); + } + let oldOnDragLeftCancel = Token.prototype._onDragLeftCancel; + Token.prototype._onDragLeftCancel = function (event) { + event.stopPropagation(); + if (canvas != null) + canvas.terrain.visible = (canvas.terrain.showterrain || ui.controls.activeControl == 'terrain'); + + oldOnDragLeftCancel.apply(this, [event]) + } + let handleDragCancel = MouseInteractionManager.prototype._handleDragCancel; + + /* + MouseInteractionManager.prototype._handleDragCancel = function (event) { + if (canvas != null) + canvas.terrain.highlight.children[0].visible = (canvas.terrain.showterrain || ui.controls.activeControl == 'terrain'); + handleDragCancel.apply(this, [event]) + }*/ +}) + +Object.defineProperty(Canvas, 'layers', { + get: function () { + return theLayers; + } +})