diff --git a/server/catalog-server.mjs b/server/catalog-server.mjs index 7d46b9c..7a6e1da 100644 --- a/server/catalog-server.mjs +++ b/server/catalog-server.mjs @@ -637,15 +637,26 @@ function promoteLegacyGridLodProfiles(settings) { selectionOutlineOpacityPercent: _selectionOutlineOpacityPercent, ...legacyFallback } = fallback; + const stepKm = settings[`gridLod${number}StepKm`] ?? fallback.stepKm; + const legacyRadiusKm = settings.gridRadiusKm ?? fallback.radiusKm; + // The flat v1 setting described one uniform grid and could legitimately + // be much larger than a fine LOD band can render. Migrate it as an upper + // bound per band so an existing application remains readable while the + // new five-band workload stays within its explicit safety limit. + const radiusKm = Math.min( + legacyRadiusKm, + fallback.radiusKm, + stepKm * MAX_LOCAL_GRID_INDEX, + ); return { ...legacyFallback, maxHeightKm: settings[`gridLod${number}MaxHeightKm`] ?? fallback.maxHeightKm, - stepKm: settings[`gridLod${number}StepKm`] ?? fallback.stepKm, + stepKm, mode: settings[`gridLod${number}Mode`] ?? fallback.mode, heightMeters: settings.gridHeightMeters ?? fallback.heightMeters, max3dViewAngleDegrees: settings.gridMax3dViewAngleDegrees ?? fallback.max3dViewAngleDegrees, tileSizeKm: settings.gridTileSizeKm ?? fallback.tileSizeKm, - radiusKm: settings.gridRadiusKm ?? fallback.radiusKm, + radiusKm, lineDiameterMeters: settings.gridLineDiameterMeters ?? fallback.lineDiameterMeters, lineColor: settings.gridColor ?? fallback.lineColor, lineOpacity: settings.gridOpacity ?? fallback.lineOpacity, @@ -658,7 +669,10 @@ function promoteLegacyGridLodProfiles(settings) { crossesWidthMeters: settings.gridCrossesWidthMeters ?? fallback.crossesWidthMeters, crossesColor: settings.gridCrossesColor ?? fallback.crossesColor, crossesOpacity: settings.gridCrossesOpacity ?? fallback.crossesOpacity, - graticuleLineWidthPx: settings.gridLineWidth ?? fallback.graticuleLineWidthPx, + graticuleLineWidthPx: Math.min( + 3, + Math.max(1, settings.gridLineWidth ?? fallback.graticuleLineWidthPx), + ), graticuleColor: settings.gridColor ?? fallback.graticuleColor, graticuleOpacity: settings.gridOpacity ?? fallback.graticuleOpacity, }; diff --git a/server/map-grid-persistence.test.mjs b/server/map-grid-persistence.test.mjs index 5ad8ffb..82ec0fd 100644 --- a/server/map-grid-persistence.test.mjs +++ b/server/map-grid-persistence.test.mjs @@ -98,6 +98,32 @@ test("flat layouts migrate mode-aware extensions while explicit invalid hierarch ); }); +test("Robot2B legacy radius migrates per LOD without hiding the application", () => { + const legacyRobot2BSettings = { + gridHeightMeters: 500, + gridLod1MaxHeightKm: 10, + gridLod1StepKm: 1, + gridLod2MaxHeightKm: 50, + gridLod2StepKm: 5, + gridLod3StepKm: 25, + gridRadiusKm: 1_000, + gridLineWidth: 4, + gridColor: "#f5f5f5", + gridOpacity: 12, + gridDotsEnabled: true, + gridDotsSize: 7, + gridDotsColor: "#ffffff", + gridDotsOpacity: 58, + }; + const migrated = structuredClone(serverGrid.validateGridLodProfiles( + serverGrid.promoteLegacyGridLodProfiles(legacyRobot2BSettings), + )); + + assert.deepEqual(migrated.map((profile) => profile.radiusKm), [50, 1_000, 1_000, 1_000, 100]); + assert.ok(migrated.every((profile) => profile.radiusKm / profile.stepKm <= 512)); + assert.ok(migrated.every((profile) => profile.graticuleLineWidthPx === 3)); +}); + test("Foundry MCP exposes every persisted sector-v2 profile field", () => { for (const key of extensionKeys) assert.match(mcpSource, new RegExp(`\\b${key}: \\{`)); });