@flighthq/node 0.2.1-next.410.a23f189 → 0.2.1-next.444.a313478
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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/stageFit.d.ts +12 -0
- package/dist/stageFit.d.ts.map +1 -0
- package/dist/stageFit.js +79 -0
- package/dist/stageFit.js.map +1 -0
- package/dist/traversal.d.ts +3 -1
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +0 -4
- package/dist/traversal.js.map +1 -1
- package/dist/viewport.d.ts +3 -7
- package/dist/viewport.d.ts.map +1 -1
- package/dist/viewport.js +13 -73
- package/dist/viewport.js.map +1 -1
- package/package.json +5 -5
- package/src/stageFit.test.ts +223 -0
- package/src/traversal.test.ts +13 -0
- package/src/viewport.test.ts +21 -218
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MatrixLike, Node, NodeTraits, ViewportAlign, ViewportScaleMode } from '@flighthq/types';
|
|
2
|
+
export interface StageFitContext<Traits extends object = NodeTraits> {
|
|
3
|
+
align: ViewportAlign;
|
|
4
|
+
root: Node<Traits> | null;
|
|
5
|
+
scaleMode: ViewportScaleMode;
|
|
6
|
+
}
|
|
7
|
+
export declare function computeStageFitAlignX(scaledContentWidth: number, viewWidth: number, align: ViewportAlign): number;
|
|
8
|
+
export declare function computeStageFitAlignY(scaledContentHeight: number, viewHeight: number, align: ViewportAlign): number;
|
|
9
|
+
export declare function computeStageFitFillScale(contentWidth: number, contentHeight: number, viewWidth: number, viewHeight: number): number;
|
|
10
|
+
export declare function computeStageFitScale(contentWidth: number, contentHeight: number, viewWidth: number, viewHeight: number): number;
|
|
11
|
+
export declare function computeStageFitTransform<Traits extends object = NodeTraits>(out: MatrixLike, stage: Readonly<StageFitContext<Traits>>, viewWidth: number, viewHeight: number): void;
|
|
12
|
+
//# sourceMappingURL=stageFit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stageFit.d.ts","sourceRoot":"","sources":["../src/stageFit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,UAAU,EACV,IAAI,EACJ,UAAU,EAEV,aAAa,EACb,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAOzB,MAAM,WAAW,eAAe,CAAC,MAAM,SAAS,MAAM,GAAG,UAAU;IACjE,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,iBAAiB,CAAC;CAC9B;AAGD,wBAAgB,qBAAqB,CAAC,kBAAkB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,CAIjH;AAGD,wBAAgB,qBAAqB,CAAC,mBAAmB,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,CAInH;AAGD,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,MAAM,CAER;AAGD,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,MAAM,CAER;AAMD,wBAAgB,wBAAwB,CAAC,MAAM,SAAS,MAAM,GAAG,UAAU,EACzE,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,EACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,IAAI,CA+CN"}
|
package/dist/stageFit.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createRectangle } from '@flighthq/geometry';
|
|
2
|
+
import { getNodeRuntime } from './node';
|
|
3
|
+
// Horizontal alignment offset of scaled content within the view, per the `align` anchor.
|
|
4
|
+
export function computeStageFitAlignX(scaledContentWidth, viewWidth, align) {
|
|
5
|
+
if (align.includes('left'))
|
|
6
|
+
return 0;
|
|
7
|
+
if (align.includes('right'))
|
|
8
|
+
return viewWidth - scaledContentWidth;
|
|
9
|
+
return (viewWidth - scaledContentWidth) / 2;
|
|
10
|
+
}
|
|
11
|
+
// Vertical alignment offset of scaled content within the view, per the `align` anchor.
|
|
12
|
+
export function computeStageFitAlignY(scaledContentHeight, viewHeight, align) {
|
|
13
|
+
if (align.includes('top'))
|
|
14
|
+
return 0;
|
|
15
|
+
if (align.includes('bottom'))
|
|
16
|
+
return viewHeight - scaledContentHeight;
|
|
17
|
+
return (viewHeight - scaledContentHeight) / 2;
|
|
18
|
+
}
|
|
19
|
+
// Uniform scale that fills the view (covers it, cropping overflow) — the larger of the axis ratios.
|
|
20
|
+
export function computeStageFitFillScale(contentWidth, contentHeight, viewWidth, viewHeight) {
|
|
21
|
+
return Math.max(viewWidth / contentWidth, viewHeight / contentHeight);
|
|
22
|
+
}
|
|
23
|
+
// Uniform scale that fits the content inside the view (letter/pillarboxing) — the smaller of the axis ratios.
|
|
24
|
+
export function computeStageFitScale(contentWidth, contentHeight, viewWidth, viewHeight) {
|
|
25
|
+
return Math.min(viewWidth / contentWidth, viewHeight / contentHeight);
|
|
26
|
+
}
|
|
27
|
+
// Writes the scale-and-align matrix that maps a stage's `root` content into a `viewWidth`×`viewHeight` view,
|
|
28
|
+
// per its `scaleMode` (`noscale`/`exactfit`/`showall`/`noborder`) and `align`. Reads the root's local bounds
|
|
29
|
+
// through its runtime; an empty or unmeasurable root yields identity. This is the stage-fit transform the 2D
|
|
30
|
+
// present pass applies before drawing the tree.
|
|
31
|
+
export function computeStageFitTransform(out, stage, viewWidth, viewHeight) {
|
|
32
|
+
let contentWidth = 0;
|
|
33
|
+
let contentHeight = 0;
|
|
34
|
+
if (stage.root !== null) {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
const runtime = getNodeRuntime(stage.root);
|
|
37
|
+
if (runtime?.computeLocalBoundsRectangle !== undefined) {
|
|
38
|
+
_tempRectangle.width = 0;
|
|
39
|
+
_tempRectangle.height = 0;
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
runtime.computeLocalBoundsRectangle(_tempRectangle, stage.root);
|
|
42
|
+
contentWidth = _tempRectangle.width;
|
|
43
|
+
contentHeight = _tempRectangle.height;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (contentWidth === 0 || contentHeight === 0) {
|
|
47
|
+
out.a = 1;
|
|
48
|
+
out.b = 0;
|
|
49
|
+
out.c = 0;
|
|
50
|
+
out.d = 1;
|
|
51
|
+
out.tx = 0;
|
|
52
|
+
out.ty = 0;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let sx;
|
|
56
|
+
let sy;
|
|
57
|
+
if (stage.scaleMode === 'noscale') {
|
|
58
|
+
sx = 1;
|
|
59
|
+
sy = 1;
|
|
60
|
+
}
|
|
61
|
+
else if (stage.scaleMode === 'exactfit') {
|
|
62
|
+
sx = viewWidth / contentWidth;
|
|
63
|
+
sy = viewHeight / contentHeight;
|
|
64
|
+
}
|
|
65
|
+
else if (stage.scaleMode === 'showall') {
|
|
66
|
+
sx = sy = computeStageFitScale(contentWidth, contentHeight, viewWidth, viewHeight);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
sx = sy = computeStageFitFillScale(contentWidth, contentHeight, viewWidth, viewHeight);
|
|
70
|
+
}
|
|
71
|
+
out.a = sx;
|
|
72
|
+
out.b = 0;
|
|
73
|
+
out.c = 0;
|
|
74
|
+
out.d = sy;
|
|
75
|
+
out.tx = computeStageFitAlignX(contentWidth * sx, viewWidth, stage.align);
|
|
76
|
+
out.ty = computeStageFitAlignY(contentHeight * sy, viewHeight, stage.align);
|
|
77
|
+
}
|
|
78
|
+
const _tempRectangle = createRectangle();
|
|
79
|
+
//# sourceMappingURL=stageFit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stageFit.js","sourceRoot":"","sources":["../src/stageFit.ts"],"names":[],"mappings":"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"}
|
package/dist/traversal.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Node, NodeDescendantVisitor, NodeOf, NodeTraits } from '@flighthq/types';
|
|
2
2
|
/**
|
|
3
3
|
* Finds the first descendant of `source` (depth-first pre-order) that satisfies `predicate`.
|
|
4
|
-
* Returns `null` if no descendant matches.
|
|
4
|
+
* Returns `null` if no descendant matches. When `predicate` is a type guard, the result is
|
|
5
|
+
* narrowed to the guarded type, so `findNode(root, isMesh)` returns `Mesh | null` with no cast.
|
|
5
6
|
*/
|
|
7
|
+
export declare function findNode<Traits extends object, Result extends Node<Traits>>(source: Readonly<Node<Traits>>, predicate: (node: Node<Traits>) => node is Result): Result | null;
|
|
6
8
|
export declare function findNode<Traits extends object = NodeTraits>(source: Readonly<Node<Traits>>, predicate: (node: Node<Traits>) => boolean): NodeOf<Traits> | null;
|
|
7
9
|
/**
|
|
8
10
|
* Finds the first descendant of `source` (depth-first pre-order) with the given `name`. Searches
|
package/dist/traversal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"traversal.d.ts","sourceRoot":"","sources":["../src/traversal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,qBAAqB,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAKvF
|
|
1
|
+
{"version":3,"file":"traversal.d.ts","sourceRoot":"","sources":["../src/traversal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,qBAAqB,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAKvF;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,SAAS,IAAI,CAAC,MAAM,CAAC,EACzE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAC9B,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI,MAAM,GAChD,MAAM,GAAG,IAAI,CAAC;AACjB,wBAAgB,QAAQ,CAAC,MAAM,SAAS,MAAM,GAAG,UAAU,EACzD,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAC9B,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,OAAO,GACzC,MAAM,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;AAgBzB;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,SAAS,MAAM,GAAG,UAAU,EAC/D,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAC9B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,CAAC,GAAG,IAAI,CAEvB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,SAAS,MAAM,GAAG,UAAU,EACpE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAC9B,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,OAAO,GACxC,IAAI,CAMN;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,SAAS,MAAM,GAAG,UAAU,EACtE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAC9B,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,GACrC,IAAI,CAON;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,SAAS,MAAM,GAAG,UAAU,EAChE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAC7B,SAAS,MAAM,CAAC,MAAM,CAAC,EAAE,CAI3B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,SAAS,MAAM,GAAG,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,CAQvG;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,SAAS,MAAM,GAAG,UAAU,EACnE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAC7B,MAAM,CAAC,MAAM,CAAC,GAAG,IAAI,CAQvB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,SAAS,MAAM,GAAG,UAAU,EACvE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAC7B,MAAM,CAAC,MAAM,CAAC,GAAG,IAAI,CAQvB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,SAAS,MAAM,GAAG,UAAU,EACpE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAC9B,KAAK,EAAE,qBAAqB,CAAC,MAAM,CAAC,GACnC,OAAO,CAST"}
|
package/dist/traversal.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { getNodeParent } from './hierarchy';
|
|
2
2
|
import { getNodeRuntime } from './node';
|
|
3
|
-
/**
|
|
4
|
-
* Finds the first descendant of `source` (depth-first pre-order) that satisfies `predicate`.
|
|
5
|
-
* Returns `null` if no descendant matches.
|
|
6
|
-
*/
|
|
7
3
|
export function findNode(source, predicate) {
|
|
8
4
|
const children = getNodeRuntime(source).children;
|
|
9
5
|
if (children === null)
|
package/dist/traversal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"traversal.js","sourceRoot":"","sources":["../src/traversal.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"traversal.js","sourceRoot":"","sources":["../src/traversal.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAexC,MAAM,UAAU,QAAQ,CACtB,MAA8B,EAC9B,SAA0C;IAE1C,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;IACjD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAiB,CAAC;QAC1C,IAAI,SAAS,CAAC,KAAK,CAAC;YAAE,OAAO,KAAuB,CAAC;QACrD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QACzC,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;IACnC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,MAA8B,EAC9B,IAAY;IAEZ,OAAO,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AACxD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAA8B,EAC9B,QAAyC;IAEzC,IAAI,OAAO,GAAG,aAAa,CAAC,MAAsB,CAAC,CAAC;IACpD,OAAO,OAAO,KAAK,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO;QAC/B,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAA8B,EAC9B,QAAsC;IAEtC,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;IACjD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAiB,CAAC,CAAC;QACtC,qBAAqB,CAAC,QAAQ,CAAC,CAAC,CAAiB,EAAE,QAAQ,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,MAA8B;IAE9B,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;IACjD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,cAA2C,CAAC;IAC1E,OAAO,QAAQ,CAAC,KAAK,EAAsB,CAAC;AAC9C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAqC,MAA8B;IAC7F,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,OAAO,GAAG,aAAa,CAAC,MAAsB,CAAC,CAAC;IACpD,OAAO,OAAO,KAAK,IAAI,EAAE,CAAC;QACxB,KAAK,EAAE,CAAC;QACR,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAA8B;IAE9B,MAAM,MAAM,GAAG,aAAa,CAAC,MAAsB,CAAC,CAAC;IACrD,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;IACjD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAsB,CAAC,CAAC;IACrD,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,GAAG,KAAK,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3D,OAAO,QAAQ,CAAC,GAAG,GAAG,CAAC,CAAmB,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAA8B;IAE9B,MAAM,MAAM,GAAG,aAAa,CAAC,MAAsB,CAAC,CAAC;IACrD,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;IACjD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAsB,CAAC,CAAC;IACrD,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1B,OAAO,QAAQ,CAAC,GAAG,GAAG,CAAC,CAAmB,CAAC;AAC7C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAA8B,EAC9B,KAAoC;IAEpC,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;IACjD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAiB,CAAC;QAC1C,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAChC,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;IACvD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,cAAc,GAAqB,EAAE,CAAC"}
|
package/dist/viewport.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function computeViewportFillScale(contentWidth: number, contentHeight: number, viewWidth: number, viewHeight: number): number;
|
|
5
|
-
export declare function computeViewportFitScale(contentWidth: number, contentHeight: number, viewWidth: number, viewHeight: number): number;
|
|
6
|
-
export declare function computeViewportRenderTransform<Traits extends object = NodeTraits>(out: MatrixLike, scene: Viewport<Traits>, viewWidth: number, viewHeight: number): void;
|
|
7
|
-
export declare function createViewport<Traits extends object = NodeTraits>(obj?: Readonly<Partial<Viewport<Traits>>>): Viewport<Traits>;
|
|
1
|
+
import type { Viewport, ViewportLike } from '@flighthq/types';
|
|
2
|
+
export declare function createViewport(obj?: Readonly<ViewportLike>): Viewport;
|
|
3
|
+
export declare function getViewportAspect(viewport: Readonly<Viewport>): number;
|
|
8
4
|
//# sourceMappingURL=viewport.d.ts.map
|
package/dist/viewport.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewport.d.ts","sourceRoot":"","sources":["../src/viewport.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"viewport.d.ts","sourceRoot":"","sources":["../src/viewport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAK9D,wBAAgB,cAAc,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,QAAQ,CAQrE;AAID,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,MAAM,CAEtE"}
|
package/dist/viewport.js
CHANGED
|
@@ -1,79 +1,19 @@
|
|
|
1
1
|
import { createEntity } from '@flighthq/entity';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (align.includes('left'))
|
|
6
|
-
return 0;
|
|
7
|
-
if (align.includes('right'))
|
|
8
|
-
return viewWidth - scaledContentWidth;
|
|
9
|
-
return (viewWidth - scaledContentWidth) / 2;
|
|
10
|
-
}
|
|
11
|
-
export function computeViewportAlignY(scaledContentHeight, viewHeight, align) {
|
|
12
|
-
if (align.includes('top'))
|
|
13
|
-
return 0;
|
|
14
|
-
if (align.includes('bottom'))
|
|
15
|
-
return viewHeight - scaledContentHeight;
|
|
16
|
-
return (viewHeight - scaledContentHeight) / 2;
|
|
17
|
-
}
|
|
18
|
-
export function computeViewportFillScale(contentWidth, contentHeight, viewWidth, viewHeight) {
|
|
19
|
-
return Math.max(viewWidth / contentWidth, viewHeight / contentHeight);
|
|
20
|
-
}
|
|
21
|
-
export function computeViewportFitScale(contentWidth, contentHeight, viewWidth, viewHeight) {
|
|
22
|
-
return Math.min(viewWidth / contentWidth, viewHeight / contentHeight);
|
|
23
|
-
}
|
|
24
|
-
export function computeViewportRenderTransform(out, scene, viewWidth, viewHeight) {
|
|
25
|
-
let contentWidth = 0;
|
|
26
|
-
let contentHeight = 0;
|
|
27
|
-
if (scene.root !== null) {
|
|
28
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
-
const runtime = getNodeRuntime(scene.root);
|
|
30
|
-
if (runtime?.computeLocalBoundsRectangle !== undefined) {
|
|
31
|
-
_tempRectangle.width = 0;
|
|
32
|
-
_tempRectangle.height = 0;
|
|
33
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
-
runtime.computeLocalBoundsRectangle(_tempRectangle, scene.root);
|
|
35
|
-
contentWidth = _tempRectangle.width;
|
|
36
|
-
contentHeight = _tempRectangle.height;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (contentWidth === 0 || contentHeight === 0) {
|
|
40
|
-
out.a = 1;
|
|
41
|
-
out.b = 0;
|
|
42
|
-
out.c = 0;
|
|
43
|
-
out.d = 1;
|
|
44
|
-
out.tx = 0;
|
|
45
|
-
out.ty = 0;
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
let sx;
|
|
49
|
-
let sy;
|
|
50
|
-
if (scene.scaleMode === 'noscale') {
|
|
51
|
-
sx = 1;
|
|
52
|
-
sy = 1;
|
|
53
|
-
}
|
|
54
|
-
else if (scene.scaleMode === 'exactfit') {
|
|
55
|
-
sx = viewWidth / contentWidth;
|
|
56
|
-
sy = viewHeight / contentHeight;
|
|
57
|
-
}
|
|
58
|
-
else if (scene.scaleMode === 'showall') {
|
|
59
|
-
sx = sy = computeViewportFitScale(contentWidth, contentHeight, viewWidth, viewHeight);
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
sx = sy = computeViewportFillScale(contentWidth, contentHeight, viewWidth, viewHeight);
|
|
63
|
-
}
|
|
64
|
-
out.a = sx;
|
|
65
|
-
out.b = 0;
|
|
66
|
-
out.c = 0;
|
|
67
|
-
out.d = sy;
|
|
68
|
-
out.tx = computeViewportAlignX(contentWidth * sx, viewWidth, scene.align);
|
|
69
|
-
out.ty = computeViewportAlignY(contentHeight * sy, viewHeight, scene.align);
|
|
70
|
-
}
|
|
2
|
+
// Allocates a Viewport — the bedrock drawable rectangle a scene renders into. Defaults to a zero-origin
|
|
3
|
+
// rect at unit device-pixel ratio; pass fields to override. Passive plain data: a Viewport does not own a
|
|
4
|
+
// drawable (a renderable surface is a Viewport paired with a RenderTarget).
|
|
71
5
|
export function createViewport(obj) {
|
|
72
6
|
return createEntity({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
7
|
+
devicePixelRatio: obj?.devicePixelRatio ?? 1,
|
|
8
|
+
height: obj?.height ?? 0,
|
|
9
|
+
width: obj?.width ?? 0,
|
|
10
|
+
x: obj?.x ?? 0,
|
|
11
|
+
y: obj?.y ?? 0,
|
|
76
12
|
});
|
|
77
13
|
}
|
|
78
|
-
|
|
14
|
+
// Returns the viewport's aspect ratio (width / height), or 1 for a degenerate (zero-height) rect. A 3D
|
|
15
|
+
// camera reads its projection aspect from this (`setCamera3DAspect(camera, getViewportAspect(viewport))`).
|
|
16
|
+
export function getViewportAspect(viewport) {
|
|
17
|
+
return viewport.height !== 0 ? viewport.width / viewport.height : 1;
|
|
18
|
+
}
|
|
79
19
|
//# sourceMappingURL=viewport.js.map
|
package/dist/viewport.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewport.js","sourceRoot":"","sources":["../src/viewport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"viewport.js","sourceRoot":"","sources":["../src/viewport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGhD,wGAAwG;AACxG,0GAA0G;AAC1G,4EAA4E;AAC5E,MAAM,UAAU,cAAc,CAAC,GAA4B;IACzD,OAAO,YAAY,CAAC;QAClB,gBAAgB,EAAE,GAAG,EAAE,gBAAgB,IAAI,CAAC;QAC5C,MAAM,EAAE,GAAG,EAAE,MAAM,IAAI,CAAC;QACxB,KAAK,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;QACtB,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC;QACd,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC;KACf,CAAC,CAAC;AACL,CAAC;AAED,uGAAuG;AACvG,2GAA2G;AAC3G,MAAM,UAAU,iBAAiB,CAAC,QAA4B;IAC5D,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACtE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/node",
|
|
3
|
-
"version": "0.2.1-next.
|
|
3
|
+
"version": "0.2.1-next.444.a313478",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@flighthq/entity": "0.2.1-next.
|
|
36
|
-
"@flighthq/geometry": "0.2.1-next.
|
|
37
|
-
"@flighthq/signals": "0.2.1-next.
|
|
38
|
-
"@flighthq/types": "0.2.1-next.
|
|
35
|
+
"@flighthq/entity": "0.2.1-next.444.a313478",
|
|
36
|
+
"@flighthq/geometry": "0.2.1-next.444.a313478",
|
|
37
|
+
"@flighthq/signals": "0.2.1-next.444.a313478",
|
|
38
|
+
"@flighthq/types": "0.2.1-next.444.a313478"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^5.3.0"
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
2
|
+
import type { HasBoundsRectangleRuntime, Node, NodeTraits, ViewportAlign, ViewportScaleMode } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { createNode, getNodeRuntime } from './node';
|
|
5
|
+
import {
|
|
6
|
+
computeStageFitAlignX,
|
|
7
|
+
computeStageFitAlignY,
|
|
8
|
+
computeStageFitFillScale,
|
|
9
|
+
computeStageFitScale,
|
|
10
|
+
computeStageFitTransform,
|
|
11
|
+
} from './stageFit';
|
|
12
|
+
|
|
13
|
+
const TestNodeKind = 'TestNode';
|
|
14
|
+
|
|
15
|
+
function makeNodeWithBounds(width: number, height: number) {
|
|
16
|
+
const node = createNode(TestNodeKind);
|
|
17
|
+
const runtime = getNodeRuntime(node) as unknown as HasBoundsRectangleRuntime;
|
|
18
|
+
runtime.computeLocalBoundsRectangle = (out) => {
|
|
19
|
+
out.width = width;
|
|
20
|
+
out.height = height;
|
|
21
|
+
};
|
|
22
|
+
return node;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A structural stage-fit context (what Stage supplies): root plus how it maps into the view.
|
|
26
|
+
function fit(overrides: { align?: ViewportAlign; root?: Node<NodeTraits> | null; scaleMode?: ViewportScaleMode }): {
|
|
27
|
+
align: ViewportAlign;
|
|
28
|
+
root: Node<NodeTraits> | null;
|
|
29
|
+
scaleMode: ViewportScaleMode;
|
|
30
|
+
} {
|
|
31
|
+
return {
|
|
32
|
+
align: overrides.align ?? 'topleft',
|
|
33
|
+
root: overrides.root ?? null,
|
|
34
|
+
scaleMode: overrides.scaleMode ?? 'noscale',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('computeStageFitAlignX', () => {
|
|
39
|
+
it('returns 0 for left-anchored alignments', () => {
|
|
40
|
+
expect(computeStageFitAlignX(400, 800, 'left')).toBe(0);
|
|
41
|
+
expect(computeStageFitAlignX(400, 800, 'topleft')).toBe(0);
|
|
42
|
+
expect(computeStageFitAlignX(400, 800, 'bottomleft')).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns viewWidth - scaledWidth for right-anchored alignments', () => {
|
|
46
|
+
expect(computeStageFitAlignX(400, 800, 'right')).toBe(400);
|
|
47
|
+
expect(computeStageFitAlignX(400, 800, 'topright')).toBe(400);
|
|
48
|
+
expect(computeStageFitAlignX(400, 800, 'bottomright')).toBe(400);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns centered offset for top/bottom alignments', () => {
|
|
52
|
+
expect(computeStageFitAlignX(400, 800, 'top')).toBe(200);
|
|
53
|
+
expect(computeStageFitAlignX(400, 800, 'bottom')).toBe(200);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('computeStageFitAlignY', () => {
|
|
58
|
+
it('returns 0 for top-anchored alignments', () => {
|
|
59
|
+
expect(computeStageFitAlignY(300, 600, 'top')).toBe(0);
|
|
60
|
+
expect(computeStageFitAlignY(300, 600, 'topleft')).toBe(0);
|
|
61
|
+
expect(computeStageFitAlignY(300, 600, 'topright')).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns viewHeight - scaledHeight for bottom-anchored alignments', () => {
|
|
65
|
+
expect(computeStageFitAlignY(300, 600, 'bottom')).toBe(300);
|
|
66
|
+
expect(computeStageFitAlignY(300, 600, 'bottomleft')).toBe(300);
|
|
67
|
+
expect(computeStageFitAlignY(300, 600, 'bottomright')).toBe(300);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns centered offset for left/right alignments', () => {
|
|
71
|
+
expect(computeStageFitAlignY(300, 600, 'left')).toBe(150);
|
|
72
|
+
expect(computeStageFitAlignY(300, 600, 'right')).toBe(150);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('computeStageFitFillScale', () => {
|
|
77
|
+
it('returns max of width and height ratios', () => {
|
|
78
|
+
expect(computeStageFitFillScale(400, 300, 800, 600)).toBe(2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('uses the larger ratio when width ratio wins', () => {
|
|
82
|
+
expect(computeStageFitFillScale(400, 300, 800, 400)).toBeCloseTo(2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('uses the larger ratio when height ratio wins', () => {
|
|
86
|
+
expect(computeStageFitFillScale(400, 300, 400, 600)).toBe(2);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('computeStageFitScale', () => {
|
|
91
|
+
it('returns min of width and height ratios', () => {
|
|
92
|
+
expect(computeStageFitScale(400, 300, 800, 600)).toBe(2);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('uses the smaller ratio when width ratio wins', () => {
|
|
96
|
+
expect(computeStageFitScale(400, 300, 400, 600)).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('uses the smaller ratio when height ratio wins', () => {
|
|
100
|
+
expect(computeStageFitScale(400, 300, 800, 400)).toBeCloseTo(400 / 300);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('computeStageFitTransform', () => {
|
|
105
|
+
it('sets identity when root is null', () => {
|
|
106
|
+
const m = createMatrix();
|
|
107
|
+
computeStageFitTransform(m, fit({}), 800, 600);
|
|
108
|
+
expect(m.a).toBe(1);
|
|
109
|
+
expect(m.b).toBe(0);
|
|
110
|
+
expect(m.c).toBe(0);
|
|
111
|
+
expect(m.d).toBe(1);
|
|
112
|
+
expect(m.tx).toBe(0);
|
|
113
|
+
expect(m.ty).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('sets identity when root has no bounds capability', () => {
|
|
117
|
+
const m = createMatrix();
|
|
118
|
+
const root = createNode(TestNodeKind);
|
|
119
|
+
computeStageFitTransform(m, fit({ root }), 800, 600);
|
|
120
|
+
expect(m.a).toBe(1);
|
|
121
|
+
expect(m.d).toBe(1);
|
|
122
|
+
expect(m.tx).toBe(0);
|
|
123
|
+
expect(m.ty).toBe(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('sets identity when root computeLocalBoundsRectangle returns zero size', () => {
|
|
127
|
+
const m = createMatrix();
|
|
128
|
+
const root = makeNodeWithBounds(0, 0);
|
|
129
|
+
computeStageFitTransform(m, fit({ root }), 800, 600);
|
|
130
|
+
expect(m.a).toBe(1);
|
|
131
|
+
expect(m.d).toBe(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('noscale with topleft: identity scale at origin', () => {
|
|
135
|
+
const m = createMatrix();
|
|
136
|
+
computeStageFitTransform(
|
|
137
|
+
m,
|
|
138
|
+
fit({ align: 'topleft', root: makeNodeWithBounds(400, 300), scaleMode: 'noscale' }),
|
|
139
|
+
800,
|
|
140
|
+
600,
|
|
141
|
+
);
|
|
142
|
+
expect(m.a).toBe(1);
|
|
143
|
+
expect(m.d).toBe(1);
|
|
144
|
+
expect(m.tx).toBe(0);
|
|
145
|
+
expect(m.ty).toBe(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('noscale with top alignment: centers horizontally', () => {
|
|
149
|
+
const m = createMatrix();
|
|
150
|
+
computeStageFitTransform(
|
|
151
|
+
m,
|
|
152
|
+
fit({ align: 'top', root: makeNodeWithBounds(400, 300), scaleMode: 'noscale' }),
|
|
153
|
+
800,
|
|
154
|
+
600,
|
|
155
|
+
);
|
|
156
|
+
expect(m.a).toBe(1);
|
|
157
|
+
expect(m.d).toBe(1);
|
|
158
|
+
expect(m.tx).toBe(200);
|
|
159
|
+
expect(m.ty).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('exactfit: scales to fill viewport exactly', () => {
|
|
163
|
+
const m = createMatrix();
|
|
164
|
+
computeStageFitTransform(
|
|
165
|
+
m,
|
|
166
|
+
fit({ align: 'topleft', root: makeNodeWithBounds(400, 300), scaleMode: 'exactfit' }),
|
|
167
|
+
800,
|
|
168
|
+
600,
|
|
169
|
+
);
|
|
170
|
+
expect(m.a).toBe(2);
|
|
171
|
+
expect(m.d).toBe(2);
|
|
172
|
+
expect(m.tx).toBe(0);
|
|
173
|
+
expect(m.ty).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('exactfit: uses independent x/y scales', () => {
|
|
177
|
+
const m = createMatrix();
|
|
178
|
+
computeStageFitTransform(
|
|
179
|
+
m,
|
|
180
|
+
fit({ align: 'topleft', root: makeNodeWithBounds(400, 300), scaleMode: 'exactfit' }),
|
|
181
|
+
800,
|
|
182
|
+
450,
|
|
183
|
+
);
|
|
184
|
+
expect(m.a).toBe(2);
|
|
185
|
+
expect(m.d).toBe(1.5);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('showall: fits content within viewport with uniform scale', () => {
|
|
189
|
+
const m = createMatrix();
|
|
190
|
+
computeStageFitTransform(
|
|
191
|
+
m,
|
|
192
|
+
fit({ align: 'topleft', root: makeNodeWithBounds(400, 300), scaleMode: 'showall' }),
|
|
193
|
+
800,
|
|
194
|
+
400,
|
|
195
|
+
);
|
|
196
|
+
expect(m.a).toBeCloseTo(400 / 300);
|
|
197
|
+
expect(m.d).toBeCloseTo(400 / 300);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('noborder: fills viewport with uniform scale', () => {
|
|
201
|
+
const m = createMatrix();
|
|
202
|
+
computeStageFitTransform(
|
|
203
|
+
m,
|
|
204
|
+
fit({ align: 'topleft', root: makeNodeWithBounds(400, 300), scaleMode: 'noborder' }),
|
|
205
|
+
800,
|
|
206
|
+
400,
|
|
207
|
+
);
|
|
208
|
+
expect(m.a).toBe(2);
|
|
209
|
+
expect(m.d).toBe(2);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('sets b and c to 0', () => {
|
|
213
|
+
const m = createMatrix();
|
|
214
|
+
computeStageFitTransform(
|
|
215
|
+
m,
|
|
216
|
+
fit({ align: 'topleft', root: makeNodeWithBounds(400, 300), scaleMode: 'showall' }),
|
|
217
|
+
800,
|
|
218
|
+
600,
|
|
219
|
+
);
|
|
220
|
+
expect(m.b).toBe(0);
|
|
221
|
+
expect(m.c).toBe(0);
|
|
222
|
+
});
|
|
223
|
+
});
|
package/src/traversal.test.ts
CHANGED
|
@@ -65,6 +65,19 @@ describe('findNode', () => {
|
|
|
65
65
|
const result = findNode(root, (n) => n === root);
|
|
66
66
|
expect(result).toBeNull();
|
|
67
67
|
});
|
|
68
|
+
|
|
69
|
+
it('narrows the result to the guarded type when the predicate is a type guard', () => {
|
|
70
|
+
interface TaggedNode extends Node<NodeTraits> {
|
|
71
|
+
tag: string;
|
|
72
|
+
}
|
|
73
|
+
const isTagged = (n: Node<NodeTraits>): n is TaggedNode => (n as Partial<TaggedNode>).tag === 'tagged';
|
|
74
|
+
(childA as TaggedNode).tag = 'tagged';
|
|
75
|
+
const result = findNode(root, isTagged);
|
|
76
|
+
// Compile-time proof of the guard overload: `result` is `TaggedNode | null`, so reading `.tag`
|
|
77
|
+
// needs no cast. Would not typecheck if findNode returned the un-narrowed NodeOf.
|
|
78
|
+
expect(result?.tag).toBe('tagged');
|
|
79
|
+
expect(result).toBe(childA);
|
|
80
|
+
});
|
|
68
81
|
});
|
|
69
82
|
|
|
70
83
|
describe('findNodeByName', () => {
|
package/src/viewport.test.ts
CHANGED
|
@@ -1,228 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { HasBoundsRectangleRuntime } from '@flighthq/types';
|
|
1
|
+
import { createViewport, getViewportAspect } from './viewport';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from './viewport';
|
|
13
|
-
|
|
14
|
-
const TestNodeKind = 'TestNode';
|
|
15
|
-
|
|
16
|
-
function makeNodeWithBounds(width: number, height: number) {
|
|
17
|
-
const node = createNode(TestNodeKind);
|
|
18
|
-
const runtime = getNodeRuntime(node) as unknown as HasBoundsRectangleRuntime;
|
|
19
|
-
runtime.computeLocalBoundsRectangle = (out) => {
|
|
20
|
-
out.width = width;
|
|
21
|
-
out.height = height;
|
|
22
|
-
};
|
|
23
|
-
return node;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe('computeViewportAlignX', () => {
|
|
27
|
-
it('returns 0 for left-anchored alignments', () => {
|
|
28
|
-
expect(computeViewportAlignX(400, 800, 'left')).toBe(0);
|
|
29
|
-
expect(computeViewportAlignX(400, 800, 'topleft')).toBe(0);
|
|
30
|
-
expect(computeViewportAlignX(400, 800, 'bottomleft')).toBe(0);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('returns viewWidth - scaledWidth for right-anchored alignments', () => {
|
|
34
|
-
expect(computeViewportAlignX(400, 800, 'right')).toBe(400);
|
|
35
|
-
expect(computeViewportAlignX(400, 800, 'topright')).toBe(400);
|
|
36
|
-
expect(computeViewportAlignX(400, 800, 'bottomright')).toBe(400);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('returns centered offset for top/bottom alignments', () => {
|
|
40
|
-
expect(computeViewportAlignX(400, 800, 'top')).toBe(200);
|
|
41
|
-
expect(computeViewportAlignX(400, 800, 'bottom')).toBe(200);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('computeViewportAlignY', () => {
|
|
46
|
-
it('returns 0 for top-anchored alignments', () => {
|
|
47
|
-
expect(computeViewportAlignY(300, 600, 'top')).toBe(0);
|
|
48
|
-
expect(computeViewportAlignY(300, 600, 'topleft')).toBe(0);
|
|
49
|
-
expect(computeViewportAlignY(300, 600, 'topright')).toBe(0);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('returns viewHeight - scaledHeight for bottom-anchored alignments', () => {
|
|
53
|
-
expect(computeViewportAlignY(300, 600, 'bottom')).toBe(300);
|
|
54
|
-
expect(computeViewportAlignY(300, 600, 'bottomleft')).toBe(300);
|
|
55
|
-
expect(computeViewportAlignY(300, 600, 'bottomright')).toBe(300);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('returns centered offset for left/right alignments', () => {
|
|
59
|
-
expect(computeViewportAlignY(300, 600, 'left')).toBe(150);
|
|
60
|
-
expect(computeViewportAlignY(300, 600, 'right')).toBe(150);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('computeViewportFillScale', () => {
|
|
65
|
-
it('returns max of width and height ratios', () => {
|
|
66
|
-
expect(computeViewportFillScale(400, 300, 800, 600)).toBe(2);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('uses the larger ratio when width ratio wins', () => {
|
|
70
|
-
expect(computeViewportFillScale(400, 300, 800, 400)).toBeCloseTo(2);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('uses the larger ratio when height ratio wins', () => {
|
|
74
|
-
expect(computeViewportFillScale(400, 300, 400, 600)).toBe(2);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe('computeViewportFitScale', () => {
|
|
79
|
-
it('returns min of width and height ratios', () => {
|
|
80
|
-
expect(computeViewportFitScale(400, 300, 800, 600)).toBe(2);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('uses the smaller ratio when width ratio wins', () => {
|
|
84
|
-
expect(computeViewportFitScale(400, 300, 400, 600)).toBe(1);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('uses the smaller ratio when height ratio wins', () => {
|
|
88
|
-
expect(computeViewportFitScale(400, 300, 800, 400)).toBeCloseTo(400 / 300);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('computeViewportRenderTransform', () => {
|
|
93
|
-
it('sets identity when root is null', () => {
|
|
94
|
-
const m = createMatrix();
|
|
95
|
-
computeViewportRenderTransform(m, createViewport(), 800, 600);
|
|
96
|
-
expect(m.a).toBe(1);
|
|
97
|
-
expect(m.b).toBe(0);
|
|
98
|
-
expect(m.c).toBe(0);
|
|
99
|
-
expect(m.d).toBe(1);
|
|
100
|
-
expect(m.tx).toBe(0);
|
|
101
|
-
expect(m.ty).toBe(0);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('sets identity when root has no bounds capability', () => {
|
|
105
|
-
const m = createMatrix();
|
|
106
|
-
const root = createNode(TestNodeKind);
|
|
107
|
-
computeViewportRenderTransform(m, createViewport({ root }), 800, 600);
|
|
108
|
-
expect(m.a).toBe(1);
|
|
109
|
-
expect(m.d).toBe(1);
|
|
110
|
-
expect(m.tx).toBe(0);
|
|
111
|
-
expect(m.ty).toBe(0);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('sets identity when root computeLocalBoundsRectangle returns zero size', () => {
|
|
115
|
-
const m = createMatrix();
|
|
116
|
-
const root = makeNodeWithBounds(0, 0);
|
|
117
|
-
computeViewportRenderTransform(m, createViewport({ root }), 800, 600);
|
|
118
|
-
expect(m.a).toBe(1);
|
|
119
|
-
expect(m.d).toBe(1);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('noscale with topleft: identity scale at origin', () => {
|
|
123
|
-
const m = createMatrix();
|
|
124
|
-
computeViewportRenderTransform(
|
|
125
|
-
m,
|
|
126
|
-
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'noscale', align: 'topleft' }),
|
|
127
|
-
800,
|
|
128
|
-
600,
|
|
129
|
-
);
|
|
130
|
-
expect(m.a).toBe(1);
|
|
131
|
-
expect(m.d).toBe(1);
|
|
132
|
-
expect(m.tx).toBe(0);
|
|
133
|
-
expect(m.ty).toBe(0);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('noscale with top alignment: centers horizontally', () => {
|
|
137
|
-
const m = createMatrix();
|
|
138
|
-
computeViewportRenderTransform(
|
|
139
|
-
m,
|
|
140
|
-
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'noscale', align: 'top' }),
|
|
141
|
-
800,
|
|
142
|
-
600,
|
|
143
|
-
);
|
|
144
|
-
expect(m.a).toBe(1);
|
|
145
|
-
expect(m.d).toBe(1);
|
|
146
|
-
expect(m.tx).toBe(200);
|
|
147
|
-
expect(m.ty).toBe(0);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('exactfit: scales to fill viewport exactly', () => {
|
|
151
|
-
const m = createMatrix();
|
|
152
|
-
computeViewportRenderTransform(
|
|
153
|
-
m,
|
|
154
|
-
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'exactfit', align: 'topleft' }),
|
|
155
|
-
800,
|
|
156
|
-
600,
|
|
157
|
-
);
|
|
158
|
-
expect(m.a).toBe(2);
|
|
159
|
-
expect(m.d).toBe(2);
|
|
160
|
-
expect(m.tx).toBe(0);
|
|
161
|
-
expect(m.ty).toBe(0);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('exactfit: uses independent x/y scales', () => {
|
|
165
|
-
const m = createMatrix();
|
|
166
|
-
computeViewportRenderTransform(
|
|
167
|
-
m,
|
|
168
|
-
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'exactfit', align: 'topleft' }),
|
|
169
|
-
800,
|
|
170
|
-
450,
|
|
171
|
-
);
|
|
172
|
-
expect(m.a).toBe(2);
|
|
173
|
-
expect(m.d).toBe(1.5);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('showall: fits content within viewport with uniform scale', () => {
|
|
177
|
-
const m = createMatrix();
|
|
178
|
-
// viewport 800x400, content 400x300: min(2, 400/300) = 400/300
|
|
179
|
-
computeViewportRenderTransform(
|
|
180
|
-
m,
|
|
181
|
-
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'showall', align: 'topleft' }),
|
|
182
|
-
800,
|
|
183
|
-
400,
|
|
184
|
-
);
|
|
185
|
-
expect(m.a).toBeCloseTo(400 / 300);
|
|
186
|
-
expect(m.d).toBeCloseTo(400 / 300);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('noborder: fills viewport with uniform scale', () => {
|
|
190
|
-
const m = createMatrix();
|
|
191
|
-
// viewport 800x400, content 400x300: max(2, 400/300) = 2
|
|
192
|
-
computeViewportRenderTransform(
|
|
193
|
-
m,
|
|
194
|
-
createViewport({ root: makeNodeWithBounds(400, 300), scaleMode: 'noborder', align: 'topleft' }),
|
|
195
|
-
800,
|
|
196
|
-
400,
|
|
197
|
-
);
|
|
198
|
-
expect(m.a).toBe(2);
|
|
199
|
-
expect(m.d).toBe(2);
|
|
3
|
+
describe('createViewport', () => {
|
|
4
|
+
it('returns a zero rect at unit device-pixel ratio by default', () => {
|
|
5
|
+
const viewport = createViewport();
|
|
6
|
+
expect(viewport.x).toBe(0);
|
|
7
|
+
expect(viewport.y).toBe(0);
|
|
8
|
+
expect(viewport.width).toBe(0);
|
|
9
|
+
expect(viewport.height).toBe(0);
|
|
10
|
+
expect(viewport.devicePixelRatio).toBe(1);
|
|
200
11
|
});
|
|
201
12
|
|
|
202
|
-
it('
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
210
|
-
expect(m.b).toBe(0);
|
|
211
|
-
expect(m.c).toBe(0);
|
|
13
|
+
it('accepts overrides', () => {
|
|
14
|
+
const viewport = createViewport({ devicePixelRatio: 2, height: 600, width: 800, x: 10, y: 20 });
|
|
15
|
+
expect(viewport.x).toBe(10);
|
|
16
|
+
expect(viewport.y).toBe(20);
|
|
17
|
+
expect(viewport.width).toBe(800);
|
|
18
|
+
expect(viewport.height).toBe(600);
|
|
19
|
+
expect(viewport.devicePixelRatio).toBe(2);
|
|
212
20
|
});
|
|
213
21
|
});
|
|
214
22
|
|
|
215
|
-
describe('
|
|
216
|
-
it('returns
|
|
217
|
-
|
|
218
|
-
expect(scene.root).toBeNull();
|
|
219
|
-
expect(scene.scaleMode).toBe('noscale');
|
|
220
|
-
expect(scene.align).toBe('topleft');
|
|
23
|
+
describe('getViewportAspect', () => {
|
|
24
|
+
it('returns width / height for a non-degenerate rect', () => {
|
|
25
|
+
expect(getViewportAspect(createViewport({ height: 600, width: 800 }))).toBeCloseTo(800 / 600);
|
|
221
26
|
});
|
|
222
27
|
|
|
223
|
-
it('
|
|
224
|
-
|
|
225
|
-
expect(scene.scaleMode).toBe('showall');
|
|
226
|
-
expect(scene.align).toBe('top');
|
|
28
|
+
it('returns 1 for a zero-height rect', () => {
|
|
29
|
+
expect(getViewportAspect(createViewport({ height: 0, width: 800 }))).toBe(1);
|
|
227
30
|
});
|
|
228
31
|
});
|