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: `
Left-Click to move the overlay
Middle-Click or Close Dialog to exit overlay positioning
`, 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]; }