@aethervtt/shard-protocol 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/index.d.ts +4 -0
- package/dist/index.js +18 -0
- package/dist/png.d.ts +9 -0
- package/dist/png.js +69 -0
- package/dist/preview.d.ts +10 -0
- package/dist/preview.js +249 -0
- package/dist/server.d.ts +59 -0
- package/dist/server.js +1086 -0
- package/dist/transport.d.ts +15 -0
- package/dist/transport.js +53 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Dias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { decodePng, encodePng, toBase64 } from './png.js';
|
|
2
|
+
export { type PreviewImage, previewAsset, previewKtx2 } from './preview.js';
|
|
3
|
+
export { createProtocolServer, type HandlerContext, type JsonRpcNotification, type JsonRpcRequest, type JsonRpcResponse, METHODS, type MethodDef, type ProtocolServer, type ProtocolServerOptions, type Topic, } from './server.js';
|
|
4
|
+
export { connectToHub, DEFAULT_HUB_PORT, type HubConnectionOptions } from './transport.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { decodePng, encodePng, toBase64 } from "./png.js";
|
|
2
|
+
import { previewAsset, previewKtx2 } from "./preview.js";
|
|
3
|
+
import {
|
|
4
|
+
createProtocolServer,
|
|
5
|
+
METHODS
|
|
6
|
+
} from "./server.js";
|
|
7
|
+
import { connectToHub, DEFAULT_HUB_PORT } from "./transport.js";
|
|
8
|
+
export {
|
|
9
|
+
DEFAULT_HUB_PORT,
|
|
10
|
+
METHODS,
|
|
11
|
+
connectToHub,
|
|
12
|
+
createProtocolServer,
|
|
13
|
+
decodePng,
|
|
14
|
+
encodePng,
|
|
15
|
+
previewAsset,
|
|
16
|
+
previewKtx2,
|
|
17
|
+
toBase64
|
|
18
|
+
};
|
package/dist/png.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** RGBA8 pixels → PNG bytes. Pure TS; zlib comes from the platform's CompressionStream. */
|
|
2
|
+
export declare function encodePng(rgba: Uint8Array, width: number, height: number): Promise<Uint8Array>;
|
|
3
|
+
/** Decodes a PNG (any color type or bit depth) to RGBA8. For tests and tools. */
|
|
4
|
+
export declare function decodePng(png: Uint8Array): Promise<{
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
data: Uint8Array;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function toBase64(bytes: Uint8Array): string;
|
package/dist/png.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { decodePngImage } from "@aethervtt/shard-texture";
|
|
2
|
+
const CRC_TABLE = (() => {
|
|
3
|
+
const table = new Uint32Array(256);
|
|
4
|
+
for (let n = 0; n < 256; n++) {
|
|
5
|
+
let c = n;
|
|
6
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
7
|
+
table[n] = c >>> 0;
|
|
8
|
+
}
|
|
9
|
+
return table;
|
|
10
|
+
})();
|
|
11
|
+
function crc32(bytes) {
|
|
12
|
+
let c = 4294967295;
|
|
13
|
+
for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
|
|
14
|
+
return (c ^ 4294967295) >>> 0;
|
|
15
|
+
}
|
|
16
|
+
async function streamBytes(input, stream) {
|
|
17
|
+
const out = new Response(new Blob([input]).stream().pipeThrough(stream));
|
|
18
|
+
return new Uint8Array(await out.arrayBuffer());
|
|
19
|
+
}
|
|
20
|
+
function chunk(type, data) {
|
|
21
|
+
const out = new Uint8Array(12 + data.length);
|
|
22
|
+
const view = new DataView(out.buffer);
|
|
23
|
+
view.setUint32(0, data.length);
|
|
24
|
+
for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
|
|
25
|
+
out.set(data, 8);
|
|
26
|
+
view.setUint32(8 + data.length, crc32(out.subarray(4, 8 + data.length)));
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
async function encodePng(rgba, width, height) {
|
|
30
|
+
const raw = new Uint8Array((width * 4 + 1) * height);
|
|
31
|
+
for (let y = 0; y < height; y++) {
|
|
32
|
+
raw[y * (width * 4 + 1)] = 0;
|
|
33
|
+
raw.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * (width * 4 + 1) + 1);
|
|
34
|
+
}
|
|
35
|
+
const ihdr = new Uint8Array(13);
|
|
36
|
+
const view = new DataView(ihdr.buffer);
|
|
37
|
+
view.setUint32(0, width);
|
|
38
|
+
view.setUint32(4, height);
|
|
39
|
+
ihdr[8] = 8;
|
|
40
|
+
ihdr[9] = 6;
|
|
41
|
+
const parts = [
|
|
42
|
+
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
43
|
+
chunk("IHDR", ihdr),
|
|
44
|
+
chunk("IDAT", await streamBytes(raw, new CompressionStream("deflate"))),
|
|
45
|
+
chunk("IEND", new Uint8Array())
|
|
46
|
+
];
|
|
47
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
48
|
+
let offset = 0;
|
|
49
|
+
for (const p of parts) {
|
|
50
|
+
out.set(p, offset);
|
|
51
|
+
offset += p.length;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
async function decodePng(png) {
|
|
56
|
+
const image = await decodePngImage(png);
|
|
57
|
+
return { width: image.width, height: image.height, data: image.data };
|
|
58
|
+
}
|
|
59
|
+
function toBase64(bytes) {
|
|
60
|
+
let s = "";
|
|
61
|
+
for (let i = 0; i < bytes.length; i += 32768)
|
|
62
|
+
s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
63
|
+
return btoa(s);
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
decodePng,
|
|
67
|
+
encodePng,
|
|
68
|
+
toBase64
|
|
69
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type World } from '@aethervtt/shard-core';
|
|
2
|
+
export interface PreviewImage {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
data: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
/** The top mip of a KTX2 texture as RGBA8, fit into width × height (HDR tonemapped). */
|
|
8
|
+
export declare function previewKtx2(bytes: Uint8Array, width: number, height: number): Promise<PreviewImage>;
|
|
9
|
+
/** A preview image of any asset: textures on the CPU, everything else rendered. */
|
|
10
|
+
export declare function previewAsset(world: World, path: string, width: number, height: number, options?: Readonly<Record<string, unknown>>): Promise<PreviewImage>;
|
package/dist/preview.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { AssetServerResource, assetServer, findAssetPreview } from "@aethervtt/shard-assets";
|
|
2
|
+
import {
|
|
3
|
+
Children,
|
|
4
|
+
findResource,
|
|
5
|
+
quat,
|
|
6
|
+
ShardError,
|
|
7
|
+
vec3
|
|
8
|
+
} from "@aethervtt/shard-core";
|
|
9
|
+
import {
|
|
10
|
+
captureView,
|
|
11
|
+
forwardPlugin,
|
|
12
|
+
Gpu,
|
|
13
|
+
Materials,
|
|
14
|
+
Mesh3d,
|
|
15
|
+
Meshes,
|
|
16
|
+
OffscreenTarget,
|
|
17
|
+
RenderStats,
|
|
18
|
+
renderPlugin,
|
|
19
|
+
Shaders
|
|
20
|
+
} from "@aethervtt/shard-render";
|
|
21
|
+
import { App } from "@aethervtt/shard-runtime";
|
|
22
|
+
import {
|
|
23
|
+
loadScene,
|
|
24
|
+
PrefabAssets,
|
|
25
|
+
releaseSceneHooks,
|
|
26
|
+
SceneAssets,
|
|
27
|
+
ScenePlugin,
|
|
28
|
+
unloadScene,
|
|
29
|
+
whenSceneReady
|
|
30
|
+
} from "@aethervtt/shard-scene";
|
|
31
|
+
import { readKtx2, Textures, transcodeBasis } from "@aethervtt/shard-texture";
|
|
32
|
+
import { GlobalTransform, TransformPlugin } from "@aethervtt/shard-transform";
|
|
33
|
+
function halfToFloat(h) {
|
|
34
|
+
const s = h & 32768 ? -1 : 1;
|
|
35
|
+
const e = h >> 10 & 31;
|
|
36
|
+
const f = h & 1023;
|
|
37
|
+
if (e === 0) return s * 2 ** -14 * (f / 1024);
|
|
38
|
+
if (e === 31) return f ? Number.NaN : s * Number.POSITIVE_INFINITY;
|
|
39
|
+
return s * 2 ** (e - 15) * (1 + f / 1024);
|
|
40
|
+
}
|
|
41
|
+
function fit(src, sw, sh, width, height) {
|
|
42
|
+
const scale = Math.min(width / sw, height / sh);
|
|
43
|
+
const w = Math.max(1, Math.round(sw * scale));
|
|
44
|
+
const h = Math.max(1, Math.round(sh * scale));
|
|
45
|
+
const out = new Uint8Array(w * h * 4);
|
|
46
|
+
for (let y = 0; y < h; y++) {
|
|
47
|
+
const sy = Math.min(sh - 1, Math.floor(y / scale));
|
|
48
|
+
for (let x = 0; x < w; x++) {
|
|
49
|
+
const sx = Math.min(sw - 1, Math.floor(x / scale));
|
|
50
|
+
out.set(src.subarray((sy * sw + sx) * 4, (sy * sw + sx) * 4 + 4), (y * w + x) * 4);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { width: w, height: h, data: out };
|
|
54
|
+
}
|
|
55
|
+
async function texturePreview(world, path, width, height) {
|
|
56
|
+
const artifact = await assetServer(world).artifact(path);
|
|
57
|
+
return previewKtx2(artifact.bytes, width, height);
|
|
58
|
+
}
|
|
59
|
+
async function previewKtx2(bytes, width, height) {
|
|
60
|
+
const ktx = readKtx2(bytes);
|
|
61
|
+
let rgba;
|
|
62
|
+
if (ktx.basis) {
|
|
63
|
+
rgba = (await transcodeBasis(ktx.bytes, "rgba8")).levels[0];
|
|
64
|
+
} else if (ktx.usage === "hdr") {
|
|
65
|
+
const half = new Uint16Array(ktx.levels[0].slice().buffer);
|
|
66
|
+
rgba = new Uint8Array(ktx.width * ktx.height * 4);
|
|
67
|
+
for (let i = 0; i < half.length; i++) {
|
|
68
|
+
const v = halfToFloat(half[i]);
|
|
69
|
+
const mapped = i % 4 === 3 ? 1 : (v / (1 + v)) ** (1 / 2.2);
|
|
70
|
+
rgba[i] = Math.round(Math.min(1, Math.max(0, mapped)) * 255);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
rgba = ktx.levels[0];
|
|
74
|
+
}
|
|
75
|
+
return fit(rgba, ktx.width, ktx.height, width, height);
|
|
76
|
+
}
|
|
77
|
+
async function renderPreview(world, path, type, width, height) {
|
|
78
|
+
const gpu = world.resource(Gpu);
|
|
79
|
+
const server = assetServer(world);
|
|
80
|
+
const target = new OffscreenTarget(gpu, { label: "preview", width, height });
|
|
81
|
+
const app = new App();
|
|
82
|
+
app.world.insertResource(AssetServerResource, server);
|
|
83
|
+
for (const def of [Meshes, Materials, Textures, SceneAssets, PrefabAssets]) {
|
|
84
|
+
app.world.insertResource(def, world.initResource(def));
|
|
85
|
+
}
|
|
86
|
+
for (const name of ["procgen/Runtime", "procgen/Generators", "procgen/Data"]) {
|
|
87
|
+
const def = findResource(name);
|
|
88
|
+
if (def && world.hasResource(def)) app.world.insertResource(def, world.resource(def));
|
|
89
|
+
}
|
|
90
|
+
app.addPlugin(
|
|
91
|
+
TransformPlugin,
|
|
92
|
+
// The game's shader library, so materials with project shaders preview with them.
|
|
93
|
+
renderPlugin({ gpu, target, shaders: world.resource(Shaders) }),
|
|
94
|
+
forwardPlugin(),
|
|
95
|
+
ScenePlugin
|
|
96
|
+
);
|
|
97
|
+
try {
|
|
98
|
+
await app.init();
|
|
99
|
+
await server.load(path);
|
|
100
|
+
let min = [-1, -1, -1];
|
|
101
|
+
let max = [1, 1, 1];
|
|
102
|
+
const subject = { "core/Transform": {} };
|
|
103
|
+
if (type === "Material") {
|
|
104
|
+
subject["render/Mesh3d"] = { mesh: { path: "procedural:sphere?radius=1&segments=48" } };
|
|
105
|
+
subject["render/MeshMaterial"] = { material: { path } };
|
|
106
|
+
} else if (type === "Prefab") {
|
|
107
|
+
subject["scene/PrefabInstance"] = { prefab: { path } };
|
|
108
|
+
} else if (type === "Mesh") {
|
|
109
|
+
subject["render/Mesh3d"] = { mesh: { path } };
|
|
110
|
+
const b = world.resource(Meshes).get(server.resolve(path)).bounds;
|
|
111
|
+
min = [b[0], b[1], b[2]];
|
|
112
|
+
max = [b[3], b[4], b[5]];
|
|
113
|
+
} else {
|
|
114
|
+
subject["scene/SceneInstance"] = { scene: { path } };
|
|
115
|
+
const bounds = server.info(path).info?.bounds;
|
|
116
|
+
if (bounds) {
|
|
117
|
+
min = bounds.min;
|
|
118
|
+
max = bounds.max;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (type === "Prefab") {
|
|
122
|
+
const probe = loadScene(
|
|
123
|
+
app.world,
|
|
124
|
+
{ version: 1, entities: [{ name: "subject", components: subject }] },
|
|
125
|
+
{ id: "preview-bounds" }
|
|
126
|
+
);
|
|
127
|
+
await whenSceneReady(app.world, "preview-bounds");
|
|
128
|
+
app.update(1 / 60);
|
|
129
|
+
const box = worldBounds(app.world, probe.entities.get("subject"));
|
|
130
|
+
if (box) {
|
|
131
|
+
min = box.min;
|
|
132
|
+
max = box.max;
|
|
133
|
+
}
|
|
134
|
+
unloadScene(app.world, "preview-bounds", { collect: false });
|
|
135
|
+
}
|
|
136
|
+
const center = [0, 1, 2].map((i) => (min[i] + max[i]) / 2);
|
|
137
|
+
const radius = Math.max(
|
|
138
|
+
1e-3,
|
|
139
|
+
Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) / 2
|
|
140
|
+
);
|
|
141
|
+
const fov = 40;
|
|
142
|
+
const distance = radius / Math.sin(fov / 2 * Math.PI / 180) * 1.05;
|
|
143
|
+
const dir = vec3.normalize(vec3.create(), vec3.create(1, 0.7, 1.2));
|
|
144
|
+
const eye = [0, 1, 2].map((i) => center[i] + dir[i] * distance);
|
|
145
|
+
const rotation = quat.lookRotation(
|
|
146
|
+
quat.create(),
|
|
147
|
+
vec3.create(-dir[0], -dir[1], -dir[2]),
|
|
148
|
+
vec3.create(0, 1, 0)
|
|
149
|
+
);
|
|
150
|
+
const scene = {
|
|
151
|
+
version: 1,
|
|
152
|
+
resources: { "render/AmbientLight": { color: [1, 1, 1], brightness: 3e3 } },
|
|
153
|
+
entities: [
|
|
154
|
+
{
|
|
155
|
+
name: "key",
|
|
156
|
+
components: {
|
|
157
|
+
"render/DirectionalLight": { illuminance: "daylight" },
|
|
158
|
+
"core/Transform": { rotationEuler: [-45, 35, 0] }
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{ name: "subject", components: subject },
|
|
162
|
+
{
|
|
163
|
+
name: "camera",
|
|
164
|
+
components: {
|
|
165
|
+
"render/Camera3d": {
|
|
166
|
+
fovY: fov,
|
|
167
|
+
near: Math.max(1e-4, distance - radius * 2) / 10,
|
|
168
|
+
clearColor: [0.05, 0.055, 0.07, 1]
|
|
169
|
+
},
|
|
170
|
+
"core/Transform": { translation: eye, rotation: [...rotation] }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
};
|
|
175
|
+
const { entities } = loadScene(app.world, scene, { id: "preview" });
|
|
176
|
+
await whenSceneReady(app.world, "preview");
|
|
177
|
+
for (let i = 0; i < 200; i++) {
|
|
178
|
+
app.update(1 / 60);
|
|
179
|
+
await app.world.resource(Shaders).whenIdle();
|
|
180
|
+
await gpu.pipelines.whenIdle();
|
|
181
|
+
const stats = [...app.world.tryResource(RenderStats)?.values() ?? []];
|
|
182
|
+
const pending = stats.reduce((n, v) => n + v.pending, 0);
|
|
183
|
+
if (i >= 3 && pending === 0 && gpu.pipelines.pending === 0 && gpu.pipelines.skipped === 0) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
await new Promise((r) => setTimeout(r, 2));
|
|
187
|
+
}
|
|
188
|
+
const shot = captureView(app.world, `camera:${entities.get("camera")}`);
|
|
189
|
+
app.update(1 / 60);
|
|
190
|
+
const image = await shot;
|
|
191
|
+
return { width: image.width, height: image.height, data: image.data };
|
|
192
|
+
} finally {
|
|
193
|
+
releaseSceneHooks(app.world);
|
|
194
|
+
target.destroy();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function worldBounds(world, root) {
|
|
198
|
+
const meshes = world.resource(Meshes);
|
|
199
|
+
const min = [Infinity, Infinity, Infinity];
|
|
200
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
201
|
+
const visit = (e) => {
|
|
202
|
+
const m = world.tryGet(e, Mesh3d);
|
|
203
|
+
const g = world.tryGet(e, GlobalTransform)?.matrix;
|
|
204
|
+
const mesh = m && meshes.get(m.mesh);
|
|
205
|
+
if (mesh && g) {
|
|
206
|
+
const b = mesh.bounds;
|
|
207
|
+
for (let c = 0; c < 8; c++) {
|
|
208
|
+
const x = c & 1 ? b[3] : b[0];
|
|
209
|
+
const y = c & 2 ? b[4] : b[1];
|
|
210
|
+
const z = c & 4 ? b[5] : b[2];
|
|
211
|
+
for (let r = 0; r < 3; r++) {
|
|
212
|
+
const v = g[r * 4] * x + g[r * 4 + 1] * y + g[r * 4 + 2] * z + g[r * 4 + 3];
|
|
213
|
+
if (v < min[r]) min[r] = v;
|
|
214
|
+
if (v > max[r]) max[r] = v;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const child of world.tryGet(e, Children)?.entities ?? []) if (child !== null) visit(child);
|
|
219
|
+
};
|
|
220
|
+
visit(root);
|
|
221
|
+
return min[0] <= max[0] ? { min, max } : void 0;
|
|
222
|
+
}
|
|
223
|
+
async function previewAsset(world, path, width, height, options) {
|
|
224
|
+
const entry = assetServer(world).entry(path);
|
|
225
|
+
if (!entry) {
|
|
226
|
+
throw new ShardError("assets/not-found", `No asset "${path}" in the catalog`, {
|
|
227
|
+
hint: "asset.list shows every asset."
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (entry.type === "Texture") return texturePreview(world, entry.path, width, height);
|
|
231
|
+
if (entry.type === "Material" || entry.type === "Mesh" || entry.type === "Scene" || entry.type === "Prefab") {
|
|
232
|
+
if (!world.tryResource(Gpu)) {
|
|
233
|
+
throw new ShardError(
|
|
234
|
+
"protocol/no-renderer",
|
|
235
|
+
"Previews of meshes, materials, and scenes need the renderer"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return renderPreview(world, entry.path, entry.type, width, height);
|
|
239
|
+
}
|
|
240
|
+
const custom = findAssetPreview(entry.type);
|
|
241
|
+
if (custom) return custom(world, entry.path, width, height, options);
|
|
242
|
+
throw new ShardError("protocol/no-preview", `${entry.type} assets have no preview`, {
|
|
243
|
+
hint: "Previews exist for textures, materials, meshes, scenes, and types that register one."
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
export {
|
|
247
|
+
previewAsset,
|
|
248
|
+
previewKtx2
|
|
249
|
+
};
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type ComponentDef, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { Platform } from '@aethervtt/shard-platform';
|
|
3
|
+
import { type App } from '@aethervtt/shard-runtime';
|
|
4
|
+
export interface JsonRpcRequest {
|
|
5
|
+
jsonrpc: '2.0';
|
|
6
|
+
id?: number | string | null;
|
|
7
|
+
method: string;
|
|
8
|
+
params?: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface JsonRpcResponse {
|
|
11
|
+
jsonrpc: '2.0';
|
|
12
|
+
id: number | string | null;
|
|
13
|
+
result?: unknown;
|
|
14
|
+
error?: {
|
|
15
|
+
code: number;
|
|
16
|
+
message: string;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface JsonRpcNotification {
|
|
21
|
+
jsonrpc: '2.0';
|
|
22
|
+
method: string;
|
|
23
|
+
params: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface MethodDef {
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
params: ComponentDef;
|
|
29
|
+
handler(ctx: HandlerContext, params: Record<string, unknown>): unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface HandlerContext {
|
|
32
|
+
app: App;
|
|
33
|
+
world: World;
|
|
34
|
+
options: ProtocolServerOptions;
|
|
35
|
+
}
|
|
36
|
+
export interface ProtocolServerOptions {
|
|
37
|
+
/**
|
|
38
|
+
* 'manual': the host has no frame loop (headless servers, tests); `time.step` runs frames itself.
|
|
39
|
+
* 'loop': a runner drives frames; `time.step` waits for it. Default 'manual'.
|
|
40
|
+
*/
|
|
41
|
+
frames?: 'manual' | 'loop';
|
|
42
|
+
/** For scene files by path. */
|
|
43
|
+
platform?: Platform;
|
|
44
|
+
/** Extra methods from the host (e.g. `project.reload`), added to the built-in ones. */
|
|
45
|
+
methods?: readonly MethodDef[];
|
|
46
|
+
}
|
|
47
|
+
export declare const METHODS: MethodDef[];
|
|
48
|
+
export type Topic = 'log' | 'error' | 'frame' | 'project';
|
|
49
|
+
export interface ProtocolServer {
|
|
50
|
+
readonly methods: readonly MethodDef[];
|
|
51
|
+
handle(request: JsonRpcRequest): Promise<JsonRpcResponse | undefined>;
|
|
52
|
+
/** Receives notifications for subscribed topics. Returns an unsubscribe function. */
|
|
53
|
+
onNotification(listener: (notification: JsonRpcNotification) => void): () => void;
|
|
54
|
+
/** Sends a notification on `topic` to subscribers (hosts use it for e.g. project reloads). */
|
|
55
|
+
publish(topic: Topic, params: unknown): void;
|
|
56
|
+
close(): void;
|
|
57
|
+
}
|
|
58
|
+
/** Serves the protocol for one app. Transports feed requests into `handle`. */
|
|
59
|
+
export declare function createProtocolServer(app: App, options?: ProtocolServerOptions): ProtocolServer;
|