@adea-ai/spatial 0.69.10
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 +201 -0
- package/README.md +22 -0
- package/dist/asset-manifests.d.ts +88 -0
- package/dist/asset-manifests.js +1 -0
- package/dist/engine.d.ts +55 -0
- package/dist/engine.js +82 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +15 -0
- package/dist/manifests.d.ts +3 -0
- package/dist/manifests.js +26 -0
- package/dist/scene-spawn.d.ts +15 -0
- package/dist/scene-spawn.js +88 -0
- package/dist/telemetry.d.ts +106 -0
- package/dist/telemetry.js +79 -0
- package/package.json +44 -0
- package/src/asset-manifests.ts +95 -0
- package/src/engine.ts +97 -0
- package/src/index.ts +41 -0
- package/src/manifests.ts +41 -0
- package/src/scene-spawn.ts +107 -0
- package/src/telemetry.ts +202 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export type SceneNavigationType = 'navigate' | 'push' | 'replace' | 'traverse' | 'scene-mount';
|
|
2
|
+
export type ScenePerformanceReport = {
|
|
3
|
+
version: 1;
|
|
4
|
+
event: 'load' | 'runtime' | 'dispose' | 'error';
|
|
5
|
+
release?: string;
|
|
6
|
+
scene: string;
|
|
7
|
+
path: string;
|
|
8
|
+
navigation: {
|
|
9
|
+
id: string;
|
|
10
|
+
type: SceneNavigationType;
|
|
11
|
+
startTime: number;
|
|
12
|
+
};
|
|
13
|
+
milestones: {
|
|
14
|
+
firstRenderedFrameMs?: number;
|
|
15
|
+
playableCharacterMs?: number;
|
|
16
|
+
phasesMs: Record<string, number>;
|
|
17
|
+
};
|
|
18
|
+
network: {
|
|
19
|
+
requests: number;
|
|
20
|
+
networkRequests: number;
|
|
21
|
+
cacheHits: number;
|
|
22
|
+
unknownCacheStatus: number;
|
|
23
|
+
transferBytes: number;
|
|
24
|
+
encodedBodyBytes: number;
|
|
25
|
+
decodedBodyBytes: number;
|
|
26
|
+
};
|
|
27
|
+
runtime: {
|
|
28
|
+
sampleWindowMs: number;
|
|
29
|
+
frames: number;
|
|
30
|
+
fps?: number;
|
|
31
|
+
p50FrameMs?: number;
|
|
32
|
+
p95FrameMs?: number;
|
|
33
|
+
longestFrameMs?: number;
|
|
34
|
+
p95FrameIntervalMs?: number;
|
|
35
|
+
longestFrameIntervalMs?: number;
|
|
36
|
+
longTasks: number;
|
|
37
|
+
longTaskDurationMs: number;
|
|
38
|
+
drawCalls?: number;
|
|
39
|
+
drawCallsP95?: number;
|
|
40
|
+
drawCallsMax?: number;
|
|
41
|
+
triangles?: number;
|
|
42
|
+
geometries?: number;
|
|
43
|
+
textures?: number;
|
|
44
|
+
programs?: number;
|
|
45
|
+
reactCommits?: number;
|
|
46
|
+
reactCommitDurationMs?: number;
|
|
47
|
+
reactMaxCommitMs?: number;
|
|
48
|
+
};
|
|
49
|
+
renderables: SceneRuntimeStats;
|
|
50
|
+
lifecycle: {
|
|
51
|
+
disposedGeometries: number;
|
|
52
|
+
disposedMaterials: number;
|
|
53
|
+
disposedTextures: number;
|
|
54
|
+
};
|
|
55
|
+
device: {
|
|
56
|
+
tier: 'high' | 'standard' | 'low';
|
|
57
|
+
mobile: boolean;
|
|
58
|
+
devicePixelRatio: number;
|
|
59
|
+
hardwareConcurrency: number;
|
|
60
|
+
deviceMemoryGb?: number;
|
|
61
|
+
webglVersion: string;
|
|
62
|
+
gpu?: string;
|
|
63
|
+
maxTextureSize?: number;
|
|
64
|
+
};
|
|
65
|
+
error?: string;
|
|
66
|
+
};
|
|
67
|
+
export type SceneRuntimeStats = {
|
|
68
|
+
meshes: number;
|
|
69
|
+
visibleMeshes: number;
|
|
70
|
+
frustumCulledMeshes: number;
|
|
71
|
+
materials: number;
|
|
72
|
+
instancedMeshes: number;
|
|
73
|
+
instances: number;
|
|
74
|
+
foliageMeshes: number;
|
|
75
|
+
foliageInstances: number;
|
|
76
|
+
fenceMeshes: number;
|
|
77
|
+
fenceInstances: number;
|
|
78
|
+
repeatedGeometryGroups: number;
|
|
79
|
+
repeatedGeometryInstances: number;
|
|
80
|
+
};
|
|
81
|
+
export declare const SCENE_TELEMETRY_LOG_PREFIX = "[scene-telemetry] ";
|
|
82
|
+
export declare const MAX_SCENE_TELEMETRY_BYTES: number;
|
|
83
|
+
export type SceneTelemetryEnvelope = {
|
|
84
|
+
receivedAt: string;
|
|
85
|
+
deployment?: string;
|
|
86
|
+
report: ScenePerformanceReport;
|
|
87
|
+
};
|
|
88
|
+
export declare function parseScenePerformanceReport(value: unknown): ScenePerformanceReport | null;
|
|
89
|
+
export declare function createSceneTelemetryEnvelope(report: ScenePerformanceReport, receivedAt?: Date, deployment?: string | undefined): SceneTelemetryEnvelope;
|
|
90
|
+
type NavigationStart = {
|
|
91
|
+
id: string;
|
|
92
|
+
url: string;
|
|
93
|
+
startTime: number;
|
|
94
|
+
type: 'navigate' | 'push' | 'replace' | 'traverse';
|
|
95
|
+
claimed?: boolean;
|
|
96
|
+
};
|
|
97
|
+
declare global {
|
|
98
|
+
interface Window {
|
|
99
|
+
__ADEA_NAVIGATION_START__?: NavigationStart;
|
|
100
|
+
__ADEA_SCENE_TELEMETRY_ENDPOINT__?: string;
|
|
101
|
+
__ADEA_RELEASE__?: string;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export declare function recordNavigation(url: string, type: 'navigate' | 'push' | 'replace' | 'traverse', startTime: number): void;
|
|
105
|
+
export declare function onRouterTransitionStart(url: string, navigationType: 'push' | 'replace' | 'traverse'): void;
|
|
106
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const SCENE_TELEMETRY_LOG_PREFIX = '[scene-telemetry] ';
|
|
2
|
+
export const MAX_SCENE_TELEMETRY_BYTES = 64 * 1024;
|
|
3
|
+
function isFiniteNumber(value) {
|
|
4
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
5
|
+
}
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function validReport(value) {
|
|
10
|
+
if (!isRecord(value))
|
|
11
|
+
return false;
|
|
12
|
+
if (value.version !== 1 || !['load', 'runtime', 'dispose', 'error'].includes(String(value.event)))
|
|
13
|
+
return false;
|
|
14
|
+
if (typeof value.scene !== 'string' || value.scene.length < 1 || value.scene.length > 80)
|
|
15
|
+
return false;
|
|
16
|
+
if (typeof value.path !== 'string' || value.path.length < 1 || value.path.length > 256)
|
|
17
|
+
return false;
|
|
18
|
+
if (value.release != null && (typeof value.release !== 'string' || value.release.length > 128))
|
|
19
|
+
return false;
|
|
20
|
+
if (!isRecord(value.navigation) ||
|
|
21
|
+
typeof value.navigation.id !== 'string' ||
|
|
22
|
+
!isFiniteNumber(value.navigation.startTime))
|
|
23
|
+
return false;
|
|
24
|
+
if (!isRecord(value.milestones) || !isRecord(value.milestones.phasesMs))
|
|
25
|
+
return false;
|
|
26
|
+
if (!isRecord(value.network) ||
|
|
27
|
+
!isFiniteNumber(value.network.requests) ||
|
|
28
|
+
!isFiniteNumber(value.network.transferBytes))
|
|
29
|
+
return false;
|
|
30
|
+
if (!isRecord(value.runtime) ||
|
|
31
|
+
!isFiniteNumber(value.runtime.sampleWindowMs) ||
|
|
32
|
+
!isFiniteNumber(value.runtime.frames))
|
|
33
|
+
return false;
|
|
34
|
+
if (!isRecord(value.device) || !['high', 'standard', 'low'].includes(String(value.device.tier)))
|
|
35
|
+
return false;
|
|
36
|
+
if (typeof value.device.mobile !== 'boolean' || !isFiniteNumber(value.device.devicePixelRatio))
|
|
37
|
+
return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
export function parseScenePerformanceReport(value) {
|
|
41
|
+
return validReport(value) ? value : null;
|
|
42
|
+
}
|
|
43
|
+
export function createSceneTelemetryEnvelope(report, receivedAt = new Date(), deployment = process.env.DEPLOY_GIT_COMMIT_SHA) {
|
|
44
|
+
return {
|
|
45
|
+
receivedAt: receivedAt.toISOString(),
|
|
46
|
+
...(deployment ? { deployment } : {}),
|
|
47
|
+
report,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function recordNavigation(url, type, startTime) {
|
|
51
|
+
window.__ADEA_NAVIGATION_START__ = {
|
|
52
|
+
id: `nav-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
53
|
+
url,
|
|
54
|
+
startTime,
|
|
55
|
+
type,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const navigation = performance.getEntriesByType('navigation')[0];
|
|
60
|
+
recordNavigation(window.location.href, 'navigate', navigation?.startTime ?? 0);
|
|
61
|
+
const endpoint = process.env.ADEA_PUBLIC_SCENE_TELEMETRY_ENDPOINT ||
|
|
62
|
+
(process.env.NODE_ENV === 'production' ? '/api/telemetry/scene-performance' : undefined);
|
|
63
|
+
if (endpoint)
|
|
64
|
+
window.__ADEA_SCENE_TELEMETRY_ENDPOINT__ = endpoint;
|
|
65
|
+
const release = process.env.ADEA_PUBLIC_DEPLOY_GIT_COMMIT_SHA;
|
|
66
|
+
if (release)
|
|
67
|
+
window.__ADEA_RELEASE__ = release;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Telemetry must never interfere with page initialization.
|
|
71
|
+
}
|
|
72
|
+
export function onRouterTransitionStart(url, navigationType) {
|
|
73
|
+
try {
|
|
74
|
+
recordNavigation(new URL(url, window.location.href).href, navigationType, performance.now());
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Invalid route metadata should not affect navigation.
|
|
78
|
+
}
|
|
79
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adea-ai/spatial",
|
|
3
|
+
"version": "0.69.10",
|
|
4
|
+
"description": "Scene and asset manifest contracts plus the shell-to-engine spatial protocol for Adea workspaces: scene mount manifests, spawn URL helpers, and scene telemetry schemas. Pure types — no rendering, no binaries.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"assets",
|
|
7
|
+
"manifest",
|
|
8
|
+
"protocol",
|
|
9
|
+
"scene",
|
|
10
|
+
"spatial",
|
|
11
|
+
"telemetry",
|
|
12
|
+
"threejs",
|
|
13
|
+
"workspace"
|
|
14
|
+
],
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/adea-ai/adea.git",
|
|
19
|
+
"directory": "packages/spatial"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src",
|
|
24
|
+
"tsconfig.json",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "dist/index.js",
|
|
29
|
+
"types": "dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"development": "./src/index.ts",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"lint": "oxlint",
|
|
39
|
+
"build": "tsc -p tsconfig.json",
|
|
40
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
41
|
+
"test": "bun test test/*.test.ts",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type SceneAvailability = 'ready' | 'planned'
|
|
2
|
+
|
|
3
|
+
export type SceneZone = {
|
|
4
|
+
id: string
|
|
5
|
+
assetUrl: string
|
|
6
|
+
/** Optional collision layer that follows this visual zone. */
|
|
7
|
+
collisionAssetUrl?: string
|
|
8
|
+
/** Additional visual layers that load and unload with this zone. */
|
|
9
|
+
assetUrls?: readonly string[]
|
|
10
|
+
/** Exact-triangle field colliders that load and unload with this zone. */
|
|
11
|
+
collisionAssetUrls?: readonly string[]
|
|
12
|
+
preload?: boolean
|
|
13
|
+
/** Load the zone automatically when the player enters this radius. */
|
|
14
|
+
preloadDistance?: number
|
|
15
|
+
/** Optional transform for a model authored in local building space. */
|
|
16
|
+
transform?: {
|
|
17
|
+
position?: readonly [number, number, number]
|
|
18
|
+
quaternion?: readonly [number, number, number, number]
|
|
19
|
+
scale?: readonly [number, number, number]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SceneStartPosition = {
|
|
24
|
+
x: number
|
|
25
|
+
y: number
|
|
26
|
+
z: number
|
|
27
|
+
yaw?: number
|
|
28
|
+
pitch?: number
|
|
29
|
+
/** Keep an authored start height instead of snapping to the first ray hit. */
|
|
30
|
+
snapToGround?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type StaticFieldAssetUrls = {
|
|
34
|
+
foliage?: string
|
|
35
|
+
props?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type AssignedPropManifestPlacement = {
|
|
39
|
+
id?: string
|
|
40
|
+
p?: readonly [number, number, number]
|
|
41
|
+
q?: readonly [number, number, number, number]
|
|
42
|
+
s?: readonly [number, number, number]
|
|
43
|
+
footprint?: readonly [number, number]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type AssignedPropManifestAsset = {
|
|
47
|
+
assetUrl: string
|
|
48
|
+
defaultScale: number
|
|
49
|
+
footprint?: readonly [number, number]
|
|
50
|
+
placementSurface?: 'floor' | 'wall'
|
|
51
|
+
wallMountHeight?: number
|
|
52
|
+
floorLift?: number
|
|
53
|
+
placeableOnTop?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Runtime-only room content for props already assigned to a scene. */
|
|
57
|
+
export type AssignedPropsManifest = {
|
|
58
|
+
version?: number
|
|
59
|
+
scene?: string
|
|
60
|
+
assets: Record<string, AssignedPropManifestAsset>
|
|
61
|
+
placements: Record<string, AssignedPropManifestPlacement[]>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type SceneManifest = {
|
|
65
|
+
id: string
|
|
66
|
+
label: string
|
|
67
|
+
availability?: SceneAvailability
|
|
68
|
+
entryAssetUrl: string
|
|
69
|
+
/** Optional id for making the initially loaded asset unloadable as a zone. */
|
|
70
|
+
entryZoneId?: string
|
|
71
|
+
/** Keep the entry collision layer active while visual zones transition. */
|
|
72
|
+
preserveEntryCollision?: boolean
|
|
73
|
+
collisionAssetUrl?: string
|
|
74
|
+
/** Additional collision layers kept active for multi-level scenes. */
|
|
75
|
+
additionalCollisionAssetUrls?: readonly string[]
|
|
76
|
+
additionalAssetUrls?: readonly string[]
|
|
77
|
+
zones?: SceneZone[]
|
|
78
|
+
startPosition?: SceneStartPosition
|
|
79
|
+
/** Build-generated instanced fields for static scenes (preferred over runtime manifests). */
|
|
80
|
+
staticFieldAssetUrls?: StaticFieldAssetUrls
|
|
81
|
+
/** Render-free exact-triangle companions for the generated static fields. */
|
|
82
|
+
staticFieldCollisionAssetUrls?: StaticFieldAssetUrls
|
|
83
|
+
/** Optional foliage placement manifest (instanced from the foliage catalog at runtime). */
|
|
84
|
+
foliageManifestUrl?: string
|
|
85
|
+
/** Optional props placement manifest (instanced from the props catalog at runtime). */
|
|
86
|
+
propsManifestUrl?: string
|
|
87
|
+
/** Runtime-only manifest for props explicitly assigned to this workspace. */
|
|
88
|
+
assignedPropsManifestUrl?: string
|
|
89
|
+
/** Public URL for persisted authored-object editor overrides. */
|
|
90
|
+
editorOverridesUrl?: string
|
|
91
|
+
/** Optional repository-relative source path for persisted overrides. */
|
|
92
|
+
editorOverridesSourcePath?: string
|
|
93
|
+
/** Optional repository-relative public copy path for persisted overrides. */
|
|
94
|
+
editorOverridesPublicPath?: string
|
|
95
|
+
}
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entitlement gate and loading contract for the Agent Sim engine.
|
|
3
|
+
*
|
|
4
|
+
* The engine (models, textures, audio, runtime code) lives in the private
|
|
5
|
+
* agent-sim repo and never ships in this public one. The shell may only
|
|
6
|
+
* resolve the engine remote when the deployment is entitled to it:
|
|
7
|
+
*
|
|
8
|
+
* - Web: the site is served from an official Adea domain (adea.dev /
|
|
9
|
+
* adea.io, including subdomains). Forked deployments on other origins
|
|
10
|
+
* are refused before any engine fetch happens.
|
|
11
|
+
* - Desktop / any build: an official build packs the engine into its own
|
|
12
|
+
* assets at build time (see scripts/pack-agent-sim.mjs, wired into the
|
|
13
|
+
* release lanes). The pack writes a manifest at a well-known same-origin
|
|
14
|
+
* URL; plain checkouts and fork builds simply do not have it.
|
|
15
|
+
*
|
|
16
|
+
* Both signals are checked; the engine is fetched only when both pass, so
|
|
17
|
+
* copying engine assets into a forked website still fails the host gate.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const OFFICIAL_AGENT_SIM_WEB_HOSTS = ['adea.dev', 'adea.io'] as const
|
|
21
|
+
|
|
22
|
+
/** Known-public hostname suffixes never treated as official even if they embed an official host. */
|
|
23
|
+
const NON_OFFICIAL_SUFFIXES = ['.local', '.localhost', '.internal']
|
|
24
|
+
const OFFICIAL_LOCAL_HOSTS = ['localhost', '127.0.0.1', '[::1]']
|
|
25
|
+
|
|
26
|
+
export type AgentSimWebPlatform = 'web' | 'desktop'
|
|
27
|
+
|
|
28
|
+
export type AgentSimEngineManifest = {
|
|
29
|
+
/**
|
|
30
|
+
* Same-origin ES module URL of the engine entry. The entry must register
|
|
31
|
+
* the `window.__adeaAgentSim` mount API documented in the package README
|
|
32
|
+
* before its load event resolves.
|
|
33
|
+
*/
|
|
34
|
+
entryUrl: string
|
|
35
|
+
/** Engine pack version (agent-sim repo release). Diagnostics only. */
|
|
36
|
+
version: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Well-known same-origin URL the official pack lane writes the manifest to. */
|
|
40
|
+
export function agentSimEngineManifestUrl(origin: string): string {
|
|
41
|
+
return new URL('/assets/agent-sim/engine.json', origin).toString()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse and validate an engine manifest fetched from a trusted origin.
|
|
46
|
+
* Rejects payloads that would point the loader off the deployment's own
|
|
47
|
+
* origin — the engine ships with the site, never from third parties.
|
|
48
|
+
*/
|
|
49
|
+
export function parseAgentSimEngineManifest(
|
|
50
|
+
value: unknown,
|
|
51
|
+
origin: string
|
|
52
|
+
): { ok: true; manifest: AgentSimEngineManifest } | { ok: false } {
|
|
53
|
+
if (typeof value !== 'object' || value === null) return { ok: false }
|
|
54
|
+
const entryUrl = (value as { engine?: { entryUrl?: unknown } }).engine?.entryUrl
|
|
55
|
+
const version = (value as { engine?: { version?: unknown } }).engine?.version
|
|
56
|
+
if (typeof entryUrl !== 'string' || entryUrl.length === 0) return { ok: false }
|
|
57
|
+
if (typeof version !== 'string' || version.length === 0) return { ok: false }
|
|
58
|
+
let resolved: URL
|
|
59
|
+
try {
|
|
60
|
+
resolved = new URL(entryUrl, origin)
|
|
61
|
+
} catch {
|
|
62
|
+
return { ok: false }
|
|
63
|
+
}
|
|
64
|
+
if (resolved.origin !== new URL(origin).origin) return { ok: false }
|
|
65
|
+
if (!resolved.pathname.endsWith('.js')) return { ok: false }
|
|
66
|
+
return { ok: true, manifest: { entryUrl: resolved.toString(), version } }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether a web origin is an official Adea deployment allowed to resolve the
|
|
71
|
+
* Agent Sim engine remote. Desktop builds skip this check: they are gated by
|
|
72
|
+
* the packed engine manifest instead.
|
|
73
|
+
*/
|
|
74
|
+
export function isOfficialAgentSimWebOrigin(origin: string): boolean {
|
|
75
|
+
let url: URL
|
|
76
|
+
try {
|
|
77
|
+
url = new URL(origin)
|
|
78
|
+
} catch {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
if (url.protocol !== 'https:') return false
|
|
82
|
+
const host = url.hostname.toLowerCase()
|
|
83
|
+
if (NON_OFFICIAL_SUFFIXES.some((suffix) => host.endsWith(suffix))) return false
|
|
84
|
+
return OFFICIAL_AGENT_SIM_WEB_HOSTS.some(
|
|
85
|
+
(official) => host === official || host.endsWith(`.${official}`)
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Hosts where a developer runs the shell locally. Local development is
|
|
91
|
+
* entitled only when the engine was explicitly packed into the local build
|
|
92
|
+
* (engine manifest present); the host check alone never entitles it.
|
|
93
|
+
*/
|
|
94
|
+
export function isLocalDevHost(hostname: string): boolean {
|
|
95
|
+
const host = hostname.toLowerCase()
|
|
96
|
+
return OFFICIAL_LOCAL_HOSTS.includes(host) || host.endsWith('.localhost')
|
|
97
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export * from './asset-manifests'
|
|
2
|
+
export {
|
|
3
|
+
agentSimEngineManifestUrl,
|
|
4
|
+
isLocalDevHost,
|
|
5
|
+
isOfficialAgentSimWebOrigin,
|
|
6
|
+
parseAgentSimEngineManifest,
|
|
7
|
+
OFFICIAL_AGENT_SIM_WEB_HOSTS,
|
|
8
|
+
type AgentSimEngineManifest,
|
|
9
|
+
} from './engine'
|
|
10
|
+
export { hqHomeManifest, hqWorkManifest } from './manifests'
|
|
11
|
+
export {
|
|
12
|
+
appRouteHref,
|
|
13
|
+
encodeSceneStartPosition,
|
|
14
|
+
portalNavigationHref,
|
|
15
|
+
readSceneStartPosition,
|
|
16
|
+
type PortalNavigation,
|
|
17
|
+
type SceneApp,
|
|
18
|
+
} from './scene-spawn'
|
|
19
|
+
export {
|
|
20
|
+
createSceneTelemetryEnvelope,
|
|
21
|
+
MAX_SCENE_TELEMETRY_BYTES,
|
|
22
|
+
onRouterTransitionStart,
|
|
23
|
+
parseScenePerformanceReport,
|
|
24
|
+
recordNavigation,
|
|
25
|
+
SCENE_TELEMETRY_LOG_PREFIX,
|
|
26
|
+
type SceneNavigationType,
|
|
27
|
+
type ScenePerformanceReport,
|
|
28
|
+
type SceneRuntimeStats,
|
|
29
|
+
type SceneTelemetryEnvelope,
|
|
30
|
+
} from './telemetry'
|
|
31
|
+
export const configurableCharacterId = 'configurable'
|
|
32
|
+
export const DEFAULT_CHARACTER_ID = configurableCharacterId
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Shell-side character param check. The engine validates against its
|
|
36
|
+
* reference/preset catalogs on mount; the shell only needs a plausible
|
|
37
|
+
* non-empty value to thread through.
|
|
38
|
+
*/
|
|
39
|
+
export function isPlausibleCharacterId(value: string | undefined): value is string {
|
|
40
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 128
|
|
41
|
+
}
|
package/src/manifests.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SceneManifest } from './asset-manifests'
|
|
2
|
+
|
|
3
|
+
// Scene ids mirror @adea-ai/app-core's HqSceneId, which remains the canonical
|
|
4
|
+
// shell-side scene identity. This package only carries mount constants.
|
|
5
|
+
type HqSceneId = 'home' | 'work'
|
|
6
|
+
|
|
7
|
+
type HqSceneConfig = {
|
|
8
|
+
label: string
|
|
9
|
+
floorVersion: number
|
|
10
|
+
foliageVersion: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sceneConfig = {
|
|
14
|
+
home: { label: 'Home', floorVersion: 20, foliageVersion: 10 },
|
|
15
|
+
work: { label: 'Work', floorVersion: 21, foliageVersion: 11 },
|
|
16
|
+
} as const satisfies Record<HqSceneId, HqSceneConfig>
|
|
17
|
+
|
|
18
|
+
// Start height mirrors the engine's ROOM_GALLERY_FOUNDATION_TOP_Y (slab base 0
|
|
19
|
+
// + height 6) plus the authored 135-unit lift. The engine's room-config is
|
|
20
|
+
// the simulation source of truth; this package carries the mount constants
|
|
21
|
+
// the shell needs without depending on engine code.
|
|
22
|
+
const START_Y = 6 + 135
|
|
23
|
+
|
|
24
|
+
function createHqManifest(scene: HqSceneId): SceneManifest {
|
|
25
|
+
const { label, floorVersion, foliageVersion } = sceneConfig[scene]
|
|
26
|
+
const sceneId = `hq-${scene}`
|
|
27
|
+
return {
|
|
28
|
+
id: sceneId,
|
|
29
|
+
label,
|
|
30
|
+
availability: 'ready',
|
|
31
|
+
entryAssetUrl: `/assets/worlds/${sceneId}/floor.glb?v=${floorVersion}`,
|
|
32
|
+
collisionAssetUrl: `/assets/worlds/${sceneId}/floor-collision.glb?v=${floorVersion}`,
|
|
33
|
+
foliageManifestUrl: `/assets/worlds/${sceneId}/foliage.json?v=${foliageVersion}`,
|
|
34
|
+
assignedPropsManifestUrl: `/assets/worlds/${sceneId}/props-runtime.json?v=room-layout`,
|
|
35
|
+
zones: [],
|
|
36
|
+
startPosition: { x: 0, y: START_Y, z: 0, yaw: 0, pitch: -0.2 },
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const hqHomeManifest = createHqManifest('home')
|
|
41
|
+
export const hqWorkManifest = createHqManifest('work')
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { SceneStartPosition } from './asset-manifests'
|
|
2
|
+
|
|
3
|
+
export type SceneApp = 'world' | 'hq'
|
|
4
|
+
|
|
5
|
+
const SPAWN_QUERY_KEY = 'spawn'
|
|
6
|
+
const APP_DEV_PORTS: Record<SceneApp, string> = { world: '3000', hq: '3004' }
|
|
7
|
+
const APP_PORTLESS_URLS: Record<SceneApp, string> = {
|
|
8
|
+
world: 'https://world.localhost',
|
|
9
|
+
hq: 'https://adea.localhost',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function firstValue(value: string | string[] | undefined): string | undefined {
|
|
13
|
+
return Array.isArray(value) ? value[0] : value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function finiteNumber(value: string | undefined): number | undefined {
|
|
17
|
+
if (!value) return undefined
|
|
18
|
+
const parsed = Number(value)
|
|
19
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Encode a scene start position for a cross-app navigation URL. */
|
|
23
|
+
export function encodeSceneStartPosition(position: SceneStartPosition): string {
|
|
24
|
+
return [
|
|
25
|
+
position.x,
|
|
26
|
+
position.y,
|
|
27
|
+
position.z,
|
|
28
|
+
position.yaw ?? '',
|
|
29
|
+
position.pitch ?? '',
|
|
30
|
+
position.snapToGround == null ? '' : position.snapToGround ? '1' : '0',
|
|
31
|
+
].join(',')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Parse a validated scene start position from a route's `spawn` query. */
|
|
35
|
+
export function readSceneStartPosition(
|
|
36
|
+
value: string | string[] | undefined
|
|
37
|
+
): SceneStartPosition | undefined {
|
|
38
|
+
const parts = firstValue(value)?.split(',')
|
|
39
|
+
if (!parts || parts.length < 3 || parts.length > 6) return undefined
|
|
40
|
+
const x = finiteNumber(parts[0])
|
|
41
|
+
const y = finiteNumber(parts[1])
|
|
42
|
+
const z = finiteNumber(parts[2])
|
|
43
|
+
if (x == null || y == null || z == null) return undefined
|
|
44
|
+
const yaw = finiteNumber(parts[3])
|
|
45
|
+
const pitch = finiteNumber(parts[4])
|
|
46
|
+
const snap = parts[5]
|
|
47
|
+
if (parts[3] && yaw == null) return undefined
|
|
48
|
+
if (parts[4] && pitch == null) return undefined
|
|
49
|
+
if (snap && snap !== '0' && snap !== '1') return undefined
|
|
50
|
+
return {
|
|
51
|
+
x,
|
|
52
|
+
y,
|
|
53
|
+
z,
|
|
54
|
+
...(yaw == null ? {} : { yaw }),
|
|
55
|
+
...(pitch == null ? {} : { pitch }),
|
|
56
|
+
...(snap === '0' ? { snapToGround: false } : snap === '1' ? { snapToGround: true } : {}),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function configuredAppBase(app: SceneApp): string | undefined {
|
|
61
|
+
const configured =
|
|
62
|
+
app === 'world' ? process.env.ADEA_PUBLIC_WORLD_URL : process.env.ADEA_PUBLIC_HQ_URL
|
|
63
|
+
return configured?.trim() || undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isDevelopmentHost(hostname: string): boolean {
|
|
67
|
+
return (
|
|
68
|
+
hostname === 'localhost' ||
|
|
69
|
+
hostname === '127.0.0.1' ||
|
|
70
|
+
hostname === '0.0.0.0' ||
|
|
71
|
+
hostname === 'amf-mb-pro' ||
|
|
72
|
+
hostname.endsWith('.localhost') ||
|
|
73
|
+
hostname.endsWith('.ts.net')
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Resolve a route for the sibling World or HQ app in dev and production. */
|
|
78
|
+
export function appRouteHref(app: SceneApp, path: string, currentHref?: string): string {
|
|
79
|
+
const current = new URL(currentHref ?? window.location.href)
|
|
80
|
+
const configuredBase = configuredAppBase(app)
|
|
81
|
+
const isPortlessHost = current.hostname.endsWith('.localhost')
|
|
82
|
+
const target = new URL(
|
|
83
|
+
path,
|
|
84
|
+
configuredBase ?? (isPortlessHost ? APP_PORTLESS_URLS[app] : current.origin)
|
|
85
|
+
)
|
|
86
|
+
if (!configuredBase && !isPortlessHost && isDevelopmentHost(current.hostname)) {
|
|
87
|
+
target.port = APP_DEV_PORTS[app]
|
|
88
|
+
}
|
|
89
|
+
return target.toString()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type PortalNavigation = {
|
|
93
|
+
app: SceneApp
|
|
94
|
+
path: string
|
|
95
|
+
spawn?: SceneStartPosition
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Build a portal URL while carrying the selected character and target spawn. */
|
|
99
|
+
export function portalNavigationHref(navigation: PortalNavigation, currentHref?: string): string {
|
|
100
|
+
const current = new URL(currentHref ?? window.location.href)
|
|
101
|
+
const target = new URL(appRouteHref(navigation.app, navigation.path, current.toString()))
|
|
102
|
+
const character = current.searchParams.get('character')
|
|
103
|
+
if (character) target.searchParams.set('character', character)
|
|
104
|
+
if (navigation.spawn)
|
|
105
|
+
target.searchParams.set(SPAWN_QUERY_KEY, encodeSceneStartPosition(navigation.spawn))
|
|
106
|
+
return target.toString()
|
|
107
|
+
}
|