Files
feuerwerk/src/editor/graph/AnnotationView.ts
T
adminandClaude Opus 5.5 8154569405
game-maker / gate (push) Successful in 3m42s
game-maker / suite (push) Successful in 14m23s
Fix the two checks the full run 304 found red
The full CI run on 4c7861b had two red checks. Neither showed when
only the touched modules ran, because each needs a module before it.

- e2e 40c-iii (#347): a stroke on a tilemap drew the captions
  "Comment" and "Held" again. They are a note and a group that e2e 36
  leaves in a graph. AnnotationView gave its caption a new TextStyle
  on every redraw, and Pixi draws a text again for a new style. The
  graph redraws on every edit of the project. The caption now keeps
  its style while colour and width stay the same.
- e2e 49a: a plain user deleting a shared file got 404 instead of
  403. Since #344 the DELETE route asked whether the file exists
  before whose it is. It now refuses a stranger first, so another
  folder tells them nothing. The wiki's API page says so.

Tests:
- Unit annotation-view (new): 2 green, counter-check 1 red
- e2e 36,39,40,49 (melektaus-atx): both checks green, 49 then hit a
  harness timeout (CDP Network.enable) once
- e2e 49 alone (melektaus-atx): ALL PASSED - 22 checks
- Gate (melektaus-atx): CHECK EXIT 0, 261 files, 4040 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:26:35 +02:00

129 lines
5.0 KiB
TypeScript
Raw 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.
import { Graphics, Rectangle, type Text, TextStyle } from 'pixi.js';
import type { AnnotationKind, GraphAnnotation } from '../../core/graph';
import { readableTextColor, Theme, truncate, Widget, crispText } from '../../ui';
/** Height of a group's title bar – the same as a node's, so the two line up in the eye. */
export const ANNOTATION_TITLE_HEIGHT = 24;
/** Side of the square at the bottom right corner that resizes the box. */
export const RESIZE_HANDLE = 14;
const PAD = 8;
/**
* One group or comment, drawn behind the nodes in graph coordinates.
*
* A group is hit on its title bar and its resize handle only: its body has to stay transparent to
* the pointer, or a rubber band could never be drawn across the nodes it holds. A comment frames
* nothing, so it is grabbed anywhere.
*/
export class AnnotationView extends Widget {
private readonly box = new Graphics();
private readonly caption: Text;
/** Colour and width of the caption's style, see `redraw`. */
private styleKey = '';
selected = false;
constructor(
public annotation: GraphAnnotation,
readonly kind: AnnotationKind,
) {
super();
this.addChild(this.box);
this.caption = crispText({ text: '', style: this.captionStyle(0xffffff) });
this.caption.position.set(PAD, kind === 'group' ? 0 : PAD);
this.addChild(this.caption);
this.cursor = Theme.hoverCursor;
this.sync(annotation);
}
sync(annotation: GraphAnnotation): void {
this.annotation = annotation;
this.position.set(annotation.x, annotation.y);
this.resize(annotation.w, annotation.h);
// `resize` puts the full rectangle back every time, changed size or not, so this comes after it.
this.applyHitArea();
this.redraw();
}
get color(): number {
return this.annotation.color ?? Theme.colors.annotation;
}
/** Black or white, picked from the fill it sits on – the price of letting the picker run free. */
get textColor(): number {
return readableTextColor(this.color);
}
private captionStyle(color: number): TextStyle {
return new TextStyle({
fontFamily: Theme.font,
fontSize: Theme.fontSize,
fill: color,
fontWeight: this.kind === 'group' ? '600' : '400',
wordWrap: this.kind === 'note',
wordWrapWidth: Math.max(this.w - PAD * 2, 1),
lineHeight: Theme.fontSize + 4,
});
}
/** Where the box answers the pointer: the title bar of a group, the whole card of a comment. */
private applyHitArea(): void {
if (this.kind === 'note') {
this.hitArea = new Rectangle(0, 0, this.w, this.h);
return;
}
this.hitArea = {
contains: (x: number, y: number) =>
(x >= 0 && x <= this.w && y >= 0 && y <= ANNOTATION_TITLE_HEIGHT) || this.handleAt(x, y),
};
}
/** Whether this local point is on the corner that resizes – outside the hit area for a group,
* so the canvas asks for it before it asks who is under the pointer. */
handleAt(x: number, y: number): boolean {
return x >= this.w - RESIZE_HANDLE && x <= this.w && y >= this.h - RESIZE_HANDLE && y <= this.h;
}
override redraw(): void {
const c = Theme.colors;
const color = this.color;
const text = this.textColor;
this.box.clear();
if (this.kind === 'group') {
// Translucent body, solid title: the nodes inside must stay readable, the title must not.
this.box.roundRect(0, 0, this.w, this.h, Theme.radius).fill({ color, alpha: 0.12 });
this.box.roundRect(0, 0, this.w, ANNOTATION_TITLE_HEIGHT, Theme.radius).fill(color);
this.box.rect(0, ANNOTATION_TITLE_HEIGHT - Theme.radius, this.w, Theme.radius).fill(color);
} else {
this.box.roundRect(0, 0, this.w, this.h, Theme.radius).fill(color);
}
this.box
.roundRect(0.5, 0.5, this.w - 1, this.h - 1, Theme.radius)
.stroke({ width: this.selected ? 2 : 1, color: this.selected ? c.nodeSelected : color, alpha: 0.9 });
// A corner that can be grabbed says so, in the text colour of the box it belongs to.
this.box
.moveTo(this.w - RESIZE_HANDLE, this.h - 2)
.lineTo(this.w - 2, this.h - RESIZE_HANDLE)
.moveTo(this.w - RESIZE_HANDLE / 2, this.h - 2)
.lineTo(this.w - 2, this.h - RESIZE_HANDLE / 2)
.stroke({ width: 1, color: text, alpha: 0.5 });
// A new style draws the caption again even when nothing changed, and the graph redraws on every
// edit of the project, a stroke on a tilemap included (#347). So only a new colour or width sets one.
const styleKey = `${text}|${this.w}`;
if (styleKey !== this.styleKey) {
this.styleKey = styleKey;
this.caption.style = this.captionStyle(text);
}
this.caption.text =
this.kind === 'group'
? truncate(this.annotation.text, this.caption.style, Math.max(this.w - PAD * 2, 1))
: this.annotation.text;
if (this.kind === 'group') this.caption.y = (ANNOTATION_TITLE_HEIGHT - this.caption.height) / 2;
}
/** What the last redraw put on screen, for the checks. */
drawnText(): string {
return this.caption.text;
}
}