sotc-foundry-suite/sotc-clash-arrow/scripts/ClashArrowLayer.js

150 lines
5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* sotc-clash-arrow | ClashArrowLayer.js
*
* En Foundry v11, les CanvasLayer custom ne se montent pas via CONFIG.Canvas.layers.
* Solution : attacher deux PIXI.Graphics directement sur canvas.interface
* (le groupe PIXI qui contient tokens, drawings, etc.).
*
* API publique (identique à l'ancienne classe) :
* ClashArrowLayer.init() – à appeler au canvasReady
* ClashArrowLayer.tearDown() – à appeler au canvasTearDown
* ClashArrowLayer.redraw() – redessine toutes les flèches du store
* ClashArrowLayer.drawPreview() – flèche temporaire pendant le drag
* ClashArrowLayer.clearPreview()
*/
import { ArrowStore } from "./ArrowStore.js";
// ── Constantes visuelles ──────────────────────────────────────────────────────
const COLOR_BLUE = 0x4a9eff;
const COLOR_YELLOW = 0xffd84a;
const COLOR_PREVIEW = 0x888899;
const COLOR_USED = 0x8B4513; // marron "Used"
const ALPHA_USED = 0.35; // légèrement visible
const LINE_WIDTH = 3;
const LINE_ALPHA = 0.92;
const ARROW_HEAD_SIZE = 14;
const BEZIER_CURVE = 0.35;
// ── Helpers géométriques ──────────────────────────────────────────────────────
function bezierControlPoints(x1, y1, x2, y2, curvature = BEZIER_CURVE) {
const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2;
const dx = x2 - x1;
const dy = y2 - y1;
const nx = -dy * curvature;
const ny = dx * curvature;
return { cp1x: mx + nx, cp1y: my + ny };
}
function bezierPoint(t, p0, p1, p2, p3) {
const mt = 1 - t;
return mt*mt*mt*p0 + 3*mt*mt*t*p1 + 3*mt*t*t*p2 + t*t*t*p3;
}
function drawArrowHead(gfx, tx, ty, dx, dy, size, color, alpha) {
const angle = Math.atan2(dy, dx);
const spread = Math.PI / 6;
const lx = tx - size * Math.cos(angle - spread);
const ly = ty - size * Math.sin(angle - spread);
const rx = tx - size * Math.cos(angle + spread);
const ry = ty - size * Math.sin(angle + spread);
gfx.lineStyle(0);
gfx.beginFill(color, alpha);
gfx.drawPolygon([tx, ty, lx, ly, rx, ry]);
gfx.endFill();
}
// ── Module singleton ──────────────────────────────────────────────────────────
/** @type {PIXI.Graphics|null} */
let _arrowsGfx = null;
/** @type {PIXI.Graphics|null} */
let _previewGfx = null;
function _drawArrow(gfx, x1, y1, x2, y2, color, alpha = LINE_ALPHA) {
const { cp1x, cp1y } = bezierControlPoints(x1, y1, x2, y2);
gfx.lineStyle(LINE_WIDTH, color, alpha);
gfx.moveTo(x1, y1);
gfx.bezierCurveTo(cp1x, cp1y, cp1x, cp1y, x2, y2);
const t = 0.98;
const px = bezierPoint(t, x1, cp1x, cp1x, x2);
const py = bezierPoint(t, y1, cp1y, cp1y, y2);
drawArrowHead(gfx, x2, y2, x2 - px, y2 - py, ARROW_HEAD_SIZE, color, alpha);
}
export const ClashArrowLayer = {
/**
* Initialise les Graphics PIXI et les attache au canvas.
* Appelé au canvasReady depuis Hooks.js.
*/
init() {
// Si déjà initialisé et Graphics toujours valides, juste redessiner
if (_arrowsGfx && !_arrowsGfx.destroyed) {
this.redraw();
return;
}
// Nettoyer si les Graphics sont corrompus
this.tearDown();
const parent = canvas.interface ?? canvas.stage;
_arrowsGfx = parent.addChild(new PIXI.Graphics());
_previewGfx = parent.addChild(new PIXI.Graphics());
_arrowsGfx.zIndex = 900;
_previewGfx.zIndex = 901;
ArrowStore.on("change", () => this.redraw());
// Dessiner les flèches déjà présentes dans le store
this.redraw();
console.log("sotc-clash-arrow | ClashArrowLayer initialisé sur canvas.interface.");
},
/** Nettoie et retire les Graphics du canvas. */
tearDown() {
ArrowStore.off("change");
if (_arrowsGfx) { _arrowsGfx.parent?.removeChild(_arrowsGfx); _arrowsGfx.destroy(); _arrowsGfx = null; }
if (_previewGfx) { _previewGfx.parent?.removeChild(_previewGfx); _previewGfx.destroy(); _previewGfx = null; }
},
/** Redessine toutes les flèches persistantes. */
redraw() {
if (!_arrowsGfx) return;
_arrowsGfx.clear();
for (const arrow of ArrowStore.all()) {
if (arrow.used) {
_drawArrow(_arrowsGfx, arrow.x1, arrow.y1, arrow.x2, arrow.y2, COLOR_USED, ALPHA_USED);
} else {
const color = arrow.mutual ? COLOR_YELLOW : COLOR_BLUE;
_drawArrow(_arrowsGfx, arrow.x1, arrow.y1, arrow.x2, arrow.y2, color);
}
}
},
/**
* Flèche de preview pendant le drag.
* @param {number} x1 @param {number} y1 Origine (coords monde)
* @param {number} x2 @param {number} y2 Position courante (coords monde)
* @param {number} color
*/
drawPreview(x1, y1, x2, y2, color = COLOR_PREVIEW) {
if (!_previewGfx) return;
_previewGfx.clear();
_drawArrow(_previewGfx, x1, y1, x2, y2, color, 0.55);
},
/** Efface la flèche de preview. */
clearPreview() {
_previewGfx?.clear();
},
};