@threenative/ui 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +8 -0
- package/dist/index.js +417 -20
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ interface IGameCanvasProps<TState extends Record<string, unknown> = Record<strin
|
|
|
8
8
|
}
|
|
9
9
|
declare function GameCanvas<TState extends Record<string, unknown>, TPhysics>({ className, game, }: IGameCanvasProps<TState, TPhysics>): react_jsx_runtime.JSX.Element;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The dev-only gate, and the reason the view below is a separate component.
|
|
13
|
+
*
|
|
14
|
+
* `isDev` folds to `false` in a production build, so this returns null before any hook runs and
|
|
15
|
+
* `DebugOverlayView` becomes unreachable — the bundler then drops the whole view, and with it every
|
|
16
|
+
* reference to the devtools global. A guard placed after the hooks keeps those references alive
|
|
17
|
+
* instead, which `scripts/check-core-boundary.ts` rejects: the overlay must not ship in a game.
|
|
18
|
+
*/
|
|
11
19
|
declare function DebugOverlay(): react_jsx_runtime.JSX.Element | null;
|
|
12
20
|
|
|
13
21
|
type GameSelector<TState, TSelected> = (state: TState) => TSelected;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createContext, useRef, useState, useEffect,
|
|
2
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
-
import { connectUiBridge, subscribeUiState, publishHitRegions, sendUiIntent, UI_READY_INTENT } from '@threenative/core/ui-layer';
|
|
1
|
+
import { createContext, useRef, useState, useEffect, useCallback, useMemo, useSyncExternalStore, useContext } from 'react';
|
|
2
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
import { connectUiBridge, subscribeUiState, publishHitRegions, sendUiIntent, UI_READY_INTENT, UI_DEV_METRICS_MESSAGE } from '@threenative/core/ui-layer';
|
|
4
4
|
|
|
5
5
|
// src/GameCanvas.tsx
|
|
6
6
|
function GameCanvas({
|
|
@@ -51,45 +51,391 @@ function GameCanvas({
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
var isDev = import.meta.env?.DEV === true;
|
|
54
|
+
var ROW_LIMIT = 50;
|
|
55
|
+
var DEFAULT_SMALL_PIXELS = 32;
|
|
56
|
+
var SORTS = ["triangles", "draws", "projected"];
|
|
57
|
+
function devTools() {
|
|
58
|
+
return globalThis.window?.__THREENATIVE__;
|
|
59
|
+
}
|
|
54
60
|
function readSnapshot() {
|
|
55
|
-
const snapshot =
|
|
61
|
+
const snapshot = devTools()?.snapshot;
|
|
56
62
|
return typeof snapshot === "function" ? snapshot() : {};
|
|
57
63
|
}
|
|
58
64
|
function displayValue(value) {
|
|
59
65
|
return typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
|
|
60
66
|
}
|
|
67
|
+
function count(value) {
|
|
68
|
+
return value === void 0 ? "\u2014" : Math.round(value).toLocaleString("en-US");
|
|
69
|
+
}
|
|
70
|
+
function sortKey(row, sort) {
|
|
71
|
+
if (sort === "draws") return row.draws;
|
|
72
|
+
if (sort === "projected") return row.projectedPixels;
|
|
73
|
+
return row.submittedTriangles;
|
|
74
|
+
}
|
|
75
|
+
function rank(rows, sort) {
|
|
76
|
+
return [...rows].sort((left, right) => {
|
|
77
|
+
const a = sortKey(left, sort);
|
|
78
|
+
const b = sortKey(right, sort);
|
|
79
|
+
if (a === void 0 && b === void 0) return left.path.localeCompare(right.path);
|
|
80
|
+
if (a === void 0) return 1;
|
|
81
|
+
if (b === void 0) return -1;
|
|
82
|
+
return b - a;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
61
85
|
function DebugOverlay() {
|
|
86
|
+
if (!isDev) return null;
|
|
87
|
+
return /* @__PURE__ */ jsx(DebugOverlayView, {});
|
|
88
|
+
}
|
|
89
|
+
function DebugOverlayView() {
|
|
62
90
|
const [open, setOpen] = useState(false);
|
|
91
|
+
const [tab, setTab] = useState("entities");
|
|
63
92
|
const [snapshot, setSnapshot] = useState({});
|
|
93
|
+
const [report, setReport] = useState(void 0);
|
|
94
|
+
const [capturing, setCapturing] = useState(false);
|
|
95
|
+
const [failure, setFailure] = useState(void 0);
|
|
96
|
+
const [group, setGroup] = useState("objects");
|
|
97
|
+
const [sort, setSort] = useState("triangles");
|
|
98
|
+
const [smallOnly, setSmallOnly] = useState(false);
|
|
99
|
+
const [smallPixels, setSmallPixels] = useState(DEFAULT_SMALL_PIXELS);
|
|
100
|
+
const [expanded, setExpanded] = useState([]);
|
|
101
|
+
const [selected, setSelected] = useState(void 0);
|
|
102
|
+
const [copied, setCopied] = useState(void 0);
|
|
64
103
|
useEffect(() => {
|
|
65
|
-
if (
|
|
104
|
+
if (typeof window === "undefined") return void 0;
|
|
66
105
|
const hostWindow = window;
|
|
67
106
|
const toggle = (event) => {
|
|
68
107
|
if (event.key === "`") setOpen((visible) => !visible);
|
|
69
108
|
};
|
|
109
|
+
const clearOutline = () => setSelected(void 0);
|
|
70
110
|
hostWindow.addEventListener("keydown", toggle);
|
|
111
|
+
hostWindow.addEventListener("resize", clearOutline);
|
|
71
112
|
const timer = hostWindow.setInterval(() => setSnapshot(readSnapshot()), 100);
|
|
72
113
|
return () => {
|
|
73
114
|
hostWindow.removeEventListener("keydown", toggle);
|
|
115
|
+
hostWindow.removeEventListener("resize", clearOutline);
|
|
74
116
|
hostWindow.clearInterval(timer);
|
|
75
117
|
};
|
|
76
118
|
}, []);
|
|
77
|
-
|
|
78
|
-
|
|
119
|
+
const capture = useCallback(() => {
|
|
120
|
+
const geometry = devTools()?.geometry;
|
|
121
|
+
if (typeof geometry !== "function" || capturing) return;
|
|
122
|
+
setCapturing(true);
|
|
123
|
+
setSelected(void 0);
|
|
124
|
+
setCopied(void 0);
|
|
125
|
+
setFailure(void 0);
|
|
126
|
+
geometry({ limit: ROW_LIMIT, sort }).then((next) => setReport(next)).catch((error) => {
|
|
127
|
+
setReport(void 0);
|
|
128
|
+
setFailure(error instanceof Error ? error.message : String(error));
|
|
129
|
+
}).finally(() => setCapturing(false));
|
|
130
|
+
}, [capturing, sort]);
|
|
131
|
+
const rows = useMemo(() => rank(report?.objects ?? [], sort), [report, sort]);
|
|
132
|
+
const visibleRows = useMemo(
|
|
133
|
+
() => smallOnly ? rows.filter(
|
|
134
|
+
(row) => row.projectedPixels !== void 0 && row.projectedPixels < smallPixels
|
|
135
|
+
) : rows,
|
|
136
|
+
[rows, smallOnly, smallPixels]
|
|
137
|
+
);
|
|
138
|
+
const selectedRow = rows.find((row) => row.id === selected);
|
|
139
|
+
if (!open) return null;
|
|
140
|
+
const entityRows = Object.entries(snapshot).flatMap(
|
|
79
141
|
([entity, fields]) => Object.entries(fields).map(([key, value]) => ({ entity, key, value }))
|
|
80
142
|
);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
143
|
+
const captureAvailable = typeof devTools()?.geometry === "function";
|
|
144
|
+
return /* @__PURE__ */ jsxs(
|
|
145
|
+
"aside",
|
|
146
|
+
{
|
|
147
|
+
"aria-label": "ThreeNative entity debug overlay",
|
|
148
|
+
"data-threenative-debug-overlay": "true",
|
|
149
|
+
"data-tn-debug-tab": tab,
|
|
150
|
+
children: [
|
|
151
|
+
/* @__PURE__ */ jsxs("div", { className: "tn-debug-tabs", role: "tablist", "aria-label": "Debug views", children: [
|
|
152
|
+
/* @__PURE__ */ jsx(
|
|
153
|
+
"button",
|
|
154
|
+
{
|
|
155
|
+
"aria-controls": "tn-debug-entities",
|
|
156
|
+
"aria-selected": tab === "entities",
|
|
157
|
+
className: "tn-debug-tab",
|
|
158
|
+
id: "tn-debug-entities-tab",
|
|
159
|
+
onClick: () => setTab("entities"),
|
|
160
|
+
role: "tab",
|
|
161
|
+
type: "button",
|
|
162
|
+
children: "Entities"
|
|
163
|
+
}
|
|
164
|
+
),
|
|
165
|
+
/* @__PURE__ */ jsx(
|
|
166
|
+
"button",
|
|
167
|
+
{
|
|
168
|
+
"aria-controls": "tn-debug-geometry",
|
|
169
|
+
"aria-selected": tab === "geometry",
|
|
170
|
+
className: "tn-debug-tab",
|
|
171
|
+
id: "tn-debug-geometry-tab",
|
|
172
|
+
onClick: () => setTab("geometry"),
|
|
173
|
+
role: "tab",
|
|
174
|
+
type: "button",
|
|
175
|
+
children: "Geometry"
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
] }),
|
|
179
|
+
tab === "entities" ? /* @__PURE__ */ jsx("div", { "aria-labelledby": "tn-debug-entities-tab", id: "tn-debug-entities", role: "tabpanel", children: /* @__PURE__ */ jsxs("table", { children: [
|
|
180
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
181
|
+
/* @__PURE__ */ jsx("th", { children: "entity" }),
|
|
182
|
+
/* @__PURE__ */ jsx("th", { children: "key" }),
|
|
183
|
+
/* @__PURE__ */ jsx("th", { children: "value" })
|
|
184
|
+
] }) }),
|
|
185
|
+
/* @__PURE__ */ jsx("tbody", { children: entityRows.map(({ entity, key, value }) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
186
|
+
/* @__PURE__ */ jsx("td", { children: entity }),
|
|
187
|
+
/* @__PURE__ */ jsx("td", { children: key }),
|
|
188
|
+
/* @__PURE__ */ jsx("td", { children: displayValue(value) })
|
|
189
|
+
] }, `${entity}.${key}`)) })
|
|
190
|
+
] }) }) : /* @__PURE__ */ jsxs("div", { "aria-labelledby": "tn-debug-geometry-tab", id: "tn-debug-geometry", role: "tabpanel", children: [
|
|
191
|
+
/* @__PURE__ */ jsxs("div", { className: "tn-debug-controls", children: [
|
|
192
|
+
/* @__PURE__ */ jsx(
|
|
193
|
+
"button",
|
|
194
|
+
{
|
|
195
|
+
className: "tn-debug-action",
|
|
196
|
+
disabled: capturing || !captureAvailable,
|
|
197
|
+
onClick: capture,
|
|
198
|
+
type: "button",
|
|
199
|
+
children: capturing ? "Capturing\u2026" : report === void 0 ? "Capture" : "Refresh"
|
|
200
|
+
}
|
|
201
|
+
),
|
|
202
|
+
report === void 0 ? null : /* @__PURE__ */ jsx(
|
|
203
|
+
"button",
|
|
204
|
+
{
|
|
205
|
+
className: "tn-debug-action",
|
|
206
|
+
onClick: () => {
|
|
207
|
+
const json = JSON.stringify(report, null, 2);
|
|
208
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
209
|
+
if (clipboard === void 0) {
|
|
210
|
+
setCopied(json);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
clipboard.writeText(json).catch(() => setCopied(json));
|
|
214
|
+
},
|
|
215
|
+
type: "button",
|
|
216
|
+
children: "Copy JSON"
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
] }),
|
|
220
|
+
captureAvailable ? null : /* @__PURE__ */ jsx("p", { className: "tn-debug-note", children: "The running game exposes no geometry capture. Mount a development build to use this view." }),
|
|
221
|
+
failure === void 0 ? null : /* @__PURE__ */ jsx("p", { className: "tn-debug-note", children: failure }),
|
|
222
|
+
report === void 0 || report.status === "unavailable" ? report?.reason === void 0 ? null : /* @__PURE__ */ jsx("p", { className: "tn-debug-note", children: report.reason }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
223
|
+
/* @__PURE__ */ jsxs("p", { className: "tn-debug-note", children: [
|
|
224
|
+
"tick ",
|
|
225
|
+
count(report.tick),
|
|
226
|
+
" \xB7 ",
|
|
227
|
+
report.backend ?? "backend unknown",
|
|
228
|
+
" \xB7",
|
|
229
|
+
" ",
|
|
230
|
+
count(report.viewport?.width),
|
|
231
|
+
"\xD7",
|
|
232
|
+
count(report.viewport?.height),
|
|
233
|
+
" \xB7",
|
|
234
|
+
" ",
|
|
235
|
+
report.camera?.type ?? "camera unknown",
|
|
236
|
+
" \xB7 ",
|
|
237
|
+
(report.durationMs ?? 0).toFixed(1),
|
|
238
|
+
" ms \xB7 ",
|
|
239
|
+
count(report.returned),
|
|
240
|
+
" of ",
|
|
241
|
+
count(report.matched),
|
|
242
|
+
" rows"
|
|
243
|
+
] }),
|
|
244
|
+
report.partialRanking === true ? /* @__PURE__ */ jsx("p", { className: "tn-debug-warn", children: "Ranking is partial \u2014 not a global claim." }) : null,
|
|
245
|
+
report.rowsTruncated === true ? /* @__PURE__ */ jsxs("p", { className: "tn-debug-warn", children: [
|
|
246
|
+
"Rows were cut by the limit (",
|
|
247
|
+
count(report.limit),
|
|
248
|
+
")."
|
|
249
|
+
] }) : null,
|
|
250
|
+
report.inspectionComplete === false ? /* @__PURE__ */ jsxs("p", { className: "tn-debug-warn", children: [
|
|
251
|
+
"Inspection stopped at the walk cap (",
|
|
252
|
+
count(report.inspectedNodes),
|
|
253
|
+
" nodes)."
|
|
254
|
+
] }) : null,
|
|
255
|
+
sort === report.sort ? null : /* @__PURE__ */ jsxs("p", { className: "tn-debug-note", children: [
|
|
256
|
+
"These rows were ranked by ",
|
|
257
|
+
report.sort,
|
|
258
|
+
"; this table is re-sorted locally."
|
|
259
|
+
] }),
|
|
260
|
+
/* @__PURE__ */ jsxs("table", { className: "tn-debug-passes", children: [
|
|
261
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
262
|
+
/* @__PURE__ */ jsx("th", { children: "pass" }),
|
|
263
|
+
/* @__PURE__ */ jsx("th", { children: "draws" }),
|
|
264
|
+
/* @__PURE__ */ jsx("th", { children: "triangles" }),
|
|
265
|
+
/* @__PURE__ */ jsx("th", { children: "rows" }),
|
|
266
|
+
/* @__PURE__ */ jsx("th", { children: "unattributed (measured \u2212 rows)" })
|
|
267
|
+
] }) }),
|
|
268
|
+
/* @__PURE__ */ jsx("tbody", { children: (report.passes ?? []).map((pass) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
269
|
+
/* @__PURE__ */ jsx("td", { children: pass.kind }),
|
|
270
|
+
/* @__PURE__ */ jsx("td", { children: count(pass.draws) }),
|
|
271
|
+
/* @__PURE__ */ jsx("td", { children: count(pass.triangles) }),
|
|
272
|
+
/* @__PURE__ */ jsx("td", { children: count(pass.attributedTriangles) }),
|
|
273
|
+
/* @__PURE__ */ jsx("td", { children: count(pass.unattributedTriangles) })
|
|
274
|
+
] }, pass.kind)) })
|
|
275
|
+
] }),
|
|
276
|
+
/* @__PURE__ */ jsxs("fieldset", { className: "tn-debug-controls", children: [
|
|
277
|
+
/* @__PURE__ */ jsx("legend", { children: "Group" }),
|
|
278
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
279
|
+
/* @__PURE__ */ jsx(
|
|
280
|
+
"input",
|
|
281
|
+
{
|
|
282
|
+
checked: group === "objects",
|
|
283
|
+
name: "tn-debug-group",
|
|
284
|
+
onChange: () => setGroup("objects"),
|
|
285
|
+
type: "radio"
|
|
286
|
+
}
|
|
287
|
+
),
|
|
288
|
+
"Objects"
|
|
289
|
+
] }),
|
|
290
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
291
|
+
/* @__PURE__ */ jsx(
|
|
292
|
+
"input",
|
|
293
|
+
{
|
|
294
|
+
checked: group === "assets",
|
|
295
|
+
name: "tn-debug-group",
|
|
296
|
+
onChange: () => setGroup("assets"),
|
|
297
|
+
type: "radio"
|
|
298
|
+
}
|
|
299
|
+
),
|
|
300
|
+
"Assets"
|
|
301
|
+
] })
|
|
302
|
+
] }),
|
|
303
|
+
/* @__PURE__ */ jsxs("div", { className: "tn-debug-controls", children: [
|
|
304
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
305
|
+
"Sort",
|
|
306
|
+
/* @__PURE__ */ jsx(
|
|
307
|
+
"select",
|
|
308
|
+
{
|
|
309
|
+
onChange: (event) => setSort(event.target.value),
|
|
310
|
+
value: sort,
|
|
311
|
+
children: SORTS.map((option) => /* @__PURE__ */ jsx("option", { value: option, children: option }, option))
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
] }),
|
|
315
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
316
|
+
/* @__PURE__ */ jsx(
|
|
317
|
+
"input",
|
|
318
|
+
{
|
|
319
|
+
checked: smallOnly,
|
|
320
|
+
onChange: (event) => setSmallOnly(event.target.checked),
|
|
321
|
+
type: "checkbox"
|
|
322
|
+
}
|
|
323
|
+
),
|
|
324
|
+
"Small on screen (< ",
|
|
325
|
+
count(smallPixels),
|
|
326
|
+
" px)"
|
|
327
|
+
] }),
|
|
328
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
329
|
+
"Threshold",
|
|
330
|
+
/* @__PURE__ */ jsx(
|
|
331
|
+
"input",
|
|
332
|
+
{
|
|
333
|
+
min: 1,
|
|
334
|
+
onChange: (event) => setSmallPixels(Math.max(1, Number(event.target.value) || 1)),
|
|
335
|
+
type: "number",
|
|
336
|
+
value: smallPixels
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
] })
|
|
340
|
+
] }),
|
|
341
|
+
/* @__PURE__ */ jsx("p", { className: "tn-debug-note", children: "A diagnostic filter, not a recommendation: whether an object matters is an authoring decision, and an object with no measured bounds is excluded rather than called small." }),
|
|
342
|
+
group === "assets" ? /* @__PURE__ */ jsxs("table", { className: "tn-debug-assets", children: [
|
|
343
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
344
|
+
/* @__PURE__ */ jsx("th", { children: "asset" }),
|
|
345
|
+
/* @__PURE__ */ jsx("th", { children: "objects" }),
|
|
346
|
+
/* @__PURE__ */ jsx("th", { children: "triangles" }),
|
|
347
|
+
/* @__PURE__ */ jsx("th", { children: "unique" }),
|
|
348
|
+
/* @__PURE__ */ jsx("th", { children: "draws" })
|
|
349
|
+
] }) }),
|
|
350
|
+
/* @__PURE__ */ jsx("tbody", { children: (report.assets ?? []).map((asset) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
351
|
+
/* @__PURE__ */ jsx("td", { children: asset.asset }),
|
|
352
|
+
/* @__PURE__ */ jsx("td", { children: count(asset.objects) }),
|
|
353
|
+
/* @__PURE__ */ jsx("td", { children: count(asset.submittedTriangles) }),
|
|
354
|
+
/* @__PURE__ */ jsx("td", { children: count(asset.uniqueTriangles) }),
|
|
355
|
+
/* @__PURE__ */ jsx("td", { children: count(asset.draws) })
|
|
356
|
+
] }, asset.asset)) })
|
|
357
|
+
] }) : /* @__PURE__ */ jsxs("table", { className: "tn-debug-objects", children: [
|
|
358
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
359
|
+
/* @__PURE__ */ jsx("th", { children: "object" }),
|
|
360
|
+
/* @__PURE__ */ jsx("th", { children: "triangles" }),
|
|
361
|
+
/* @__PURE__ */ jsx("th", { children: "draws" }),
|
|
362
|
+
/* @__PURE__ */ jsx("th", { children: "projected px" }),
|
|
363
|
+
/* @__PURE__ */ jsx("th", { children: "copies" }),
|
|
364
|
+
/* @__PURE__ */ jsx("th", { children: "asset" })
|
|
365
|
+
] }) }),
|
|
366
|
+
/* @__PURE__ */ jsx("tbody", { children: visibleRows.flatMap((row) => {
|
|
367
|
+
const isExpanded = expanded.includes(row.id);
|
|
368
|
+
const head = /* @__PURE__ */ jsxs("tr", { title: (row.unavailable ?? []).join(" "), children: [
|
|
369
|
+
/* @__PURE__ */ jsxs("td", { children: [
|
|
370
|
+
/* @__PURE__ */ jsx(
|
|
371
|
+
"button",
|
|
372
|
+
{
|
|
373
|
+
"aria-expanded": isExpanded,
|
|
374
|
+
className: "tn-debug-expand",
|
|
375
|
+
onClick: () => setExpanded(
|
|
376
|
+
(current) => current.includes(row.id) ? current.filter((id) => id !== row.id) : [...current, row.id]
|
|
377
|
+
),
|
|
378
|
+
type: "button",
|
|
379
|
+
children: isExpanded ? "\u2212" : "+"
|
|
380
|
+
}
|
|
381
|
+
),
|
|
382
|
+
/* @__PURE__ */ jsx(
|
|
383
|
+
"button",
|
|
384
|
+
{
|
|
385
|
+
"aria-pressed": selected === row.id,
|
|
386
|
+
className: "tn-debug-select",
|
|
387
|
+
onClick: () => setSelected((current) => current === row.id ? void 0 : row.id),
|
|
388
|
+
type: "button",
|
|
389
|
+
children: row.name
|
|
390
|
+
}
|
|
391
|
+
),
|
|
392
|
+
(row.unavailable ?? []).length === 0 ? null : /* @__PURE__ */ jsx("small", { className: "tn-debug-unavailable", children: (row.unavailable ?? []).join(" ") })
|
|
393
|
+
] }),
|
|
394
|
+
/* @__PURE__ */ jsxs("td", { children: [
|
|
395
|
+
count(row.submittedTriangles),
|
|
396
|
+
row.trianglesSource === "batchMembers" ? " (derived)" : ""
|
|
397
|
+
] }),
|
|
398
|
+
/* @__PURE__ */ jsx("td", { children: count(row.draws) }),
|
|
399
|
+
/* @__PURE__ */ jsx("td", { children: count(row.projectedPixels) }),
|
|
400
|
+
/* @__PURE__ */ jsx("td", { children: count(row.copies) }),
|
|
401
|
+
/* @__PURE__ */ jsx("td", { children: row.asset ?? "\u2014" })
|
|
402
|
+
] }, row.id);
|
|
403
|
+
if (!isExpanded) return [head];
|
|
404
|
+
return [
|
|
405
|
+
head,
|
|
406
|
+
...row.meshes.map((mesh) => /* @__PURE__ */ jsxs("tr", { className: "tn-debug-mesh", children: [
|
|
407
|
+
/* @__PURE__ */ jsx("td", { children: mesh.name }),
|
|
408
|
+
/* @__PURE__ */ jsx("td", { children: count(mesh.submittedTriangles) }),
|
|
409
|
+
/* @__PURE__ */ jsx("td", { children: count(mesh.draws) }),
|
|
410
|
+
/* @__PURE__ */ jsx("td", { children: count(mesh.materials) }),
|
|
411
|
+
/* @__PURE__ */ jsx("td", { children: count(mesh.instances) }),
|
|
412
|
+
/* @__PURE__ */ jsx("td", { children: mesh.batchOwner ?? "\u2014" })
|
|
413
|
+
] }, mesh.id))
|
|
414
|
+
];
|
|
415
|
+
}) })
|
|
416
|
+
] }),
|
|
417
|
+
selectedRow === void 0 ? null : selectedRow.projectedCenter === void 0 || selectedRow.projectedPixels === void 0 || report.viewport === void 0 ? /* @__PURE__ */ jsx("p", { className: "tn-debug-note", children: "bounds unavailable" }) : (
|
|
418
|
+
// Percentages of the captured drawing buffer, which is the canvas the overlay sits
|
|
419
|
+
// over. The box is the capture's ESTIMATED bounds, not a silhouette or a pixel mask.
|
|
420
|
+
/* @__PURE__ */ jsx("div", { className: "tn-debug-outline-layer", children: /* @__PURE__ */ jsx(
|
|
421
|
+
"div",
|
|
422
|
+
{
|
|
423
|
+
className: "tn-debug-outline",
|
|
424
|
+
style: {
|
|
425
|
+
height: `${String(selectedRow.projectedPixels / report.viewport.height * 100)}%`,
|
|
426
|
+
left: `${String((selectedRow.projectedCenter[0] - selectedRow.projectedPixels / 2) / report.viewport.width * 100)}%`,
|
|
427
|
+
top: `${String((selectedRow.projectedCenter[1] - selectedRow.projectedPixels / 2) / report.viewport.height * 100)}%`,
|
|
428
|
+
width: `${String(selectedRow.projectedPixels / report.viewport.width * 100)}%`
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
) })
|
|
432
|
+
),
|
|
433
|
+
copied === void 0 ? null : /* @__PURE__ */ jsx("textarea", { className: "tn-debug-json", readOnly: true, rows: 8, value: copied })
|
|
434
|
+
] })
|
|
435
|
+
] })
|
|
436
|
+
]
|
|
437
|
+
}
|
|
438
|
+
);
|
|
93
439
|
}
|
|
94
440
|
function useGameState(game, selector = (state) => state) {
|
|
95
441
|
return useSyncExternalStore(
|
|
@@ -106,6 +452,54 @@ function useUiLayer(name) {
|
|
|
106
452
|
}
|
|
107
453
|
return value;
|
|
108
454
|
}
|
|
455
|
+
function DevFrameRateChip({ bridge }) {
|
|
456
|
+
const [fps, setFps] = useState(void 0);
|
|
457
|
+
const [verdict, setVerdict] = useState(void 0);
|
|
458
|
+
useEffect(
|
|
459
|
+
() => bridge.onMessage((message) => {
|
|
460
|
+
if (message.type !== UI_DEV_METRICS_MESSAGE) return;
|
|
461
|
+
const value = message.fps;
|
|
462
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) setFps(value);
|
|
463
|
+
const warning = message.sceneWarning;
|
|
464
|
+
if (typeof warning === "string" && warning.length > 0) setVerdict(warning);
|
|
465
|
+
}),
|
|
466
|
+
[bridge]
|
|
467
|
+
);
|
|
468
|
+
if (fps === void 0) return null;
|
|
469
|
+
return (
|
|
470
|
+
// `<output>` carries the status role natively, so the readout is announced without an ARIA
|
|
471
|
+
// attribute standing in for an element that already means this.
|
|
472
|
+
/* @__PURE__ */ jsxs(
|
|
473
|
+
"output",
|
|
474
|
+
{
|
|
475
|
+
className: "tn-dev-fps",
|
|
476
|
+
style: {
|
|
477
|
+
position: "absolute",
|
|
478
|
+
top: 8,
|
|
479
|
+
right: 8,
|
|
480
|
+
padding: "2px 6px",
|
|
481
|
+
borderRadius: 4,
|
|
482
|
+
background: "rgba(0, 0, 0, 0.55)",
|
|
483
|
+
color: "#e8f0ff",
|
|
484
|
+
font: "500 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
485
|
+
pointerEvents: "none",
|
|
486
|
+
zIndex: 2147483647
|
|
487
|
+
},
|
|
488
|
+
children: [
|
|
489
|
+
`${Math.round(fps)} fps`,
|
|
490
|
+
verdict === void 0 ? null : /* @__PURE__ */ jsx(
|
|
491
|
+
"span",
|
|
492
|
+
{
|
|
493
|
+
className: "tn-dev-scene-warning",
|
|
494
|
+
style: { display: "block", maxWidth: 420, color: "#ffd98a", whiteSpace: "normal" },
|
|
495
|
+
children: verdict
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
]
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
);
|
|
502
|
+
}
|
|
109
503
|
function UiLayer({ children }) {
|
|
110
504
|
const [value, setValue] = useState(void 0);
|
|
111
505
|
const registryRef = useRef(void 0);
|
|
@@ -131,7 +525,10 @@ function UiLayer({ children }) {
|
|
|
131
525
|
};
|
|
132
526
|
}, [value]);
|
|
133
527
|
if (value === void 0) return null;
|
|
134
|
-
return /* @__PURE__ */
|
|
528
|
+
return /* @__PURE__ */ jsxs(UiLayerContext.Provider, { value, children: [
|
|
529
|
+
children,
|
|
530
|
+
/* @__PURE__ */ jsx(DevFrameRateChip, { bridge: value.bridge })
|
|
531
|
+
] });
|
|
135
532
|
}
|
|
136
533
|
function useUiState(selector) {
|
|
137
534
|
const { mirror } = useUiLayer("useUiState");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threenative/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React bindings for mounting a ThreeNative game and reading throttled state.",
|
|
6
6
|
"keywords": [
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"sideEffects": false,
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@threenative/core": ">=0.3.
|
|
44
|
+
"@threenative/core": ">=0.3.3 <0.4.0",
|
|
45
45
|
"react": ">=19.2.0 <20",
|
|
46
46
|
"react-dom": ">=19.2.0 <20"
|
|
47
47
|
},
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"react-test-renderer": "19.2.0",
|
|
56
56
|
"tsup": "8.5.1",
|
|
57
57
|
"typescript": "5.9.3",
|
|
58
|
-
"@threenative/core": "0.3.
|
|
58
|
+
"@threenative/core": "0.3.3"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "tsup --config tsup.config.ts && publint",
|