150 lines
5 KiB
JavaScript
150 lines
5 KiB
JavaScript
/**
|
||
* 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();
|
||
},
|
||
};
|