Why a Facing Arc?


When a token turns, it’s helpful—especially in tactical or role-playing scenarios—to see exactly which direction it’s “looking.” A subtle 20º arc projected in front of the token can communicate much more clearly than a bare rotation value.
But how do I hook into Foundry’s event system, check permissions, and still keep my code clean and loosely coupled?
FoundryHookAdapter (Infrastructure Layer)
Detects raw Foundry events and converts them to domain events. By hiding Foundry specifics here, the rest of the app remains blissfully ignorant of Foundry’s peculiarities.
updateToken
hook, the Token’s document
hasn’t yet been fully updated. If you read document.rotation
at that moment, you may still get the old value. Always rely first on the changedState.rotation
from the hook payload—and only fall back to document.rotation
as a default if no change was provided.// When a token is updated in Foundry:
handleTokenUpdate() {
// Captures the update from Foundry's updateToken hook
// Creates a TokenUpdateEvent with rotation data
eventBus.emit('token:update', {
tokenId: document.id,
currentState: { rotation: 45, x: 100, y: 100, ... },
changedState: { rotation: 90 }, // New rotation
userId: 'abc123'
});
}
TokenRotationCoordinator (Application Layer)
Listens for token updates and coordinates overlay rendering when rotation changes.
Detect rotation change
Fetch overlay types that respond to token rotation
Check who’s allowed to see them
Build a render context (dimensions, angles, positions)
Fire off render requests
// Subscribed to 'token:update' events
handleTokenUpdate(event: TokenUpdateEvent) {
// Check if rotation changed
if (event.changedState.rotation !== undefined) {
// Get overlays that respond to rotation (like facing-arc)
const rotationOverlays = overlayRegistry.getTypesForEvent('tokenRotate');
// Check permissions - can this user see overlays on this token?
const viewableOverlays = await permissionCoordinator.getViewableOverlaysForToken();
// Build render context with rotation data
const context: OverlayRenderContext = {
rotation: {
currentRotation: 45,
newRotation: 90,
tokenXCoordinate: 100,
tokenYCoordinate: 100,
tokenWidth: 50,
tokenHeight: 50
}
};
OverlayRenderingService (Presentation Layer)
Manages the PixiJS graphics containers and coordinates rendering.
Retrieves the overlay definition (
FacingArcDefinition
)Instantiates—or reuses—a
PIXI.Graphics
objectPositions it at the token’s centre
Hands off drawing to the renderer
renderTokenOverlay(overlayTypeId: 'facing-arc', tokenId: string, context: OverlayRenderContext) {
// Get the overlay definition from registry
const overlayDef = overlayRegistry.get('facing-arc'); // Returns FacingArcDefinition
// Get the renderer from the definition
const renderer = overlayDef.getRenderer(); // Returns FacingArcRenderer instance
// Get or create PIXI graphics object for this token+overlay
const graphics = getOrCreateGraphicsInstance(`${tokenId}-facing-arc`);
// Position graphics at token centre
const centerX = context.rotation.tokenXCoordinate + context.rotation.tokenWidth / 2;
const centerY = context.rotation.tokenYCoordinate + context.rotation.tokenHeight / 2;
graphics.position.set(centerX, centerY);
// Delegate actual drawing to the renderer
renderer.render(graphics, context);
}
FacingArcDefinition (Infrastructure Layer)
Defines the facing arc overlay configuration.
export const FacingArcDefinition: OverlayDefinition = {
id: 'facing-arc',
renderTriggers: ['tokenRotate', 'tokenMove'],
};
Because it lives in a registry, adding new overlays later (boundary, aura, custom shapes…) is just a matter of dropping in another definition.
FacingArcRenderer (Presentation Layer)
Actually draws the facing arc using PixiJS.
render(graphics: PIXI.Graphics, context: OverlayRenderContext) {
// Clear previous drawing
graphics.clear();
// Get rotation from context
const rotation = context.rotation?.currentRotation ?? 0;
const radius = Math.max(context.rotation.tokenWidth, context.rotation.tokenHeight) / 2;
// Calculate arc angles (20 degree arc)
const arcAngle = 20 * Math.PI / 180;
const facingAngle = ((rotation - 90) * (Math.PI / 180));
const startAngle = facingAngle - arcAngle / 2;
const endAngle = facingAngle + arcAngle / 2;
// Draw the arc line
graphics.lineStyle({
width: 8,
color: 0x8A6A1C,
alpha: 0.8,
cap: PIXI.LINE_CAP.ROUND
});
graphics.arc(0, 0, radius, startAngle, endAngle, false);
}
Complete Flow Sequence
User rotates token in Foundry UI
Foundry fires
updateToken
hookFoundryHookAdapter catches hook, emits
'token:update'
eventTokenRotationCoordinator receives event, checks if rotation changed
Coordinator queries
OverlayRegistry
for overlays with'tokenRotate'
triggerCoordinator checks permissions via
OverlayPermissionCoordinator
Coordinator builds render context with rotation data
Coordinator calls
overlayRenderingService.renderTokenOverlay()
OverlayRenderingService gets
FacingArcDefinition
from registryService calls
definition.getRenderer()
to getFacingArcRenderer
Service creates/positions PIXI
Graphics
objectService calls
renderer.render(graphics, context)
FacingArcRenderer draws the arc at the new rotation angle
Facing arc appears on screen at token’s new rotation
Key Design Patterns
Event-Driven: Loose coupling via an EventBus
Clean Architecture: Clear separation between infrastructure, application, and presentation layers
Strategy Pattern: Different renderers implement a common interface for each overlay type
Registry Pattern: Dynamic registration and lookup of overlay definitions
Permission System: Fine-grained control over who can see each overlay
This modular architecture makes it trivial to extend—simply drop in a new overlay definition and renderer, and the core flow remains untouched. Happy coding!
Subscribe to my newsletter
Read articles from stonedtroll directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
