@pixi-ui-editor/runtime-pixi 0.14.0 → 0.16.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/README.md +27 -6
- package/dist/SceneViewport.d.ts +5 -1
- package/dist/SceneViewport.d.ts.map +1 -1
- package/dist/SceneViewport.js +13 -3
- package/dist/SceneViewport.js.map +1 -1
- package/dist/assets/bitmapFonts.d.ts +2 -0
- package/dist/assets/bitmapFonts.d.ts.map +1 -1
- package/dist/assets/bitmapFonts.js +3 -1
- package/dist/assets/bitmapFonts.js.map +1 -1
- package/dist/assets/sounds.d.ts +1 -7
- package/dist/assets/sounds.d.ts.map +1 -1
- package/dist/assets/sounds.js +7 -90
- package/dist/assets/sounds.js.map +1 -1
- package/dist/assets/textures.d.ts +10 -0
- package/dist/assets/textures.d.ts.map +1 -1
- package/dist/assets/textures.js +10 -2
- package/dist/assets/textures.js.map +1 -1
- package/dist/audio/bus.d.ts +60 -0
- package/dist/audio/bus.d.ts.map +1 -0
- package/dist/audio/bus.js +38 -0
- package/dist/audio/bus.js.map +1 -0
- package/dist/audio/director.d.ts +35 -0
- package/dist/audio/director.d.ts.map +1 -0
- package/dist/audio/director.js +137 -0
- package/dist/audio/director.js.map +1 -0
- package/dist/controls.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +19 -3
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +34 -4
- package/dist/layout.js.map +1 -1
- package/dist/particles.js.map +1 -1
- package/dist/scene.d.ts +13 -2
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +79 -3
- package/dist/scene.js.map +1 -1
- package/dist/transformSpace.d.ts +119 -0
- package/dist/transformSpace.d.ts.map +1 -0
- package/dist/transformSpace.js +487 -0
- package/dist/transformSpace.js.map +1 -0
- package/dist/views/ButtonNodeView.d.ts +4 -3
- package/dist/views/ButtonNodeView.d.ts.map +1 -1
- package/dist/views/ButtonNodeView.js +9 -8
- package/dist/views/ButtonNodeView.js.map +1 -1
- package/dist/views/CheckboxNodeView.d.ts +4 -3
- package/dist/views/CheckboxNodeView.d.ts.map +1 -1
- package/dist/views/CheckboxNodeView.js +7 -7
- package/dist/views/CheckboxNodeView.js.map +1 -1
- package/dist/views/ItemTableNodeView.js.map +1 -1
- package/dist/views/PaginationNodeView.js.map +1 -1
- package/dist/views/ParticleEmitterNodeView.js.map +1 -1
- package/dist/views/StagedButtonNodeView.d.ts +4 -3
- package/dist/views/StagedButtonNodeView.d.ts.map +1 -1
- package/dist/views/StagedButtonNodeView.js +7 -9
- package/dist/views/StagedButtonNodeView.js.map +1 -1
- package/dist/views/basic.js.map +1 -1
- package/dist/views/createNodeView.d.ts +2 -2
- package/dist/views/createNodeView.d.ts.map +1 -1
- package/dist/views/createNodeView.js +4 -4
- package/dist/views/createNodeView.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type LayoutProfileId, type UINode } from "@pixi-ui-editor/schema";
|
|
2
|
+
import { type LayoutSize } from "./layout.js";
|
|
3
|
+
export type AffineTransform = {
|
|
4
|
+
a: number;
|
|
5
|
+
b: number;
|
|
6
|
+
c: number;
|
|
7
|
+
d: number;
|
|
8
|
+
tx: number;
|
|
9
|
+
ty: number;
|
|
10
|
+
};
|
|
11
|
+
/** Viewport pair a scene carries; a prefab being edited on its own has none. */
|
|
12
|
+
export type NodeHierarchyLayout = {
|
|
13
|
+
readonly referenceViewports: Record<LayoutProfileId, LayoutSize>;
|
|
14
|
+
readonly preferredViewports: Record<LayoutProfileId, LayoutSize>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Любая иерархия авторинга, по которой резолвятся родители. Владелец обязан отдавать индекс, а не
|
|
18
|
+
* список: линейный поиск родителя делал обход всей иерархии квадратичным.
|
|
19
|
+
*/
|
|
20
|
+
export type NodeHierarchy = {
|
|
21
|
+
readonly nodesById: ReadonlyMap<string, UINode>;
|
|
22
|
+
readonly layout?: NodeHierarchyLayout;
|
|
23
|
+
};
|
|
24
|
+
/** Подстановка для иерархии без собственного viewport: якорные слагаемые обнуляются. */
|
|
25
|
+
export declare const NO_PARENT_LAYOUT_SIZE: LayoutSize;
|
|
26
|
+
export declare const IDENTITY_TRANSFORM: AffineTransform;
|
|
27
|
+
/** Единственный индекс id → узел. */
|
|
28
|
+
export declare function indexNodesById(nodes: readonly UINode[]): ReadonlyMap<string, UINode>;
|
|
29
|
+
/** Оборачивает сырого владельца узлов (сцену, пресет) в форму, которую принимают helpers иерархии. */
|
|
30
|
+
export declare function indexNodes(owner: {
|
|
31
|
+
readonly nodes: readonly UINode[];
|
|
32
|
+
readonly layout?: NodeHierarchyLayout;
|
|
33
|
+
}): NodeHierarchy;
|
|
34
|
+
export declare function multiplyAffine(left: AffineTransform, right: AffineTransform): AffineTransform;
|
|
35
|
+
export declare function invertAffine(matrix: AffineTransform): AffineTransform | undefined;
|
|
36
|
+
/** Матрица одного узла в системе координат его родителя — ровно то, что строит Pixi из transform. */
|
|
37
|
+
export declare function localTransformMatrix(transform: UINode["transform"]): AffineTransform;
|
|
38
|
+
/** Converts a point in scene coordinates into coordinates local to a parent's world matrix. */
|
|
39
|
+
export declare function worldPointToLocal(parentWorldMatrix: AffineTransform | undefined, point: {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
}): {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
} | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Converts a world matrix to a schema transform relative to a new parent.
|
|
48
|
+
* Undefined means the result needs skew (or an inverse of a zero-scale parent), neither of which
|
|
49
|
+
* the current document format can represent without changing the rendered object.
|
|
50
|
+
*/
|
|
51
|
+
export declare function transformRelativeToParent(worldMatrix: AffineTransform, parentWorldMatrix: AffineTransform | undefined, source: UINode["transform"]): UINode["transform"] | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Resolves the rectangle that Yoga gives a direct GridLayout child. The authored x/y, anchors and
|
|
54
|
+
* size do not participate while the child is in the grid, so hierarchy reparenting must preserve
|
|
55
|
+
* this managed rectangle rather than the dormant authored transform.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveGridManagedTransform(node: UINode, parent: Extract<UINode, {
|
|
58
|
+
type: "grid-layout";
|
|
59
|
+
}>, profile: LayoutProfileId, parentSize: LayoutSize): UINode["transform"];
|
|
60
|
+
/**
|
|
61
|
+
* Снимок системы координат одной иерархии в одном layout-профиле: мировые матрицы, отрисованные
|
|
62
|
+
* прямоугольники и перенос узла к другому родителю без визуального сдвига.
|
|
63
|
+
*
|
|
64
|
+
* Экземпляр кэширует всё, что посчитал, поэтому он действителен ровно до следующей мутации узлов —
|
|
65
|
+
* после правки документа создавайте новый. Это единственная реализация: и редактор, и рантайм
|
|
66
|
+
* обязаны считать репарент здесь, иначе картинки разъедутся.
|
|
67
|
+
*/
|
|
68
|
+
export declare class NodeTransformSpace {
|
|
69
|
+
#private;
|
|
70
|
+
constructor(hierarchy: NodeHierarchy, profile: LayoutProfileId);
|
|
71
|
+
/** Snapshot for the same hierarchy in another profile; каждый профиль кэшируется отдельно. */
|
|
72
|
+
static forOwner(owner: {
|
|
73
|
+
readonly nodes: readonly UINode[];
|
|
74
|
+
readonly layout?: NodeHierarchyLayout;
|
|
75
|
+
}, profile: LayoutProfileId): NodeTransformSpace;
|
|
76
|
+
get profile(): LayoutProfileId;
|
|
77
|
+
/** Логический RectTransform-размер Canvas; `undefined` у иерархии без собственного viewport. */
|
|
78
|
+
get canvasSize(): LayoutSize | undefined;
|
|
79
|
+
/** Matrix of the Unity-like scaled Canvas that owns top-level nodes. */
|
|
80
|
+
getOwnerWorldMatrix(): AffineTransform | undefined;
|
|
81
|
+
/** Returns the node rectangle that is actually rendered, including a layout parent's managed cell. */
|
|
82
|
+
getRenderedTransform(nodeId: string): UINode["transform"] | undefined;
|
|
83
|
+
/** Returns the same world matrix Pixi builds from the serialized parent chain. */
|
|
84
|
+
getWorldMatrix(nodeId: string): AffineTransform | undefined;
|
|
85
|
+
/** Scene-space point converted into the coordinates of `parentId`'s children. */
|
|
86
|
+
worldPointToLocal(parentId: string | null, point: {
|
|
87
|
+
x: number;
|
|
88
|
+
y: number;
|
|
89
|
+
}): {
|
|
90
|
+
x: number;
|
|
91
|
+
y: number;
|
|
92
|
+
} | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* Логический размер, относительно которого разрешается anchoredPosition узла.
|
|
95
|
+
*
|
|
96
|
+
* Узел может быть черновиком мутации (его ещё нет в иерархии) — тогда берётся его собственный
|
|
97
|
+
* parentId. `undefined` означает «у иерархии нет своего viewport» (редактируемый пресет): это не
|
|
98
|
+
* то же самое, что нулевой размер, `resolveAnchoredTransform` в этом случае не трогает transform.
|
|
99
|
+
*/
|
|
100
|
+
getParentLayoutSize(node: UINode): LayoutSize | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* Размер, который получает ЛЮБОЙ прямой ребёнок узла `parentId` (null — корень иерархии).
|
|
103
|
+
* Именно его требует перенос: якорные слагаемые ребёнка должны сниматься с размера будущего
|
|
104
|
+
* родителя, а не текущего.
|
|
105
|
+
*/
|
|
106
|
+
getChildLayoutSize(parentId: string | null): LayoutSize | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* Transform, который сохраняет узел на экране после переноса под `newParentId` — то, что нужно
|
|
109
|
+
* записать в документ. `undefined`, если сохранить картинку нельзя: перенос потребовал бы skew
|
|
110
|
+
* либо родитель невырожденно не обращается (нулевой масштаб).
|
|
111
|
+
*/
|
|
112
|
+
getReparentedTransform(nodeId: string, newParentId: string | null): UINode["transform"] | undefined;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Разовый запрос размера родителя без явного снимка: удобен на единичных вызовах.
|
|
116
|
+
* В цикле по узлам создавайте один `NodeTransformSpace` — он переиспользует кэши.
|
|
117
|
+
*/
|
|
118
|
+
export declare function getParentLayoutSize(hierarchy: NodeHierarchy, node: UINode, profile: LayoutProfileId): LayoutSize | undefined;
|
|
119
|
+
//# sourceMappingURL=transformSpace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformSpace.d.ts","sourceRoot":"","sources":["../src/transformSpace.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { resolveLayoutGroupSettings, } from "@pixi-ui-editor/schema";
|
|
2
|
+
import { getCanvasLogicalSize, getCanvasScale, resolveAnchoredTransform, resolveProfileTransform } from "./layout.js";
|
|
3
|
+
import { resolveGridFlow } from "./layoutGroups.js";
|
|
4
|
+
/** Подстановка для иерархии без собственного viewport: якорные слагаемые обнуляются. */
|
|
5
|
+
export const NO_PARENT_LAYOUT_SIZE = { width: 0, height: 0 };
|
|
6
|
+
export const IDENTITY_TRANSFORM = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
7
|
+
const EPSILON = 1e-8;
|
|
8
|
+
/** Единственный индекс id → узел. */
|
|
9
|
+
export function indexNodesById(nodes) {
|
|
10
|
+
return new Map(nodes.map((node) => [node.id, node]));
|
|
11
|
+
}
|
|
12
|
+
/** Оборачивает сырого владельца узлов (сцену, пресет) в форму, которую принимают helpers иерархии. */
|
|
13
|
+
export function indexNodes(owner) {
|
|
14
|
+
return owner.layout === undefined
|
|
15
|
+
? { nodesById: indexNodesById(owner.nodes) }
|
|
16
|
+
: { nodesById: indexNodesById(owner.nodes), layout: owner.layout };
|
|
17
|
+
}
|
|
18
|
+
export function multiplyAffine(left, right) {
|
|
19
|
+
return {
|
|
20
|
+
a: left.a * right.a + left.c * right.b,
|
|
21
|
+
b: left.b * right.a + left.d * right.b,
|
|
22
|
+
c: left.a * right.c + left.c * right.d,
|
|
23
|
+
d: left.b * right.c + left.d * right.d,
|
|
24
|
+
tx: left.a * right.tx + left.c * right.ty + left.tx,
|
|
25
|
+
ty: left.b * right.tx + left.d * right.ty + left.ty,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function invertAffine(matrix) {
|
|
29
|
+
const determinant = matrix.a * matrix.d - matrix.b * matrix.c;
|
|
30
|
+
if (Math.abs(determinant) < EPSILON)
|
|
31
|
+
return undefined;
|
|
32
|
+
return {
|
|
33
|
+
a: matrix.d / determinant,
|
|
34
|
+
b: -matrix.b / determinant,
|
|
35
|
+
c: -matrix.c / determinant,
|
|
36
|
+
d: matrix.a / determinant,
|
|
37
|
+
tx: (matrix.c * matrix.ty - matrix.d * matrix.tx) / determinant,
|
|
38
|
+
ty: (matrix.b * matrix.tx - matrix.a * matrix.ty) / determinant,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Матрица одного узла в системе координат его родителя — ровно то, что строит Pixi из transform. */
|
|
42
|
+
export function localTransformMatrix(transform) {
|
|
43
|
+
const cosine = Math.cos(transform.rotation);
|
|
44
|
+
const sine = Math.sin(transform.rotation);
|
|
45
|
+
const a = cosine * transform.scaleX;
|
|
46
|
+
const b = sine * transform.scaleX;
|
|
47
|
+
const c = -sine * transform.scaleY;
|
|
48
|
+
const d = cosine * transform.scaleY;
|
|
49
|
+
const pivotX = (transform.pivotX ?? 0) * transform.width;
|
|
50
|
+
const pivotY = (transform.pivotY ?? 0) * transform.height;
|
|
51
|
+
return { a, b, c, d, tx: transform.x - a * pivotX - c * pivotY, ty: transform.y - b * pivotX - d * pivotY };
|
|
52
|
+
}
|
|
53
|
+
/** Converts a point in scene coordinates into coordinates local to a parent's world matrix. */
|
|
54
|
+
export function worldPointToLocal(parentWorldMatrix, point) {
|
|
55
|
+
const inverseParent = parentWorldMatrix === undefined ? IDENTITY_TRANSFORM : invertAffine(parentWorldMatrix);
|
|
56
|
+
if (inverseParent === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
return {
|
|
59
|
+
x: inverseParent.a * point.x + inverseParent.c * point.y + inverseParent.tx,
|
|
60
|
+
y: inverseParent.b * point.x + inverseParent.d * point.y + inverseParent.ty,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function nearlyEqual(left, right) {
|
|
64
|
+
return Math.abs(left - right) <= 1e-6 * Math.max(1, Math.abs(left), Math.abs(right));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Converts a world matrix to a schema transform relative to a new parent.
|
|
68
|
+
* Undefined means the result needs skew (or an inverse of a zero-scale parent), neither of which
|
|
69
|
+
* the current document format can represent without changing the rendered object.
|
|
70
|
+
*/
|
|
71
|
+
export function transformRelativeToParent(worldMatrix, parentWorldMatrix, source) {
|
|
72
|
+
const inverseParent = parentWorldMatrix === undefined ? IDENTITY_TRANSFORM : invertAffine(parentWorldMatrix);
|
|
73
|
+
if (inverseParent === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const local = multiplyAffine(inverseParent, worldMatrix);
|
|
76
|
+
const scaleX = Math.hypot(local.a, local.b);
|
|
77
|
+
if (scaleX < EPSILON)
|
|
78
|
+
return undefined;
|
|
79
|
+
const rotation = Math.atan2(local.b, local.a);
|
|
80
|
+
const scaleY = (local.a * local.d - local.b * local.c) / scaleX;
|
|
81
|
+
const cosine = Math.cos(rotation);
|
|
82
|
+
const sine = Math.sin(rotation);
|
|
83
|
+
const reconstructed = {
|
|
84
|
+
a: cosine * scaleX,
|
|
85
|
+
b: sine * scaleX,
|
|
86
|
+
c: -sine * scaleY,
|
|
87
|
+
d: cosine * scaleY,
|
|
88
|
+
};
|
|
89
|
+
if (!nearlyEqual(local.a, reconstructed.a) || !nearlyEqual(local.b, reconstructed.b)
|
|
90
|
+
|| !nearlyEqual(local.c, reconstructed.c) || !nearlyEqual(local.d, reconstructed.d))
|
|
91
|
+
return undefined;
|
|
92
|
+
const pivotX = (source.pivotX ?? 0) * source.width;
|
|
93
|
+
const pivotY = (source.pivotY ?? 0) * source.height;
|
|
94
|
+
return {
|
|
95
|
+
...source,
|
|
96
|
+
x: local.tx + local.a * pivotX + local.c * pivotY,
|
|
97
|
+
y: local.ty + local.b * pivotX + local.d * pivotY,
|
|
98
|
+
scaleX,
|
|
99
|
+
scaleY,
|
|
100
|
+
rotation,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Доля свободного места до начала блока: 0 для start, 0.5 для center, 1 для end. */
|
|
104
|
+
function alignmentFraction(alignment, axis) {
|
|
105
|
+
if (axis === "horizontal")
|
|
106
|
+
return alignment.endsWith("right") ? 1 : alignment.endsWith("center") ? 0.5 : 0;
|
|
107
|
+
return alignment.startsWith("lower") ? 1 : alignment.startsWith("middle") ? 0.5 : 0;
|
|
108
|
+
}
|
|
109
|
+
function alignmentOffset(alignment, freeSpace, axis) {
|
|
110
|
+
return Math.max(0, freeSpace) * alignmentFraction(alignment, axis);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Смещение линии по одной оси. Свободное место считается по фактическому числу ячеек в линии,
|
|
114
|
+
* поэтому неполная последняя строка выравнивается сама по себе — ровно как justify-content
|
|
115
|
+
* во flex-wrap. Реверс оси меняет физический смысл выравнивания на противоположный, а
|
|
116
|
+
* отрицательное свободное место не зажимается: при переполнении Yoga уводит блок за край.
|
|
117
|
+
*/
|
|
118
|
+
function lineOffset(alignment, axis, reversed, inner, count, cell, spacing) {
|
|
119
|
+
const free = inner - (count * cell + Math.max(0, count - 1) * spacing);
|
|
120
|
+
const offset = free * alignmentFraction(alignment, axis);
|
|
121
|
+
return reversed ? free - offset : offset;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Раскладывает детей сетки по линиям переноса ровно так, как это делает Yoga: линия закрывается
|
|
125
|
+
* либо когда ячейки в неё больше не влезают, либо на границе, кратной constraint, где стоит
|
|
126
|
+
* элемент-разрыв. Поперечный зазор приходит только от разрыва, поэтому линии, закрытые ранним
|
|
127
|
+
* переносом, стоят вплотную — см. applyLayoutGroup.
|
|
128
|
+
*/
|
|
129
|
+
function gridLines(childCount, options) {
|
|
130
|
+
const lines = [];
|
|
131
|
+
let start = 0;
|
|
132
|
+
let crossOffset = 0;
|
|
133
|
+
while (start < childCount) {
|
|
134
|
+
let count = 0;
|
|
135
|
+
while (count < options.capacity && start + count < childCount) {
|
|
136
|
+
count += 1;
|
|
137
|
+
if (options.usesLineBreaks && (start + count) % options.constraintCount === 0)
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
lines.push({ start, count, crossOffset });
|
|
141
|
+
start += count;
|
|
142
|
+
crossOffset += options.crossCell;
|
|
143
|
+
const spaced = options.usesLineBreaks ? start % options.constraintCount === 0 : true;
|
|
144
|
+
if (start < childCount && spaced)
|
|
145
|
+
crossOffset += options.crossSpacing;
|
|
146
|
+
}
|
|
147
|
+
if (lines.length === 0)
|
|
148
|
+
lines.push({ start: 0, count: 1, crossOffset: 0 });
|
|
149
|
+
return lines;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolves the rectangle that Yoga gives a direct GridLayout child. The authored x/y, anchors and
|
|
153
|
+
* size do not participate while the child is in the grid, so hierarchy reparenting must preserve
|
|
154
|
+
* this managed rectangle rather than the dormant authored transform.
|
|
155
|
+
*/
|
|
156
|
+
export function resolveGridManagedTransform(node, parent, profile, parentSize) {
|
|
157
|
+
const settings = resolveLayoutGroupSettings(parent, profile);
|
|
158
|
+
const flow = resolveGridFlow(settings);
|
|
159
|
+
const childIndex = Math.max(0, parent.children.indexOf(node.id));
|
|
160
|
+
const childCount = Math.max(1, parent.children.length);
|
|
161
|
+
const innerWidth = Math.max(0, parentSize.width - settings.padding.left - settings.padding.right);
|
|
162
|
+
const innerHeight = Math.max(0, parentSize.height - settings.padding.top - settings.padding.bottom);
|
|
163
|
+
const fittingColumns = Math.max(1, Math.floor((innerWidth + settings.spacingX) / (settings.cellWidth + settings.spacingX)));
|
|
164
|
+
const fittingRows = Math.max(1, Math.floor((innerHeight + settings.spacingY) / (settings.cellHeight + settings.spacingY)));
|
|
165
|
+
const mainCell = flow.fillsByRow ? settings.cellWidth : settings.cellHeight;
|
|
166
|
+
const mainSpacing = flow.fillsByRow ? settings.spacingX : settings.spacingY;
|
|
167
|
+
const crossCell = flow.fillsByRow ? settings.cellHeight : settings.cellWidth;
|
|
168
|
+
const crossSpacing = flow.fillsByRow ? settings.spacingY : settings.spacingX;
|
|
169
|
+
const lines = gridLines(childCount, {
|
|
170
|
+
capacity: Math.max(1, flow.fillsByRow ? fittingColumns : fittingRows),
|
|
171
|
+
constraintCount: flow.usesLineBreaks ? Math.max(1, settings.constraintCount ?? 1) : Number.POSITIVE_INFINITY,
|
|
172
|
+
usesLineBreaks: flow.usesLineBreaks,
|
|
173
|
+
crossCell,
|
|
174
|
+
crossSpacing,
|
|
175
|
+
});
|
|
176
|
+
const line = lines.find((candidate) => childIndex < candidate.start + candidate.count) ?? lines[lines.length - 1];
|
|
177
|
+
const itemsInLine = line.count;
|
|
178
|
+
const indexInLine = Math.min(itemsInLine - 1, childIndex - line.start);
|
|
179
|
+
const slotInLine = flow.mainReversed ? itemsInLine - 1 - indexInLine : indexInLine;
|
|
180
|
+
const crossAxis = flow.fillsByRow ? "vertical" : "horizontal";
|
|
181
|
+
const crossTotal = flow.fillsByRow ? parentSize.height : parentSize.width;
|
|
182
|
+
const crossLead = flow.fillsByRow ? settings.padding.top : settings.padding.left;
|
|
183
|
+
const crossFree = (flow.fillsByRow ? innerHeight : innerWidth) - (lines[lines.length - 1].crossOffset + crossCell);
|
|
184
|
+
const crossAlign = crossFree * alignmentFraction(settings.childAlignment, crossAxis);
|
|
185
|
+
const mainPosition = (flow.fillsByRow ? settings.padding.left : settings.padding.top)
|
|
186
|
+
+ lineOffset(settings.childAlignment, flow.fillsByRow ? "horizontal" : "vertical", flow.mainReversed, flow.fillsByRow ? innerWidth : innerHeight, itemsInLine, mainCell, mainSpacing)
|
|
187
|
+
+ slotInLine * (mainCell + mainSpacing);
|
|
188
|
+
// wrap-reverse у Yoga отсчитывает линии от дальнего края, отступая ведущим padding, а не
|
|
189
|
+
// замыкающим; выравнивание при этом меряется от той же дальней кромки. Зеркало обязано повторять
|
|
190
|
+
// именно это поведение, иначе рамки выделения разъедутся с картинкой.
|
|
191
|
+
const crossPosition = flow.crossReversed
|
|
192
|
+
? crossTotal - crossLead - crossCell - crossAlign - line.crossOffset
|
|
193
|
+
: crossLead + crossAlign + line.crossOffset;
|
|
194
|
+
const cellX = flow.fillsByRow ? mainPosition : crossPosition;
|
|
195
|
+
const cellY = flow.fillsByRow ? crossPosition : mainPosition;
|
|
196
|
+
// Yoga кладёт результат на пиксельную сетку (pointScaleFactor = 1), округляя обе кромки ячейки.
|
|
197
|
+
// Зеркало округляет так же, иначе на дробных выравниваниях рамка уезжает на полпикселя.
|
|
198
|
+
const x = Math.round(cellX);
|
|
199
|
+
const y = Math.round(cellY);
|
|
200
|
+
const width = Math.round(cellX + settings.cellWidth) - x;
|
|
201
|
+
const height = Math.round(cellY + settings.cellHeight) - y;
|
|
202
|
+
const authored = resolveProfileTransform(node, profile).transform;
|
|
203
|
+
const pivotX = authored.pivotX ?? 0;
|
|
204
|
+
const pivotY = authored.pivotY ?? 0;
|
|
205
|
+
return { ...authored, x: x + pivotX * width, y: y + pivotY * height, width, height };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Раскладка ВСЕЙ линейной группы за один проход: Yoga решает линию целиком (grow/shrink делят одно
|
|
209
|
+
* общее свободное место), поэтому пересчитывать её на каждого ребёнка отдельно — квадратичная
|
|
210
|
+
* работа. `NodeTransformSpace` кэширует результат по родителю и берёт из него нужную ячейку.
|
|
211
|
+
*/
|
|
212
|
+
function resolveLinearManagedTransforms(parent, profile, parentSize, nodesById) {
|
|
213
|
+
const settings = resolveLayoutGroupSettings(parent, profile);
|
|
214
|
+
const horizontal = parent.type === "horizontal-layout";
|
|
215
|
+
const children = parent.children.map((id) => nodesById.get(id)).filter((child) => child !== undefined);
|
|
216
|
+
const innerMain = Math.max(0, (horizontal ? parentSize.width : parentSize.height)
|
|
217
|
+
- (horizontal ? settings.padding.left + settings.padding.right : settings.padding.top + settings.padding.bottom));
|
|
218
|
+
const innerCross = Math.max(0, (horizontal ? parentSize.height : parentSize.width)
|
|
219
|
+
- (horizontal ? settings.padding.top + settings.padding.bottom : settings.padding.left + settings.padding.right));
|
|
220
|
+
const items = children.map((child) => {
|
|
221
|
+
const authored = resolveProfileTransform(child, profile).transform;
|
|
222
|
+
const ownMain = horizontal ? authored.width : authored.height;
|
|
223
|
+
const ownCross = horizontal ? authored.height : authored.width;
|
|
224
|
+
const expandsMain = horizontal
|
|
225
|
+
? settings.controlChildWidth && settings.forceExpandWidth
|
|
226
|
+
: settings.controlChildHeight && settings.forceExpandHeight;
|
|
227
|
+
const expandsCross = horizontal
|
|
228
|
+
? settings.controlChildHeight && settings.forceExpandHeight
|
|
229
|
+
: settings.controlChildWidth && settings.forceExpandWidth;
|
|
230
|
+
const alignSelf = child.layoutItem?.alignSelf;
|
|
231
|
+
return {
|
|
232
|
+
child,
|
|
233
|
+
authored,
|
|
234
|
+
main: child.layoutItem?.flexBasis ?? ownMain,
|
|
235
|
+
cross: expandsCross && (alignSelf === undefined || alignSelf === "auto" || alignSelf === "stretch") ? innerCross : ownCross,
|
|
236
|
+
grow: expandsMain ? Math.max(1, child.layoutItem?.flexGrow ?? 0) : child.layoutItem?.flexGrow ?? 0,
|
|
237
|
+
shrink: child.layoutItem?.flexShrink ?? 0,
|
|
238
|
+
alignSelf,
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
const totalGap = Math.max(0, items.length - 1) * settings.spacing;
|
|
242
|
+
const initialMain = items.reduce((sum, item) => sum + item.main, 0);
|
|
243
|
+
const freeMain = innerMain - totalGap - initialMain;
|
|
244
|
+
if (freeMain > 0) {
|
|
245
|
+
const totalGrow = items.reduce((sum, item) => sum + item.grow, 0);
|
|
246
|
+
if (totalGrow > 0)
|
|
247
|
+
for (const item of items)
|
|
248
|
+
item.main += freeMain * item.grow / totalGrow;
|
|
249
|
+
}
|
|
250
|
+
else if (freeMain < 0) {
|
|
251
|
+
const totalShrinkWeight = items.reduce((sum, item) => sum + item.shrink * item.main, 0);
|
|
252
|
+
if (totalShrinkWeight > 0) {
|
|
253
|
+
for (const item of items)
|
|
254
|
+
item.main = Math.max(0, item.main + freeMain * item.shrink * item.main / totalShrinkWeight);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const occupiedMain = items.reduce((sum, item) => sum + item.main, 0) + totalGap;
|
|
258
|
+
const remainingMain = Math.max(0, innerMain - occupiedMain);
|
|
259
|
+
const physicalMainOffset = alignmentOffset(settings.childAlignment, remainingMain, horizontal ? "horizontal" : "vertical");
|
|
260
|
+
const blockOffset = settings.reverseOrder ? remainingMain - physicalMainOffset : physicalMainOffset;
|
|
261
|
+
const orderedItems = settings.reverseOrder ? [...items].reverse() : items;
|
|
262
|
+
const positions = new Map();
|
|
263
|
+
let cursor = (horizontal ? settings.padding.left : settings.padding.top) + blockOffset;
|
|
264
|
+
for (const item of orderedItems) {
|
|
265
|
+
positions.set(item.child.id, cursor);
|
|
266
|
+
cursor += item.main + settings.spacing;
|
|
267
|
+
}
|
|
268
|
+
const crossLead = horizontal ? settings.padding.top : settings.padding.left;
|
|
269
|
+
const transforms = new Map();
|
|
270
|
+
for (const item of items) {
|
|
271
|
+
const groupCrossOffset = alignmentOffset(settings.childAlignment, innerCross - item.cross, horizontal ? "vertical" : "horizontal");
|
|
272
|
+
const selfCrossOffset = item.alignSelf === "center"
|
|
273
|
+
? Math.max(0, innerCross - item.cross) / 2
|
|
274
|
+
: item.alignSelf === "flex-end"
|
|
275
|
+
? Math.max(0, innerCross - item.cross)
|
|
276
|
+
: item.alignSelf === "flex-start" || item.alignSelf === "stretch" ? 0 : groupCrossOffset;
|
|
277
|
+
const mainPosition = positions.get(item.child.id) ?? 0;
|
|
278
|
+
const crossPosition = crossLead + selfCrossOffset;
|
|
279
|
+
const width = horizontal ? item.main : item.cross;
|
|
280
|
+
const height = horizontal ? item.cross : item.main;
|
|
281
|
+
const x = horizontal ? mainPosition : crossPosition;
|
|
282
|
+
const y = horizontal ? crossPosition : mainPosition;
|
|
283
|
+
const pivotX = item.authored.pivotX ?? 0;
|
|
284
|
+
const pivotY = item.authored.pivotY ?? 0;
|
|
285
|
+
transforms.set(item.child.id, { ...item.authored, x: x + pivotX * width, y: y + pivotY * height, width, height });
|
|
286
|
+
}
|
|
287
|
+
return transforms;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Снимок системы координат одной иерархии в одном layout-профиле: мировые матрицы, отрисованные
|
|
291
|
+
* прямоугольники и перенос узла к другому родителю без визуального сдвига.
|
|
292
|
+
*
|
|
293
|
+
* Экземпляр кэширует всё, что посчитал, поэтому он действителен ровно до следующей мутации узлов —
|
|
294
|
+
* после правки документа создавайте новый. Это единственная реализация: и редактор, и рантайм
|
|
295
|
+
* обязаны считать репарент здесь, иначе картинки разъедутся.
|
|
296
|
+
*/
|
|
297
|
+
export class NodeTransformSpace {
|
|
298
|
+
#hierarchy;
|
|
299
|
+
#profile;
|
|
300
|
+
#canvasSize;
|
|
301
|
+
#canvasMatrix;
|
|
302
|
+
#resolved = new Map();
|
|
303
|
+
#parentSizes = new Map();
|
|
304
|
+
#linearLayouts = new Map();
|
|
305
|
+
#visiting = new Set();
|
|
306
|
+
constructor(hierarchy, profile) {
|
|
307
|
+
this.#hierarchy = hierarchy;
|
|
308
|
+
this.#profile = profile;
|
|
309
|
+
const layout = hierarchy.layout;
|
|
310
|
+
const scale = layout === undefined ? 1 : getCanvasScale(layout.referenceViewports[profile], layout.preferredViewports[profile]);
|
|
311
|
+
this.#canvasSize = layout === undefined ? undefined : getCanvasLogicalSize(layout.referenceViewports[profile], layout.preferredViewports[profile]);
|
|
312
|
+
this.#canvasMatrix = { a: scale, b: 0, c: 0, d: scale, tx: 0, ty: 0 };
|
|
313
|
+
}
|
|
314
|
+
/** Snapshot for the same hierarchy in another profile; каждый профиль кэшируется отдельно. */
|
|
315
|
+
static forOwner(owner, profile) {
|
|
316
|
+
return new NodeTransformSpace(indexNodes(owner), profile);
|
|
317
|
+
}
|
|
318
|
+
get profile() {
|
|
319
|
+
return this.#profile;
|
|
320
|
+
}
|
|
321
|
+
/** Логический RectTransform-размер Canvas; `undefined` у иерархии без собственного viewport. */
|
|
322
|
+
get canvasSize() {
|
|
323
|
+
return this.#canvasSize;
|
|
324
|
+
}
|
|
325
|
+
/** Matrix of the Unity-like scaled Canvas that owns top-level nodes. */
|
|
326
|
+
getOwnerWorldMatrix() {
|
|
327
|
+
return this.#hierarchy.layout === undefined ? undefined : this.#canvasMatrix;
|
|
328
|
+
}
|
|
329
|
+
/** Returns the node rectangle that is actually rendered, including a layout parent's managed cell. */
|
|
330
|
+
getRenderedTransform(nodeId) {
|
|
331
|
+
return this.#resolve(nodeId)?.transform;
|
|
332
|
+
}
|
|
333
|
+
/** Returns the same world matrix Pixi builds from the serialized parent chain. */
|
|
334
|
+
getWorldMatrix(nodeId) {
|
|
335
|
+
return this.#resolve(nodeId)?.matrix;
|
|
336
|
+
}
|
|
337
|
+
/** Scene-space point converted into the coordinates of `parentId`'s children. */
|
|
338
|
+
worldPointToLocal(parentId, point) {
|
|
339
|
+
return worldPointToLocal(parentId === null ? this.getOwnerWorldMatrix() : this.getWorldMatrix(parentId), point);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Логический размер, относительно которого разрешается anchoredPosition узла.
|
|
343
|
+
*
|
|
344
|
+
* Узел может быть черновиком мутации (его ещё нет в иерархии) — тогда берётся его собственный
|
|
345
|
+
* parentId. `undefined` означает «у иерархии нет своего viewport» (редактируемый пресет): это не
|
|
346
|
+
* то же самое, что нулевой размер, `resolveAnchoredTransform` в этом случае не трогает transform.
|
|
347
|
+
*/
|
|
348
|
+
getParentLayoutSize(node) {
|
|
349
|
+
return this.getChildLayoutSize(this.#hierarchy.nodesById.get(node.id)?.parentId ?? node.parentId);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Размер, который получает ЛЮБОЙ прямой ребёнок узла `parentId` (null — корень иерархии).
|
|
353
|
+
* Именно его требует перенос: якорные слагаемые ребёнка должны сниматься с размера будущего
|
|
354
|
+
* родителя, а не текущего.
|
|
355
|
+
*/
|
|
356
|
+
getChildLayoutSize(parentId) {
|
|
357
|
+
const cached = this.#parentSizes.get(parentId);
|
|
358
|
+
if (cached !== undefined || this.#parentSizes.has(parentId))
|
|
359
|
+
return cached;
|
|
360
|
+
const size = this.#computeChildLayoutSize(parentId);
|
|
361
|
+
this.#parentSizes.set(parentId, size);
|
|
362
|
+
return size;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Transform, который сохраняет узел на экране после переноса под `newParentId` — то, что нужно
|
|
366
|
+
* записать в документ. `undefined`, если сохранить картинку нельзя: перенос потребовал бы skew
|
|
367
|
+
* либо родитель невырожденно не обращается (нулевой масштаб).
|
|
368
|
+
*/
|
|
369
|
+
getReparentedTransform(nodeId, newParentId) {
|
|
370
|
+
const node = this.#hierarchy.nodesById.get(nodeId);
|
|
371
|
+
if (node === undefined)
|
|
372
|
+
return undefined;
|
|
373
|
+
const resolved = this.#resolve(nodeId);
|
|
374
|
+
if (resolved === undefined)
|
|
375
|
+
return undefined;
|
|
376
|
+
const parentWorldMatrix = newParentId === null ? this.getOwnerWorldMatrix() : this.getWorldMatrix(newParentId);
|
|
377
|
+
const preserved = transformRelativeToParent(resolved.matrix, parentWorldMatrix, resolved.transform);
|
|
378
|
+
if (preserved === undefined)
|
|
379
|
+
return undefined;
|
|
380
|
+
// Anchors живут в схеме как доли размера родителя, поэтому мировой transform, разложенный в
|
|
381
|
+
// локальный, ещё содержит вклад НОВОГО родителя: его нужно вычесть, чтобы authored-поля дали
|
|
382
|
+
// на экране ту же рамку.
|
|
383
|
+
const destinationSize = this.getChildLayoutSize(newParentId) ?? NO_PARENT_LAYOUT_SIZE;
|
|
384
|
+
const authored = resolveProfileTransform(node, this.#profile).transform;
|
|
385
|
+
const anchorMinX = authored.anchorMinX ?? 0;
|
|
386
|
+
const anchorMinY = authored.anchorMinY ?? 0;
|
|
387
|
+
const anchorMaxX = authored.anchorMaxX ?? anchorMinX;
|
|
388
|
+
const anchorMaxY = authored.anchorMaxY ?? anchorMinY;
|
|
389
|
+
const pivotX = authored.pivotX ?? 0;
|
|
390
|
+
const pivotY = authored.pivotY ?? 0;
|
|
391
|
+
const spanX = anchorMaxX - anchorMinX;
|
|
392
|
+
const spanY = anchorMaxY - anchorMinY;
|
|
393
|
+
return {
|
|
394
|
+
...preserved,
|
|
395
|
+
x: preserved.x - (anchorMinX + spanX * pivotX) * destinationSize.width,
|
|
396
|
+
y: preserved.y - (anchorMinY + spanY * pivotY) * destinationSize.height,
|
|
397
|
+
width: preserved.width - spanX * destinationSize.width,
|
|
398
|
+
height: preserved.height - spanY * destinationSize.height,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
#computeChildLayoutSize(parentId) {
|
|
402
|
+
if (parentId === null)
|
|
403
|
+
return this.#canvasSize;
|
|
404
|
+
const parent = this.#hierarchy.nodesById.get(parentId);
|
|
405
|
+
if (parent === undefined)
|
|
406
|
+
return this.#canvasSize;
|
|
407
|
+
if (this.#hierarchy.layout !== undefined && parent.parentId === null)
|
|
408
|
+
return this.#canvasSize;
|
|
409
|
+
// Прямой ребёнок grid-layout не владеет своим отрисованным размером: фиксированная ячейка Unity
|
|
410
|
+
// GridLayoutGroup полностью переопределяет authored transform (см. applyLayoutItem). Без этой
|
|
411
|
+
// ветки внуки грида (например текст внутри button-ячейки) якорились бы относительно устаревшего
|
|
412
|
+
// authored-размера родителя и не реагировали бы на изменение Cell width/height.
|
|
413
|
+
const grandparent = parent.parentId === null ? undefined : this.#hierarchy.nodesById.get(parent.parentId);
|
|
414
|
+
if (grandparent !== undefined && grandparent.type === "grid-layout") {
|
|
415
|
+
const grid = resolveLayoutGroupSettings(grandparent, this.#profile);
|
|
416
|
+
return { width: grid.cellWidth, height: grid.cellHeight };
|
|
417
|
+
}
|
|
418
|
+
// Тот же принцип для прямого ребёнка page-group (страницы): она не владеет собственным
|
|
419
|
+
// отрисованным размером — растягивается на весь resolved rectangle группы независимо от своих
|
|
420
|
+
// anchors (`PageItemContainer`/`NodeView.applyResolvedTransform`, managedByLayout). Её собственным
|
|
421
|
+
// детям нужен именно этот forced rectangle, а не anchor-resolved authored-размер страницы —
|
|
422
|
+
// иначе они якорятся относительно значения, которое страница на экране никогда не получит.
|
|
423
|
+
if (grandparent !== undefined && grandparent.type === "page-group")
|
|
424
|
+
return this.getChildLayoutSize(parent.parentId);
|
|
425
|
+
// Родитель может быть сам растянут anchors, поэтому его логический размер разрешается рекурсивно.
|
|
426
|
+
const transform = resolveAnchoredTransform(resolveProfileTransform(parent, this.#profile).transform, this.getChildLayoutSize(parent.parentId));
|
|
427
|
+
return { width: transform.width, height: transform.height };
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Один обход вверх по цепочке родителей на узел: и отрисованный прямоугольник, и мировая матрица
|
|
431
|
+
* берутся из него. Результат кэшируется, поэтому у ветки с общим предком предки считаются один раз.
|
|
432
|
+
*/
|
|
433
|
+
#resolve(nodeId) {
|
|
434
|
+
const cached = this.#resolved.get(nodeId);
|
|
435
|
+
if (cached !== undefined || this.#resolved.has(nodeId))
|
|
436
|
+
return cached;
|
|
437
|
+
if (this.#visiting.has(nodeId))
|
|
438
|
+
return undefined;
|
|
439
|
+
const node = this.#hierarchy.nodesById.get(nodeId);
|
|
440
|
+
if (node === undefined)
|
|
441
|
+
return undefined;
|
|
442
|
+
this.#visiting.add(nodeId);
|
|
443
|
+
const parentNode = node.parentId === null ? undefined : this.#hierarchy.nodesById.get(node.parentId);
|
|
444
|
+
const parent = node.parentId === null ? undefined : this.#resolve(node.parentId);
|
|
445
|
+
this.#visiting.delete(nodeId);
|
|
446
|
+
let result;
|
|
447
|
+
if (node.parentId === null || parent !== undefined) {
|
|
448
|
+
const parentSize = this.#hierarchy.layout !== undefined && parentNode?.parentId === null
|
|
449
|
+
? this.#canvasSize
|
|
450
|
+
: parent?.size ?? this.#canvasSize;
|
|
451
|
+
const managed = parentNode !== undefined && parent !== undefined
|
|
452
|
+
? this.#resolveManagedTransform(node, parentNode, parent.size)
|
|
453
|
+
: undefined;
|
|
454
|
+
const transform = managed ?? resolveAnchoredTransform(resolveProfileTransform(node, this.#profile).transform, parentSize);
|
|
455
|
+
result = {
|
|
456
|
+
transform,
|
|
457
|
+
matrix: multiplyAffine(parent?.matrix ?? this.#canvasMatrix, localTransformMatrix(transform)),
|
|
458
|
+
size: node.parentId === null && this.#hierarchy.layout !== undefined
|
|
459
|
+
? this.#canvasSize
|
|
460
|
+
: { width: transform.width, height: transform.height },
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
this.#resolved.set(nodeId, result);
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
/** Прямоугольник, который назначает ребёнку родитель-раскладка; `undefined` — позицией владеет сам узел. */
|
|
467
|
+
#resolveManagedTransform(node, parent, parentSize) {
|
|
468
|
+
if (parent.type === "grid-layout")
|
|
469
|
+
return resolveGridManagedTransform(node, parent, this.#profile, parentSize);
|
|
470
|
+
if (parent.type !== "horizontal-layout" && parent.type !== "vertical-layout")
|
|
471
|
+
return undefined;
|
|
472
|
+
let layout = this.#linearLayouts.get(parent.id);
|
|
473
|
+
if (layout === undefined) {
|
|
474
|
+
layout = resolveLinearManagedTransforms(parent, this.#profile, parentSize, this.#hierarchy.nodesById);
|
|
475
|
+
this.#linearLayouts.set(parent.id, layout);
|
|
476
|
+
}
|
|
477
|
+
return layout.get(node.id) ?? resolveProfileTransform(node, this.#profile).transform;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Разовый запрос размера родителя без явного снимка: удобен на единичных вызовах.
|
|
482
|
+
* В цикле по узлам создавайте один `NodeTransformSpace` — он переиспользует кэши.
|
|
483
|
+
*/
|
|
484
|
+
export function getParentLayoutSize(hierarchy, node, profile) {
|
|
485
|
+
return new NodeTransformSpace(hierarchy, profile).getParentLayoutSize(node);
|
|
486
|
+
}
|
|
487
|
+
//# sourceMappingURL=transformSpace.js.map
|