Files
2025-01-04 00:34:03 +01:00

391 lines
12 KiB
JavaScript

/**
* A tool for fine-tuning the grid in a Scene
* @param {Scene} scene The scene whose grid is being configured.
* @param {SceneConfig} sheet The Scene Configuration sheet that spawned this dialog.
* @param {FormApplicationOptions} [options] Application configuration options.
*/
class GridConfig extends FormApplication {
constructor(scene, sheet, ...args) {
super(scene, ...args);
/**
* Track the Scene Configuration sheet reference
* @type {SceneConfig}
*/
this.sheet = sheet;
}
/**
* A reference to the bound key handler function
* @type {Function}
*/
#keyHandler;
/**
* A reference to the bound mousewheel handler function
* @type {Function}
*/
#wheelHandler;
/**
* The preview scene
* @type {Scene}
*/
#scene = null;
/**
* The container containing the preview background image and grid
* @type {PIXI.Container|null}
*/
#preview = null;
/**
* The background preview
* @type {PIXI.Sprite|null}
*/
#background = null;
/**
* The grid preview
* @type {GridMesh|null}
*/
#grid = null;
/* -------------------------------------------- */
/** @inheritDoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "grid-config",
template: "templates/scene/grid-config.html",
title: game.i18n.localize("SCENES.GridConfigTool"),
width: 480,
height: "auto",
closeOnSubmit: true
});
}
/* -------------------------------------------- */
/** @inheritDoc */
async _render(force, options) {
const states = Application.RENDER_STATES;
if ( force && [states.CLOSED, states.NONE].includes(this._state) ) {
if ( !this.object.background.src ) {
ui.notifications.warn("WARNING.GridConfigNoBG", {localize: true});
}
this.#scene = this.object.clone();
}
await super._render(force, options);
await this.#createPreview();
}
/* -------------------------------------------- */
/** @override */
getData(options={}) {
const bg = getTexture(this.#scene.background.src);
return {
gridTypes: SceneConfig._getGridTypes(),
scale: this.#scene.background.src ? this.object.width / bg.width : 1,
scene: this.#scene
};
}
/* -------------------------------------------- */
/** @inheritDoc */
_getSubmitData(updateData) {
const formData = super._getSubmitData(updateData);
const bg = getTexture(this.#scene.background.src);
const tex = bg ? bg : {width: this.object.width, height: this.object.height};
formData.width = tex.width * formData.scale;
formData.height = tex.height * formData.scale;
delete formData.scale;
return formData;
}
/* -------------------------------------------- */
/** @inheritDoc */
async close(options={}) {
document.removeEventListener("keydown", this.#keyHandler);
document.removeEventListener("wheel", this.#wheelHandler);
this.#keyHandler = this.#wheelHandler = undefined;
await this.sheet.maximize();
const states = Application.RENDER_STATES;
if ( options.force || [states.RENDERED, states.ERROR].includes(this._state) ) {
this.#scene = null;
this.#destroyPreview();
}
return super.close(options);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @inheritDoc */
activateListeners(html) {
super.activateListeners(html);
this.#keyHandler ||= this.#onKeyDown.bind(this);
document.addEventListener("keydown", this.#keyHandler);
this.#wheelHandler ||= this.#onWheel.bind(this);
document.addEventListener("wheel", this.#wheelHandler, {passive: false});
html.find('button[name="reset"]').click(this.#onReset.bind(this));
}
/* -------------------------------------------- */
/**
* Handle keyboard events.
* @param {KeyboardEvent} event The original keydown event
*/
#onKeyDown(event) {
const key = event.code;
const up = ["KeyW", "ArrowUp"];
const down = ["KeyS", "ArrowDown"];
const left = ["KeyA", "ArrowLeft"];
const right = ["KeyD", "ArrowRight"];
const moveKeys = up.concat(down).concat(left).concat(right);
if ( !moveKeys.includes(key) ) return;
// Increase the Scene scale on shift + up or down
if ( event.shiftKey ) {
event.preventDefault();
event.stopPropagation();
const delta = up.includes(key) ? 1 : (down.includes(key) ? -1 : 0);
this.#scaleBackgroundSize(delta);
}
// Resize grid size on ALT
else if ( event.altKey ) {
event.preventDefault();
event.stopPropagation();
const delta = up.includes(key) ? 1 : (down.includes(key) ? -1 : 0);
this.#scaleGridSize(delta);
}
// Shift grid position
else if ( !game.keyboard.hasFocus ) {
event.preventDefault();
event.stopPropagation();
if ( up.includes(key) ) this.#shiftBackground({deltaY: -1});
else if ( down.includes(key) ) this.#shiftBackground({deltaY: 1});
else if ( left.includes(key) ) this.#shiftBackground({deltaX: -1});
else if ( right.includes(key) ) this.#shiftBackground({deltaX: 1});
}
}
/* -------------------------------------------- */
/**
* Handle mousewheel events.
* @param {WheelEvent} event The original wheel event
*/
#onWheel(event) {
if ( event.deltaY === 0 ) return;
const normalizedDelta = -Math.sign(event.deltaY);
const activeElement = document.activeElement;
const noShiftAndAlt = !(event.shiftKey || event.altKey);
const focus = game.keyboard.hasFocus && document.hasFocus;
// Increase/Decrease the Scene scale
if ( event.shiftKey || (!event.altKey && focus && activeElement.name === "scale") ) {
event.preventDefault();
event.stopImmediatePropagation();
this.#scaleBackgroundSize(normalizedDelta);
}
// Increase/Decrease the Grid scale
else if ( event.altKey || (focus && activeElement.name === "grid.size") ) {
event.preventDefault();
event.stopImmediatePropagation();
this.#scaleGridSize(normalizedDelta);
}
// If no shift or alt key are pressed
else if ( noShiftAndAlt && focus ) {
// Increase/Decrease the background x offset
if ( activeElement.name === "background.offsetX" ) {
event.preventDefault();
event.stopImmediatePropagation();
this.#shiftBackground({deltaX: normalizedDelta});
}
// Increase/Decrease the background y offset
else if ( activeElement.name === "background.offsetY" ) {
event.preventDefault();
event.stopImmediatePropagation();
this.#shiftBackground({deltaY: normalizedDelta});
}
}
}
/* -------------------------------------------- */
/**
* Handle reset.
*/
#onReset() {
if ( !this.#scene ) return;
this.#scene = this.object.clone();
this.render();
}
/* -------------------------------------------- */
/** @inheritdoc */
async _onChangeInput(event) {
await super._onChangeInput(event);
const previewData = this._getSubmitData();
this.#previewChanges(previewData);
}
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
const changes = foundry.utils.flattenObject(
foundry.utils.diffObject(this.object.toObject(), foundry.utils.expandObject(formData)));
if ( ["width", "height", "padding", "background.offsetX", "background.offsetY", "grid.size", "grid.type"].some(k => k in changes) ) {
const confirm = await Dialog.confirm({
title: game.i18n.localize("SCENES.DimensionChangeTitle"),
content: `<p>${game.i18n.localize("SCENES.DimensionChangeWarning")}</p>`
});
// Update only if the dialog is confirmed
if ( confirm ) return this.object.update(formData, {fromSheet: true});
}
}
/* -------------------------------------------- */
/* Previewing and Updating Functions */
/* -------------------------------------------- */
/**
* Create preview
*/
async #createPreview() {
if ( !this.#scene ) return;
if ( this.#preview ) this.#destroyPreview();
this.#preview = canvas.stage.addChild(new PIXI.Container());
this.#preview.eventMode = "none";
const fill = this.#preview.addChild(new PIXI.Sprite(PIXI.Texture.WHITE));
fill.tint = 0x000000;
fill.eventMode = "static";
fill.hitArea = canvas.app.screen;
// Patching updateTransform to render the fill in screen space
fill.updateTransform = function() {
const screen = canvas.app.screen;
this.width = screen.width;
this.height = screen.height;
this._boundsID++;
this.transform.updateTransform(PIXI.Transform.IDENTITY);
this.worldAlpha = this.alpha;
};
this.#background = this.#preview.addChild(new PIXI.Sprite());
this.#background.eventMode = "none";
if ( this.#scene.background.src ) {
try {
this.#background.texture = await loadTexture(this.#scene.background.src);
} catch(e) {
this.#background.texture = PIXI.Texture.WHITE;
console.error(e);
}
} else {
this.#background.texture = PIXI.Texture.WHITE;
}
this.#grid = this.#preview.addChild(new GridMesh().initialize({color: 0xFF0000}));
this.#refreshPreview();
}
/* -------------------------------------------- */
/**
* Preview changes to the Scene document as if they were true document updates.
* @param {object} [change] A change to preview.
*/
#previewChanges(change) {
if ( !this.#scene ) return;
if ( change ) this.#scene.updateSource(change);
this.#refreshPreview();
}
/* -------------------------------------------- */
/**
* Refresh the preview
*/
#refreshPreview() {
if ( !this.#scene || (this.#preview?.destroyed !== false) ) return;
// Update the background image
const d = this.#scene.dimensions;
this.#background.position.set(d.sceneX, d.sceneY);
this.#background.width = d.sceneWidth;
this.#background.height = d.sceneHeight;
// Update the grid
this.#grid.initialize({
type: this.#scene.grid.type,
width: d.width,
height: d.height,
size: d.size
});
}
/* -------------------------------------------- */
/**
* Destroy the preview
*/
#destroyPreview() {
if ( this.#preview?.destroyed === false ) this.#preview.destroy({children: true});
this.#preview = null;
this.#background = null;
this.#grid = null;
}
/* -------------------------------------------- */
/**
* Scale the background size relative to the grid size
* @param {number} delta The directional change in background size
*/
#scaleBackgroundSize(delta) {
const scale = (parseFloat(this.form.scale.value) + (delta * 0.001)).toNearest(0.001);
this.form.scale.value = Math.clamp(scale, 0.25, 10.0);
this.form.scale.dispatchEvent(new Event("change", {bubbles: true}));
}
/* -------------------------------------------- */
/**
* Scale the grid size relative to the background image.
* When scaling the grid size in this way, constrain the allowed values between 50px and 300px.
* @param {number} delta The grid size in pixels
*/
#scaleGridSize(delta) {
const gridSize = this.form.elements["grid.size"];
gridSize.value = Math.clamp(gridSize.valueAsNumber + delta, 50, 300);
gridSize.dispatchEvent(new Event("change", {bubbles: true}));
}
/* -------------------------------------------- */
/**
* Shift the background image relative to the grid layer
* @param {object} position The position configuration to preview
* @param {number} [position.deltaX=0] The number of pixels to shift in the x-direction
* @param {number} [position.deltaY=0] The number of pixels to shift in the y-direction
*/
#shiftBackground({deltaX=0, deltaY=0}) {
const ox = this.form["background.offsetX"];
ox.value = parseInt(this.form["background.offsetX"].value) + deltaX;
this.form["background.offsetY"].value = parseInt(this.form["background.offsetY"].value) + deltaY;
ox.dispatchEvent(new Event("change", {bubbles: true}));
}
}