@world-engines/spatial-authoring 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ import { type PlaneGenerationV1 } from "./plane-generation.js";
2
+ /** 空间作者源的唯一内存模型;持久化由宿主的 protobuf/WESP adapter 负责。 */
3
+ export declare const SPATIAL_WORLD_SOURCE_VERSION: 1;
4
+ export type SpatialTopologyKind = "grid" | "freeform";
5
+ export type SpatialObjectKind = "location" | "portal" | "region";
6
+ export interface SpatialTopologyV1 {
7
+ readonly kind: SpatialTopologyKind;
8
+ readonly width: number;
9
+ readonly height: number;
10
+ readonly cellSize?: number;
11
+ /** 可重放生成参数;高度/纹理缓存不进入作者源。 */
12
+ readonly generation?: PlaneGenerationV1;
13
+ }
14
+ /** 外部作者域对空间 id 的引用;删除前必须由宿主消费影响列表。 */
15
+ export interface SpatialReferenceV1 {
16
+ readonly ownerId: string;
17
+ readonly ownerKind: "ladybug" | "trigger" | "scenario";
18
+ readonly fieldPath: string;
19
+ }
20
+ export interface SpatialObjectV1 {
21
+ readonly id: string;
22
+ readonly key: string;
23
+ readonly kind: SpatialObjectKind;
24
+ readonly label: string;
25
+ readonly x: number;
26
+ readonly y: number;
27
+ readonly references: readonly SpatialReferenceV1[];
28
+ }
29
+ export interface SpatialWorldSourceV1 {
30
+ readonly schemaVersion: typeof SPATIAL_WORLD_SOURCE_VERSION;
31
+ readonly worldId: string;
32
+ /** 由 source authority 递增;worker 的结果必须回带此 revision。 */
33
+ readonly revision: number;
34
+ readonly topology: SpatialTopologyV1;
35
+ readonly objects: readonly SpatialObjectV1[];
36
+ }
37
+ export interface SpatialValidationIssue {
38
+ readonly path: string;
39
+ readonly message: string;
40
+ }
41
+ export declare class SpatialWorldValidationError extends Error {
42
+ readonly issues: readonly SpatialValidationIssue[];
43
+ constructor(issues: readonly SpatialValidationIssue[]);
44
+ }
45
+ export declare function stableSpatialId(kind: SpatialObjectKind, key: string): string;
46
+ /** 验证并返回同一对象,不复制或序列化 source,避免制造 JSON 第二 authority。 */
47
+ export declare function validateSpatialWorldSourceV1(source: SpatialWorldSourceV1): SpatialWorldSourceV1;
package/dist/schema.js ADDED
@@ -0,0 +1,82 @@
1
+ import { validatePlaneGeneration } from "./plane-generation.js";
2
+ /** 空间作者源的唯一内存模型;持久化由宿主的 protobuf/WESP adapter 负责。 */
3
+ export const SPATIAL_WORLD_SOURCE_VERSION = 1;
4
+ export class SpatialWorldValidationError extends Error {
5
+ issues;
6
+ constructor(issues) {
7
+ super(`SpatialWorldSourceV1 无效:${issues.map((issue) => `${issue.path} ${issue.message}`).join(";")}`);
8
+ this.name = "SpatialWorldValidationError";
9
+ this.issues = issues;
10
+ }
11
+ }
12
+ const KEY_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
13
+ export function stableSpatialId(kind, key) {
14
+ return `spatial:${kind}:${key}`;
15
+ }
16
+ function isPositiveFinite(value) {
17
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
18
+ }
19
+ function isFiniteCoordinate(value) {
20
+ return typeof value === "number" && Number.isFinite(value);
21
+ }
22
+ /** 验证并返回同一对象,不复制或序列化 source,避免制造 JSON 第二 authority。 */
23
+ export function validateSpatialWorldSourceV1(source) {
24
+ if (source.topology.generation !== undefined) {
25
+ const generation = validatePlaneGeneration(source.topology.generation);
26
+ if (generation.widthMeters !== source.topology.width || generation.heightMeters !== source.topology.height) {
27
+ throw new SpatialWorldValidationError([{ path: "topology.generation", message: "生成范围必须与空间范围一致" }]);
28
+ }
29
+ }
30
+ const issues = [];
31
+ if (source.schemaVersion !== SPATIAL_WORLD_SOURCE_VERSION) {
32
+ issues.push({ path: "schemaVersion", message: "必须为 1" });
33
+ }
34
+ if (!KEY_PATTERN.test(source.worldId))
35
+ issues.push({ path: "worldId", message: "必须是稳定的 kebab-case key" });
36
+ if (!Number.isSafeInteger(source.revision) || source.revision < 0) {
37
+ issues.push({ path: "revision", message: "必须是非负安全整数" });
38
+ }
39
+ if (!isPositiveFinite(source.topology.width) || !isPositiveFinite(source.topology.height)) {
40
+ issues.push({ path: "topology", message: "width 和 height 必须为正数" });
41
+ }
42
+ if (source.topology.kind === "grid" && !isPositiveFinite(source.topology.cellSize)) {
43
+ issues.push({ path: "topology.cellSize", message: "grid 必须有正 cellSize" });
44
+ }
45
+ if (source.topology.kind !== "grid" && source.topology.kind !== "freeform") {
46
+ issues.push({ path: "topology.kind", message: "不支持的 topology kind" });
47
+ }
48
+ const ids = new Set();
49
+ const keys = new Set();
50
+ source.objects.forEach((object, index) => {
51
+ const path = `objects[${index}]`;
52
+ if (!KEY_PATTERN.test(object.key))
53
+ issues.push({ path: `${path}.key`, message: "必须是稳定的 kebab-case key" });
54
+ const expectedId = stableSpatialId(object.kind, object.key);
55
+ if (object.id !== expectedId)
56
+ issues.push({ path: `${path}.id`, message: `必须等于 ${expectedId}` });
57
+ if (ids.has(object.id))
58
+ issues.push({ path: `${path}.id`, message: "不能重复" });
59
+ ids.add(object.id);
60
+ if (keys.has(`${object.kind}:${object.key}`))
61
+ issues.push({ path: `${path}.key`, message: "同 kind 下不能重复" });
62
+ keys.add(`${object.kind}:${object.key}`);
63
+ if (object.kind !== "location" && object.kind !== "portal" && object.kind !== "region") {
64
+ issues.push({ path: `${path}.kind`, message: "不支持的 spatial object kind" });
65
+ }
66
+ if (object.label.trim().length === 0)
67
+ issues.push({ path: `${path}.label`, message: "不能为空" });
68
+ if (!isFiniteCoordinate(object.x) || !isFiniteCoordinate(object.y))
69
+ issues.push({ path, message: "坐标必须为有限数" });
70
+ if (object.x < 0 || object.x > source.topology.width || object.y < 0 || object.y > source.topology.height) {
71
+ issues.push({ path, message: "坐标必须位于 topology 边界内" });
72
+ }
73
+ object.references.forEach((reference, referenceIndex) => {
74
+ if (reference.ownerId.trim().length === 0 || reference.fieldPath.trim().length === 0) {
75
+ issues.push({ path: `${path}.references[${referenceIndex}]`, message: "引用必须有 ownerId 和 fieldPath" });
76
+ }
77
+ });
78
+ });
79
+ if (issues.length > 0)
80
+ throw new SpatialWorldValidationError(issues);
81
+ return source;
82
+ }
@@ -0,0 +1,15 @@
1
+ import type { SpatialObjectV1, SpatialReferenceV1, SpatialWorldSourceV1 } from "./schema.js";
2
+ export interface SpatialReferenceImpact {
3
+ readonly spatialId: string;
4
+ readonly reference: SpatialReferenceV1;
5
+ }
6
+ export interface SpatialRemovalPlan {
7
+ readonly target: SpatialObjectV1;
8
+ readonly impacts: readonly SpatialReferenceImpact[];
9
+ readonly requiresConfirmation: boolean;
10
+ readonly sourceRevision: number;
11
+ }
12
+ export declare function collectSpatialReferenceImpacts(source: SpatialWorldSourceV1, spatialId: string): readonly SpatialReferenceImpact[];
13
+ /** 删除只生成计划;实际 source mutation 必须由宿主在确认后完成。 */
14
+ export declare function planSpatialObjectRemoval(source: SpatialWorldSourceV1, spatialId: string): SpatialRemovalPlan;
15
+ export declare function assertSpatialRemovalConfirmation(plan: SpatialRemovalPlan, confirmed: boolean, sourceRevision: number): void;
@@ -0,0 +1,23 @@
1
+ import { validateSpatialWorldSourceV1 } from "./schema.js";
2
+ export function collectSpatialReferenceImpacts(source, spatialId) {
3
+ const validSource = validateSpatialWorldSourceV1(source);
4
+ const target = validSource.objects.find((object) => object.id === spatialId);
5
+ if (target === undefined)
6
+ return [];
7
+ return target.references.map((reference) => ({ spatialId: target.id, reference }));
8
+ }
9
+ /** 删除只生成计划;实际 source mutation 必须由宿主在确认后完成。 */
10
+ export function planSpatialObjectRemoval(source, spatialId) {
11
+ const validSource = validateSpatialWorldSourceV1(source);
12
+ const target = validSource.objects.find((object) => object.id === spatialId);
13
+ if (target === undefined)
14
+ throw new RangeError(`不存在空间对象 ${spatialId}`);
15
+ const impacts = collectSpatialReferenceImpacts(validSource, spatialId);
16
+ return { target, impacts, requiresConfirmation: impacts.length > 0, sourceRevision: validSource.revision };
17
+ }
18
+ export function assertSpatialRemovalConfirmation(plan, confirmed, sourceRevision) {
19
+ if (plan.sourceRevision !== sourceRevision)
20
+ throw new Error("source revision 已变化,必须重新计算引用影响");
21
+ if (plan.requiresConfirmation && !confirmed)
22
+ throw new Error("存在空间引用影响,必须显式确认删除");
23
+ }
@@ -0,0 +1,210 @@
1
+ import { type Bounds2D, type InteriorDoor, type InteriorRoom, type ProceduralBuilding, type ProceduralParcel, type ProceduralRoad, type ProceduralWorldV1 } from "./procedural-city.js";
2
+ import { type RoadParcelBoundary, type RoadParcelTerrainGrid } from "./road-parcels.js";
3
+ import { type RoadGenerationV1 } from "./road-generation.js";
4
+ export declare const SPATIAL_DELTA_VERSION: 1;
5
+ export declare const SPATIAL_RUNTIME_CACHE_LIMITS: Readonly<{
6
+ cityLayouts: 8;
7
+ activatedParcels: 128;
8
+ buildingInteriors: 64;
9
+ }>;
10
+ export declare const SPATIAL_DELTA_QUOTAS: Readonly<{
11
+ maxOverrides: 2048;
12
+ maxTombstones: 2048;
13
+ maxUtf8Bytes: 1048576;
14
+ }>;
15
+ export declare const BUILDING_ENTRANCE_TOLERANCE_METERS = 1.5;
16
+ export type SpatialTargetType = "city" | "road" | "parcel" | "building" | "room" | "door";
17
+ export type SpatialOverrideField = "label" | "blocked" | "destroyed";
18
+ export type SpatialOverrideValue = string | boolean;
19
+ export type SpatialTargetLocatorV1 = {
20
+ readonly cityId: string;
21
+ } | {
22
+ readonly cityId: string;
23
+ readonly parcelId: string;
24
+ } | {
25
+ readonly cityId: string;
26
+ readonly parcelId: string;
27
+ readonly buildingId: string;
28
+ };
29
+ export interface SpatialRuntimeBinding {
30
+ readonly artifactDigest: string;
31
+ readonly baseDigest: string;
32
+ readonly worldlineId: string;
33
+ readonly controlledActorId: string;
34
+ }
35
+ export interface SpatialDeltaBindingV1 {
36
+ readonly artifactDigest: string;
37
+ readonly baseDigest: string;
38
+ readonly worldlineId: string;
39
+ }
40
+ export interface ControlledSpatialPose {
41
+ readonly actorId: string;
42
+ readonly x: number;
43
+ readonly y: number;
44
+ readonly revision: string | number;
45
+ }
46
+ export interface SpatialDeltaOverrideV1 {
47
+ readonly targetType: SpatialTargetType;
48
+ readonly targetId: string;
49
+ readonly locator: SpatialTargetLocatorV1;
50
+ readonly field: SpatialOverrideField;
51
+ readonly value: SpatialOverrideValue;
52
+ }
53
+ export interface SpatialTombstoneV1 {
54
+ readonly targetType: SpatialTargetType;
55
+ readonly targetId: string;
56
+ readonly locator: SpatialTargetLocatorV1;
57
+ }
58
+ export interface SpatialDeltaV1 {
59
+ readonly version: typeof SPATIAL_DELTA_VERSION;
60
+ readonly binding: SpatialDeltaBindingV1;
61
+ readonly overrides: readonly SpatialDeltaOverrideV1[];
62
+ readonly tombstones: readonly SpatialTombstoneV1[];
63
+ }
64
+ export interface SpatialRuntimeAttributes {
65
+ readonly label?: string;
66
+ readonly blocked?: boolean;
67
+ readonly destroyed?: boolean;
68
+ }
69
+ export interface TerrainRoadPointV1 {
70
+ readonly x: number;
71
+ readonly y: number;
72
+ readonly z: number;
73
+ }
74
+ export interface TerrainRoadSegmentV1 {
75
+ readonly kind: "surface" | "bridge" | "tunnel";
76
+ readonly points: readonly TerrainRoadPointV1[];
77
+ readonly lengthMeters: number;
78
+ }
79
+ /** terrain-roads-v1 的瞬态 WASM route cache;它不得进入 source sidecar 或 SAV。 */
80
+ export interface TerrainRoadV1 {
81
+ readonly id: string;
82
+ readonly kind: "intercity" | "arterial" | "local";
83
+ readonly roadClass: "street" | "motorway";
84
+ readonly cityId?: string;
85
+ readonly fromCityId?: string;
86
+ readonly toCityId?: string;
87
+ readonly widthMeters: number;
88
+ readonly points: readonly TerrainRoadPointV1[];
89
+ readonly segments: readonly TerrainRoadSegmentV1[];
90
+ }
91
+ export interface TerrainRoadNetworkV1 {
92
+ readonly profile: "terrain-roads-v1";
93
+ readonly roads: readonly TerrainRoadV1[];
94
+ readonly unreachable: readonly Readonly<Record<string, unknown>>[];
95
+ readonly cityLayouts: readonly TerrainRoadCityLayoutV1[];
96
+ }
97
+ export interface TerrainRoadCityLayoutV1 {
98
+ readonly cityId: string;
99
+ readonly roads: readonly string[];
100
+ /** core 以实际 sampler 重建的城市可建边界;unbuildable 绝不回退为圆形。 */
101
+ readonly boundary: TerrainRoadBoundaryV1;
102
+ /** 给地块生成器的 city-bbox route cache;不进入 source 或 SAV。 */
103
+ readonly terrain: RoadParcelTerrainGrid;
104
+ }
105
+ /** wire cache 额外带 cityId,使 boundary 不能被错绑到另一个 layout。 */
106
+ export interface TerrainRoadBoundaryV1 extends RoadParcelBoundary {
107
+ readonly cityId: string;
108
+ }
109
+ type RuntimeRoadBase = ProceduralRoad | TerrainRoadV1;
110
+ export type RuntimeRoad = Readonly<RuntimeRoadBase & SpatialRuntimeAttributes>;
111
+ export type RuntimeParcel = Readonly<ProceduralParcel & SpatialRuntimeAttributes>;
112
+ export type RuntimeBuilding = Readonly<ProceduralBuilding & SpatialRuntimeAttributes>;
113
+ export type RuntimeRoom = Readonly<InteriorRoom & SpatialRuntimeAttributes>;
114
+ export type RuntimeDoor = Readonly<InteriorDoor & SpatialRuntimeAttributes>;
115
+ export interface SpatialViewportRequest {
116
+ readonly bounds: Bounds2D;
117
+ readonly metersPerCssPixel: number;
118
+ }
119
+ export interface SpatialViewport {
120
+ readonly roads: readonly RuntimeRoad[];
121
+ readonly parcels: readonly RuntimeParcel[];
122
+ /** 仅返回已由真实进入 gate 物化且仍在 cache 中的建筑。 */
123
+ readonly buildings: readonly RuntimeBuilding[];
124
+ }
125
+ /**
126
+ * 视口请求的 canonical transport 形状。相机缩放是 projection 信息;
127
+ * chunk identity 只由已冻结 base/revision 与世界坐标决定。
128
+ */
129
+ export interface SpatialViewportChunkRequest {
130
+ readonly baseDigest: string;
131
+ readonly spatialRevision: string | number;
132
+ readonly centerMeters: Readonly<{
133
+ x: number;
134
+ y: number;
135
+ }>;
136
+ readonly metersPerCssPixel: number;
137
+ readonly viewportSize: Readonly<{
138
+ width: number;
139
+ height: number;
140
+ }>;
141
+ readonly chunk: Readonly<{
142
+ level: number;
143
+ x: number;
144
+ y: number;
145
+ }>;
146
+ }
147
+ export interface SpatialViewportChunkCache<T> {
148
+ read(request: SpatialViewportChunkRequest, signal?: AbortSignal): Promise<T>;
149
+ clear(): void;
150
+ retainRevision(baseDigest: string, spatialRevision: string | number): void;
151
+ getCacheSize(): number;
152
+ }
153
+ export interface ParcelEntry {
154
+ readonly pose: ControlledSpatialPose;
155
+ readonly parcel: RuntimeParcel;
156
+ readonly buildings: readonly RuntimeBuilding[];
157
+ }
158
+ export interface RuntimeBuildingInterior {
159
+ readonly buildingId: string;
160
+ readonly rooms: readonly RuntimeRoom[];
161
+ readonly doors: readonly RuntimeDoor[];
162
+ }
163
+ export interface BuildingEntry {
164
+ readonly pose: ControlledSpatialPose;
165
+ readonly building: RuntimeBuilding;
166
+ readonly interior: RuntimeBuildingInterior;
167
+ }
168
+ export interface SpatialRuntimeCacheStats {
169
+ readonly cityLayouts: number;
170
+ readonly activatedParcels: number;
171
+ readonly materializedBuildings: number;
172
+ readonly buildingInteriors: number;
173
+ }
174
+ export type SpatialRuntimeErrorCode = "HOST_POSE_UNAVAILABLE" | "HOST_POSE_ACTOR_MISMATCH" | "HOST_POSE_INVALID" | "POSE_OUTSIDE_PARCEL" | "POSE_OUTSIDE_BUILDING" | "TARGET_NOT_FOUND" | "PARENT_PARCEL_NOT_ACTIVATED" | "TARGET_UNAVAILABLE" | "DELTA_BINDING_MISMATCH" | "DELTA_INVALID" | "DELTA_QUOTA_EXCEEDED";
175
+ export declare class SpatialRuntimeError extends Error {
176
+ readonly code: SpatialRuntimeErrorCode;
177
+ constructor(code: SpatialRuntimeErrorCode, message: string);
178
+ }
179
+ export interface SpatialRuntimeOptions {
180
+ readonly world: ProceduralWorldV1;
181
+ /** 仅在 sidecar 显式 roadGeneration 时由 Player 预加载并传入。 */
182
+ readonly roadNetwork?: TerrainRoadNetworkV1;
183
+ /** 新道路地块的坡度门必须消费与 route cache 相同的发布配置。 */
184
+ readonly roadGeneration?: RoadGenerationV1;
185
+ readonly binding: SpatialRuntimeBinding;
186
+ readonly readControlledPose: () => ControlledSpatialPose | null | undefined;
187
+ }
188
+ export interface SpatialRuntime {
189
+ readonly world: ProceduralWorldV1;
190
+ readonly binding: SpatialRuntimeBinding;
191
+ readViewport(request: SpatialViewportRequest): SpatialViewport;
192
+ enterCurrentParcel(): ParcelEntry;
193
+ enterCurrentBuilding(buildingId: string): BuildingEntry;
194
+ clearCache(): void;
195
+ getCacheStats(): SpatialRuntimeCacheStats;
196
+ setOverride(targetType: SpatialTargetType, targetId: string, field: SpatialOverrideField, value: SpatialOverrideValue): void;
197
+ applyOverride(targetType: SpatialTargetType, targetId: string, patch: Readonly<Partial<Record<SpatialOverrideField, SpatialOverrideValue>>>): void;
198
+ setTombstone(targetType: SpatialTargetType, targetId: string, tombstoned?: boolean): void;
199
+ serializeDelta(): SpatialDeltaV1;
200
+ serializeDeltaUtf8(): Uint8Array;
201
+ restoreDelta(input: unknown): void;
202
+ applyDelta(input: unknown): void;
203
+ }
204
+ /**
205
+ * scope-bound、可取消的 chunk cache。它不生成或持久化几何;调用者只能从同一个
206
+ * 已确认 base binding 请求 tile,取消或旧 revision 的响应绝不会成为新 revision cache。
207
+ */
208
+ export declare function createSpatialViewportChunkCache<T>(load: (request: SpatialViewportChunkRequest, signal: AbortSignal) => Promise<T>): SpatialViewportChunkCache<T>;
209
+ export declare function createSpatialRuntime(options: SpatialRuntimeOptions): SpatialRuntime;
210
+ export {};