Initial
This commit is contained in:
488
resources/app/client/pixi/layers/placeables/regions.js
Normal file
488
resources/app/client/pixi/layers/placeables/regions.js
Normal file
@@ -0,0 +1,488 @@
|
||||
/**
|
||||
* The Regions Container.
|
||||
* @category - Canvas
|
||||
*/
|
||||
class RegionLayer extends PlaceablesLayer {
|
||||
|
||||
/** @inheritDoc */
|
||||
static get layerOptions() {
|
||||
return foundry.utils.mergeObject(super.layerOptions, {
|
||||
name: "regions",
|
||||
controllableObjects: true,
|
||||
confirmDeleteKey: true,
|
||||
quadtree: false,
|
||||
zIndex: 100,
|
||||
zIndexActive: 600
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static documentName = "Region";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The method to sort the Regions.
|
||||
* @type {Function}
|
||||
*/
|
||||
static #sortRegions = function() {
|
||||
for ( let i = 0; i < this.children.length; i++ ) {
|
||||
this.children[i]._lastSortedIndex = i;
|
||||
}
|
||||
this.children.sort((a, b) => (a.zIndex - b.zIndex)
|
||||
|| (a.top - b.top)
|
||||
|| (a.bottom - b.bottom)
|
||||
|| (a._lastSortedIndex - b._lastSortedIndex));
|
||||
this.sortDirty = false;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
get hookName() {
|
||||
return RegionLayer.name;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The RegionLegend application of this RegionLayer.
|
||||
* @type {foundry.applications.ui.RegionLegend}
|
||||
*/
|
||||
get legend() {
|
||||
return this.#legend ??= new foundry.applications.ui.RegionLegend();
|
||||
}
|
||||
|
||||
#legend;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The graphics used to draw the highlighted shape.
|
||||
* @type {PIXI.Graphics}
|
||||
*/
|
||||
#highlight;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The graphics used to draw the preview of the shape that is drawn.
|
||||
* @type {PIXI.Graphics}
|
||||
*/
|
||||
#preview;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw shapes as holes?
|
||||
* @type {boolean}
|
||||
* @internal
|
||||
*/
|
||||
_holeMode = false;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Methods
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_activate() {
|
||||
super._activate();
|
||||
// noinspection ES6MissingAwait
|
||||
this.legend.render({force: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_deactivate() {
|
||||
super._deactivate();
|
||||
this.objects.visible = true;
|
||||
// noinspection ES6MissingAwait
|
||||
this.legend.close({animate: false});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
storeHistory(type, data) {
|
||||
super.storeHistory(type, type === "update" ? data.map(d => {
|
||||
if ( "behaviors" in d ) {
|
||||
d = foundry.utils.deepClone(d);
|
||||
delete d.behaviors;
|
||||
}
|
||||
return d;
|
||||
}) : data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
copyObjects() {
|
||||
return []; // Prevent copy & paste
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getSnappedPoint(point) {
|
||||
const M = CONST.GRID_SNAPPING_MODES;
|
||||
const size = canvas.dimensions.size;
|
||||
return canvas.grid.getSnappedPoint(point, canvas.forceSnapVertices ? {mode: M.VERTEX} : {
|
||||
mode: M.CENTER | M.VERTEX | M.CORNER | M.SIDE_MIDPOINT,
|
||||
resolution: size >= 128 ? 8 : (size >= 64 ? 4 : 2)
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getZIndex() {
|
||||
return this.active ? this.options.zIndexActive : this.options.zIndex;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _draw(options) {
|
||||
await super._draw(options);
|
||||
this.objects.sortChildren = RegionLayer.#sortRegions;
|
||||
this.objects.visible = true;
|
||||
this.#highlight = this.addChild(new PIXI.Graphics());
|
||||
this.#highlight.eventMode = "none";
|
||||
this.#highlight.visible = false;
|
||||
this.#preview = this.addChild(new PIXI.Graphics());
|
||||
this.#preview.eventMode = "none";
|
||||
this.#preview.visible = false;
|
||||
this.filters = [VisionMaskFilter.create()];
|
||||
this.filterArea = canvas.app.screen;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Highlight the shape or clear the highlight.
|
||||
* @param {foundry.data.BaseShapeData|null} data The shape to highlight, or null to clear the highlight
|
||||
* @internal
|
||||
*/
|
||||
_highlightShape(data) {
|
||||
this.#highlight.clear();
|
||||
this.#highlight.visible = false;
|
||||
if ( !data ) return;
|
||||
this.#highlight.visible = true;
|
||||
this.#highlight.lineStyle({
|
||||
width: CONFIG.Canvas.objectBorderThickness,
|
||||
color: 0x000000,
|
||||
join: PIXI.LINE_JOIN.ROUND,
|
||||
shader: new PIXI.smooth.DashLineShader()
|
||||
});
|
||||
const shape = foundry.canvas.regions.RegionShape.create(data);
|
||||
shape._drawShape(this.#highlight);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the preview shape.
|
||||
* @param {PIXI.FederatedEvent} event
|
||||
*/
|
||||
#refreshPreview(event) {
|
||||
this.#preview.clear();
|
||||
this.#preview.lineStyle({
|
||||
width: CONFIG.Canvas.objectBorderThickness,
|
||||
color: 0x000000,
|
||||
join: PIXI.LINE_JOIN.ROUND,
|
||||
cap: PIXI.LINE_CAP.ROUND,
|
||||
alignment: 0.75
|
||||
});
|
||||
this.#preview.beginFill(event.interactionData.drawingColor, 0.5);
|
||||
this.#drawPreviewShape(event);
|
||||
this.#preview.endFill();
|
||||
this.#preview.lineStyle({
|
||||
width: CONFIG.Canvas.objectBorderThickness / 2,
|
||||
color: CONFIG.Canvas.dispositionColors.CONTROLLED,
|
||||
join: PIXI.LINE_JOIN.ROUND,
|
||||
cap: PIXI.LINE_CAP.ROUND,
|
||||
alignment: 1
|
||||
});
|
||||
this.#drawPreviewShape(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw the preview shape.
|
||||
* @param {PIXI.FederatedEvent} event
|
||||
*/
|
||||
#drawPreviewShape(event) {
|
||||
const data = this.#createShapeData(event);
|
||||
if ( !data ) return;
|
||||
switch ( data.type ) {
|
||||
case "rectangle": this.#preview.drawRect(data.x, data.y, data.width, data.height); break;
|
||||
case "circle": this.#preview.drawCircle(data.x, data.y, data.radius); break;
|
||||
case "ellipse": this.#preview.drawEllipse(data.x, data.y, data.radiusX, data.radiusY); break;
|
||||
case "polygon":
|
||||
const polygon = new PIXI.Polygon(data.points);
|
||||
if ( !polygon.isPositive ) polygon.reverseOrientation();
|
||||
this.#preview.drawPath(polygon.points);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create the shape data.
|
||||
* @param {PIXI.FederatedEvent} event
|
||||
* @returns {object|void}
|
||||
*/
|
||||
#createShapeData(event) {
|
||||
let data;
|
||||
switch ( event.interactionData.drawingTool ) {
|
||||
case "rectangle": data = this.#createRectangleData(event); break;
|
||||
case "ellipse": data = this.#createCircleOrEllipseData(event); break;
|
||||
case "polygon": data = this.#createPolygonData(event); break;
|
||||
}
|
||||
if ( data ) {
|
||||
data.elevation = {
|
||||
bottom: Number.isFinite(this.legend.elevation.bottom) ? this.legend.elevation.bottom : null,
|
||||
top: Number.isFinite(this.legend.elevation.top) ? this.legend.elevation.top : null
|
||||
};
|
||||
if ( this._holeMode ) data.hole = true;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create the rectangle shape data.
|
||||
* @param {PIXI.FederatedEvent} event
|
||||
* @returns {object|void}
|
||||
*/
|
||||
#createRectangleData(event) {
|
||||
const {origin, destination} = event.interactionData;
|
||||
let dx = Math.abs(destination.x - origin.x);
|
||||
let dy = Math.abs(destination.y - origin.y);
|
||||
if ( event.altKey ) dx = dy = Math.min(dx, dy);
|
||||
let x = origin.x;
|
||||
let y = origin.y;
|
||||
if ( event.ctrlKey || event.metaKey ) {
|
||||
x -= dx;
|
||||
y -= dy;
|
||||
dx *= 2;
|
||||
dy *= 2;
|
||||
} else {
|
||||
if ( origin.x > destination.x ) x -= dx;
|
||||
if ( origin.y > destination.y ) y -= dy;
|
||||
}
|
||||
if ( (dx === 0) || (dy === 0) ) return;
|
||||
return {type: "rectangle", x, y, width: dx, height: dy, rotation: 0};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create the circle or ellipse shape data.
|
||||
* @param {PIXI.FederatedEvent} event
|
||||
* @returns {object|void}
|
||||
*/
|
||||
#createCircleOrEllipseData(event) {
|
||||
const {origin, destination} = event.interactionData;
|
||||
let dx = Math.abs(destination.x - origin.x);
|
||||
let dy = Math.abs(destination.y - origin.y);
|
||||
if ( event.altKey ) dx = dy = Math.min(dx, dy);
|
||||
let x = origin.x;
|
||||
let y = origin.y;
|
||||
if ( !(event.ctrlKey || event.metaKey) ) {
|
||||
if ( origin.x > destination.x ) x -= dx;
|
||||
if ( origin.y > destination.y ) y -= dy;
|
||||
dx /= 2;
|
||||
dy /= 2;
|
||||
x += dx;
|
||||
y += dy;
|
||||
}
|
||||
if ( (dx === 0) || (dy === 0) ) return;
|
||||
return event.altKey
|
||||
? {type: "circle", x, y, radius: dx}
|
||||
: {type: "ellipse", x, y, radiusX: dx, radiusY: dy, rotation: 0};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create the polygon shape data.
|
||||
* @param {PIXI.FederatedEvent} event
|
||||
* @returns {object|void}
|
||||
*/
|
||||
#createPolygonData(event) {
|
||||
let {destination, points, complete} = event.interactionData;
|
||||
if ( !complete ) points = [...points, destination.x, destination.y];
|
||||
else if ( points.length < 6 ) return;
|
||||
return {type: "polygon", points};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onClickLeft(event) {
|
||||
const interaction = event.interactionData;
|
||||
|
||||
// Continue polygon point placement
|
||||
if ( interaction.drawingTool === "polygon" ) {
|
||||
const {destination, points} = interaction;
|
||||
const point = !event.shiftKey ? this.getSnappedPoint(destination) : destination;
|
||||
|
||||
// Clicking on the first point closes the shape
|
||||
if ( (point.x === points.at(0)) && (point.y === points.at(1)) ) {
|
||||
interaction.complete = true;
|
||||
}
|
||||
|
||||
// Don't add the point if it is equal to the last one
|
||||
else if ( (point.x !== points.at(-2)) || (point.y !== points.at(-1)) ) {
|
||||
interaction.points.push(point.x, point.y);
|
||||
this.#refreshPreview(event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If one of the drawing tools is selected, prevent left-click-to-release
|
||||
if ( ["rectangle", "ellipse", "polygon"].includes(game.activeTool) ) return;
|
||||
|
||||
// Standard left-click handling
|
||||
super._onClickLeft(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onClickLeft2(event) {
|
||||
const interaction = event.interactionData;
|
||||
|
||||
// Conclude polygon drawing with a double-click
|
||||
if ( interaction.drawingTool === "polygon" ) {
|
||||
interaction.complete = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard double-click handling
|
||||
super._onClickLeft2(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_canDragLeftStart(user, event) {
|
||||
if ( !super._canDragLeftStart(user, event) ) return false;
|
||||
if ( !["rectangle", "ellipse", "polygon"].includes(game.activeTool) ) return false;
|
||||
if ( this.controlled.length > 1 ) {
|
||||
ui.notifications.error("REGION.NOTIFICATIONS.DrawingMultipleRegionsControlled", {localize: true});
|
||||
return false;
|
||||
}
|
||||
if ( this.controlled.at(0)?.document.locked ) {
|
||||
ui.notifications.warn(game.i18n.format("CONTROLS.ObjectIsLocked", {
|
||||
type: game.i18n.localize(RegionDocument.metadata.label)}));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_onDragLeftStart(event) {
|
||||
const interaction = event.interactionData;
|
||||
if ( !event.shiftKey ) interaction.origin = this.getSnappedPoint(interaction.origin);
|
||||
|
||||
// Set drawing tool
|
||||
interaction.drawingTool = game.activeTool;
|
||||
interaction.drawingRegion = this.controlled.at(0);
|
||||
interaction.drawingColor = interaction.drawingRegion?.document.color
|
||||
?? Color.from(RegionDocument.schema.fields.color.getInitialValue({}));
|
||||
|
||||
// Initialize the polygon points with the origin
|
||||
if ( interaction.drawingTool === "polygon" ) {
|
||||
const point = interaction.origin;
|
||||
interaction.points = [point.x, point.y];
|
||||
}
|
||||
this.#refreshPreview(event);
|
||||
this.#preview.visible = true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_onDragLeftMove(event) {
|
||||
const interaction = event.interactionData;
|
||||
if ( !interaction.drawingTool ) return;
|
||||
if ( !event.shiftKey ) interaction.destination = this.getSnappedPoint(interaction.destination);
|
||||
this.#refreshPreview(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_onDragLeftDrop(event) {
|
||||
const interaction = event.interactionData;
|
||||
if ( !interaction.drawingTool ) return;
|
||||
if ( !event.shiftKey ) interaction.destination = this.getSnappedPoint(interaction.destination);
|
||||
|
||||
// In-progress polygon drawing
|
||||
if ( (interaction.drawingTool === "polygon") && (interaction.complete !== true) ) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear preview and refresh Regions
|
||||
this.#preview.clear();
|
||||
this.#preview.visible = false;
|
||||
|
||||
// Create the shape from the preview
|
||||
const shape = this.#createShapeData(event);
|
||||
if ( !shape ) return;
|
||||
|
||||
// Add the shape to controlled Region or create a new Region if none is controlled
|
||||
const region = interaction.drawingRegion;
|
||||
if ( region ) {
|
||||
if ( !region.document.locked ) region.document.update({shapes: [...region.document.shapes, shape]});
|
||||
} else RegionDocument.implementation.create({
|
||||
name: RegionDocument.implementation.defaultName({parent: canvas.scene}),
|
||||
color: interaction.drawingColor,
|
||||
shapes: [shape]
|
||||
}, {parent: canvas.scene, renderSheet: true}).then(r => r.object.control({releaseOthers: true}));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_onDragLeftCancel(event) {
|
||||
const interaction = event.interactionData;
|
||||
if ( !interaction.drawingTool ) return;
|
||||
|
||||
// Remove point from in-progress polygon drawing
|
||||
if ( (interaction.drawingTool === "polygon") && (interaction.complete !== true) ) {
|
||||
interaction.points.splice(-2, 2);
|
||||
if ( interaction.points.length ) {
|
||||
event.preventDefault();
|
||||
this.#refreshPreview(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear preview and refresh Regions
|
||||
this.#preview.clear();
|
||||
this.#preview.visible = false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onClickRight(event) {
|
||||
const interaction = event.interactionData;
|
||||
if ( interaction.drawingTool ) return canvas.mouseInteractionManager._dragRight = false;
|
||||
super._onClickRight(event);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user