Files
Foundry-VTT-Docker/resources/app/client/pixi/placeables/note.js

350 lines
10 KiB
JavaScript
Raw Normal View History

2025-01-04 00:34:03 +01:00
/**
* A Note is an implementation of PlaceableObject which represents an annotated location within the Scene.
* Each Note links to a JournalEntry document and represents its location on the map.
* @category - Canvas
* @see {@link NoteDocument}
* @see {@link NotesLayer}
*/
class Note extends PlaceableObject {
/** @inheritdoc */
static embeddedName = "Note";
/** @override */
static RENDER_FLAGS = {
redraw: {propagate: ["refresh"]},
refresh: {propagate: ["refreshState", "refreshPosition", "refreshTooltip", "refreshElevation"], alias: true},
refreshState: {propagate: ["refreshVisibility"]},
refreshVisibility: {},
refreshPosition: {},
refreshTooltip: {},
refreshElevation: {propagate: ["refreshVisibility"]},
/** @deprecated since v12 */
refreshText: {propagate: ["refreshTooltip"], deprecated: {since: 12, until: 14}, alias: true}
};
/* -------------------------------------------- */
/**
* The control icon.
* @type {ControlIcon}
*/
controlIcon;
/* -------------------------------------------- */
/**
* The tooltip.
* @type {PreciseText}
*/
tooltip;
/* -------------------------------------------- */
/** @override */
get bounds() {
const {x, y, iconSize} = this.document;
const r = iconSize / 2;
return new PIXI.Rectangle(x - r, y - r, 2*r, 2*r);
}
/* -------------------------------------------- */
/**
* The associated JournalEntry which is referenced by this Note
* @type {JournalEntry}
*/
get entry() {
return this.document.entry;
}
/* -------------------------------------------- */
/**
* The specific JournalEntryPage within the associated JournalEntry referenced by this Note.
*/
get page() {
return this.document.page;
}
/* -------------------------------------------- */
/**
* Determine whether the Note is visible to the current user based on their perspective of the Scene.
* Visibility depends on permission to the underlying journal entry, as well as the perspective of controlled Tokens.
* If Token Vision is required, the user must have a token with vision over the note to see it.
* @type {boolean}
*/
get isVisible() {
const accessTest = this.document.page ?? this.document.entry;
const access = accessTest?.testUserPermission(game.user, "LIMITED") ?? true;
if ( (access === false) || !canvas.visibility.tokenVision || this.document.global ) return access;
const point = {x: this.document.x, y: this.document.y};
const tolerance = this.document.iconSize / 4;
return canvas.visibility.testVisibility(point, {tolerance, object: this});
}
/* -------------------------------------------- */
/* Rendering
/* -------------------------------------------- */
/** @override */
async _draw(options) {
this.controlIcon = this.addChild(this._drawControlIcon());
this.tooltip = this.addChild(this._drawTooltip());
}
/* -------------------------------------------- */
/**
* Draw the control icon.
* @returns {ControlIcon}
* @protected
*/
_drawControlIcon() {
const {texture, iconSize} = this.document;
const icon = new ControlIcon({texture: texture.src, size: iconSize, tint: texture.tint});
icon.x -= (iconSize / 2);
icon.y -= (iconSize / 2);
return icon;
}
/* -------------------------------------------- */
/**
* Draw the tooltip.
* @returns {PreciseText}
* @protected
*/
_drawTooltip() {
const tooltip = new PreciseText(this.document.label, this._getTextStyle());
tooltip.eventMode = "none";
return tooltip;
}
/* -------------------------------------------- */
/**
* Refresh the tooltip.
* @protected
*/
_refreshTooltip() {
this.tooltip.text = this.document.label;
this.tooltip.style = this._getTextStyle();
const halfPad = (0.5 * this.document.iconSize) + 12;
switch ( this.document.textAnchor ) {
case CONST.TEXT_ANCHOR_POINTS.CENTER:
this.tooltip.anchor.set(0.5, 0.5);
this.tooltip.position.set(0, 0);
break;
case CONST.TEXT_ANCHOR_POINTS.BOTTOM:
this.tooltip.anchor.set(0.5, 0);
this.tooltip.position.set(0, halfPad);
break;
case CONST.TEXT_ANCHOR_POINTS.TOP:
this.tooltip.anchor.set(0.5, 1);
this.tooltip.position.set(0, -halfPad);
break;
case CONST.TEXT_ANCHOR_POINTS.LEFT:
this.tooltip.anchor.set(1, 0.5);
this.tooltip.position.set(-halfPad, 0);
break;
case CONST.TEXT_ANCHOR_POINTS.RIGHT:
this.tooltip.anchor.set(0, 0.5);
this.tooltip.position.set(halfPad, 0);
break;
}
}
/* -------------------------------------------- */
/**
* Define a PIXI TextStyle object which is used for the tooltip displayed for this Note
* @returns {PIXI.TextStyle}
* @protected
*/
_getTextStyle() {
const style = CONFIG.canvasTextStyle.clone();
// Positioning
if ( this.document.textAnchor === CONST.TEXT_ANCHOR_POINTS.LEFT ) style.align = "right";
else if ( this.document.textAnchor === CONST.TEXT_ANCHOR_POINTS.RIGHT ) style.align = "left";
// Font preferences
style.fontFamily = this.document.fontFamily || CONFIG.defaultFontFamily;
style.fontSize = this.document.fontSize;
// Toggle stroke style depending on whether the text color is dark or light
const color = this.document.textColor;
style.fill = color;
style.stroke = color.hsv[2] > 0.6 ? 0x000000 : 0xFFFFFF;
style.strokeThickness = 4;
return style;
}
/* -------------------------------------------- */
/* Incremental Refresh */
/* -------------------------------------------- */
/** @override */
_applyRenderFlags(flags) {
if ( flags.refreshState ) this._refreshState();
if ( flags.refreshVisibility ) this._refreshVisibility();
if ( flags.refreshPosition ) this._refreshPosition();
if ( flags.refreshTooltip ) this._refreshTooltip();
if ( flags.refreshElevation ) this._refreshElevation();
}
/* -------------------------------------------- */
/**
* Refresh the visibility.
* @protected
*/
_refreshVisibility() {
const wasVisible = this.visible;
this.visible = this.isVisible;
if ( this.controlIcon ) this.controlIcon.refresh({
visible: this.visible,
borderVisible: this.hover || this.layer.highlightObjects
});
if ( wasVisible !== this.visible ) {
this.layer.hintMapNotes();
MouseInteractionManager.emulateMoveEvent();
}
}
/* -------------------------------------------- */
/**
* Refresh the state of the Note. Called the Note enters a different interaction state.
* @protected
*/
_refreshState() {
this.alpha = this._getTargetAlpha();
this.tooltip.visible = this.hover || this.layer.highlightObjects;
this.zIndex = this.hover ? 1 : 0;
}
/* -------------------------------------------- */
/**
* Refresh the position of the Note. Called with the coordinates change.
* @protected
*/
_refreshPosition() {
const {x, y} = this.document;
if ( (this.position.x !== x) || (this.position.y !== y) ) MouseInteractionManager.emulateMoveEvent();
this.position.set(this.document.x, this.document.y);
}
/* -------------------------------------------- */
/**
* Refresh the elevation of the control icon.
* @protected
*/
_refreshElevation() {
this.controlIcon.elevation = this.document.elevation;
}
/* -------------------------------------------- */
/* Document Event Handlers */
/* -------------------------------------------- */
/** @inheritDoc */
_onUpdate(changed, options, userId) {
super._onUpdate(changed, options, userId);
// Incremental Refresh
const positionChanged = ("x" in changed) || ("y" in changed);
this.renderFlags.set({
redraw: ("texture" in changed) || ("iconSize" in changed),
refreshVisibility: positionChanged || ["entryId", "pageId", "global"].some(k => k in changed),
refreshPosition: positionChanged,
refreshTooltip: ["text", "fontFamily", "fontSize", "textAnchor", "textColor", "iconSize"].some(k => k in changed),
refreshElevation: "elevation" in changed
});
}
/* -------------------------------------------- */
/* Interactivity */
/* -------------------------------------------- */
/** @override */
_canHover(user) {
return true;
}
/* -------------------------------------------- */
/** @override */
_canView(user) {
const {entry, page} = this.document;
if ( !entry ) return false;
if ( game.user.isGM ) return true;
if ( page?.testUserPermission(game.user, "LIMITED", {exact: true}) ) {
// Special-case handling for image pages.
return page.type === "image";
}
const accessTest = page ?? entry;
return accessTest.testUserPermission(game.user, "OBSERVER");
}
/* -------------------------------------------- */
/** @override */
_canConfigure(user) {
return canvas.notes.active && this.document.canUserModify(game.user, "update");
}
/* -------------------------------------------- */
/** @inheritdoc */
_onClickLeft2(event) {
const {entry, page} = this.document;
if ( !entry ) return;
const options = {};
if ( page ) {
options.mode = JournalSheet.VIEW_MODES.SINGLE;
options.pageId = page.id;
}
const allowed = Hooks.call("activateNote", this, options);
if ( allowed === false ) return;
if ( page?.type === "image" ) {
return new ImagePopout(page.src, {
uuid: page.uuid,
title: page.name,
caption: page.image.caption
}).render(true);
}
entry.sheet.render(true, options);
}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
get text() {
const msg = "Note#text has been deprecated. Use Note#document#label instead.";
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
return this.document.label;
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
get size() {
const msg = "Note#size has been deprecated. Use Note#document#iconSize instead.";
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
return this.document.iconSize;
}
}