import EffectMappingForm from '../applications/effectMappingForm.js';
|
|
import { OverlayConfig } from '../applications/overlayConfig.js';
|
|
import { TVAOverlay } from './sprite/TVAOverlay.js';
|
|
import { evaluateOverlayExpressions } from './token/overlay.js';
|
|
|
|
export class Reticle {
|
|
static app;
|
|
static fields;
|
|
static reticleOverlay;
|
|
static active = false;
|
|
static hitTest;
|
|
static token = null;
|
|
static dialog = null;
|
|
|
|
// Offset calculation controls
|
|
static mode = 'tooltip';
|
|
static increment = 1;
|
|
|
|
static _onReticleMove(event) {
|
|
if (this.reticleOverlay.isMouseDown) {
|
|
let pos = event.data.getLocalPosition(this.reticleOverlay);
|
|
|
|
this.config.pOffsetX = 0;
|
|
this.config.pOffsetY = 0;
|
|
this.config.offsetX = 0;
|
|
this.config.offsetY = 0;
|
|
|
|
if (this.mode === 'token') {
|
|
this.config.linkRotation = true;
|
|
this.config.linkMirror = true;
|
|
}
|
|
|
|
this.tvaOverlay.refresh(this.config, { preview: true });
|
|
|
|
const tCoord = { x: this.tvaOverlay.x, y: this.tvaOverlay.y };
|
|
|
|
if (this.tvaOverlay.overlayConfig.parentID) {
|
|
let parent = this.tvaOverlay;
|
|
do {
|
|
parent = parent.parent;
|
|
tCoord.x += parent.x;
|
|
tCoord.y += parent.y;
|
|
} while (!(parent instanceof TVAOverlay));
|
|
}
|
|
|
|
let dx = pos.x - tCoord.x;
|
|
let dy = pos.y - tCoord.y;
|
|
|
|
let angle = 0;
|
|
if (!this.config.animation.relative) {
|
|
angle = this.config.angle;
|
|
if (this.config.linkRotation) angle += this.tvaOverlay.object.document.rotation;
|
|
}
|
|
|
|
[dx, dy] = rotate(0, 0, dx, dy, angle);
|
|
dx = round(dx, this.increment);
|
|
dy = round(dy, this.increment);
|
|
|
|
// let lPos = event.data.getLocalPosition(this.tvaOverlay);
|
|
// console.log(lPos);
|
|
// // let dx = lPos.x;
|
|
// // let dy = lPos.y;
|
|
|
|
if (this.mode === 'static') {
|
|
this.config.pOffsetX = dx;
|
|
this.config.pOffsetY = dy;
|
|
} else if (this.mode === 'token') {
|
|
this.config.offsetX = -dx / this.tvaOverlay.object.w;
|
|
this.config.offsetY = -dy / this.tvaOverlay.object.h;
|
|
} else {
|
|
let token = this.tvaOverlay.object;
|
|
|
|
let pWidth;
|
|
let pHeight;
|
|
|
|
if (this.tvaOverlay.overlayConfig.parentID) {
|
|
pWidth =
|
|
(this.tvaOverlay.parent.shapesWidth ?? this.tvaOverlay.parent.width) / this.tvaOverlay.parent.scale.x;
|
|
pHeight =
|
|
(this.tvaOverlay.parent.shapesHeight ?? this.tvaOverlay.parent.height) / this.tvaOverlay.parent.scale.y;
|
|
} else {
|
|
pWidth = token.w;
|
|
pHeight = token.h;
|
|
}
|
|
|
|
if (this.mode === 'tooltip') {
|
|
if (Math.abs(dx) >= pWidth / 2) {
|
|
this.config.offsetX = 0.5 * (dx < 0 ? 1 : -1);
|
|
dx += (pWidth / 2) * (dx < 0 ? 1 : -1);
|
|
} else {
|
|
this.config.offsetX = -dx / this.tvaOverlay.object.w;
|
|
dx = 0;
|
|
}
|
|
|
|
if (Math.abs(dy) >= pHeight / 2) {
|
|
this.config.offsetY = 0.5 * (dy < 0 ? 1 : -1);
|
|
dy += (pHeight / 2) * (dy < 0 ? 1 : -1);
|
|
} else {
|
|
this.config.offsetY = -dy / this.tvaOverlay.object.h;
|
|
dy = 0;
|
|
}
|
|
} else {
|
|
if (Math.abs(dx) >= pWidth / 2) {
|
|
this.config.offsetX = 0.5 * (dx < 0 ? 1 : -1);
|
|
dx += (pWidth / 2) * (dx < 0 ? 1 : -1);
|
|
} else if (Math.abs(dy) >= pHeight / 2) {
|
|
this.config.offsetY = 0.5 * (dy < 0 ? 1 : -1);
|
|
dy += (pHeight / 2) * (dy < 0 ? 1 : -1);
|
|
} else {
|
|
this.config.offsetX = -dx / this.tvaOverlay.object.w;
|
|
dx = 0;
|
|
this.config.offsetY = -dy / this.tvaOverlay.object.h;
|
|
dy = 0;
|
|
}
|
|
}
|
|
|
|
this.config.pOffsetX = dx;
|
|
this.config.pOffsetY = dy;
|
|
}
|
|
|
|
this.tvaOverlay.refresh(this.config, { preview: true });
|
|
}
|
|
}
|
|
|
|
static minimizeApps() {
|
|
Object.values(ui.windows).forEach((app) => {
|
|
if (app instanceof OverlayConfig || app instanceof EffectMappingForm) {
|
|
app.minimize();
|
|
}
|
|
});
|
|
}
|
|
|
|
static maximizeApps() {
|
|
Object.values(ui.windows).forEach((app) => {
|
|
if (app instanceof OverlayConfig || app instanceof EffectMappingForm) {
|
|
app.maximize();
|
|
}
|
|
});
|
|
}
|
|
|
|
static activate({ tvaOverlay = null, config = {} } = {}) {
|
|
if (this.deactivate() || !canvas.ready) return false;
|
|
if (!tvaOverlay || !config) return false;
|
|
|
|
if (this.reticleOverlay) {
|
|
this.reticleOverlay.destroy(true);
|
|
}
|
|
|
|
const interaction = canvas.app.renderer.plugins.interaction;
|
|
if (!interaction.cursorStyles['reticle']) {
|
|
interaction.cursorStyles['reticle'] = "url('modules/token-variants/img/reticle.webp'), auto";
|
|
}
|
|
|
|
this.tvaOverlay = tvaOverlay;
|
|
|
|
this.minimizeApps();
|
|
this.config = evaluateOverlayExpressions(deepClone(config), this.tvaOverlay.object, {
|
|
overlayConfig: config,
|
|
});
|
|
|
|
// Setup the overlay to be always visible while we're adjusting its position
|
|
this.config.alwaysVisible = true;
|
|
|
|
this.active = true;
|
|
|
|
// Create the reticle overlay
|
|
this.reticleOverlay = new PIXI.Container();
|
|
this.reticleOverlay.hitArea = canvas.dimensions.rect;
|
|
this.reticleOverlay.cursor = 'reticle';
|
|
this.reticleOverlay.interactive = true;
|
|
this.reticleOverlay.zIndex = Infinity;
|
|
|
|
const stopEvent = function (event) {
|
|
event.preventDefault();
|
|
// event.stopPropagation();
|
|
};
|
|
|
|
this.reticleOverlay.on('mousedown', (event) => {
|
|
event.preventDefault();
|
|
|
|
if (event.data.originalEvent.which != 2 && event.data.originalEvent.nativeEvent.which != 2) {
|
|
this.reticleOverlay.isMouseDown = true;
|
|
this._onReticleMove(event);
|
|
}
|
|
});
|
|
this.reticleOverlay.on('pointermove', (event) => {
|
|
event.preventDefault();
|
|
// event.stopPropagation();
|
|
this._onReticleMove(event);
|
|
});
|
|
this.reticleOverlay.on('mouseup', (event) => {
|
|
event.preventDefault();
|
|
this.reticleOverlay.isMouseDown = false;
|
|
});
|
|
this.reticleOverlay.on('click', (event) => {
|
|
event.preventDefault();
|
|
if (event.data.originalEvent.which == 2 || event.data.originalEvent.nativeEvent.which == 2) {
|
|
this.deactivate();
|
|
}
|
|
});
|
|
|
|
canvas.stage.addChild(this.reticleOverlay);
|
|
this.dialog = displayControlDialog();
|
|
return true;
|
|
}
|
|
|
|
static deactivate() {
|
|
if (this.active) {
|
|
if (this.reticleOverlay) this.reticleOverlay.parent?.removeChild(this.reticleOverlay);
|
|
this.active = false;
|
|
this.tvaOverlay = null;
|
|
if (this.dialog && this.dialog._state !== Application.RENDER_STATES.CLOSED) this.dialog.close(true);
|
|
this.dialog = null;
|
|
this.maximizeApps();
|
|
|
|
const app = Object.values(ui.windows).find((app) => app instanceof OverlayConfig);
|
|
if (!app) {
|
|
this.config = null;
|
|
return;
|
|
}
|
|
const form = $(app.form);
|
|
|
|
['pOffsetX', 'pOffsetY', 'offsetX', 'offsetY'].forEach((field) => {
|
|
if (field in this.config) {
|
|
form.find(`[name="${field}"]`).val(this.config[field]);
|
|
}
|
|
});
|
|
|
|
if (this.mode === 'token') {
|
|
['linkRotation', 'linkMirror'].forEach((field) => {
|
|
form.find(`[name="${field}"]`).prop('checked', true);
|
|
});
|
|
['linkDimensionsX', 'linkDimensionsY'].forEach((field) => {
|
|
form.find(`[name="${field}"]`).prop('checked', false);
|
|
});
|
|
} else {
|
|
['linkRotation', 'linkMirror'].forEach((field) => {
|
|
form.find(`[name="${field}"]`).prop('checked', false);
|
|
});
|
|
}
|
|
|
|
if (this.mode === 'hud') {
|
|
form.find('[name="ui"]').prop('checked', true).trigger('change');
|
|
}
|
|
|
|
form.find('[name="anchor.x"]').val(this.config.anchor.x);
|
|
form.find('[name="anchor.y"]').val(this.config.anchor.y).trigger('change');
|
|
this.config = null;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function displayControlDialog() {
|
|
const d = new Dialog({
|
|
title: 'Set Overlay Position',
|
|
content: `
|
|
<style>
|
|
.images { display: flex; }
|
|
.images a { flex: 20%; width: 50px; margin: 2px; }
|
|
.images a.active img { border-color: orange; border-width: 2px; }
|
|
.anchorlbl {margin: auto; display: table; }
|
|
</style>
|
|
<div class="images">
|
|
<a data-id="token"><img src="modules/token-variants/img/token_mode.png"></img></a>
|
|
<a data-id="tooltip"><img src="modules/token-variants/img/tooltip_mode.png"></img></a>
|
|
<a data-id="hud"><img src="modules/token-variants/img/hud_mode.png"></img></a>
|
|
<a data-id="static"><img src="modules/token-variants/img/static_mode.png"></img></a>
|
|
</div>
|
|
<br>
|
|
<label class="anchorlbl">Anchor</label>
|
|
<div class="tva-anchor">
|
|
<input type="radio" class="top left" name="anchor">
|
|
<input type="radio" class="top center" name="anchor">
|
|
<input type="radio" class="top right" name="anchor">
|
|
<input type="radio" class="mid left" name="anchor">
|
|
<input type="radio" class="mid center" name="anchor">
|
|
<input type="radio" class="mid right" name="anchor">
|
|
<input type="radio" class="bot left" name="anchor">
|
|
<input type="radio" class="bot center" name="anchor">
|
|
<input type="radio" class="bot right" name="anchor">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Step Size</label>
|
|
<div class="form-fields">
|
|
<input type="number" name="step" min="0" step="1" value="${Reticle.increment}">
|
|
</div>
|
|
</div>
|
|
<p class="notes"><b>Left-Click</b> to move the overlay</p>
|
|
<p class="notes"><b>Middle-Click</b> or <b>Close Dialog</b> to exit overlay positioning</p>
|
|
`,
|
|
buttons: {},
|
|
render: (html) => {
|
|
// Mode Images
|
|
const images = html.find('.images a');
|
|
html.find('.images a').on('click', (event) => {
|
|
images.removeClass('active');
|
|
const target = $(event.target).closest('a');
|
|
target.addClass('active');
|
|
Reticle.mode = target.data('id');
|
|
});
|
|
html.find(`[data-id="${Reticle.mode}"]`).addClass('active');
|
|
|
|
// Anchor
|
|
let anchorX = Reticle.config?.anchor?.x || 0;
|
|
let anchorY = Reticle.config?.anchor?.y || 0;
|
|
|
|
let classes = '';
|
|
if (anchorX < 0.5) classes += '.left';
|
|
else if (anchorX > 0.5) classes += '.right';
|
|
else classes += '.center';
|
|
|
|
if (anchorY < 0.5) classes += '.top';
|
|
else if (anchorY > 0.5) classes += '.bot';
|
|
else classes += '.mid';
|
|
|
|
html.find('.tva-anchor').find(classes).prop('checked', true);
|
|
// end - Pre-select anchor
|
|
|
|
html.find('input[name="anchor"]').on('change', (event) => {
|
|
const anchor = $(event.target);
|
|
let x;
|
|
let y;
|
|
if (anchor.hasClass('left')) x = 0;
|
|
else if (anchor.hasClass('center')) x = 0.5;
|
|
else x = 1;
|
|
|
|
if (anchor.hasClass('top')) y = 0;
|
|
else if (anchor.hasClass('mid')) y = 0.5;
|
|
else y = 1;
|
|
|
|
Reticle.config.anchor.x = x;
|
|
Reticle.config.anchor.y = y;
|
|
});
|
|
|
|
html.find('[name="step"]').on('input', (event) => {
|
|
Reticle.increment = $(event.target).val() || 1;
|
|
});
|
|
},
|
|
close: () => Reticle.deactivate(),
|
|
});
|
|
d.render(true);
|
|
setTimeout(() => d.setPosition({ left: 200, top: window.innerHeight / 2, height: 'auto' }), 100);
|
|
return d;
|
|
}
|
|
|
|
function round(number, increment, offset = 0) {
|
|
return Math.ceil((number - offset) / increment) * increment + offset;
|
|
}
|
|
|
|
function rotate(cx, cy, x, y, angle) {
|
|
var radians = (Math.PI / 180) * angle,
|
|
cos = Math.cos(radians),
|
|
sin = Math.sin(radians),
|
|
nx = cos * (x - cx) + sin * (y - cy) + cx,
|
|
ny = cos * (y - cy) - sin * (x - cx) + cy;
|
|
return [nx, ny];
|
|
}
|