mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(ui): leave out a path label that finds no free spot
When a path label found no free spot along its edge, it was drawn where it covered the least, and that could still be the hierarchy band title (a wide title across a steep edge): both texts were unreadable. The hierarchy edge labels already leave out a label in that case. Path labels now do the same. The edge type stays in the step list, and the label comes back as soon as the camera gives it a free spot. A later label keeps clear of the earlier ones as before. Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
1 parent
011f414ce1
commit
6980f6d5d3
2 files changed
+95
-9
No files matched your search
@@ -57,6 +57,31 @@ export function screenBlockers(host: ParentNode): ScreenRect[] {
|
||||
/* Nur jedes dritte Bild: die Lage der Namen aendert sich mit der Kamera, nicht schneller. */
|
||||
const PLACE_EVERY_FRAMES = 3;
|
||||
|
||||
/**
|
||||
* Wo die Kantenlabels des Pfades stehen, in der Reihenfolge des Pfades, und
|
||||
* welches keinen Platz bekommt.
|
||||
*
|
||||
* Review zu K30: fand ein Label keinen freien Platz, stand es dort, wo es am
|
||||
* wenigsten verdeckte, und das konnte die Ueberschrift des Bandes der
|
||||
* Hierarchie sein (ein breiter Titel ueber einer steilen Kante). Wie die
|
||||
* Kantenschilder der Hierarchie (HierarchyEdgeLabels.tsx) faellt es jetzt
|
||||
* weg, statt zwei Texte unlesbar zu machen: die Art der Kante steht weiter in
|
||||
* der Schrittliste, und mit der naechsten Kamera kommt es wieder, sobald es
|
||||
* frei steht. Ein spaeteres Label weicht den frueheren aus wie den Namen.
|
||||
*/
|
||||
export function pathLabelSpots(labels: readonly { index: number; from: { x: number; y: number }; to: { x: number; y: number }; size: { width: number; height: number } }[],
|
||||
blockers: readonly ScreenRect[]): Map<number, { t: number; rect: ScreenRect }> {
|
||||
const placed: ScreenRect[] = [];
|
||||
const spots = new Map<number, { t: number; rect: ScreenRect }>();
|
||||
for (const { index, from, to, size } of labels) {
|
||||
const choice = placeAlongSegment(from, to, size, blockers, placed);
|
||||
if (choice.overlap > 0) continue;
|
||||
placed.push(choice.rect);
|
||||
spots.set(index, { t: choice.t, rect: choice.rect });
|
||||
}
|
||||
return spots;
|
||||
}
|
||||
|
||||
export function PathLayer({ nodes, path, namedRoots }: {
|
||||
nodes: readonly GraphNode[];
|
||||
path: ScenePath;
|
||||
@@ -90,6 +115,8 @@ export function PathLayer({ nodes, path, namedRoots }: {
|
||||
|
||||
const groups = useRef(new Map<number, THREE.Group>());
|
||||
const labels = useRef(new Map<number, HTMLElement>());
|
||||
// Die Groesse eines Labels nach seinem Text: ein weggelassenes Label (display: none) hat keine, die man messen koennte.
|
||||
const sizes = useRef(new Map<string, { width: number; height: number }>());
|
||||
const camera = useThree((state) => state.camera);
|
||||
const gl = useThree((state) => state.gl);
|
||||
// drei hangs Html into the element the events are connected to, not into the canvas parent.
|
||||
@@ -106,17 +133,28 @@ export function PathLayer({ nodes, path, namedRoots }: {
|
||||
scratch.point.set(node.x, node.y, node.z).project(camera);
|
||||
return { x: box.left + ((scratch.point.x + 1) / 2) * box.width, y: box.top + ((1 - scratch.point.y) / 2) * box.height };
|
||||
};
|
||||
const placed: ScreenRect[] = [];
|
||||
for (const { index, from, to } of labelled) {
|
||||
const measured = labelled.flatMap(({ step, index, from, to }) => {
|
||||
const group = groups.current.get(index), label = labels.current.get(index);
|
||||
if (!group || !label || label.offsetWidth === 0) continue;
|
||||
const a = toScreen(from), b = toScreen(to);
|
||||
const choice = placeAlongSegment(a, b, { width: label.offsetWidth, height: label.offsetHeight }, blockers, placed);
|
||||
placed.push(choice.rect);
|
||||
group.position.lerpVectors(scratch.from.set(from.x, from.y, from.z), scratch.to.set(to.x, to.y, to.z), choice.t);
|
||||
if (!group || !label) return [];
|
||||
let size = sizes.current.get(step.edge.type);
|
||||
if (!size) {
|
||||
label.style.display = '';
|
||||
size = { width: label.offsetWidth, height: label.offsetHeight };
|
||||
if (size.width === 0) return [];
|
||||
sizes.current.set(step.edge.type, size);
|
||||
}
|
||||
return [{ index, group, label, from, to, a: toScreen(from), b: toScreen(to), size }];
|
||||
});
|
||||
const spots = pathLabelSpots(measured.map(({ index, a, b, size }) => ({ index, from: a, to: b, size })), blockers);
|
||||
for (const { index, group, label, from, to, a, b } of measured) {
|
||||
const spot = spots.get(index);
|
||||
const display = spot ? '' : 'none';
|
||||
if (label.style.display !== display) label.style.display = display;
|
||||
if (!spot) continue;
|
||||
group.position.lerpVectors(scratch.from.set(from.x, from.y, from.z), scratch.to.set(to.x, to.y, to.z), spot.t);
|
||||
// The side step from placeAlongSegment, applied in screen space around the point on the edge.
|
||||
const dx = (choice.rect.left + choice.rect.right) / 2, dy = (choice.rect.top + choice.rect.bottom) / 2;
|
||||
const onEdge = { x: a.x + (b.x - a.x) * choice.t, y: a.y + (b.y - a.y) * choice.t };
|
||||
const dx = (spot.rect.left + spot.rect.right) / 2, dy = (spot.rect.top + spot.rect.bottom) / 2;
|
||||
const onEdge = { x: a.x + (b.x - a.x) * spot.t, y: a.y + (b.y - a.y) * spot.t };
|
||||
label.style.transform = `translate(${dx - onEdge.x}px, ${dy - onEdge.y}px)`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Review zu K30: "ueberlappen nie" galt fuer die Kantenlabels des Pfades nicht.
|
||||
* Fand ein Label keinen freien Platz, nahm placeAlongSegment den mit der
|
||||
* kleinsten Ueberdeckung, und PathLayer zeichnete es dort, auch mitten auf der
|
||||
* Ueberschrift des Bandes. Die Kantenschilder der Hierarchie fallen in diesem
|
||||
* Fall weg; die Labels des Pfades jetzt auch. Die Art jeder Kante steht
|
||||
* weiter in der Schrittliste.
|
||||
*/
|
||||
import { expect, it } from 'vitest';
|
||||
import { pathLabelSpots } from './PathLayer';
|
||||
import type { ScreenRect } from './path-frame';
|
||||
|
||||
const overlaps = (a: ScreenRect, b: ScreenRect) => !(a.right <= b.left || b.right <= a.left || a.bottom <= b.top || b.bottom <= a.top);
|
||||
|
||||
// Measured in the browser (CSS px): JSONBAgg <-DEFINES- general.py -DEFINES-> __init__ at two layers of the hierarchy.
|
||||
const JSONBAGG = { x: 1050, y: 368 }, GENERAL = { x: 808, y: 572 }, INIT = { x: 1160, y: 757 };
|
||||
const NAMES: ScreenRect[] = [{ left: 1018, right: 1082, top: 336, bottom: 353 }, { left: 778, right: 838, top: 540, bottom: 557 }, { left: 1135, right: 1185, top: 725, bottom: 742 }];
|
||||
const LABEL = { width: 50, height: 14 };
|
||||
const edges = [{ index: 0, from: GENERAL, to: JSONBAGG, size: LABEL }, { index: 1, from: GENERAL, to: INIT, size: LABEL }];
|
||||
|
||||
it('K30: a path label that finds no free spot on the band title is left out, not drawn on the title', () => {
|
||||
// A band title that covers the edge from general.py to __init__ from end to end (the title of the browser run, made larger).
|
||||
const title = { left: 512, right: 1590, top: 503, bottom: 835 };
|
||||
const spots = pathLabelSpots(edges, [...NAMES, title]);
|
||||
expect(spots.get(1)).toBeUndefined();
|
||||
// The other edge runs above the title and keeps its label, clear of every name and of the title.
|
||||
const kept = spots.get(0);
|
||||
expect(kept).toBeDefined();
|
||||
for (const blocker of [...NAMES, title]) expect(overlaps(kept!.rect, blocker)).toBe(false);
|
||||
});
|
||||
|
||||
it('K30: with the title at its real size both labels stand, and none touches a name, the title or the other label', () => {
|
||||
const title = { left: 834, right: 1268, top: 652, bottom: 687 };
|
||||
const spots = pathLabelSpots(edges, [...NAMES, title]);
|
||||
const rects = [spots.get(0)?.rect, spots.get(1)?.rect];
|
||||
expect(rects.every(Boolean)).toBe(true);
|
||||
for (const rect of rects) for (const blocker of [...NAMES, title]) expect(overlaps(rect!, blocker)).toBe(false);
|
||||
expect(overlaps(rects[0]!, rects[1]!)).toBe(false);
|
||||
});
|
||||
|
||||
it('K30: a later label also keeps clear of an earlier one, and gives way when only its place is left', () => {
|
||||
// Two labels on the same short edge: the second has nowhere to go but onto the first.
|
||||
const short = [{ index: 0, from: { x: 100, y: 100 }, to: { x: 130, y: 100 }, size: LABEL }, { index: 1, from: { x: 100, y: 100 }, to: { x: 130, y: 100 }, size: LABEL }];
|
||||
const walls = [{ left: 0, right: 400, top: 0, bottom: 88 }, { left: 0, right: 400, top: 112, bottom: 300 }, { left: 0, right: 56, top: 0, bottom: 300 }, { left: 174, right: 400, top: 0, bottom: 300 }];
|
||||
const spots = pathLabelSpots(short, walls);
|
||||
expect(spots.get(0)).toBeDefined();
|
||||
expect(spots.get(1)).toBeUndefined();
|
||||
});
|
||||
Reference in new issue
Block a user