@godot-scene-web/layout 0.1.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/LICENSE +21 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1162 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1162 @@
|
|
|
1
|
+
import { asBoolean, asNumber, asResourceRef, asString, asVector2 } from "@godot-scene-web/core";
|
|
2
|
+
import { deriveNodeVisuals } from "@godot-scene-web/scene-graph";
|
|
3
|
+
//#region src/anchor-grammar.ts
|
|
4
|
+
const verticals = [
|
|
5
|
+
["contentbottom", "contentBottom"],
|
|
6
|
+
["contenttop", "contentTop"],
|
|
7
|
+
["vcenter", "vcenter"],
|
|
8
|
+
["bottom", "bottom"],
|
|
9
|
+
["top", "top"]
|
|
10
|
+
];
|
|
11
|
+
const horizontals = {
|
|
12
|
+
contentright: "contentRight",
|
|
13
|
+
contentleft: "contentLeft",
|
|
14
|
+
hcenter: "hcenter",
|
|
15
|
+
right: "right",
|
|
16
|
+
left: "left"
|
|
17
|
+
};
|
|
18
|
+
function parseAnchorEdge(token) {
|
|
19
|
+
const normalized = token.toLowerCase();
|
|
20
|
+
if (normalized === "center") return {
|
|
21
|
+
vertical: "vcenter",
|
|
22
|
+
horizontal: "hcenter"
|
|
23
|
+
};
|
|
24
|
+
for (const [key, vertical] of verticals) {
|
|
25
|
+
if (!normalized.startsWith(key)) continue;
|
|
26
|
+
const horizontal = horizontals[normalized.slice(key.length)];
|
|
27
|
+
if (horizontal) return {
|
|
28
|
+
vertical,
|
|
29
|
+
horizontal
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function anchorEdgePoint(token, own, content) {
|
|
34
|
+
const edge = parseAnchorEdge(token);
|
|
35
|
+
if (!edge) return void 0;
|
|
36
|
+
return {
|
|
37
|
+
x: edge.horizontal === "contentRight" ? content.x + content.width : edge.horizontal === "contentLeft" ? content.x : edge.horizontal === "hcenter" ? own.x + own.width / 2 : edge.horizontal === "right" ? own.x + own.width : own.x,
|
|
38
|
+
y: edge.vertical === "contentBottom" ? content.y + content.height : edge.vertical === "contentTop" ? content.y : edge.vertical === "vcenter" ? own.y + own.height / 2 : edge.vertical === "bottom" ? own.y + own.height : own.y
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/anchors.ts
|
|
43
|
+
const edgePoint = anchorEdgePoint;
|
|
44
|
+
/** Union of a node's visible direct children's rendered rects (its content box). */
|
|
45
|
+
function contentExtent(node, layoutByPath) {
|
|
46
|
+
let minX = Infinity;
|
|
47
|
+
let minY = Infinity;
|
|
48
|
+
let maxX = -Infinity;
|
|
49
|
+
let maxY = -Infinity;
|
|
50
|
+
let found = false;
|
|
51
|
+
for (const childPath of node.children) {
|
|
52
|
+
const child = layoutByPath.get(childPath);
|
|
53
|
+
if (!child || child.visible === false) continue;
|
|
54
|
+
const rect = child.renderedRect;
|
|
55
|
+
minX = Math.min(minX, rect.x);
|
|
56
|
+
minY = Math.min(minY, rect.y);
|
|
57
|
+
maxX = Math.max(maxX, rect.x + rect.width);
|
|
58
|
+
maxY = Math.max(maxY, rect.y + rect.height);
|
|
59
|
+
found = true;
|
|
60
|
+
}
|
|
61
|
+
if (!found) {
|
|
62
|
+
const { x, y } = node.renderedRect;
|
|
63
|
+
return {
|
|
64
|
+
x,
|
|
65
|
+
y,
|
|
66
|
+
width: 0,
|
|
67
|
+
height: 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
x: minX,
|
|
72
|
+
y: minY,
|
|
73
|
+
width: maxX - minX,
|
|
74
|
+
height: maxY - minY
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Translate a node and its whole subtree by `(dx, dy)` in global space. */
|
|
78
|
+
function translateSubtree(root, dx, dy, layoutByPath) {
|
|
79
|
+
const stack = [root.path];
|
|
80
|
+
while (stack.length > 0) {
|
|
81
|
+
const node = layoutByPath.get(stack.pop());
|
|
82
|
+
if (!node) continue;
|
|
83
|
+
node.rect = {
|
|
84
|
+
x: node.rect.x + dx,
|
|
85
|
+
y: node.rect.y + dy,
|
|
86
|
+
width: node.rect.width,
|
|
87
|
+
height: node.rect.height
|
|
88
|
+
};
|
|
89
|
+
node.renderedRect = {
|
|
90
|
+
x: node.renderedRect.x + dx,
|
|
91
|
+
y: node.renderedRect.y + dy,
|
|
92
|
+
width: node.renderedRect.width,
|
|
93
|
+
height: node.renderedRect.height
|
|
94
|
+
};
|
|
95
|
+
if (node.cumulativeTransform) node.cumulativeTransform = {
|
|
96
|
+
...node.cumulativeTransform,
|
|
97
|
+
tx: node.cumulativeTransform.tx + dx,
|
|
98
|
+
ty: node.cumulativeTransform.ty + dy
|
|
99
|
+
};
|
|
100
|
+
for (const childPath of node.children) stack.push(childPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Resolve every declared anchor, translating each anchored node (and its subtree)
|
|
105
|
+
* so its `to` edge lands on its target's `from` edge. Targets that are themselves
|
|
106
|
+
* anchored are resolved first; cycles are broken with a diagnostic.
|
|
107
|
+
*/
|
|
108
|
+
function resolveAnchors(layoutByPath, anchors, diagnostics) {
|
|
109
|
+
if (!anchors) return;
|
|
110
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
111
|
+
const inProgress = /* @__PURE__ */ new Set();
|
|
112
|
+
const resolveOne = (path) => {
|
|
113
|
+
if (resolved.has(path)) return;
|
|
114
|
+
const anchor = anchors[path];
|
|
115
|
+
const self = layoutByPath.get(path);
|
|
116
|
+
if (!anchor || !self) {
|
|
117
|
+
resolved.add(path);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (inProgress.has(path)) {
|
|
121
|
+
diagnostics.push({
|
|
122
|
+
severity: "warning",
|
|
123
|
+
code: "anchor-cycle",
|
|
124
|
+
message: `Anchor cycle detected resolving ${path}; leaving it unmoved.`,
|
|
125
|
+
nodePath: path
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
inProgress.add(path);
|
|
130
|
+
if (anchors[anchor.anchorTo]) resolveOne(anchor.anchorTo);
|
|
131
|
+
const target = layoutByPath.get(anchor.anchorTo);
|
|
132
|
+
if (!target) diagnostics.push({
|
|
133
|
+
severity: "warning",
|
|
134
|
+
code: "anchor-target-missing",
|
|
135
|
+
message: `Anchor target '${anchor.anchorTo}' not found for ${path}.`,
|
|
136
|
+
nodePath: path
|
|
137
|
+
});
|
|
138
|
+
else {
|
|
139
|
+
const targetPoint = edgePoint(anchor.from, target.renderedRect, contentExtent(target, layoutByPath));
|
|
140
|
+
const selfPoint = edgePoint(anchor.to, self.renderedRect, contentExtent(self, layoutByPath));
|
|
141
|
+
if (!targetPoint || !selfPoint) diagnostics.push({
|
|
142
|
+
severity: "warning",
|
|
143
|
+
code: "anchor-edge-unparsed",
|
|
144
|
+
message: `Unrecognized anchor edge (from='${anchor.from}', to='${anchor.to}') for ${path}.`,
|
|
145
|
+
nodePath: path
|
|
146
|
+
});
|
|
147
|
+
else {
|
|
148
|
+
const dx = targetPoint.x - selfPoint.x + (anchor.offset?.x ?? 0);
|
|
149
|
+
const dy = targetPoint.y - selfPoint.y + (anchor.offset?.y ?? 0);
|
|
150
|
+
if (dx !== 0 || dy !== 0) translateSubtree(self, dx, dy, layoutByPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
inProgress.delete(path);
|
|
154
|
+
resolved.add(path);
|
|
155
|
+
};
|
|
156
|
+
for (const path of Object.keys(anchors)) resolveOne(path);
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/flow-wrap.ts
|
|
160
|
+
/**
|
|
161
|
+
* Wraps flow-container entries into lines, mirroring Godot's
|
|
162
|
+
* `FlowContainer::_resort()` wrapping. The cross axis advances by the line's
|
|
163
|
+
* largest cross extent plus the relevant separation.
|
|
164
|
+
*/
|
|
165
|
+
function flowWrapLines(rect, entries, vertical, hSeparation, vSeparation) {
|
|
166
|
+
const lines = [];
|
|
167
|
+
let currentLine = [];
|
|
168
|
+
let cursorX = rect.x;
|
|
169
|
+
let cursorY = rect.y;
|
|
170
|
+
let lineCrossSize = 0;
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
const size = entry.size;
|
|
173
|
+
if (vertical) {
|
|
174
|
+
if (cursorY > rect.y && cursorY + size.height > rect.y + rect.height) {
|
|
175
|
+
lines.push(currentLine);
|
|
176
|
+
currentLine = [];
|
|
177
|
+
cursorX += lineCrossSize + hSeparation;
|
|
178
|
+
cursorY = rect.y;
|
|
179
|
+
lineCrossSize = 0;
|
|
180
|
+
}
|
|
181
|
+
currentLine.push({
|
|
182
|
+
...entry,
|
|
183
|
+
x: cursorX,
|
|
184
|
+
y: cursorY
|
|
185
|
+
});
|
|
186
|
+
cursorY += size.height + vSeparation;
|
|
187
|
+
lineCrossSize = Math.max(lineCrossSize, size.width);
|
|
188
|
+
} else {
|
|
189
|
+
if (cursorX > rect.x && cursorX + size.width > rect.x + rect.width) {
|
|
190
|
+
lines.push(currentLine);
|
|
191
|
+
currentLine = [];
|
|
192
|
+
cursorX = rect.x;
|
|
193
|
+
cursorY += lineCrossSize + vSeparation;
|
|
194
|
+
lineCrossSize = 0;
|
|
195
|
+
}
|
|
196
|
+
currentLine.push({
|
|
197
|
+
...entry,
|
|
198
|
+
x: cursorX,
|
|
199
|
+
y: cursorY
|
|
200
|
+
});
|
|
201
|
+
cursorX += size.width + hSeparation;
|
|
202
|
+
lineCrossSize = Math.max(lineCrossSize, size.height);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (currentLine.length > 0) lines.push(currentLine);
|
|
206
|
+
return lines;
|
|
207
|
+
}
|
|
208
|
+
/** Total cross-axis extent of wrapped lines: Σ line cross size + separations. */
|
|
209
|
+
function flowCrossExtent(lines, vertical, hSeparation, vSeparation) {
|
|
210
|
+
const crossSeparation = vertical ? hSeparation : vSeparation;
|
|
211
|
+
return lines.map((line) => line.reduce((max, entry) => Math.max(max, vertical ? entry.size.width : entry.size.height), 0)).reduce((sum, size) => sum + size, 0) + Math.max(0, lines.length - 1) * crossSeparation;
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/container-types.ts
|
|
215
|
+
function isBoxContainerType(type) {
|
|
216
|
+
return type === "BoxContainer" || type === "HBoxContainer" || type === "VBoxContainer";
|
|
217
|
+
}
|
|
218
|
+
function boxContainerHorizontal(indexed) {
|
|
219
|
+
if (indexed.node.type === "HBoxContainer") return true;
|
|
220
|
+
if (indexed.node.type === "VBoxContainer") return false;
|
|
221
|
+
return !(asBoolean(indexed.props.vertical) ?? false);
|
|
222
|
+
}
|
|
223
|
+
function isFlowContainerType(type) {
|
|
224
|
+
return type === "FlowContainer" || type === "HFlowContainer" || type === "VFlowContainer";
|
|
225
|
+
}
|
|
226
|
+
function flowContainerVertical(indexed) {
|
|
227
|
+
if (indexed.node.type === "VFlowContainer") return true;
|
|
228
|
+
if (indexed.node.type === "HFlowContainer") return false;
|
|
229
|
+
return asBoolean(indexed.props.vertical) ?? false;
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/style-box.ts
|
|
233
|
+
function contentRectForStyleBox(rect, metrics) {
|
|
234
|
+
return normalizeRect({
|
|
235
|
+
x: rect.x + metrics.left,
|
|
236
|
+
y: rect.y + metrics.top,
|
|
237
|
+
width: rect.width - metrics.left - metrics.right,
|
|
238
|
+
height: rect.height - metrics.top - metrics.bottom
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function panelStyleBoxMetrics(indexed, options) {
|
|
242
|
+
return styleBoxMetrics(resolveStyleBox(indexed.props["theme_override_styles/panel"], indexed, options) ?? resolveStyleBox(options.resolveTheme?.(indexed.node, "panel"), indexed, options));
|
|
243
|
+
}
|
|
244
|
+
function resolveStyleBox(value, indexed, options) {
|
|
245
|
+
const ref = asResourceRef(value);
|
|
246
|
+
return ref ? options.resolveResource?.(ref, indexed.node) : value;
|
|
247
|
+
}
|
|
248
|
+
function styleBoxMetrics(resource) {
|
|
249
|
+
const { type, properties } = resourceDocument(resource);
|
|
250
|
+
if (!type || type === "StyleBoxEmpty") return {
|
|
251
|
+
left: 0,
|
|
252
|
+
top: 0,
|
|
253
|
+
right: 0,
|
|
254
|
+
bottom: 0
|
|
255
|
+
};
|
|
256
|
+
const props = properties ?? {};
|
|
257
|
+
if (type === "StyleBoxFlat") {
|
|
258
|
+
const borders = {
|
|
259
|
+
left: numeric(props, "border_width_left") ?? numeric(props, "border_width_all") ?? 0,
|
|
260
|
+
top: numeric(props, "border_width_top") ?? numeric(props, "border_width_all") ?? 0,
|
|
261
|
+
right: numeric(props, "border_width_right") ?? numeric(props, "border_width_all") ?? 0,
|
|
262
|
+
bottom: numeric(props, "border_width_bottom") ?? numeric(props, "border_width_all") ?? 0
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
left: styleBoxMargin(props, "left", borders.left),
|
|
266
|
+
top: styleBoxMargin(props, "top", borders.top),
|
|
267
|
+
right: styleBoxMargin(props, "right", borders.right),
|
|
268
|
+
bottom: styleBoxMargin(props, "bottom", borders.bottom)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
left: Math.max(0, numeric(props, "content_margin_left") ?? numeric(props, "content_margin_all") ?? 0),
|
|
273
|
+
top: Math.max(0, numeric(props, "content_margin_top") ?? numeric(props, "content_margin_all") ?? 0),
|
|
274
|
+
right: Math.max(0, numeric(props, "content_margin_right") ?? numeric(props, "content_margin_all") ?? 0),
|
|
275
|
+
bottom: Math.max(0, numeric(props, "content_margin_bottom") ?? numeric(props, "content_margin_all") ?? 0)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function styleBoxMargin(props, side, fallback) {
|
|
279
|
+
const value = numeric(props, `content_margin_${side}`) ?? numeric(props, "content_margin_all");
|
|
280
|
+
return value !== void 0 && value >= 0 ? value : fallback;
|
|
281
|
+
}
|
|
282
|
+
function resourceDocument(resource) {
|
|
283
|
+
if (!resource || typeof resource !== "object") return {};
|
|
284
|
+
const record = resource;
|
|
285
|
+
const directDocument = record.document;
|
|
286
|
+
const document = directDocument && typeof directDocument === "object" && !Array.isArray(directDocument) ? directDocument : void 0;
|
|
287
|
+
const properties = document?.properties ?? (record.properties && typeof record.properties === "object" && !Array.isArray(record.properties) ? record.properties : void 0);
|
|
288
|
+
return {
|
|
289
|
+
type: asString(document?.header?.attributes.type) ?? (typeof record.type === "string" ? record.type : void 0),
|
|
290
|
+
properties
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/theme.ts
|
|
295
|
+
/**
|
|
296
|
+
* Godot's built-in default theme constants for container nodes, mirroring
|
|
297
|
+
* `scene/theme/default_theme.cpp` (4.5.1, `set_constant(..., Math::round(4 *
|
|
298
|
+
* scale))` at scale 1). A node that does not carry an explicit
|
|
299
|
+
* `theme_override_constants/*` (and whose project theme provides nothing) still
|
|
300
|
+
* inherits these from the default theme, so the layout engine must fall back to
|
|
301
|
+
* them rather than to 0 — e.g. an HBoxContainer with no separation override lays
|
|
302
|
+
* its children out 4px apart, not flush. MarginContainer margins default to 0 in
|
|
303
|
+
* the default theme, so they intentionally have no entry here.
|
|
304
|
+
*/
|
|
305
|
+
const DEFAULT_THEME_CONSTANTS = {
|
|
306
|
+
BoxContainer: { separation: 4 },
|
|
307
|
+
HBoxContainer: { separation: 4 },
|
|
308
|
+
VBoxContainer: { separation: 4 },
|
|
309
|
+
GridContainer: {
|
|
310
|
+
h_separation: 4,
|
|
311
|
+
v_separation: 4
|
|
312
|
+
},
|
|
313
|
+
FlowContainer: {
|
|
314
|
+
h_separation: 4,
|
|
315
|
+
v_separation: 4
|
|
316
|
+
},
|
|
317
|
+
HFlowContainer: {
|
|
318
|
+
h_separation: 4,
|
|
319
|
+
v_separation: 4
|
|
320
|
+
},
|
|
321
|
+
VFlowContainer: {
|
|
322
|
+
h_separation: 4,
|
|
323
|
+
v_separation: 4
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
function defaultThemeConstant(indexed, name) {
|
|
327
|
+
const type = indexed.node.type;
|
|
328
|
+
if (!type) return;
|
|
329
|
+
return DEFAULT_THEME_CONSTANTS[type]?.[name];
|
|
330
|
+
}
|
|
331
|
+
function themeNumber(indexed, name, options) {
|
|
332
|
+
return numeric(indexed.props, `theme_override_constants/${name}`) ?? numeric(indexed.props, `theme_constant_${name}`) ?? asNumber(options.resolveTheme?.(indexed.node, name)) ?? defaultThemeConstant(indexed, name);
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/minimum-size.ts
|
|
336
|
+
function preferredSize(indexed, byPath, options) {
|
|
337
|
+
return combinedMinimum(indexed, byPath, options) ?? explicitSizeFromProps(indexed) ?? sizeFromOffsets(indexed) ?? {
|
|
338
|
+
width: 0,
|
|
339
|
+
height: 0
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function explicitSizeFromProps(indexed) {
|
|
343
|
+
const size = asVector2(indexed.props.size);
|
|
344
|
+
if (size) return {
|
|
345
|
+
width: size.x,
|
|
346
|
+
height: size.y
|
|
347
|
+
};
|
|
348
|
+
const width = numeric(indexed.props, "size_width");
|
|
349
|
+
const height = numeric(indexed.props, "size_height");
|
|
350
|
+
return width !== void 0 && height !== void 0 ? {
|
|
351
|
+
width,
|
|
352
|
+
height
|
|
353
|
+
} : void 0;
|
|
354
|
+
}
|
|
355
|
+
function sizeFromOffsets(indexed) {
|
|
356
|
+
const left = numeric(indexed.props, "offset_left") ?? 0;
|
|
357
|
+
const top = numeric(indexed.props, "offset_top") ?? 0;
|
|
358
|
+
const right = numeric(indexed.props, "offset_right");
|
|
359
|
+
const bottom = numeric(indexed.props, "offset_bottom");
|
|
360
|
+
if (right === void 0 || bottom === void 0) return;
|
|
361
|
+
return {
|
|
362
|
+
width: Math.max(0, right - left),
|
|
363
|
+
height: Math.max(0, bottom - top)
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function customMinimum(indexed) {
|
|
367
|
+
const vector = asVector2(indexed.props.custom_minimum_size);
|
|
368
|
+
if (vector) return {
|
|
369
|
+
width: vector.x,
|
|
370
|
+
height: vector.y
|
|
371
|
+
};
|
|
372
|
+
const width = numeric(indexed.props, "custom_minimum_width") ?? numeric(indexed.props, "minimum_width");
|
|
373
|
+
const height = numeric(indexed.props, "custom_minimum_height") ?? numeric(indexed.props, "minimum_height");
|
|
374
|
+
return width !== void 0 && height !== void 0 ? {
|
|
375
|
+
width,
|
|
376
|
+
height
|
|
377
|
+
} : void 0;
|
|
378
|
+
}
|
|
379
|
+
function declaredCombinedMinimum(indexed) {
|
|
380
|
+
const vector = asVector2(indexed.props.combined_minimum_size);
|
|
381
|
+
if (vector) return {
|
|
382
|
+
width: vector.x,
|
|
383
|
+
height: vector.y
|
|
384
|
+
};
|
|
385
|
+
const width = numeric(indexed.props, "combined_minimum_width");
|
|
386
|
+
const height = numeric(indexed.props, "combined_minimum_height");
|
|
387
|
+
return width !== void 0 && height !== void 0 ? {
|
|
388
|
+
width,
|
|
389
|
+
height
|
|
390
|
+
} : void 0;
|
|
391
|
+
}
|
|
392
|
+
function combinedMinimum(indexed, byPath, options, currentRect) {
|
|
393
|
+
return maxSize(declaredCombinedMinimum(indexed), maxSize(customMinimum(indexed), maxSize(internalMinimum(indexed, options, currentRect), containerMinimumSize(indexed, byPath, options))));
|
|
394
|
+
}
|
|
395
|
+
function maxSize(a, b) {
|
|
396
|
+
if (!a) return b;
|
|
397
|
+
if (!b) return a;
|
|
398
|
+
return {
|
|
399
|
+
width: Math.max(a.width, b.width),
|
|
400
|
+
height: Math.max(a.height, b.height)
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Container analogue of Godot `Container::get_minimum_size()`, memoized on the
|
|
405
|
+
* IndexedNode. Non-container controls return `undefined` (a plain Control/Node
|
|
406
|
+
* does not size to its children). Recurses through `byPath` children, which
|
|
407
|
+
* already include flattened instanced PackedScene content.
|
|
408
|
+
*/
|
|
409
|
+
function containerMinimumSize(indexed, byPath, options) {
|
|
410
|
+
if (indexed.minimumSize !== void 0) return indexed.minimumSize ?? void 0;
|
|
411
|
+
const size = computeContainerMinimum(indexed, byPath, options);
|
|
412
|
+
indexed.minimumSize = size ?? null;
|
|
413
|
+
return size;
|
|
414
|
+
}
|
|
415
|
+
function computeContainerMinimum(indexed, byPath, options) {
|
|
416
|
+
switch (indexed.node.type ?? "Node") {
|
|
417
|
+
case "BoxContainer":
|
|
418
|
+
case "HBoxContainer":
|
|
419
|
+
case "VBoxContainer": return boxMinimum(indexed, byPath, options);
|
|
420
|
+
case "GridContainer": return gridMinimum(indexed, byPath, options);
|
|
421
|
+
case "FlowContainer":
|
|
422
|
+
case "HFlowContainer":
|
|
423
|
+
case "VFlowContainer": return flowMinimum(indexed, byPath, options);
|
|
424
|
+
case "MarginContainer": return marginMinimum(indexed, byPath, options);
|
|
425
|
+
case "CenterContainer": return maxChildMinimum(indexed, byPath, options);
|
|
426
|
+
case "AspectRatioContainer": return maxChildMinimum(indexed, byPath, options);
|
|
427
|
+
case "PanelContainer": return panelMinimum(indexed, byPath, options);
|
|
428
|
+
case "ScrollContainer": return scrollMinimum(indexed, byPath, options);
|
|
429
|
+
default: return;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function visibleChildren(indexed, byPath) {
|
|
433
|
+
return indexed.children.map((path) => byPath.get(path)).filter((child) => Boolean(child) && (asBoolean(child.props.visible) ?? true));
|
|
434
|
+
}
|
|
435
|
+
function childSizes(indexed, byPath, options) {
|
|
436
|
+
return visibleChildren(indexed, byPath).map((child) => combinedMinimum(child, byPath, options) ?? {
|
|
437
|
+
width: 0,
|
|
438
|
+
height: 0
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
function boxMinimum(indexed, byPath, options) {
|
|
442
|
+
const horizontal = boxContainerHorizontal(indexed);
|
|
443
|
+
const separation = themeNumber(indexed, "separation", options) ?? 0;
|
|
444
|
+
const sizes = childSizes(indexed, byPath, options);
|
|
445
|
+
let main = 0;
|
|
446
|
+
let cross = 0;
|
|
447
|
+
sizes.forEach((size, index) => {
|
|
448
|
+
const childMain = horizontal ? size.width : size.height;
|
|
449
|
+
const childCross = horizontal ? size.height : size.width;
|
|
450
|
+
main += childMain + (index === 0 ? 0 : separation);
|
|
451
|
+
cross = Math.max(cross, childCross);
|
|
452
|
+
});
|
|
453
|
+
return horizontal ? {
|
|
454
|
+
width: main,
|
|
455
|
+
height: cross
|
|
456
|
+
} : {
|
|
457
|
+
width: cross,
|
|
458
|
+
height: main
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function gridMinimum(indexed, byPath, options) {
|
|
462
|
+
const columns = Math.max(1, Math.floor(numeric(indexed.props, "columns") ?? 1));
|
|
463
|
+
const hSeparation = themeNumber(indexed, "h_separation", options) ?? themeNumber(indexed, "separation", options) ?? 0;
|
|
464
|
+
const vSeparation = themeNumber(indexed, "v_separation", options) ?? themeNumber(indexed, "separation", options) ?? 0;
|
|
465
|
+
const sizes = childSizes(indexed, byPath, options);
|
|
466
|
+
const columnWidths = /* @__PURE__ */ new Map();
|
|
467
|
+
const rowHeights = /* @__PURE__ */ new Map();
|
|
468
|
+
let maxColumn = 0;
|
|
469
|
+
let maxRow = 0;
|
|
470
|
+
sizes.forEach((size, index) => {
|
|
471
|
+
const column = index % columns;
|
|
472
|
+
const row = Math.floor(index / columns);
|
|
473
|
+
columnWidths.set(column, Math.max(columnWidths.get(column) ?? 0, size.width));
|
|
474
|
+
rowHeights.set(row, Math.max(rowHeights.get(row) ?? 0, size.height));
|
|
475
|
+
maxColumn = Math.max(maxColumn, column);
|
|
476
|
+
maxRow = Math.max(maxRow, row);
|
|
477
|
+
});
|
|
478
|
+
return {
|
|
479
|
+
width: [...columnWidths.values()].reduce((sum, value) => sum + value, 0) + hSeparation * maxColumn,
|
|
480
|
+
height: [...rowHeights.values()].reduce((sum, value) => sum + value, 0) + vSeparation * maxRow
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function flowMinimum(indexed, byPath, options) {
|
|
484
|
+
const vertical = flowContainerVertical(indexed);
|
|
485
|
+
const hSeparation = themeNumber(indexed, "h_separation", options) ?? themeNumber(indexed, "separation", options) ?? 0;
|
|
486
|
+
const vSeparation = themeNumber(indexed, "v_separation", options) ?? themeNumber(indexed, "separation", options) ?? 0;
|
|
487
|
+
const sizes = childSizes(indexed, byPath, options);
|
|
488
|
+
const maxWidth = sizes.reduce((max, size) => Math.max(max, size.width), 0);
|
|
489
|
+
const maxHeight = sizes.reduce((max, size) => Math.max(max, size.height), 0);
|
|
490
|
+
const offsetSize = explicitSizeFromProps(indexed) ?? sizeFromOffsets(indexed);
|
|
491
|
+
const offsetMain = vertical ? offsetSize?.height : offsetSize?.width;
|
|
492
|
+
const mainExtent = indexed.flowMainExtent ?? (offsetMain !== void 0 && offsetMain > 0 ? offsetMain : void 0);
|
|
493
|
+
if (mainExtent !== void 0 && mainExtent > 0) {
|
|
494
|
+
const crossExtent = flowCrossExtent(flowWrapLines(vertical ? {
|
|
495
|
+
x: 0,
|
|
496
|
+
y: 0,
|
|
497
|
+
width: maxWidth,
|
|
498
|
+
height: mainExtent
|
|
499
|
+
} : {
|
|
500
|
+
x: 0,
|
|
501
|
+
y: 0,
|
|
502
|
+
width: mainExtent,
|
|
503
|
+
height: maxHeight
|
|
504
|
+
}, sizes.map((size) => ({
|
|
505
|
+
item: size,
|
|
506
|
+
size
|
|
507
|
+
})), vertical, hSeparation, vSeparation), vertical, hSeparation, vSeparation);
|
|
508
|
+
return vertical ? {
|
|
509
|
+
width: crossExtent,
|
|
510
|
+
height: maxHeight
|
|
511
|
+
} : {
|
|
512
|
+
width: maxWidth,
|
|
513
|
+
height: crossExtent
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
width: maxWidth,
|
|
518
|
+
height: maxHeight
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function marginMinimum(indexed, byPath, options) {
|
|
522
|
+
const left = themeNumber(indexed, "margin_left", options) ?? 0;
|
|
523
|
+
const top = themeNumber(indexed, "margin_top", options) ?? 0;
|
|
524
|
+
const right = themeNumber(indexed, "margin_right", options) ?? 0;
|
|
525
|
+
const bottom = themeNumber(indexed, "margin_bottom", options) ?? 0;
|
|
526
|
+
const max = maxChildMinimum(indexed, byPath, options);
|
|
527
|
+
return {
|
|
528
|
+
width: max.width + left + right,
|
|
529
|
+
height: max.height + top + bottom
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function panelMinimum(indexed, byPath, options) {
|
|
533
|
+
const metrics = panelStyleBoxMetrics(indexed, options);
|
|
534
|
+
const max = maxChildMinimum(indexed, byPath, options);
|
|
535
|
+
return {
|
|
536
|
+
width: max.width + metrics.left + metrics.right,
|
|
537
|
+
height: max.height + metrics.top + metrics.bottom
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function scrollMinimum(indexed, byPath, options) {
|
|
541
|
+
const metrics = panelStyleBoxMetrics(indexed, options);
|
|
542
|
+
const largest = maxChildMinimum(indexed, byPath, options);
|
|
543
|
+
const horizontalDisabled = (numeric(indexed.props, "horizontal_scroll_mode") ?? 1) === 0;
|
|
544
|
+
const verticalDisabled = (numeric(indexed.props, "vertical_scroll_mode") ?? 1) === 0;
|
|
545
|
+
return {
|
|
546
|
+
width: metrics.left + metrics.right + (horizontalDisabled ? largest.width : 0),
|
|
547
|
+
height: metrics.top + metrics.bottom + (verticalDisabled ? largest.height : 0)
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function maxChildMinimum(indexed, byPath, options) {
|
|
551
|
+
return childSizes(indexed, byPath, options).reduce((max, size) => ({
|
|
552
|
+
width: Math.max(max.width, size.width),
|
|
553
|
+
height: Math.max(max.height, size.height)
|
|
554
|
+
}), {
|
|
555
|
+
width: 0,
|
|
556
|
+
height: 0
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
const TEXT_CONTENT_TYPES = new Set(["Label", "RichTextLabel"]);
|
|
560
|
+
function internalMinimum(indexed, options, currentRect) {
|
|
561
|
+
if (TEXT_CONTENT_TYPES.has(indexed.node.type ?? "")) {
|
|
562
|
+
const size = options.resolveTextContentSize?.(indexed.node, indexed.path, indexed.props, currentRect?.width);
|
|
563
|
+
if (!size || size.width < 0 || size.height < 0) return;
|
|
564
|
+
return size;
|
|
565
|
+
}
|
|
566
|
+
if (indexed.node.type !== "TextureRect") return;
|
|
567
|
+
const textureRef = asResourceRef(indexed.props.texture);
|
|
568
|
+
if (!textureRef) return;
|
|
569
|
+
const textureSize = resourceSize(options.resolveResource?.(textureRef, indexed.node));
|
|
570
|
+
if (!textureSize || textureSize.width <= 0 || textureSize.height <= 0) return;
|
|
571
|
+
const expandMode = numeric(indexed.props, "expand_mode") ?? 0;
|
|
572
|
+
if (expandMode === 0) return textureSize;
|
|
573
|
+
if (expandMode === 2) return {
|
|
574
|
+
width: currentRect?.height ?? 0,
|
|
575
|
+
height: 0
|
|
576
|
+
};
|
|
577
|
+
if (expandMode === 3) return {
|
|
578
|
+
width: (currentRect?.height ?? 0) * textureSize.width / textureSize.height,
|
|
579
|
+
height: 0
|
|
580
|
+
};
|
|
581
|
+
if (expandMode === 4) return {
|
|
582
|
+
width: 0,
|
|
583
|
+
height: currentRect?.width ?? 0
|
|
584
|
+
};
|
|
585
|
+
if (expandMode === 5) return {
|
|
586
|
+
width: 0,
|
|
587
|
+
height: (currentRect?.width ?? 0) * textureSize.height / textureSize.width
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function resourceSize(resource) {
|
|
591
|
+
if (!resource || typeof resource !== "object") return;
|
|
592
|
+
const record = resource;
|
|
593
|
+
const size = record.size;
|
|
594
|
+
if (size && typeof size === "object" && !Array.isArray(size)) {
|
|
595
|
+
const sizeRecord = size;
|
|
596
|
+
const width = typeof sizeRecord.width === "number" ? sizeRecord.width : void 0;
|
|
597
|
+
const height = typeof sizeRecord.height === "number" ? sizeRecord.height : void 0;
|
|
598
|
+
return width !== void 0 && height !== void 0 ? {
|
|
599
|
+
width,
|
|
600
|
+
height
|
|
601
|
+
} : void 0;
|
|
602
|
+
}
|
|
603
|
+
const width = typeof record.width === "number" ? record.width : void 0;
|
|
604
|
+
const height = typeof record.height === "number" ? record.height : void 0;
|
|
605
|
+
return width !== void 0 && height !== void 0 ? {
|
|
606
|
+
width,
|
|
607
|
+
height
|
|
608
|
+
} : void 0;
|
|
609
|
+
}
|
|
610
|
+
function hasExpandFlag(indexed, horizontal) {
|
|
611
|
+
return ((numeric(indexed.props, horizontal ? "size_flags_horizontal" : "size_flags_vertical") ?? 0) & 2) === 2;
|
|
612
|
+
}
|
|
613
|
+
function hasFillFlag(indexed, horizontal) {
|
|
614
|
+
return ((numeric(indexed.props, horizontal ? "size_flags_horizontal" : "size_flags_vertical") ?? 0) & 1) === 1;
|
|
615
|
+
}
|
|
616
|
+
function alignmentOffset(indexed, available, content) {
|
|
617
|
+
const alignment = numeric(indexed.props, "alignment") ?? 0;
|
|
618
|
+
if (alignment === 1) return Math.max(0, (available - content) / 2);
|
|
619
|
+
if (alignment === 2) return Math.max(0, available - content);
|
|
620
|
+
return 0;
|
|
621
|
+
}
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/rects.ts
|
|
624
|
+
function rootRect(indexed, viewport, byPath, options) {
|
|
625
|
+
const minimum = combinedMinimum(indexed, byPath, options);
|
|
626
|
+
const width = numeric(indexed.props, "size_width") ?? sizeFromOffsets(indexed)?.width ?? minimum?.width ?? viewport.width;
|
|
627
|
+
const height = numeric(indexed.props, "size_height") ?? sizeFromOffsets(indexed)?.height ?? minimum?.height ?? viewport.height;
|
|
628
|
+
return {
|
|
629
|
+
x: viewport.x,
|
|
630
|
+
y: viewport.y,
|
|
631
|
+
width,
|
|
632
|
+
height
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function controlRect(indexed, parent, byPath, options) {
|
|
636
|
+
const leftAnchor = numeric(indexed.props, "anchor_left") ?? 0;
|
|
637
|
+
const topAnchor = numeric(indexed.props, "anchor_top") ?? 0;
|
|
638
|
+
const rightAnchor = numeric(indexed.props, "anchor_right") ?? leftAnchor;
|
|
639
|
+
const bottomAnchor = numeric(indexed.props, "anchor_bottom") ?? topAnchor;
|
|
640
|
+
const position = asVector2(indexed.props.position);
|
|
641
|
+
const leftOffset = numeric(indexed.props, "offset_left") ?? numeric(indexed.props, "position_x") ?? position?.x ?? 0;
|
|
642
|
+
const topOffset = numeric(indexed.props, "offset_top") ?? numeric(indexed.props, "position_y") ?? position?.y ?? 0;
|
|
643
|
+
const explicitSize = explicitSizeFromProps(indexed);
|
|
644
|
+
const rightOffset = numeric(indexed.props, "offset_right") ?? (explicitSize ? leftOffset + explicitSize.width : 0);
|
|
645
|
+
const bottomOffset = numeric(indexed.props, "offset_bottom") ?? (explicitSize ? topOffset + explicitSize.height : 0);
|
|
646
|
+
const left = parent.x + parent.width * leftAnchor + leftOffset;
|
|
647
|
+
const top = parent.y + parent.height * topAnchor + topOffset;
|
|
648
|
+
const right = parent.x + parent.width * rightAnchor + rightOffset;
|
|
649
|
+
const bottom = parent.y + parent.height * bottomAnchor + bottomOffset;
|
|
650
|
+
return growToMinimum(normalizeRect({
|
|
651
|
+
x: left,
|
|
652
|
+
y: top,
|
|
653
|
+
width: right - left,
|
|
654
|
+
height: bottom - top
|
|
655
|
+
}), indexed, byPath, options);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Expands and repositions a rect when its combined minimum size exceeds the
|
|
659
|
+
* offset-derived size, mirroring `Control::_size_changed`. The grow direction
|
|
660
|
+
* (BEGIN shifts the start edge, BOTH centers, END only expands) applies
|
|
661
|
+
* unconditionally, independent of anchors.
|
|
662
|
+
*/
|
|
663
|
+
function growToMinimum(rect, indexed, byPath, options) {
|
|
664
|
+
const minimum = combinedMinimum(indexed, byPath, options, rect);
|
|
665
|
+
if (!minimum) return rect;
|
|
666
|
+
let { x, y, width, height } = rect;
|
|
667
|
+
if (minimum.width > width) {
|
|
668
|
+
const delta = minimum.width - width;
|
|
669
|
+
const grow = numeric(indexed.props, "grow_horizontal") ?? 1;
|
|
670
|
+
if (grow === 0) x -= delta;
|
|
671
|
+
else if (grow === 2) x -= delta / 2;
|
|
672
|
+
width = minimum.width;
|
|
673
|
+
}
|
|
674
|
+
if (minimum.height > height) {
|
|
675
|
+
const delta = minimum.height - height;
|
|
676
|
+
const grow = numeric(indexed.props, "grow_vertical") ?? 1;
|
|
677
|
+
if (grow === 0) y -= delta;
|
|
678
|
+
else if (grow === 2) y -= delta / 2;
|
|
679
|
+
height = minimum.height;
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
x,
|
|
683
|
+
y,
|
|
684
|
+
width,
|
|
685
|
+
height
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function numeric(props, name) {
|
|
689
|
+
return asNumber(props[name]);
|
|
690
|
+
}
|
|
691
|
+
function normalizeRect(rect) {
|
|
692
|
+
return {
|
|
693
|
+
x: rect.x,
|
|
694
|
+
y: rect.y,
|
|
695
|
+
width: Math.max(0, rect.width),
|
|
696
|
+
height: Math.max(0, rect.height)
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Apply a node's own `scale` around `pivotOffset` to its layout rect, producing
|
|
701
|
+
* the on-screen global rect. Mirrors the renderer's `transform: scale(...)` with
|
|
702
|
+
* `transform-origin: <pivotOffset>` (see node-style.ts): the pivot is a fixed
|
|
703
|
+
* point, so the top-left corner maps to `rect.pos + pivot·(1 − scale)` and the
|
|
704
|
+
* size scales. Godot's `Control.get_global_rect()` includes scale the same way,
|
|
705
|
+
* which is what the live-game layout-diff compares against. Identity scale
|
|
706
|
+
* returns the rect unchanged.
|
|
707
|
+
*/
|
|
708
|
+
function scaledRect(rect, scale, pivotOffset) {
|
|
709
|
+
if (scale.x === 1 && scale.y === 1) return rect;
|
|
710
|
+
return {
|
|
711
|
+
x: rect.x + pivotOffset.x * (1 - scale.x),
|
|
712
|
+
y: rect.y + pivotOffset.y * (1 - scale.y),
|
|
713
|
+
width: rect.width * scale.x,
|
|
714
|
+
height: rect.height * scale.y
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
const IDENTITY_AFFINE = {
|
|
718
|
+
a: 1,
|
|
719
|
+
b: 0,
|
|
720
|
+
c: 0,
|
|
721
|
+
d: 1,
|
|
722
|
+
tx: 0,
|
|
723
|
+
ty: 0
|
|
724
|
+
};
|
|
725
|
+
/** Compose two affines (2×3 matrix multiply) so the result maps `x -> outer(inner(x))`. */
|
|
726
|
+
function composeAffine(outer, inner) {
|
|
727
|
+
return {
|
|
728
|
+
a: outer.a * inner.a + outer.c * inner.b,
|
|
729
|
+
b: outer.b * inner.a + outer.d * inner.b,
|
|
730
|
+
c: outer.a * inner.c + outer.c * inner.d,
|
|
731
|
+
d: outer.b * inner.c + outer.d * inner.d,
|
|
732
|
+
tx: outer.a * inner.tx + outer.c * inner.ty + outer.tx,
|
|
733
|
+
ty: outer.b * inner.tx + outer.d * inner.ty + outer.ty
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Apply an affine to a rect, producing an axis-aligned `GodotRect` matching Godot
|
|
738
|
+
* `get_global_rect()`: the top-left ORIGIN is transformed through the full
|
|
739
|
+
* (rotation-bearing) matrix, while width/height take the *unrotated* extent — each
|
|
740
|
+
* axis scaled by its column magnitude (`hypot`), NOT inflated to a rotated bounding
|
|
741
|
+
* box. For a pure scale+translate (`b=c=0`) this reduces bit-for-bit to the old
|
|
742
|
+
* `sx·x+tx` behavior.
|
|
743
|
+
*/
|
|
744
|
+
function applyAffine(transform, rect) {
|
|
745
|
+
const { a, b, c, d, tx, ty } = transform;
|
|
746
|
+
if (a === 1 && b === 0 && c === 0 && d === 1 && tx === 0 && ty === 0) return rect;
|
|
747
|
+
return {
|
|
748
|
+
x: a * rect.x + c * rect.y + tx,
|
|
749
|
+
y: b * rect.x + d * rect.y + ty,
|
|
750
|
+
width: rect.width * Math.hypot(a, b),
|
|
751
|
+
height: rect.height * Math.hypot(c, d)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* The scale+rotation transform a node applies to its own content and all its
|
|
756
|
+
* descendants, in the global (unscaled-layout) coordinate space, pivoting about the
|
|
757
|
+
* node's global pivot `rect.pos + pivotOffset`. The linear part is Godot's
|
|
758
|
+
* `Transform2D(rotation, scale)` basis — x-axis `(cos·sx, sin·sx)`, y-axis
|
|
759
|
+
* `(-sin·sy, cos·sy)` — i.e. rotate∘scale, which is what the live game composes.
|
|
760
|
+
*
|
|
761
|
+
* NOTE: `rotation` is in radians. Correct when the rotation pivot is the rect origin
|
|
762
|
+
* (`pivotOffset` 0, the only case in the captured data and the case the CSS renderer
|
|
763
|
+
* pivots at the origin for); a non-zero pivot combined with self-rotation would also
|
|
764
|
+
* shift the node's own top-left, which `renderedRect` does not model.
|
|
765
|
+
*/
|
|
766
|
+
function ownTransformAffine(rect, scale, rotation, pivotOffset) {
|
|
767
|
+
const gx = rect.x + pivotOffset.x;
|
|
768
|
+
const gy = rect.y + pivotOffset.y;
|
|
769
|
+
const cos = Math.cos(rotation);
|
|
770
|
+
const sin = Math.sin(rotation);
|
|
771
|
+
const a = cos * scale.x;
|
|
772
|
+
const b = sin * scale.x;
|
|
773
|
+
const c = -sin * scale.y;
|
|
774
|
+
const d = cos * scale.y;
|
|
775
|
+
return {
|
|
776
|
+
a,
|
|
777
|
+
b,
|
|
778
|
+
c,
|
|
779
|
+
d,
|
|
780
|
+
tx: gx - (a * gx + c * gy),
|
|
781
|
+
ty: gy - (b * gx + d * gy)
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
//#endregion
|
|
785
|
+
//#region src/layout-node.ts
|
|
786
|
+
function makeLayoutNode(indexed, rect, parent) {
|
|
787
|
+
const base = deriveNodeVisuals(indexed, parent?.zIndex);
|
|
788
|
+
const rotationDegrees = asNumber(indexed.props.rotation_degrees);
|
|
789
|
+
const rotation = asNumber(indexed.props.rotation) ?? (rotationDegrees === void 0 ? 0 : rotationDegrees * Math.PI / 180);
|
|
790
|
+
const parentTransform = parent?.cumulativeTransform ?? IDENTITY_AFFINE;
|
|
791
|
+
const renderedRect = applyAffine(parentTransform, scaledRect(rect, base.scale, base.pivotOffset));
|
|
792
|
+
const cumulativeTransform = composeAffine(parentTransform, ownTransformAffine(rect, base.scale, rotation, base.pivotOffset));
|
|
793
|
+
return {
|
|
794
|
+
...base,
|
|
795
|
+
rect,
|
|
796
|
+
renderedRect,
|
|
797
|
+
cumulativeTransform
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
//#endregion
|
|
801
|
+
//#region src/container-layout.ts
|
|
802
|
+
function aspectAlignmentFactor(value) {
|
|
803
|
+
const alignment = asNumber(value) ?? 1;
|
|
804
|
+
if (alignment === 0) return 0;
|
|
805
|
+
if (alignment === 2) return 1;
|
|
806
|
+
return .5;
|
|
807
|
+
}
|
|
808
|
+
function layoutBoxContainerChildren(parent, rect, byPath, layoutByPath, diagnostics, options, horizontal, layoutChildren) {
|
|
809
|
+
const separation = themeNumber(parent, "separation", options) ?? 0;
|
|
810
|
+
const children = visibleChildren(parent, byPath);
|
|
811
|
+
const sizes = children.map((child) => preferredSize(child, byPath, options));
|
|
812
|
+
if (!horizontal) children.forEach((child, index) => {
|
|
813
|
+
if (!TEXT_CONTENT_TYPES.has(child.node.type ?? "")) return;
|
|
814
|
+
const reflowed = combinedMinimum(child, byPath, options, {
|
|
815
|
+
x: 0,
|
|
816
|
+
y: 0,
|
|
817
|
+
width: axisPlacement(rect.x, rect.width, sizes[index].width, child, true).size,
|
|
818
|
+
height: 0
|
|
819
|
+
});
|
|
820
|
+
if (reflowed) sizes[index] = {
|
|
821
|
+
width: sizes[index].width,
|
|
822
|
+
height: reflowed.height
|
|
823
|
+
};
|
|
824
|
+
});
|
|
825
|
+
const totalMinimum = sizes.reduce((sum, size) => sum + (horizontal ? size.width : size.height), 0) + Math.max(0, children.length - 1) * separation;
|
|
826
|
+
const available = horizontal ? rect.width : rect.height;
|
|
827
|
+
const remaining = Math.max(0, available - totalMinimum);
|
|
828
|
+
const expandCount = children.filter((child) => hasExpandFlag(child, horizontal)).length;
|
|
829
|
+
let cursor = (horizontal ? rect.x : rect.y) + (expandCount > 0 ? 0 : alignmentOffset(parent, available, totalMinimum));
|
|
830
|
+
children.forEach((child, index) => {
|
|
831
|
+
const minimum = sizes[index] ?? {
|
|
832
|
+
width: 0,
|
|
833
|
+
height: 0
|
|
834
|
+
};
|
|
835
|
+
const extra = expandCount > 0 && hasExpandFlag(child, horizontal) ? remaining / expandCount : 0;
|
|
836
|
+
const fillExtra = hasFillFlag(child, horizontal) ? extra : 0;
|
|
837
|
+
const crossAxis = horizontal ? axisPlacement(rect.y, rect.height, minimum.height, child, false) : axisPlacement(rect.x, rect.width, minimum.width, child, true);
|
|
838
|
+
const childRect = horizontal ? {
|
|
839
|
+
y: crossAxis.position,
|
|
840
|
+
x: cursor,
|
|
841
|
+
width: minimum.width + fillExtra,
|
|
842
|
+
height: crossAxis.size
|
|
843
|
+
} : {
|
|
844
|
+
x: crossAxis.position,
|
|
845
|
+
y: cursor,
|
|
846
|
+
width: crossAxis.size,
|
|
847
|
+
height: minimum.height + fillExtra
|
|
848
|
+
};
|
|
849
|
+
cursor += (horizontal ? minimum.width + extra : minimum.height + extra) + separation;
|
|
850
|
+
const layout = makeLayoutNode(child, childRect, layoutByPath.get(parent.path));
|
|
851
|
+
layoutByPath.set(child.path, layout);
|
|
852
|
+
layoutChildren(child, childRect, byPath, layoutByPath, diagnostics, options);
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
function layoutGridContainerChildren(parent, rect, byPath, layoutByPath, diagnostics, options, layoutChildren) {
|
|
856
|
+
const columns = Math.max(1, Math.floor(numeric(parent.props, "columns") ?? 1));
|
|
857
|
+
const hSeparation = themeNumber(parent, "h_separation", options) ?? themeNumber(parent, "separation", options) ?? 0;
|
|
858
|
+
const vSeparation = themeNumber(parent, "v_separation", options) ?? themeNumber(parent, "separation", options) ?? 0;
|
|
859
|
+
const children = visibleChildren(parent, byPath);
|
|
860
|
+
const sizes = children.map((child) => preferredSize(child, byPath, options));
|
|
861
|
+
const columnWidths = Array.from({ length: columns }, (_, column) => Math.max(0, ...sizes.filter((_, index) => index % columns === column).map((size) => size.width)));
|
|
862
|
+
const rowCount = Math.ceil(children.length / columns);
|
|
863
|
+
const rowHeights = Array.from({ length: rowCount }, (_, row) => Math.max(0, ...sizes.slice(row * columns, row * columns + columns).map((size) => size.height)));
|
|
864
|
+
const parentLayout = layoutByPath.get(parent.path);
|
|
865
|
+
children.forEach((child, index) => {
|
|
866
|
+
const column = index % columns;
|
|
867
|
+
const row = Math.floor(index / columns);
|
|
868
|
+
const cell = {
|
|
869
|
+
x: rect.x + columnWidths.slice(0, column).reduce((sum, width) => sum + width, 0) + column * hSeparation,
|
|
870
|
+
y: rect.y + rowHeights.slice(0, row).reduce((sum, height) => sum + height, 0) + row * vSeparation,
|
|
871
|
+
width: columnWidths[column] ?? 0,
|
|
872
|
+
height: rowHeights[row] ?? 0
|
|
873
|
+
};
|
|
874
|
+
const horizontalPlacement = axisPlacement(cell.x, cell.width, sizes[index]?.width ?? 0, child, true);
|
|
875
|
+
const verticalPlacement = axisPlacement(cell.y, cell.height, sizes[index]?.height ?? 0, child, false);
|
|
876
|
+
const childRect = {
|
|
877
|
+
x: horizontalPlacement.position,
|
|
878
|
+
y: verticalPlacement.position,
|
|
879
|
+
width: horizontalPlacement.size,
|
|
880
|
+
height: verticalPlacement.size
|
|
881
|
+
};
|
|
882
|
+
layoutByPath.set(child.path, makeLayoutNode(child, childRect, parentLayout));
|
|
883
|
+
layoutChildren(child, childRect, byPath, layoutByPath, diagnostics, options);
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
function layoutFlowContainerChildren(parent, rect, byPath, layoutByPath, diagnostics, options, vertical, layoutChildren) {
|
|
887
|
+
const hSeparation = themeNumber(parent, "h_separation", options) ?? themeNumber(parent, "separation", options) ?? 0;
|
|
888
|
+
const vSeparation = themeNumber(parent, "v_separation", options) ?? themeNumber(parent, "separation", options) ?? 0;
|
|
889
|
+
const lines = flowWrapLines(rect, visibleChildren(parent, byPath).map((child) => ({
|
|
890
|
+
item: child,
|
|
891
|
+
size: preferredSize(child, byPath, options)
|
|
892
|
+
})), vertical, hSeparation, vSeparation);
|
|
893
|
+
for (const line of lines) {
|
|
894
|
+
const crossSize = line.reduce((max, entry) => Math.max(max, vertical ? entry.size.width : entry.size.height), 0);
|
|
895
|
+
for (const entry of line) {
|
|
896
|
+
const childRect = vertical ? {
|
|
897
|
+
x: entry.x,
|
|
898
|
+
y: entry.y,
|
|
899
|
+
width: crossSize,
|
|
900
|
+
height: entry.size.height
|
|
901
|
+
} : {
|
|
902
|
+
x: entry.x,
|
|
903
|
+
y: entry.y,
|
|
904
|
+
width: entry.size.width,
|
|
905
|
+
height: crossSize
|
|
906
|
+
};
|
|
907
|
+
layoutByPath.set(entry.item.path, makeLayoutNode(entry.item, childRect, layoutByPath.get(parent.path)));
|
|
908
|
+
layoutChildren(entry.item, childRect, byPath, layoutByPath, diagnostics, options);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function layoutAspectRatioContainerChildren(parent, rect, byPath, layoutByPath, diagnostics, options, layoutChildren) {
|
|
913
|
+
const ratio = numeric(parent.props, "ratio") ?? 1;
|
|
914
|
+
const safeRatio = ratio === 0 ? 1 : ratio;
|
|
915
|
+
const stretchMode = numeric(parent.props, "stretch_mode") ?? 2;
|
|
916
|
+
const alignX = aspectAlignmentFactor(parent.props.alignment_horizontal);
|
|
917
|
+
const alignY = aspectAlignmentFactor(parent.props.alignment_vertical);
|
|
918
|
+
const parentLayout = layoutByPath.get(parent.path);
|
|
919
|
+
for (const childPath of parent.children) {
|
|
920
|
+
const child = byPath.get(childPath);
|
|
921
|
+
if (!child) continue;
|
|
922
|
+
const minimum = combinedMinimum(child, byPath, options) ?? {
|
|
923
|
+
width: 0,
|
|
924
|
+
height: 0
|
|
925
|
+
};
|
|
926
|
+
const base = {
|
|
927
|
+
width: safeRatio,
|
|
928
|
+
height: 1
|
|
929
|
+
};
|
|
930
|
+
let scaleFactor;
|
|
931
|
+
if (stretchMode === 0) scaleFactor = rect.width / base.width;
|
|
932
|
+
else if (stretchMode === 1) scaleFactor = rect.height / base.height;
|
|
933
|
+
else if (stretchMode === 3) scaleFactor = Math.max(rect.width / base.width, rect.height / base.height);
|
|
934
|
+
else scaleFactor = Math.min(rect.width / base.width, rect.height / base.height);
|
|
935
|
+
const width = Math.max(minimum.width, base.width * scaleFactor);
|
|
936
|
+
const height = Math.max(minimum.height, base.height * scaleFactor);
|
|
937
|
+
const childRect = {
|
|
938
|
+
x: rect.x + (rect.width - width) * alignX,
|
|
939
|
+
y: rect.y + (rect.height - height) * alignY,
|
|
940
|
+
width,
|
|
941
|
+
height
|
|
942
|
+
};
|
|
943
|
+
layoutByPath.set(child.path, makeLayoutNode(child, childRect, parentLayout));
|
|
944
|
+
layoutChildren(child, childRect, byPath, layoutByPath, diagnostics, options);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function layoutPanelContainerChildren(parent, rect, byPath, layoutByPath, diagnostics, options, layoutChildren) {
|
|
948
|
+
layoutFitChildren(parent, contentRectForStyleBox(rect, panelStyleBoxMetrics(parent, options)), byPath, layoutByPath, diagnostics, options, layoutChildren);
|
|
949
|
+
}
|
|
950
|
+
function layoutScrollContainerChildren(parent, rect, byPath, layoutByPath, diagnostics, options, layoutChildren) {
|
|
951
|
+
const content = contentRectForStyleBox(rect, panelStyleBoxMetrics(parent, options));
|
|
952
|
+
const scrollX = numeric(parent.props, "scroll_horizontal") ?? 0;
|
|
953
|
+
const scrollY = numeric(parent.props, "scroll_vertical") ?? 0;
|
|
954
|
+
const parentLayout = layoutByPath.get(parent.path);
|
|
955
|
+
for (const childPath of parent.children) {
|
|
956
|
+
const child = byPath.get(childPath);
|
|
957
|
+
if (!child) continue;
|
|
958
|
+
const size = preferredSize(child, byPath, options);
|
|
959
|
+
const childRect = {
|
|
960
|
+
x: content.x - scrollX,
|
|
961
|
+
y: content.y - scrollY,
|
|
962
|
+
width: hasExpandFlag(child, true) ? Math.max(content.width, size.width) : size.width,
|
|
963
|
+
height: hasExpandFlag(child, false) ? Math.max(content.height, size.height) : size.height
|
|
964
|
+
};
|
|
965
|
+
layoutByPath.set(child.path, makeLayoutNode(child, childRect, parentLayout));
|
|
966
|
+
layoutChildren(child, childRect, byPath, layoutByPath, diagnostics, options);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function layoutMarginChildren(parent, rect, byPath, layoutByPath, diagnostics, options, layoutChildren) {
|
|
970
|
+
const left = themeNumber(parent, "margin_left", options) ?? 0;
|
|
971
|
+
const top = themeNumber(parent, "margin_top", options) ?? 0;
|
|
972
|
+
const right = themeNumber(parent, "margin_right", options) ?? 0;
|
|
973
|
+
const bottom = themeNumber(parent, "margin_bottom", options) ?? 0;
|
|
974
|
+
layoutFitChildren(parent, normalizeRect({
|
|
975
|
+
x: rect.x + left,
|
|
976
|
+
y: rect.y + top,
|
|
977
|
+
width: rect.width - left - right,
|
|
978
|
+
height: rect.height - top - bottom
|
|
979
|
+
}), byPath, layoutByPath, diagnostics, options, layoutChildren);
|
|
980
|
+
}
|
|
981
|
+
function layoutFitChildren(parent, content, byPath, layoutByPath, diagnostics, options, layoutChildren) {
|
|
982
|
+
const parentLayout = layoutByPath.get(parent.path);
|
|
983
|
+
for (const childPath of parent.children) {
|
|
984
|
+
const child = byPath.get(childPath);
|
|
985
|
+
if (!child) continue;
|
|
986
|
+
const minimum = preferredSize(child, byPath, options);
|
|
987
|
+
const horizontal = axisPlacement(content.x, content.width, minimum.width, child, true);
|
|
988
|
+
const vertical = axisPlacement(content.y, content.height, minimum.height, child, false);
|
|
989
|
+
const childRect = {
|
|
990
|
+
x: horizontal.position,
|
|
991
|
+
y: vertical.position,
|
|
992
|
+
width: horizontal.size,
|
|
993
|
+
height: vertical.size
|
|
994
|
+
};
|
|
995
|
+
layoutByPath.set(child.path, makeLayoutNode(child, childRect, parentLayout));
|
|
996
|
+
layoutChildren(child, childRect, byPath, layoutByPath, diagnostics, options);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function layoutCenterChildren(parent, rect, byPath, layoutByPath, diagnostics, options, layoutChildren) {
|
|
1000
|
+
for (const childPath of parent.children) {
|
|
1001
|
+
const child = byPath.get(childPath);
|
|
1002
|
+
if (!child) continue;
|
|
1003
|
+
const size = preferredSize(child, byPath, options);
|
|
1004
|
+
const childRect = {
|
|
1005
|
+
x: rect.x + (rect.width - size.width) / 2,
|
|
1006
|
+
y: rect.y + (rect.height - size.height) / 2,
|
|
1007
|
+
width: size.width,
|
|
1008
|
+
height: size.height
|
|
1009
|
+
};
|
|
1010
|
+
layoutByPath.set(childPath, makeLayoutNode(child, childRect, layoutByPath.get(parent.path)));
|
|
1011
|
+
layoutChildren(child, childRect, byPath, layoutByPath, diagnostics, options);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function axisPlacement(start, available, minimum, indexed, horizontalAxis) {
|
|
1015
|
+
const flags = numeric(indexed.props, horizontalAxis ? "size_flags_horizontal" : "size_flags_vertical") ?? 0;
|
|
1016
|
+
const shrinkCenter = (flags & 4) === 4;
|
|
1017
|
+
const shrinkEnd = (flags & 8) === 8;
|
|
1018
|
+
const size = shrinkCenter || shrinkEnd ? minimum : Math.max(available, minimum);
|
|
1019
|
+
return {
|
|
1020
|
+
position: shrinkEnd ? start + Math.max(0, available - size) : shrinkCenter ? start + Math.max(0, (available - size) / 2) : start,
|
|
1021
|
+
size
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
//#endregion
|
|
1025
|
+
//#region src/membership.ts
|
|
1026
|
+
/**
|
|
1027
|
+
* The nodes the rect cascade ({@link resolveGodotSceneTree}) would lay out, without
|
|
1028
|
+
* running it. The cascade drops exactly one class of node: an invisible child of a
|
|
1029
|
+
* Box/Grid/Flow container — and its whole subtree — because a hidden control takes
|
|
1030
|
+
* no space in those containers (`visibleChildren` in minimum-size.ts; every other
|
|
1031
|
+
* container type and plain parents lay out all children). Consumers that only need
|
|
1032
|
+
* per-node structural fields (`drawOrder`, `type`, `source`, `properties`) can use
|
|
1033
|
+
* this instead of the cascade and skip all rect math; `membership.test.ts` pins the
|
|
1034
|
+
* equivalence against the cascade.
|
|
1035
|
+
*/
|
|
1036
|
+
function flattenSceneGraphNodes(graph) {
|
|
1037
|
+
const byPath = new Map(graph.nodes.map((node) => [node.path, node]));
|
|
1038
|
+
const out = [];
|
|
1039
|
+
const visit = (node) => {
|
|
1040
|
+
out.push(node);
|
|
1041
|
+
const skipsInvisibleChildren = isBoxContainerType(node.type) || isFlowContainerType(node.type) || node.type === "GridContainer";
|
|
1042
|
+
for (const childPath of node.children) {
|
|
1043
|
+
const child = byPath.get(childPath);
|
|
1044
|
+
if (!child) continue;
|
|
1045
|
+
if (skipsInvisibleChildren && !child.visible) continue;
|
|
1046
|
+
visit(child);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
for (const node of graph.nodes) if (node.parentPath === null) visit(node);
|
|
1050
|
+
return out;
|
|
1051
|
+
}
|
|
1052
|
+
//#endregion
|
|
1053
|
+
//#region src/index.ts
|
|
1054
|
+
function isGodotSceneTree(value) {
|
|
1055
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1056
|
+
const tree = value;
|
|
1057
|
+
return isRectLike(tree.viewport) && Array.isArray(tree.nodes) && tree.nodes.every(isTreeNode) && (tree.diagnostics === void 0 || Array.isArray(tree.diagnostics)) && (tree.resourceStatuses === void 0 || Array.isArray(tree.resourceStatuses));
|
|
1058
|
+
}
|
|
1059
|
+
function isRectLike(value) {
|
|
1060
|
+
return typeof value === "object" && value !== null && typeof value.x === "number" && typeof value.y === "number" && typeof value.width === "number" && typeof value.height === "number";
|
|
1061
|
+
}
|
|
1062
|
+
function isTreeNode(value) {
|
|
1063
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1064
|
+
const node = value;
|
|
1065
|
+
return typeof node.path === "string" && typeof node.name === "string" && typeof node.type === "string" && (typeof node.parentPath === "string" || node.parentPath === null) && Array.isArray(node.children) && isRectLike(node.rect) && typeof node.visible === "boolean" && typeof node.zIndex === "number" && typeof node.drawOrder === "number" && typeof node.zAsRelative === "boolean" && typeof node.showBehindParent === "boolean" && typeof node.clipContents === "boolean" && typeof node.properties === "object" && node.properties !== null && Array.isArray(node.resourceRefs);
|
|
1066
|
+
}
|
|
1067
|
+
const DEFAULT_VIEWPORT = {
|
|
1068
|
+
x: 0,
|
|
1069
|
+
y: 0,
|
|
1070
|
+
width: 1280,
|
|
1071
|
+
height: 720
|
|
1072
|
+
};
|
|
1073
|
+
const MAX_LAYOUT_PASSES = 8;
|
|
1074
|
+
function toIndexedNode(node) {
|
|
1075
|
+
return {
|
|
1076
|
+
node: node.source ?? {
|
|
1077
|
+
name: node.name,
|
|
1078
|
+
type: node.type,
|
|
1079
|
+
attributes: {},
|
|
1080
|
+
properties: node.properties
|
|
1081
|
+
},
|
|
1082
|
+
path: node.path,
|
|
1083
|
+
parentPath: node.parentPath,
|
|
1084
|
+
children: [...node.children],
|
|
1085
|
+
props: node.properties,
|
|
1086
|
+
order: node.drawOrder
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
function resolveGodotSceneTree(graph, options = {}) {
|
|
1090
|
+
const viewport = {
|
|
1091
|
+
...DEFAULT_VIEWPORT,
|
|
1092
|
+
...options.viewport
|
|
1093
|
+
};
|
|
1094
|
+
const indexed = graph.nodes.map(toIndexedNode);
|
|
1095
|
+
const byPath = new Map(indexed.map((node) => [node.path, node]));
|
|
1096
|
+
const roots = indexed.filter((node) => node.parentPath === null);
|
|
1097
|
+
const flows = indexed.filter((node) => isFlowContainerType(node.node.type ?? "Node"));
|
|
1098
|
+
let layoutByPath = /* @__PURE__ */ new Map();
|
|
1099
|
+
let diagnostics = [];
|
|
1100
|
+
for (let pass = 0; pass < MAX_LAYOUT_PASSES; pass++) {
|
|
1101
|
+
layoutByPath = /* @__PURE__ */ new Map();
|
|
1102
|
+
diagnostics = [];
|
|
1103
|
+
for (const node of indexed) node.minimumSize = void 0;
|
|
1104
|
+
for (const root of roots) layoutNode(root.path, viewport, byPath, layoutByPath, diagnostics, options);
|
|
1105
|
+
if (flows.length === 0 || !updateFlowExtents(flows, layoutByPath)) break;
|
|
1106
|
+
if (pass === MAX_LAYOUT_PASSES - 1) diagnostics.push({
|
|
1107
|
+
severity: "warning",
|
|
1108
|
+
code: "flow-layout-unconverged",
|
|
1109
|
+
message: "Flow container layout did not converge within the pass limit; rects may be unsettled."
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
resolveAnchors(layoutByPath, options.anchorsByPath, diagnostics);
|
|
1113
|
+
return {
|
|
1114
|
+
viewport,
|
|
1115
|
+
nodes: [...layoutByPath.values()].sort((left, right) => left.zIndex - right.zIndex || left.drawOrder - right.drawOrder),
|
|
1116
|
+
diagnostics,
|
|
1117
|
+
resourceStatuses: graph.resourceStatuses
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Records each flow container's resolved main-axis extent for the next pass.
|
|
1122
|
+
* Returns true if any extent changed (i.e. another pass is warranted).
|
|
1123
|
+
*/
|
|
1124
|
+
function updateFlowExtents(flows, layoutByPath) {
|
|
1125
|
+
let changed = false;
|
|
1126
|
+
for (const flow of flows) {
|
|
1127
|
+
const node = layoutByPath.get(flow.path);
|
|
1128
|
+
if (!node) continue;
|
|
1129
|
+
const extent = flowContainerVertical(flow) ? node.rect.height : node.rect.width;
|
|
1130
|
+
if (flow.flowMainExtent === void 0 || Math.abs(flow.flowMainExtent - extent) > 1e-6) {
|
|
1131
|
+
flow.flowMainExtent = extent;
|
|
1132
|
+
changed = true;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return changed;
|
|
1136
|
+
}
|
|
1137
|
+
function layoutNode(path, parentRect, byPath, layoutByPath, diagnostics, options) {
|
|
1138
|
+
const indexed = byPath.get(path);
|
|
1139
|
+
if (!indexed) return;
|
|
1140
|
+
if (layoutByPath.has(path)) return layoutByPath.get(path);
|
|
1141
|
+
const rect = indexed.parentPath === null ? rootRect(indexed, parentRect, byPath, options) : controlRect(indexed, parentRect, byPath, options);
|
|
1142
|
+
const computedNode = makeLayoutNode(indexed, rect, indexed.parentPath ? layoutByPath.get(indexed.parentPath) : void 0);
|
|
1143
|
+
layoutByPath.set(path, computedNode);
|
|
1144
|
+
layoutNodeChildren(indexed, rect, byPath, layoutByPath, diagnostics, options);
|
|
1145
|
+
return computedNode;
|
|
1146
|
+
}
|
|
1147
|
+
function layoutNodeChildren(indexed, rect, byPath, layoutByPath, diagnostics, options) {
|
|
1148
|
+
const type = indexed.node.type ?? "Node";
|
|
1149
|
+
if (isBoxContainerType(type)) layoutBoxContainerChildren(indexed, rect, byPath, layoutByPath, diagnostics, options, boxContainerHorizontal(indexed), layoutNodeChildren);
|
|
1150
|
+
else if (type === "AspectRatioContainer") layoutAspectRatioContainerChildren(indexed, rect, byPath, layoutByPath, diagnostics, options, layoutNodeChildren);
|
|
1151
|
+
else if (type === "GridContainer") layoutGridContainerChildren(indexed, rect, byPath, layoutByPath, diagnostics, options, layoutNodeChildren);
|
|
1152
|
+
else if (isFlowContainerType(type)) layoutFlowContainerChildren(indexed, rect, byPath, layoutByPath, diagnostics, options, flowContainerVertical(indexed), layoutNodeChildren);
|
|
1153
|
+
else if (type === "PanelContainer") layoutPanelContainerChildren(indexed, rect, byPath, layoutByPath, diagnostics, options, layoutNodeChildren);
|
|
1154
|
+
else if (type === "ScrollContainer") layoutScrollContainerChildren(indexed, rect, byPath, layoutByPath, diagnostics, options, layoutNodeChildren);
|
|
1155
|
+
else if (type === "MarginContainer") layoutMarginChildren(indexed, rect, byPath, layoutByPath, diagnostics, options, layoutNodeChildren);
|
|
1156
|
+
else if (type === "CenterContainer") layoutCenterChildren(indexed, rect, byPath, layoutByPath, diagnostics, options, layoutNodeChildren);
|
|
1157
|
+
else for (const childPath of indexed.children) layoutNode(childPath, rect, byPath, layoutByPath, diagnostics, options);
|
|
1158
|
+
}
|
|
1159
|
+
//#endregion
|
|
1160
|
+
export { anchorEdgePoint, flattenSceneGraphNodes, isGodotSceneTree, parseAnchorEdge, resolveGodotSceneTree };
|
|
1161
|
+
|
|
1162
|
+
//# sourceMappingURL=index.js.map
|