feat(assets): publish reusable area packages

This commit is contained in:
2026-08-11 16:07:07 +08:00
parent b4a81331e8
commit f385009043
18 changed files with 292 additions and 108 deletions

View File

@@ -0,0 +1,66 @@
"use strict";
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const SCHEMA = "osm-asset-package/v1";
const CATEGORIES = new Set(["scene", "roads", "buildings", "vegetation", "water"]);
const ROLES = new Set(["scene", "layer"]);
function relativeUri(uri) {
if (typeof uri !== "string" || !uri || uri.startsWith("/") || path.isAbsolute(uri) || uri.includes("\\") || uri.split("/").includes("..")) {
throw new Error(`Asset URI must be a package-relative forward-slash path: ${uri}`);
}
return uri;
}
function validateManifest(manifest, packageDir, options = {}) {
const errors = [];
const fail = (message) => errors.push(message);
if (!manifest || manifest.schema !== SCHEMA) fail(`schema must be '${SCHEMA}'`);
if (typeof manifest?.packageVersion !== "string" || !manifest.packageVersion) fail("packageVersion is required");
if (typeof manifest?.areaId !== "string" || !manifest.areaId) fail("areaId is required");
const coordinateSystem = manifest?.coordinateSystem || {};
if (coordinateSystem.axes !== "ENU" || coordinateSystem.units !== "meters" || coordinateSystem.x !== "east" || coordinateSystem.y !== "north" || coordinateSystem.z !== "up") fail("coordinateSystem must declare ENU meters (east/north/up)");
const placement = manifest?.placement || {};
for (const key of ["longitude", "latitude", "height", "headingCorrectionDegrees"]) if (!Number.isFinite(placement[key])) fail(`placement.${key} must be finite`);
if (Number.isFinite(placement.longitude) && (placement.longitude < -180 || placement.longitude > 180)) fail("placement.longitude is out of range");
if (Number.isFinite(placement.latitude) && (placement.latitude < -90 || placement.latitude > 90)) fail("placement.latitude is out of range");
const bounds = manifest?.bounds || {};
for (const key of ["minLon", "minLat", "maxLon", "maxLat"]) if (!Number.isFinite(bounds[key])) fail(`bounds.${key} must be finite`);
if (Number.isFinite(bounds.minLon) && Number.isFinite(bounds.maxLon) && bounds.minLon > bounds.maxLon) fail("bounds longitude order is invalid");
if (Number.isFinite(bounds.minLat) && Number.isFinite(bounds.maxLat) && bounds.minLat > bounds.maxLat) fail("bounds latitude order is invalid");
if (!Array.isArray(manifest?.assets) || !manifest.assets.length) fail("assets must be a non-empty array");
const ids = new Set(); let sceneCount = 0;
for (const asset of manifest?.assets || []) {
if (!asset || typeof asset.id !== "string" || !asset.id) { fail("asset id is required"); continue; }
if (ids.has(asset.id)) fail(`duplicate asset id '${asset.id}'`); ids.add(asset.id);
if (!ROLES.has(asset.role)) fail(`asset '${asset.id}' has invalid role`);
if (!CATEGORIES.has(asset.category)) fail(`asset '${asset.id}' has invalid category`);
if (typeof asset.defaultLoad !== "boolean") fail(`asset '${asset.id}' defaultLoad must be boolean`);
try { relativeUri(asset.uri); } catch (error) { fail(error.message); continue; }
if (asset.role === "scene") {
sceneCount += 1;
if (asset.category !== "scene" || !asset.defaultLoad) fail("scene asset must be category scene and defaultLoad true");
} else if (asset.category === "scene" || asset.defaultLoad) fail("layer assets must use a semantic category and defaultLoad false");
if (packageDir && options.requireFiles !== false) {
const resolved = path.resolve(packageDir, asset.uri);
if (!resolved.startsWith(`${path.resolve(packageDir)}${path.sep}`) || !fs.existsSync(resolved)) fail(`asset '${asset.id}' is missing from package: ${asset.uri}`);
}
}
if (sceneCount !== 1) fail("exactly one scene asset is required");
if (errors.length) throw new Error(`Invalid asset package manifest: ${errors.join("; ")}`);
return manifest;
}
function addIntegrity(manifest, packageDir) {
for (const asset of manifest.assets) {
const file = path.resolve(packageDir, asset.uri);
const buffer = fs.readFileSync(file);
asset.integrity = { bytes: buffer.length, sha256: crypto.createHash("sha256").update(buffer).digest("hex") };
}
return manifest;
}
module.exports = { SCHEMA, CATEGORIES, ROLES, relativeUri, validateManifest, addIntegrity };