@aethervtt/shard-ui 0.0.1
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/README.md +3 -0
- package/dist/components.d.ts +144 -0
- package/dist/components.js +355 -0
- package/dist/flex.d.ts +107 -0
- package/dist/flex.js +751 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +133 -0
- package/dist/interaction.d.ts +44 -0
- package/dist/interaction.js +416 -0
- package/dist/length.d.ts +17 -0
- package/dist/length.js +92 -0
- package/dist/methods.d.ts +52 -0
- package/dist/methods.js +187 -0
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.js +40 -0
- package/dist/render.d.ts +76 -0
- package/dist/render.js +715 -0
- package/dist/shaders.d.ts +2 -0
- package/dist/shaders.js +124 -0
- package/dist/style.d.ts +45 -0
- package/dist/style.js +111 -0
- package/dist/testing.d.ts +20 -0
- package/dist/testing.js +68 -0
- package/dist/theme.d.ts +30 -0
- package/dist/theme.js +172 -0
- package/dist/tree.d.ts +172 -0
- package/dist/tree.js +1109 -0
- package/package.json +42 -0
package/dist/tree.js
ADDED
|
@@ -0,0 +1,1109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChildOf,
|
|
3
|
+
Children,
|
|
4
|
+
defineResource,
|
|
5
|
+
defineSystem,
|
|
6
|
+
onAdd,
|
|
7
|
+
onRemove,
|
|
8
|
+
onSet,
|
|
9
|
+
ShardError
|
|
10
|
+
} from "@aethervtt/shard-core";
|
|
11
|
+
import { Camera3d, RenderTargets, Window } from "@aethervtt/shard-render";
|
|
12
|
+
import { LogResource } from "@aethervtt/shard-runtime";
|
|
13
|
+
import { TextureAtlases } from "@aethervtt/shard-sprite";
|
|
14
|
+
import {
|
|
15
|
+
Fonts,
|
|
16
|
+
Localized,
|
|
17
|
+
layoutText,
|
|
18
|
+
TextLayout
|
|
19
|
+
} from "@aethervtt/shard-text";
|
|
20
|
+
import { Textures } from "@aethervtt/shard-texture";
|
|
21
|
+
import { GlobalTransform } from "@aethervtt/shard-transform";
|
|
22
|
+
import {
|
|
23
|
+
UiAnchor,
|
|
24
|
+
UiAnchorArrow,
|
|
25
|
+
UiButton,
|
|
26
|
+
UiDefaults,
|
|
27
|
+
UiImage,
|
|
28
|
+
UiInteraction,
|
|
29
|
+
UiLayout,
|
|
30
|
+
UiNode,
|
|
31
|
+
UiRoot,
|
|
32
|
+
UiSlider,
|
|
33
|
+
UiStyle,
|
|
34
|
+
UiText,
|
|
35
|
+
UiTextInput,
|
|
36
|
+
UiToggle
|
|
37
|
+
} from "./components.js";
|
|
38
|
+
import { FlexTree, LENGTHS, layoutTree } from "./flex.js";
|
|
39
|
+
import { ResolvedStyle, resolveStyle } from "./style.js";
|
|
40
|
+
import { UiThemes } from "./theme.js";
|
|
41
|
+
const Kind = { None: 0, Button: 1, Toggle: 2, Slider: 3, TextInput: 4 };
|
|
42
|
+
const AnchorState = { None: 0, OnScreen: 1, Clamped: 2, Hidden: 3 };
|
|
43
|
+
const Overflow = { Visible: 0, Clip: 1, Scroll: 2 };
|
|
44
|
+
function growF(a, n) {
|
|
45
|
+
if (a.length >= n) return a;
|
|
46
|
+
const out = new Float32Array(Math.max(n, a.length * 2));
|
|
47
|
+
out.set(a);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function growI(a, n) {
|
|
51
|
+
if (a.length >= n) return a;
|
|
52
|
+
const out = new Int32Array(Math.max(n, a.length * 2));
|
|
53
|
+
out.set(a);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function growU(a, n) {
|
|
57
|
+
if (a.length >= n) return a;
|
|
58
|
+
const out = new Uint8Array(Math.max(n, a.length * 2));
|
|
59
|
+
out.set(a);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
class UiRootState {
|
|
63
|
+
entity;
|
|
64
|
+
tree = new FlexTree();
|
|
65
|
+
/** Node index → entity. Node 0 is the root. */
|
|
66
|
+
entities = [];
|
|
67
|
+
parent = new Int32Array(16);
|
|
68
|
+
/** Index past the node's subtree. */
|
|
69
|
+
end = new Int32Array(16);
|
|
70
|
+
/** Node indices in paint order (siblings by zIndex, stable). */
|
|
71
|
+
paint = new Int32Array(16);
|
|
72
|
+
/** Computed rect (root pixels), after scroll and anchor scale. */
|
|
73
|
+
x = new Float32Array(16);
|
|
74
|
+
y = new Float32Array(16);
|
|
75
|
+
w = new Float32Array(16);
|
|
76
|
+
h = new Float32Array(16);
|
|
77
|
+
/** Clip applied to the node (x0, y0, x1, y1), and what it passes to its children. */
|
|
78
|
+
clip = new Float32Array(64);
|
|
79
|
+
childClip = new Float32Array(64);
|
|
80
|
+
visible = new Uint8Array(16);
|
|
81
|
+
scale = new Float32Array(16);
|
|
82
|
+
angle = new Float32Array(16);
|
|
83
|
+
anchorState = new Uint8Array(16);
|
|
84
|
+
distance = new Float32Array(16);
|
|
85
|
+
contentW = new Float32Array(16);
|
|
86
|
+
contentH = new Float32Array(16);
|
|
87
|
+
overflow = new Uint8Array(16);
|
|
88
|
+
scrollX = new Float32Array(16);
|
|
89
|
+
scrollY = new Float32Array(16);
|
|
90
|
+
/** 1: has UiAnchor; 2: UiAnchorArrow. */
|
|
91
|
+
anchorKind = new Uint8Array(16);
|
|
92
|
+
/** Widget kind (Kind), disabled, and whether the node takes the pointer. */
|
|
93
|
+
kind = new Uint8Array(16);
|
|
94
|
+
disabled = new Uint8Array(16);
|
|
95
|
+
solid = new Uint8Array(16);
|
|
96
|
+
/** This frame: its flex tree is refilled and laid out; its UiLayout rows are written. */
|
|
97
|
+
filling = false;
|
|
98
|
+
writing = false;
|
|
99
|
+
/** Widgets, styles, or images changed: refresh kind, disabled, and solid. */
|
|
100
|
+
flagsDirty = true;
|
|
101
|
+
/** order and zIndex per node when the tree was built. */
|
|
102
|
+
sortKeys = new Int16Array(32);
|
|
103
|
+
/** Distance scale of each anchor (1 for other nodes). */
|
|
104
|
+
anchorScale = new Float32Array(16);
|
|
105
|
+
/** Nodes that measure text, by node index. */
|
|
106
|
+
texts = [];
|
|
107
|
+
/** Image intrinsic size (0 until the texture loads). */
|
|
108
|
+
imageW = new Float32Array(16);
|
|
109
|
+
imageH = new Float32Array(16);
|
|
110
|
+
count = 0;
|
|
111
|
+
hasAnchors = false;
|
|
112
|
+
/** Root pixels → screen pixels. */
|
|
113
|
+
factor = 1;
|
|
114
|
+
width = 0;
|
|
115
|
+
height = 0;
|
|
116
|
+
viewportW = 0;
|
|
117
|
+
viewportH = 0;
|
|
118
|
+
camera = null;
|
|
119
|
+
theme;
|
|
120
|
+
themeVersion = -1;
|
|
121
|
+
order = 0;
|
|
122
|
+
dirty = true;
|
|
123
|
+
/** Frame counter of the last layout, for describe. */
|
|
124
|
+
laidOutAt = -1;
|
|
125
|
+
/** Something visual changed since the renderer last built this root. */
|
|
126
|
+
visualVersion = 0;
|
|
127
|
+
constructor(entity) {
|
|
128
|
+
this.entity = entity;
|
|
129
|
+
}
|
|
130
|
+
reserve(n) {
|
|
131
|
+
this.parent = growI(this.parent, n);
|
|
132
|
+
this.end = growI(this.end, n);
|
|
133
|
+
this.paint = growI(this.paint, n);
|
|
134
|
+
this.x = growF(this.x, n);
|
|
135
|
+
this.y = growF(this.y, n);
|
|
136
|
+
this.w = growF(this.w, n);
|
|
137
|
+
this.h = growF(this.h, n);
|
|
138
|
+
this.clip = growF(this.clip, n * 4);
|
|
139
|
+
this.childClip = growF(this.childClip, n * 4);
|
|
140
|
+
this.visible = growU(this.visible, n);
|
|
141
|
+
this.scale = growF(this.scale, n);
|
|
142
|
+
this.angle = growF(this.angle, n);
|
|
143
|
+
this.anchorState = growU(this.anchorState, n);
|
|
144
|
+
this.distance = growF(this.distance, n);
|
|
145
|
+
this.contentW = growF(this.contentW, n);
|
|
146
|
+
this.contentH = growF(this.contentH, n);
|
|
147
|
+
this.overflow = growU(this.overflow, n);
|
|
148
|
+
this.scrollX = growF(this.scrollX, n);
|
|
149
|
+
this.scrollY = growF(this.scrollY, n);
|
|
150
|
+
this.anchorKind = growU(this.anchorKind, n);
|
|
151
|
+
this.anchorScale = growF(this.anchorScale, n);
|
|
152
|
+
this.kind = growU(this.kind, n);
|
|
153
|
+
this.disabled = growU(this.disabled, n);
|
|
154
|
+
this.solid = growU(this.solid, n);
|
|
155
|
+
if (this.sortKeys.length < n * 2) {
|
|
156
|
+
const keys = new Int16Array(n * 4);
|
|
157
|
+
keys.set(this.sortKeys);
|
|
158
|
+
this.sortKeys = keys;
|
|
159
|
+
}
|
|
160
|
+
this.imageW = growF(this.imageW, n);
|
|
161
|
+
this.imageH = growF(this.imageH, n);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
class UiStore {
|
|
165
|
+
roots = [];
|
|
166
|
+
rootOf = /* @__PURE__ */ new Map();
|
|
167
|
+
indexOf = /* @__PURE__ */ new Map();
|
|
168
|
+
/** UiNode entities outside any UiRoot (ui/no-root), reported once each. */
|
|
169
|
+
orphans = /* @__PURE__ */ new Set();
|
|
170
|
+
reportedOrphans = /* @__PURE__ */ new Set();
|
|
171
|
+
/** Styles nodes name that their root's theme lacks (ui/unknown-style), reported once each. */
|
|
172
|
+
reportedStyles = /* @__PURE__ */ new Set();
|
|
173
|
+
structureDirty = true;
|
|
174
|
+
frame = 0;
|
|
175
|
+
/** This frame: roots laid out, nodes they had, anchor-only repositions, layout calls. */
|
|
176
|
+
layouts = 0;
|
|
177
|
+
nodesLaidOut = 0;
|
|
178
|
+
repositions = 0;
|
|
179
|
+
flexCalls = 0;
|
|
180
|
+
totalLayouts = 0;
|
|
181
|
+
root(entity) {
|
|
182
|
+
return this.rootOf.get(entity);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const UiState = defineResource("ui/UiState", {
|
|
186
|
+
description: "UI trees by root: node order, computed rects, clips, and per-frame layout counts.",
|
|
187
|
+
init: () => new UiStore()
|
|
188
|
+
});
|
|
189
|
+
function observeUiStructure(world) {
|
|
190
|
+
const mark = () => {
|
|
191
|
+
const store = world.tryResource(UiState);
|
|
192
|
+
if (store) store.structureDirty = true;
|
|
193
|
+
};
|
|
194
|
+
world.observe(onAdd(UiNode), mark);
|
|
195
|
+
world.observe(onRemove(UiNode), mark);
|
|
196
|
+
world.observe(onAdd(UiRoot), mark);
|
|
197
|
+
world.observe(onRemove(UiRoot), mark);
|
|
198
|
+
world.observe(onAdd(UiAnchor), mark);
|
|
199
|
+
world.observe(onRemove(UiAnchor), mark);
|
|
200
|
+
world.observe(onAdd(UiText), mark);
|
|
201
|
+
world.observe(onRemove(UiText), mark);
|
|
202
|
+
world.observe(onAdd(UiImage), mark);
|
|
203
|
+
world.observe(onRemove(UiImage), mark);
|
|
204
|
+
const reparent = ({ entity }) => {
|
|
205
|
+
if (world.isAlive(entity) && world.has(entity, UiNode)) mark();
|
|
206
|
+
};
|
|
207
|
+
world.observe(onAdd(ChildOf), reparent);
|
|
208
|
+
world.observe(onSet(ChildOf), reparent);
|
|
209
|
+
world.observe(onRemove(ChildOf), reparent);
|
|
210
|
+
}
|
|
211
|
+
function nodeColumn(world, entity, field) {
|
|
212
|
+
const table = world.entityTable(entity);
|
|
213
|
+
return table.column(UiNode, field)[world.entityRow(entity)];
|
|
214
|
+
}
|
|
215
|
+
function rebuildTrees(world, store, roots) {
|
|
216
|
+
const previous = new Map(store.roots.map((r) => [r.entity, r]));
|
|
217
|
+
store.roots = [];
|
|
218
|
+
store.rootOf.clear();
|
|
219
|
+
store.indexOf.clear();
|
|
220
|
+
for (const rootEntity of roots) {
|
|
221
|
+
const r = previous.get(rootEntity) ?? new UiRootState(rootEntity);
|
|
222
|
+
const entities = [];
|
|
223
|
+
const parents = [];
|
|
224
|
+
const ends = [];
|
|
225
|
+
const visit = (e, parent) => {
|
|
226
|
+
const index = entities.length;
|
|
227
|
+
entities.push(e);
|
|
228
|
+
parents.push(parent);
|
|
229
|
+
ends.push(0);
|
|
230
|
+
const children = world.tryGet(e, Children)?.entities ?? [];
|
|
231
|
+
const ui = children.filter(
|
|
232
|
+
(c) => c !== null && world.isAlive(c) && world.has(c, UiNode) && !world.has(c, UiRoot)
|
|
233
|
+
);
|
|
234
|
+
ui.sort((a, b) => nodeColumn(world, a, "order") - nodeColumn(world, b, "order"));
|
|
235
|
+
for (const c of ui) visit(c, index);
|
|
236
|
+
ends[index] = entities.length;
|
|
237
|
+
};
|
|
238
|
+
visit(rootEntity, -1);
|
|
239
|
+
const n = entities.length;
|
|
240
|
+
r.reserve(n);
|
|
241
|
+
r.entities = entities;
|
|
242
|
+
r.count = n;
|
|
243
|
+
r.texts.length = n;
|
|
244
|
+
for (let i = 0; i < n; i++) {
|
|
245
|
+
r.texts[i] = void 0;
|
|
246
|
+
r.parent[i] = parents[i];
|
|
247
|
+
r.end[i] = ends[i];
|
|
248
|
+
r.anchorScale[i] = 1;
|
|
249
|
+
r.anchorState[i] = 0;
|
|
250
|
+
r.imageW[i] = 0;
|
|
251
|
+
r.imageH[i] = 0;
|
|
252
|
+
}
|
|
253
|
+
let p = 0;
|
|
254
|
+
const paintVisit = (at) => {
|
|
255
|
+
r.paint[p++] = at;
|
|
256
|
+
const children = [];
|
|
257
|
+
for (let c = at + 1; c < r.end[at]; c = r.end[c]) children.push(c);
|
|
258
|
+
children.sort(
|
|
259
|
+
(a, b) => nodeColumn(world, r.entities[a], "zIndex") - nodeColumn(world, r.entities[b], "zIndex")
|
|
260
|
+
);
|
|
261
|
+
for (const c of children) paintVisit(c);
|
|
262
|
+
};
|
|
263
|
+
paintVisit(0);
|
|
264
|
+
r.hasAnchors = false;
|
|
265
|
+
for (let i = 0; i < n; i++) {
|
|
266
|
+
const e = r.entities[i];
|
|
267
|
+
r.sortKeys[i * 2] = nodeColumn(world, e, "order");
|
|
268
|
+
r.sortKeys[i * 2 + 1] = nodeColumn(world, e, "zIndex");
|
|
269
|
+
store.rootOf.set(e, r);
|
|
270
|
+
store.indexOf.set(e, i);
|
|
271
|
+
const kind = world.has(e, UiAnchor) ? 1 : world.has(e, UiAnchorArrow) ? 2 : 0;
|
|
272
|
+
r.anchorKind[i] = kind;
|
|
273
|
+
if (kind === 1) r.hasAnchors = true;
|
|
274
|
+
}
|
|
275
|
+
r.dirty = true;
|
|
276
|
+
r.flagsDirty = true;
|
|
277
|
+
store.roots.push(r);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function cameraFor(world, wanted, cameras) {
|
|
281
|
+
if (wanted !== null && world.isAlive(wanted) && world.has(wanted, Camera3d)) return wanted;
|
|
282
|
+
let best = null;
|
|
283
|
+
let bestOrder = Number.POSITIVE_INFINITY;
|
|
284
|
+
for (const table of cameras) {
|
|
285
|
+
const order = table.column(Camera3d, "order");
|
|
286
|
+
const active = table.column(Camera3d, "active");
|
|
287
|
+
for (let i = 0; i < table.count; i++) {
|
|
288
|
+
if (active[i] === 0) continue;
|
|
289
|
+
if (order[i] < bestOrder) {
|
|
290
|
+
bestOrder = order[i];
|
|
291
|
+
best = table.entities[i];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return best;
|
|
296
|
+
}
|
|
297
|
+
const viewport = new Float32Array(2);
|
|
298
|
+
function viewportOf(world, camera) {
|
|
299
|
+
let target;
|
|
300
|
+
if (camera !== null) {
|
|
301
|
+
const ref = world.get(camera, Camera3d).target;
|
|
302
|
+
if (ref) target = world.tryResource(RenderTargets)?.get(ref);
|
|
303
|
+
}
|
|
304
|
+
target ??= world.tryResource(Window);
|
|
305
|
+
if (!target) return false;
|
|
306
|
+
viewport[0] = target.width;
|
|
307
|
+
viewport[1] = target.height;
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
const measureOptions = {
|
|
311
|
+
anchor: [0, 1],
|
|
312
|
+
size: 16,
|
|
313
|
+
align: "left",
|
|
314
|
+
maxWidth: 0,
|
|
315
|
+
lineHeight: 1.2
|
|
316
|
+
};
|
|
317
|
+
const scratchLayout = new TextLayout();
|
|
318
|
+
function measureTextNode(t, maxWidth, out, into = scratchLayout) {
|
|
319
|
+
if (!t.font) {
|
|
320
|
+
out[0] = 0;
|
|
321
|
+
out[1] = 0;
|
|
322
|
+
return into;
|
|
323
|
+
}
|
|
324
|
+
measureOptions.size = t.size;
|
|
325
|
+
measureOptions.lineHeight = t.lineHeight;
|
|
326
|
+
measureOptions.maxWidth = t.wrap && !Number.isNaN(maxWidth) ? Math.max(maxWidth, 1e-3) : 0;
|
|
327
|
+
layoutText(t.font, t.value, measureOptions, into);
|
|
328
|
+
out[0] = into.width;
|
|
329
|
+
out[1] = Math.max(1, into.lineCount) * t.lineHeight * t.size;
|
|
330
|
+
return into;
|
|
331
|
+
}
|
|
332
|
+
const flagStyle = new ResolvedStyle();
|
|
333
|
+
function refreshFlags(world, r) {
|
|
334
|
+
for (let i = 0; i < r.count; i++) {
|
|
335
|
+
const e = r.entities[i];
|
|
336
|
+
r.kind[i] = Kind.None;
|
|
337
|
+
r.solid[i] = 0;
|
|
338
|
+
r.disabled[i] = 0;
|
|
339
|
+
if (!world.isAlive(e)) continue;
|
|
340
|
+
const table = world.entityTable(e);
|
|
341
|
+
const row = world.entityRow(e);
|
|
342
|
+
let kind = Kind.None;
|
|
343
|
+
let disabled = 0;
|
|
344
|
+
if (table.has(UiButton)) {
|
|
345
|
+
kind = Kind.Button;
|
|
346
|
+
disabled = table.column(UiButton, "disabled")[row];
|
|
347
|
+
} else if (table.has(UiToggle)) {
|
|
348
|
+
kind = Kind.Toggle;
|
|
349
|
+
disabled = table.column(UiToggle, "disabled")[row];
|
|
350
|
+
} else if (table.has(UiSlider)) {
|
|
351
|
+
kind = Kind.Slider;
|
|
352
|
+
disabled = table.column(UiSlider, "disabled")[row];
|
|
353
|
+
} else if (table.has(UiTextInput)) {
|
|
354
|
+
kind = Kind.TextInput;
|
|
355
|
+
disabled = table.column(UiTextInput, "disabled")[row];
|
|
356
|
+
}
|
|
357
|
+
r.kind[i] = kind;
|
|
358
|
+
r.disabled[i] = disabled;
|
|
359
|
+
let solid = kind !== Kind.None || table.has(UiImage) || r.overflow[i] === 2;
|
|
360
|
+
if (!solid && table.has(UiStyle)) {
|
|
361
|
+
resolveStyle(
|
|
362
|
+
flagStyle,
|
|
363
|
+
textColumns(table),
|
|
364
|
+
row,
|
|
365
|
+
r.theme,
|
|
366
|
+
table.column(UiNode, "style")[row] ?? "",
|
|
367
|
+
0
|
|
368
|
+
);
|
|
369
|
+
solid = flagStyle.background[3] > 0 || flagStyle.borderWidth > 0 && flagStyle.borderColor[3] > 0;
|
|
370
|
+
} else if (!solid && r.theme) {
|
|
371
|
+
const name2 = table.column(UiNode, "style")[row] ?? "";
|
|
372
|
+
if (name2 !== "") {
|
|
373
|
+
resolveStyle(flagStyle, textColumns(table), row, r.theme, name2, 0);
|
|
374
|
+
solid = flagStyle.background[3] > 0;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
r.solid[i] = solid ? 1 : 0;
|
|
378
|
+
const name = table.column(UiNode, "style")[row] ?? "";
|
|
379
|
+
if (name !== "" && r.theme && !r.theme.styles.has(name)) reportStyle(world, name);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function reportStyle(world, name) {
|
|
383
|
+
const store = world.resource(UiState);
|
|
384
|
+
if (store.reportedStyles.has(name)) return;
|
|
385
|
+
store.reportedStyles.add(name);
|
|
386
|
+
const log = world.tryResource(LogResource);
|
|
387
|
+
if (!log) return;
|
|
388
|
+
warn(
|
|
389
|
+
log,
|
|
390
|
+
new ShardError("ui/unknown-style", `No style "${name}" in the UI theme`, {
|
|
391
|
+
hint: "Add it to the root's *.theme.json styles, or fix UiNode.style (ui.describe shows each node's)."
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
function warn(log, err) {
|
|
396
|
+
log.warn(`${err.code}: ${err.message}`, { code: err.code, hint: err.hint });
|
|
397
|
+
}
|
|
398
|
+
function markVisual(store, tables, def, since, flags) {
|
|
399
|
+
for (const table of tables) {
|
|
400
|
+
if (table.lastChanged(def) <= since) continue;
|
|
401
|
+
const ticks = table.changedTicks(def);
|
|
402
|
+
for (let i = 0; i < table.count; i++) {
|
|
403
|
+
if (ticks[i] <= since) continue;
|
|
404
|
+
const root = store.rootOf.get(table.entities[i]);
|
|
405
|
+
if (!root) continue;
|
|
406
|
+
root.visualVersion++;
|
|
407
|
+
if (flags) root.flagsDirty = true;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function markNodes(store, tables, since) {
|
|
412
|
+
for (const table of tables) {
|
|
413
|
+
if (table.lastChanged(UiNode) <= since) continue;
|
|
414
|
+
const ticks = table.changedTicks(UiNode);
|
|
415
|
+
const order = table.column(UiNode, "order");
|
|
416
|
+
const z = table.column(UiNode, "zIndex");
|
|
417
|
+
for (let i = 0; i < table.count; i++) {
|
|
418
|
+
if (ticks[i] <= since) continue;
|
|
419
|
+
const e = table.entities[i];
|
|
420
|
+
const root = store.rootOf.get(e);
|
|
421
|
+
if (!root) {
|
|
422
|
+
store.structureDirty = true;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
root.dirty = true;
|
|
426
|
+
const k = store.indexOf.get(e) * 2;
|
|
427
|
+
if (order[i] !== root.sortKeys[k] || z[i] !== root.sortKeys[k + 1])
|
|
428
|
+
store.structureDirty = true;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function markChanged(store, tables, def, since) {
|
|
433
|
+
for (const table of tables) {
|
|
434
|
+
if (table.lastChanged(def) <= since) continue;
|
|
435
|
+
const ticks = table.changedTicks(def);
|
|
436
|
+
for (let i = 0; i < table.count; i++) {
|
|
437
|
+
if (ticks[i] <= since) continue;
|
|
438
|
+
const root = store.rootOf.get(table.entities[i]);
|
|
439
|
+
if (root) root.dirty = true;
|
|
440
|
+
else store.structureDirty = true;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const layoutUi = defineSystem({
|
|
445
|
+
name: "ui/layout",
|
|
446
|
+
description: "Lays out changed UI trees (flexbox), places anchored nodes, and writes UiLayout.",
|
|
447
|
+
setup: (world) => {
|
|
448
|
+
const s = {
|
|
449
|
+
roots: world.query({ with: [UiRoot, UiNode] }),
|
|
450
|
+
nodes: world.query({ with: [UiNode] }),
|
|
451
|
+
texts: world.query({ with: [UiText, UiNode] }),
|
|
452
|
+
images: world.query({ with: [UiImage, UiNode] }),
|
|
453
|
+
anchors: world.query({ with: [UiAnchor, UiNode] }),
|
|
454
|
+
cameras: world.query({ with: [Camera3d, GlobalTransform] }),
|
|
455
|
+
inputs: world.query({ with: [UiTextInput, UiNode] }),
|
|
456
|
+
styles: world.query({ with: [UiStyle, UiNode] }),
|
|
457
|
+
widgets: [
|
|
458
|
+
world.query({ with: [UiButton, UiNode] }),
|
|
459
|
+
world.query({ with: [UiToggle, UiNode] }),
|
|
460
|
+
world.query({ with: [UiSlider, UiNode] })
|
|
461
|
+
],
|
|
462
|
+
interactions: world.query({ with: [UiInteraction, UiNode] }),
|
|
463
|
+
measure: () => {
|
|
464
|
+
},
|
|
465
|
+
current: void 0,
|
|
466
|
+
style: new ResolvedStyle(),
|
|
467
|
+
lastTick: 0
|
|
468
|
+
};
|
|
469
|
+
s.measure = (node, maxWidth, out) => {
|
|
470
|
+
const r = s.current;
|
|
471
|
+
const text = r.texts[node];
|
|
472
|
+
if (text) {
|
|
473
|
+
measureTextNode(text, maxWidth, out);
|
|
474
|
+
if (r.imageW[node] > out[0]) out[0] = r.imageW[node];
|
|
475
|
+
if (r.imageH[node] > out[1]) out[1] = r.imageH[node];
|
|
476
|
+
} else {
|
|
477
|
+
out[0] = r.imageW[node];
|
|
478
|
+
out[1] = r.imageH[node];
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
return s;
|
|
482
|
+
},
|
|
483
|
+
run: (s, world, ctx) => {
|
|
484
|
+
const store = world.resource(UiState);
|
|
485
|
+
store.frame++;
|
|
486
|
+
store.layouts = 0;
|
|
487
|
+
store.nodesLaidOut = 0;
|
|
488
|
+
store.repositions = 0;
|
|
489
|
+
store.flexCalls = 0;
|
|
490
|
+
const since = s.lastTick;
|
|
491
|
+
s.lastTick = ctx.thisRunTick;
|
|
492
|
+
markNodes(store, s.nodes.tables, since);
|
|
493
|
+
markChanged(store, s.images.tables, UiImage, since);
|
|
494
|
+
markChanged(store, s.roots.tables, UiRoot, since);
|
|
495
|
+
if (store.structureDirty) {
|
|
496
|
+
store.structureDirty = false;
|
|
497
|
+
const roots = [];
|
|
498
|
+
for (const table of s.roots.tables)
|
|
499
|
+
for (let i = 0; i < table.count; i++) roots.push(table.entities[i]);
|
|
500
|
+
rebuildTrees(world, store, roots);
|
|
501
|
+
findOrphans(store, s.nodes.tables);
|
|
502
|
+
}
|
|
503
|
+
const themes = world.tryResource(UiThemes);
|
|
504
|
+
for (const r of store.roots) {
|
|
505
|
+
if (!world.isAlive(r.entity)) continue;
|
|
506
|
+
const table = world.entityTable(r.entity);
|
|
507
|
+
const row = world.entityRow(r.entity);
|
|
508
|
+
const scaleMode = table.column(UiRoot, "scale")[row];
|
|
509
|
+
const ref = table.column(UiRoot, "referenceSize");
|
|
510
|
+
const refW = ref[row * 2];
|
|
511
|
+
const refH = ref[row * 2 + 1];
|
|
512
|
+
r.order = table.column(UiRoot, "order")[row];
|
|
513
|
+
r.camera = cameraFor(world, table.column(UiRoot, "camera")[row] ?? null, s.cameras.tables);
|
|
514
|
+
const themeRef = table.column(UiRoot, "theme")[row];
|
|
515
|
+
const theme = themes?.get(themeRef);
|
|
516
|
+
if (theme !== r.theme || theme && theme.version !== r.themeVersion) {
|
|
517
|
+
r.theme = theme;
|
|
518
|
+
r.themeVersion = theme ? theme.version : -1;
|
|
519
|
+
r.dirty = true;
|
|
520
|
+
r.flagsDirty = true;
|
|
521
|
+
r.visualVersion++;
|
|
522
|
+
}
|
|
523
|
+
let vw = refW;
|
|
524
|
+
let vh = refH;
|
|
525
|
+
if (viewportOf(world, r.camera)) {
|
|
526
|
+
vw = viewport[0];
|
|
527
|
+
vh = viewport[1];
|
|
528
|
+
}
|
|
529
|
+
const factor = scaleMode === 1 ? vh / refH : scaleMode === 2 ? vw / refW : 1;
|
|
530
|
+
if (vw !== r.viewportW || vh !== r.viewportH || factor !== r.factor) {
|
|
531
|
+
r.viewportW = vw;
|
|
532
|
+
r.viewportH = vh;
|
|
533
|
+
r.factor = factor;
|
|
534
|
+
r.width = vw / factor;
|
|
535
|
+
r.height = vh / factor;
|
|
536
|
+
r.dirty = true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
store.roots.sort((a, b) => a.order - b.order);
|
|
540
|
+
syncTexts(world, store, s);
|
|
541
|
+
syncImages(world, store, s.images.tables);
|
|
542
|
+
markVisual(store, s.styles.tables, UiStyle, since, true);
|
|
543
|
+
markVisual(store, s.images.tables, UiImage, since, true);
|
|
544
|
+
markVisual(store, s.texts.tables, UiText, since, false);
|
|
545
|
+
markVisual(store, s.inputs.tables, UiTextInput, since, true);
|
|
546
|
+
markVisual(store, s.interactions.tables, UiInteraction, since, false);
|
|
547
|
+
for (const q of s.widgets) markVisual(store, q.tables, q.with[0], since, true);
|
|
548
|
+
const anchorsMoved = placeAnchors(world, store, s.anchors.tables);
|
|
549
|
+
let fill = false;
|
|
550
|
+
let write = false;
|
|
551
|
+
for (const r of store.roots) {
|
|
552
|
+
r.filling = r.dirty;
|
|
553
|
+
r.writing = r.dirty || anchorsMoved.has(r);
|
|
554
|
+
if (r.filling) {
|
|
555
|
+
prepareTree(r);
|
|
556
|
+
fill = true;
|
|
557
|
+
}
|
|
558
|
+
if (r.writing) write = true;
|
|
559
|
+
}
|
|
560
|
+
if (fill) fillNodes(store, s.nodes.tables);
|
|
561
|
+
for (const r of store.roots) {
|
|
562
|
+
if (r.filling) {
|
|
563
|
+
r.dirty = false;
|
|
564
|
+
s.current = r;
|
|
565
|
+
layoutTree(r.tree, r.width, r.height, s.measure);
|
|
566
|
+
s.current = void 0;
|
|
567
|
+
store.layouts++;
|
|
568
|
+
store.totalLayouts++;
|
|
569
|
+
store.nodesLaidOut += r.count;
|
|
570
|
+
store.flexCalls += r.tree.calls;
|
|
571
|
+
r.laidOutAt = store.frame;
|
|
572
|
+
derive(r);
|
|
573
|
+
r.visualVersion++;
|
|
574
|
+
} else if (r.writing) {
|
|
575
|
+
derive(r);
|
|
576
|
+
store.repositions++;
|
|
577
|
+
r.visualVersion++;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (write) writeLayouts(store, s.nodes.tables);
|
|
581
|
+
for (const r of store.roots) {
|
|
582
|
+
if (!r.flagsDirty) continue;
|
|
583
|
+
r.flagsDirty = false;
|
|
584
|
+
refreshFlags(world, r);
|
|
585
|
+
}
|
|
586
|
+
reportProblems(world, store);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
const movedRoots = /* @__PURE__ */ new Set();
|
|
590
|
+
function findOrphans(store, tables) {
|
|
591
|
+
store.orphans.clear();
|
|
592
|
+
for (const table of tables) {
|
|
593
|
+
const visible = table.column(UiLayout, "visible");
|
|
594
|
+
for (let i = 0; i < table.count; i++) {
|
|
595
|
+
const e = table.entities[i];
|
|
596
|
+
if (store.rootOf.has(e)) continue;
|
|
597
|
+
store.orphans.add(e);
|
|
598
|
+
visible[i] = 0;
|
|
599
|
+
}
|
|
600
|
+
table.markChanged(UiLayout);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function reportProblems(world, store) {
|
|
604
|
+
const log = world.tryResource(LogResource);
|
|
605
|
+
if (!log) return;
|
|
606
|
+
for (const e of store.orphans) {
|
|
607
|
+
if (store.reportedOrphans.has(e) || !world.isAlive(e)) continue;
|
|
608
|
+
store.reportedOrphans.add(e);
|
|
609
|
+
warn(
|
|
610
|
+
log,
|
|
611
|
+
new ShardError(
|
|
612
|
+
"ui/no-root",
|
|
613
|
+
`UI node ${e} isn't under a UiRoot, so it's not laid out or drawn`,
|
|
614
|
+
{
|
|
615
|
+
hint: "Parent it (ChildOf) under an entity with ui/UiRoot, or add UiRoot to the top node."
|
|
616
|
+
}
|
|
617
|
+
)
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function syncTexts(world, store, s) {
|
|
622
|
+
const fonts = world.tryResource(Fonts);
|
|
623
|
+
const defaults = world.tryResource(UiDefaults);
|
|
624
|
+
for (const table of s.texts.tables) {
|
|
625
|
+
const c = textColumns(table);
|
|
626
|
+
const styleName = table.column(UiNode, "style");
|
|
627
|
+
const input = table.has(UiTextInput) ? table.column(UiTextInput, "value") : void 0;
|
|
628
|
+
const placeholder = table.has(UiTextInput) ? table.column(UiTextInput, "placeholder") : void 0;
|
|
629
|
+
const text = table.has(Localized) ? table.column(Localized, "value") : table.column(UiText, "text");
|
|
630
|
+
for (let i = 0; i < table.count; i++) {
|
|
631
|
+
const e = table.entities[i];
|
|
632
|
+
const r = store.rootOf.get(e);
|
|
633
|
+
if (!r) continue;
|
|
634
|
+
const node = store.indexOf.get(e);
|
|
635
|
+
resolveStyle(s.style, c, i, r.theme, styleName[i] ?? "", 0);
|
|
636
|
+
const ref = s.style.font ?? defaults?.font ?? null;
|
|
637
|
+
const font = fonts?.get(ref);
|
|
638
|
+
let value = text[i] ?? "";
|
|
639
|
+
if (input) value = input[i] || placeholder[i] || "";
|
|
640
|
+
let t = r.texts[node];
|
|
641
|
+
if (!t || t.entity !== e) {
|
|
642
|
+
t = {
|
|
643
|
+
entity: e,
|
|
644
|
+
value: "",
|
|
645
|
+
font: void 0,
|
|
646
|
+
fontVersion: -2,
|
|
647
|
+
size: -1,
|
|
648
|
+
lineHeight: 0,
|
|
649
|
+
wrap: true,
|
|
650
|
+
cacheW: new Float32Array(2),
|
|
651
|
+
cacheOut: new Float32Array(4),
|
|
652
|
+
cacheN: 0,
|
|
653
|
+
layout: new TextLayout(),
|
|
654
|
+
laidWidth: -1,
|
|
655
|
+
laidVersion: -1
|
|
656
|
+
};
|
|
657
|
+
r.texts[node] = t;
|
|
658
|
+
r.dirty = true;
|
|
659
|
+
}
|
|
660
|
+
const version = font ? font.version : -1;
|
|
661
|
+
if (t.value !== value || t.font !== font || t.fontVersion !== version || t.size !== s.style.size || t.lineHeight !== s.style.lineHeight || t.wrap !== s.style.wrap) {
|
|
662
|
+
t.value = value;
|
|
663
|
+
t.font = font;
|
|
664
|
+
t.fontVersion = version;
|
|
665
|
+
t.size = s.style.size;
|
|
666
|
+
t.lineHeight = s.style.lineHeight;
|
|
667
|
+
t.wrap = s.style.wrap;
|
|
668
|
+
t.laidVersion = -1;
|
|
669
|
+
r.dirty = true;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function syncImages(world, store, tables) {
|
|
675
|
+
const textures = world.tryResource(Textures);
|
|
676
|
+
const atlases = world.tryResource(TextureAtlases);
|
|
677
|
+
for (const table of tables) {
|
|
678
|
+
const texture = table.column(UiImage, "texture");
|
|
679
|
+
const atlas = table.column(UiImage, "atlas");
|
|
680
|
+
const region = table.column(UiImage, "region");
|
|
681
|
+
for (let i = 0; i < table.count; i++) {
|
|
682
|
+
const e = table.entities[i];
|
|
683
|
+
const r = store.rootOf.get(e);
|
|
684
|
+
if (!r) continue;
|
|
685
|
+
const node = store.indexOf.get(e);
|
|
686
|
+
let w = 0;
|
|
687
|
+
let h = 0;
|
|
688
|
+
const a = atlas[i] ? atlases?.get(atlas[i]) : void 0;
|
|
689
|
+
const ri = a && region[i] ? a.region(region[i]) : -1;
|
|
690
|
+
if (a && ri >= 0) {
|
|
691
|
+
w = a.rects[ri * 4 + 2];
|
|
692
|
+
h = a.rects[ri * 4 + 3];
|
|
693
|
+
} else {
|
|
694
|
+
const tex = textures?.get(texture[i]);
|
|
695
|
+
if (tex) {
|
|
696
|
+
w = tex.width;
|
|
697
|
+
h = tex.height;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (w !== r.imageW[node] || h !== r.imageH[node]) {
|
|
701
|
+
r.imageW[node] = w;
|
|
702
|
+
r.imageH[node] = h;
|
|
703
|
+
r.dirty = true;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const columnsCache = /* @__PURE__ */ new WeakMap();
|
|
709
|
+
function textColumns(table) {
|
|
710
|
+
let c = columnsCache.get(table);
|
|
711
|
+
const text = table.has(UiText);
|
|
712
|
+
const style = table.has(UiStyle);
|
|
713
|
+
if (c && (!text || c.size === table.column(UiText, "size")) && (!style || c.background === table.column(UiStyle, "background")))
|
|
714
|
+
return c;
|
|
715
|
+
c = { table };
|
|
716
|
+
if (style) {
|
|
717
|
+
c.background = table.column(UiStyle, "background");
|
|
718
|
+
c.borderColor = table.column(UiStyle, "borderColor");
|
|
719
|
+
c.borderWidth = table.column(UiStyle, "borderWidth");
|
|
720
|
+
c.radius = table.column(UiStyle, "radius");
|
|
721
|
+
c.opacity = table.column(UiStyle, "opacity");
|
|
722
|
+
}
|
|
723
|
+
if (text) {
|
|
724
|
+
c.size = table.column(UiText, "size");
|
|
725
|
+
c.color = table.column(UiText, "color");
|
|
726
|
+
c.font = table.column(UiText, "font");
|
|
727
|
+
c.align = table.column(UiText, "align");
|
|
728
|
+
c.wrap = table.column(UiText, "wrap");
|
|
729
|
+
c.lineHeight = table.column(UiText, "lineHeight");
|
|
730
|
+
}
|
|
731
|
+
columnsCache.set(table, c);
|
|
732
|
+
return c;
|
|
733
|
+
}
|
|
734
|
+
function prepareTree(r) {
|
|
735
|
+
const t = r.tree;
|
|
736
|
+
const n = r.count;
|
|
737
|
+
t.reset(n);
|
|
738
|
+
const last = scratchLast(n);
|
|
739
|
+
for (let i = 1; i < n; i++) t.append(r.parent[i], i, last);
|
|
740
|
+
}
|
|
741
|
+
function fillNodes(store, tables) {
|
|
742
|
+
for (const table of tables) {
|
|
743
|
+
const c = nodeColumns(table);
|
|
744
|
+
for (let row = 0; row < table.count; row++) {
|
|
745
|
+
const e = table.entities[row];
|
|
746
|
+
const r = store.rootOf.get(e);
|
|
747
|
+
if (!r?.filling) continue;
|
|
748
|
+
writeNode(r.tree, r, store.indexOf.get(e), c, row);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
for (const r of store.roots) {
|
|
752
|
+
if (!r.filling) continue;
|
|
753
|
+
r.tree.absolute[0] = 0;
|
|
754
|
+
r.tree.hidden[0] = 0;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const nodeColumnCache = /* @__PURE__ */ new WeakMap();
|
|
758
|
+
function nodeColumns(table) {
|
|
759
|
+
const cached = nodeColumnCache.get(table);
|
|
760
|
+
if (cached && cached.display === table.column(UiNode, "display")) return cached;
|
|
761
|
+
const f = (name) => table.column(UiNode, name);
|
|
762
|
+
const c = {
|
|
763
|
+
display: table.column(UiNode, "display"),
|
|
764
|
+
position: table.column(UiNode, "position"),
|
|
765
|
+
direction: table.column(UiNode, "direction"),
|
|
766
|
+
wrap: table.column(UiNode, "wrap"),
|
|
767
|
+
justify: table.column(UiNode, "justify"),
|
|
768
|
+
alignItems: table.column(UiNode, "alignItems"),
|
|
769
|
+
alignSelf: table.column(UiNode, "alignSelf"),
|
|
770
|
+
lengths: LENGTH_FIELDS.map(f),
|
|
771
|
+
padding: f("padding"),
|
|
772
|
+
margin: f("margin"),
|
|
773
|
+
gap: f("gap"),
|
|
774
|
+
grow: table.column(UiNode, "grow"),
|
|
775
|
+
shrink: table.column(UiNode, "shrink"),
|
|
776
|
+
overflow: table.column(UiNode, "overflow"),
|
|
777
|
+
scroll: f("scroll"),
|
|
778
|
+
image: table.has(UiImage),
|
|
779
|
+
pivot: table.has(UiAnchor) ? table.column(UiAnchor, "pivot") : void 0
|
|
780
|
+
};
|
|
781
|
+
nodeColumnCache.set(table, c);
|
|
782
|
+
return c;
|
|
783
|
+
}
|
|
784
|
+
let lastScratch = new Int32Array(64);
|
|
785
|
+
function scratchLast(n) {
|
|
786
|
+
if (lastScratch.length < n) lastScratch = new Int32Array(n * 2);
|
|
787
|
+
lastScratch.fill(-1, 0, n);
|
|
788
|
+
return lastScratch;
|
|
789
|
+
}
|
|
790
|
+
const LENGTH_FIELDS = [
|
|
791
|
+
"width",
|
|
792
|
+
"height",
|
|
793
|
+
"minWidth",
|
|
794
|
+
"maxWidth",
|
|
795
|
+
"minHeight",
|
|
796
|
+
"maxHeight",
|
|
797
|
+
"left",
|
|
798
|
+
"top",
|
|
799
|
+
"right",
|
|
800
|
+
"bottom",
|
|
801
|
+
"basis"
|
|
802
|
+
];
|
|
803
|
+
function writeNode(t, r, i, c, row) {
|
|
804
|
+
t.hidden[i] = c.display[row];
|
|
805
|
+
t.absolute[i] = c.position[row];
|
|
806
|
+
t.direction[i] = c.direction[row];
|
|
807
|
+
t.wrap[i] = c.wrap[row];
|
|
808
|
+
t.justify[i] = c.justify[row];
|
|
809
|
+
t.alignItems[i] = c.alignItems[row];
|
|
810
|
+
t.alignSelf[i] = c.alignSelf[row];
|
|
811
|
+
for (let slot = 0; slot < LENGTHS; slot++) {
|
|
812
|
+
const col = c.lengths[slot];
|
|
813
|
+
const o = (i * LENGTHS + slot) * 2;
|
|
814
|
+
t.lengths[o] = col[row * 2];
|
|
815
|
+
t.lengths[o + 1] = col[row * 2 + 1];
|
|
816
|
+
}
|
|
817
|
+
for (let k = 0; k < 4; k++) {
|
|
818
|
+
t.padding[i * 4 + k] = c.padding[row * 4 + k];
|
|
819
|
+
t.margin[i * 4 + k] = c.margin[row * 4 + k];
|
|
820
|
+
}
|
|
821
|
+
t.gap[i * 2] = c.gap[row * 2];
|
|
822
|
+
t.gap[i * 2 + 1] = c.gap[row * 2 + 1];
|
|
823
|
+
t.flexGrow[i] = c.grow[row];
|
|
824
|
+
t.flexShrink[i] = c.shrink[row];
|
|
825
|
+
r.overflow[i] = c.overflow[row];
|
|
826
|
+
r.scrollX[i] = c.scroll[row * 2];
|
|
827
|
+
r.scrollY[i] = c.scroll[row * 2 + 1];
|
|
828
|
+
t.measured[i] = r.texts[i] !== void 0 || c.image ? 1 : 0;
|
|
829
|
+
if (r.anchorKind[i] === 1 && c.pivot) {
|
|
830
|
+
t.anchored[i] = 1;
|
|
831
|
+
t.pivotX[i] = c.pivot[row * 2];
|
|
832
|
+
t.pivotY[i] = c.pivot[row * 2 + 1];
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function placeAnchors(world, store, tables) {
|
|
836
|
+
movedRoots.clear();
|
|
837
|
+
for (const table of tables) {
|
|
838
|
+
const target = table.column(UiAnchor, "target");
|
|
839
|
+
const offset = table.column(UiAnchor, "offset");
|
|
840
|
+
const screenOffset = table.column(UiAnchor, "screenOffset");
|
|
841
|
+
const clamp = table.column(UiAnchor, "clamp");
|
|
842
|
+
const margin = table.column(UiAnchor, "margin");
|
|
843
|
+
const hideBehind = table.column(UiAnchor, "hideBehind");
|
|
844
|
+
const scaleDistance = table.column(UiAnchor, "scaleDistance");
|
|
845
|
+
const minScale = table.column(UiAnchor, "minScale");
|
|
846
|
+
const maxScale = table.column(UiAnchor, "maxScale");
|
|
847
|
+
for (let i = 0; i < table.count; i++) {
|
|
848
|
+
const e = table.entities[i];
|
|
849
|
+
const r = store.rootOf.get(e);
|
|
850
|
+
if (!r) continue;
|
|
851
|
+
const node = store.indexOf.get(e);
|
|
852
|
+
const t = r.tree;
|
|
853
|
+
let state = AnchorState.Hidden;
|
|
854
|
+
let ax = -1e6;
|
|
855
|
+
let ay = -1e6;
|
|
856
|
+
let angle = 0;
|
|
857
|
+
let distance = 0;
|
|
858
|
+
let scale = 1;
|
|
859
|
+
const cam = r.camera;
|
|
860
|
+
const te = target[i];
|
|
861
|
+
if (cam !== null && te !== null && te !== void 0 && world.isAlive(te) && world.has(te, GlobalTransform)) {
|
|
862
|
+
const g = world.entityTable(te).column(GlobalTransform, "matrix");
|
|
863
|
+
const go = world.entityRow(te) * 12;
|
|
864
|
+
const px = g[go + 3] + offset[i * 3];
|
|
865
|
+
const py = g[go + 7] + offset[i * 3 + 1];
|
|
866
|
+
const pz = g[go + 11] + offset[i * 3 + 2];
|
|
867
|
+
const ct = world.entityTable(cam);
|
|
868
|
+
const cr = world.entityRow(cam);
|
|
869
|
+
const m = ct.column(GlobalTransform, "matrix");
|
|
870
|
+
const o = cr * 12;
|
|
871
|
+
const dx = px - m[o + 3];
|
|
872
|
+
const dy = py - m[o + 7];
|
|
873
|
+
const dz = pz - m[o + 11];
|
|
874
|
+
const rx = m[o], ry = m[o + 4], rz = m[o + 8];
|
|
875
|
+
const ux = m[o + 1], uy = m[o + 5], uz = m[o + 9];
|
|
876
|
+
const bx = m[o + 2], by = m[o + 6], bz = m[o + 10];
|
|
877
|
+
const rl = Math.sqrt(rx * rx + ry * ry + rz * rz) || 1;
|
|
878
|
+
const ul = Math.sqrt(ux * ux + uy * uy + uz * uz) || 1;
|
|
879
|
+
const bl = Math.sqrt(bx * bx + by * by + bz * bz) || 1;
|
|
880
|
+
const xv = (dx * rx + dy * ry + dz * rz) / rl;
|
|
881
|
+
const yv = (dx * ux + dy * uy + dz * uz) / ul;
|
|
882
|
+
const zv = (dx * bx + dy * by + dz * bz) / bl;
|
|
883
|
+
distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
884
|
+
const W = r.width;
|
|
885
|
+
const H = r.height;
|
|
886
|
+
const aspect = W / Math.max(1e-6, H);
|
|
887
|
+
const ortho = ct.column(Camera3d, "projection")[cr] !== 0;
|
|
888
|
+
let nx;
|
|
889
|
+
let ny;
|
|
890
|
+
const behind = ortho ? false : zv >= -1e-6;
|
|
891
|
+
if (ortho) {
|
|
892
|
+
const half = ct.column(Camera3d, "orthoHeight")[cr] / 2;
|
|
893
|
+
nx = xv / (half * aspect);
|
|
894
|
+
ny = yv / half;
|
|
895
|
+
} else {
|
|
896
|
+
const f = 1 / Math.tan(ct.column(Camera3d, "fovY")[cr] * Math.PI / 360);
|
|
897
|
+
const depth = Math.max(1e-6, -zv);
|
|
898
|
+
nx = xv * f / aspect / depth;
|
|
899
|
+
ny = yv * f / depth;
|
|
900
|
+
}
|
|
901
|
+
const sx = (nx * 0.5 + 0.5) * W + screenOffset[i * 2];
|
|
902
|
+
const sy = (0.5 - ny * 0.5) * H + screenOffset[i * 2 + 1];
|
|
903
|
+
const mg = margin[i];
|
|
904
|
+
const inside = !behind && sx >= mg && sx <= W - mg && sy >= mg && sy <= H - mg;
|
|
905
|
+
if (behind && hideBehind[i]) state = AnchorState.Hidden;
|
|
906
|
+
else if (inside || !behind && !clamp[i]) {
|
|
907
|
+
state = AnchorState.OnScreen;
|
|
908
|
+
ax = sx;
|
|
909
|
+
ay = sy;
|
|
910
|
+
} else if (clamp[i]) {
|
|
911
|
+
let dirX = behind ? xv : sx - W / 2;
|
|
912
|
+
let dirY = behind ? -yv : sy - H / 2;
|
|
913
|
+
const len = Math.sqrt(dirX * dirX + dirY * dirY);
|
|
914
|
+
if (len < 1e-6) {
|
|
915
|
+
dirX = 0;
|
|
916
|
+
dirY = 1;
|
|
917
|
+
} else {
|
|
918
|
+
dirX /= len;
|
|
919
|
+
dirY /= len;
|
|
920
|
+
}
|
|
921
|
+
const hw = Math.max(0, W / 2 - mg);
|
|
922
|
+
const hh = Math.max(0, H / 2 - mg);
|
|
923
|
+
const tx = Math.abs(dirX) > 1e-6 ? hw / Math.abs(dirX) : Number.POSITIVE_INFINITY;
|
|
924
|
+
const ty = Math.abs(dirY) > 1e-6 ? hh / Math.abs(dirY) : Number.POSITIVE_INFINITY;
|
|
925
|
+
const k = Math.min(tx, ty);
|
|
926
|
+
ax = W / 2 + dirX * k;
|
|
927
|
+
ay = H / 2 + dirY * k;
|
|
928
|
+
angle = Math.atan2(dirY, dirX);
|
|
929
|
+
state = AnchorState.Clamped;
|
|
930
|
+
}
|
|
931
|
+
if (scaleDistance[i] > 0) {
|
|
932
|
+
scale = scaleDistance[i] / Math.max(1e-3, distance);
|
|
933
|
+
if (scale < minScale[i]) scale = minScale[i];
|
|
934
|
+
if (scale > maxScale[i]) scale = maxScale[i];
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (t.anchorX[node] !== ax || t.anchorY[node] !== ay || r.anchorState[node] !== state || r.angle[node] !== angle || r.anchorScale[node] !== scale || r.distance[node] !== distance) {
|
|
938
|
+
t.anchorX[node] = ax;
|
|
939
|
+
t.anchorY[node] = ay;
|
|
940
|
+
r.anchorState[node] = state;
|
|
941
|
+
r.angle[node] = angle;
|
|
942
|
+
r.distance[node] = distance;
|
|
943
|
+
r.anchorScale[node] = scale;
|
|
944
|
+
movedRoots.add(r);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return movedRoots;
|
|
949
|
+
}
|
|
950
|
+
function derive(r) {
|
|
951
|
+
const t = r.tree;
|
|
952
|
+
const n = r.count;
|
|
953
|
+
for (let i = 0; i < n; i++) {
|
|
954
|
+
const p = r.parent[i];
|
|
955
|
+
let x;
|
|
956
|
+
let y;
|
|
957
|
+
let s;
|
|
958
|
+
let visible;
|
|
959
|
+
if (p < 0) {
|
|
960
|
+
x = 0;
|
|
961
|
+
y = 0;
|
|
962
|
+
s = 1;
|
|
963
|
+
visible = true;
|
|
964
|
+
r.clip[0] = 0;
|
|
965
|
+
r.clip[1] = 0;
|
|
966
|
+
r.clip[2] = r.width;
|
|
967
|
+
r.clip[3] = r.height;
|
|
968
|
+
} else {
|
|
969
|
+
visible = r.visible[p] !== 0 && t.hidden[i] === 0;
|
|
970
|
+
if (t.anchored[i]) {
|
|
971
|
+
s = r.scale[p] * r.anchorScale[i];
|
|
972
|
+
const w = t.w[i] * s;
|
|
973
|
+
const h = t.h[i] * s;
|
|
974
|
+
x = t.anchorX[i] - t.pivotX[i] * w;
|
|
975
|
+
y = t.anchorY[i] - t.pivotY[i] * h;
|
|
976
|
+
if (r.anchorState[i] === AnchorState.Hidden) visible = false;
|
|
977
|
+
} else {
|
|
978
|
+
s = r.scale[p];
|
|
979
|
+
x = r.x[p] + (t.x[i] - r.scrollX[p]) * s;
|
|
980
|
+
y = r.y[p] + (t.y[i] - r.scrollY[p]) * s;
|
|
981
|
+
if (r.anchorKind[i] === 2 && r.anchorState[p] !== AnchorState.Clamped) visible = false;
|
|
982
|
+
}
|
|
983
|
+
for (let k = 0; k < 4; k++) r.clip[i * 4 + k] = r.childClip[p * 4 + k];
|
|
984
|
+
}
|
|
985
|
+
r.w[i] = t.w[i] * s;
|
|
986
|
+
r.h[i] = t.h[i] * s;
|
|
987
|
+
if (r.anchorKind[i] === 2 && p >= 0) {
|
|
988
|
+
const a = r.angle[p];
|
|
989
|
+
r.angle[i] = a;
|
|
990
|
+
const dx = Math.cos(a);
|
|
991
|
+
const dy = Math.sin(a);
|
|
992
|
+
const hw = r.w[p] / 2;
|
|
993
|
+
const hh = r.h[p] / 2;
|
|
994
|
+
const k = Math.min(
|
|
995
|
+
Math.abs(dx) > 1e-6 ? hw / Math.abs(dx) : Number.POSITIVE_INFINITY,
|
|
996
|
+
Math.abs(dy) > 1e-6 ? hh / Math.abs(dy) : Number.POSITIVE_INFINITY
|
|
997
|
+
);
|
|
998
|
+
const reach = k + 4 * s + r.w[i] / 2;
|
|
999
|
+
x = r.x[p] + hw + dx * reach - r.w[i] / 2;
|
|
1000
|
+
y = r.y[p] + hh + dy * reach - r.h[i] / 2;
|
|
1001
|
+
}
|
|
1002
|
+
r.x[i] = x;
|
|
1003
|
+
r.y[i] = y;
|
|
1004
|
+
r.scale[i] = s;
|
|
1005
|
+
r.visible[i] = visible && t.hidden[i] === 0 ? 1 : 0;
|
|
1006
|
+
let cw = 0;
|
|
1007
|
+
let ch = 0;
|
|
1008
|
+
for (let c = i + 1; c < r.end[i]; c = r.end[c]) {
|
|
1009
|
+
if (t.hidden[c] || t.anchored[c]) continue;
|
|
1010
|
+
const right = t.x[c] + t.w[c] + t.margin[c * 4 + 1];
|
|
1011
|
+
const bottom = t.y[c] + t.h[c] + t.margin[c * 4 + 2];
|
|
1012
|
+
if (right > cw) cw = right;
|
|
1013
|
+
if (bottom > ch) ch = bottom;
|
|
1014
|
+
}
|
|
1015
|
+
cw += t.padding[i * 4 + 1];
|
|
1016
|
+
ch += t.padding[i * 4 + 2];
|
|
1017
|
+
r.contentW[i] = cw;
|
|
1018
|
+
r.contentH[i] = ch;
|
|
1019
|
+
if (r.overflow[i] === Overflow.Scroll) {
|
|
1020
|
+
const maxX = Math.max(0, cw - t.w[i]);
|
|
1021
|
+
const maxY = Math.max(0, ch - t.h[i]);
|
|
1022
|
+
r.scrollX[i] = Math.min(Math.max(0, r.scrollX[i]), maxX);
|
|
1023
|
+
r.scrollY[i] = Math.min(Math.max(0, r.scrollY[i]), maxY);
|
|
1024
|
+
} else {
|
|
1025
|
+
r.scrollX[i] = 0;
|
|
1026
|
+
r.scrollY[i] = 0;
|
|
1027
|
+
}
|
|
1028
|
+
const o = i * 4;
|
|
1029
|
+
if (r.overflow[i] !== Overflow.Visible) {
|
|
1030
|
+
r.childClip[o] = Math.max(r.clip[o], x);
|
|
1031
|
+
r.childClip[o + 1] = Math.max(r.clip[o + 1], y);
|
|
1032
|
+
r.childClip[o + 2] = Math.min(r.clip[o + 2], x + r.w[i]);
|
|
1033
|
+
r.childClip[o + 3] = Math.min(r.clip[o + 3], y + r.h[i]);
|
|
1034
|
+
} else {
|
|
1035
|
+
for (let k = 0; k < 4; k++) r.childClip[o + k] = r.clip[o + k];
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const layoutColumnCache = /* @__PURE__ */ new WeakMap();
|
|
1040
|
+
function layoutColumns(table) {
|
|
1041
|
+
const cached = layoutColumnCache.get(table);
|
|
1042
|
+
if (cached && cached.x === table.column(UiLayout, "x")) return cached;
|
|
1043
|
+
const c = {
|
|
1044
|
+
x: table.column(UiLayout, "x"),
|
|
1045
|
+
y: table.column(UiLayout, "y"),
|
|
1046
|
+
width: table.column(UiLayout, "width"),
|
|
1047
|
+
height: table.column(UiLayout, "height"),
|
|
1048
|
+
clip: table.column(UiLayout, "clip"),
|
|
1049
|
+
content: table.column(UiLayout, "content"),
|
|
1050
|
+
scale: table.column(UiLayout, "scale"),
|
|
1051
|
+
angle: table.column(UiLayout, "angle"),
|
|
1052
|
+
visible: table.column(UiLayout, "visible"),
|
|
1053
|
+
anchor: table.column(UiLayout, "anchor"),
|
|
1054
|
+
distance: table.column(UiLayout, "distance")
|
|
1055
|
+
};
|
|
1056
|
+
layoutColumnCache.set(table, c);
|
|
1057
|
+
return c;
|
|
1058
|
+
}
|
|
1059
|
+
function writeLayouts(store, tables) {
|
|
1060
|
+
for (const table of tables) {
|
|
1061
|
+
const c = layoutColumns(table);
|
|
1062
|
+
let wrote = false;
|
|
1063
|
+
for (let row = 0; row < table.count; row++) {
|
|
1064
|
+
const e = table.entities[row];
|
|
1065
|
+
const r = store.rootOf.get(e);
|
|
1066
|
+
if (!r?.writing) continue;
|
|
1067
|
+
const i = store.indexOf.get(e);
|
|
1068
|
+
wrote = true;
|
|
1069
|
+
const x = r.x[i];
|
|
1070
|
+
const y = r.y[i];
|
|
1071
|
+
const w = r.w[i];
|
|
1072
|
+
const h = r.h[i];
|
|
1073
|
+
c.x[row] = x;
|
|
1074
|
+
c.y[row] = y;
|
|
1075
|
+
c.width[row] = w;
|
|
1076
|
+
c.height[row] = h;
|
|
1077
|
+
const vx0 = Math.max(r.clip[i * 4], x);
|
|
1078
|
+
const vy0 = Math.max(r.clip[i * 4 + 1], y);
|
|
1079
|
+
const vx1 = Math.min(r.clip[i * 4 + 2], x + w);
|
|
1080
|
+
const vy1 = Math.min(r.clip[i * 4 + 3], y + h);
|
|
1081
|
+
c.clip[row * 4] = vx0;
|
|
1082
|
+
c.clip[row * 4 + 1] = vy0;
|
|
1083
|
+
c.clip[row * 4 + 2] = Math.max(0, vx1 - vx0);
|
|
1084
|
+
c.clip[row * 4 + 3] = Math.max(0, vy1 - vy0);
|
|
1085
|
+
c.content[row * 2] = r.contentW[i];
|
|
1086
|
+
c.content[row * 2 + 1] = r.contentH[i];
|
|
1087
|
+
c.scale[row] = r.scale[i];
|
|
1088
|
+
c.angle[row] = r.angle[i];
|
|
1089
|
+
c.visible[row] = r.visible[i];
|
|
1090
|
+
c.anchor[row] = r.anchorKind[i] === 1 ? r.anchorState[i] : 0;
|
|
1091
|
+
c.distance[row] = r.anchorKind[i] === 1 ? r.distance[i] : 0;
|
|
1092
|
+
}
|
|
1093
|
+
if (wrote) table.markChanged(UiLayout);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
export {
|
|
1097
|
+
AnchorState,
|
|
1098
|
+
Kind,
|
|
1099
|
+
Overflow,
|
|
1100
|
+
UiRootState,
|
|
1101
|
+
UiState,
|
|
1102
|
+
UiStore,
|
|
1103
|
+
derive,
|
|
1104
|
+
layoutUi,
|
|
1105
|
+
measureTextNode,
|
|
1106
|
+
observeUiStructure,
|
|
1107
|
+
refreshFlags,
|
|
1108
|
+
textColumns
|
|
1109
|
+
};
|