@mpgd/target-config 0.15.2 → 0.17.0
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/dist/effective.d.ts +1 -0
- package/dist/effective.js +5 -1
- package/dist/index.js +117 -81
- package/dist/runtime.d.ts +11 -6
- package/dist/runtime.js +176 -40
- package/dist/viewport.d.ts +90 -0
- package/dist/viewport.js +171 -0
- package/package.json +5 -5
package/dist/viewport.js
CHANGED
|
@@ -152,6 +152,104 @@ export function resolveTargetViewportSnapshot(input, breakpoints = defaultTarget
|
|
|
152
152
|
safeArea: resolveTargetViewportSafeArea(plan.layout, input.safeAreaInsets),
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
+
/** Whether a size can produce a viewport plan: both sides finite and at least one CSS pixel. */
|
|
156
|
+
export function isMeasurableTargetViewport(size) {
|
|
157
|
+
return isMeasurableViewportDimension(size.width) && isMeasurableViewportDimension(size.height);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Measure the first host surface with a usable area: the game container, then the visual
|
|
161
|
+
* viewport, then the window. Returns `null` while every surface is still zero-sized, as in a
|
|
162
|
+
* hidden iframe, a collapsed embed or a background tab before layout. Resolve a viewport plan
|
|
163
|
+
* only from a measurement; {@link waitForTargetViewportMeasurement} waits for the first one.
|
|
164
|
+
*/
|
|
165
|
+
export function measureTargetViewport(sources) {
|
|
166
|
+
const rect = sources.container?.getBoundingClientRect();
|
|
167
|
+
if (rect !== undefined && isMeasurableTargetViewport(rect)) {
|
|
168
|
+
return { width: rect.width, height: rect.height, source: 'container' };
|
|
169
|
+
}
|
|
170
|
+
const visualViewport = sources.visualViewport;
|
|
171
|
+
if (visualViewport !== undefined &&
|
|
172
|
+
visualViewport !== null &&
|
|
173
|
+
isMeasurableTargetViewport(visualViewport)) {
|
|
174
|
+
return {
|
|
175
|
+
width: visualViewport.width,
|
|
176
|
+
height: visualViewport.height,
|
|
177
|
+
source: 'visual-viewport',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const host = sources.window;
|
|
181
|
+
if (host !== undefined && host !== null) {
|
|
182
|
+
const size = { width: host.innerWidth, height: host.innerHeight };
|
|
183
|
+
if (isMeasurableTargetViewport(size)) {
|
|
184
|
+
return { ...size, source: 'window' };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Resolve with the first usable measurement, so a game that boots inside a zero-sized surface
|
|
191
|
+
* starts once the host lays it out instead of failing viewport validation. Resolves immediately
|
|
192
|
+
* when the surface is already measurable and unsubscribes as soon as it settles. A measurement
|
|
193
|
+
* that throws, including the first one, rejects the returned promise.
|
|
194
|
+
*/
|
|
195
|
+
export function waitForTargetViewportMeasurement(input) {
|
|
196
|
+
let initial;
|
|
197
|
+
try {
|
|
198
|
+
initial = input.measure();
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
return Promise.reject(error);
|
|
202
|
+
}
|
|
203
|
+
if (initial !== null) {
|
|
204
|
+
return Promise.resolve(initial);
|
|
205
|
+
}
|
|
206
|
+
if (input.signal?.aborted === true) {
|
|
207
|
+
return Promise.reject(input.signal.reason);
|
|
208
|
+
}
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
let settled = false;
|
|
211
|
+
// The listener may settle synchronously inside `subscribe`, before its cleanup is returned.
|
|
212
|
+
const subscription = {};
|
|
213
|
+
const settle = () => {
|
|
214
|
+
settled = true;
|
|
215
|
+
subscription.unsubscribe?.();
|
|
216
|
+
input.signal?.removeEventListener('abort', abort);
|
|
217
|
+
};
|
|
218
|
+
const check = () => {
|
|
219
|
+
if (settled) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
let measurement;
|
|
223
|
+
try {
|
|
224
|
+
measurement = input.measure();
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
settle();
|
|
228
|
+
reject(error);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (measurement !== null) {
|
|
232
|
+
settle();
|
|
233
|
+
resolve(measurement);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const abort = () => {
|
|
237
|
+
if (!settled) {
|
|
238
|
+
settle();
|
|
239
|
+
reject(input.signal?.reason);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
input.signal?.addEventListener('abort', abort, { once: true });
|
|
243
|
+
const unsubscribe = input.subscribe(check);
|
|
244
|
+
if (settled) {
|
|
245
|
+
unsubscribe();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
subscription.unsubscribe = unsubscribe;
|
|
249
|
+
// A resize may land between the first measurement and the subscription.
|
|
250
|
+
check();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
155
253
|
/**
|
|
156
254
|
* Resolve an adaptive game shell inside the snapshot's safe content bounds.
|
|
157
255
|
*
|
|
@@ -255,6 +353,75 @@ export function resolveTargetViewportSafeArea(layout, insets) {
|
|
|
255
353
|
},
|
|
256
354
|
};
|
|
257
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Resolve all edge occlusions in one CSS-pixel coordinate space. System bars
|
|
358
|
+
* and CSS safe-area measurements can describe the same pixels, as can a banner
|
|
359
|
+
* and the home-indicator inset. Taking the maximum intrusion per edge avoids
|
|
360
|
+
* subtracting those regions twice. A surface's bounds are absolute viewport
|
|
361
|
+
* coordinates, not a banner height to add to another inset.
|
|
362
|
+
*/
|
|
363
|
+
export function resolveTargetViewportUsableArea(layout, input = {}) {
|
|
364
|
+
const viewport = {
|
|
365
|
+
width: normalizeViewportDimension(layout.width, 'width'),
|
|
366
|
+
height: normalizeViewportDimension(layout.height, 'height'),
|
|
367
|
+
};
|
|
368
|
+
const safeArea = normalizeTargetViewportSafeAreaInsets(input.safeAreaInsets);
|
|
369
|
+
const systemBars = normalizeTargetViewportSafeAreaInsets(input.systemBarInsets);
|
|
370
|
+
const keyboard = normalizeTargetViewportSafeAreaInsets(input.keyboardInsets);
|
|
371
|
+
const insets = {
|
|
372
|
+
top: Math.max(safeArea.top, systemBars.top, keyboard.top),
|
|
373
|
+
right: Math.max(safeArea.right, systemBars.right, keyboard.right),
|
|
374
|
+
bottom: Math.max(safeArea.bottom, systemBars.bottom, keyboard.bottom),
|
|
375
|
+
left: Math.max(safeArea.left, systemBars.left, keyboard.left),
|
|
376
|
+
};
|
|
377
|
+
const occupiedSurfaceIds = new Set();
|
|
378
|
+
for (const surface of input.occupiedSurfaces ?? []) {
|
|
379
|
+
if (surface.surfaceId.trim() === '' || occupiedSurfaceIds.has(surface.surfaceId)) {
|
|
380
|
+
throw new Error('Viewport occupied surface IDs must be non-empty and unique.');
|
|
381
|
+
}
|
|
382
|
+
occupiedSurfaceIds.add(surface.surfaceId);
|
|
383
|
+
const { x: rawX, y: rawY, width: rawWidth, height: rawHeight } = surface.bounds;
|
|
384
|
+
if (![rawX, rawY, rawWidth, rawHeight].every(Number.isFinite)
|
|
385
|
+
|| rawX < 0 || rawY < 0 || rawWidth < 0 || rawHeight < 0) {
|
|
386
|
+
throw new Error(`Viewport occupied surface ${surface.surfaceId} is outside the viewport.`);
|
|
387
|
+
}
|
|
388
|
+
// Round endpoints using the same whole-CSS-pixel convention as viewport
|
|
389
|
+
// dimensions. Rounding an origin and size separately can make a valid
|
|
390
|
+
// full-width fractional surface appear to exceed the rounded viewport.
|
|
391
|
+
const x = Math.round(rawX);
|
|
392
|
+
const y = Math.round(rawY);
|
|
393
|
+
const right = Math.round(rawX + rawWidth);
|
|
394
|
+
const bottom = Math.round(rawY + rawHeight);
|
|
395
|
+
const width = right - x;
|
|
396
|
+
const height = bottom - y;
|
|
397
|
+
if (right > viewport.width || bottom > viewport.height) {
|
|
398
|
+
throw new Error(`Viewport occupied surface ${surface.surfaceId} is outside the viewport.`);
|
|
399
|
+
}
|
|
400
|
+
if (width === 0 || height === 0) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
switch (surface.edge) {
|
|
404
|
+
case 'top':
|
|
405
|
+
insets.top = Math.max(insets.top, y + height);
|
|
406
|
+
break;
|
|
407
|
+
case 'right':
|
|
408
|
+
insets.right = Math.max(insets.right, viewport.width - x);
|
|
409
|
+
break;
|
|
410
|
+
case 'bottom':
|
|
411
|
+
insets.bottom = Math.max(insets.bottom, viewport.height - y);
|
|
412
|
+
break;
|
|
413
|
+
case 'left':
|
|
414
|
+
insets.left = Math.max(insets.left, x + width);
|
|
415
|
+
break;
|
|
416
|
+
default:
|
|
417
|
+
throw new Error('Viewport occupied surface edge is invalid.');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
...resolveTargetViewportSafeArea(viewport, insets),
|
|
422
|
+
occupiedSurfaceIds: [...occupiedSurfaceIds],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
258
425
|
/**
|
|
259
426
|
* Read the starter's computed CSS variables. Unknown or non-pixel values are
|
|
260
427
|
* intentionally treated as zero so browser shells without CSS env support keep
|
|
@@ -345,6 +512,10 @@ function readTargetViewportCssPixels(style, property) {
|
|
|
345
512
|
const match = /^(\d+(?:\.\d+)?)px$/u.exec(value);
|
|
346
513
|
return match === null ? 0 : Math.round(Number(match[1]));
|
|
347
514
|
}
|
|
515
|
+
/** Whether a dimension would pass {@link normalizeViewportDimension}. */
|
|
516
|
+
function isMeasurableViewportDimension(value) {
|
|
517
|
+
return Number.isFinite(value) && value > 0 && Math.round(value) > 0;
|
|
518
|
+
}
|
|
348
519
|
/** Normalize a positive viewport dimension to at least one CSS pixel. */
|
|
349
520
|
function normalizeViewportDimension(value, name) {
|
|
350
521
|
if (!Number.isFinite(value) || value <= 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/target-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Target-specific platform feature availability and effective config helpers for mpgd.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"typia": "13.0.2",
|
|
40
|
-
"@mpgd/catalog": "0.7.
|
|
41
|
-
"@mpgd/i18n": "0.6.
|
|
42
|
-
"@mpgd/platform": "0.
|
|
40
|
+
"@mpgd/catalog": "0.7.6",
|
|
41
|
+
"@mpgd/i18n": "0.6.5",
|
|
42
|
+
"@mpgd/platform": "0.14.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"ttsc": "0.30.4",
|
|
@@ -55,6 +55,6 @@
|
|
|
55
55
|
"lint": "ttsc --noEmit",
|
|
56
56
|
"format": "ttsc format",
|
|
57
57
|
"fix": "ttsc fix",
|
|
58
|
-
"test": "cd ../.. && node tools/run-ttsx.mjs packages/target-config/test/target-config-smoke.ts && node tools/run-ttsx.mjs packages/target-config/test/platform-version-allocation-smoke.ts"
|
|
58
|
+
"test": "cd ../.. && node tools/run-ttsx.mjs packages/target-config/test/target-config-smoke.ts && node tools/run-ttsx.mjs packages/target-config/test/platform-version-allocation-smoke.ts && node tools/run-ttsx.mjs packages/target-config/test/provider-availability-smoke.ts"
|
|
59
59
|
}
|
|
60
60
|
}
|