2026-05-24 22:52:22 +09:00
import { generateMapAsync } from "./mapGenerator.js" ;
2026-05-20 13:50:56 +09:00
import { drawMap } from "./renderer.js" ;
2026-05-24 17:38:51 +09:00
import { landuseLabel } from "./landuseCodes.js" ;
2026-05-28 19:37:28 +09:00
import { CELL _SIZE , MAP _H , MAP _W } from "./mapUtils.js" ;
2026-05-28 23:51:55 +09:00
import { clampCameraToWorld , createInitialCamera , createWorldMap , ensureWorldPaddingForCamera } from "./worldMap.js" ;
2026-05-28 19:37:28 +09:00
import { getViewportMap } from "./worldViewport.js" ;
import { PATCH _MIN _AREA , PATCH _MIN _HEIGHT , PATCH _MIN _WIDTH , buildPatchRects , generatePatch , validatePatchRect } from "./mapPatch.js" ;
2026-05-20 13:50:56 +09:00
const modes = [
[ "all" , "All" ] ,
[ "terrain" , "Terrain" ] ,
[ "modern" , "Modern" ] ,
2026-05-29 23:49:02 +09:00
[ "history" , "Premodern" ] ,
2026-05-20 13:50:56 +09:00
[ "landuse" , "Land Use" ] ,
2026-05-29 23:49:02 +09:00
[ "admin" , "Admin" ] ,
2026-05-20 13:50:56 +09:00
] ;
const state = {
seedText : "114514" ,
2026-05-28 16:45:00 +09:00
generationType : "auto" ,
2026-05-20 13:50:56 +09:00
mode : "all" ,
2026-05-29 23:49:02 +09:00
toolMode : "pan" ,
2026-05-20 13:50:56 +09:00
showFeatures : true ,
showLabels : true ,
map : null ,
2026-05-28 19:37:28 +09:00
world : null ,
camera : { x : 0 , y : 0 } ,
viewportMap : null ,
2026-05-29 14:31:42 +09:00
viewWidth : MAP _W ,
viewHeight : MAP _H ,
2026-05-26 16:56:18 +09:00
hoverEntities : [ ] ,
2026-05-28 19:37:28 +09:00
selectionRect : null ,
2026-05-28 23:51:55 +09:00
patchVariant : 0 ,
zoom : 1 ,
2026-05-28 19:37:28 +09:00
lastPatchResult : null ,
2026-05-29 22:00:42 +09:00
pendingPatch : null ,
2026-05-29 23:49:02 +09:00
generationRuns : [ ] ,
patchRuns : [ ] ,
interactionRuns : [ ] ,
diagnosticLog : [ ] ,
diagnostics : {
worldExpansionCount : 0 ,
lastWorldExpansion : null ,
lastViewport : null ,
lastFeatureCounts : null ,
lastWorkerUsed : null ,
lastWorkerFallbackReason : null ,
lastPatchWorkerKind : null ,
} ,
2026-05-20 13:50:56 +09:00
} ;
const canvas = document . getElementById ( "mapCanvas" ) ;
2026-05-28 00:30:09 +09:00
const canvasShell = document . querySelector ( ".canvas-shell" ) ;
2026-05-20 13:50:56 +09:00
const seedInput = document . getElementById ( "seed" ) ;
2026-05-28 16:45:00 +09:00
const generationTypeInput = document . getElementById ( "generationType" ) ;
2026-05-28 19:37:28 +09:00
const patchTerrainTypeInput = document . getElementById ( "patchTerrainType" ) ;
2026-05-28 23:51:55 +09:00
const patchVariantInput = document . getElementById ( "patchVariant" ) ;
2026-05-28 19:37:28 +09:00
const generatePatchButton = document . getElementById ( "generatePatch" ) ;
2026-05-28 23:51:55 +09:00
const alternativePatchButton = document . getElementById ( "alternativePatch" ) ;
2026-05-28 19:37:28 +09:00
const patchStatusEl = document . getElementById ( "patchStatus" ) ;
2026-05-29 23:49:02 +09:00
const generateMapButton = document . getElementById ( "generateMap" ) ;
2026-05-20 13:50:56 +09:00
const randomSeedButton = document . getElementById ( "randomSeed" ) ;
2026-05-29 23:49:02 +09:00
const toolPanButton = document . getElementById ( "toolPan" ) ;
const toolPatchButton = document . getElementById ( "toolPatch" ) ;
const toolHintEl = document . getElementById ( "toolHint" ) ;
const zoomInButton = document . getElementById ( "zoomIn" ) ;
const zoomOutButton = document . getElementById ( "zoomOut" ) ;
const zoomResetButton = document . getElementById ( "zoomReset" ) ;
const centerMapButton = document . getElementById ( "centerMap" ) ;
const applyPatchButton = document . getElementById ( "applyPatch" ) ;
const discardPatchButton = document . getElementById ( "discardPatch" ) ;
2026-05-20 13:50:56 +09:00
const showFeaturesInput = document . getElementById ( "showFeatures" ) ;
const showLabelsInput = document . getElementById ( "showLabels" ) ;
const modeGrid = document . getElementById ( "modeGrid" ) ;
2026-05-29 23:49:02 +09:00
const mainLegendGrid = document . getElementById ( "mainLegendGrid" ) ;
const floatingLegendGrid = document . getElementById ( "floatingLegendGrid" ) ;
2026-05-20 13:50:56 +09:00
const statsEl = document . getElementById ( "stats" ) ;
2026-05-29 23:49:02 +09:00
const advancedGenerationStatsEl = document . getElementById ( "advancedGenerationStats" ) ;
const advancedGenerationHistoryEl = document . getElementById ( "advancedGenerationHistory" ) ;
const advancedPatchStatsEl = document . getElementById ( "advancedPatchStats" ) ;
const advancedPatchHistoryEl = document . getElementById ( "advancedPatchHistory" ) ;
const advancedInteractionStatsEl = document . getElementById ( "advancedInteractionStats" ) ;
const advancedInteractionHistoryEl = document . getElementById ( "advancedInteractionHistory" ) ;
const advancedViewportDiagnosticsEl = document . getElementById ( "advancedViewportDiagnostics" ) ;
const advancedFeatureCountsEl = document . getElementById ( "advancedFeatureCounts" ) ;
const advancedPatchDiagnosticsEl = document . getElementById ( "advancedPatchDiagnostics" ) ;
const advancedWorkerDiagnosticsEl = document . getElementById ( "advancedWorkerDiagnostics" ) ;
const advancedWorldDiagnosticsEl = document . getElementById ( "advancedWorldDiagnostics" ) ;
const advancedWarningHistoryEl = document . getElementById ( "advancedWarningHistory" ) ;
const copyImportantDataButton = document . getElementById ( "copyImportantData" ) ;
const copyDebugStatusEl = document . getElementById ( "copyDebugStatus" ) ;
2026-05-20 13:50:56 +09:00
const tooltipEl = document . getElementById ( "mapTooltip" ) ;
2026-05-29 14:31:42 +09:00
const selectionSvgEl = document . getElementById ( "mapSelectionSvg" ) ;
2026-05-28 19:37:28 +09:00
const selectionEl = document . getElementById ( "mapSelection" ) ;
2026-05-24 19:33:09 +09:00
const progressEl = document . getElementById ( "generationProgress" ) ;
const progressStageEl = document . getElementById ( "generationProgressStage" ) ;
const progressTimingsEl = document . getElementById ( "generationProgressTimings" ) ;
2026-05-24 22:52:22 +09:00
let generationStartedAt = 0 ;
let generationCurrentStage = "" ;
let generationTimer = null ;
2026-05-29 14:31:42 +09:00
let zoomRedrawRaf = null ;
let zoomSettledTimer = null ;
2026-05-29 22:00:42 +09:00
let zoomVisualState = null ;
2026-05-29 23:49:02 +09:00
let zoomLatencyStartedAt = null ;
2026-05-29 22:00:42 +09:00
let patchWorker = null ;
let patchJobSeq = 0 ;
2026-05-20 13:50:56 +09:00
2026-05-28 19:37:28 +09:00
const dragState = {
mode : null ,
pointerId : null ,
startClientX : 0 ,
startClientY : 0 ,
startCameraX : 0 ,
startCameraY : 0 ,
2026-05-29 18:50:54 +09:00
lastClientX : 0 ,
lastClientY : 0 ,
panRemainderX : 0 ,
panRemainderY : 0 ,
2026-05-28 19:37:28 +09:00
selectStart : null ,
selectEnd : null ,
2026-05-29 14:31:42 +09:00
selectPath : null ,
2026-05-28 19:37:28 +09:00
pendingCamera : null ,
panRaf : null ,
2026-05-29 23:49:02 +09:00
panLatencyStartedAt : null ,
2026-05-28 19:37:28 +09:00
} ;
2026-05-29 22:00:42 +09:00
function displayWorld ( ) {
return state . pendingPatch ? . world || state . world ;
}
function displaySourceMap ( ) {
return displayWorld ( ) ? . sourceMap || state . map ;
}
2026-05-28 19:37:28 +09:00
function activeMap ( ) {
2026-05-29 22:00:42 +09:00
return state . viewportMap || displaySourceMap ( ) ;
2026-05-28 19:37:28 +09:00
}
2026-05-28 00:30:09 +09:00
2026-05-28 23:51:55 +09:00
function clampZoom ( value ) {
const parsed = Number ( value ) ;
if ( ! Number . isFinite ( parsed ) ) return 1 ;
return Math . min ( Math . max ( parsed , 0.55 ) , 2.8 ) ;
}
2026-05-29 14:31:42 +09:00
function viewportSizeForZoom ( zoom = state . zoom ) {
const z = clampZoom ( zoom || 1 ) ;
2026-05-28 23:51:55 +09:00
return {
2026-05-29 14:31:42 +09:00
width : Math . max ( 1 , Math . ceil ( MAP _W / z ) ) ,
height : Math . max ( 1 , Math . ceil ( MAP _H / z ) ) ,
2026-05-28 23:51:55 +09:00
} ;
}
2026-05-29 22:00:42 +09:00
function clampCameraForView ( camera , size = viewportSizeForZoom ( state . zoom ) , world = displayWorld ( ) ) {
return clampCameraToWorld ( camera , world , size ? . width || MAP _W , size ? . height || MAP _H ) ;
2026-05-29 14:31:42 +09:00
}
function syncViewportSize ( ) {
const size = viewportSizeForZoom ( state . zoom ) ;
state . viewWidth = size . width ;
state . viewHeight = size . height ;
return size ;
}
2026-05-29 22:00:42 +09:00
function canvasInteractionRect ( ) {
return zoomVisualState ? . baseRect || canvas . getBoundingClientRect ( ) ;
}
2026-05-29 14:31:42 +09:00
function mapCellScreenSize ( map = activeMap ( ) ) {
2026-05-29 22:00:42 +09:00
const rect = canvasInteractionRect ( ) ;
2026-05-29 14:31:42 +09:00
const mapWidth = Math . max ( 1 , map ? . width || state . viewWidth || MAP _W ) ;
return rect . width ? rect . width / mapWidth : CELL _SIZE * clampZoom ( state . zoom || 1 ) ;
}
2026-05-28 23:51:55 +09:00
function applyCanvasZoom ( ) {
if ( ! canvas ) return ;
state . zoom = clampZoom ( state . zoom || 1 ) ;
2026-05-29 23:49:02 +09:00
const baseWidth = MAP _W * CELL _SIZE ;
const baseHeight = MAP _H * CELL _SIZE ;
const shellRect = canvasShell ? . getBoundingClientRect ( ) ;
const availableWidth = Math . max ( 160 , ( shellRect ? . width || baseWidth ) - 24 ) ;
const availableHeight = Math . max ( 160 , ( shellRect ? . height || baseHeight ) - 24 ) ;
const displayScale = Math . max ( 0.18 , Math . min ( availableWidth / baseWidth , availableHeight / baseHeight ) ) ;
canvas . style . width = ` ${ Math . round ( baseWidth * displayScale ) } px ` ;
canvas . style . height = ` ${ Math . round ( baseHeight * displayScale ) } px ` ;
syncSelectionSvgToCanvas ( ) ;
2026-05-28 23:51:55 +09:00
updateSelectionOverlayFromWorldRect ( ) ;
}
function displayedCellSize ( ) {
2026-05-29 14:31:42 +09:00
return mapCellScreenSize ( ) ;
2026-05-28 23:51:55 +09:00
}
2026-05-29 14:31:42 +09:00
function screenPointToMapPixel ( clientX , clientY , sizeOverride = null ) {
2026-05-29 22:00:42 +09:00
const rect = canvasInteractionRect ( ) ;
2026-05-28 23:51:55 +09:00
if ( ! rect . width || ! rect . height ) return null ;
2026-05-29 14:31:42 +09:00
const map = activeMap ( ) ;
const viewWidth = Math . max ( 1 , sizeOverride ? . width || map ? . width || state . viewWidth || MAP _W ) ;
const viewHeight = Math . max ( 1 , sizeOverride ? . height || map ? . height || state . viewHeight || MAP _H ) ;
const canvasX = ( clientX - rect . left ) * ( ( canvas . width || MAP _W * CELL _SIZE ) / rect . width ) ;
const canvasY = ( clientY - rect . top ) * ( ( canvas . height || MAP _H * CELL _SIZE ) / rect . height ) ;
const cellX = canvasX / Math . max ( 1e-6 , ( canvas . width || MAP _W * CELL _SIZE ) / viewWidth ) ;
const cellY = canvasY / Math . max ( 1e-6 , ( canvas . height || MAP _H * CELL _SIZE ) / viewHeight ) ;
return { x : cellX * CELL _SIZE , y : cellY * CELL _SIZE } ;
2026-05-28 23:51:55 +09:00
}
function mapPixelToScreenPoint ( px , py ) {
2026-05-29 22:00:42 +09:00
const rect = canvasInteractionRect ( ) ;
2026-05-29 14:31:42 +09:00
const map = activeMap ( ) ;
const viewWidth = Math . max ( 1 , map ? . width || state . viewWidth || MAP _W ) ;
const viewHeight = Math . max ( 1 , map ? . height || state . viewHeight || MAP _H ) ;
const canvasX = ( px / CELL _SIZE ) * ( ( canvas . width || MAP _W * CELL _SIZE ) / viewWidth ) ;
const canvasY = ( py / CELL _SIZE ) * ( ( canvas . height || MAP _H * CELL _SIZE ) / viewHeight ) ;
2026-05-28 23:51:55 +09:00
return {
2026-05-29 14:31:42 +09:00
x : canvas . offsetLeft + canvasX * ( rect . width / Math . max ( 1 , canvas . width || MAP _W * CELL _SIZE ) ) ,
y : canvas . offsetTop + canvasY * ( rect . height / Math . max ( 1 , canvas . height || MAP _H * CELL _SIZE ) ) ,
2026-05-28 23:51:55 +09:00
} ;
}
2026-05-29 14:31:42 +09:00
function mapClientToCell ( event , sizeOverride = null ) {
2026-05-28 19:37:28 +09:00
const map = activeMap ( ) ;
2026-05-29 14:31:42 +09:00
if ( ! map && ! sizeOverride ) return null ;
const p = screenPointToMapPixel ( event . clientX , event . clientY , sizeOverride ) ;
2026-05-28 23:51:55 +09:00
if ( ! p ) return null ;
2026-05-28 00:30:09 +09:00
return {
2026-05-28 23:51:55 +09:00
x : Math . floor ( p . x / CELL _SIZE ) ,
y : Math . floor ( p . y / CELL _SIZE ) ,
2026-05-28 00:30:09 +09:00
} ;
}
2026-05-28 19:37:28 +09:00
function viewportCellToWorldCell ( cell ) {
if ( ! cell || ! state . camera ) return null ;
return {
x : Math . round ( state . camera . x || 0 ) + cell . x ,
y : Math . round ( state . camera . y || 0 ) + cell . y ,
} ;
2026-05-28 00:30:09 +09:00
}
2026-05-28 19:37:28 +09:00
function clampCanvasPoint ( event ) {
2026-05-29 22:00:42 +09:00
const rect = canvasInteractionRect ( ) ;
2026-05-28 19:37:28 +09:00
return {
x : Math . min ( Math . max ( event . clientX - rect . left , 0 ) , rect . width ) ,
y : Math . min ( Math . max ( event . clientY - rect . top , 0 ) , rect . height ) ,
} ;
}
2026-05-29 14:31:42 +09:00
function screenPointToWorldCell ( point ) {
const map = activeMap ( ) ;
if ( ! map || ! point ) return null ;
2026-05-29 22:00:42 +09:00
const rect = canvasInteractionRect ( ) ;
2026-05-29 14:31:42 +09:00
if ( ! rect . width || ! rect . height ) return null ;
const viewWidth = Math . max ( 1 , map . width || state . viewWidth || MAP _W ) ;
const viewHeight = Math . max ( 1 , map . height || state . viewHeight || MAP _H ) ;
const canvasX = point . x * ( ( canvas . width || MAP _W * CELL _SIZE ) / rect . width ) ;
const canvasY = point . y * ( ( canvas . height || MAP _H * CELL _SIZE ) / rect . height ) ;
const localX = Math . floor ( ( canvasX / Math . max ( 1e-6 , ( canvas . width || MAP _W * CELL _SIZE ) / viewWidth ) ) ) ;
const localY = Math . floor ( ( canvasY / Math . max ( 1e-6 , ( canvas . height || MAP _H * CELL _SIZE ) / viewHeight ) ) ) ;
const cameraX = Math . round ( state . camera ? . x || 0 ) ;
const cameraY = Math . round ( state . camera ? . y || 0 ) ;
return {
x : cameraX + Math . min ( Math . max ( localX , 0 ) , Math . max ( 0 , map . width - 1 ) ) ,
y : cameraY + Math . min ( Math . max ( localY , 0 ) , Math . max ( 0 , map . height - 1 ) ) ,
} ;
}
function worldCellToOverlayPoint ( point ) {
const cameraX = Math . round ( state . camera ? . x || 0 ) ;
const cameraY = Math . round ( state . camera ? . y || 0 ) ;
const screen = mapPixelToScreenPoint ( ( point . x - cameraX + 0.5 ) * CELL _SIZE , ( point . y - cameraY + 0.5 ) * CELL _SIZE ) ;
return { x : screen . x - canvas . offsetLeft , y : screen . y - canvas . offsetTop } ;
}
function simplifySelectionPath ( points ) {
const out = [ ] ;
for ( const p of points || [ ] ) {
if ( ! out . length || Math . hypot ( out [ out . length - 1 ] . x - p . x , out [ out . length - 1 ] . y - p . y ) >= 6 ) out . push ( p ) ;
}
return out ;
}
function polygonArea ( points ) {
let area = 0 ;
for ( let i = 0 ; i < points . length ; i ++ ) {
const a = points [ i ] ;
const b = points [ ( i + 1 ) % points . length ] ;
area += a . x * b . y - b . x * a . y ;
}
return Math . abs ( area ) * 0.5 ;
}
function selectionPathToShape ( points ) {
const simplified = simplifySelectionPath ( points || [ ] ) ;
if ( simplified . length < 3 ) return null ;
const polygon = simplified . map ( screenPointToWorldCell ) . filter ( Boolean ) ;
if ( polygon . length < 3 ) return null ;
const xs = polygon . map ( ( p ) => p . x ) ;
const ys = polygon . map ( ( p ) => p . y ) ;
return {
kind : "lasso" ,
polygon ,
x0 : Math . min ( ... xs ) ,
y0 : Math . min ( ... ys ) ,
x1 : Math . max ( ... xs ) + 1 ,
y1 : Math . max ( ... ys ) + 1 ,
areaCells : Math . max ( 1 , Math . round ( polygonArea ( polygon ) ) ) ,
} ;
}
2026-05-29 18:50:54 +09:00
function syncSelectionSvgToCanvas ( ) {
if ( ! selectionSvgEl || ! canvas ) return null ;
const rect = canvas . getBoundingClientRect ( ) ;
const width = Math . max ( 1 , rect . width || canvas . clientWidth || canvas . width || 1 ) ;
const height = Math . max ( 1 , rect . height || canvas . clientHeight || canvas . height || 1 ) ;
selectionSvgEl . style . left = ` ${ canvas . offsetLeft } px ` ;
selectionSvgEl . style . top = ` ${ canvas . offsetTop } px ` ;
selectionSvgEl . style . width = ` ${ width } px ` ;
selectionSvgEl . style . height = ` ${ height } px ` ;
selectionSvgEl . setAttribute ( "width" , String ( width ) ) ;
selectionSvgEl . setAttribute ( "height" , String ( height ) ) ;
selectionSvgEl . setAttribute ( "viewBox" , ` 0 0 ${ width } ${ height } ` ) ;
return { width , height } ;
}
2026-05-29 14:31:42 +09:00
function drawSelectionSvg ( points , invalid = false ) {
if ( ! selectionSvgEl ) return ;
2026-05-29 18:50:54 +09:00
const bounds = syncSelectionSvgToCanvas ( ) ;
if ( ! bounds || ! points || points . length < 3 ) {
2026-05-29 14:31:42 +09:00
selectionSvgEl . style . display = "none" ;
selectionSvgEl . innerHTML = "" ;
return ;
}
2026-05-29 18:50:54 +09:00
const pts = points . map ( ( p ) => {
const x = Math . min ( Math . max ( p . x , 0 ) , bounds . width ) ;
const y = Math . min ( Math . max ( p . y , 0 ) , bounds . height ) ;
return ` ${ x } , ${ y } ` ;
} ) . join ( " " ) ;
2026-05-29 14:31:42 +09:00
selectionSvgEl . innerHTML = ` <polygon points=" ${ pts } " /> ` ;
selectionSvgEl . style . display = "block" ;
selectionSvgEl . classList . toggle ( "invalid" , ! ! invalid ) ;
}
function hideSelectionSvg ( ) {
if ( ! selectionSvgEl ) return ;
selectionSvgEl . style . display = "none" ;
selectionSvgEl . innerHTML = "" ;
selectionSvgEl . classList . remove ( "invalid" ) ;
}
2026-05-28 19:37:28 +09:00
function updateSelectionOverlay ( ) {
2026-05-29 14:31:42 +09:00
if ( ! dragState . selectPath ? . length ) return ;
const liveShape = selectionPathToShape ( dragState . selectPath ) ;
const validation = validatePatchRect ( liveShape , state . world ) ;
drawSelectionSvg ( dragState . selectPath , ! validation . ok ) ;
if ( selectionEl ) selectionEl . style . display = "none" ;
2026-05-28 19:37:28 +09:00
if ( generatePatchButton ) generatePatchButton . disabled = true ;
2026-05-28 23:51:55 +09:00
if ( alternativePatchButton ) alternativePatchButton . disabled = true ;
2026-05-28 19:37:28 +09:00
if ( patchStatusEl ) {
2026-05-29 14:31:42 +09:00
const current = validation . rect || liveShape ;
2026-05-28 19:37:28 +09:00
patchStatusEl . textContent = validation . ok
? ` Selected: ${ formatRectSize ( validation . rect ) } . Release to confirm patch selection. `
: ` ${ validation . reason } Current: ${ formatRectSize ( current ) } . ` ;
patchStatusEl . classList . toggle ( "invalid" , ! validation . ok ) ;
}
}
function updateSelectionOverlayFromWorldRect ( ) {
2026-05-29 14:31:42 +09:00
if ( ! state . selectionRect || ! state . camera || ! activeMap ( ) ) return ;
2026-05-28 19:37:28 +09:00
const rect = canvas . getBoundingClientRect ( ) ;
if ( ! rect . width || ! rect . height ) return ;
2026-05-29 14:31:42 +09:00
if ( Array . isArray ( state . selectionRect . polygon ) && state . selectionRect . polygon . length >= 3 ) {
const points = state . selectionRect . polygon . map ( worldCellToOverlayPoint )
. map ( ( p ) => ( { x : Math . min ( Math . max ( p . x , 0 ) , rect . width ) , y : Math . min ( Math . max ( p . y , 0 ) , rect . height ) } ) ) ;
drawSelectionSvg ( points , ! validatePatchRect ( state . selectionRect , state . world ) . ok ) ;
if ( selectionEl ) selectionEl . style . display = "none" ;
return ;
}
2026-05-28 19:37:28 +09:00
const cameraX = Math . round ( state . camera . x || 0 ) ;
const cameraY = Math . round ( state . camera . y || 0 ) ;
2026-05-28 23:51:55 +09:00
const p0 = mapPixelToScreenPoint ( ( state . selectionRect . x0 - cameraX ) * CELL _SIZE , ( state . selectionRect . y0 - cameraY ) * CELL _SIZE ) ;
const p1 = mapPixelToScreenPoint ( ( state . selectionRect . x1 - cameraX ) * CELL _SIZE , ( state . selectionRect . y1 - cameraY ) * CELL _SIZE ) ;
const vx0 = p0 . x - canvas . offsetLeft ;
const vy0 = p0 . y - canvas . offsetTop ;
const vx1 = p1 . x - canvas . offsetLeft ;
const vy1 = p1 . y - canvas . offsetTop ;
2026-05-28 19:37:28 +09:00
const x0 = Math . min ( Math . max ( Math . min ( vx0 , vx1 ) , 0 ) , rect . width ) ;
const y0 = Math . min ( Math . max ( Math . min ( vy0 , vy1 ) , 0 ) , rect . height ) ;
const x1 = Math . min ( Math . max ( Math . max ( vx0 , vx1 ) , 0 ) , rect . width ) ;
const y1 = Math . min ( Math . max ( Math . max ( vy0 , vy1 ) , 0 ) , rect . height ) ;
if ( x1 - x0 < 1 || y1 - y0 < 1 ) {
selectionEl . style . display = "none" ;
2026-05-28 00:30:09 +09:00
return ;
}
2026-05-29 14:31:42 +09:00
hideSelectionSvg ( ) ;
2026-05-28 19:37:28 +09:00
selectionEl . style . display = "block" ;
selectionEl . style . left = ` ${ canvas . offsetLeft + x0 } px ` ;
selectionEl . style . top = ` ${ canvas . offsetTop + y0 } px ` ;
selectionEl . style . width = ` ${ Math . max ( 1 , x1 - x0 ) } px ` ;
selectionEl . style . height = ` ${ Math . max ( 1 , y1 - y0 ) } px ` ;
const validation = validatePatchRect ( state . selectionRect , state . world ) ;
selectionEl . classList . toggle ( "invalid" , ! validation . ok ) ;
}
function formatRectSize ( rect ) {
if ( ! rect ) return "-" ;
const w = Math . max ( 0 , rect . x1 - rect . x0 ) ;
const h = Math . max ( 0 , rect . y1 - rect . y0 ) ;
2026-05-29 14:31:42 +09:00
const area = Math . max ( 0 , rect . areaCells || ( w * h ) ) ;
return ` ${ w } x ${ h } cells / ${ area . toLocaleString ( ) } cells ` ;
2026-05-28 19:37:28 +09:00
}
2026-05-28 23:51:55 +09:00
function normalizePatchVariant ( value ) {
const parsed = Number . parseInt ( value , 10 ) ;
return Number . isFinite ( parsed ) ? Math . max ( 0 , parsed ) >>> 0 : 0 ;
}
function setPatchVariant ( value , { update = true } = { } ) {
state . patchVariant = normalizePatchVariant ( value ) ;
if ( patchVariantInput && patchVariantInput . value !== String ( state . patchVariant ) ) {
patchVariantInput . value = String ( state . patchVariant ) ;
}
if ( update ) updatePatchControls ( ) ;
return state . patchVariant ;
}
function readPatchVariant ( ) {
return setPatchVariant ( patchVariantInput ? . value ? ? state . patchVariant , { update : false } ) ;
}
function resetPatchVariant ( { update = true } = { } ) {
return setPatchVariant ( 0 , { update } ) ;
}
2026-05-28 19:37:28 +09:00
function updatePatchControls ( ) {
2026-05-28 23:51:55 +09:00
const variant = readPatchVariant ( ) ;
2026-05-29 23:49:02 +09:00
const validation = validatePatchRect ( state . selectionRect , state . world ) ;
const hasValidSelection = ! ! validation . ok ;
const hasPreview = ! ! state . pendingPatch ;
if ( generatePatchButton ) generatePatchButton . disabled = ! hasValidSelection ;
if ( alternativePatchButton ) alternativePatchButton . disabled = ! hasValidSelection ;
if ( applyPatchButton ) applyPatchButton . disabled = ! hasPreview ;
if ( discardPatchButton ) discardPatchButton . disabled = ! hasPreview ;
2026-05-28 19:37:28 +09:00
if ( ! patchStatusEl ) return ;
if ( ! state . selectionRect ) {
2026-05-29 23:49:02 +09:00
patchStatusEl . textContent = state . toolMode === "patch"
? ` Right-drag a freeform patch area. Minimum: ${ PATCH _MIN _WIDTH } x ${ PATCH _MIN _HEIGHT } cells and ${ PATCH _MIN _AREA . toLocaleString ( ) } cells. `
: "Right-drag on the map to draw a patch area." ;
2026-05-28 19:37:28 +09:00
patchStatusEl . classList . toggle ( "invalid" , false ) ;
return ;
}
if ( ! validation . ok ) {
patchStatusEl . textContent = ` ${ validation . reason } Current: ${ formatRectSize ( validation . rect || state . selectionRect ) } . ` ;
patchStatusEl . classList . toggle ( "invalid" , true ) ;
return ;
2026-05-28 00:30:09 +09:00
}
2026-05-28 19:37:28 +09:00
const rects = buildPatchRects ( validation . rect , state . world ) ;
2026-05-29 22:00:42 +09:00
const shownPatch = state . pendingPatch ? . result || state . lastPatchResult ;
2026-05-29 23:49:02 +09:00
const previewText = state . pendingPatch
? ` Preview ready: ${ shownPatch ? . label || "candidate" } , variant ${ shownPatch ? . variant ? ? variant } . Use Apply Preview or Discard. `
: shownPatch
? ` Last applied: ${ shownPatch . label || "patch" } , variant ${ shownPatch . variant ? ? "-" } . `
: "" ;
patchStatusEl . textContent = ` Selection: ${ formatRectSize ( validation . rect ) } . Core ${ formatRectSize ( rects . coreRect ) } / write ${ formatRectSize ( rects . writeRect ) } . ${ previewText } ` ;
2026-05-28 19:37:28 +09:00
patchStatusEl . classList . toggle ( "invalid" , false ) ;
}
2026-05-29 23:49:02 +09:00
function setToolMode ( mode ) {
state . toolMode = mode === "patch" ? "patch" : "pan" ;
toolPanButton ? . classList . toggle ( "active" , state . toolMode === "pan" ) ;
toolPatchButton ? . classList . toggle ( "active" , state . toolMode === "patch" ) ;
toolHintEl ? . classList . toggle ( "hidden" , state . toolMode !== "patch" ) ;
canvasShell ? . classList . toggle ( "patch-intent" , state . toolMode === "patch" ) ;
updatePatchControls ( ) ;
}
function setZoomKeepingCenter ( nextZoom ) {
const startedAt = performance . now ( ) ;
const renderWorld = displayWorld ( ) ;
if ( ! renderWorld ) return ;
const oldSize = viewportSizeForZoom ( state . zoom ) ;
const centerWorld = {
x : Math . round ( ( state . camera ? . x || 0 ) + oldSize . width / 2 ) ,
y : Math . round ( ( state . camera ? . y || 0 ) + oldSize . height / 2 ) ,
} ;
state . zoom = clampZoom ( nextZoom ) ;
const nextSize = syncViewportSize ( ) ;
state . camera = clampCameraForView ( {
x : Math . round ( centerWorld . x - nextSize . width / 2 ) ,
y : Math . round ( centerWorld . y - nextSize . height / 2 ) ,
} , nextSize , renderWorld ) ;
redraw ( { fastTerrain : false , allowWorldExpand : false } ) ;
recordInteractionLatency ( "zoom button" , startedAt , { zoom : state . zoom } ) ;
}
function recenterMap ( ) {
const renderWorld = displayWorld ( ) ;
if ( ! renderWorld ) return ;
state . camera = createInitialCamera ( renderWorld ) ;
redraw ( { fastTerrain : false , allowWorldExpand : false } ) ;
}
2026-05-28 19:37:28 +09:00
function clearDragMode ( ) {
dragState . mode = null ;
dragState . pointerId = null ;
dragState . pendingCamera = null ;
2026-05-29 18:50:54 +09:00
dragState . panRemainderX = 0 ;
dragState . panRemainderY = 0 ;
2026-05-28 19:37:28 +09:00
if ( dragState . panRaf != null ) {
cancelAnimationFrame ( dragState . panRaf ) ;
dragState . panRaf = null ;
}
2026-05-29 23:49:02 +09:00
dragState . panLatencyStartedAt = null ;
2026-05-28 19:37:28 +09:00
canvasShell ? . classList . remove ( "panning" , "selecting" ) ;
}
function schedulePanRedraw ( camera ) {
dragState . pendingCamera = camera ;
2026-05-29 23:49:02 +09:00
if ( ! dragState . panLatencyStartedAt ) dragState . panLatencyStartedAt = performance . now ( ) ;
2026-05-28 19:37:28 +09:00
if ( dragState . panRaf != null ) return ;
dragState . panRaf = requestAnimationFrame ( ( ) => {
dragState . panRaf = null ;
if ( ! dragState . pendingCamera ) return ;
const next = dragState . pendingCamera ;
dragState . pendingCamera = null ;
if ( next . x === state . camera . x && next . y === state . camera . y ) return ;
2026-05-29 23:49:02 +09:00
const startedAt = dragState . panLatencyStartedAt || performance . now ( ) ;
dragState . panLatencyStartedAt = null ;
2026-05-28 19:37:28 +09:00
state . camera = next ;
2026-05-29 18:50:54 +09:00
// Do not auto-expand the backing world while a pointer drag is active.
// Expansion shifts world coordinates; doing it mid-drag invalidates the
// pointer-to-camera baseline and can make the viewport appear to jump.
redraw ( { fastTerrain : true , allowWorldExpand : false } ) ;
2026-05-29 23:49:02 +09:00
recordInteractionLatency ( "pan" , startedAt , { zoom : state . zoom , fast : true } ) ;
2026-05-28 19:37:28 +09:00
} ) ;
}
2026-05-29 22:00:42 +09:00
function commitPendingPatch ( { redrawAfter = true } = { } ) {
if ( ! state . pendingPatch ? . world ) return false ;
state . world = state . pendingPatch . world ;
state . map = state . world . sourceMap || state . map ;
state . lastPatchResult = state . pendingPatch . result || state . world . lastPatchResult || state . lastPatchResult ;
state . pendingPatch = null ;
state . viewportMap = null ;
if ( redrawAfter ) {
renderStats ( displaySourceMap ( ) ) ;
redraw ( { fastTerrain : true , allowWorldExpand : false } ) ;
window . setTimeout ( ( ) => redraw ( { fastTerrain : false , allowWorldExpand : false } ) , 80 ) ;
}
return true ;
}
function discardPendingPatch ( { redrawAfter = true } = { } ) {
if ( ! state . pendingPatch ) return false ;
state . pendingPatch = null ;
state . viewportMap = null ;
if ( redrawAfter ) {
renderStats ( displaySourceMap ( ) ) ;
redraw ( { fastTerrain : false , allowWorldExpand : false } ) ;
}
return true ;
}
function hideSelectionOverlay ( options = { } ) {
const commitPreview = options . commitPreview === true ;
if ( commitPreview ) commitPendingPatch ( { redrawAfter : false } ) ;
else if ( options . discardPreview === true ) discardPendingPatch ( { redrawAfter : false } ) ;
2026-05-28 19:37:28 +09:00
dragState . selectStart = null ;
dragState . selectEnd = null ;
2026-05-29 14:31:42 +09:00
dragState . selectPath = null ;
2026-05-28 19:37:28 +09:00
state . selectionRect = null ;
2026-05-28 23:51:55 +09:00
resetPatchVariant ( { update : false } ) ;
2026-05-29 14:31:42 +09:00
hideSelectionSvg ( ) ;
2026-05-28 19:37:28 +09:00
if ( selectionEl ) selectionEl . style . display = "none" ;
2026-05-29 22:00:42 +09:00
state . viewportMap = null ;
renderStats ( displaySourceMap ( ) ) ;
2026-05-28 19:37:28 +09:00
updatePatchControls ( ) ;
2026-05-29 22:00:42 +09:00
if ( commitPreview || options . discardPreview === true ) redraw ( { fastTerrain : false , allowWorldExpand : false } ) ;
2026-05-28 00:30:09 +09:00
}
2026-05-28 19:37:28 +09:00
function selectionPixelsToCells ( start , end ) {
2026-05-29 14:31:42 +09:00
const a = screenPointToWorldCell ( start ) ;
const b = screenPointToWorldCell ( end ) ;
if ( ! a || ! b ) return null ;
2026-05-28 19:37:28 +09:00
return {
2026-05-29 14:31:42 +09:00
x0 : Math . min ( a . x , b . x ) ,
y0 : Math . min ( a . y , b . y ) ,
x1 : Math . max ( a . x , b . x ) + 1 ,
y1 : Math . max ( a . y , b . y ) + 1 ,
2026-05-28 19:37:28 +09:00
} ;
2026-05-28 00:30:09 +09:00
}
2026-05-29 14:31:42 +09:00
function selectionPixelsToShape ( start , end , path = null ) {
if ( Array . isArray ( path ) && path . length >= 3 ) return selectionPathToShape ( path ) ;
return selectionPixelsToCells ( start , end ) ;
}
2026-05-28 19:37:28 +09:00
function handleMapPointerDown ( event ) {
if ( ! state . world || ! canvasShell ) return ;
if ( event . button !== 0 && event . button !== 2 ) return ;
dragState . pointerId = event . pointerId ;
dragState . startClientX = event . clientX ;
dragState . startClientY = event . clientY ;
dragState . startCameraX = state . camera . x ;
dragState . startCameraY = state . camera . y ;
2026-05-29 18:50:54 +09:00
dragState . lastClientX = event . clientX ;
dragState . lastClientY = event . clientY ;
dragState . panRemainderX = 0 ;
dragState . panRemainderY = 0 ;
2026-05-28 19:37:28 +09:00
tooltipEl ? . classList . remove ( "visible" ) ;
if ( event . button === 0 ) {
dragState . mode = "pan" ;
canvasShell . classList . add ( "panning" ) ;
} else {
2026-05-29 23:49:02 +09:00
if ( state . toolMode !== "patch" ) setToolMode ( "patch" ) ;
if ( state . pendingPatch ) discardPendingPatch ( { redrawAfter : false } ) ;
state . selectionRect = null ;
hideSelectionSvg ( ) ;
if ( selectionEl ) selectionEl . style . display = "none" ;
2026-05-28 19:37:28 +09:00
dragState . mode = "select" ;
dragState . selectStart = clampCanvasPoint ( event ) ;
dragState . selectEnd = dragState . selectStart ;
2026-05-29 14:31:42 +09:00
dragState . selectPath = [ dragState . selectStart ] ;
2026-05-28 19:37:28 +09:00
canvasShell . classList . add ( "selecting" ) ;
updateSelectionOverlay ( ) ;
}
canvas . setPointerCapture ? . ( event . pointerId ) ;
2026-05-28 00:30:09 +09:00
event . preventDefault ( ) ;
}
2026-05-28 19:37:28 +09:00
function handleMapPointerMove ( event ) {
if ( ! dragState . mode || dragState . pointerId !== event . pointerId || ! canvasShell ) return ;
tooltipEl ? . classList . remove ( "visible" ) ;
if ( dragState . mode === "pan" ) {
2026-05-29 18:50:54 +09:00
const rect = canvas . getBoundingClientRect ( ) ;
const maxDeltaX = Math . max ( 320 , rect . width * 0.72 ) ;
const maxDeltaY = Math . max ( 240 , rect . height * 0.72 ) ;
const rawDx = event . clientX - dragState . lastClientX ;
const rawDy = event . clientY - dragState . lastClientY ;
dragState . lastClientX = event . clientX ;
dragState . lastClientY = event . clientY ;
// Pointer capture can occasionally deliver a stale/outlier coordinate after
// a tab switch, resize, context-menu gesture, or OS-level event hiccup. A
// single implausibly large delta would otherwise become a large camera jump.
if ( Math . abs ( rawDx ) <= maxDeltaX && Math . abs ( rawDy ) <= maxDeltaY ) {
const cellSize = Math . max ( 1 , displayedCellSize ( ) ) ;
const totalX = dragState . panRemainderX + rawDx / cellSize ;
const totalY = dragState . panRemainderY + rawDy / cellSize ;
const dxCells = totalX < 0 ? Math . ceil ( totalX ) : Math . floor ( totalX ) ;
const dyCells = totalY < 0 ? Math . ceil ( totalY ) : Math . floor ( totalY ) ;
dragState . panRemainderX = totalX - dxCells ;
dragState . panRemainderY = totalY - dyCells ;
if ( dxCells || dyCells ) {
const base = dragState . pendingCamera || state . camera ;
const nextCamera = clampCameraForView ( {
x : base . x - dxCells ,
y : base . y - dyCells ,
} , viewportSizeForZoom ( state . zoom ) ) ;
schedulePanRedraw ( nextCamera ) ;
}
}
2026-05-28 19:37:28 +09:00
} else if ( dragState . mode === "select" ) {
dragState . selectEnd = clampCanvasPoint ( event ) ;
2026-05-29 14:31:42 +09:00
if ( ! dragState . selectPath || Math . hypot ( dragState . selectEnd . x - dragState . selectPath [ dragState . selectPath . length - 1 ] . x , dragState . selectEnd . y - dragState . selectPath [ dragState . selectPath . length - 1 ] . y ) >= 3 ) {
dragState . selectPath = [ ... ( dragState . selectPath || [ ] ) , dragState . selectEnd ] ;
}
2026-05-28 19:37:28 +09:00
updateSelectionOverlay ( ) ;
}
event . preventDefault ( ) ;
}
function handleMapPointerUp ( event ) {
if ( dragState . pointerId !== event . pointerId ) return ;
const wasPanning = dragState . mode === "pan" ;
if ( dragState . mode === "select" ) {
dragState . selectEnd = clampCanvasPoint ( event ) ;
2026-05-29 14:31:42 +09:00
if ( ! dragState . selectPath || dragState . selectPath . length < 2 ) dragState . selectPath = [ dragState . selectStart , dragState . selectEnd ] ;
else dragState . selectPath = [ ... dragState . selectPath , dragState . selectEnd ] ;
const shape = selectionPixelsToShape ( dragState . selectStart , dragState . selectEnd , dragState . selectPath ) ;
2026-05-28 19:37:28 +09:00
const width = Math . abs ( dragState . selectEnd . x - dragState . selectStart . x ) ;
const height = Math . abs ( dragState . selectEnd . y - dragState . selectStart . y ) ;
2026-05-29 14:31:42 +09:00
if ( shape && ( dragState . selectPath . length >= 3 || ( width >= 4 && height >= 4 ) ) ) {
state . selectionRect = shape ;
2026-05-28 23:51:55 +09:00
state . lastPatchResult = null ;
resetPatchVariant ( { update : false } ) ;
2026-05-28 19:37:28 +09:00
updateSelectionOverlayFromWorldRect ( ) ;
updatePatchControls ( ) ;
} else {
hideSelectionOverlay ( ) ;
}
}
canvas . releasePointerCapture ? . ( event . pointerId ) ;
if ( wasPanning && dragState . pendingCamera ) {
state . camera = dragState . pendingCamera ;
dragState . pendingCamera = null ;
}
clearDragMode ( ) ;
2026-05-29 23:49:02 +09:00
if ( wasPanning ) {
const startedAt = performance . now ( ) ;
redraw ( { fastTerrain : false } ) ;
recordInteractionLatency ( "pan settle" , startedAt , { zoom : state . zoom , fast : false } ) ;
}
2026-05-28 00:30:09 +09:00
event . preventDefault ( ) ;
}
2026-05-20 13:50:56 +09:00
function parseSeed ( seedText ) {
const numeric = Number . parseInt ( seedText , 10 ) ;
if ( Number . isFinite ( numeric ) ) return numeric >>> 0 ;
let hash = 2166136261 ;
for ( const ch of seedText ) hash = Math . imul ( hash ^ ch . charCodeAt ( 0 ) , 16777619 ) ;
return hash >>> 0 ;
}
function insideCount ( items ) {
return items . filter ( ( item ) => item . insidePrefecture ) . length ;
}
function outsideCount ( items ) {
return items . length - insideCount ( items ) ;
}
function countText ( items ) {
return ` ${ insideCount ( items ) } / outside ${ outsideCount ( items ) } ` ;
}
2026-05-24 19:33:09 +09:00
function formatMs ( ms ) {
if ( ! Number . isFinite ( ms ) ) return "-" ;
return ms >= 1000 ? ` ${ ( ms / 1000 ) . toFixed ( 2 ) } s ` : ` ${ Math . round ( ms ) } ms ` ;
}
2026-05-29 23:49:02 +09:00
function formatSeconds ( value , digits = 4 ) {
if ( ! Number . isFinite ( value ) ) return "-" ;
return ` ${ value . toFixed ( digits ) } s ` ;
}
function formatNumber ( value , digits = 1 ) {
if ( ! Number . isFinite ( value ) ) return "-" ;
return value . toFixed ( digits ) ;
}
function pushCapped ( history , item , limit = 10 ) {
history . unshift ( item ) ;
if ( history . length > limit ) history . length = limit ;
}
function countTruthyCells ( mask ) {
if ( ! mask || typeof mask . length !== "number" ) return 0 ;
let count = 0 ;
for ( let i = 0 ; i < mask . length ; i ++ ) if ( mask [ i ] ) count ++ ;
return count ;
}
function mapAreaCells ( map ) {
if ( ! map ) return MAP _W * MAP _H ;
const focusedArea = countTruthyCells ( map . focusedPrefectureMask || map . prefectureMask || map . humanRegionMask ) ;
if ( focusedArea > 0 ) return focusedArea ;
return Math . max ( 1 , ( map . width || MAP _W ) * ( map . height || MAP _H ) ) ;
}
function formatAreaCells ( cells ) {
return ` ${ Math . max ( 0 , Math . round ( cells || 0 ) ) . toLocaleString ( ) } cells ` ;
}
function rectAreaCells ( rect ) {
if ( ! rect ) return 0 ;
return Math . max ( 0 , Math . round ( ( rect . x1 - rect . x0 ) * ( rect . y1 - rect . y0 ) ) ) ;
}
function terrainTypeLabel ( value ) {
const option = generationTypeInput ? Array . from ( generationTypeInput . options ) . find ( ( item ) => item . value === value ) : null ;
return option ? . textContent || value || "Auto" ;
}
function secondsPerThousandCells ( totalMs , areaCells ) {
const area = Math . max ( 1 , areaCells || 0 ) ;
return ( totalMs || 0 ) / area ;
}
function numericValues ( items , selector ) {
return ( items || [ ] )
. map ( selector )
. map ( Number )
. filter ( ( value ) => Number . isFinite ( value ) && value >= 0 ) ;
}
function percentile ( values , percentileRank ) {
const sorted = [ ... values ] . sort ( ( a , b ) => a - b ) ;
if ( ! sorted . length ) return NaN ;
if ( sorted . length === 1 ) return sorted [ 0 ] ;
const rank = Math . min ( Math . max ( percentileRank , 0 ) , 100 ) / 100 * ( sorted . length - 1 ) ;
const lower = Math . floor ( rank ) ;
const upper = Math . ceil ( rank ) ;
if ( lower === upper ) return sorted [ lower ] ;
const weight = rank - lower ;
return sorted [ lower ] * ( 1 - weight ) + sorted [ upper ] * weight ;
}
function summarizeValues ( values ) {
if ( ! values . length ) return null ;
const sum = values . reduce ( ( acc , value ) => acc + value , 0 ) ;
return {
count : values . length ,
avg : sum / values . length ,
max : Math . max ( ... values ) ,
p95 : percentile ( values , 95 ) ,
} ;
}
function renderMetricGrid ( container , metrics , emptyText = "No samples yet." ) {
if ( ! container ) return ;
container . innerHTML = "" ;
const visibleMetrics = ( metrics || [ ] ) . filter ( Boolean ) ;
if ( ! visibleMetrics . length ) {
renderEmptyState ( container , emptyText ) ;
return ;
}
for ( const metric of visibleMetrics ) {
const card = document . createElement ( "div" ) ;
card . className = "metric-card" ;
card . innerHTML = `
< span > $ { metric . label } < / s p a n >
< strong > $ { metric . value } < / s t r o n g >
$ { metric . sub ? ` <small> ${ metric . sub } </small> ` : "" }
` ;
container . append ( card ) ;
}
}
function formatPercent ( value , digits = 1 ) {
if ( ! Number . isFinite ( value ) ) return "-" ;
return ` ${ value . toFixed ( digits ) } % ` ;
}
function countArray ( value ) {
return Array . isArray ( value ) ? value . length : 0 ;
}
function countPaths ( paths ) {
return ( paths || [ ] ) . reduce ( ( sum , path ) => sum + ( Array . isArray ( path ) ? path . length : 0 ) , 0 ) ;
}
function renderDiagnosticGrid ( container , metrics , emptyText = "No diagnostics yet." ) {
renderMetricGrid ( container , metrics , emptyText ) ;
}
function renderDiagnosticTable ( container , rows , emptyText = "No diagnostics yet." ) {
if ( ! container ) return ;
container . innerHTML = "" ;
const visibleRows = ( rows || [ ] ) . filter ( Boolean ) ;
if ( ! visibleRows . length ) {
renderEmptyState ( container , emptyText ) ;
return ;
}
for ( const row of visibleRows ) {
const item = document . createElement ( "div" ) ;
item . className = "diagnostic-row" ;
item . innerHTML = `
< span > $ { row . label } < / s p a n >
< strong > $ { row . value } < / s t r o n g >
$ { row . sub ? ` <small> ${ row . sub } </small> ` : "" }
` ;
container . append ( item ) ;
}
}
function collectFeatureCounts ( map = activeMap ( ) ) {
if ( ! map ) return null ;
const roads = [
... ( map . nationalRoads || [ ] ) ,
... ( map . ringRoads || [ ] ) ,
... ( map . externalRoads || [ ] ) ,
... ( map . expressways || [ ] ) ,
... ( map . externalExpressways || [ ] ) ,
... ( map . minorRoads || [ ] ) ,
... ( map . icAccessRoads || [ ] ) ,
] ;
const railways = [
... ( map . railways || [ ] ) ,
... ( map . branchRailways || [ ] ) ,
... ( map . ringRailways || [ ] ) ,
... ( map . externalRailways || [ ] ) ,
] ;
const rivers = [
... ( map . mainRivers || [ ] ) ,
... ( map . tributaryRivers || [ ] ) ,
... ( map . smallStreams || [ ] ) ,
] ;
const settlements = [
... ( map . modernCities || [ ] ) ,
... ( map . satelliteCities || [ ] ) ,
... ( map . villages || [ ] ) ,
... ( map . markets || [ ] ) ,
... ( map . castles || [ ] ) ,
... ( map . ports || [ ] ) ,
... ( map . newTowns || [ ] ) ,
] ;
const adminBorders = [
... ( map . adminBorders || [ ] ) ,
... ( map . prefectureBorder || [ ] ) ,
... ( map . regionalPrefectureBorders || [ ] ) ,
] ;
return {
roads : roads . length ,
roadCells : countPaths ( roads ) ,
railways : railways . length ,
railwayCells : countPaths ( railways ) ,
rivers : rivers . length ,
riverCells : countPaths ( rivers ) ,
settlements : settlements . length ,
stations : countArray ( map . stations ) ,
labels : countArray ( state . hoverEntities ) ,
adminCenters : countArray ( map . adminCenters ) ,
adminBorders : adminBorders . length ,
adminBorderCells : countPaths ( adminBorders ) ,
industrialZones : countArray ( map . industrialZones ) ,
logisticsParks : countArray ( map . logisticsParks ) ,
} ;
}
function recordDiagnosticLog ( level , title , message = "" , meta = { } ) {
pushCapped ( state . diagnosticLog , {
id : Date . now ( ) + Math . random ( ) ,
createdAt : new Date ( ) ,
level : level || "info" ,
title : title || "Diagnostic" ,
message : String ( message || "" ) ,
meta ,
} ) ;
renderAdvancedData ( ) ;
}
function updateRenderDiagnostics ( options = { } , timings = { } ) {
const map = activeMap ( ) ;
const canvasRect = canvas ? . getBoundingClientRect ? . ( ) ;
const viewWidth = Math . max ( 1 , state . viewWidth || map ? . width || MAP _W ) ;
const viewHeight = Math . max ( 1 , state . viewHeight || map ? . height || MAP _H ) ;
state . diagnostics . lastViewport = {
viewWidth ,
viewHeight ,
cells : viewWidth * viewHeight ,
mapWidth : map ? . width || viewWidth ,
mapHeight : map ? . height || viewHeight ,
cssWidth : canvasRect ? . width || 0 ,
cssHeight : canvasRect ? . height || 0 ,
bitmapWidth : canvas ? . width || 0 ,
bitmapHeight : canvas ? . height || 0 ,
zoom : state . zoom || 1 ,
mode : state . mode ,
fast : ! ! options . fastTerrain ,
showFeatures : state . showFeatures && ! options . fastTerrain ,
showLabels : state . showLabels && ! options . fastTerrain ,
viewportMs : timings . viewportMs || 0 ,
hoverMs : timings . hoverMs || 0 ,
drawMs : timings . drawMs || 0 ,
totalRenderMs : timings . totalRenderMs || 0 ,
2026-05-30 03:25:19 +09:00
renderBreakdown : timings . renderBreakdown || null ,
2026-05-29 23:49:02 +09:00
} ;
state . diagnostics . lastFeatureCounts = collectFeatureCounts ( map ) ;
}
function selectionWriteDiagnostics ( ) {
const rect = state . selectionRect ;
const validation = validatePatchRect ( rect , state . world ) ;
const currentSelection = validation . rect || rect ;
const rects = validation . ok ? buildPatchRects ( validation . rect , state . world ) : null ;
const selectedArea = currentSelection ? . areaCells || rectAreaCells ( currentSelection ) ;
const writeArea = rects ? . writeRect ? rectAreaCells ( rects . writeRect ) : 0 ;
const last = state . patchRuns [ 0 ] ;
const lastRatio = last ? . areaCells ? ( last . writeAreaCells || 0 ) / Math . max ( 1 , last . areaCells ) * 100 : NaN ;
return [
{ label : "Current selection" , value : selectedArea ? formatAreaCells ( selectedArea ) : "none" , sub : validation . ok ? "valid" : ( rect ? validation . reason : "no active selection" ) } ,
{ label : "Current write area" , value : writeArea ? formatAreaCells ( writeArea ) : "-" , sub : selectedArea ? ` ${ formatPercent ( writeArea / Math . max ( 1 , selectedArea ) * 100 ) } of selection ` : "requires valid selection" } ,
{ label : "Last patch selection" , value : last ? formatAreaCells ( last . areaCells ) : "-" , sub : last ? ` ${ last . kind || "Patch" } · ${ terrainTypeLabel ( last . terrainType ) } ` : "no patch runs" } ,
{ label : "Last patch write" , value : last ? . writeAreaCells ? formatAreaCells ( last . writeAreaCells ) : "-" , sub : Number . isFinite ( lastRatio ) ? ` ${ formatPercent ( lastRatio ) } of selection ` : "no patch runs" } ,
{ label : "Last patch variant" , value : last ? String ( last . variant ? ? "-" ) : "-" , sub : last ? . label || "no candidate" } ,
] ;
}
function renderViewportDiagnostics ( ) {
const viewport = state . diagnostics . lastViewport ;
2026-05-30 03:25:19 +09:00
const renderRows = viewport ? . renderBreakdown ? [
{ label : "Draw / base" , value : formatMs ( viewport . renderBreakdown . baseTerrain || 0 ) , sub : "terrain raster" } ,
{ label : "Draw / urban" , value : formatMs ( viewport . renderBreakdown . urbanFill || 0 ) , sub : "land-use overlay" } ,
{ label : "Draw / coast" , value : formatMs ( viewport . renderBreakdown . coastline || 0 ) , sub : "coastline vectors" } ,
{ label : "Draw / rivers" , value : formatMs ( viewport . renderBreakdown . rivers || 0 ) , sub : "river paths" } ,
{ label : "Draw / admin" , value : formatMs ( viewport . renderBreakdown . adminBorders || 0 ) , sub : "fills and borders" } ,
{ label : "Draw / transport" , value : formatMs ( viewport . renderBreakdown . transport || 0 ) , sub : "roads and rail" } ,
{ label : "Draw / labels" , value : formatMs ( ( viewport . renderBreakdown . icons || 0 ) + ( viewport . renderBreakdown . labels || 0 ) ) , sub : "icons and text" } ,
] : [ ] ;
2026-05-29 23:49:02 +09:00
renderDiagnosticGrid ( advancedViewportDiagnosticsEl , viewport ? [
{ label : "Viewport" , value : ` ${ viewport . viewWidth } × ${ viewport . viewHeight } ` , sub : ` ${ formatAreaCells ( viewport . cells ) } drawn ` } ,
{ label : "Canvas CSS" , value : ` ${ Math . round ( viewport . cssWidth ) } × ${ Math . round ( viewport . cssHeight ) } ` , sub : "display pixels" } ,
{ label : "Canvas bitmap" , value : ` ${ viewport . bitmapWidth } × ${ viewport . bitmapHeight } ` , sub : "render target" } ,
{ label : "Zoom" , value : ` ${ Number ( viewport . zoom || 1 ) . toFixed ( 2 ) } x ` , sub : viewport . fast ? "fast redraw" : "full redraw" } ,
{ label : "Viewport build" , value : formatMs ( viewport . viewportMs ) , sub : "getViewportMap" } ,
{ label : "Canvas draw" , value : formatMs ( viewport . drawMs ) , sub : "drawMap" } ,
{ label : "Hover index" , value : formatMs ( viewport . hoverMs ) , sub : "labels / hit targets" } ,
{ label : "Render total" , value : formatMs ( viewport . totalRenderMs ) , sub : ` ${ viewport . mode } mode ` } ,
2026-05-30 03:25:19 +09:00
... renderRows ,
2026-05-29 23:49:02 +09:00
] : [ ] , "No viewport render recorded yet." ) ;
const counts = state . diagnostics . lastFeatureCounts ;
renderDiagnosticTable ( advancedFeatureCountsEl , counts ? [
{ label : "Road paths" , value : counts . roads . toLocaleString ( ) , sub : ` ${ counts . roadCells . toLocaleString ( ) } path cells ` } ,
{ label : "Rail paths" , value : counts . railways . toLocaleString ( ) , sub : ` ${ counts . railwayCells . toLocaleString ( ) } path cells ` } ,
{ label : "River paths" , value : counts . rivers . toLocaleString ( ) , sub : ` ${ counts . riverCells . toLocaleString ( ) } path cells ` } ,
{ label : "Settlements" , value : counts . settlements . toLocaleString ( ) , sub : "cities, towns, ports, castles" } ,
{ label : "Stations" , value : counts . stations . toLocaleString ( ) , sub : "rail markers" } ,
{ label : "Hover labels" , value : counts . labels . toLocaleString ( ) , sub : "active hit targets" } ,
{ label : "Admin centers" , value : counts . adminCenters . toLocaleString ( ) , sub : "municipality labels" } ,
{ label : "Admin borders" , value : counts . adminBorders . toLocaleString ( ) , sub : ` ${ counts . adminBorderCells . toLocaleString ( ) } path cells ` } ,
{ label : "Industry / logistics" , value : ( counts . industrialZones + counts . logisticsParks ) . toLocaleString ( ) , sub : ` ${ counts . industrialZones } industrial, ${ counts . logisticsParks } logistics ` } ,
] : [ ] , "No feature counts recorded yet." ) ;
}
function renderPatchDiagnostics ( ) {
renderDiagnosticTable ( advancedPatchDiagnosticsEl , selectionWriteDiagnostics ( ) , "No patch diagnostics yet." ) ;
}
function renderWorkerWorldDiagnostics ( ) {
const workerAvailable = typeof Worker !== "undefined" ;
const workerRows = [
{ label : "Worker API" , value : workerAvailable ? "available" : "unavailable" , sub : "browser capability" } ,
{ label : "Patch worker object" , value : patchWorker ? "active" : "not active" , sub : patchWorker ? "created" : "created on demand" } ,
{ label : "Last patch worker" , value : state . diagnostics . lastWorkerUsed == null ? "-" : ( state . diagnostics . lastWorkerUsed ? "used" : "main thread" ) , sub : state . diagnostics . lastPatchWorkerKind || "no patch run" } ,
{ label : "Fallback reason" , value : state . diagnostics . lastWorkerFallbackReason || "none" , sub : "last worker fallback" } ,
] ;
renderDiagnosticTable ( advancedWorkerDiagnosticsEl , workerRows ) ;
const world = displayWorld ( ) ;
const source = displaySourceMap ( ) ;
const expansion = state . diagnostics . lastWorldExpansion ;
renderDiagnosticTable ( advancedWorldDiagnosticsEl , [
{ label : "World size" , value : world ? ` ${ world . width } × ${ world . height } ` : "-" , sub : world ? formatAreaCells ( world . width * world . height ) : "no world" } ,
{ label : "Source map" , value : source ? ` ${ source . width || MAP _W } × ${ source . height || MAP _H } ` : "-" , sub : source ? formatAreaCells ( ( source . width || MAP _W ) * ( source . height || MAP _H ) ) : "no source" } ,
{ label : "Camera" , value : state . camera ? ` ${ Math . round ( state . camera . x || 0 ) } , ${ Math . round ( state . camera . y || 0 ) } ` : "-" , sub : "world-space origin" } ,
{ label : "World expansions" , value : state . diagnostics . worldExpansionCount . toLocaleString ( ) , sub : expansion ? ` last dx ${ expansion . dx } , dy ${ expansion . dy } ` : "none yet" } ,
{ label : "Pending patch" , value : state . pendingPatch ? "yes" : "no" , sub : state . pendingPatch ? ` ${ terrainTypeLabel ( state . pendingPatch . terrainType ) } · variant ${ state . pendingPatch . variant } ` : "committed world" } ,
] ) ;
}
function renderWarningHistory ( ) {
if ( ! advancedWarningHistoryEl ) return ;
advancedWarningHistoryEl . innerHTML = "" ;
if ( ! state . diagnosticLog . length ) {
renderEmptyState ( advancedWarningHistoryEl , "No warnings or errors recorded yet." ) ;
return ;
}
for ( const entry of state . diagnosticLog ) {
const row = document . createElement ( "div" ) ;
row . className = ` diagnostic-log-row ${ entry . level || "info" } ` ;
const time = entry . createdAt instanceof Date ? entry . createdAt . toLocaleTimeString ( ) : "-" ;
row . innerHTML = `
< span > $ { time } < / s p a n >
< strong > $ { entry . title } < / s t r o n g >
< p > $ { entry . message || "-" } < / p >
` ;
advancedWarningHistoryEl . append ( row ) ;
}
}
function performanceMetricsForRuns ( runs ) {
const totals = summarizeValues ( numericValues ( runs , ( run ) => run . totalMs ) ) ;
const perArea = summarizeValues ( numericValues ( runs , ( run ) => run . secondsPerThousand ) ) ;
if ( ! totals ) return [ ] ;
2026-05-30 03:25:19 +09:00
const hasSmallAreaRuns = ( runs || [ ] ) . some ( ( run ) => Number ( run ? . areaCells ) > 0 && Number ( run . areaCells ) < 1000 ) ;
const perAreaSub = hasSmallAreaRuns ? "area-normalized; small runs include fixed overhead" : "seconds / 1k cells" ;
2026-05-29 23:49:02 +09:00
return [
{ label : "Samples" , value : String ( totals . count ) , sub : "last 10" } ,
{ label : "Avg total" , value : formatMs ( totals . avg ) , sub : "generation time" } ,
{ label : "Max total" , value : formatMs ( totals . max ) , sub : "slowest run" } ,
{ label : "P95 total" , value : formatMs ( totals . p95 ) , sub : "tail latency" } ,
2026-05-30 03:25:19 +09:00
perArea ? { label : "Avg / 1k" , value : formatSeconds ( perArea . avg ) , sub : perAreaSub } : null ,
perArea ? { label : "P95 / 1k" , value : formatSeconds ( perArea . p95 ) , sub : hasSmallAreaRuns ? "fixed overhead dominated below 1k cells" : "area-normalized" } : null ,
2026-05-29 23:49:02 +09:00
] ;
}
function recordGenerationRun ( map , terrainType ) {
if ( ! map ) return ;
const area = mapAreaCells ( map ) ;
const totalMs = Number . isFinite ( map . generationTotalMs )
? map . generationTotalMs
: ( map . generationTimings || [ ] ) . reduce ( ( sum , row ) => sum + ( Number ( row ? . ms ) || 0 ) , 0 ) ;
2026-05-30 03:25:19 +09:00
const featureTimings = ( map . transportDebug ? . featureTimings || [ ] ) . map ( ( row ) => ( {
label : ` Settlements / ${ row . key || "substage" } ` ,
ms : Number ( row . ms ) || 0 ,
} ) ) ;
2026-05-29 23:49:02 +09:00
pushCapped ( state . generationRuns , {
id : Date . now ( ) ,
createdAt : new Date ( ) ,
terrainType : terrainType || "auto" ,
areaCells : area ,
totalMs ,
secondsPerThousand : secondsPerThousandCells ( totalMs , area ) ,
timings : ( map . generationTimings || [ ] ) . map ( ( row ) => ( {
label : row . label || row . key || "Stage" ,
ms : Number ( row . ms ) || 0 ,
2026-05-30 03:25:19 +09:00
} ) ) . concat ( featureTimings ) ,
2026-05-29 23:49:02 +09:00
} ) ;
renderAdvancedData ( ) ;
}
function recordPatchRun ( result , terrainType , selectionRect , variant , meta = { } ) {
if ( ! result ) return ;
const timings = ( result . patchTimings || [ ] ) . map ( ( row ) => ( {
label : row . label || row . key || "Stage" ,
ms : Number ( row . ms ) || 0 ,
} ) ) ;
const totalMs = Number . isFinite ( meta . wallMs )
? meta . wallMs
: timings . reduce ( ( sum , row ) => sum + ( Number ( row . ms ) || 0 ) , 0 ) ;
const selectionArea = result . selectionShape ? . areaCells || selectionRect ? . areaCells || rectAreaCells ( selectionRect ) ;
const writeArea = rectAreaCells ( result . writeRect || result . rects ? . writeRect || selectionRect ) ;
const area = Math . max ( 1 , Math . round ( selectionArea || writeArea || 1 ) ) ;
const writeRatio = writeArea / Math . max ( 1 , area ) ;
const usedWorker = meta . worker === true ;
pushCapped ( state . patchRuns , {
id : Date . now ( ) ,
createdAt : new Date ( ) ,
kind : meta . kind || "Patch preview" ,
terrainType : terrainType || result . terrainType || "auto" ,
variant : Number . isFinite ( variant ) ? variant : result . variant ,
worker : usedWorker ,
label : result . label || "candidate" ,
areaCells : area ,
writeAreaCells : writeArea ,
writeRatio ,
totalMs ,
secondsPerThousand : secondsPerThousandCells ( totalMs , area ) ,
timings ,
} ) ;
state . diagnostics . lastWorkerUsed = usedWorker ;
state . diagnostics . lastPatchWorkerKind = meta . kind || "Patch preview" ;
if ( usedWorker ) state . diagnostics . lastWorkerFallbackReason = null ;
renderAdvancedData ( ) ;
}
function interactionAction ( kind = "" ) {
const label = String ( kind ) . toLowerCase ( ) ;
if ( label . includes ( "pan" ) ) return "pan" ;
if ( label . includes ( "zoom" ) ) return "zoom" ;
return label || "other" ;
}
function interactionPhase ( run ) {
return run . fast ? "fast" : "full" ;
}
function interactionGroupLabel ( action , phase ) {
const actionLabel = action === "pan" ? "Pan" : action === "zoom" ? "Zoom" : action ;
return ` ${ actionLabel } / ${ phase === "fast" ? "fast redraw" : "full redraw" } ` ;
}
function recordInteractionLatency ( kind , startedAt , meta = { } ) {
if ( ! Number . isFinite ( startedAt ) ) return ;
const ms = performance . now ( ) - startedAt ;
if ( ! Number . isFinite ( ms ) || ms < 0 ) return ;
const item = {
id : Date . now ( ) ,
createdAt : new Date ( ) ,
kind ,
ms ,
zoom : Number . isFinite ( meta . zoom ) ? meta . zoom : state . zoom ,
fast : meta . fast === true ,
} ;
item . action = interactionAction ( kind ) ;
item . phase = interactionPhase ( item ) ;
pushCapped ( state . interactionRuns , item ) ;
renderAdvancedData ( ) ;
}
function renderEmptyState ( container , text ) {
if ( ! container ) return ;
container . innerHTML = "" ;
const empty = document . createElement ( "div" ) ;
empty . className = "perf-empty" ;
empty . textContent = text ;
container . append ( empty ) ;
}
function appendRunHistory ( container , runs , options = { } ) {
if ( ! container ) return ;
container . innerHTML = "" ;
if ( ! runs . length ) {
renderEmptyState ( container , options . emptyText || "No runs recorded yet." ) ;
return ;
}
runs . forEach ( ( run , index ) => {
const item = document . createElement ( "details" ) ;
item . className = "perf-run" ;
if ( index === 0 ) item . open = true ;
const summary = document . createElement ( "summary" ) ;
summary . className = options . patch ? "perf-summary patch" : "perf-summary" ;
const name = options . patch
? ` ${ run . kind || "Patch" } · ${ terrainTypeLabel ( run . terrainType ) } `
: terrainTypeLabel ( run . terrainType ) ;
summary . innerHTML = `
< span class = "perf-rank" > # $ { index + 1 } < / s p a n >
< span > $ { name } < / s p a n >
< strong > $ { formatMs ( run . totalMs ) } < / s t r o n g >
< span > $ { formatAreaCells ( run . areaCells ) } $ { options . patch && run . writeAreaCells ? ` / write ${ formatAreaCells ( run . writeAreaCells ) } ` : "" } < / s p a n >
2026-05-30 03:25:19 +09:00
< span > $ { formatSeconds ( run . secondsPerThousand ) } / 1 k cells$ { run . areaCells && run . areaCells < 1000 ? " (fixed overhead)" : "" } < / s p a n >
2026-05-29 23:49:02 +09:00
` ;
const body = document . createElement ( "div" ) ;
body . className = "timing-grid" ;
if ( options . patch ) {
const meta = document . createElement ( "div" ) ;
meta . className = "timing-pill meta" ;
meta . innerHTML = ` <span>Variant</span><strong> ${ run . variant ? ? "-" } </strong> ` ;
const worker = document . createElement ( "div" ) ;
worker . className = "timing-pill meta" ;
worker . innerHTML = ` <span>Worker</span><strong> ${ run . worker ? "yes" : "no" } </strong> ` ;
const ratio = document . createElement ( "div" ) ;
ratio . className = "timing-pill meta" ;
ratio . innerHTML = ` <span>Write / selection</span><strong> ${ formatPercent ( ( run . writeRatio || 0 ) * 100 ) } </strong> ` ;
body . append ( meta , worker , ratio ) ;
}
for ( const row of run . timings ) {
const timing = document . createElement ( "div" ) ;
timing . className = "timing-pill" ;
timing . innerHTML = ` <span> ${ row . label } </span><strong> ${ formatMs ( row . ms ) } </strong> ` ;
body . append ( timing ) ;
}
if ( ! run . timings . length ) {
const empty = document . createElement ( "div" ) ;
empty . className = "perf-empty inline" ;
empty . textContent = "No stage breakdown available." ;
body . append ( empty ) ;
}
item . append ( summary , body ) ;
container . append ( item ) ;
} ) ;
}
function renderGenerationHistory ( ) {
renderMetricGrid ( advancedGenerationStatsEl , performanceMetricsForRuns ( state . generationRuns ) , "No generation runs recorded yet." ) ;
appendRunHistory ( advancedGenerationHistoryEl , state . generationRuns , {
emptyText : "No generation runs recorded yet." ,
} ) ;
}
function renderPatchHistory ( ) {
renderMetricGrid ( advancedPatchStatsEl , performanceMetricsForRuns ( state . patchRuns ) , "No patch generation runs recorded yet." ) ;
appendRunHistory ( advancedPatchHistoryEl , state . patchRuns , {
patch : true ,
emptyText : "No patch generation runs recorded yet." ,
} ) ;
}
function renderInteractionSummary ( ) {
if ( ! advancedInteractionStatsEl ) return ;
advancedInteractionStatsEl . innerHTML = "" ;
if ( ! state . interactionRuns . length ) {
renderEmptyState ( advancedInteractionStatsEl , "No pan or zoom operations recorded yet." ) ;
return ;
}
const groups = new Map ( ) ;
for ( const run of state . interactionRuns ) {
const action = run . action || interactionAction ( run . kind ) ;
const phase = run . phase || interactionPhase ( run ) ;
const key = ` ${ action } : ${ phase } ` ;
if ( ! groups . has ( key ) ) groups . set ( key , { action , phase , values : [ ] } ) ;
groups . get ( key ) . values . push ( run . ms ) ;
}
const table = document . createElement ( "div" ) ;
table . className = "aggregate-table" ;
for ( const group of groups . values ( ) ) {
const stats = summarizeValues ( group . values ) ;
const row = document . createElement ( "div" ) ;
row . className = "aggregate-row" ;
row . innerHTML = `
< span > $ { interactionGroupLabel ( group . action , group . phase ) } < / s p a n >
< small > n = $ { stats . count } < / s m a l l >
< strong > $ { formatMs ( stats . avg ) } < / s t r o n g >
< strong > $ { formatMs ( stats . max ) } < / s t r o n g >
< strong > $ { formatMs ( stats . p95 ) } < / s t r o n g >
` ;
table . append ( row ) ;
}
advancedInteractionStatsEl . append ( table ) ;
}
function renderInteractionHistory ( ) {
if ( ! advancedInteractionHistoryEl ) return ;
advancedInteractionHistoryEl . innerHTML = "" ;
if ( ! state . interactionRuns . length ) {
renderEmptyState ( advancedInteractionHistoryEl , "No pan or zoom operations recorded yet." ) ;
return ;
}
const table = document . createElement ( "div" ) ;
table . className = "interaction-table" ;
for ( const run of state . interactionRuns ) {
const row = document . createElement ( "div" ) ;
row . className = "interaction-row" ;
row . innerHTML = `
< span > $ { run . kind } < / s p a n >
< strong > $ { formatMs ( run . ms ) } < / s t r o n g >
< span > $ { Number ( run . zoom || 1 ) . toFixed ( 2 ) } x < / s p a n >
< span > $ { run . fast ? "fast" : "full" } < / s p a n >
` ;
table . append ( row ) ;
}
advancedInteractionHistoryEl . append ( table ) ;
}
function renderAdvancedData ( ) {
renderGenerationHistory ( ) ;
renderPatchHistory ( ) ;
renderInteractionSummary ( ) ;
renderInteractionHistory ( ) ;
renderViewportDiagnostics ( ) ;
renderPatchDiagnostics ( ) ;
renderWorkerWorldDiagnostics ( ) ;
renderWarningHistory ( ) ;
}
function reportValue ( value ) {
return value == null || value === "" ? "-" : String ( value ) ;
}
function reportRows ( title , rows = [ ] ) {
const lines = [ ` [ ${ title } ] ` ] ;
const visibleRows = ( rows || [ ] ) . filter ( Boolean ) ;
if ( ! visibleRows . length ) {
lines . push ( "- none" ) ;
return lines ;
}
for ( const row of visibleRows ) {
const sub = row . sub ? ` ( ${ row . sub } ) ` : "" ;
lines . push ( ` - ${ row . label } : ${ reportValue ( row . value ) } ${ sub } ` ) ;
}
return lines ;
}
function reportMetrics ( title , runs = [ ] ) {
const lines = reportRows ( title , performanceMetricsForRuns ( runs ) ) ;
if ( ! runs . length ) return lines ;
lines . push ( "Runs:" ) ;
runs . forEach ( ( run , index ) => {
const prefix = run . kind ? ` ${ run . kind } · ` : "" ;
const variant = run . kind ? ` , variant= ${ run . variant ? ? "-" } , worker= ${ run . worker ? "yes" : "no" } ` : "" ;
const write = run . writeAreaCells ? ` , write= ${ formatAreaCells ( run . writeAreaCells ) } , write/selection= ${ formatPercent ( ( run . writeRatio || 0 ) * 100 ) } ` : "" ;
lines . push ( ` ${ index + 1 } . ${ prefix } ${ terrainTypeLabel ( run . terrainType ) } : total= ${ formatMs ( run . totalMs ) } , area= ${ formatAreaCells ( run . areaCells ) } , sec/1000 cells= ${ formatSeconds ( run . secondsPerThousand ) } ${ variant } ${ write } ` ) ;
if ( run . timings ? . length ) {
lines . push ( ` breakdown: ${ run . timings . map ( ( row ) => ` ${ row . label } = ${ formatMs ( row . ms ) } ` ) . join ( ", " ) } ` ) ;
}
} ) ;
return lines ;
}
function interactionGroupsForReport ( ) {
const groups = new Map ( ) ;
for ( const run of state . interactionRuns ) {
const action = run . action || interactionAction ( run . kind ) ;
const phase = run . phase || interactionPhase ( run ) ;
const key = ` ${ action } : ${ phase } ` ;
if ( ! groups . has ( key ) ) groups . set ( key , { action , phase , values : [ ] } ) ;
groups . get ( key ) . values . push ( run . ms ) ;
}
return Array . from ( groups . values ( ) ) . map ( ( group ) => {
const stats = summarizeValues ( group . values ) ;
return {
label : interactionGroupLabel ( group . action , group . phase ) ,
value : ` n= ${ stats . count } , avg= ${ formatMs ( stats . avg ) } , max= ${ formatMs ( stats . max ) } , p95= ${ formatMs ( stats . p95 ) } ` ,
} ;
} ) ;
}
function reportInteractions ( ) {
const lines = reportRows ( "Viewport Interaction Latency" , interactionGroupsForReport ( ) ) ;
if ( ! state . interactionRuns . length ) return lines ;
lines . push ( "Recent operations:" ) ;
state . interactionRuns . forEach ( ( run , index ) => {
lines . push ( ` ${ index + 1 } . ${ run . kind } : ${ formatMs ( run . ms ) } , zoom= ${ Number ( run . zoom || 1 ) . toFixed ( 2 ) } x, phase= ${ run . fast ? "fast" : "full" } ` ) ;
} ) ;
return lines ;
}
function viewportDiagnosticRowsForReport ( ) {
const viewport = state . diagnostics . lastViewport ;
return viewport ? [
{ label : "Viewport" , value : ` ${ viewport . viewWidth } × ${ viewport . viewHeight } ` , sub : ` ${ formatAreaCells ( viewport . cells ) } drawn ` } ,
{ label : "Canvas CSS" , value : ` ${ Math . round ( viewport . cssWidth ) } × ${ Math . round ( viewport . cssHeight ) } ` , sub : "display pixels" } ,
{ label : "Canvas bitmap" , value : ` ${ viewport . bitmapWidth } × ${ viewport . bitmapHeight } ` , sub : "render target" } ,
{ label : "Zoom" , value : ` ${ Number ( viewport . zoom || 1 ) . toFixed ( 2 ) } x ` , sub : viewport . fast ? "fast redraw" : "full redraw" } ,
{ label : "Viewport build" , value : formatMs ( viewport . viewportMs ) , sub : "getViewportMap" } ,
{ label : "Canvas draw" , value : formatMs ( viewport . drawMs ) , sub : "drawMap" } ,
{ label : "Hover index" , value : formatMs ( viewport . hoverMs ) , sub : "labels / hit targets" } ,
{ label : "Render total" , value : formatMs ( viewport . totalRenderMs ) , sub : ` ${ viewport . mode } mode ` } ,
] : [ ] ;
}
function featureCountRowsForReport ( ) {
const counts = state . diagnostics . lastFeatureCounts ;
return counts ? [
{ label : "Road paths" , value : counts . roads . toLocaleString ( ) , sub : ` ${ counts . roadCells . toLocaleString ( ) } path cells ` } ,
{ label : "Rail paths" , value : counts . railways . toLocaleString ( ) , sub : ` ${ counts . railwayCells . toLocaleString ( ) } path cells ` } ,
{ label : "River paths" , value : counts . rivers . toLocaleString ( ) , sub : ` ${ counts . riverCells . toLocaleString ( ) } path cells ` } ,
{ label : "Settlements" , value : counts . settlements . toLocaleString ( ) , sub : "cities, towns, ports, castles" } ,
{ label : "Stations" , value : counts . stations . toLocaleString ( ) , sub : "rail markers" } ,
{ label : "Hover labels" , value : counts . labels . toLocaleString ( ) , sub : "active hit targets" } ,
{ label : "Admin centers" , value : counts . adminCenters . toLocaleString ( ) , sub : "municipality labels" } ,
{ label : "Admin borders" , value : counts . adminBorders . toLocaleString ( ) , sub : ` ${ counts . adminBorderCells . toLocaleString ( ) } path cells ` } ,
{ label : "Industry / logistics" , value : ( counts . industrialZones + counts . logisticsParks ) . toLocaleString ( ) , sub : ` ${ counts . industrialZones } industrial, ${ counts . logisticsParks } logistics ` } ,
] : [ ] ;
}
function workerDiagnosticRowsForReport ( ) {
const workerAvailable = typeof Worker !== "undefined" ;
return [
{ label : "Worker API" , value : workerAvailable ? "available" : "unavailable" , sub : "browser capability" } ,
{ label : "Patch worker object" , value : patchWorker ? "active" : "not active" , sub : patchWorker ? "created" : "created on demand" } ,
{ label : "Last patch worker" , value : state . diagnostics . lastWorkerUsed == null ? "-" : ( state . diagnostics . lastWorkerUsed ? "used" : "main thread" ) , sub : state . diagnostics . lastPatchWorkerKind || "no patch run" } ,
{ label : "Fallback reason" , value : state . diagnostics . lastWorkerFallbackReason || "none" , sub : "last worker fallback" } ,
] ;
}
function worldDiagnosticRowsForReport ( ) {
const world = displayWorld ( ) ;
const source = displaySourceMap ( ) ;
const expansion = state . diagnostics . lastWorldExpansion ;
return [
{ label : "World size" , value : world ? ` ${ world . width } × ${ world . height } ` : "-" , sub : world ? formatAreaCells ( world . width * world . height ) : "no world" } ,
{ label : "Source map" , value : source ? ` ${ source . width || MAP _W } × ${ source . height || MAP _H } ` : "-" , sub : source ? formatAreaCells ( ( source . width || MAP _W ) * ( source . height || MAP _H ) ) : "no source" } ,
{ label : "Camera" , value : state . camera ? ` ${ Math . round ( state . camera . x || 0 ) } , ${ Math . round ( state . camera . y || 0 ) } ` : "-" , sub : "world-space origin" } ,
{ label : "World expansions" , value : state . diagnostics . worldExpansionCount . toLocaleString ( ) , sub : expansion ? ` last dx ${ expansion . dx } , dy ${ expansion . dy } ` : "none yet" } ,
{ label : "Pending patch" , value : state . pendingPatch ? "yes" : "no" , sub : state . pendingPatch ? ` ${ terrainTypeLabel ( state . pendingPatch . terrainType ) } · variant ${ state . pendingPatch . variant } ` : "committed world" } ,
] ;
}
function reportWarnings ( ) {
const lines = [ "[Warnings / Errors]" ] ;
if ( ! state . diagnosticLog . length ) {
lines . push ( "- none" ) ;
return lines ;
}
state . diagnosticLog . forEach ( ( entry , index ) => {
const time = entry . createdAt instanceof Date ? entry . createdAt . toISOString ( ) : "-" ;
lines . push ( ` - ${ index + 1 } . ${ time } ${ String ( entry . level || "info" ) . toUpperCase ( ) } ${ entry . title } : ${ entry . message || "-" } ` ) ;
} ) ;
return lines ;
}
function buildImportantDebugReport ( ) {
const source = displaySourceMap ( ) ;
const summaryRows = getStats ( source ) . map ( ( [ label , value ] ) => ( { label , value } ) ) ;
const contextRows = [
{ label : "Generated at" , value : new Date ( ) . toISOString ( ) } ,
{ label : "Seed" , value : seedInput ? . value || state . seedText || "-" } ,
{ label : "Generation type" , value : terrainTypeLabel ( generationTypeInput ? . value || state . generationType ) , sub : generationTypeInput ? . value || state . generationType } ,
{ label : "Patch terrain" , value : terrainTypeLabel ( patchTerrainTypeInput ? . value || "auto" ) , sub : patchTerrainTypeInput ? . value || "auto" } ,
{ label : "Tool mode" , value : state . toolMode || "-" } ,
{ label : "Display mode" , value : state . mode || "-" } ,
{ label : "Features" , value : state . showFeatures ? "on" : "off" } ,
{ label : "Labels" , value : state . showLabels ? "on" : "off" } ,
] ;
return [
"Prefecture Map Generator — Important Debug Data" ,
"===============================================" ,
... reportRows ( "Context" , contextRows ) ,
"" ,
... reportRows ( "Current Map Summary" , summaryRows ) ,
"" ,
... reportMetrics ( "Full Generation Performance" , state . generationRuns ) ,
"" ,
... reportMetrics ( "Patch / Additional Generation Performance" , state . patchRuns ) ,
"" ,
... reportInteractions ( ) ,
"" ,
... reportRows ( "Viewport Diagnostics" , viewportDiagnosticRowsForReport ( ) ) ,
"" ,
... reportRows ( "Feature Counts" , featureCountRowsForReport ( ) ) ,
"" ,
... reportRows ( "Selection / Write Ratio" , selectionWriteDiagnostics ( ) ) ,
"" ,
... reportRows ( "Worker Diagnostics" , workerDiagnosticRowsForReport ( ) ) ,
"" ,
... reportRows ( "World Diagnostics" , worldDiagnosticRowsForReport ( ) ) ,
"" ,
... reportWarnings ( ) ,
] . join ( "\n" ) ;
}
function setCopyDebugStatus ( text , isError = false ) {
if ( ! copyDebugStatusEl ) return ;
copyDebugStatusEl . textContent = text ;
copyDebugStatusEl . classList . toggle ( "error" , ! ! isError ) ;
window . clearTimeout ( setCopyDebugStatus . timer ) ;
setCopyDebugStatus . timer = window . setTimeout ( ( ) => {
if ( copyDebugStatusEl . textContent === text ) copyDebugStatusEl . textContent = "" ;
copyDebugStatusEl . classList . remove ( "error" ) ;
} , 1800 ) ;
}
function copyTextFallback ( text ) {
const textarea = document . createElement ( "textarea" ) ;
textarea . value = text ;
textarea . setAttribute ( "readonly" , "" ) ;
textarea . style . position = "fixed" ;
textarea . style . left = "-9999px" ;
textarea . style . top = "0" ;
document . body . append ( textarea ) ;
textarea . select ( ) ;
const ok = document . execCommand ( "copy" ) ;
textarea . remove ( ) ;
if ( ! ok ) throw new Error ( "Copy command failed" ) ;
}
async function copyImportantDebugData ( event ) {
event ? . preventDefault ? . ( ) ;
event ? . stopPropagation ? . ( ) ;
const text = buildImportantDebugReport ( ) ;
try {
if ( navigator . clipboard ? . writeText ) await navigator . clipboard . writeText ( text ) ;
else copyTextFallback ( text ) ;
setCopyDebugStatus ( "Copied" ) ;
} catch ( error ) {
console . warn ( "Failed to copy debug data" , error ) ;
try {
copyTextFallback ( text ) ;
setCopyDebugStatus ( "Copied" ) ;
} catch ( fallbackError ) {
console . warn ( "Fallback copy failed" , fallbackError ) ;
setCopyDebugStatus ( "Copy failed" , true ) ;
}
}
}
2026-05-24 19:33:09 +09:00
function renderTimingRows ( timings = [ ] ) {
if ( ! progressTimingsEl ) return ;
progressTimingsEl . innerHTML = "" ;
for ( const row of timings ) {
const item = document . createElement ( "div" ) ;
item . className = "progress-timing-row" ;
const label = document . createElement ( "span" ) ;
label . textContent = row . label ;
const value = document . createElement ( "strong" ) ;
value . textContent = formatMs ( row . ms ) ;
item . append ( label , value ) ;
progressTimingsEl . append ( item ) ;
}
}
function updateGenerationProgress ( event ) {
if ( ! progressEl ) return ;
progressEl . classList . remove ( "hidden" ) ;
2026-05-24 22:52:22 +09:00
if ( event ? . status === "start" ) generationCurrentStage = event . label || "Preparing" ;
2026-05-24 19:33:09 +09:00
if ( progressStageEl ) {
2026-05-24 22:52:22 +09:00
const elapsed = generationStartedAt ? ` / elapsed ${ formatMs ( performance . now ( ) - generationStartedAt ) } ` : "" ;
2026-05-24 19:33:09 +09:00
progressStageEl . textContent = event ? . status === "done"
2026-05-24 22:52:22 +09:00
? ` Completed: ${ event . label } / ${ formatMs ( event . ms ) } ${ elapsed } `
: ` Running: ${ event ? . label || generationCurrentStage || "Preparing" } ${ elapsed } ` ;
2026-05-24 19:33:09 +09:00
}
renderTimingRows ( event ? . timings || [ ] ) ;
}
function setProgressVisible ( visible , message = "Preparing" ) {
if ( ! progressEl ) return ;
progressEl . classList . toggle ( "hidden" , ! visible ) ;
2026-05-24 22:52:22 +09:00
if ( visible ) {
generationStartedAt = performance . now ( ) ;
generationCurrentStage = message ;
if ( generationTimer ) window . clearInterval ( generationTimer ) ;
generationTimer = window . setInterval ( ( ) => {
if ( progressStageEl && ! progressEl . classList . contains ( "hidden" ) ) {
progressStageEl . textContent = ` Running: ${ generationCurrentStage || "Preparing" } / elapsed ${ formatMs ( performance . now ( ) - generationStartedAt ) } ` ;
}
} , 100 ) ;
} else if ( generationTimer ) {
window . clearInterval ( generationTimer ) ;
generationTimer = null ;
}
2026-05-24 19:33:09 +09:00
if ( progressStageEl ) progressStageEl . textContent = message ;
if ( visible ) renderTimingRows ( [ ] ) ;
}
function nextFrame ( ) {
return new Promise ( ( resolve ) => requestAnimationFrame ( ( ) => resolve ( ) ) ) ;
}
2026-05-29 23:49:02 +09:00
function countInside ( items ) {
return ( items || [ ] ) . filter ( ( item ) => item ? . insidePrefecture !== false ) . length ;
}
function totalRailLineCount ( map ) {
return ( map . railways || [ ] ) . length + ( map . branchRailways || [ ] ) . length + ( map . ringRailways || [ ] ) . length + ( map . externalRailways || [ ] ) . length ;
}
function totalRoadLineCount ( map ) {
return ( map . nationalRoads || [ ] ) . length + ( map . ringRoads || [ ] ) . length + ( map . externalRoads || [ ] ) . length + ( map . expressways || [ ] ) . length + ( map . externalExpressways || [ ] ) . length ;
}
2026-05-20 13:50:56 +09:00
function getStats ( map ) {
2026-05-29 23:49:02 +09:00
if ( ! map ) return [ ] ;
2026-05-20 13:50:56 +09:00
return [
[ "Population" , ( map . totalPopulation || 0 ) . toLocaleString ( ) ] ,
2026-05-29 23:49:02 +09:00
[ "Municipalities" , ( map . adminCenters || [ ] ) . length . toLocaleString ( ) ] ,
[ "Major cities" , countInside ( map . modernCities ) . toLocaleString ( ) ] ,
[ "Ports" , countInside ( map . ports ) . toLocaleString ( ) ] ,
[ "Rail lines" , totalRailLineCount ( map ) . toLocaleString ( ) ] ,
[ "Generation" , map . generationTotalMs ? formatMs ( map . generationTotalMs ) : "-" ] ,
2026-05-20 13:50:56 +09:00
] ;
}
function renderStats ( map ) {
2026-05-29 23:49:02 +09:00
if ( ! statsEl ) return ;
2026-05-20 13:50:56 +09:00
statsEl . innerHTML = "" ;
for ( const [ labelText , valueText ] of getStats ( map ) ) {
const row = document . createElement ( "div" ) ;
row . className = "stat-row" ;
const label = document . createElement ( "span" ) ;
label . textContent = labelText ;
const value = document . createElement ( "strong" ) ;
value . textContent = String ( valueText ) ;
row . append ( label , value ) ;
statsEl . append ( row ) ;
}
}
2026-05-29 23:49:02 +09:00
const legendItems = {
base : [
[ "terrain-swatch" , "Terrain shading" ] ,
[ "river-major" , "Rivers and lakes" , "line" ] ,
[ "border-swatch" , "Prefecture border" ] ,
] ,
all : [
[ "city-icon" , "City / major town" , "icon" ] ,
[ "rail-line" , "Railway" , "line" ] ,
[ "road-line" , "Major road" , "line" ] ,
[ "river-major" , "River" , "line" ] ,
[ "border-swatch" , "Prefecture border" ] ,
] ,
terrain : [
[ "terrain-swatch" , "Elevation and relief" ] ,
[ "sea-swatch" , "Sea / lake" ] ,
[ "river-major" , "River system" , "line" ] ,
[ "border-swatch" , "Prefecture border" ] ,
] ,
modern : [
[ "city-icon" , "Modern city" , "icon" ] ,
[ "station-icon" , "Station" , "icon" ] ,
[ "rail-line" , "Railway" , "line" ] ,
[ "minor-road-line" , "Local road" , "line" ] ,
] ,
history : [
[ "town-icon" , "Market / village" , "icon" ] ,
[ "castle-icon" , "Castle / ruins" , "icon" ] ,
[ "port-icon" , "Historical port" , "icon" ] ,
[ "old-road-line" , "Premodern road" , "line" ] ,
] ,
landuse : [
[ "urban-swatch" , "Urban land use" ] ,
[ "industry-icon" , "Industry / logistics" , "icon" ] ,
[ "city-icon" , "City core" , "icon" ] ,
[ "river-major" , "Water body" , "line" ] ,
] ,
admin : [
[ "admin-swatch" , "Municipal border" ] ,
[ "border-swatch" , "Prefecture border" ] ,
[ "city-icon" , "Admin center" , "icon" ] ,
] ,
} ;
function legendRowsForMode ( mode ) {
return legendItems [ mode ] || legendItems . all ;
}
function renderLegendGrid ( container , rows ) {
if ( ! container ) return ;
container . innerHTML = "" ;
for ( const [ className , text , kind = "swatch" ] of rows ) {
const row = document . createElement ( "div" ) ;
row . className = "legend-row" ;
const mark = document . createElement ( "span" ) ;
mark . className = kind === "line" ? ` legend-line ${ className } ` : kind === "icon" ? ` legend-icon ${ className } ` : ` legend-swatch ${ className } ` ;
const label = document . createElement ( "span" ) ;
label . textContent = text ;
row . append ( mark , label ) ;
container . append ( row ) ;
}
}
function renderLegend ( ) {
const rows = legendRowsForMode ( state . mode ) ;
renderLegendGrid ( mainLegendGrid , rows ) ;
renderLegendGrid ( floatingLegendGrid , rows . slice ( 0 , 5 ) ) ;
}
2026-05-26 16:56:18 +09:00
function buildHoverEntities ( map ) {
return [
2026-05-20 13:50:56 +09:00
... ( map . modernCities || [ ] ) ,
... ( map . ports || [ ] ) ,
... ( map . stations || [ ] ) ,
... ( map . interchanges || [ ] ) ,
... ( map . industrialZones || [ ] ) ,
... ( map . logisticsParks || [ ] ) ,
... ( map . newTowns || [ ] ) ,
... ( map . castles || [ ] ) ,
... ( map . markets || [ ] ) ,
... ( map . villages || [ ] ) ,
... ( map . adminCenters || [ ] ) ,
] ;
2026-05-26 16:56:18 +09:00
}
function nearestEntity ( items , x , y , maxDistance = 5 ) {
2026-05-20 13:50:56 +09:00
let best = null ;
let bestD = maxDistance ;
2026-05-29 14:31:42 +09:00
for ( const item of items || [ ] ) {
if ( ! item || ! Number . isFinite ( item . x ) || ! Number . isFinite ( item . y ) ) continue ;
2026-05-20 13:50:56 +09:00
const d = Math . hypot ( item . x - x , item . y - y ) ;
if ( d < bestD ) { best = item ; bestD = d ; }
}
return best ;
}
function landuseName ( value ) {
2026-05-24 17:38:51 +09:00
return landuseLabel ( value ) ;
2026-05-20 13:50:56 +09:00
}
2026-05-29 14:31:42 +09:00
const ADMIN _ID _KEYS = [ "adminId" , "municipalityId" , "adminNumericId" , "id" , "numericId" ] ;
const PREFECTURE _ID _KEYS = [ "prefectureRegionId" , "prefectureId" , "id" , "numericId" ] ;
const MUNICIPALITY _NAME _KEYS = [ "municipalityName" , "name" , "canonicalSettlementName" , "municipalityRootName" , "generatedMunicipalityName" , "label" ] ;
const PREFECTURE _NAME _KEYS = [ "prefectureName" , "prefectureRegionName" , "regionName" , "name" , "label" ] ;
const POPULATION _KEYS = [ "municipalityPopulation" , "adminPopulation" , "population" , "estimatedPopulation" ] ;
function numericIdOf ( item , keys = ADMIN _ID _KEYS ) {
for ( const key of keys ) {
const value = item ? . [ key ] ;
if ( Number . isFinite ( value ) ) return Math . floor ( value ) ;
}
return null ;
}
function hasNumericId ( item , id , keys = ADMIN _ID _KEYS ) {
if ( ! item || id == null || id < 0 ) return false ;
return keys . some ( ( key ) => Number . isFinite ( item ? . [ key ] ) && Math . floor ( item [ key ] ) === Math . floor ( id ) ) ;
}
2026-05-29 22:00:42 +09:00
function numericPrefectureId ( item ) {
const value = numericIdOf ( item , PREFECTURE _ID _KEYS ) ;
return value == null ? - 1 : value ;
}
2026-05-29 14:31:42 +09:00
function firstUsableText ( item , keys ) {
for ( const key of keys ) {
2026-05-28 23:51:55 +09:00
const value = item ? . [ key ] ;
2026-05-29 14:31:42 +09:00
if ( ! looksNumericName ( value ) ) return String ( value ) . trim ( ) ;
}
return "" ;
}
function firstPopulation ( item ) {
for ( const key of POPULATION _KEYS ) {
const value = item ? . [ key ] ;
if ( Number . isFinite ( value ) && value > 0 ) return Math . round ( value ) ;
2026-05-28 23:51:55 +09:00
}
return null ;
}
function adminCenterForId ( map , adminId ) {
if ( ! map || adminId == null || adminId < 0 ) return null ;
2026-05-29 14:31:42 +09:00
const centers = ( map . adminCenters || [ ] ) . filter ( Boolean ) ;
const exact = centers . find ( ( center ) => hasNumericId ( center , adminId ) ) ;
if ( exact ) return exact ;
// Some legacy/admin debug arrays were once addressed by array index. Keep this
// only as a guarded fallback so numeric IDs are not mistaken for indexes.
const direct = map . adminCenters ? . [ adminId ] ;
return hasNumericId ( direct , adminId ) ? direct : null ;
2026-05-28 23:51:55 +09:00
}
function looksNumericName ( name ) {
if ( ! name ) return true ;
const text = String ( name ) . trim ( ) ;
2026-05-29 22:00:42 +09:00
return ! text || /^県域\d*$/u . test ( text ) || /^-?\d+(?:\s*[,, ]\s*\d+)*$/u . test ( text ) || /^(Municipality|Prefecture|Admin|Region)\s*-?\d+/i . test ( text ) ;
2026-05-20 13:50:56 +09:00
}
2026-05-29 14:31:42 +09:00
function nearestNamedAdminCenter ( map , cellIndex , maxDistance = 36 , adminId = null ) {
2026-05-28 23:51:55 +09:00
if ( ! map || cellIndex < 0 ) return null ;
const x = cellIndex % map . width ;
const y = Math . floor ( cellIndex / map . width ) ;
let best = null ;
let bestD = maxDistance ;
for ( const center of map . adminCenters || [ ] ) {
2026-05-29 14:31:42 +09:00
if ( ! center || ! Number . isFinite ( center . x ) || ! Number . isFinite ( center . y ) ) continue ;
if ( adminId != null && adminId >= 0 && ! hasNumericId ( center , adminId ) ) continue ;
if ( ! firstUsableText ( center , MUNICIPALITY _NAME _KEYS ) ) continue ;
2026-05-28 23:51:55 +09:00
const d = Math . hypot ( center . x - x , center . y - y ) ;
if ( d < bestD ) { best = center ; bestD = d ; }
}
return best ;
}
function adminName ( map , adminId , cellIndex = - 1 ) {
2026-05-29 14:31:42 +09:00
const center = adminCenterForId ( map , adminId ) || nearestNamedAdminCenter ( map , cellIndex , 54 , adminId ) ;
const name = firstUsableText ( center , MUNICIPALITY _NAME _KEYS ) ;
if ( name ) return name ;
2026-05-28 23:51:55 +09:00
return adminId >= 0 ? "Unnamed municipality" : "-" ;
}
function adminPopulation ( map , adminId , cellIndex = - 1 ) {
2026-05-29 14:31:42 +09:00
const center = adminCenterForId ( map , adminId ) || nearestNamedAdminCenter ( map , cellIndex , 54 , adminId ) ;
const centerPop = firstPopulation ( center ) ;
if ( centerPop !== null ) return centerPop ;
let sum = 0 ;
let found = false ;
for ( const key of [ "modernCities" , "satelliteCities" , "ports" , "markets" , "villages" ] ) {
for ( const p of map ? . [ key ] || [ ] ) {
if ( ! hasNumericId ( p , adminId ) ) continue ;
const pop = firstPopulation ( p ) ;
if ( pop !== null ) { sum += pop ; found = true ; }
}
}
return found ? sum : null ;
2026-05-26 16:56:18 +09:00
}
2026-05-29 22:00:42 +09:00
function worldSourceMap ( ) {
return displayWorld ( ) ? . sourceMap || null ;
}
function prefectureRegionById ( id , maps = [ ] ) {
if ( ! Number . isFinite ( id ) || id < 0 ) return null ;
for ( const source of maps ) {
const region = ( source ? . prefectureRegions || [ ] ) . find ( ( p ) => hasNumericId ( p , id , PREFECTURE _ID _KEYS ) ) ;
if ( region ) return region ;
}
return null ;
}
function prefectureNameForId ( id , adminId = - 1 ) {
if ( ! Number . isFinite ( id ) || id < 0 ) return "" ;
const sources = [ activeMap ( ) , worldSourceMap ( ) ] . filter ( Boolean ) ;
const region = prefectureRegionById ( id , sources ) ;
2026-05-29 14:31:42 +09:00
const regionName = firstUsableText ( region , PREFECTURE _NAME _KEYS ) ;
if ( regionName ) return regionName ;
2026-05-29 22:00:42 +09:00
for ( const source of sources ) {
for ( const center of source ? . adminCenters || [ ] ) {
if ( adminId >= 0 && ! hasNumericId ( center , adminId ) ) continue ;
const centerPref = numericPrefectureId ( center ) ;
if ( centerPref >= 0 && centerPref !== id ) continue ;
const name = firstUsableText ( center , [ "prefectureName" , "prefectureRegionName" , "regionName" ] ) ;
if ( name ) return name ;
}
}
return "" ;
}
function prefectureNameForCell ( map , i ) {
const id = map . prefectureRegionId ? . [ i ] ? ? - 1 ;
const direct = prefectureNameForId ( id ) ;
if ( direct ) return direct ;
2026-05-29 15:49:09 +09:00
const adminId = map . adminId ? . [ i ] ? ? - 1 ;
2026-05-29 22:00:42 +09:00
const mappedPref = adminId >= 0 ? map . municipalityToPrefectureId ? . [ adminId ] ? ? state . world ? . sourceMap ? . municipalityToPrefectureId ? . [ adminId ] ? ? - 1 : - 1 ;
const mapped = prefectureNameForId ( mappedPref , adminId ) ;
if ( mapped ) return mapped ;
2026-05-29 15:49:09 +09:00
const center = mappedPref === id ? nearestNamedAdminCenter ( map , i , 90 , adminId ) : null ;
2026-05-29 14:31:42 +09:00
const fromCenter = firstUsableText ( center , [ "prefectureName" , "prefectureRegionName" , "regionName" ] ) ;
return fromCenter || ( id >= 0 ? "Unnamed prefecture" : "-" ) ;
2026-05-26 15:32:27 +09:00
}
2026-05-20 13:50:56 +09:00
function updateTooltip ( event ) {
2026-05-28 19:37:28 +09:00
const map = activeMap ( ) ;
if ( ! map || ! tooltipEl || dragState . mode ) return ;
2026-05-20 13:50:56 +09:00
const rect = canvas . getBoundingClientRect ( ) ;
2026-05-28 00:30:09 +09:00
const cell = mapClientToCell ( event ) ;
if ( ! cell ) return ;
const { x , y } = cell ;
2026-05-28 19:37:28 +09:00
if ( x < 0 || y < 0 || x >= map . width || y >= map . height ) {
2026-05-20 13:50:56 +09:00
tooltipEl . classList . remove ( "visible" ) ;
return ;
}
2026-05-28 19:37:28 +09:00
const i = y * map . width + x ;
const worldCell = viewportCellToWorldCell ( { x , y } ) ;
2026-05-26 16:56:18 +09:00
const entity = nearestEntity ( state . hoverEntities , x , y ) ;
2026-05-28 19:37:28 +09:00
const elevation = map . elevation ? . [ i ] ? ? 0 ;
2026-05-28 23:51:55 +09:00
const density = map . populationDensity ? . [ i ] ? ? map . settlementScore ? . [ i ] ? ? 0 ;
2026-05-28 19:37:28 +09:00
const hoveredAdminId = map . adminId ? . [ i ] ? ? - 1 ;
2026-05-28 23:51:55 +09:00
const hoveredAdminPopulation = adminPopulation ( map , hoveredAdminId , i ) ;
2026-05-28 19:37:28 +09:00
const coordinateText = worldCell ? ` World ${ worldCell . x } , ${ worldCell . y } / View ${ x } , ${ y } ` : ` ${ x } , ${ y } ` ;
2026-05-29 14:31:42 +09:00
const entityName = firstUsableText ( entity , [ "name" , "facilityLabel" , "municipalityName" , "canonicalSettlementName" , "kind" ] ) ;
2026-05-28 19:37:28 +09:00
const entityTitle = entity
2026-05-29 14:31:42 +09:00
? ` ${ entityName || adminName ( map , hoveredAdminId , i ) || entity . kind || "Feature" } / ${ entity . kind || "Feature" } `
2026-05-28 19:37:28 +09:00
: coordinateText ;
2026-05-20 13:50:56 +09:00
const lines = [
2026-05-28 19:37:28 +09:00
` <strong> ${ entityTitle } </strong> ` ,
` Prefecture: ${ prefectureNameForCell ( map , i ) } ` ,
2026-05-28 23:51:55 +09:00
` Admin: ${ adminName ( map , hoveredAdminId , i ) } ` ,
2026-05-26 16:56:18 +09:00
` Admin Pop: ${ hoveredAdminPopulation === null ? "-" : hoveredAdminPopulation . toLocaleString ( ) } ` ,
2026-05-28 19:37:28 +09:00
` Land: ${ map . sea ? . [ i ] ? "Sea" : landuseName ( map . landuse ? . [ i ] ) } ` ,
` Elevation: ${ elevation . toFixed ( 3 ) } / Slope: ${ ( map . slope ? . [ i ] ? ? 0 ) . toFixed ( 3 ) } ` ,
` River: ${ ( map . river ? . [ i ] ? ? 0 ) . toFixed ( 2 ) } / Density: ${ density . toFixed ( 2 ) } ` ,
2026-05-20 13:50:56 +09:00
] ;
if ( entity ? . population ) lines . splice ( 1 , 0 , ` Population: ${ entity . population . toLocaleString ( ) } ` ) ;
tooltipEl . innerHTML = lines . join ( "<br>" ) ;
2026-05-26 15:32:27 +09:00
const margin = 8 ;
const offset = 14 ;
const maxLeft = Math . max ( margin , rect . width - tooltipEl . offsetWidth - margin ) ;
const maxTop = Math . max ( margin , rect . height - tooltipEl . offsetHeight - margin ) ;
const desiredLeft = event . clientX - rect . left + offset ;
const desiredTop = event . clientY - rect . top + offset ;
tooltipEl . style . left = ` ${ Math . min ( Math . max ( margin , desiredLeft ) , maxLeft ) } px ` ;
tooltipEl . style . top = ` ${ Math . min ( Math . max ( margin , desiredTop ) , maxTop ) } px ` ;
2026-05-20 13:50:56 +09:00
tooltipEl . classList . add ( "visible" ) ;
}
2026-05-26 16:56:18 +09:00
function renderModeButtons ( ) {
2026-05-29 23:49:02 +09:00
if ( ! modeGrid ) return ;
2026-05-26 16:56:18 +09:00
modeGrid . innerHTML = "" ;
2026-05-20 13:50:56 +09:00
for ( const [ key , label ] of modes ) {
const button = document . createElement ( "button" ) ;
button . type = "button" ;
button . textContent = label ;
button . className = key === state . mode ? "mode-button active" : "mode-button" ;
button . addEventListener ( "click" , ( ) => {
state . mode = key ;
renderModeButtons ( ) ;
2026-05-29 23:49:02 +09:00
renderLegend ( ) ;
2026-05-20 13:50:56 +09:00
redraw ( ) ;
} ) ;
modeGrid . append ( button ) ;
}
2026-05-29 23:49:02 +09:00
renderLegend ( ) ;
2026-05-20 13:50:56 +09:00
}
2026-05-24 19:33:09 +09:00
async function regenerate ( ) {
2026-05-20 13:50:56 +09:00
state . seedText = seedInput . value ;
2026-05-28 16:45:00 +09:00
state . generationType = generationTypeInput ? . value || "auto" ;
2026-05-24 19:33:09 +09:00
setProgressVisible ( true , "Preparing generation..." ) ;
await nextFrame ( ) ;
try {
2026-05-28 16:45:00 +09:00
state . map = await generateMapAsync ( parseSeed ( state . seedText ) , { onProgress : updateGenerationProgress , terrainType : state . generationType } ) ;
2026-05-28 19:37:28 +09:00
state . world = createWorldMap ( state . map ) ;
state . camera = createInitialCamera ( state . world ) ;
state . lastPatchResult = null ;
2026-05-29 22:00:42 +09:00
state . pendingPatch = null ;
2026-05-28 23:51:55 +09:00
resetPatchVariant ( { update : false } ) ;
2026-05-29 22:00:42 +09:00
hideSelectionOverlay ( { discardPreview : true } ) ;
2026-05-29 23:49:02 +09:00
recordGenerationRun ( state . map , state . generationType ) ;
2026-05-24 19:33:09 +09:00
renderStats ( state . map ) ;
redraw ( ) ;
if ( progressStageEl ) progressStageEl . textContent = ` Done in ${ formatMs ( state . map . generationTotalMs || 0 ) } ` ;
renderTimingRows ( state . map . generationTimings || [ ] ) ;
window . setTimeout ( ( ) => setProgressVisible ( false ) , 900 ) ;
} catch ( error ) {
2026-05-29 23:49:02 +09:00
const reason = error ? . message || String ( error || "unknown generation error" ) ;
recordDiagnosticLog ( "error" , "Full generation failed" , reason , { terrainType : state . generationType } ) ;
if ( progressStageEl ) progressStageEl . textContent = ` Generation failed: ${ reason } ` ;
2026-05-24 19:33:09 +09:00
throw error ;
}
2026-05-20 13:50:56 +09:00
}
2026-05-28 19:37:28 +09:00
2026-05-28 23:51:55 +09:00
function derivePatchSeed ( rect , terrainType , variant = 0 ) {
2026-05-28 19:37:28 +09:00
let h = parseSeed ( state . seedText ) ^ 0x9e3779b9 ;
h = Math . imul ( h ^ ( rect . x0 | 0 ) , 1664525 ) >>> 0 ;
h = Math . imul ( h ^ ( rect . y0 | 0 ) , 1013904223 ) >>> 0 ;
h = Math . imul ( h ^ ( rect . x1 | 0 ) , 2246822519 ) >>> 0 ;
h = Math . imul ( h ^ ( rect . y1 | 0 ) , 3266489917 ) >>> 0 ;
2026-05-28 23:51:55 +09:00
h = Math . imul ( h ^ normalizePatchVariant ( variant ) , 668265263 ) >>> 0 ;
2026-05-28 19:37:28 +09:00
for ( const ch of String ( terrainType || "auto" ) ) h = Math . imul ( h ^ ch . charCodeAt ( 0 ) , 16777619 ) >>> 0 ;
return h >>> 0 ;
}
2026-05-28 23:51:55 +09:00
2026-05-29 22:00:42 +09:00
function beginZoomVisual ( oldZoom , event ) {
if ( zoomVisualState ) return ;
const rect = canvas . getBoundingClientRect ( ) ;
zoomVisualState = {
baseRect : rect ,
startZoom : clampZoom ( oldZoom || state . zoom || 1 ) ,
originX : Math . min ( Math . max ( event . clientX - rect . left , 0 ) , rect . width ) ,
originY : Math . min ( Math . max ( event . clientY - rect . top , 0 ) , rect . height ) ,
} ;
canvas . style . transformOrigin = ` ${ zoomVisualState . originX } px ${ zoomVisualState . originY } px ` ;
canvas . style . willChange = "transform" ;
canvas . classList . add ( "is-zooming" ) ;
if ( selectionSvgEl ) selectionSvgEl . style . visibility = "hidden" ;
}
function scheduleZoomVisualUpdate ( ) {
if ( ! zoomVisualState || zoomRedrawRaf != null ) return ;
zoomRedrawRaf = requestAnimationFrame ( ( ) => {
zoomRedrawRaf = null ;
if ( ! zoomVisualState ) return ;
const scale = clampZoom ( state . zoom || 1 ) / Math . max ( 1e-6 , zoomVisualState . startZoom || 1 ) ;
canvas . style . transform = ` scale( ${ scale } ) ` ;
} ) ;
}
function finishZoomVisual ( ) {
if ( zoomRedrawRaf != null ) {
cancelAnimationFrame ( zoomRedrawRaf ) ;
zoomRedrawRaf = null ;
}
if ( zoomVisualState ) {
canvas . style . transform = "" ;
canvas . style . transformOrigin = "" ;
canvas . style . willChange = "" ;
canvas . classList . remove ( "is-zooming" ) ;
if ( selectionSvgEl ) selectionSvgEl . style . visibility = "" ;
zoomVisualState = null ;
}
}
2026-05-28 23:51:55 +09:00
function handleCanvasWheel ( event ) {
if ( ! state . world || ! activeMap ( ) ) return ;
event . preventDefault ( ) ;
2026-05-29 23:49:02 +09:00
if ( zoomLatencyStartedAt == null ) zoomLatencyStartedAt = performance . now ( ) ;
2026-05-28 23:51:55 +09:00
tooltipEl ? . classList . remove ( "visible" ) ;
2026-05-29 14:31:42 +09:00
const beforeSize = viewportSizeForZoom ( state . zoom ) ;
const beforeCell = mapClientToCell ( event , beforeSize ) ;
2026-05-28 23:51:55 +09:00
const beforeWorld = beforeCell ? viewportCellToWorldCell ( beforeCell ) : null ;
const oldZoom = clampZoom ( state . zoom || 1 ) ;
2026-05-29 14:31:42 +09:00
const delta = event . deltaY < 0 ? 1.10 : 1 / 1.10 ;
2026-05-28 23:51:55 +09:00
const nextZoom = clampZoom ( oldZoom * delta ) ;
if ( Math . abs ( nextZoom - oldZoom ) < 0.001 ) return ;
state . zoom = nextZoom ;
2026-05-29 14:31:42 +09:00
const nextSize = syncViewportSize ( ) ;
2026-05-28 23:51:55 +09:00
if ( beforeWorld ) {
2026-05-29 14:31:42 +09:00
const afterCell = mapClientToCell ( event , nextSize ) ;
2026-05-28 23:51:55 +09:00
if ( afterCell ) {
2026-05-29 14:31:42 +09:00
state . camera = clampCameraForView ( {
2026-05-28 23:51:55 +09:00
x : beforeWorld . x - afterCell . x ,
y : beforeWorld . y - afterCell . y ,
2026-05-29 14:31:42 +09:00
} , nextSize ) ;
2026-05-28 23:51:55 +09:00
}
}
2026-05-29 14:31:42 +09:00
2026-05-29 22:00:42 +09:00
// Wheel events can fire dozens of times per second. During the gesture, keep
// the last rendered bitmap and only transform it on the GPU; rebuild the
// viewport and labels once the gesture settles.
beginZoomVisual ( oldZoom , event ) ;
scheduleZoomVisualUpdate ( ) ;
2026-05-29 14:31:42 +09:00
if ( zoomSettledTimer != null ) clearTimeout ( zoomSettledTimer ) ;
zoomSettledTimer = window . setTimeout ( ( ) => {
zoomSettledTimer = null ;
2026-05-29 23:49:02 +09:00
const startedAt = zoomLatencyStartedAt || performance . now ( ) ;
zoomLatencyStartedAt = null ;
2026-05-29 22:00:42 +09:00
finishZoomVisual ( ) ;
2026-05-29 14:31:42 +09:00
redraw ( { fastTerrain : false , allowWorldExpand : false } ) ;
2026-05-29 23:49:02 +09:00
recordInteractionLatency ( "wheel zoom" , startedAt , { zoom : state . zoom } ) ;
2026-05-29 22:00:42 +09:00
} , 170 ) ;
}
function cloneForPatchPreview ( value , seen = new Map ( ) ) {
if ( value == null || typeof value !== "object" ) return value ;
if ( ArrayBuffer . isView ( value ) ) return new value . constructor ( value ) ;
if ( value instanceof ArrayBuffer ) return value . slice ( 0 ) ;
if ( seen . has ( value ) ) return seen . get ( value ) ;
if ( value instanceof Map ) {
const out = new Map ( ) ;
seen . set ( value , out ) ;
for ( const [ k , v ] of value . entries ( ) ) out . set ( cloneForPatchPreview ( k , seen ) , cloneForPatchPreview ( v , seen ) ) ;
return out ;
}
if ( Array . isArray ( value ) ) {
const out = [ ] ;
seen . set ( value , out ) ;
for ( const item of value ) out . push ( cloneForPatchPreview ( item , seen ) ) ;
return out ;
}
const out = { } ;
seen . set ( value , out ) ;
for ( const [ key , item ] of Object . entries ( value ) ) out [ key ] = cloneForPatchPreview ( item , seen ) ;
return out ;
}
function createPatchWorker ( ) {
2026-05-29 23:49:02 +09:00
if ( patchWorker ) return patchWorker ;
if ( typeof Worker === "undefined" ) {
state . diagnostics . lastWorkerFallbackReason = "Worker API unavailable" ;
return null ;
}
2026-05-29 22:00:42 +09:00
try {
patchWorker = new Worker ( new URL ( "./mapPatchWorker.js" , import . meta . url ) , { type : "module" } ) ;
2026-05-29 23:49:02 +09:00
patchWorker . addEventListener ( "error" , ( event ) => {
const reason = event ? . message || "Patch worker runtime error" ;
state . diagnostics . lastWorkerFallbackReason = reason ;
recordDiagnosticLog ( "warning" , "Patch worker reset" , reason ) ;
2026-05-29 22:00:42 +09:00
patchWorker ? . terminate ? . ( ) ;
patchWorker = null ;
} ) ;
2026-05-29 23:49:02 +09:00
} catch ( error ) {
state . diagnostics . lastWorkerFallbackReason = error ? . message || "Patch worker creation failed" ;
2026-05-29 22:00:42 +09:00
patchWorker = null ;
}
return patchWorker ;
}
function runPatchInWorker ( world , rect , options ) {
const worker = createPatchWorker ( ) ;
if ( ! worker ) return null ;
const id = ++ patchJobSeq ;
return new Promise ( ( resolve , reject ) => {
const cleanup = ( ) => {
worker . removeEventListener ( "message" , onMessage ) ;
worker . removeEventListener ( "error" , onError ) ;
worker . removeEventListener ( "messageerror" , onMessageError ) ;
} ;
const onMessage = ( event ) => {
if ( event . data ? . id !== id ) return ;
cleanup ( ) ;
if ( event . data . ok ) resolve ( { world : event . data . world , result : event . data . result , worker : true } ) ;
else reject ( new Error ( event . data . error || "Patch worker failed" ) ) ;
} ;
const onError = ( event ) => {
cleanup ( ) ;
reject ( new Error ( event . message || "Patch worker error" ) ) ;
} ;
const onMessageError = ( ) => {
cleanup ( ) ;
reject ( new Error ( "Patch worker message clone failed" ) ) ;
} ;
worker . addEventListener ( "message" , onMessage ) ;
worker . addEventListener ( "error" , onError ) ;
worker . addEventListener ( "messageerror" , onMessageError ) ;
worker . postMessage ( { id , world , rect , options } ) ;
} ) ;
}
async function generatePatchPreviewWorld ( baseWorld , rect , options ) {
const workerPromise = runPatchInWorker ( baseWorld , rect , options ) ;
if ( workerPromise ) {
try {
return await workerPromise ;
} catch ( error ) {
2026-05-29 23:49:02 +09:00
const reason = error ? . message || String ( error || "unknown worker failure" ) ;
2026-05-29 22:00:42 +09:00
console . warn ( "Patch worker unavailable; falling back to main-thread preview generation." , error ) ;
2026-05-29 23:49:02 +09:00
state . diagnostics . lastWorkerFallbackReason = reason ;
recordDiagnosticLog ( "warning" , "Patch worker fallback" , reason ) ;
2026-05-29 22:00:42 +09:00
patchWorker ? . terminate ? . ( ) ;
patchWorker = null ;
}
2026-05-29 23:49:02 +09:00
} else if ( state . diagnostics . lastWorkerFallbackReason ) {
recordDiagnosticLog ( "info" , "Patch generated on main thread" , state . diagnostics . lastWorkerFallbackReason ) ;
2026-05-29 22:00:42 +09:00
}
const previewWorld = cloneForPatchPreview ( baseWorld ) ;
const result = generatePatch ( previewWorld , rect , options ) ;
return { world : previewWorld , result , worker : false } ;
2026-05-28 23:51:55 +09:00
}
2026-05-29 23:49:02 +09:00
async function generateSelectedPatch ( kind = "Patch preview" ) {
2026-05-28 19:37:28 +09:00
const validation = validatePatchRect ( state . selectionRect , state . world ) ;
if ( ! validation . ok ) {
2026-05-29 23:49:02 +09:00
recordDiagnosticLog ( "warning" , "Invalid patch selection" , validation . reason || "Selection is not valid." ) ;
2026-05-28 19:37:28 +09:00
updatePatchControls ( ) ;
return ;
}
const terrainType = patchTerrainTypeInput ? . value || generationTypeInput ? . value || "auto" ;
2026-05-28 23:51:55 +09:00
const variant = readPatchVariant ( ) ;
const seed = derivePatchSeed ( validation . rect , terrainType , variant ) ;
2026-05-29 22:00:42 +09:00
setProgressVisible ( true , "Generating preview patch..." ) ;
2026-05-28 19:37:28 +09:00
await nextFrame ( ) ;
2026-05-29 23:49:02 +09:00
const patchStartedAt = performance . now ( ) ;
2026-05-28 19:37:28 +09:00
try {
2026-05-29 22:00:42 +09:00
const job = await generatePatchPreviewWorld ( state . world , validation . rect , { terrainType , seed , variant } ) ;
const result = job . result ;
2026-05-28 19:37:28 +09:00
if ( ! result . ok ) {
2026-05-29 23:49:02 +09:00
const reason = result . reason || "invalid selection" ;
recordDiagnosticLog ( "warning" , "Patch failed" , reason , { kind , terrainType , variant } ) ;
if ( progressStageEl ) progressStageEl . textContent = ` Patch failed: ${ reason } ` ;
2026-05-28 19:37:28 +09:00
updatePatchControls ( ) ;
window . setTimeout ( ( ) => setProgressVisible ( false ) , 1200 ) ;
return ;
}
2026-05-29 23:49:02 +09:00
const patchWallMs = performance . now ( ) - patchStartedAt ;
2026-05-29 22:00:42 +09:00
state . pendingPatch = { world : job . world , result , rect : validation . rect , terrainType , seed , variant , worker : job . worker } ;
2026-05-29 23:49:02 +09:00
recordPatchRun ( result , terrainType , validation . rect , variant , { kind , worker : job . worker , wallMs : patchWallMs } ) ;
2026-05-29 22:00:42 +09:00
// Keep the committed world untouched. The preview world is rendered until
// the user clicks once without dragging; Alternative replaces this preview
// from the same committed base, so old candidate artifacts cannot accumulate.
state . viewportMap = null ;
redraw ( { fastTerrain : true , allowWorldExpand : false } ) ;
window . setTimeout ( ( ) => redraw ( { fastTerrain : false , allowWorldExpand : false } ) , 80 ) ;
renderStats ( displaySourceMap ( ) ) ;
2026-05-28 19:37:28 +09:00
updatePatchControls ( ) ;
2026-05-29 23:49:02 +09:00
if ( progressStageEl ) progressStageEl . textContent = ` Preview generated ${ job . worker ? " in worker" : "" } : ${ result . label } / variant ${ result . variant ? ? variant } / write ${ formatRectSize ( result . rects . writeRect ) } . Use Apply Preview to commit. ` ;
2026-05-29 15:49:09 +09:00
renderTimingRows ( result . patchTimings || [ ] ) ;
2026-05-28 19:37:28 +09:00
window . setTimeout ( ( ) => setProgressVisible ( false ) , 900 ) ;
} catch ( error ) {
2026-05-29 23:49:02 +09:00
const reason = error ? . message || String ( error || "unknown patch error" ) ;
recordDiagnosticLog ( "error" , "Patch exception" , reason , { kind , terrainType , variant } ) ;
if ( progressStageEl ) progressStageEl . textContent = ` Patch failed: ${ reason } ` ;
2026-05-28 19:37:28 +09:00
throw error ;
}
}
2026-05-28 23:51:55 +09:00
async function generateAlternativePatch ( ) {
const validation = validatePatchRect ( state . selectionRect , state . world ) ;
if ( ! validation . ok ) {
updatePatchControls ( ) ;
return ;
}
setPatchVariant ( readPatchVariant ( ) + 1 , { update : false } ) ;
2026-05-29 23:49:02 +09:00
await generateSelectedPatch ( "Patch alternative" ) ;
2026-05-28 23:51:55 +09:00
}
2026-05-28 19:37:28 +09:00
function redraw ( options = { } ) {
2026-05-29 23:49:02 +09:00
const redrawStartedAt = performance . now ( ) ;
2026-05-29 22:00:42 +09:00
const renderWorld = displayWorld ( ) ;
if ( ! renderWorld ) return ;
2026-05-29 14:31:42 +09:00
const viewSize = syncViewportSize ( ) ;
2026-05-29 22:00:42 +09:00
const mayExpand = ! state . pendingPatch && options . allowWorldExpand !== false ;
const expansion = mayExpand ? ensureWorldPaddingForCamera ( state . world , state . camera , viewSize . width , viewSize . height ) : null ;
2026-05-28 23:51:55 +09:00
if ( expansion ? . expanded ) {
2026-05-29 18:50:54 +09:00
const ex = expansion . dx || 0 ;
const ey = expansion . dy || 0 ;
2026-05-29 23:49:02 +09:00
state . diagnostics . worldExpansionCount += 1 ;
state . diagnostics . lastWorldExpansion = {
at : new Date ( ) ,
dx : ex ,
dy : ey ,
worldWidth : state . world ? . width || 0 ,
worldHeight : state . world ? . height || 0 ,
viewWidth : viewSize . width ,
viewHeight : viewSize . height ,
} ;
2026-05-29 18:50:54 +09:00
state . camera = { x : ( state . camera ? . x || 0 ) + ex , y : ( state . camera ? . y || 0 ) + ey } ;
if ( dragState . mode === "pan" ) {
dragState . startCameraX += ex ;
dragState . startCameraY += ey ;
if ( dragState . pendingCamera ) dragState . pendingCamera = { x : dragState . pendingCamera . x + ex , y : dragState . pendingCamera . y + ey } ;
}
2026-05-28 23:51:55 +09:00
if ( state . selectionRect ) {
state . selectionRect = {
2026-05-29 14:31:42 +09:00
... state . selectionRect ,
2026-05-29 18:50:54 +09:00
x0 : state . selectionRect . x0 + ex ,
y0 : state . selectionRect . y0 + ey ,
x1 : state . selectionRect . x1 + ex ,
y1 : state . selectionRect . y1 + ey ,
polygon : Array . isArray ( state . selectionRect . polygon ) ? state . selectionRect . polygon . map ( ( p ) => ( { x : p . x + ex , y : p . y + ey } ) ) : state . selectionRect . polygon ,
2026-05-28 23:51:55 +09:00
} ;
}
}
2026-05-29 22:00:42 +09:00
const activeWorldForRender = displayWorld ( ) ;
state . camera = clampCameraForView ( state . camera , viewSize , activeWorldForRender ) ;
2026-05-29 23:49:02 +09:00
const viewportStartedAt = performance . now ( ) ;
2026-05-29 22:00:42 +09:00
state . viewportMap = getViewportMap ( activeWorldForRender , state . camera , viewSize . width , viewSize . height , { light : ! ! options . fastTerrain } ) ;
2026-05-29 23:49:02 +09:00
const viewportMs = performance . now ( ) - viewportStartedAt ;
const hoverStartedAt = performance . now ( ) ;
2026-05-29 14:31:42 +09:00
state . hoverEntities = options . fastTerrain ? [ ] : buildHoverEntities ( state . viewportMap ) ;
2026-05-29 23:49:02 +09:00
const hoverMs = performance . now ( ) - hoverStartedAt ;
const drawStartedAt = performance . now ( ) ;
2026-05-30 03:25:19 +09:00
const renderBreakdown = drawMap ( canvas , state . viewportMap , {
2026-05-20 13:50:56 +09:00
mode : state . mode ,
2026-05-29 14:31:42 +09:00
showFeatures : state . showFeatures && ! options . fastTerrain ,
2026-05-28 19:37:28 +09:00
showLabels : state . showLabels && ! options . fastTerrain ,
continuousTerrain : ! options . fastTerrain ,
2026-05-29 14:31:42 +09:00
fastTerrain : ! ! options . fastTerrain ,
2026-05-28 23:51:55 +09:00
zoom : state . zoom || 1 ,
2026-05-20 13:50:56 +09:00
} ) ;
2026-05-29 23:49:02 +09:00
const drawMs = performance . now ( ) - drawStartedAt ;
2026-05-28 23:51:55 +09:00
applyCanvasZoom ( ) ;
2026-05-28 19:37:28 +09:00
if ( state . selectionRect && dragState . mode !== "select" ) updateSelectionOverlayFromWorldRect ( ) ;
2026-05-29 23:49:02 +09:00
updateRenderDiagnostics ( options , {
viewportMs ,
hoverMs ,
drawMs ,
totalRenderMs : performance . now ( ) - redrawStartedAt ,
2026-05-30 03:25:19 +09:00
renderBreakdown ,
2026-05-29 23:49:02 +09:00
} ) ;
renderAdvancedData ( ) ;
2026-05-20 13:50:56 +09:00
}
function init ( ) {
renderModeButtons ( ) ;
2026-05-29 23:49:02 +09:00
setToolMode ( "pan" ) ;
2026-05-20 13:50:56 +09:00
2026-05-29 23:49:02 +09:00
generateMapButton ? . addEventListener ( "click" , regenerate ) ;
2026-05-20 13:50:56 +09:00
seedInput . addEventListener ( "change" , regenerate ) ;
seedInput . addEventListener ( "keydown" , ( event ) => {
if ( event . key === "Enter" ) regenerate ( ) ;
} ) ;
2026-05-28 16:45:00 +09:00
generationTypeInput ? . addEventListener ( "change" , regenerate ) ;
2026-05-28 23:51:55 +09:00
patchTerrainTypeInput ? . addEventListener ( "change" , ( ) => {
2026-05-29 22:00:42 +09:00
discardPendingPatch ( { redrawAfter : true } ) ;
2026-05-28 23:51:55 +09:00
state . lastPatchResult = null ;
resetPatchVariant ( { update : false } ) ;
updatePatchControls ( ) ;
} ) ;
patchVariantInput ? . addEventListener ( "change" , ( ) => setPatchVariant ( patchVariantInput . value ) ) ;
patchVariantInput ? . addEventListener ( "keydown" , ( event ) => {
if ( event . key === "Enter" ) {
setPatchVariant ( patchVariantInput . value ) ;
generateSelectedPatch ( ) ;
}
} ) ;
2026-05-29 23:49:02 +09:00
generatePatchButton ? . addEventListener ( "click" , ( ) => generateSelectedPatch ( "Patch preview" ) ) ;
2026-05-28 23:51:55 +09:00
alternativePatchButton ? . addEventListener ( "click" , generateAlternativePatch ) ;
2026-05-29 23:49:02 +09:00
applyPatchButton ? . addEventListener ( "click" , ( ) => hideSelectionOverlay ( { commitPreview : true } ) ) ;
discardPatchButton ? . addEventListener ( "click" , ( ) => hideSelectionOverlay ( { discardPreview : true } ) ) ;
toolPanButton ? . addEventListener ( "click" , ( ) => setToolMode ( "pan" ) ) ;
toolPatchButton ? . addEventListener ( "click" , ( ) => setToolMode ( "patch" ) ) ;
copyImportantDataButton ? . addEventListener ( "click" , copyImportantDebugData ) ;
zoomInButton ? . addEventListener ( "click" , ( ) => setZoomKeepingCenter ( ( state . zoom || 1 ) * 1.2 ) ) ;
zoomOutButton ? . addEventListener ( "click" , ( ) => setZoomKeepingCenter ( ( state . zoom || 1 ) / 1.2 ) ) ;
zoomResetButton ? . addEventListener ( "click" , ( ) => setZoomKeepingCenter ( 1 ) ) ;
centerMapButton ? . addEventListener ( "click" , recenterMap ) ;
2026-05-28 16:45:00 +09:00
2026-05-20 13:50:56 +09:00
randomSeedButton . addEventListener ( "click" , ( ) => {
seedInput . value = String ( Math . floor ( Math . random ( ) * 9999999 ) ) ;
regenerate ( ) ;
} ) ;
showFeaturesInput . addEventListener ( "change" , ( ) => {
state . showFeatures = showFeaturesInput . checked ;
redraw ( ) ;
} ) ;
showLabelsInput . addEventListener ( "change" , ( ) => {
state . showLabels = showLabelsInput . checked ;
redraw ( ) ;
} ) ;
2026-05-28 00:30:09 +09:00
canvasShell ? . setAttribute ( "tabindex" , "0" ) ;
2026-05-28 19:37:28 +09:00
canvas . addEventListener ( "contextmenu" , ( event ) => event . preventDefault ( ) ) ;
2026-05-28 23:51:55 +09:00
canvas . addEventListener ( "wheel" , handleCanvasWheel , { passive : false } ) ;
2026-05-28 19:37:28 +09:00
canvas . addEventListener ( "pointerdown" , handleMapPointerDown ) ;
canvas . addEventListener ( "pointermove" , handleMapPointerMove ) ;
canvas . addEventListener ( "pointerup" , handleMapPointerUp ) ;
canvas . addEventListener ( "pointercancel" , handleMapPointerUp ) ;
2026-05-20 13:50:56 +09:00
canvas . addEventListener ( "mousemove" , updateTooltip ) ;
2026-05-28 00:30:09 +09:00
canvas . addEventListener ( "mouseleave" , ( ) => {
tooltipEl ? . classList . remove ( "visible" ) ;
} ) ;
2026-05-20 13:50:56 +09:00
2026-05-29 23:49:02 +09:00
let resizeRaf = null ;
window . addEventListener ( "resize" , ( ) => {
if ( resizeRaf ) cancelAnimationFrame ( resizeRaf ) ;
resizeRaf = requestAnimationFrame ( ( ) => {
resizeRaf = null ;
if ( state . world ) redraw ( { allowWorldExpand : false } ) ;
else applyCanvasZoom ( ) ;
} ) ;
} ) ;
2026-05-28 19:37:28 +09:00
updatePatchControls ( ) ;
2026-05-29 23:49:02 +09:00
renderAdvancedData ( ) ;
2026-05-20 13:50:56 +09:00
regenerate ( ) ;
}
init ( ) ;