"use strict"; const managedDialogOrigins = new WeakMap(); function managedDialogFocusable(dialog) { if (!dialog) return []; return [...dialog.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')] .filter(el => !el.hidden && el.getClientRects().length > 0); } function openManagedDialog(dialog, opener = document.activeElement) { if (!dialog) return; if (opener instanceof HTMLElement && !dialog.contains(opener)) managedDialogOrigins.set(dialog, opener); if (!dialog.hasAttribute("tabindex")) dialog.tabIndex = -1; dialog.classList.remove("hidden"); dialog.setAttribute("aria-hidden", "false"); requestAnimationFrame(() => { const focusables = managedDialogFocusable(dialog); (focusables[0] || dialog).focus?.({ preventScroll: true }); }); } function closeManagedDialog(dialog) { if (!dialog) return; dialog.classList.add("hidden"); dialog.setAttribute("aria-hidden", "true"); const origin = managedDialogOrigins.get(dialog); managedDialogOrigins.delete(dialog); if (origin?.isConnected) requestAnimationFrame(() => origin.focus?.({ preventScroll: true })); } function managedDialogVisible(dialog) { if (!dialog || dialog.hidden || dialog.getAttribute("aria-hidden") === "true") return false; if (dialog.classList.contains("hidden") || dialog.closest(".hidden")) return false; return dialog.getClientRects().length > 0; } function topVisibleDialog() { const dialogs = [...document.querySelectorAll('[role="dialog"], [role="alertdialog"]')].filter(managedDialogVisible); return dialogs[dialogs.length - 1] || null; } document.addEventListener("keydown", (event) => { const dialog = topVisibleDialog(); if (!dialog) return; if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); dialog.dispatchEvent(new CustomEvent("tarinai:dialog-close-request")); return; } if (event.key !== "Tab") return; const focusables = managedDialogFocusable(dialog); if (!focusables.length) { event.preventDefault(); dialog.focus?.(); return; } const first = focusables[0]; const last = focusables[focusables.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } else if (!dialog.contains(document.activeElement)) { event.preventDefault(); first.focus(); } }, true); window.TarinaiManagedDialogs = Object.freeze({ open: openManagedDialog, close: closeManagedDialog }); function resizeCanvas() { const rect = canvas.getBoundingClientRect(); const perfScale = window.TarinaiPerf.dprScale(); const dpr = Math.max(0.5, Math.min((window.devicePixelRatio || 1) * perfScale, 2)); const nextW = Math.max(1, Math.floor(rect.width * dpr)); const nextH = Math.max(1, Math.floor(rect.height * dpr)); if (canvas.width === nextW && canvas.height === nextH && uiCache.canvasDpr === dpr) return; uiCache.canvasDpr = dpr; canvas.width = nextW; canvas.height = nextH; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.fillStyle = "#cfe3d3"; ctx.fillRect(0, 0, Math.max(1, rect.width), Math.max(1, rect.height)); world.setViewportSize(Math.max(1, rect.width), Math.max(1, rect.height), { resizeWorld: true, scaleContents: true }); uiCache.lastChartDraw = ""; if (typeof render === "function") render(); } function applyFieldLayout(fieldType = world.fieldType || "garden") { const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden; const layout = FIELD_LAYOUT; const root = document.documentElement; root.style.setProperty("--field-height", layout.height); root.style.setProperty("--field-min-height", layout.minHeight); root.style.setProperty("--field-mobile-height", layout.mobileHeight); root.style.setProperty("--field-mobile-min-height", layout.mobileMinHeight); world.fieldType = field.id; const rect = canvas.getBoundingClientRect(); world.setViewportSize(rect.width, rect.height, { resizeWorld: true, scaleContents: true }); resizeCanvas(); } function normalizeResetPresetId(presetId = "default") { if (window.TarinaiResetPresets?.normalize) return window.TarinaiResetPresets.normalize(presetId); return new Set(["empty", "default", "athletic", "crowded", "family", "war", "happy", "hair_trigger"]).has(String(presetId || "")) ? String(presetId) : "default"; } function syncFieldDialogChoices() { const fieldType = FIELD_TYPES?.[uiCache.resetFieldType]?.id || world?.fieldType || "garden"; const presetId = normalizeResetPresetId(uiCache.resetPreset || "default"); uiCache.resetFieldType = fieldType; uiCache.resetPreset = presetId; ui.fieldDialog?.querySelectorAll("[data-field-type]").forEach((btn) => { const active = btn.dataset.fieldType === fieldType; btn.classList.toggle("active", active); btn.setAttribute("aria-pressed", active ? "true" : "false"); }); ui.fieldDialog?.querySelectorAll("[data-reset-preset]").forEach((btn) => { const active = btn.dataset.resetPreset === presetId; btn.classList.toggle("active", active); btn.setAttribute("aria-pressed", active ? "true" : "false"); }); } function selectResetFieldType(fieldType = "garden") { uiCache.resetFieldType = FIELD_TYPES?.[fieldType]?.id || "garden"; syncFieldDialogChoices(); } function selectResetPreset(presetId = "default") { uiCache.resetPreset = normalizeResetPresetId(presetId); syncFieldDialogChoices(); } function openFieldDialog() { uiCache.resetFieldType = FIELD_TYPES?.[world?.fieldType]?.id || uiCache.resetFieldType || "garden"; uiCache.resetPreset = normalizeResetPresetId(uiCache.resetPreset || "default"); syncFieldDialogChoices(); openManagedDialog(ui.fieldDialog); } function closeFieldDialog() { closeManagedDialog(ui.fieldDialog); } function hydrateLazyImages(root) { const scope = root || document; for (const img of scope.querySelectorAll("img[data-src]")) { if (!img.getAttribute("src")) img.setAttribute("src", img.dataset.src || ""); } } function renderEcologyCards() { if (!ui.ecologyGrid || ui.ecologyGrid.dataset.ready === "1") return; const cards = window.TEXT_CATALOG?.ecologyCards || []; ui.ecologyGrid.innerHTML = cards.map(card => `
${window.TarinaiUIHelpers.htmlEscape(card.title || "")}

${window.TarinaiUIHelpers.htmlEscape(card.text || "")}

`).join(""); ui.ecologyGrid.dataset.ready = "1"; } function openEcologyDialog() { renderEcologyCards(); hydrateLazyImages(ui.ecologyDialog); openManagedDialog(ui.ecologyDialog); } function closeEcologyDialog() { closeManagedDialog(ui.ecologyDialog); } function openArchiveResetDialog() { openManagedDialog(ui.archiveResetDialog); } function closeArchiveResetDialog() { closeManagedDialog(ui.archiveResetDialog); } function resetWithField(fieldType, presetId = uiCache.resetPreset || "default") { const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden; const preset = normalizeResetPresetId(presetId); uiCache.resetFieldType = field.id; uiCache.resetPreset = preset; closeFieldDialog(); applyFieldLayout(field.id); world.reset(null, field.id, preset); uiCache.archiveVersion = ""; renderLog(world.logs); renderArchive(); renderStats(); render(); } function resizeFieldFromWheel(e) { const itemType = toolItemType(world?.tool || ""); if ((e.shiftKey || e.altKey) && itemType && isRotatableItemType(itemType) && world?.rotateToolAngle) { e.preventDefault(); const step = (e.altKey ? 1 : 5) * Math.PI / 180; world.rotateToolAngle(itemType, (e.deltaY > 0 ? 1 : -1) * step); render(); return; } if (!world.setFieldZoom) return; e.preventDefault(); const rect = canvas.getBoundingClientRect(); const sx = e.clientX - rect.left; const sy = e.clientY - rect.top; const focus = world.screenToWorld ? world.screenToWorld(sx, sy) : { x: sx, y: sy }; const factor = e.deltaY > 0 ? 1 / 1.08 : 1.08; if (!world.setFieldZoom((world.fieldZoom || 1) * factor)) return; const off = world.fieldScreenOffset ? world.fieldScreenOffset() : { x: 0, y: 0 }; const scale = world.viewScale ? world.viewScale() : 1; world.cameraX = focus.x - (sx - off.x) / scale; world.cameraY = focus.y - (sy - off.y) / scale; world.clampCamera?.(); render(); } function selectTool(tool) { const wasCopyArmed = world.tool === "copy" && tool === "copy" && Boolean(world.copyBuffer); const result = window.TarinaiCommands.dispatch(world, { type: "tool.select", toolId: tool }); const activeTool = result?.toolId || world.tool || tool; if (!["rope", "rod", "spring", "wire", "insulated_wire"].includes(activeTool)) world.pendingLinkEndpoint = null; if (activeTool !== "copy") world.copyBuffer = null; else if (wasCopyArmed) { world.copyBuffer = null; showToast("\u30b3\u30d4\u30fc\u3092\u89e3\u9664\u3057\u307e\u3057\u305f\u3002"); } for (const btn of uiCache.toolButtons) { btn.classList.toggle("selected", btn.dataset.tool === activeTool); } syncToolSizeBadges(); canvas.style.cursor = window.TarinaiUIInputShared.cursorForTool(activeTool) || (activeTool === "observe" ? "default" : (activeTool === "pinch" ? "grab" : (activeTool === "poke" || activeTool === "delete" ? "pointer" : "crosshair"))); } function showToast(text) { ui.toast.textContent = text; ui.toast.classList.remove("hidden"); clearTimeout(showToast._timer); showToast._timer = setTimeout(() => ui.toast.classList.add("hidden"), 1400); } function syncTopButtons() { ui.pauseBtn.textContent = world.paused ? "\u518d\u958b" : "\u4e00\u6642\u505c\u6b62"; if (ui.speedSelect) ui.speedSelect.value = String(world.speed); if (ui.speedMenuBtn) ui.speedMenuBtn.textContent = `速度 ×${world.speed}`; for (const option of ui.speedMenuPanel?.querySelectorAll?.("[data-speed-value]") || []) { const active = Number(option.dataset.speedValue) === Number(world.speed); option.classList.toggle("active", active); option.setAttribute("aria-checked", active ? "true" : "false"); } syncAudioControls(); syncArchiveUpdateButton(); } function archiveAutoUpdateEnabled() { return Boolean(uiCache.archiveUpdateEnabled); } function syncArchiveUpdateButton() { const btn = ui.archiveUpdateToggleBtn; if (!btn) return; const on = archiveAutoUpdateEnabled(); btn.textContent = on ? "\u66f4\u65b0 ON" : "\u66f4\u65b0 OFF"; btn.classList.toggle("active", on); btn.setAttribute("aria-pressed", on ? "true" : "false"); } function setArchiveUpdateEnabled(on) { uiCache.archiveUpdateEnabled = Boolean(on); syncArchiveUpdateButton(); if (uiCache.archiveUpdateEnabled) { uiCache.archiveVersion = ""; uiCache.archiveFamilyVersion = null; uiCache.archiveViewActivated = true; world.familyTreeDirty = true; renderArchive?.({ userRequested: true }); showToast("\u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u3092ON\u306b\u3057\u307e\u3057\u305f\u3002"); } else { if (uiCache.archiveRenderTimer) { clearTimeout(uiCache.archiveRenderTimer); uiCache.archiveRenderTimer = 0; } uiCache.archiveScheduled = false; showToast("\u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u3092OFF\u306b\u3057\u307e\u3057\u305f\u3002"); } }