feat(module-studio): add configurable map page
This commit is contained in:
@@ -13,6 +13,8 @@ const icons = await parse("registry/icons.json");
|
||||
const pages = await parse("registry/pages.json");
|
||||
const applicationManifestSchema = await parse("registry/schemas/application-manifest-v0.1.schema.json");
|
||||
const designProfileSchema = await parse("registry/schemas/design-profile-v0.1.schema.json");
|
||||
const mapSceneFixtureSchema = await parse("registry/schemas/map-scene-fixture-v0.1.schema.json");
|
||||
const mapLodPolicySchema = await parse("registry/schemas/map-lod-policy-v0.1.schema.json");
|
||||
|
||||
const failures = [];
|
||||
const requireValue = (condition, message) => {
|
||||
@@ -30,8 +32,12 @@ requireValue(pages.schemaVersion === "0.1.0", "page registry schemaVersion must
|
||||
requireValue(Array.isArray(pages.templates) && pages.templates.length >= 1, "page registry must contain at least one template");
|
||||
requireValue(registry.applicationManifestSchema === "schemas/application-manifest-v0.1.schema.json", "application manifest schema must be registered");
|
||||
requireValue(registry.designProfileSchema === "schemas/design-profile-v0.1.schema.json", "design profile schema must be registered");
|
||||
requireValue(registry.mapSceneFixtureSchema === "schemas/map-scene-fixture-v0.1.schema.json", "map scene fixture schema must be registered");
|
||||
requireValue(registry.mapLodPolicySchema === "schemas/map-lod-policy-v0.1.schema.json", "map LOD policy schema must be registered");
|
||||
requireValue(applicationManifestSchema.properties?.schemaVersion?.const === "0.1.0", "application manifest schema version must be 0.1.0");
|
||||
requireValue(designProfileSchema.properties?.schemaVersion?.const === "0.1.0", "design profile schema version must be 0.1.0");
|
||||
requireValue(mapSceneFixtureSchema.properties?.schemaVersion?.const === "0.1.0", "map scene fixture schema version must be 0.1.0");
|
||||
requireValue(mapLodPolicySchema.properties?.schemaVersion?.const === "0.1.0", "map LOD policy schema version must be 0.1.0");
|
||||
|
||||
const ids = components.components.map((component) => component.id);
|
||||
requireValue(new Set(ids).size === ids.length, "component ids must be unique");
|
||||
@@ -50,6 +56,82 @@ for (const template of pages.templates) {
|
||||
requireValue(Array.isArray(template.features), `page template features are missing: ${template.id ?? "unknown"}`);
|
||||
requireValue(Array.isArray(template.slots), `page template slots are missing: ${template.id ?? "unknown"}`);
|
||||
requireValue(Array.isArray(template.actions), `page template actions are missing: ${template.id ?? "unknown"}`);
|
||||
|
||||
if (template.contracts) {
|
||||
requireValue(typeof template.contracts.fixtureSchema === "string", `page template fixture schema is missing: ${template.id ?? "unknown"}`);
|
||||
requireValue(Array.isArray(template.contracts.acceptanceFixtures) && template.contracts.acceptanceFixtures.length >= 1, `page template acceptance fixtures are missing: ${template.id ?? "unknown"}`);
|
||||
|
||||
for (const fixturePath of template.contracts.acceptanceFixtures ?? []) {
|
||||
let fixture;
|
||||
try {
|
||||
fixture = await parse(fixturePath);
|
||||
} catch {
|
||||
failures.push(`missing or invalid acceptance fixture: ${fixturePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
requireValue(fixture.schemaVersion === "0.1.0", `unsupported fixture schema: ${fixturePath}`);
|
||||
requireValue(fixture.template?.id === template.id && fixture.template?.version === template.version, `fixture template mismatch: ${fixturePath}`);
|
||||
requireValue(["ready", "loading", "empty", "stale", "error", "offline"].includes(fixture.state), `unsupported fixture state: ${fixturePath}`);
|
||||
requireValue(Array.isArray(fixture.capabilities), `fixture capabilities are missing: ${fixturePath}`);
|
||||
requireValue(Array.isArray(fixture.styleProfiles), `fixture style profiles are missing: ${fixturePath}`);
|
||||
requireValue(Array.isArray(fixture.acceptance) && fixture.acceptance.length >= 1, `fixture acceptance checks are missing: ${fixturePath}`);
|
||||
|
||||
const collections = ["layers", "places", "movingObjects", "stations", "routes", "tracks", "zones"];
|
||||
requireValue(collections.every((name) => Array.isArray(fixture.scene?.[name])), `fixture scene collections are incomplete: ${fixturePath}`);
|
||||
|
||||
const entities = collections.flatMap((name) => fixture.scene?.[name] ?? []);
|
||||
const entityIds = entities.map((entity) => entity.id);
|
||||
requireValue(entityIds.every(Boolean), `fixture entity id is missing: ${fixturePath}`);
|
||||
requireValue(new Set(entityIds).size === entityIds.length, `fixture entity ids must be unique: ${fixturePath}`);
|
||||
|
||||
const styleIds = fixture.styleProfiles?.map((style) => style.id) ?? [];
|
||||
requireValue(styleIds.every(Boolean), `fixture style profile id is missing: ${fixturePath}`);
|
||||
requireValue(new Set(styleIds).size === styleIds.length, `fixture style profile ids must be unique: ${fixturePath}`);
|
||||
|
||||
const referencedStyleIds = entities.flatMap((entity) => [entity.styleProfileId, entity.pinStyleProfileId, entity.label?.styleProfileId]).filter(Boolean);
|
||||
requireValue(referencedStyleIds.every((id) => styleIds.includes(id)), `fixture references an unknown style profile: ${fixturePath}`);
|
||||
|
||||
const selectedEntityId = fixture.selection?.entityId;
|
||||
requireValue(selectedEntityId === null || entityIds.includes(selectedEntityId), `fixture selection references an unknown entity: ${fixturePath}`);
|
||||
|
||||
for (const layer of fixture.scene?.layers ?? []) {
|
||||
if (!layer.lodLevels) continue;
|
||||
const levels = [...layer.lodLevels].sort((a, b) => a.minRange - b.minRange);
|
||||
requireValue(levels.every((level) => level.maxRange > level.minRange), `fixture LOD range is invalid: ${fixturePath}#${layer.id}`);
|
||||
requireValue(levels.every((level, index) => index === 0 || level.minRange === levels[index - 1].maxRange), `fixture LOD ranges must be contiguous: ${fixturePath}#${layer.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (template.contracts.lodPolicies) {
|
||||
let policySet;
|
||||
try {
|
||||
policySet = await parse(template.contracts.lodPolicies);
|
||||
} catch {
|
||||
failures.push(`missing or invalid LOD policy set: ${template.contracts.lodPolicies}`);
|
||||
}
|
||||
|
||||
if (policySet) {
|
||||
requireValue(policySet.schemaVersion === "0.1.0", `unsupported LOD policy schema: ${template.contracts.lodPolicies}`);
|
||||
requireValue(policySet.template?.id === template.id && policySet.template?.version === template.version, `LOD policy template mismatch: ${template.contracts.lodPolicies}`);
|
||||
requireValue(policySet.metric === "camera-range-meters", `LOD policy metric must be provider-neutral camera range: ${template.contracts.lodPolicies}`);
|
||||
requireValue(policySet.intervalRule === "min-inclusive-max-exclusive", `LOD interval rule is unsupported: ${template.contracts.lodPolicies}`);
|
||||
requireValue(Array.isArray(policySet.policies) && policySet.policies.length >= 1, `LOD policies are missing: ${template.contracts.lodPolicies}`);
|
||||
|
||||
const policyIds = policySet.policies?.map((policy) => policy.id) ?? [];
|
||||
requireValue(new Set(policyIds).size === policyIds.length, `LOD policy ids must be unique: ${template.contracts.lodPolicies}`);
|
||||
|
||||
for (const policy of policySet.policies ?? []) {
|
||||
const bands = [...(policy.bands ?? [])].sort((a, b) => a.minRange - b.minRange);
|
||||
requireValue(bands.length >= 1, `LOD policy bands are missing: ${policy.id}`);
|
||||
requireValue(new Set(bands.map((band) => band.id)).size === bands.length, `LOD band ids must be unique: ${policy.id}`);
|
||||
requireValue(bands.every((band) => band.maxRange > band.minRange), `LOD range is invalid: ${policy.id}`);
|
||||
requireValue(bands.every((band, index) => index === 0 || band.minRange === bands[index - 1].maxRange), `LOD ranges must be contiguous: ${policy.id}`);
|
||||
requireValue(typeof policy.hysteresisRatio === "number" && policy.hysteresisRatio >= 0 && policy.hysteresisRatio <= 0.5, `LOD hysteresis ratio is invalid: ${policy.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const component of components.components) {
|
||||
|
||||
Reference in New Issue
Block a user