@futdevpro/fsm-dynamo 1.20.120 → 1.20.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ import { DyFM_LabelPlacement_Rect, DyFM_LabelPlacement_Request, DyFM_LabelPlacement_Result } from '../_models/label-placement.interface';
2
+ /**
3
+ * BFR-PH-007 — HUD-exclusion-aware placement of world-linked labels (pickup "+6 Wood", damage numbers…), pure and
4
+ * renderer-independent (the caller measures the label and registers its HUD rectangles; any renderer draws the result).
5
+ *
6
+ * Measured (Portalhold, 1280×720): a pickup label landed under the Basics panel — the onboarding overlay positions only
7
+ * its own bubble, and the viewport transform knows no exclusions. The rules here:
8
+ * - the label stays FULLY inside the viewport and never intersects a protected rectangle or an earlier label of the
9
+ * burst (each widened by `gap`) — it never silently lands on a button;
10
+ * - the PREFERRED spot is centred above the anchor; a still-valid previous spot is kept first (no jitter on
11
+ * resize / zoom / reduced motion);
12
+ * - otherwise the valid candidate closest to the preferred spot wins, ties broken by a fixed order (deterministic);
13
+ * - nowhere valid ⇒ an explicit `no-space` result (the caller keeps the information in the HUD / log), never an
14
+ * overlap; invalid geometry ⇒ `invalid-geometry` with the reason.
15
+ */
16
+ export declare class DyFM_LabelPlacement_Util {
17
+ /** The default clearance between the label and the anchor / a blocked rectangle (pixels). */
18
+ static readonly defaultGap: number;
19
+ /** Places one label (see the class description for the rules). */
20
+ static place(request: DyFM_LabelPlacement_Request): DyFM_LabelPlacement_Result;
21
+ /** Whether two rectangles overlap when each is widened by `gap` (touching at exactly `gap` is allowed). */
22
+ static intersects(a: DyFM_LabelPlacement_Rect, b: DyFM_LabelPlacement_Rect, gap?: number): boolean;
23
+ /** Whether a rectangle is fully on screen and clear of every protected area and earlier label (widened by `gap`). */
24
+ private static isClear;
25
+ /** The spot the label wants: centred horizontally above the anchor, `gap` above it. */
26
+ private static preferredRect;
27
+ /**
28
+ * The candidates in the fixed try-order: previous · above · below · right · left (each kept inside the viewport
29
+ * along its free axis) · then clear of each protected area and each earlier label (above / below / left / right).
30
+ */
31
+ private static candidates;
32
+ /** The ids of the protected areas a rectangle would cover (attribution — why a label moved). */
33
+ private static blockingIds;
34
+ /** A value kept between `min` and `max`. */
35
+ private static clamp;
36
+ /** Whether every value is a finite number. */
37
+ private static allFinite;
38
+ /** Whether a rectangle is present, finite, and has a non-negative size. */
39
+ private static isFiniteRect;
40
+ /** Whether the rectangle lies fully inside the viewport. */
41
+ private static inside;
42
+ /** The distance between two rectangle centres. */
43
+ private static centreDistance;
44
+ /** The first invalid input, as a readable reason; `null` when the geometry is usable. */
45
+ private static findInvalidGeometry;
46
+ }
47
+ //# sourceMappingURL=label-placement.util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label-placement.util.d.ts","sourceRoot":"","sources":["../../../../src/_modules/game/_collections/label-placement.util.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,wBAAwB,EACxB,2BAA2B,EAC3B,0BAA0B,EAE3B,MAAM,sCAAsC,CAAC;AAQ9C;;;;;;;;;;;;;GAaG;AACH,qBAAa,wBAAwB;IACnC,6FAA6F;IAC7F,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAK;IAEvC,kEAAkE;IAClE,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,2BAA2B,GAAG,0BAA0B;IAqF9E,2GAA2G;IAC3G,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,wBAAwB,EAAE,CAAC,EAAE,wBAAwB,EAAE,GAAG,GAAE,MAAU,GAAG,OAAO;IAKrG,qHAAqH;IACrH,OAAO,CAAC,MAAM,CAAC,OAAO;IActB,uFAAuF;IACvF,OAAO,CAAC,MAAM,CAAC,aAAa;IAS5B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,UAAU;IAwCzB,gGAAgG;IAChG,OAAO,CAAC,MAAM,CAAC,WAAW;IAW1B,4CAA4C;IAC5C,OAAO,CAAC,MAAM,CAAC,KAAK;IAIpB,8CAA8C;IAC9C,OAAO,CAAC,MAAM,CAAC,SAAS;IAIxB,2EAA2E;IAC3E,OAAO,CAAC,MAAM,CAAC,YAAY;IAK3B,4DAA4D;IAC5D,OAAO,CAAC,MAAM,CAAC,MAAM;IAKrB,kDAAkD;IAClD,OAAO,CAAC,MAAM,CAAC,cAAc;IAI7B,yFAAyF;IACzF,OAAO,CAAC,MAAM,CAAC,mBAAmB;CAsCnC"}
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DyFM_LabelPlacement_Util = void 0;
4
+ /**
5
+ * BFR-PH-007 — HUD-exclusion-aware placement of world-linked labels (pickup "+6 Wood", damage numbers…), pure and
6
+ * renderer-independent (the caller measures the label and registers its HUD rectangles; any renderer draws the result).
7
+ *
8
+ * Measured (Portalhold, 1280×720): a pickup label landed under the Basics panel — the onboarding overlay positions only
9
+ * its own bubble, and the viewport transform knows no exclusions. The rules here:
10
+ * - the label stays FULLY inside the viewport and never intersects a protected rectangle or an earlier label of the
11
+ * burst (each widened by `gap`) — it never silently lands on a button;
12
+ * - the PREFERRED spot is centred above the anchor; a still-valid previous spot is kept first (no jitter on
13
+ * resize / zoom / reduced motion);
14
+ * - otherwise the valid candidate closest to the preferred spot wins, ties broken by a fixed order (deterministic);
15
+ * - nowhere valid ⇒ an explicit `no-space` result (the caller keeps the information in the HUD / log), never an
16
+ * overlap; invalid geometry ⇒ `invalid-geometry` with the reason.
17
+ */
18
+ class DyFM_LabelPlacement_Util {
19
+ /** The default clearance between the label and the anchor / a blocked rectangle (pixels). */
20
+ static defaultGap = 4;
21
+ /** Places one label (see the class description for the rules). */
22
+ static place(request) {
23
+ const invalid = DyFM_LabelPlacement_Util.findInvalidGeometry(request);
24
+ if (invalid) {
25
+ return { placed: false, reason: 'invalid-geometry', detail: invalid, blockedBy: [] };
26
+ }
27
+ const gap = request.gap ?? DyFM_LabelPlacement_Util.defaultGap;
28
+ const exclusions = request.exclusions ?? [];
29
+ const occupied = request.occupied ?? [];
30
+ const preferred = DyFM_LabelPlacement_Util.preferredRect(request, gap);
31
+ const blockedBy = DyFM_LabelPlacement_Util.blockingIds(preferred, exclusions, gap);
32
+ if (request.label.width > request.viewport.width || request.label.height > request.viewport.height) {
33
+ return {
34
+ placed: false,
35
+ reason: 'no-space',
36
+ detail: `the label (${request.label.width}×${request.label.height}) is larger than the viewport `
37
+ + `(${request.viewport.width}×${request.viewport.height})`,
38
+ blockedBy: blockedBy,
39
+ };
40
+ }
41
+ /** A candidate is usable when it is on screen and clear of every blocked rectangle. */
42
+ const isValid = (rect) => DyFM_LabelPlacement_Util.isClear({
43
+ rect: rect, viewport: request.viewport, exclusions: exclusions, occupied: occupied, gap: gap,
44
+ });
45
+ const candidates = DyFM_LabelPlacement_Util.candidates(request, gap);
46
+ const previous = candidates[0]?.side === 'previous' && isValid(candidates[0].rect) ? candidates[0] : undefined;
47
+ let best = previous;
48
+ if (!best) {
49
+ let bestDistance = Number.POSITIVE_INFINITY;
50
+ for (const candidate of candidates) {
51
+ if (candidate.side === 'previous' || !isValid(candidate.rect)) {
52
+ continue;
53
+ }
54
+ const distance = DyFM_LabelPlacement_Util.centreDistance(candidate.rect, preferred);
55
+ // strictly smaller only ⇒ on a tie the EARLIER candidate stays (fixed order = deterministic)
56
+ if (distance < bestDistance) {
57
+ best = candidate;
58
+ bestDistance = distance;
59
+ }
60
+ }
61
+ }
62
+ if (!best) {
63
+ const everyBlocker = [...blockedBy];
64
+ for (const candidate of candidates) {
65
+ for (const id of DyFM_LabelPlacement_Util.blockingIds(candidate.rect, exclusions, gap)) {
66
+ if (!everyBlocker.includes(id)) {
67
+ everyBlocker.push(id);
68
+ }
69
+ }
70
+ }
71
+ return {
72
+ placed: false,
73
+ reason: 'no-space',
74
+ detail: `no position inside the viewport is clear of the ${exclusions.length} protected area(s) `
75
+ + `and ${occupied.length} earlier label(s)`,
76
+ blockedBy: everyBlocker,
77
+ };
78
+ }
79
+ const displacement = {
80
+ x: best.rect.x - preferred.x,
81
+ y: best.rect.y - preferred.y,
82
+ };
83
+ return {
84
+ placed: true,
85
+ rect: { ...best.rect },
86
+ side: best.side,
87
+ displacement: displacement,
88
+ displaced: displacement.x !== 0 || displacement.y !== 0,
89
+ blockedBy: blockedBy,
90
+ };
91
+ }
92
+ /** Whether two rectangles overlap when each is widened by `gap` (touching at exactly `gap` is allowed). */
93
+ static intersects(a, b, gap = 0) {
94
+ return a.x < b.x + b.width + gap && a.x + a.width + gap > b.x
95
+ && a.y < b.y + b.height + gap && a.y + a.height + gap > b.y;
96
+ }
97
+ /** Whether a rectangle is fully on screen and clear of every protected area and earlier label (widened by `gap`). */
98
+ static isClear(set) {
99
+ return DyFM_LabelPlacement_Util.inside(set.rect, set.viewport)
100
+ && !set.exclusions.some((each) => DyFM_LabelPlacement_Util.intersects(set.rect, each.rect, set.gap))
101
+ && !set.occupied.some((each) => DyFM_LabelPlacement_Util.intersects(set.rect, each, set.gap));
102
+ }
103
+ /** The spot the label wants: centred horizontally above the anchor, `gap` above it. */
104
+ static preferredRect(request, gap) {
105
+ return {
106
+ x: request.anchor.x - request.label.width / 2,
107
+ y: request.anchor.y - gap - request.label.height,
108
+ width: request.label.width,
109
+ height: request.label.height,
110
+ };
111
+ }
112
+ /**
113
+ * The candidates in the fixed try-order: previous · above · below · right · left (each kept inside the viewport
114
+ * along its free axis) · then clear of each protected area and each earlier label (above / below / left / right).
115
+ */
116
+ static candidates(request, gap) {
117
+ const width = request.label.width;
118
+ const height = request.label.height;
119
+ const anchor = request.anchor;
120
+ const vp = request.viewport;
121
+ /** A label-sized rectangle at a top-left corner. */
122
+ const rect = (x, y) => ({ x: x, y: y, width: width, height: height });
123
+ // centred on the anchor along the free axis, but kept on screen
124
+ const centredX = DyFM_LabelPlacement_Util.clamp(anchor.x - width / 2, vp.x, vp.x + vp.width - width);
125
+ const centredY = DyFM_LabelPlacement_Util.clamp(anchor.y - height / 2, vp.y, vp.y + vp.height - height);
126
+ const list = [];
127
+ if (request.previous) {
128
+ list.push({ rect: rect(request.previous.x, request.previous.y), side: 'previous' });
129
+ }
130
+ list.push({ rect: rect(centredX, anchor.y - gap - height), side: 'above' }, { rect: rect(centredX, anchor.y + gap), side: 'below' }, { rect: rect(anchor.x + gap, centredY), side: 'right' }, { rect: rect(anchor.x - gap - width, centredY), side: 'left' });
131
+ const blockers = [
132
+ ...(request.exclusions ?? []).map((each) => each.rect),
133
+ ...(request.occupied ?? []),
134
+ ];
135
+ for (const blocker of blockers) {
136
+ list.push({ rect: rect(centredX, blocker.y - gap - height), side: 'clear-of-exclusion' }, { rect: rect(centredX, blocker.y + blocker.height + gap), side: 'clear-of-exclusion' }, { rect: rect(blocker.x - gap - width, centredY), side: 'clear-of-exclusion' }, { rect: rect(blocker.x + blocker.width + gap, centredY), side: 'clear-of-exclusion' });
137
+ }
138
+ return list;
139
+ }
140
+ /** The ids of the protected areas a rectangle would cover (attribution — why a label moved). */
141
+ static blockingIds(rect, exclusions, gap) {
142
+ return exclusions
143
+ .filter((each) => DyFM_LabelPlacement_Util.intersects(rect, each.rect, gap))
144
+ .map((each) => each.id);
145
+ }
146
+ /** A value kept between `min` and `max`. */
147
+ static clamp(value, min, max) {
148
+ return Math.min(Math.max(value, min), max);
149
+ }
150
+ /** Whether every value is a finite number. */
151
+ static allFinite(...values) {
152
+ return values.every((value) => Number.isFinite(value));
153
+ }
154
+ /** Whether a rectangle is present, finite, and has a non-negative size. */
155
+ static isFiniteRect(rect) {
156
+ return !!rect && DyFM_LabelPlacement_Util.allFinite(rect.x, rect.y, rect.width, rect.height)
157
+ && rect.width >= 0 && rect.height >= 0;
158
+ }
159
+ /** Whether the rectangle lies fully inside the viewport. */
160
+ static inside(rect, vp) {
161
+ return rect.x >= vp.x && rect.y >= vp.y && rect.x + rect.width <= vp.x + vp.width
162
+ && rect.y + rect.height <= vp.y + vp.height;
163
+ }
164
+ /** The distance between two rectangle centres. */
165
+ static centreDistance(a, b) {
166
+ return Math.hypot((a.x + a.width / 2) - (b.x + b.width / 2), (a.y + a.height / 2) - (b.y + b.height / 2));
167
+ }
168
+ /** The first invalid input, as a readable reason; `null` when the geometry is usable. */
169
+ static findInvalidGeometry(request) {
170
+ const finite = DyFM_LabelPlacement_Util.allFinite;
171
+ const finiteRect = DyFM_LabelPlacement_Util.isFiniteRect;
172
+ if (!request?.anchor || !finite(request.anchor.x, request.anchor.y)) {
173
+ return 'the anchor must be a finite point';
174
+ }
175
+ if (!request.label || !finite(request.label.width, request.label.height)
176
+ || !(request.label.width > 0) || !(request.label.height > 0)) {
177
+ return 'the label needs a measured, positive size';
178
+ }
179
+ if (!finiteRect(request.viewport) || !(request.viewport.width > 0) || !(request.viewport.height > 0)) {
180
+ return 'the viewport needs a finite, positive size';
181
+ }
182
+ if (request.gap !== undefined && (!finite(request.gap) || request.gap < 0)) {
183
+ return 'the gap must be a finite, non-negative number';
184
+ }
185
+ const badExclusion = (request.exclusions ?? [])
186
+ .find((each) => !each?.id || !finiteRect(each.rect));
187
+ if (badExclusion) {
188
+ return `the protected area "${badExclusion?.id ?? '?'}" needs an id and a finite, non-negative rectangle`;
189
+ }
190
+ if ((request.occupied ?? []).some((each) => !finiteRect(each))) {
191
+ return 'every occupied rectangle must be finite and non-negative';
192
+ }
193
+ if (request.previous && !finite(request.previous.x, request.previous.y)) {
194
+ return 'the previous position must be a finite point';
195
+ }
196
+ return null;
197
+ }
198
+ }
199
+ exports.DyFM_LabelPlacement_Util = DyFM_LabelPlacement_Util;
200
+ //# sourceMappingURL=label-placement.util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label-placement.util.js","sourceRoot":"","sources":["../../../../src/_modules/game/_collections/label-placement.util.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import { DyFM_Viewport_Point } from './viewport.interface';
2
+ /** BFR-PH-007 — an axis-aligned screen rectangle (pixels; `x` / `y` = the top-left corner). */
3
+ export interface DyFM_LabelPlacement_Rect {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ /** BFR-PH-007 — a protected screen area a label must never cover (a HUD panel, a button…), with a stable id. */
10
+ export interface DyFM_LabelPlacement_Exclusion {
11
+ id: string;
12
+ rect: DyFM_LabelPlacement_Rect;
13
+ }
14
+ /** BFR-PH-007 — the candidate side a placed label ended up on, relative to its anchor. */
15
+ export type DyFM_LabelPlacement_Side = 'previous' | 'above' | 'below' | 'right' | 'left' | 'clear-of-exclusion';
16
+ /** BFR-PH-007 — one placement request: a world-linked label (e.g. "+6 Wood") at its screen anchor. */
17
+ export interface DyFM_LabelPlacement_Request {
18
+ /** The screen point the label belongs to (the world position projected to the screen). */
19
+ anchor: DyFM_Viewport_Point;
20
+ /** The MEASURED label size (pixels). */
21
+ label: {
22
+ width: number;
23
+ height: number;
24
+ };
25
+ /** The visible area; the label must stay fully inside it. */
26
+ viewport: DyFM_LabelPlacement_Rect;
27
+ /** Protected areas (HUD) — never covered. */
28
+ exclusions?: DyFM_LabelPlacement_Exclusion[];
29
+ /** Rectangles already taken in this burst (earlier labels) — a new label stacks clear of them. */
30
+ occupied?: DyFM_LabelPlacement_Rect[];
31
+ /** The minimum clearance between the label and the anchor / any blocked rectangle (pixels). Default: `4`. */
32
+ gap?: number;
33
+ /** The label's previous top-left (the frame before) — kept when still valid, so a label never jitters. */
34
+ previous?: DyFM_Viewport_Point | null;
35
+ }
36
+ /** BFR-PH-007 — the label fits: where it goes and what moved it. */
37
+ export interface DyFM_LabelPlacement_Placed {
38
+ placed: true;
39
+ /** The label rectangle to draw. */
40
+ rect: DyFM_LabelPlacement_Rect;
41
+ side: DyFM_LabelPlacement_Side;
42
+ /** How far the label centre moved from the preferred spot (centred above the anchor). */
43
+ displacement: DyFM_Viewport_Point;
44
+ /** `true` when the preferred spot was blocked or off-screen. */
45
+ displaced: boolean;
46
+ /** The exclusion ids that blocked the preferred spot (attribution — why the label moved). */
47
+ blockedBy: string[];
48
+ }
49
+ /** BFR-PH-007 — the label does NOT fit anywhere valid: the caller keeps the information elsewhere (HUD / log). */
50
+ export interface DyFM_LabelPlacement_NotPlaced {
51
+ placed: false;
52
+ reason: 'no-space' | 'invalid-geometry';
53
+ /** A readable explanation (which input is invalid, or that every candidate was blocked). */
54
+ detail: string;
55
+ /** The exclusion ids that blocked candidates (empty for invalid geometry). */
56
+ blockedBy: string[];
57
+ }
58
+ /** BFR-PH-007 — the placement answer: placed, or an explicit "nowhere to put it" (never a silent overlap). */
59
+ export type DyFM_LabelPlacement_Result = DyFM_LabelPlacement_Placed | DyFM_LabelPlacement_NotPlaced;
60
+ //# sourceMappingURL=label-placement.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label-placement.interface.d.ts","sourceRoot":"","sources":["../../../../src/_modules/game/_models/label-placement.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D,+FAA+F;AAC/F,MAAM,WAAW,wBAAwB;IACvC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,gHAAgH;AAChH,MAAM,WAAW,6BAA6B;IAC5C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,wBAAwB,CAAC;CAChC;AAED,0FAA0F;AAC1F,MAAM,MAAM,wBAAwB,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,oBAAoB,CAAC;AAEhH,sGAAsG;AACtG,MAAM,WAAW,2BAA2B;IAC1C,0FAA0F;IAC1F,MAAM,EAAE,mBAAmB,CAAC;IAC5B,wCAAwC;IACxC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,6DAA6D;IAC7D,QAAQ,EAAE,wBAAwB,CAAC;IACnC,6CAA6C;IAC7C,UAAU,CAAC,EAAE,6BAA6B,EAAE,CAAC;IAC7C,kGAAkG;IAClG,QAAQ,CAAC,EAAE,wBAAwB,EAAE,CAAC;IACtC,6GAA6G;IAC7G,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0GAA0G;IAC1G,QAAQ,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACvC;AAED,oEAAoE;AACpE,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,IAAI,CAAC;IACb,mCAAmC;IACnC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,IAAI,EAAE,wBAAwB,CAAC;IAC/B,yFAAyF;IACzF,YAAY,EAAE,mBAAmB,CAAC;IAClC,gEAAgE;IAChE,SAAS,EAAE,OAAO,CAAC;IACnB,6FAA6F;IAC7F,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,kHAAkH;AAClH,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,KAAK,CAAC;IACd,MAAM,EAAE,UAAU,GAAG,kBAAkB,CAAC;IACxC,4FAA4F;IAC5F,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,8GAA8G;AAC9G,MAAM,MAAM,0BAA0B,GAAG,0BAA0B,GAAG,6BAA6B,CAAC"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=label-placement.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label-placement.interface.js","sourceRoot":"","sources":["../../../../src/_modules/game/_models/label-placement.interface.ts"],"names":[],"mappings":""}
@@ -18,6 +18,7 @@ export * from './_models/work-order-scheduler.control-model';
18
18
  export * from './_models/build-reservation.interface';
19
19
  export * from './_models/viewport.interface';
20
20
  export * from './_models/viewport.control-model';
21
+ export * from './_models/label-placement.interface';
21
22
  export * from './_models/input-action.interface';
22
23
  export * from './_models/input-action-registry.control-model';
23
24
  export * from './_models/guided-onboarding.interface';
@@ -28,4 +29,5 @@ export * from './_collections/audio-mixer.util';
28
29
  export * from './_collections/audio-scale.util';
29
30
  export * from './_collections/game-save.util';
30
31
  export * from './_collections/build-reservation.util';
32
+ export * from './_collections/label-placement.util';
31
33
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/_modules/game/index.ts"],"names":[],"mappings":"AACA,cAAc,6BAA6B,CAAC;AAG5C,cAAc,wCAAwC,CAAC;AACvD,cAAc,0CAA0C,CAAC;AACzD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,uCAAuC,CAAC;AACtD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,uCAAuC,CAAC;AACtD,cAAc,sCAAsC,CAAC;AACrD,cAAc,0CAA0C,CAAC;AACzD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0CAA0C,CAAC;AACzD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,uCAAuC,CAAC;AACtD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,kCAAkC,CAAC;AACjD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,uCAAuC,CAAC;AACtD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,sCAAsC,CAAC;AACrD,cAAc,0CAA0C,CAAC;AAGzD,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/_modules/game/index.ts"],"names":[],"mappings":"AACA,cAAc,6BAA6B,CAAC;AAG5C,cAAc,wCAAwC,CAAC;AACvD,cAAc,0CAA0C,CAAC;AACzD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,uCAAuC,CAAC;AACtD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,uCAAuC,CAAC;AACtD,cAAc,sCAAsC,CAAC;AACrD,cAAc,0CAA0C,CAAC;AACzD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0CAA0C,CAAC;AACzD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,uCAAuC,CAAC;AACtD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,qCAAqC,CAAC;AACpD,cAAc,kCAAkC,CAAC;AACjD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,uCAAuC,CAAC;AACtD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,sCAAsC,CAAC;AACrD,cAAc,0CAA0C,CAAC;AAGzD,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC"}
@@ -23,6 +23,7 @@ tslib_1.__exportStar(require("./_models/work-order-scheduler.control-model"), ex
23
23
  tslib_1.__exportStar(require("./_models/build-reservation.interface"), exports);
24
24
  tslib_1.__exportStar(require("./_models/viewport.interface"), exports);
25
25
  tslib_1.__exportStar(require("./_models/viewport.control-model"), exports);
26
+ tslib_1.__exportStar(require("./_models/label-placement.interface"), exports);
26
27
  tslib_1.__exportStar(require("./_models/input-action.interface"), exports);
27
28
  tslib_1.__exportStar(require("./_models/input-action-registry.control-model"), exports);
28
29
  tslib_1.__exportStar(require("./_models/guided-onboarding.interface"), exports);
@@ -34,4 +35,5 @@ tslib_1.__exportStar(require("./_collections/audio-mixer.util"), exports);
34
35
  tslib_1.__exportStar(require("./_collections/audio-scale.util"), exports);
35
36
  tslib_1.__exportStar(require("./_collections/game-save.util"), exports);
36
37
  tslib_1.__exportStar(require("./_collections/build-reservation.util"), exports);
38
+ tslib_1.__exportStar(require("./_collections/label-placement.util"), exports);
37
39
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/_modules/game/index.ts"],"names":[],"mappings":";;;AAAA,QAAQ;AACR,sEAA4C;AAE5C,SAAS;AACT,iFAAuD;AACvD,mFAAyD;AACzD,gFAAsD;AACtD,8EAAoD;AACpD,0EAAgD;AAChD,kFAAwD;AACxD,gFAAsD;AACtD,qFAA2D;AAC3D,yFAA+D;AAC/D,4EAAkD;AAClD,gFAAsD;AACtD,+EAAqD;AACrD,mFAAyD;AACzD,wEAA8C;AAC9C,mFAAyD;AACzD,uFAA6D;AAC7D,gFAAsD;AACtD,uEAA6C;AAC7C,2EAAiD;AACjD,2EAAiD;AACjD,wFAA8D;AAC9D,gFAAsD;AACtD,oFAA0D;AAC1D,+EAAqD;AACrD,mFAAyD;AAEzD,cAAc;AACd,0EAAgD;AAChD,0EAAgD;AAChD,wEAA8C;AAC9C,gFAAsD"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/_modules/game/index.ts"],"names":[],"mappings":";;;AAAA,QAAQ;AACR,sEAA4C;AAE5C,SAAS;AACT,iFAAuD;AACvD,mFAAyD;AACzD,gFAAsD;AACtD,8EAAoD;AACpD,0EAAgD;AAChD,kFAAwD;AACxD,gFAAsD;AACtD,qFAA2D;AAC3D,yFAA+D;AAC/D,4EAAkD;AAClD,gFAAsD;AACtD,+EAAqD;AACrD,mFAAyD;AACzD,wEAA8C;AAC9C,mFAAyD;AACzD,uFAA6D;AAC7D,gFAAsD;AACtD,uEAA6C;AAC7C,2EAAiD;AACjD,8EAAoD;AACpD,2EAAiD;AACjD,wFAA8D;AAC9D,gFAAsD;AACtD,oFAA0D;AAC1D,+EAAqD;AACrD,mFAAyD;AAEzD,cAAc;AACd,0EAAgD;AAChD,0EAAgD;AAChD,wEAA8C;AAC9C,gFAAsD;AACtD,8EAAoD"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * BFR-PH-007 — HUD-exclusion-aware placement of world-linked labels (pickup "+6 Wood", damage numbers…), pure and
3
+ * renderer-independent (the caller measures the label and registers its HUD rectangles; any renderer draws the result).
4
+ *
5
+ * Measured (Portalhold, 1280×720): a pickup label landed under the Basics panel — the onboarding overlay positions only
6
+ * its own bubble, and the viewport transform knows no exclusions. The rules here:
7
+ * - the label stays FULLY inside the viewport and never intersects a protected rectangle or an earlier label of the
8
+ * burst (each widened by `gap`) — it never silently lands on a button;
9
+ * - the PREFERRED spot is centred above the anchor; a still-valid previous spot is kept first (no jitter on
10
+ * resize / zoom / reduced motion);
11
+ * - otherwise the valid candidate closest to the preferred spot wins, ties broken by a fixed order (deterministic);
12
+ * - nowhere valid ⇒ an explicit `no-space` result (the caller keeps the information in the HUD / log), never an
13
+ * overlap; invalid geometry ⇒ `invalid-geometry` with the reason.
14
+ */
15
+ export class DyFM_LabelPlacement_Util {
16
+ /** The default clearance between the label and the anchor / a blocked rectangle (pixels). */
17
+ static defaultGap = 4;
18
+ /** Places one label (see the class description for the rules). */
19
+ static place(request) {
20
+ const invalid = DyFM_LabelPlacement_Util.findInvalidGeometry(request);
21
+ if (invalid) {
22
+ return { placed: false, reason: 'invalid-geometry', detail: invalid, blockedBy: [] };
23
+ }
24
+ const gap = request.gap ?? DyFM_LabelPlacement_Util.defaultGap;
25
+ const exclusions = request.exclusions ?? [];
26
+ const occupied = request.occupied ?? [];
27
+ const preferred = DyFM_LabelPlacement_Util.preferredRect(request, gap);
28
+ const blockedBy = DyFM_LabelPlacement_Util.blockingIds(preferred, exclusions, gap);
29
+ if (request.label.width > request.viewport.width || request.label.height > request.viewport.height) {
30
+ return {
31
+ placed: false,
32
+ reason: 'no-space',
33
+ detail: `the label (${request.label.width}×${request.label.height}) is larger than the viewport `
34
+ + `(${request.viewport.width}×${request.viewport.height})`,
35
+ blockedBy: blockedBy,
36
+ };
37
+ }
38
+ /** A candidate is usable when it is on screen and clear of every blocked rectangle. */
39
+ const isValid = (rect) => DyFM_LabelPlacement_Util.isClear({
40
+ rect: rect, viewport: request.viewport, exclusions: exclusions, occupied: occupied, gap: gap,
41
+ });
42
+ const candidates = DyFM_LabelPlacement_Util.candidates(request, gap);
43
+ const previous = candidates[0]?.side === 'previous' && isValid(candidates[0].rect) ? candidates[0] : undefined;
44
+ let best = previous;
45
+ if (!best) {
46
+ let bestDistance = Number.POSITIVE_INFINITY;
47
+ for (const candidate of candidates) {
48
+ if (candidate.side === 'previous' || !isValid(candidate.rect)) {
49
+ continue;
50
+ }
51
+ const distance = DyFM_LabelPlacement_Util.centreDistance(candidate.rect, preferred);
52
+ // strictly smaller only ⇒ on a tie the EARLIER candidate stays (fixed order = deterministic)
53
+ if (distance < bestDistance) {
54
+ best = candidate;
55
+ bestDistance = distance;
56
+ }
57
+ }
58
+ }
59
+ if (!best) {
60
+ const everyBlocker = [...blockedBy];
61
+ for (const candidate of candidates) {
62
+ for (const id of DyFM_LabelPlacement_Util.blockingIds(candidate.rect, exclusions, gap)) {
63
+ if (!everyBlocker.includes(id)) {
64
+ everyBlocker.push(id);
65
+ }
66
+ }
67
+ }
68
+ return {
69
+ placed: false,
70
+ reason: 'no-space',
71
+ detail: `no position inside the viewport is clear of the ${exclusions.length} protected area(s) `
72
+ + `and ${occupied.length} earlier label(s)`,
73
+ blockedBy: everyBlocker,
74
+ };
75
+ }
76
+ const displacement = {
77
+ x: best.rect.x - preferred.x,
78
+ y: best.rect.y - preferred.y,
79
+ };
80
+ return {
81
+ placed: true,
82
+ rect: { ...best.rect },
83
+ side: best.side,
84
+ displacement: displacement,
85
+ displaced: displacement.x !== 0 || displacement.y !== 0,
86
+ blockedBy: blockedBy,
87
+ };
88
+ }
89
+ /** Whether two rectangles overlap when each is widened by `gap` (touching at exactly `gap` is allowed). */
90
+ static intersects(a, b, gap = 0) {
91
+ return a.x < b.x + b.width + gap && a.x + a.width + gap > b.x
92
+ && a.y < b.y + b.height + gap && a.y + a.height + gap > b.y;
93
+ }
94
+ /** Whether a rectangle is fully on screen and clear of every protected area and earlier label (widened by `gap`). */
95
+ static isClear(set) {
96
+ return DyFM_LabelPlacement_Util.inside(set.rect, set.viewport)
97
+ && !set.exclusions.some((each) => DyFM_LabelPlacement_Util.intersects(set.rect, each.rect, set.gap))
98
+ && !set.occupied.some((each) => DyFM_LabelPlacement_Util.intersects(set.rect, each, set.gap));
99
+ }
100
+ /** The spot the label wants: centred horizontally above the anchor, `gap` above it. */
101
+ static preferredRect(request, gap) {
102
+ return {
103
+ x: request.anchor.x - request.label.width / 2,
104
+ y: request.anchor.y - gap - request.label.height,
105
+ width: request.label.width,
106
+ height: request.label.height,
107
+ };
108
+ }
109
+ /**
110
+ * The candidates in the fixed try-order: previous · above · below · right · left (each kept inside the viewport
111
+ * along its free axis) · then clear of each protected area and each earlier label (above / below / left / right).
112
+ */
113
+ static candidates(request, gap) {
114
+ const width = request.label.width;
115
+ const height = request.label.height;
116
+ const anchor = request.anchor;
117
+ const vp = request.viewport;
118
+ /** A label-sized rectangle at a top-left corner. */
119
+ const rect = (x, y) => ({ x: x, y: y, width: width, height: height });
120
+ // centred on the anchor along the free axis, but kept on screen
121
+ const centredX = DyFM_LabelPlacement_Util.clamp(anchor.x - width / 2, vp.x, vp.x + vp.width - width);
122
+ const centredY = DyFM_LabelPlacement_Util.clamp(anchor.y - height / 2, vp.y, vp.y + vp.height - height);
123
+ const list = [];
124
+ if (request.previous) {
125
+ list.push({ rect: rect(request.previous.x, request.previous.y), side: 'previous' });
126
+ }
127
+ list.push({ rect: rect(centredX, anchor.y - gap - height), side: 'above' }, { rect: rect(centredX, anchor.y + gap), side: 'below' }, { rect: rect(anchor.x + gap, centredY), side: 'right' }, { rect: rect(anchor.x - gap - width, centredY), side: 'left' });
128
+ const blockers = [
129
+ ...(request.exclusions ?? []).map((each) => each.rect),
130
+ ...(request.occupied ?? []),
131
+ ];
132
+ for (const blocker of blockers) {
133
+ list.push({ rect: rect(centredX, blocker.y - gap - height), side: 'clear-of-exclusion' }, { rect: rect(centredX, blocker.y + blocker.height + gap), side: 'clear-of-exclusion' }, { rect: rect(blocker.x - gap - width, centredY), side: 'clear-of-exclusion' }, { rect: rect(blocker.x + blocker.width + gap, centredY), side: 'clear-of-exclusion' });
134
+ }
135
+ return list;
136
+ }
137
+ /** The ids of the protected areas a rectangle would cover (attribution — why a label moved). */
138
+ static blockingIds(rect, exclusions, gap) {
139
+ return exclusions
140
+ .filter((each) => DyFM_LabelPlacement_Util.intersects(rect, each.rect, gap))
141
+ .map((each) => each.id);
142
+ }
143
+ /** A value kept between `min` and `max`. */
144
+ static clamp(value, min, max) {
145
+ return Math.min(Math.max(value, min), max);
146
+ }
147
+ /** Whether every value is a finite number. */
148
+ static allFinite(...values) {
149
+ return values.every((value) => Number.isFinite(value));
150
+ }
151
+ /** Whether a rectangle is present, finite, and has a non-negative size. */
152
+ static isFiniteRect(rect) {
153
+ return !!rect && DyFM_LabelPlacement_Util.allFinite(rect.x, rect.y, rect.width, rect.height)
154
+ && rect.width >= 0 && rect.height >= 0;
155
+ }
156
+ /** Whether the rectangle lies fully inside the viewport. */
157
+ static inside(rect, vp) {
158
+ return rect.x >= vp.x && rect.y >= vp.y && rect.x + rect.width <= vp.x + vp.width
159
+ && rect.y + rect.height <= vp.y + vp.height;
160
+ }
161
+ /** The distance between two rectangle centres. */
162
+ static centreDistance(a, b) {
163
+ return Math.hypot((a.x + a.width / 2) - (b.x + b.width / 2), (a.y + a.height / 2) - (b.y + b.height / 2));
164
+ }
165
+ /** The first invalid input, as a readable reason; `null` when the geometry is usable. */
166
+ static findInvalidGeometry(request) {
167
+ const finite = DyFM_LabelPlacement_Util.allFinite;
168
+ const finiteRect = DyFM_LabelPlacement_Util.isFiniteRect;
169
+ if (!request?.anchor || !finite(request.anchor.x, request.anchor.y)) {
170
+ return 'the anchor must be a finite point';
171
+ }
172
+ if (!request.label || !finite(request.label.width, request.label.height)
173
+ || !(request.label.width > 0) || !(request.label.height > 0)) {
174
+ return 'the label needs a measured, positive size';
175
+ }
176
+ if (!finiteRect(request.viewport) || !(request.viewport.width > 0) || !(request.viewport.height > 0)) {
177
+ return 'the viewport needs a finite, positive size';
178
+ }
179
+ if (request.gap !== undefined && (!finite(request.gap) || request.gap < 0)) {
180
+ return 'the gap must be a finite, non-negative number';
181
+ }
182
+ const badExclusion = (request.exclusions ?? [])
183
+ .find((each) => !each?.id || !finiteRect(each.rect));
184
+ if (badExclusion) {
185
+ return `the protected area "${badExclusion?.id ?? '?'}" needs an id and a finite, non-negative rectangle`;
186
+ }
187
+ if ((request.occupied ?? []).some((each) => !finiteRect(each))) {
188
+ return 'every occupied rectangle must be finite and non-negative';
189
+ }
190
+ if (request.previous && !finite(request.previous.x, request.previous.y)) {
191
+ return 'the previous position must be a finite point';
192
+ }
193
+ return null;
194
+ }
195
+ }
@@ -20,6 +20,7 @@ export * from './_models/work-order-scheduler.control-model';
20
20
  export * from './_models/build-reservation.interface';
21
21
  export * from './_models/viewport.interface';
22
22
  export * from './_models/viewport.control-model';
23
+ export * from './_models/label-placement.interface';
23
24
  export * from './_models/input-action.interface';
24
25
  export * from './_models/input-action-registry.control-model';
25
26
  export * from './_models/guided-onboarding.interface';
@@ -31,3 +32,4 @@ export * from './_collections/audio-mixer.util';
31
32
  export * from './_collections/audio-scale.util';
32
33
  export * from './_collections/game-save.util';
33
34
  export * from './_collections/build-reservation.util';
35
+ export * from './_collections/label-placement.util';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futdevpro/fsm-dynamo",
3
- "version": "1.20.120",
3
+ "version": "1.20.122",
4
4
  "description": "Full Stack Model Collection for Dynamic (NodeJS-Typescript) Framework called Dynamo, by Future Development Ltd.",
5
5
  "DyBu_settings": {
6
6
  "packageType": "full-stack-package",
@@ -371,7 +371,7 @@
371
371
  "uuid": "~11.1.1"
372
372
  },
373
373
  "devDependencies": {
374
- "@futdevpro/dynamo-eslint": "1.20.57",
374
+ "@futdevpro/dynamo-eslint": "1.20.65",
375
375
  "@types/jasmine": "~4.3.5",
376
376
  "@typescript-eslint/eslint-plugin": "^8.41.0",
377
377
  "@typescript-eslint/parser": "^8.41.0",