fix(assets): publish controllable traffic signal runtime
This commit is contained in:
@@ -166,8 +166,8 @@ transcode,成功后替换**package staging** 中的主 GLB 与 manifest;未
|
|||||||
### `outputs`(逃生舱)
|
### `outputs`(逃生舱)
|
||||||
|
|
||||||
默认全部从 `id` 推导。发布给下游的静态资产固定在
|
默认全部从 `id` 推导。发布给下游的静态资产固定在
|
||||||
`<outputRoot>/<id>/package/`,临时静态资产在 `_pipeline/package-staging/`,预览专用动态资源在
|
`<outputRoot>/<id>/package/`(含 `models/` 与可控信号灯 `runtime/`),临时静态资产在
|
||||||
`<outputRoot>/<id>/_preview/`。需要定制时逐项覆盖:
|
`_pipeline/package-staging/`,预览专用车辆/路线资源在 `<outputRoot>/<id>/_preview/`。需要定制时逐项覆盖:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -168,7 +168,10 @@ outputs/<area>/package/manifest.json
|
|||||||
- `coordinateSystem` 固定为 ENU meters,`placement` 是 WGS84 锚点与 heading correction。
|
- `coordinateSystem` 固定为 ENU meters,`placement` 是 WGS84 锚点与 heading correction。
|
||||||
- 必须恰有一个 `role="scene"` / `category="scene"` / `defaultLoad=true` 主资产;roads、buildings、
|
- 必须恰有一个 `role="scene"` / `category="scene"` / `defaultLoad=true` 主资产;roads、buildings、
|
||||||
vegetation、water 是 `role="layer"` 且 `defaultLoad=false` 的可选语义层。
|
vegetation、water 是 `role="layer"` 且 `defaultLoad=false` 的可选语义层。
|
||||||
- `assets[*].uri` 只能是包内正向相对路径,禁止绝对路径、反斜杠和 `..`。
|
- `assets[*].uri` 与 `runtime[*].uri` 只能是包内正向相对路径,禁止绝对路径、反斜杠和 `..`。
|
||||||
|
- `runtime` 声明可编程运行时资产;当前至少包含 `traffic-signals` 锚点 JSON、
|
||||||
|
`traffic-signals-dynamic` 灯珠 GLB,以及两个 `traffic-signals-countdown-*` 倒计时 GLB。
|
||||||
|
它们是资产包的一部分,不属于 `_preview/`。
|
||||||
- `package` 在晋升前调用 `validateManifest()`;再为每个模型写 bytes 与 SHA-256,然后复验。
|
- `package` 在晋升前调用 `validateManifest()`;再为每个模型写 bytes 与 SHA-256,然后复验。
|
||||||
- `package` 是唯一拥有最终 package file records 的 stage。`cesium` / `compress` 只能拥有 staging
|
- `package` 是唯一拥有最终 package file records 的 stage。`cesium` / `compress` 只能拥有 staging
|
||||||
或计算摘要,不能留下指向最终 package 文件的 outputs record。
|
或计算摘要,不能留下指向最终 package 文件的 outputs record。
|
||||||
@@ -190,7 +193,7 @@ outputs/<area>/package/manifest.json
|
|||||||
|
|
||||||
- Good: 下游只带走 `package/`,按 manifest 的 WGS84 placement 放置主模型,按需加载 layer。
|
- Good: 下游只带走 `package/`,按 manifest 的 WGS84 placement 放置主模型,按需加载 layer。
|
||||||
- Base: 只有 main scene 的区域仍是合法 package;没有几何的语义类别不写空 GLB。
|
- Base: 只有 main scene 的区域仍是合法 package;没有几何的语义类别不写空 GLB。
|
||||||
- Bad: 将 `_preview/` 的车辆模型或动态交通灯加进 `assets`,使资产包绑定本项目的验证运行时。
|
- Bad: 将 `_preview/` 的车辆模型或路线加进 package;它们是本项目的验证运行时,信号灯 runtime 则必须发布。
|
||||||
|
|
||||||
### 6. Tests Required
|
### 6. Tests Required
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const config = window.OSM_ASSET_PREVIEW_CONFIG || {}; // :4
|
|||||||
`build-area.js` 只保留已发布 package / 动态输入的依赖检查、写入顺序和 preview manifest ownership;
|
`build-area.js` 只保留已发布 package / 动态输入的依赖检查、写入顺序和 preview manifest ownership;
|
||||||
不要把 HTML 模板、runtime copy 或转义实现移回阶段调度器。路线 JSON 与车辆 glTF 分别由
|
不要把 HTML 模板、runtime copy 或转义实现移回阶段调度器。路线 JSON 与车辆 glTF 分别由
|
||||||
`vehicle-route.js` 和 `vehicle-model.js` 生成,二者都是不启动外部工具的 Node 模块。
|
`vehicle-route.js` 和 `vehicle-model.js` 生成,二者都是不启动外部工具的 Node 模块。
|
||||||
|
交通信号灯 runtime 则从 package manifest 的 `runtime` 读取;预览只负责驱动其状态,不拥有这些文件。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -675,6 +675,13 @@ def export(args):
|
|||||||
else:
|
else:
|
||||||
center_lon = center_lat = 0.0
|
center_lon = center_lat = 0.0
|
||||||
area_id = os.path.splitext(os.path.basename(args["glb"]))[0]
|
area_id = os.path.splitext(os.path.basename(args["glb"]))[0]
|
||||||
|
runtime_assets = []
|
||||||
|
if args.get("dynamic_glb"):
|
||||||
|
runtime_assets.append({"id": "traffic-signals-dynamic", "type": "traffic-signal-lenses", "uri": "runtime/traffic-signals-dynamic.glb"})
|
||||||
|
if args.get("countdown_0_glb"):
|
||||||
|
runtime_assets.append({"id": "traffic-signals-countdown-0", "type": "traffic-signal-countdown", "phaseGroup": 0, "uri": "runtime/traffic-signals-countdown-0.glb"})
|
||||||
|
if args.get("countdown_1_glb"):
|
||||||
|
runtime_assets.append({"id": "traffic-signals-countdown-1", "type": "traffic-signal-countdown", "phaseGroup": 1, "uri": "runtime/traffic-signals-countdown-1.glb"})
|
||||||
metadata = {
|
metadata = {
|
||||||
"schema": "osm-asset-package/v1",
|
"schema": "osm-asset-package/v1",
|
||||||
"packageVersion": "1.0.0",
|
"packageVersion": "1.0.0",
|
||||||
@@ -686,6 +693,7 @@ def export(args):
|
|||||||
"id": asset["id"], "role": "layer", "category": asset["id"],
|
"id": asset["id"], "role": "layer", "category": asset["id"],
|
||||||
"uri": "models/" + os.path.basename(asset["path"]), "defaultLoad": False,
|
"uri": "models/" + os.path.basename(asset["path"]), "defaultLoad": False,
|
||||||
} for asset in semantic_assets],
|
} for asset in semantic_assets],
|
||||||
|
"runtime": runtime_assets,
|
||||||
"sceneStats": {
|
"sceneStats": {
|
||||||
"buildings": scene.get("building_count", 0),
|
"buildings": scene.get("building_count", 0),
|
||||||
"industrial_buildings": scene.get("industrial_building_count", 0),
|
"industrial_buildings": scene.get("industrial_building_count", 0),
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ function resolveAsset(manifestUrl, manifest, id = "main") {
|
|||||||
return new URL(asset.uri, manifestUrl).href;
|
return new URL(asset.uri, manifestUrl).href;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveRuntime(manifestUrl, manifest, id) {
|
||||||
|
const runtime = (manifest.runtime || []).find((candidate) => candidate.id === id);
|
||||||
|
if (!runtime) throw new Error(`Unknown runtime asset '${id}'`);
|
||||||
|
return new URL(runtime.uri, manifestUrl).href;
|
||||||
|
}
|
||||||
|
|
||||||
function placement(manifest) {
|
function placement(manifest) {
|
||||||
return { ...manifest.placement, coordinateSystem: manifest.coordinateSystem };
|
return { ...manifest.placement, coordinateSystem: manifest.coordinateSystem };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { resolveAsset, placement };
|
module.exports = { resolveAsset, resolveRuntime, placement };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const { resolveAsset, placement } = require("./asset-package-resolver");
|
const { resolveAsset, resolveRuntime, placement } = require("./asset-package-resolver");
|
||||||
|
|
||||||
async function loadPackage(viewer, manifestUrl, assetId = "main") {
|
async function loadPackage(viewer, manifestUrl, assetId = "main") {
|
||||||
const manifest = await fetch(manifestUrl).then((response) => {
|
const manifest = await fetch(manifestUrl).then((response) => {
|
||||||
@@ -14,7 +14,7 @@ async function loadPackage(viewer, manifestUrl, assetId = "main") {
|
|||||||
const modelMatrix = Cesium.Matrix4.multiplyByMatrix3(enu, rotation, new Cesium.Matrix4());
|
const modelMatrix = Cesium.Matrix4.multiplyByMatrix3(enu, rotation, new Cesium.Matrix4());
|
||||||
const model = await Cesium.Model.fromGltfAsync({ url: resolveAsset(manifestUrl, manifest, assetId), modelMatrix });
|
const model = await Cesium.Model.fromGltfAsync({ url: resolveAsset(manifestUrl, manifest, assetId), modelMatrix });
|
||||||
viewer.scene.primitives.add(model);
|
viewer.scene.primitives.add(model);
|
||||||
return { manifest, model, placement: p };
|
return { manifest, model, placement: p, runtime: (id) => resolveRuntime(manifestUrl, manifest, id) };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { loadPackage };
|
module.exports = { loadPackage };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const { resolveAsset, placement } = require("./asset-package-resolver");
|
const { resolveAsset, resolveRuntime, placement } = require("./asset-package-resolver");
|
||||||
|
|
||||||
async function loadPackage(loader, manifestUrl, assetId = "main", handoffEnuPlacement) {
|
async function loadPackage(loader, manifestUrl, assetId = "main", handoffEnuPlacement) {
|
||||||
const manifest = await fetch(manifestUrl).then((response) => {
|
const manifest = await fetch(manifestUrl).then((response) => {
|
||||||
@@ -11,7 +11,7 @@ async function loadPackage(loader, manifestUrl, assetId = "main", handoffEnuPlac
|
|||||||
// Three.js does not georeference local ENU itself; the embedding host owns it.
|
// Three.js does not georeference local ENU itself; the embedding host owns it.
|
||||||
const enuPlacement = placement(manifest);
|
const enuPlacement = placement(manifest);
|
||||||
if (handoffEnuPlacement) handoffEnuPlacement(enuPlacement, scene.scene);
|
if (handoffEnuPlacement) handoffEnuPlacement(enuPlacement, scene.scene);
|
||||||
return { manifest, scene: scene.scene, placement: enuPlacement };
|
return { manifest, scene: scene.scene, placement: enuPlacement, runtime: (id) => resolveRuntime(manifestUrl, manifest, id) };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { loadPackage };
|
module.exports = { loadPackage };
|
||||||
|
|||||||
@@ -280,6 +280,9 @@ function exportCesium(area) {
|
|||||||
fs.rmSync(area.outputs.packageStagingDir, { recursive: true, force: true });
|
fs.rmSync(area.outputs.packageStagingDir, { recursive: true, force: true });
|
||||||
fs.mkdirSync(path.dirname(area.outputs.glb), { recursive: true });
|
fs.mkdirSync(path.dirname(area.outputs.glb), { recursive: true });
|
||||||
fs.mkdirSync(path.dirname(area.outputs.metadata), { recursive: true });
|
fs.mkdirSync(path.dirname(area.outputs.metadata), { recursive: true });
|
||||||
|
fs.mkdirSync(path.dirname(area.outputs.trafficSignalsDynamicGlb), { recursive: true });
|
||||||
|
fs.mkdirSync(path.dirname(area.outputs.trafficSignalsCountdown0Glb), { recursive: true });
|
||||||
|
fs.mkdirSync(path.dirname(area.outputs.trafficSignalsCountdown1Glb), { recursive: true });
|
||||||
|
|
||||||
console.log("Stage: cesium");
|
console.log("Stage: cesium");
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
@@ -420,8 +423,15 @@ function compressCesiumGlb(area) {
|
|||||||
|
|
||||||
function publishPackage(area) {
|
function publishPackage(area) {
|
||||||
ensureFile(area.outputs.packageStagingManifest, "Staged package manifest");
|
ensureFile(area.outputs.packageStagingManifest, "Staged package manifest");
|
||||||
|
ensureFile(area.outputs.trafficSignals, "Traffic signal anchors");
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
const manifest = JSON.parse(fs.readFileSync(area.outputs.packageStagingManifest, "utf8"));
|
const manifest = JSON.parse(fs.readFileSync(area.outputs.packageStagingManifest, "utf8"));
|
||||||
|
fs.mkdirSync(area.outputs.packageStagingRuntimeDir, { recursive: true });
|
||||||
|
fs.copyFileSync(area.outputs.trafficSignals, area.outputs.packageStagingTrafficSignals);
|
||||||
|
manifest.runtime = Array.isArray(manifest.runtime) ? manifest.runtime : [];
|
||||||
|
if (!manifest.runtime.some((runtime) => runtime.id === "traffic-signals")) {
|
||||||
|
manifest.runtime.push({ id: "traffic-signals", type: "traffic-signal-anchors", uri: "runtime/traffic-signals.json" });
|
||||||
|
}
|
||||||
validateManifest(manifest, area.outputs.packageStagingDir);
|
validateManifest(manifest, area.outputs.packageStagingDir);
|
||||||
addIntegrity(manifest, area.outputs.packageStagingDir);
|
addIntegrity(manifest, area.outputs.packageStagingDir);
|
||||||
fs.writeFileSync(area.outputs.packageStagingManifest, `${JSON.stringify(manifest, null, 2)}\n`);
|
fs.writeFileSync(area.outputs.packageStagingManifest, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||||
@@ -473,7 +483,7 @@ function runCommand(command, commandArgs, stage) {
|
|||||||
|
|
||||||
function writeCesiumPreview(area) {
|
function writeCesiumPreview(area) {
|
||||||
ensureFile(area.outputs.packageManifest, "Published asset package manifest");
|
ensureFile(area.outputs.packageManifest, "Published asset package manifest");
|
||||||
ensureFile(area.outputs.trafficSignals, "Traffic signal anchors");
|
ensureFile(area.outputs.packageTrafficSignals, "Published traffic signal anchors");
|
||||||
const lanePolygons = path.join(area.outputs.geojsonDir, "lane_polygons.geojson");
|
const lanePolygons = path.join(area.outputs.geojsonDir, "lane_polygons.geojson");
|
||||||
const network = path.join(area.outputs.geojsonDir, "network.json");
|
const network = path.join(area.outputs.geojsonDir, "network.json");
|
||||||
const intersectionSurface = path.join(area.outputs.geojsonDir, "intersection_surface.geojson");
|
const intersectionSurface = path.join(area.outputs.geojsonDir, "intersection_surface.geojson");
|
||||||
@@ -493,14 +503,10 @@ function writeCesiumPreview(area) {
|
|||||||
const metadataName = "package/manifest.json";
|
const metadataName = "package/manifest.json";
|
||||||
const routeName = previewRelativePath(area.outputs.areaDir, area.outputs.vehicleRoute);
|
const routeName = previewRelativePath(area.outputs.areaDir, area.outputs.vehicleRoute);
|
||||||
const vehicleModelName = previewRelativePath(area.outputs.areaDir, area.outputs.vehicleModel);
|
const vehicleModelName = previewRelativePath(area.outputs.areaDir, area.outputs.vehicleModel);
|
||||||
const descriptor = { routeName: previewRelativePath(area.outputs.areaDir, area.outputs.vehicleRoute), vehicleModelName: previewRelativePath(area.outputs.areaDir, area.outputs.vehicleModel), vehicleModelNames: vehicleModelNames.map((name) => `_preview/${name}`), trafficSignalsName: previewRelativePath(area.outputs.areaDir, area.outputs.trafficSignals), assets: [
|
const descriptor = { routeName: previewRelativePath(area.outputs.areaDir, area.outputs.vehicleRoute), vehicleModelName: previewRelativePath(area.outputs.areaDir, area.outputs.vehicleModel), vehicleModelNames: vehicleModelNames.map((name) => `_preview/${name}`), trafficSignalsName: "package/runtime/traffic-signals.json", assets: [] };
|
||||||
{ id: "traffic-dynamic", url: previewRelativePath(area.outputs.areaDir, area.outputs.trafficSignalsDynamicGlb), category: "dynamic", enabled: true },
|
|
||||||
{ id: "traffic-countdown-0", url: previewRelativePath(area.outputs.areaDir, area.outputs.trafficSignalsCountdown0Glb), category: "countdown", enabled: true, phaseGroup: 0 },
|
|
||||||
{ id: "traffic-countdown-1", url: previewRelativePath(area.outputs.areaDir, area.outputs.trafficSignalsCountdown1Glb), category: "countdown", enabled: true, phaseGroup: 1 },
|
|
||||||
] };
|
|
||||||
fs.mkdirSync(area.outputs.previewDir, { recursive: true });
|
fs.mkdirSync(area.outputs.previewDir, { recursive: true });
|
||||||
fs.writeFileSync(area.outputs.previewDescriptor, `${JSON.stringify(descriptor, null, 2)}\n`);
|
fs.writeFileSync(area.outputs.previewDescriptor, `${JSON.stringify(descriptor, null, 2)}\n`);
|
||||||
fs.writeFileSync(htmlPath, cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, area.id, vehicleModelNames, previewRelativePath(area.outputs.areaDir, area.outputs.trafficSignals), "_preview/descriptor.json"));
|
fs.writeFileSync(htmlPath, cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, area.id, vehicleModelNames.map((name) => `_preview/${name}`), "package/runtime/traffic-signals.json", "_preview/descriptor.json"));
|
||||||
console.log(`Cesium preview: ${htmlPath}`);
|
console.log(`Cesium preview: ${htmlPath}`);
|
||||||
const finished = Date.now();
|
const finished = Date.now();
|
||||||
writeStageManifest(area, {
|
writeStageManifest(area, {
|
||||||
@@ -525,7 +531,7 @@ function writeCesiumPreview(area) {
|
|||||||
cesiumPreview: fileRecord(area.outputs.cesiumPreview),
|
cesiumPreview: fileRecord(area.outputs.cesiumPreview),
|
||||||
vehicleRoute: fileRecord(area.outputs.vehicleRoute),
|
vehicleRoute: fileRecord(area.outputs.vehicleRoute),
|
||||||
vehicleModel: fileRecord(area.outputs.vehicleModel),
|
vehicleModel: fileRecord(area.outputs.vehicleModel),
|
||||||
trafficSignals: fileRecord(area.outputs.trafficSignals),
|
trafficSignals: fileRecord(area.outputs.packageTrafficSignals),
|
||||||
},
|
},
|
||||||
summary: previewSummary(area),
|
summary: previewSummary(area),
|
||||||
warnings: [],
|
warnings: [],
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ function normalizeAreaConfig(raw, options = {}) {
|
|||||||
const pipelineDir = path.resolve(outputOverrides.pipelineDir || path.join(areaDir, "_pipeline"));
|
const pipelineDir = path.resolve(outputOverrides.pipelineDir || path.join(areaDir, "_pipeline"));
|
||||||
const packageDir = path.resolve(outputOverrides.packageDir || path.join(areaDir, "package"));
|
const packageDir = path.resolve(outputOverrides.packageDir || path.join(areaDir, "package"));
|
||||||
const packageStagingDir = path.resolve(outputOverrides.packageStagingDir || path.join(pipelineDir, "package-staging"));
|
const packageStagingDir = path.resolve(outputOverrides.packageStagingDir || path.join(pipelineDir, "package-staging"));
|
||||||
|
const packageRuntimeDir = path.resolve(outputOverrides.packageRuntimeDir || path.join(packageDir, "runtime"));
|
||||||
|
const packageStagingRuntimeDir = path.resolve(outputOverrides.packageStagingRuntimeDir || path.join(packageStagingDir, "runtime"));
|
||||||
const previewDir = path.resolve(outputOverrides.previewDir || path.join(areaDir, "_preview"));
|
const previewDir = path.resolve(outputOverrides.previewDir || path.join(areaDir, "_preview"));
|
||||||
const geojsonDir = path.resolve(outputOverrides.geojsonDir || path.join(areaDir, "osm2streets_web_out"));
|
const geojsonDir = path.resolve(outputOverrides.geojsonDir || path.join(areaDir, "osm2streets_web_out"));
|
||||||
const outputs = {
|
const outputs = {
|
||||||
@@ -47,11 +49,13 @@ function normalizeAreaConfig(raw, options = {}) {
|
|||||||
packageStagingModelDir: path.resolve(outputOverrides.packageStagingModelDir || path.join(packageStagingDir, "models")),
|
packageStagingModelDir: path.resolve(outputOverrides.packageStagingModelDir || path.join(packageStagingDir, "models")),
|
||||||
packagePrimaryGlb: path.resolve(outputOverrides.packagePrimaryGlb || path.join(packageDir, "models", `${fileStem}.glb`)),
|
packagePrimaryGlb: path.resolve(outputOverrides.packagePrimaryGlb || path.join(packageDir, "models", `${fileStem}.glb`)),
|
||||||
glb: path.resolve(outputOverrides.glb || path.join(packageStagingDir, "models", `${fileStem}.glb`)),
|
glb: path.resolve(outputOverrides.glb || path.join(packageStagingDir, "models", `${fileStem}.glb`)),
|
||||||
trafficSignalsDynamicGlb: path.resolve(
|
packageRuntimeDir,
|
||||||
outputOverrides.trafficSignalsDynamicGlb || path.join(previewDir, `${fileStem}-traffic-signals-dynamic.glb`),
|
packageStagingRuntimeDir,
|
||||||
),
|
trafficSignalsDynamicGlb: path.resolve(outputOverrides.trafficSignalsDynamicGlb || path.join(packageStagingRuntimeDir, "traffic-signals-dynamic.glb")),
|
||||||
trafficSignalsCountdown0Glb: path.resolve(outputOverrides.trafficSignalsCountdown0Glb || path.join(previewDir, `${fileStem}-traffic-signals-countdown-0.glb`)),
|
trafficSignalsCountdown0Glb: path.resolve(outputOverrides.trafficSignalsCountdown0Glb || path.join(packageStagingRuntimeDir, "traffic-signals-countdown-0.glb")),
|
||||||
trafficSignalsCountdown1Glb: path.resolve(outputOverrides.trafficSignalsCountdown1Glb || path.join(previewDir, `${fileStem}-traffic-signals-countdown-1.glb`)),
|
trafficSignalsCountdown1Glb: path.resolve(outputOverrides.trafficSignalsCountdown1Glb || path.join(packageStagingRuntimeDir, "traffic-signals-countdown-1.glb")),
|
||||||
|
packageTrafficSignals: path.resolve(outputOverrides.packageTrafficSignals || path.join(packageRuntimeDir, "traffic-signals.json")),
|
||||||
|
packageStagingTrafficSignals: path.resolve(outputOverrides.packageStagingTrafficSignals || path.join(packageStagingRuntimeDir, "traffic-signals.json")),
|
||||||
metadata: path.resolve(outputOverrides.metadata || path.join(packageStagingDir, "manifest.json")),
|
metadata: path.resolve(outputOverrides.metadata || path.join(packageStagingDir, "manifest.json")),
|
||||||
cesiumPreview: path.resolve(
|
cesiumPreview: path.resolve(
|
||||||
outputOverrides.cesiumPreview || path.join(areaDir, `${fileStem}-cesium-preview.html`),
|
outputOverrides.cesiumPreview || path.join(areaDir, `${fileStem}-cesium-preview.html`),
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ function artifactStatus(area) {
|
|||||||
["QGIS project", area.outputs.qgisProject, true, "file"],
|
["QGIS project", area.outputs.qgisProject, true, "file"],
|
||||||
["QGIS preview", area.outputs.qgisPreview, true, "file"],
|
["QGIS preview", area.outputs.qgisPreview, true, "file"],
|
||||||
["Traffic signal assemblies", area.outputs.trafficSignalAssemblies, true, "file"],
|
["Traffic signal assemblies", area.outputs.trafficSignalAssemblies, true, "file"],
|
||||||
["Traffic signal runtime", area.outputs.trafficSignals, true, "file"],
|
["Traffic signal runtime", area.outputs.packageTrafficSignals, true, "file"],
|
||||||
["Blend scene", area.outputs.blend, true, "file"],
|
["Blend scene", area.outputs.blend, true, "file"],
|
||||||
["Render PNG", area.outputs.render, true, "file"],
|
["Render PNG", area.outputs.render, true, "file"],
|
||||||
["Package manifest", area.outputs.packageManifest, true, "file"],
|
["Package manifest", area.outputs.packageManifest, true, "file"],
|
||||||
@@ -403,7 +403,7 @@ function stageManifestStatus(area, configPath = null) {
|
|||||||
geojsonDir: area.outputs.geojsonDir,
|
geojsonDir: area.outputs.geojsonDir,
|
||||||
...sceneGeojsonFiles(area),
|
...sceneGeojsonFiles(area),
|
||||||
trafficSignalAssemblies: area.outputs.trafficSignalAssemblies,
|
trafficSignalAssemblies: area.outputs.trafficSignalAssemblies,
|
||||||
trafficSignals: area.outputs.trafficSignals,
|
trafficSignals: area.outputs.packageTrafficSignals,
|
||||||
gpkg: area.outputs.gpkg,
|
gpkg: area.outputs.gpkg,
|
||||||
qgisProject: area.outputs.qgisProject,
|
qgisProject: area.outputs.qgisProject,
|
||||||
qgisPreview: optionalExpectedFile(area.outputs.qgisPreview),
|
qgisPreview: optionalExpectedFile(area.outputs.qgisPreview),
|
||||||
@@ -421,7 +421,7 @@ function stageManifestStatus(area, configPath = null) {
|
|||||||
geojsonDir: area.outputs.geojsonDir,
|
geojsonDir: area.outputs.geojsonDir,
|
||||||
...sceneGeojsonFiles(area),
|
...sceneGeojsonFiles(area),
|
||||||
trafficSignalAssemblies: area.outputs.trafficSignalAssemblies,
|
trafficSignalAssemblies: area.outputs.trafficSignalAssemblies,
|
||||||
trafficSignals: area.outputs.trafficSignals,
|
trafficSignals: area.outputs.packageTrafficSignals,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -433,7 +433,7 @@ function stageManifestStatus(area, configPath = null) {
|
|||||||
geojsonDir: area.outputs.geojsonDir,
|
geojsonDir: area.outputs.geojsonDir,
|
||||||
...sceneGeojsonFiles(area),
|
...sceneGeojsonFiles(area),
|
||||||
trafficSignalAssemblies: area.outputs.trafficSignalAssemblies,
|
trafficSignalAssemblies: area.outputs.trafficSignalAssemblies,
|
||||||
trafficSignals: area.outputs.trafficSignals,
|
trafficSignals: area.outputs.packageTrafficSignals,
|
||||||
},
|
},
|
||||||
outputs: {
|
outputs: {
|
||||||
blend: area.outputs.blend,
|
blend: area.outputs.blend,
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ function previewSummary(area) {
|
|||||||
metadataName: "package/manifest.json",
|
metadataName: "package/manifest.json",
|
||||||
routeName: path.relative(area.outputs.areaDir, area.outputs.vehicleRoute).split(path.sep).join("/"),
|
routeName: path.relative(area.outputs.areaDir, area.outputs.vehicleRoute).split(path.sep).join("/"),
|
||||||
vehicleModelName: path.relative(area.outputs.areaDir, area.outputs.vehicleModel).split(path.sep).join("/"),
|
vehicleModelName: path.relative(area.outputs.areaDir, area.outputs.vehicleModel).split(path.sep).join("/"),
|
||||||
trafficSignalsName: path.relative(area.outputs.areaDir, area.outputs.trafficSignals).split(path.sep).join("/"),
|
trafficSignalsName: "package/runtime/traffic-signals.json",
|
||||||
routeSegments: Array.isArray(route.segments) ? route.segments.length : null,
|
routeSegments: Array.isArray(route.segments) ? route.segments.length : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,16 @@
|
|||||||
category: asset.category,
|
category: asset.category,
|
||||||
semantic: asset.role === "layer",
|
semantic: asset.role === "layer",
|
||||||
}));
|
}));
|
||||||
return staticAssets.concat(metadata.previewAssets || []);
|
const runtimeAssets = (metadata.runtime || []).filter((asset) => asset.type !== "traffic-signal-anchors").map((asset) => ({
|
||||||
|
id: asset.id,
|
||||||
|
label: asset.id,
|
||||||
|
type: "model",
|
||||||
|
url: new URL(asset.uri, new URL(config.metadataName, window.location.href)).href,
|
||||||
|
enabled: true,
|
||||||
|
category: asset.type === "traffic-signal-lenses" ? "dynamic" : asset.type === "traffic-signal-countdown" ? "countdown" : asset.type,
|
||||||
|
phaseGroup: asset.phaseGroup,
|
||||||
|
}));
|
||||||
|
return staticAssets.concat(runtimeAssets, metadata.previewAssets || []);
|
||||||
}
|
}
|
||||||
const assets = Array.isArray(metadata.assets) && metadata.assets.length
|
const assets = Array.isArray(metadata.assets) && metadata.assets.length
|
||||||
? metadata.assets
|
? metadata.assets
|
||||||
|
|||||||
@@ -49,6 +49,17 @@ function validateManifest(manifest, packageDir, options = {}) {
|
|||||||
if (!resolved.startsWith(`${path.resolve(packageDir)}${path.sep}`) || !fs.existsSync(resolved)) fail(`asset '${asset.id}' is missing from package: ${asset.uri}`);
|
if (!resolved.startsWith(`${path.resolve(packageDir)}${path.sep}`) || !fs.existsSync(resolved)) fail(`asset '${asset.id}' is missing from package: ${asset.uri}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (manifest.runtime !== undefined && !Array.isArray(manifest.runtime)) fail("runtime must be an array");
|
||||||
|
for (const runtime of manifest?.runtime || []) {
|
||||||
|
if (!runtime || typeof runtime.id !== "string" || !runtime.id) { fail("runtime asset id is required"); continue; }
|
||||||
|
if (ids.has(runtime.id)) fail(`duplicate asset id '${runtime.id}'`); ids.add(runtime.id);
|
||||||
|
if (typeof runtime.type !== "string" || !runtime.type) fail(`runtime '${runtime.id}' type is required`);
|
||||||
|
try { relativeUri(runtime.uri); } catch (error) { fail(error.message); continue; }
|
||||||
|
if (packageDir && options.requireFiles !== false) {
|
||||||
|
const resolved = path.resolve(packageDir, runtime.uri);
|
||||||
|
if (!resolved.startsWith(`${path.resolve(packageDir)}${path.sep}`) || !fs.existsSync(resolved)) fail(`runtime '${runtime.id}' is missing from package: ${runtime.uri}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (sceneCount !== 1) fail("exactly one scene asset is required");
|
if (sceneCount !== 1) fail("exactly one scene asset is required");
|
||||||
if (errors.length) throw new Error(`Invalid asset package manifest: ${errors.join("; ")}`);
|
if (errors.length) throw new Error(`Invalid asset package manifest: ${errors.join("; ")}`);
|
||||||
return manifest;
|
return manifest;
|
||||||
@@ -60,6 +71,11 @@ function addIntegrity(manifest, packageDir) {
|
|||||||
const buffer = fs.readFileSync(file);
|
const buffer = fs.readFileSync(file);
|
||||||
asset.integrity = { bytes: buffer.length, sha256: crypto.createHash("sha256").update(buffer).digest("hex") };
|
asset.integrity = { bytes: buffer.length, sha256: crypto.createHash("sha256").update(buffer).digest("hex") };
|
||||||
}
|
}
|
||||||
|
for (const runtime of manifest.runtime || []) {
|
||||||
|
const file = path.resolve(packageDir, runtime.uri);
|
||||||
|
const buffer = fs.readFileSync(file);
|
||||||
|
runtime.integrity = { bytes: buffer.length, sha256: crypto.createHash("sha256").update(buffer).digest("hex") };
|
||||||
|
}
|
||||||
return manifest;
|
return manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ const { SCHEMA, validateManifest } = require("./lib/package-contract");
|
|||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "asset-package-"));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "asset-package-"));
|
||||||
fs.mkdirSync(path.join(dir, "models"));
|
fs.mkdirSync(path.join(dir, "models"));
|
||||||
fs.writeFileSync(path.join(dir, "models", "area.glb"), "glb");
|
fs.writeFileSync(path.join(dir, "models", "area.glb"), "glb");
|
||||||
const valid = { schema: SCHEMA, packageVersion: "1.0.0", areaId: "area", coordinateSystem: { axes: "ENU", units: "meters", x: "east", y: "north", z: "up" }, placement: { longitude: 120, latitude: 30, height: 0, headingCorrectionDegrees: -90 }, bounds: { minLon: 119, minLat: 29, maxLon: 121, maxLat: 31 }, assets: [{ id: "main", role: "scene", category: "scene", uri: "models/area.glb", defaultLoad: true }] };
|
fs.writeFileSync(path.join(dir, "runtime.json"), "{}");
|
||||||
|
const valid = { schema: SCHEMA, packageVersion: "1.0.0", areaId: "area", coordinateSystem: { axes: "ENU", units: "meters", x: "east", y: "north", z: "up" }, placement: { longitude: 120, latitude: 30, height: 0, headingCorrectionDegrees: -90 }, bounds: { minLon: 119, minLat: 29, maxLon: 121, maxLat: 31 }, assets: [{ id: "main", role: "scene", category: "scene", uri: "models/area.glb", defaultLoad: true }], runtime: [{ id: "traffic-signals", type: "traffic-signal-anchors", uri: "runtime.json" }] };
|
||||||
assert.doesNotThrow(() => validateManifest(valid, dir));
|
assert.doesNotThrow(() => validateManifest(valid, dir));
|
||||||
for (const change of [
|
for (const change of [
|
||||||
(m) => { m.schema = "bad"; }, (m) => { m.assets[0].uri = "../area.glb"; },
|
(m) => { m.schema = "bad"; }, (m) => { m.assets[0].uri = "../area.glb"; },
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
"use strict";
|
"use strict";
|
||||||
const assert = require("assert");
|
const assert = require("assert");
|
||||||
const { resolveAsset, placement } = require("../examples/asset-package-resolver");
|
const { resolveAsset, resolveRuntime, placement } = require("../examples/asset-package-resolver");
|
||||||
const manifest = { coordinateSystem: { axes: "ENU" }, placement: { longitude: 120, latitude: 30, height: 2, headingCorrectionDegrees: -90 }, assets: [{ id: "main", uri: "models/a.glb" }, { id: "roads", uri: "models/roads.glb" }] };
|
const manifest = { coordinateSystem: { axes: "ENU" }, placement: { longitude: 120, latitude: 30, height: 2, headingCorrectionDegrees: -90 }, assets: [{ id: "main", uri: "models/a.glb" }, { id: "roads", uri: "models/roads.glb" }], runtime: [{ id: "traffic-signals", uri: "runtime/traffic-signals.json" }] };
|
||||||
assert.equal(resolveAsset("https://consumer.example/copied/manifest.json", manifest), "https://consumer.example/copied/models/a.glb");
|
assert.equal(resolveAsset("https://consumer.example/copied/manifest.json", manifest), "https://consumer.example/copied/models/a.glb");
|
||||||
assert.equal(resolveAsset("https://consumer.example/copied/manifest.json", manifest, "roads"), "https://consumer.example/copied/models/roads.glb");
|
assert.equal(resolveAsset("https://consumer.example/copied/manifest.json", manifest, "roads"), "https://consumer.example/copied/models/roads.glb");
|
||||||
|
assert.equal(resolveRuntime("https://consumer.example/copied/manifest.json", manifest, "traffic-signals"), "https://consumer.example/copied/runtime/traffic-signals.json");
|
||||||
assert.equal(placement(manifest).coordinateSystem.axes, "ENU");
|
assert.equal(placement(manifest).coordinateSystem.axes, "ENU");
|
||||||
console.log("Asset package example resolver tests passed.");
|
console.log("Asset package example resolver tests passed.");
|
||||||
|
|||||||
@@ -257,6 +257,8 @@ assert.match(html, /data-view-mode="inspect"/);
|
|||||||
assert.match(html, /id="semanticToggles" class="control-subgroup hidden"/);
|
assert.match(html, /id="semanticToggles" class="control-subgroup hidden"/);
|
||||||
|
|
||||||
const previewRuntime = fs.readFileSync(path.join(__dirname, "lib", "cesium-preview.js"), "utf8");
|
const previewRuntime = fs.readFileSync(path.join(__dirname, "lib", "cesium-preview.js"), "utf8");
|
||||||
|
const buildAreaSource = fs.readFileSync(path.join(__dirname, "build-area.js"), "utf8");
|
||||||
|
assert.match(buildAreaSource, /vehicleModelNames\.map\(\(name\) => `_preview\/\$\{name\}`\)/);
|
||||||
const countdownFont = path.join(__dirname, "..", "assets", "fonts", "7LED-1.ttf");
|
const countdownFont = path.join(__dirname, "..", "assets", "fonts", "7LED-1.ttf");
|
||||||
assert.ok(fs.existsSync(countdownFont), "7LED countdown font must be versioned with the project");
|
assert.ok(fs.existsSync(countdownFont), "7LED countdown font must be versioned with the project");
|
||||||
assert.doesNotMatch(previewRuntime, /cylinder: \{ length: 6\.7/);
|
assert.doesNotMatch(previewRuntime, /cylinder: \{ length: 6\.7/);
|
||||||
|
|||||||
Reference in New Issue
Block a user