@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.
- package/LICENSE +46 -0
- package/dist/SpatialWorldCanvas.d.ts +18 -0
- package/dist/SpatialWorldCanvas.js +24 -0
- package/dist/SpatialWorldEditor.d.ts +38 -0
- package/dist/SpatialWorldEditor.js +61 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/plane-generation.d.ts +48 -0
- package/dist/plane-generation.js +126 -0
- package/dist/plane-generator/package.json +15 -0
- package/dist/plane-generator/plane-generator.d.ts +57 -0
- package/dist/plane-generator/plane-generator.js +310 -0
- package/dist/plane-generator/plane-generator_bg.wasm +0 -0
- package/dist/plane-generator/plane-generator_bg.wasm.d.ts +13 -0
- package/dist/procedural-city.d.ts +135 -0
- package/dist/procedural-city.js +595 -0
- package/dist/protobuf-source-codec.d.ts +57 -0
- package/dist/protobuf-source-codec.js +317 -0
- package/dist/road-generation.d.ts +52 -0
- package/dist/road-generation.js +129 -0
- package/dist/road-parcels.d.ts +83 -0
- package/dist/road-parcels.js +1584 -0
- package/dist/schema.d.ts +47 -0
- package/dist/schema.js +82 -0
- package/dist/source-adapter.d.ts +15 -0
- package/dist/source-adapter.js +23 -0
- package/dist/spatial-runtime.d.ts +210 -0
- package/dist/spatial-runtime.js +1231 -0
- package/dist/spatial-templates.d.ts +117 -0
- package/dist/spatial-templates.js +200 -0
- package/dist/worker.d.ts +13 -0
- package/dist/worker.js +9 -0
- package/package.json +155 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
WorldEngine 官方作者工具许可证
|
|
2
|
+
|
|
3
|
+
版权所有 (c) 2026 Nixdorfer。保留所有权利。
|
|
4
|
+
|
|
5
|
+
本仓库的源代码、构建材料及附带资源(统称“本作品”)不是开源软件,
|
|
6
|
+
不适用任何 OSI 认证的开源许可证。
|
|
7
|
+
|
|
8
|
+
一、官方作者工具分发许可
|
|
9
|
+
|
|
10
|
+
版权所有者可以通过其官方网站、npm registry、签名安装包或其他官方渠道,
|
|
11
|
+
复制并分发由本作品构建的 WorldEngine 作者工具及其必要运行资源
|
|
12
|
+
(统称“官方作者工具”)。仅版权所有者或其书面指定的发布者享有此分发权。
|
|
13
|
+
|
|
14
|
+
二、作者用户许可
|
|
15
|
+
|
|
16
|
+
从官方渠道取得官方作者工具的作者用户,可以:
|
|
17
|
+
|
|
18
|
+
1. 安装和运行官方作者工具;
|
|
19
|
+
2. 使用官方作者工具创作、编辑、预览、导入、导出和提交其有权处理的内容;
|
|
20
|
+
3. 为上述使用目的制作合理必要的本地备份副本。
|
|
21
|
+
|
|
22
|
+
三、未授予的权利
|
|
23
|
+
|
|
24
|
+
除第二条明确允许的行为外,本许可证不授予作者用户或其他第三方以下权利:
|
|
25
|
+
|
|
26
|
+
1. 修改、改编、反编译、反汇编或制作官方作者工具的派生作品;
|
|
27
|
+
2. 复制、镜像、转发、再上传、出售、出租、再分发或再许可官方作者工具;
|
|
28
|
+
3. 使用本作品的源代码、构建材料或任何部分开发、提供或训练其他产品或服务;
|
|
29
|
+
4. 删除或规避版权、许可、签名、访问控制或其他权利管理信息。
|
|
30
|
+
|
|
31
|
+
法律强制允许且合同不得排除的权利不受上述限制影响。
|
|
32
|
+
|
|
33
|
+
四、源代码查看
|
|
34
|
+
|
|
35
|
+
第三方可以在已获合法访问权限的范围内查看本作品,用于审查、安全研究或学习参考;
|
|
36
|
+
查看不授予运行、复制、修改、分发、再许可或用于其他产品与服务的权利。
|
|
37
|
+
|
|
38
|
+
五、无担保与责任限制
|
|
39
|
+
|
|
40
|
+
本作品与官方作者工具均按“现状”提供,不附带任何明示或暗示的担保。
|
|
41
|
+
在适用法律允许的最大范围内,版权所有者不对因访问或使用本作品或官方作者工具
|
|
42
|
+
造成的任何损失承担责任。
|
|
43
|
+
|
|
44
|
+
六、终止
|
|
45
|
+
|
|
46
|
+
违反本许可证将立即终止相应的访问与使用权;终止不影响版权所有者已经产生的权利和救济。
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import type { SpatialObjectV1, SpatialWorldSourceV1 } from "./schema.js";
|
|
3
|
+
export interface SpatialCanvasObject {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly x: number;
|
|
7
|
+
readonly y: number;
|
|
8
|
+
readonly selected: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface SpatialWorldCanvasProps {
|
|
11
|
+
readonly source: SpatialWorldSourceV1;
|
|
12
|
+
readonly selectedId?: string;
|
|
13
|
+
readonly onSelect?: (object: SpatialObjectV1) => void;
|
|
14
|
+
readonly className?: string;
|
|
15
|
+
}
|
|
16
|
+
/** 将唯一 source 映射为纯 SVG 图元;不产生 Three scene 或复制作者数据。 */
|
|
17
|
+
export declare function planSpatialCanvas(source: SpatialWorldSourceV1, selectedId?: string): readonly SpatialCanvasObject[];
|
|
18
|
+
export declare function SpatialWorldCanvas({ source, selectedId, onSelect, className }: SpatialWorldCanvasProps): JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { validateSpatialWorldSourceV1 } from "./schema.js";
|
|
3
|
+
/** 将唯一 source 映射为纯 SVG 图元;不产生 Three scene 或复制作者数据。 */
|
|
4
|
+
export function planSpatialCanvas(source, selectedId) {
|
|
5
|
+
const validSource = validateSpatialWorldSourceV1(source);
|
|
6
|
+
return validSource.objects.map((object) => ({
|
|
7
|
+
id: object.id,
|
|
8
|
+
label: object.label,
|
|
9
|
+
x: object.x,
|
|
10
|
+
y: object.y,
|
|
11
|
+
selected: object.id === selectedId,
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
export function SpatialWorldCanvas({ source, selectedId, onSelect, className }) {
|
|
15
|
+
const validSource = validateSpatialWorldSourceV1(source);
|
|
16
|
+
const select = (object) => onSelect?.(object);
|
|
17
|
+
const selectFromKeyboard = (event, object) => {
|
|
18
|
+
if (event.key !== "Enter" && event.key !== " ")
|
|
19
|
+
return;
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
select(object);
|
|
22
|
+
};
|
|
23
|
+
return (_jsxs("svg", { "aria-label": "\u7A7A\u95F4\u4E16\u754C\u753B\u5E03", className: className, "data-source-revision": validSource.revision, role: "group", viewBox: `0 0 ${validSource.topology.width} ${validSource.topology.height}`, children: [_jsx("rect", { fill: "transparent", height: validSource.topology.height, width: validSource.topology.width }), validSource.objects.map((object) => (_jsxs("g", { "aria-label": object.label, onClick: () => select(object), onKeyDown: (event) => selectFromKeyboard(event, object), role: onSelect === undefined ? undefined : "button", tabIndex: onSelect === undefined ? undefined : 0, transform: `translate(${object.x} ${object.y})`, children: [_jsx("circle", { fill: object.id === selectedId ? "#2563eb" : "#64748b", r: "4" }), _jsx("text", { dy: "-7", textAnchor: "middle", children: object.label })] }, object.id)))] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type JSX } from "react";
|
|
2
|
+
import type { SpatialObjectV1, SpatialWorldSourceV1 } from "./schema.js";
|
|
3
|
+
export interface SpatialObjectDraftV1 {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly kind: SpatialObjectV1["kind"];
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly x: number;
|
|
8
|
+
readonly y: number;
|
|
9
|
+
}
|
|
10
|
+
/** 仅是传给 source authority 的意图;组件绝不在本地拼装下一份 source。 */
|
|
11
|
+
export interface SpatialCreateRequest {
|
|
12
|
+
readonly sourceRevision: number;
|
|
13
|
+
readonly object: SpatialObjectDraftV1;
|
|
14
|
+
}
|
|
15
|
+
export interface SpatialUpdateRequest {
|
|
16
|
+
readonly sourceRevision: number;
|
|
17
|
+
readonly spatialId: string;
|
|
18
|
+
readonly patch: Pick<SpatialObjectDraftV1, "label" | "x" | "y">;
|
|
19
|
+
}
|
|
20
|
+
export interface SpatialRemoveRequest {
|
|
21
|
+
readonly sourceRevision: number;
|
|
22
|
+
readonly spatialId: string;
|
|
23
|
+
readonly confirmed: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface SpatialWorldEditorProps {
|
|
26
|
+
/** source 由宿主提供,是唯一可编辑 authority;本组件不持久化、不直接 mutation。 */
|
|
27
|
+
readonly source: SpatialWorldSourceV1;
|
|
28
|
+
/** 由宿主控制的选择;null 表示明确取消选择,undefined 则使用本组件局部状态。 */
|
|
29
|
+
readonly selectedId?: string | null;
|
|
30
|
+
readonly onSelectionChange?: (object: SpatialObjectV1 | undefined) => void;
|
|
31
|
+
readonly onCreate?: (request: SpatialCreateRequest) => void;
|
|
32
|
+
readonly onUpdate?: (request: SpatialUpdateRequest) => void;
|
|
33
|
+
readonly onRemove?: (request: SpatialRemoveRequest) => void;
|
|
34
|
+
/** 宿主 readback 处于围栏外时必须关闭所有 source mutation。 */
|
|
35
|
+
readonly disabled?: boolean;
|
|
36
|
+
readonly className?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function SpatialWorldEditor({ source, selectedId: controlledSelectedId, onSelectionChange, onCreate, onUpdate, onRemove, disabled, className }: SpatialWorldEditorProps): JSX.Element;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { SpatialWorldCanvas } from "./SpatialWorldCanvas.js";
|
|
4
|
+
import { validateSpatialWorldSourceV1 } from "./schema.js";
|
|
5
|
+
import { planSpatialObjectRemoval } from "./source-adapter.js";
|
|
6
|
+
export function SpatialWorldEditor({ source, selectedId: controlledSelectedId, onSelectionChange, onCreate, onUpdate, onRemove, disabled = false, className }) {
|
|
7
|
+
const validSource = validateSpatialWorldSourceV1(source);
|
|
8
|
+
const [uncontrolledSelectedId, setUncontrolledSelectedId] = useState(null);
|
|
9
|
+
const [newKey, setNewKey] = useState("");
|
|
10
|
+
const [newKind, setNewKind] = useState("location");
|
|
11
|
+
const [newLabel, setNewLabel] = useState("");
|
|
12
|
+
const [newX, setNewX] = useState("0");
|
|
13
|
+
const [newY, setNewY] = useState("0");
|
|
14
|
+
const [editLabel, setEditLabel] = useState("");
|
|
15
|
+
const [editX, setEditX] = useState("");
|
|
16
|
+
const [editY, setEditY] = useState("");
|
|
17
|
+
const [removalPlan, setRemovalPlan] = useState(null);
|
|
18
|
+
const selectedId = controlledSelectedId === undefined ? uncontrolledSelectedId : controlledSelectedId;
|
|
19
|
+
const selected = useMemo(() => selectedId === null ? undefined : validSource.objects.find((object) => object.id === selectedId), [selectedId, validSource.objects]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setEditLabel(selected?.label ?? "");
|
|
22
|
+
setEditX(selected === undefined ? "" : String(selected.x));
|
|
23
|
+
setEditY(selected === undefined ? "" : String(selected.y));
|
|
24
|
+
setRemovalPlan(null);
|
|
25
|
+
}, [selected]);
|
|
26
|
+
const select = (object) => {
|
|
27
|
+
if (controlledSelectedId === undefined)
|
|
28
|
+
setUncontrolledSelectedId(object.id);
|
|
29
|
+
onSelectionChange?.(object);
|
|
30
|
+
};
|
|
31
|
+
const createObject = (event) => {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
const x = Number(newX);
|
|
34
|
+
const y = Number(newY);
|
|
35
|
+
if (onCreate === undefined || newKey.trim().length === 0 || newLabel.trim().length === 0 || !Number.isFinite(x) || !Number.isFinite(y))
|
|
36
|
+
return;
|
|
37
|
+
onCreate({ sourceRevision: validSource.revision, object: { key: newKey.trim(), kind: newKind, label: newLabel.trim(), x, y } });
|
|
38
|
+
};
|
|
39
|
+
const updateObject = (event) => {
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
const x = Number(editX);
|
|
42
|
+
const y = Number(editY);
|
|
43
|
+
if (selected === undefined || onUpdate === undefined || editLabel.trim().length === 0 || !Number.isFinite(x) || !Number.isFinite(y))
|
|
44
|
+
return;
|
|
45
|
+
onUpdate({ sourceRevision: validSource.revision, spatialId: selected.id, patch: { label: editLabel.trim(), x, y } });
|
|
46
|
+
};
|
|
47
|
+
const requestRemoval = () => {
|
|
48
|
+
if (selected !== undefined)
|
|
49
|
+
setRemovalPlan(planSpatialObjectRemoval(validSource, selected.id));
|
|
50
|
+
};
|
|
51
|
+
const confirmRemoval = (event) => {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
if (removalPlan === null || onRemove === undefined)
|
|
54
|
+
return;
|
|
55
|
+
const confirmed = new FormData(event.currentTarget).get("confirm-removal") === "on";
|
|
56
|
+
if (removalPlan.requiresConfirmation && !confirmed)
|
|
57
|
+
return;
|
|
58
|
+
onRemove({ sourceRevision: removalPlan.sourceRevision, spatialId: removalPlan.target.id, confirmed });
|
|
59
|
+
};
|
|
60
|
+
return (_jsxs("section", { className: className, "data-spatial-world-id": validSource.worldId, "data-source-revision": validSource.revision, children: [_jsxs("header", { children: [_jsx("h2", { children: validSource.worldId }), _jsxs("p", { children: [validSource.objects.length, " \u4E2A\u7A7A\u95F4\u5BF9\u8C61\uFF0Crevision ", validSource.revision] })] }), _jsx(SpatialWorldCanvas, { onSelect: select, source: validSource, ...(selectedId === null ? {} : { selectedId }) }), _jsx("aside", { "aria-live": "polite", children: selected === undefined ? "请选择地图对象" : `${selected.label}(${selected.references.length} 个引用)` }), onCreate === undefined ? null : (_jsxs("form", { "aria-label": "\u521B\u5EFA\u7A7A\u95F4\u5BF9\u8C61", onSubmit: createObject, children: [_jsx("h3", { children: "\u521B\u5EFA\u7A7A\u95F4\u5BF9\u8C61" }), _jsx("input", { "aria-label": "\u65B0\u5BF9\u8C61 key", disabled: disabled, onChange: (event) => setNewKey(event.target.value), value: newKey }), _jsxs("select", { "aria-label": "\u65B0\u5BF9\u8C61\u7C7B\u578B", disabled: disabled, onChange: (event) => setNewKind(event.target.value), value: newKind, children: [_jsx("option", { value: "location", children: "location" }), _jsx("option", { value: "portal", children: "portal" }), _jsx("option", { value: "region", children: "region" })] }), _jsx("input", { "aria-label": "\u65B0\u5BF9\u8C61\u6807\u7B7E", disabled: disabled, onChange: (event) => setNewLabel(event.target.value), value: newLabel }), _jsx("input", { "aria-label": "\u65B0\u5BF9\u8C61 X", disabled: disabled, inputMode: "decimal", onChange: (event) => setNewX(event.target.value), value: newX }), _jsx("input", { "aria-label": "\u65B0\u5BF9\u8C61 Y", disabled: disabled, inputMode: "decimal", onChange: (event) => setNewY(event.target.value), value: newY }), _jsx("button", { disabled: disabled || newKey.trim().length === 0 || newLabel.trim().length === 0, type: "submit", children: "\u521B\u5EFA\u5BF9\u8C61" })] })), selected === undefined || onUpdate === undefined ? null : (_jsxs("form", { "aria-label": "\u7F16\u8F91\u7A7A\u95F4\u5BF9\u8C61", onSubmit: updateObject, children: [_jsxs("h3", { children: ["\u7F16\u8F91 ", selected.id] }), _jsx("input", { "aria-label": "\u5BF9\u8C61\u6807\u7B7E", disabled: disabled, onChange: (event) => setEditLabel(event.target.value), value: editLabel }), _jsx("input", { "aria-label": "\u5BF9\u8C61 X", disabled: disabled, inputMode: "decimal", onChange: (event) => setEditX(event.target.value), value: editX }), _jsx("input", { "aria-label": "\u5BF9\u8C61 Y", disabled: disabled, inputMode: "decimal", onChange: (event) => setEditY(event.target.value), value: editY }), _jsx("button", { disabled: disabled || editLabel.trim().length === 0, type: "submit", children: "\u4FDD\u5B58\u5BF9\u8C61" })] })), selected === undefined || onRemove === undefined ? null : _jsx("button", { disabled: disabled, onClick: requestRemoval, type: "button", children: "\u5220\u9664\u5BF9\u8C61" }), removalPlan === null ? null : (_jsxs("form", { "aria-label": "\u786E\u8BA4\u5220\u9664\u7A7A\u95F4\u5BF9\u8C61", onSubmit: confirmRemoval, children: [_jsxs("h3", { children: ["\u5220\u9664 ", removalPlan.target.label] }), removalPlan.impacts.length === 0 ? _jsx("p", { children: "\u8BE5\u5BF9\u8C61\u6CA1\u6709\u5F15\u7528\u5F71\u54CD\u3002" }) : (_jsxs(_Fragment, { children: [_jsx("p", { children: "\u5220\u9664\u4F1A\u5F71\u54CD\u4EE5\u4E0B\u5F15\u7528\uFF1A" }), _jsx("ul", { "aria-label": "\u7A7A\u95F4\u5F15\u7528\u5F71\u54CD\u5217\u8868", children: removalPlan.impacts.map(({ reference }) => _jsxs("li", { children: [reference.ownerKind, " ", reference.ownerId, " \u00B7 ", reference.fieldPath] }, `${reference.ownerId}:${reference.fieldPath}`)) }), _jsxs("label", { children: [_jsx("input", { disabled: disabled, name: "confirm-removal", type: "checkbox" }), " \u6211\u5DF2\u77E5\u6089\u8FD9\u4E9B\u5F15\u7528\u4F1A\u5931\u6548"] })] })), _jsx("button", { disabled: disabled, type: "submit", children: "\u786E\u8BA4\u5220\u9664" }), _jsx("button", { disabled: disabled, onClick: () => setRemovalPlan(null), type: "button", children: "\u53D6\u6D88" })] }))] }));
|
|
61
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./schema.js";
|
|
2
|
+
export * from "./plane-generation.js";
|
|
3
|
+
export * from "./procedural-city.js";
|
|
4
|
+
export * from "./road-generation.js";
|
|
5
|
+
export * from "./road-parcels.js";
|
|
6
|
+
export * from "./spatial-runtime.js";
|
|
7
|
+
export * from "./spatial-templates.js";
|
|
8
|
+
export * from "./protobuf-source-codec.js";
|
|
9
|
+
export * from "./source-adapter.js";
|
|
10
|
+
export * from "./worker.js";
|
|
11
|
+
export * from "./SpatialWorldCanvas.js";
|
|
12
|
+
export * from "./SpatialWorldEditor.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./schema.js";
|
|
2
|
+
export * from "./plane-generation.js";
|
|
3
|
+
export * from "./procedural-city.js";
|
|
4
|
+
export * from "./road-generation.js";
|
|
5
|
+
export * from "./road-parcels.js";
|
|
6
|
+
export * from "./spatial-runtime.js";
|
|
7
|
+
export * from "./spatial-templates.js";
|
|
8
|
+
export * from "./protobuf-source-codec.js";
|
|
9
|
+
export * from "./source-adapter.js";
|
|
10
|
+
export * from "./worker.js";
|
|
11
|
+
export * from "./SpatialWorldCanvas.js";
|
|
12
|
+
export * from "./SpatialWorldEditor.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export declare const PLANE_GENERATION_PROFILE: "plane-perlin-hydrology-v1";
|
|
2
|
+
export declare const SPHERE_GENERATION_PROFILE: "sphere-climate-v2";
|
|
3
|
+
export declare const SPHERE_CLIMATE_V2_IDENTITY: "sha256:76923954c7c8681a750e59d273539db5065a48a16d26c8980449891b5561cfd2";
|
|
4
|
+
export declare const PLANE_GENERATION_MAX_EXTENT_METERS: 1000000000;
|
|
5
|
+
export declare const PLANE_GENERATION_MAX_ASPECT_RATIO: 64;
|
|
6
|
+
export type PlaneGenerationResolution = 128 | 256 | 512;
|
|
7
|
+
/** 旧平面 profile 的 exact-compatible 输入。 */
|
|
8
|
+
export interface LegacyPlaneGenerationV1 {
|
|
9
|
+
readonly profile: typeof PLANE_GENERATION_PROFILE;
|
|
10
|
+
readonly seed: number;
|
|
11
|
+
readonly widthMeters: number;
|
|
12
|
+
readonly heightMeters: number;
|
|
13
|
+
readonly resolution: PlaneGenerationResolution;
|
|
14
|
+
readonly waterCoverage: number;
|
|
15
|
+
readonly relief: number;
|
|
16
|
+
readonly riverDensity: number;
|
|
17
|
+
readonly lakeDensity: number;
|
|
18
|
+
readonly rainfall: number;
|
|
19
|
+
}
|
|
20
|
+
/** 新建世界使用的球面气候生成合同。 */
|
|
21
|
+
export interface WorldGenerationV2 {
|
|
22
|
+
readonly profile: typeof SPHERE_GENERATION_PROFILE;
|
|
23
|
+
readonly topology: "sphere";
|
|
24
|
+
readonly seed: number;
|
|
25
|
+
readonly widthMeters: number;
|
|
26
|
+
readonly heightMeters: number;
|
|
27
|
+
readonly resolution: PlaneGenerationResolution;
|
|
28
|
+
readonly waterCoverage: number;
|
|
29
|
+
readonly relief: number;
|
|
30
|
+
readonly riverDensity: number;
|
|
31
|
+
readonly lakeDensity: number;
|
|
32
|
+
readonly rainfall: number;
|
|
33
|
+
readonly temperatureC: number;
|
|
34
|
+
}
|
|
35
|
+
/** 保留既有导出名;调用方按 profile 判别旧平面与球面 v2。 */
|
|
36
|
+
export type PlaneGenerationV1 = LegacyPlaneGenerationV1 | WorldGenerationV2;
|
|
37
|
+
export type BiomeId = "ocean" | "freshwater" | "icecap" | "permafrost" | "tundra" | "alpine" | "desert" | "grassland" | "temperate" | "rainforest";
|
|
38
|
+
export interface BiomePaletteEntry {
|
|
39
|
+
readonly id: BiomeId;
|
|
40
|
+
readonly label: string;
|
|
41
|
+
readonly color: `#${string}`;
|
|
42
|
+
}
|
|
43
|
+
/** 生态颜色的唯一 authority;WASM 只输出稳定 biome id。 */
|
|
44
|
+
export declare const BIOME_PALETTE: readonly BiomePaletteEntry[];
|
|
45
|
+
export declare const DEFAULT_PLANE_GENERATION: Readonly<LegacyPlaneGenerationV1>;
|
|
46
|
+
export declare const DEFAULT_WORLD_GENERATION: Readonly<WorldGenerationV2>;
|
|
47
|
+
/** 严格验证并返回按 profile 判别的 canonical 参数;未知字段不会被静默丢弃。 */
|
|
48
|
+
export declare function validatePlaneGeneration(value: unknown): PlaneGenerationV1;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export const PLANE_GENERATION_PROFILE = "plane-perlin-hydrology-v1";
|
|
2
|
+
export const SPHERE_GENERATION_PROFILE = "sphere-climate-v2";
|
|
3
|
+
export const SPHERE_CLIMATE_V2_IDENTITY = "sha256:76923954c7c8681a750e59d273539db5065a48a16d26c8980449891b5561cfd2";
|
|
4
|
+
export const PLANE_GENERATION_MAX_EXTENT_METERS = 1_000_000_000;
|
|
5
|
+
export const PLANE_GENERATION_MAX_ASPECT_RATIO = 64;
|
|
6
|
+
/** 生态颜色的唯一 authority;WASM 只输出稳定 biome id。 */
|
|
7
|
+
export const BIOME_PALETTE = Object.freeze([
|
|
8
|
+
{ id: "ocean", label: "海洋", color: "#1769aa" },
|
|
9
|
+
{ id: "freshwater", label: "湖泊", color: "#2f9ee5" },
|
|
10
|
+
{ id: "icecap", label: "冰盖", color: "#f7fbff" },
|
|
11
|
+
{ id: "permafrost", label: "永久冻土", color: "#b7d4d2" },
|
|
12
|
+
{ id: "tundra", label: "苔原", color: "#8f9d6a" },
|
|
13
|
+
{ id: "alpine", label: "高山", color: "#77777f" },
|
|
14
|
+
{ id: "desert", label: "荒漠", color: "#e3b341" },
|
|
15
|
+
{ id: "grassland", label: "草原", color: "#79ad45" },
|
|
16
|
+
{ id: "temperate", label: "温带林地", color: "#3f8f45" },
|
|
17
|
+
{ id: "rainforest", label: "热带雨林", color: "#075c36" },
|
|
18
|
+
]);
|
|
19
|
+
export const DEFAULT_PLANE_GENERATION = Object.freeze({
|
|
20
|
+
profile: PLANE_GENERATION_PROFILE,
|
|
21
|
+
seed: 2_050_986_450,
|
|
22
|
+
widthMeters: 100_000,
|
|
23
|
+
heightMeters: 100_000,
|
|
24
|
+
resolution: 256,
|
|
25
|
+
waterCoverage: 0.62,
|
|
26
|
+
relief: 0.38,
|
|
27
|
+
riverDensity: 0.5,
|
|
28
|
+
lakeDensity: 0.08,
|
|
29
|
+
rainfall: 1,
|
|
30
|
+
});
|
|
31
|
+
export const DEFAULT_WORLD_GENERATION = Object.freeze({
|
|
32
|
+
profile: SPHERE_GENERATION_PROFILE,
|
|
33
|
+
topology: "sphere",
|
|
34
|
+
seed: 2_050_986_450,
|
|
35
|
+
widthMeters: 40_030_000,
|
|
36
|
+
heightMeters: 20_015_000,
|
|
37
|
+
resolution: 512,
|
|
38
|
+
waterCoverage: 0.62,
|
|
39
|
+
relief: 0.38,
|
|
40
|
+
riverDensity: 0.5,
|
|
41
|
+
lakeDensity: 0.08,
|
|
42
|
+
rainfall: 1,
|
|
43
|
+
temperatureC: 30,
|
|
44
|
+
});
|
|
45
|
+
const LEGACY_KEYS = new Set([
|
|
46
|
+
"profile", "seed", "widthMeters", "heightMeters", "resolution", "waterCoverage", "relief",
|
|
47
|
+
"riverDensity", "lakeDensity", "rainfall",
|
|
48
|
+
]);
|
|
49
|
+
const SPHERE_KEYS = new Set([
|
|
50
|
+
"profile", "topology", "seed", "widthMeters", "heightMeters", "resolution", "waterCoverage",
|
|
51
|
+
"relief", "riverDensity", "lakeDensity", "rainfall", "temperatureC",
|
|
52
|
+
]);
|
|
53
|
+
function isRecord(value) {
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
function finiteNumber(value, field) {
|
|
57
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
58
|
+
throw new TypeError(`${field} 必须是有限数`);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function boundedNumber(value, field, minimum, maximum) {
|
|
62
|
+
const number = finiteNumber(value, field);
|
|
63
|
+
if (number < minimum || number > maximum)
|
|
64
|
+
throw new RangeError(`${field} 必须位于 ${minimum}..${maximum}`);
|
|
65
|
+
return number;
|
|
66
|
+
}
|
|
67
|
+
function validateExactKeys(value, expected, label) {
|
|
68
|
+
const unknownKeys = Object.keys(value).filter((key) => !expected.has(key));
|
|
69
|
+
if (unknownKeys.length > 0)
|
|
70
|
+
throw new TypeError(`${label} 包含未知字段:${unknownKeys.join(", ")}`);
|
|
71
|
+
const missingKeys = [...expected].filter((key) => !Object.hasOwn(value, key));
|
|
72
|
+
if (missingKeys.length > 0)
|
|
73
|
+
throw new TypeError(`${label} 缺少字段:${missingKeys.join(", ")}`);
|
|
74
|
+
}
|
|
75
|
+
function validateSharedNumbers(value) {
|
|
76
|
+
const seed = finiteNumber(value.seed, "seed");
|
|
77
|
+
if (!Number.isInteger(seed) || seed < 0 || seed > 0xffff_ffff)
|
|
78
|
+
throw new RangeError("seed 必须是 uint32");
|
|
79
|
+
const widthMeters = boundedNumber(value.widthMeters, "widthMeters", 1, PLANE_GENERATION_MAX_EXTENT_METERS);
|
|
80
|
+
const heightMeters = boundedNumber(value.heightMeters, "heightMeters", 1, PLANE_GENERATION_MAX_EXTENT_METERS);
|
|
81
|
+
const resolution = finiteNumber(value.resolution, "resolution");
|
|
82
|
+
if (resolution !== 128 && resolution !== 256 && resolution !== 512) {
|
|
83
|
+
throw new RangeError("resolution 必须为 128、256 或 512");
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
seed,
|
|
87
|
+
widthMeters,
|
|
88
|
+
heightMeters,
|
|
89
|
+
resolution: resolution,
|
|
90
|
+
waterCoverage: boundedNumber(value.waterCoverage, "waterCoverage", 0, 0.95),
|
|
91
|
+
relief: boundedNumber(value.relief, "relief", 0, 1),
|
|
92
|
+
riverDensity: boundedNumber(value.riverDensity, "riverDensity", 0, 1),
|
|
93
|
+
lakeDensity: boundedNumber(value.lakeDensity, "lakeDensity", 0, 0.25),
|
|
94
|
+
rainfall: boundedNumber(value.rainfall, "rainfall", 0.1, 8),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** 严格验证并返回按 profile 判别的 canonical 参数;未知字段不会被静默丢弃。 */
|
|
98
|
+
export function validatePlaneGeneration(value) {
|
|
99
|
+
if (!isRecord(value))
|
|
100
|
+
throw new TypeError("PlaneGenerationV1 必须是对象");
|
|
101
|
+
if (value.profile === PLANE_GENERATION_PROFILE) {
|
|
102
|
+
validateExactKeys(value, LEGACY_KEYS, "LegacyPlaneGenerationV1");
|
|
103
|
+
const shared = validateSharedNumbers(value);
|
|
104
|
+
if (Math.max(shared.widthMeters, shared.heightMeters) / Math.min(shared.widthMeters, shared.heightMeters)
|
|
105
|
+
> PLANE_GENERATION_MAX_ASPECT_RATIO) {
|
|
106
|
+
throw new RangeError(`widthMeters 与 heightMeters 的比例不得超过 ${PLANE_GENERATION_MAX_ASPECT_RATIO}:1`);
|
|
107
|
+
}
|
|
108
|
+
return Object.freeze({ profile: PLANE_GENERATION_PROFILE, ...shared });
|
|
109
|
+
}
|
|
110
|
+
if (value.profile === SPHERE_GENERATION_PROFILE) {
|
|
111
|
+
validateExactKeys(value, SPHERE_KEYS, "WorldGenerationV2");
|
|
112
|
+
if (value.topology !== "sphere")
|
|
113
|
+
throw new TypeError("topology 必须为 sphere");
|
|
114
|
+
const shared = validateSharedNumbers(value);
|
|
115
|
+
if (shared.widthMeters !== shared.heightMeters * 2) {
|
|
116
|
+
throw new RangeError("球面 widthMeters 必须严格等于 heightMeters 的两倍");
|
|
117
|
+
}
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
profile: SPHERE_GENERATION_PROFILE,
|
|
120
|
+
topology: "sphere",
|
|
121
|
+
...shared,
|
|
122
|
+
temperatureC: boundedNumber(value.temperatureC, "temperatureC", -40, 60),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
throw new TypeError(`profile 必须为 ${PLANE_GENERATION_PROFILE} 或 ${SPHERE_GENERATION_PROFILE}`);
|
|
126
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "worldengine-plane-generator",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"files": [
|
|
6
|
+
"plane-generator_bg.wasm",
|
|
7
|
+
"plane-generator.js",
|
|
8
|
+
"plane-generator.d.ts"
|
|
9
|
+
],
|
|
10
|
+
"main": "plane-generator.js",
|
|
11
|
+
"types": "plane-generator.d.ts",
|
|
12
|
+
"sideEffects": [
|
|
13
|
+
"./snippets/*"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
export function generate_plane_json(input: string): string;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 原始 terrain-roads-v1 core ABI,供行为测试和非 plane adapter 调用。
|
|
8
|
+
*/
|
|
9
|
+
export function generate_road_network_json(input: string): string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 为旧平面地形生成固定、与相机无关的道路缓存。
|
|
13
|
+
*
|
|
14
|
+
* sphere-climate-v2 尚未接入此 plane-only adapter;调用者须按 profile
|
|
15
|
+
* 明确选择适配器,不能把球面参数悄然降级为平面。
|
|
16
|
+
*/
|
|
17
|
+
export function generate_world_roads_json(input: string): string;
|
|
18
|
+
|
|
19
|
+
export function sample_plane_tile_json(input: string): string;
|
|
20
|
+
|
|
21
|
+
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
22
|
+
|
|
23
|
+
export interface InitOutput {
|
|
24
|
+
readonly memory: WebAssembly.Memory;
|
|
25
|
+
readonly generate_plane_json: (a: number, b: number) => [number, number, number, number];
|
|
26
|
+
readonly generate_road_network_json: (a: number, b: number) => [number, number, number, number];
|
|
27
|
+
readonly generate_world_roads_json: (a: number, b: number) => [number, number, number, number];
|
|
28
|
+
readonly sample_plane_tile_json: (a: number, b: number) => [number, number, number, number];
|
|
29
|
+
readonly __wbindgen_externrefs: WebAssembly.Table;
|
|
30
|
+
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
|
31
|
+
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
32
|
+
readonly __externref_table_dealloc: (a: number) => void;
|
|
33
|
+
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
|
34
|
+
readonly __wbindgen_start: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Instantiates the given `module`, which can either be bytes or
|
|
41
|
+
* a precompiled `WebAssembly.Module`.
|
|
42
|
+
*
|
|
43
|
+
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
|
44
|
+
*
|
|
45
|
+
* @returns {InitOutput}
|
|
46
|
+
*/
|
|
47
|
+
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
|
51
|
+
* for everything else, calls `WebAssembly.instantiate` directly.
|
|
52
|
+
*
|
|
53
|
+
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
|
54
|
+
*
|
|
55
|
+
* @returns {Promise<InitOutput>}
|
|
56
|
+
*/
|
|
57
|
+
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|