@futdevpro/fsm-dynamo 1.20.99 → 1.20.100
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.
- package/build/_modules/game/_models/viewport.control-model.d.ts +58 -0
- package/build/_modules/game/_models/viewport.control-model.d.ts.map +1 -0
- package/build/_modules/game/_models/viewport.control-model.js +167 -0
- package/build/_modules/game/_models/viewport.control-model.js.map +1 -0
- package/build/_modules/game/_models/viewport.interface.d.ts +35 -0
- package/build/_modules/game/_models/viewport.interface.d.ts.map +1 -0
- package/build/_modules/game/_models/viewport.interface.js +3 -0
- package/build/_modules/game/_models/viewport.interface.js.map +1 -0
- package/build/_modules/game/index.d.ts +2 -0
- package/build/_modules/game/index.d.ts.map +1 -1
- package/build/_modules/game/index.js +2 -0
- package/build/_modules/game/index.js.map +1 -1
- package/build-esm/_modules/game/_models/viewport.control-model.js +162 -0
- package/build-esm/_modules/game/_models/viewport.interface.js +1 -0
- package/build-esm/_modules/game/index.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { DyFM_Viewport_Config, DyFM_Viewport_Point, DyFM_Viewport_State } from './viewport.interface';
|
|
2
|
+
/**
|
|
3
|
+
* BFR-WARFACTORY-013 — framework-independent zoom / pan viewport. It is deliberately NOT "the camera": the same model
|
|
4
|
+
* drives a world canvas and a DOM panel (e.g. a zoomable tech tree). Input handling (pointer, wheel, keys) belongs to
|
|
5
|
+
* a UI adapter that calls these methods.
|
|
6
|
+
*
|
|
7
|
+
* - `worldToScreen` / `screenToWorld` — the only transform; the screen centre shows `center`.
|
|
8
|
+
* - `zoomAt(screenPoint, factor)` — the world point UNDER the pointer stays under the pointer (wheel / pinch feel).
|
|
9
|
+
* - `panByScreen(dx, dy)` — drag: the content follows the pointer at any zoom.
|
|
10
|
+
* - `move(direction, speed, elapsedSeconds)` — elapsed-time keyboard panning; each step is capped
|
|
11
|
+
* (`maxMoveStepSeconds`), so a hitch never makes the camera jump; speed is in SCREEN px/s (same feel at any zoom).
|
|
12
|
+
* - Zoom is clamped to [minZoom, maxZoom], the centre to `worldBounds`; non-finite input is ignored, never stored.
|
|
13
|
+
*/
|
|
14
|
+
export declare class DyFM_Viewport_ControlModel {
|
|
15
|
+
private screenWidth;
|
|
16
|
+
private screenHeight;
|
|
17
|
+
private readonly minZoom;
|
|
18
|
+
private readonly maxZoom;
|
|
19
|
+
private readonly bounds;
|
|
20
|
+
private readonly stepFactor;
|
|
21
|
+
private readonly maxMoveStep;
|
|
22
|
+
private centerPoint;
|
|
23
|
+
private zoomValue;
|
|
24
|
+
/** Creates the viewport; an invalid configuration throws `DYFM-VIEWPORT-CONFIG`. */
|
|
25
|
+
constructor(config: DyFM_Viewport_Config);
|
|
26
|
+
/** The world point shown at the screen centre. */
|
|
27
|
+
get center(): DyFM_Viewport_Point;
|
|
28
|
+
/** Screen pixels per world unit. */
|
|
29
|
+
get zoom(): number;
|
|
30
|
+
/** World → screen pixels. */
|
|
31
|
+
worldToScreen(world: DyFM_Viewport_Point): DyFM_Viewport_Point;
|
|
32
|
+
/** Screen pixels → world. */
|
|
33
|
+
screenToWorld(screen: DyFM_Viewport_Point): DyFM_Viewport_Point;
|
|
34
|
+
/** Sets the zoom (clamped), keeping the centre. */
|
|
35
|
+
setZoom(zoom: number): void;
|
|
36
|
+
/** Zooms by `factor` keeping the world point under `screenPoint` fixed on screen (then clamps). */
|
|
37
|
+
zoomAt(screenPoint: DyFM_Viewport_Point, factor: number): void;
|
|
38
|
+
/** `steps` notches (+ in, − out) at `screenPoint` (default: the screen centre). */
|
|
39
|
+
zoomStep(steps: number, screenPoint?: DyFM_Viewport_Point): void;
|
|
40
|
+
/** Drag by a screen delta: the content moves WITH the pointer. */
|
|
41
|
+
panByScreen(dx: number, dy: number): void;
|
|
42
|
+
/**
|
|
43
|
+
* Elapsed-time panning (keyboard): `direction` is normalised, `speedScreenPxPerSecond` is screen speed, the elapsed
|
|
44
|
+
* time is capped at `maxMoveStepSeconds` per call. Returns the world distance actually moved.
|
|
45
|
+
*/
|
|
46
|
+
move(direction: DyFM_Viewport_Point, speedScreenPxPerSecond: number, elapsedSeconds: number): number;
|
|
47
|
+
/** Centres on a world point (clamped). */
|
|
48
|
+
centerOn(world: DyFM_Viewport_Point): void;
|
|
49
|
+
/** The screen changed size (e.g. a window resize); the centre stays. */
|
|
50
|
+
resize(screenWidth: number, screenHeight: number): void;
|
|
51
|
+
/** Save state. */
|
|
52
|
+
exportState(): DyFM_Viewport_State;
|
|
53
|
+
/** Restore state (clamped to THIS viewport's limits); `false` and no change when unreadable. */
|
|
54
|
+
importState(state: unknown): boolean;
|
|
55
|
+
private clampZoom;
|
|
56
|
+
private clampCenter;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=viewport.control-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewport.control-model.d.ts","sourceRoot":"","sources":["../../../../src/_modules/game/_models/viewport.control-model.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEtG;;;;;;;;;;;GAWG;AACH,qBAAa,0BAA0B;IACrC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsC;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,SAAS,CAAa;IAE9B,oFAAoF;gBACxE,MAAM,EAAE,oBAAoB;IAyBxC,kDAAkD;IAClD,IAAI,MAAM,IAAI,mBAAmB,CAEhC;IAED,oCAAoC;IACpC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,6BAA6B;IAC7B,aAAa,CAAC,KAAK,EAAE,mBAAmB,GAAG,mBAAmB;IAO9D,6BAA6B;IAC7B,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,mBAAmB;IAO/D,mDAAmD;IACnD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM3B,mGAAmG;IACnG,MAAM,CAAC,WAAW,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAe9D,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,mBAAmB,GAAG,IAAI;IAMhE,kEAAkE;IAClE,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IASzC;;;OAGG;IACH,IAAI,CAAC,SAAS,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM;IAoBpG,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAM1C,wEAAwE;IACxE,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAOvD,kBAAkB;IAClB,WAAW,IAAI,mBAAmB;IAIlC,gGAAgG;IAChG,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IAapC,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,WAAW;CAUpB"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DyFM_Viewport_ControlModel = void 0;
|
|
4
|
+
const error_control_model_1 = require("../../../_models/control-models/error.control-model");
|
|
5
|
+
/**
|
|
6
|
+
* BFR-WARFACTORY-013 — framework-independent zoom / pan viewport. It is deliberately NOT "the camera": the same model
|
|
7
|
+
* drives a world canvas and a DOM panel (e.g. a zoomable tech tree). Input handling (pointer, wheel, keys) belongs to
|
|
8
|
+
* a UI adapter that calls these methods.
|
|
9
|
+
*
|
|
10
|
+
* - `worldToScreen` / `screenToWorld` — the only transform; the screen centre shows `center`.
|
|
11
|
+
* - `zoomAt(screenPoint, factor)` — the world point UNDER the pointer stays under the pointer (wheel / pinch feel).
|
|
12
|
+
* - `panByScreen(dx, dy)` — drag: the content follows the pointer at any zoom.
|
|
13
|
+
* - `move(direction, speed, elapsedSeconds)` — elapsed-time keyboard panning; each step is capped
|
|
14
|
+
* (`maxMoveStepSeconds`), so a hitch never makes the camera jump; speed is in SCREEN px/s (same feel at any zoom).
|
|
15
|
+
* - Zoom is clamped to [minZoom, maxZoom], the centre to `worldBounds`; non-finite input is ignored, never stored.
|
|
16
|
+
*/
|
|
17
|
+
class DyFM_Viewport_ControlModel {
|
|
18
|
+
screenWidth;
|
|
19
|
+
screenHeight;
|
|
20
|
+
minZoom;
|
|
21
|
+
maxZoom;
|
|
22
|
+
bounds;
|
|
23
|
+
stepFactor;
|
|
24
|
+
maxMoveStep;
|
|
25
|
+
centerPoint = { x: 0, y: 0 };
|
|
26
|
+
zoomValue = 1;
|
|
27
|
+
/** Creates the viewport; an invalid configuration throws `DYFM-VIEWPORT-CONFIG`. */
|
|
28
|
+
constructor(config) {
|
|
29
|
+
this.minZoom = config.minZoom ?? 0.25;
|
|
30
|
+
this.maxZoom = config.maxZoom ?? 4;
|
|
31
|
+
this.stepFactor = config.zoomStepFactor ?? 1.1;
|
|
32
|
+
this.maxMoveStep = config.maxMoveStepSeconds ?? 0.1;
|
|
33
|
+
this.bounds = config.worldBounds;
|
|
34
|
+
const positive = (value) => Number.isFinite(value) && value > 0;
|
|
35
|
+
if (!positive(config.screenWidth) || !positive(config.screenHeight) || !positive(this.minZoom)
|
|
36
|
+
|| !(this.maxZoom >= this.minZoom) || !(this.stepFactor > 1) || !positive(this.maxMoveStep)
|
|
37
|
+
|| (this.bounds && !(this.bounds.maxX >= this.bounds.minX && this.bounds.maxY >= this.bounds.minY))) {
|
|
38
|
+
throw new error_control_model_1.DyFM_Error({
|
|
39
|
+
message: 'DyFM_Viewport: needs a positive screen size, 0 < minZoom ≤ maxZoom, zoomStepFactor > 1, '
|
|
40
|
+
+ 'maxMoveStepSeconds > 0 and ordered worldBounds',
|
|
41
|
+
errorCode: 'DYFM-VIEWPORT-CONFIG',
|
|
42
|
+
issuerService: 'DyFM_Viewport_ControlModel',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
this.screenWidth = config.screenWidth;
|
|
46
|
+
this.screenHeight = config.screenHeight;
|
|
47
|
+
this.zoomValue = this.clampZoom(1);
|
|
48
|
+
this.centerPoint = this.clampCenter({ x: 0, y: 0 });
|
|
49
|
+
}
|
|
50
|
+
/** The world point shown at the screen centre. */
|
|
51
|
+
get center() {
|
|
52
|
+
return { ...this.centerPoint };
|
|
53
|
+
}
|
|
54
|
+
/** Screen pixels per world unit. */
|
|
55
|
+
get zoom() {
|
|
56
|
+
return this.zoomValue;
|
|
57
|
+
}
|
|
58
|
+
/** World → screen pixels. */
|
|
59
|
+
worldToScreen(world) {
|
|
60
|
+
return {
|
|
61
|
+
x: (world.x - this.centerPoint.x) * this.zoomValue + this.screenWidth / 2,
|
|
62
|
+
y: (world.y - this.centerPoint.y) * this.zoomValue + this.screenHeight / 2,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Screen pixels → world. */
|
|
66
|
+
screenToWorld(screen) {
|
|
67
|
+
return {
|
|
68
|
+
x: (screen.x - this.screenWidth / 2) / this.zoomValue + this.centerPoint.x,
|
|
69
|
+
y: (screen.y - this.screenHeight / 2) / this.zoomValue + this.centerPoint.y,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Sets the zoom (clamped), keeping the centre. */
|
|
73
|
+
setZoom(zoom) {
|
|
74
|
+
if (Number.isFinite(zoom)) {
|
|
75
|
+
this.zoomValue = this.clampZoom(zoom);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Zooms by `factor` keeping the world point under `screenPoint` fixed on screen (then clamps). */
|
|
79
|
+
zoomAt(screenPoint, factor) {
|
|
80
|
+
if (!Number.isFinite(factor) || factor <= 0 || !Number.isFinite(screenPoint?.x) || !Number.isFinite(screenPoint?.y)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const anchor = this.screenToWorld(screenPoint);
|
|
84
|
+
this.zoomValue = this.clampZoom(this.zoomValue * factor);
|
|
85
|
+
// re-centre so that `anchor` maps back to `screenPoint`
|
|
86
|
+
this.centerPoint = this.clampCenter({
|
|
87
|
+
x: anchor.x - (screenPoint.x - this.screenWidth / 2) / this.zoomValue,
|
|
88
|
+
y: anchor.y - (screenPoint.y - this.screenHeight / 2) / this.zoomValue,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** `steps` notches (+ in, − out) at `screenPoint` (default: the screen centre). */
|
|
92
|
+
zoomStep(steps, screenPoint) {
|
|
93
|
+
if (Number.isFinite(steps) && steps !== 0) {
|
|
94
|
+
this.zoomAt(screenPoint ?? { x: this.screenWidth / 2, y: this.screenHeight / 2 }, this.stepFactor ** steps);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Drag by a screen delta: the content moves WITH the pointer. */
|
|
98
|
+
panByScreen(dx, dy) {
|
|
99
|
+
if (Number.isFinite(dx) && Number.isFinite(dy)) {
|
|
100
|
+
this.centerPoint = this.clampCenter({
|
|
101
|
+
x: this.centerPoint.x - dx / this.zoomValue,
|
|
102
|
+
y: this.centerPoint.y - dy / this.zoomValue,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Elapsed-time panning (keyboard): `direction` is normalised, `speedScreenPxPerSecond` is screen speed, the elapsed
|
|
108
|
+
* time is capped at `maxMoveStepSeconds` per call. Returns the world distance actually moved.
|
|
109
|
+
*/
|
|
110
|
+
move(direction, speedScreenPxPerSecond, elapsedSeconds) {
|
|
111
|
+
const length = Math.hypot(direction?.x ?? NaN, direction?.y ?? NaN);
|
|
112
|
+
if (!Number.isFinite(length) || length === 0 || !Number.isFinite(speedScreenPxPerSecond)
|
|
113
|
+
|| speedScreenPxPerSecond <= 0 || !Number.isFinite(elapsedSeconds) || elapsedSeconds <= 0) {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
const seconds = Math.min(elapsedSeconds, this.maxMoveStep);
|
|
117
|
+
const worldDistance = speedScreenPxPerSecond * seconds / this.zoomValue;
|
|
118
|
+
const before = this.centerPoint;
|
|
119
|
+
this.centerPoint = this.clampCenter({
|
|
120
|
+
x: before.x + direction.x / length * worldDistance,
|
|
121
|
+
y: before.y + direction.y / length * worldDistance,
|
|
122
|
+
});
|
|
123
|
+
return Math.hypot(this.centerPoint.x - before.x, this.centerPoint.y - before.y);
|
|
124
|
+
}
|
|
125
|
+
/** Centres on a world point (clamped). */
|
|
126
|
+
centerOn(world) {
|
|
127
|
+
if (Number.isFinite(world?.x) && Number.isFinite(world?.y)) {
|
|
128
|
+
this.centerPoint = this.clampCenter(world);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** The screen changed size (e.g. a window resize); the centre stays. */
|
|
132
|
+
resize(screenWidth, screenHeight) {
|
|
133
|
+
if (Number.isFinite(screenWidth) && screenWidth > 0 && Number.isFinite(screenHeight) && screenHeight > 0) {
|
|
134
|
+
this.screenWidth = screenWidth;
|
|
135
|
+
this.screenHeight = screenHeight;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Save state. */
|
|
139
|
+
exportState() {
|
|
140
|
+
return { schemaVersion: 1, center: this.center, zoom: this.zoomValue };
|
|
141
|
+
}
|
|
142
|
+
/** Restore state (clamped to THIS viewport's limits); `false` and no change when unreadable. */
|
|
143
|
+
importState(state) {
|
|
144
|
+
const candidate = state && typeof state === 'object' ? state : null;
|
|
145
|
+
if (!candidate || candidate.schemaVersion !== 1 || !Number.isFinite(candidate.zoom)
|
|
146
|
+
|| !Number.isFinite(candidate.center?.x) || !Number.isFinite(candidate.center?.y)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
this.zoomValue = this.clampZoom(candidate.zoom);
|
|
150
|
+
this.centerPoint = this.clampCenter(candidate.center);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
clampZoom(zoom) {
|
|
154
|
+
return Math.min(this.maxZoom, Math.max(this.minZoom, zoom));
|
|
155
|
+
}
|
|
156
|
+
clampCenter(point) {
|
|
157
|
+
if (!this.bounds) {
|
|
158
|
+
return { x: point.x, y: point.y };
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
x: Math.min(this.bounds.maxX, Math.max(this.bounds.minX, point.x)),
|
|
162
|
+
y: Math.min(this.bounds.maxY, Math.max(this.bounds.minY, point.y)),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.DyFM_Viewport_ControlModel = DyFM_Viewport_ControlModel;
|
|
167
|
+
//# sourceMappingURL=viewport.control-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewport.control-model.js","sourceRoot":"","sources":["../../../../src/_modules/game/_models/viewport.control-model.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** BFR-WARFACTORY-013 — a 2D point (world units or screen pixels, depending on the call). */
|
|
2
|
+
export interface DyFM_Viewport_Point {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
/** BFR-WARFACTORY-013 — `DyFM_Viewport_ControlModel` configuration (one model for a world canvas AND a DOM panel). */
|
|
7
|
+
export interface DyFM_Viewport_Config {
|
|
8
|
+
/** Screen size in pixels (update with `resize`). */
|
|
9
|
+
screenWidth: number;
|
|
10
|
+
screenHeight: number;
|
|
11
|
+
/** Zoom limits (1 = one world unit per pixel). Defaults: `0.25` .. `4`. */
|
|
12
|
+
minZoom?: number;
|
|
13
|
+
maxZoom?: number;
|
|
14
|
+
/** Optional world rectangle the CENTER is kept inside. */
|
|
15
|
+
worldBounds?: {
|
|
16
|
+
minX: number;
|
|
17
|
+
minY: number;
|
|
18
|
+
maxX: number;
|
|
19
|
+
maxY: number;
|
|
20
|
+
};
|
|
21
|
+
/** Multiplicative zoom step for `zoomStep(±n)` (e.g. a wheel notch). Default: `1.1`. */
|
|
22
|
+
zoomStepFactor?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Largest elapsed time one `move` step may use (seconds) — a frame hitch / tab switch must not become a camera
|
|
25
|
+
* JUMP. Default: `0.1`.
|
|
26
|
+
*/
|
|
27
|
+
maxMoveStepSeconds?: number;
|
|
28
|
+
}
|
|
29
|
+
/** BFR-WARFACTORY-013 — the save / restore state. */
|
|
30
|
+
export interface DyFM_Viewport_State {
|
|
31
|
+
schemaVersion: 1;
|
|
32
|
+
center: DyFM_Viewport_Point;
|
|
33
|
+
zoom: number;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=viewport.interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewport.interface.d.ts","sourceRoot":"","sources":["../../../../src/_modules/game/_models/viewport.interface.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,MAAM,WAAW,mBAAmB;IAClC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,sHAAsH;AACtH,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACzE,wFAAwF;IACxF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,qDAAqD;AACrD,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,CAAC,CAAC;IACjB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewport.interface.js","sourceRoot":"","sources":["../../../../src/_modules/game/_models/viewport.interface.ts"],"names":[],"mappings":""}
|
|
@@ -16,6 +16,8 @@ export * from './_models/game-save.interface';
|
|
|
16
16
|
export * from './_models/work-order-scheduler.interface';
|
|
17
17
|
export * from './_models/work-order-scheduler.control-model';
|
|
18
18
|
export * from './_models/build-reservation.interface';
|
|
19
|
+
export * from './_models/viewport.interface';
|
|
20
|
+
export * from './_models/viewport.control-model';
|
|
19
21
|
export * from './_collections/audio-mixer.util';
|
|
20
22
|
export * from './_collections/audio-scale.util';
|
|
21
23
|
export * from './_collections/game-save.util';
|
|
@@ -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;
|
|
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;AAGjD,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,CAAC"}
|
|
@@ -21,6 +21,8 @@ tslib_1.__exportStar(require("./_models/game-save.interface"), exports);
|
|
|
21
21
|
tslib_1.__exportStar(require("./_models/work-order-scheduler.interface"), exports);
|
|
22
22
|
tslib_1.__exportStar(require("./_models/work-order-scheduler.control-model"), exports);
|
|
23
23
|
tslib_1.__exportStar(require("./_models/build-reservation.interface"), exports);
|
|
24
|
+
tslib_1.__exportStar(require("./_models/viewport.interface"), exports);
|
|
25
|
+
tslib_1.__exportStar(require("./_models/viewport.control-model"), exports);
|
|
24
26
|
// COLLECTIONS
|
|
25
27
|
tslib_1.__exportStar(require("./_collections/audio-mixer.util"), exports);
|
|
26
28
|
tslib_1.__exportStar(require("./_collections/audio-scale.util"), exports);
|
|
@@ -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;
|
|
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;AAEjD,cAAc;AACd,0EAAgD;AAChD,0EAAgD;AAChD,wEAA8C;AAC9C,gFAAsD"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { DyFM_Error } from '../../../_models/control-models/error.control-model';
|
|
2
|
+
/**
|
|
3
|
+
* BFR-WARFACTORY-013 — framework-independent zoom / pan viewport. It is deliberately NOT "the camera": the same model
|
|
4
|
+
* drives a world canvas and a DOM panel (e.g. a zoomable tech tree). Input handling (pointer, wheel, keys) belongs to
|
|
5
|
+
* a UI adapter that calls these methods.
|
|
6
|
+
*
|
|
7
|
+
* - `worldToScreen` / `screenToWorld` — the only transform; the screen centre shows `center`.
|
|
8
|
+
* - `zoomAt(screenPoint, factor)` — the world point UNDER the pointer stays under the pointer (wheel / pinch feel).
|
|
9
|
+
* - `panByScreen(dx, dy)` — drag: the content follows the pointer at any zoom.
|
|
10
|
+
* - `move(direction, speed, elapsedSeconds)` — elapsed-time keyboard panning; each step is capped
|
|
11
|
+
* (`maxMoveStepSeconds`), so a hitch never makes the camera jump; speed is in SCREEN px/s (same feel at any zoom).
|
|
12
|
+
* - Zoom is clamped to [minZoom, maxZoom], the centre to `worldBounds`; non-finite input is ignored, never stored.
|
|
13
|
+
*/
|
|
14
|
+
export class DyFM_Viewport_ControlModel {
|
|
15
|
+
screenWidth;
|
|
16
|
+
screenHeight;
|
|
17
|
+
minZoom;
|
|
18
|
+
maxZoom;
|
|
19
|
+
bounds;
|
|
20
|
+
stepFactor;
|
|
21
|
+
maxMoveStep;
|
|
22
|
+
centerPoint = { x: 0, y: 0 };
|
|
23
|
+
zoomValue = 1;
|
|
24
|
+
/** Creates the viewport; an invalid configuration throws `DYFM-VIEWPORT-CONFIG`. */
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.minZoom = config.minZoom ?? 0.25;
|
|
27
|
+
this.maxZoom = config.maxZoom ?? 4;
|
|
28
|
+
this.stepFactor = config.zoomStepFactor ?? 1.1;
|
|
29
|
+
this.maxMoveStep = config.maxMoveStepSeconds ?? 0.1;
|
|
30
|
+
this.bounds = config.worldBounds;
|
|
31
|
+
const positive = (value) => Number.isFinite(value) && value > 0;
|
|
32
|
+
if (!positive(config.screenWidth) || !positive(config.screenHeight) || !positive(this.minZoom)
|
|
33
|
+
|| !(this.maxZoom >= this.minZoom) || !(this.stepFactor > 1) || !positive(this.maxMoveStep)
|
|
34
|
+
|| (this.bounds && !(this.bounds.maxX >= this.bounds.minX && this.bounds.maxY >= this.bounds.minY))) {
|
|
35
|
+
throw new DyFM_Error({
|
|
36
|
+
message: 'DyFM_Viewport: needs a positive screen size, 0 < minZoom ≤ maxZoom, zoomStepFactor > 1, '
|
|
37
|
+
+ 'maxMoveStepSeconds > 0 and ordered worldBounds',
|
|
38
|
+
errorCode: 'DYFM-VIEWPORT-CONFIG',
|
|
39
|
+
issuerService: 'DyFM_Viewport_ControlModel',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
this.screenWidth = config.screenWidth;
|
|
43
|
+
this.screenHeight = config.screenHeight;
|
|
44
|
+
this.zoomValue = this.clampZoom(1);
|
|
45
|
+
this.centerPoint = this.clampCenter({ x: 0, y: 0 });
|
|
46
|
+
}
|
|
47
|
+
/** The world point shown at the screen centre. */
|
|
48
|
+
get center() {
|
|
49
|
+
return { ...this.centerPoint };
|
|
50
|
+
}
|
|
51
|
+
/** Screen pixels per world unit. */
|
|
52
|
+
get zoom() {
|
|
53
|
+
return this.zoomValue;
|
|
54
|
+
}
|
|
55
|
+
/** World → screen pixels. */
|
|
56
|
+
worldToScreen(world) {
|
|
57
|
+
return {
|
|
58
|
+
x: (world.x - this.centerPoint.x) * this.zoomValue + this.screenWidth / 2,
|
|
59
|
+
y: (world.y - this.centerPoint.y) * this.zoomValue + this.screenHeight / 2,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Screen pixels → world. */
|
|
63
|
+
screenToWorld(screen) {
|
|
64
|
+
return {
|
|
65
|
+
x: (screen.x - this.screenWidth / 2) / this.zoomValue + this.centerPoint.x,
|
|
66
|
+
y: (screen.y - this.screenHeight / 2) / this.zoomValue + this.centerPoint.y,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Sets the zoom (clamped), keeping the centre. */
|
|
70
|
+
setZoom(zoom) {
|
|
71
|
+
if (Number.isFinite(zoom)) {
|
|
72
|
+
this.zoomValue = this.clampZoom(zoom);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Zooms by `factor` keeping the world point under `screenPoint` fixed on screen (then clamps). */
|
|
76
|
+
zoomAt(screenPoint, factor) {
|
|
77
|
+
if (!Number.isFinite(factor) || factor <= 0 || !Number.isFinite(screenPoint?.x) || !Number.isFinite(screenPoint?.y)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const anchor = this.screenToWorld(screenPoint);
|
|
81
|
+
this.zoomValue = this.clampZoom(this.zoomValue * factor);
|
|
82
|
+
// re-centre so that `anchor` maps back to `screenPoint`
|
|
83
|
+
this.centerPoint = this.clampCenter({
|
|
84
|
+
x: anchor.x - (screenPoint.x - this.screenWidth / 2) / this.zoomValue,
|
|
85
|
+
y: anchor.y - (screenPoint.y - this.screenHeight / 2) / this.zoomValue,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** `steps` notches (+ in, − out) at `screenPoint` (default: the screen centre). */
|
|
89
|
+
zoomStep(steps, screenPoint) {
|
|
90
|
+
if (Number.isFinite(steps) && steps !== 0) {
|
|
91
|
+
this.zoomAt(screenPoint ?? { x: this.screenWidth / 2, y: this.screenHeight / 2 }, this.stepFactor ** steps);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Drag by a screen delta: the content moves WITH the pointer. */
|
|
95
|
+
panByScreen(dx, dy) {
|
|
96
|
+
if (Number.isFinite(dx) && Number.isFinite(dy)) {
|
|
97
|
+
this.centerPoint = this.clampCenter({
|
|
98
|
+
x: this.centerPoint.x - dx / this.zoomValue,
|
|
99
|
+
y: this.centerPoint.y - dy / this.zoomValue,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Elapsed-time panning (keyboard): `direction` is normalised, `speedScreenPxPerSecond` is screen speed, the elapsed
|
|
105
|
+
* time is capped at `maxMoveStepSeconds` per call. Returns the world distance actually moved.
|
|
106
|
+
*/
|
|
107
|
+
move(direction, speedScreenPxPerSecond, elapsedSeconds) {
|
|
108
|
+
const length = Math.hypot(direction?.x ?? NaN, direction?.y ?? NaN);
|
|
109
|
+
if (!Number.isFinite(length) || length === 0 || !Number.isFinite(speedScreenPxPerSecond)
|
|
110
|
+
|| speedScreenPxPerSecond <= 0 || !Number.isFinite(elapsedSeconds) || elapsedSeconds <= 0) {
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
const seconds = Math.min(elapsedSeconds, this.maxMoveStep);
|
|
114
|
+
const worldDistance = speedScreenPxPerSecond * seconds / this.zoomValue;
|
|
115
|
+
const before = this.centerPoint;
|
|
116
|
+
this.centerPoint = this.clampCenter({
|
|
117
|
+
x: before.x + direction.x / length * worldDistance,
|
|
118
|
+
y: before.y + direction.y / length * worldDistance,
|
|
119
|
+
});
|
|
120
|
+
return Math.hypot(this.centerPoint.x - before.x, this.centerPoint.y - before.y);
|
|
121
|
+
}
|
|
122
|
+
/** Centres on a world point (clamped). */
|
|
123
|
+
centerOn(world) {
|
|
124
|
+
if (Number.isFinite(world?.x) && Number.isFinite(world?.y)) {
|
|
125
|
+
this.centerPoint = this.clampCenter(world);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** The screen changed size (e.g. a window resize); the centre stays. */
|
|
129
|
+
resize(screenWidth, screenHeight) {
|
|
130
|
+
if (Number.isFinite(screenWidth) && screenWidth > 0 && Number.isFinite(screenHeight) && screenHeight > 0) {
|
|
131
|
+
this.screenWidth = screenWidth;
|
|
132
|
+
this.screenHeight = screenHeight;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Save state. */
|
|
136
|
+
exportState() {
|
|
137
|
+
return { schemaVersion: 1, center: this.center, zoom: this.zoomValue };
|
|
138
|
+
}
|
|
139
|
+
/** Restore state (clamped to THIS viewport's limits); `false` and no change when unreadable. */
|
|
140
|
+
importState(state) {
|
|
141
|
+
const candidate = state && typeof state === 'object' ? state : null;
|
|
142
|
+
if (!candidate || candidate.schemaVersion !== 1 || !Number.isFinite(candidate.zoom)
|
|
143
|
+
|| !Number.isFinite(candidate.center?.x) || !Number.isFinite(candidate.center?.y)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
this.zoomValue = this.clampZoom(candidate.zoom);
|
|
147
|
+
this.centerPoint = this.clampCenter(candidate.center);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
clampZoom(zoom) {
|
|
151
|
+
return Math.min(this.maxZoom, Math.max(this.minZoom, zoom));
|
|
152
|
+
}
|
|
153
|
+
clampCenter(point) {
|
|
154
|
+
if (!this.bounds) {
|
|
155
|
+
return { x: point.x, y: point.y };
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
x: Math.min(this.bounds.maxX, Math.max(this.bounds.minX, point.x)),
|
|
159
|
+
y: Math.min(this.bounds.maxY, Math.max(this.bounds.minY, point.y)),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -18,6 +18,8 @@ export * from './_models/game-save.interface';
|
|
|
18
18
|
export * from './_models/work-order-scheduler.interface';
|
|
19
19
|
export * from './_models/work-order-scheduler.control-model';
|
|
20
20
|
export * from './_models/build-reservation.interface';
|
|
21
|
+
export * from './_models/viewport.interface';
|
|
22
|
+
export * from './_models/viewport.control-model';
|
|
21
23
|
// COLLECTIONS
|
|
22
24
|
export * from './_collections/audio-mixer.util';
|
|
23
25
|
export * from './_collections/audio-scale.util';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@futdevpro/fsm-dynamo",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.100",
|
|
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",
|