Skip to content

Ai4RenderCapture.kt

src/jvmMain/kotlin/afield/ai/tools/Ai4RenderCapture.kt · 162 lines

package and imports — 47 lines, from line 1
package afield.ai.tools

import afield.ai.agent.*
import afield.ai.conversation.*
import afield.ai.tools.*
import afield.animation.gesture.*
import afield.animation.interpolation.*
import afield.animation.keyframes.*
import afield.app.diagnostics.*
import afield.app.notifications.*
import afield.app.protocol.*
import afield.app.server.*
import afield.app.settings.*
import afield.audio.synth.*
import afield.data.table.*
import afield.document.context.*
import afield.document.graph.*
import afield.document.history.*
import afield.document.persistence.*
import afield.document.properties.*
import afield.document.workspace.*
import afield.graphics.color.*
import afield.graphics.paint.*
import afield.graphics.vulkan.*
import afield.math.functions.*
import afield.media.capture.*
import afield.media.stream.*
import afield.media.video.*
import afield.runtime.completion.*
import afield.runtime.execution.*
import afield.runtime.kotlin.*
import afield.runtime.lifecycle.*
import afield.tools.automation.*
import afield.tools.docs.*
import afield.ui.controls.*
import afield.ui.layout.*
import afield.ui.notation.*
import afield.ui.output.*
import afield.ui.output.templates.*
import afield.ui.slides.*
import afield.ui.visualization.*
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.util.Window
import afield.graphics.vulkan.util.lastWindowToOpen
import afield.graphics.vulkan.util.openWindowsSnapshot
import java.nio.file.Files
import java.util.Base64

internal fun addWindowsFromValue(value: Any?, windows: MutableList<Window>)

internal fun addWindowsFromValue(value: Any?, windows: MutableList<Window>) {
    when (value) {
        is Window -> windows.add(value)
        is Renderer -> windows.addAll(value.windows)
    }
}

internal fun Box.hierarchyForChatScreenshot(): List<Box>

internal fun Box.hierarchyForChatScreenshot(): List<Box> {
    val seen = mutableSetOf<Box>()
    val queue = ArrayDeque<Box>()
    val ordered = mutableListOf<Box>()
    queue.add(this)
    while (queue.isNotEmpty()) {
        val next = queue.removeFirst()
        if (!seen.add(next)) continue
        ordered.add(next)
        next.parents.forEach { queue.add(it) }
    }
    return ordered
}

internal fun windowsVisibleFrom(box: Box?, globalFallback: Boolean = false): List<Window>

internal fun windowsVisibleFrom(box: Box?, globalFallback: Boolean = false): List<Window> {
    val windows = mutableListOf<Window>()
    if (box != null) {
        box.hierarchyForChatScreenshot().forEach { candidate ->
            candidate.properties.values.forEach { addWindowsFromValue(it, windows) }
            runCatching {
                candidate.ki.context.vars.values.forEach { addWindowsFromValue(it.value, windows) }
            }
        }
    }
    if (globalFallback) {
        openWindowsSnapshot().forEach { windows.add(it) }
        lastWindowToOpen?.let { windows.add(it) }
    }
    return windows.distinct()
}

internal fun chooseChatScreenshotWindow(box: Box?, target: String, globalFallback: Boolean = false): Window?

internal fun chooseChatScreenshotWindow(box: Box?, target: String, globalFallback: Boolean = false): Window? {
    val windows = windowsVisibleFrom(box, globalFallback).filter { window ->
        val renderer = window.associatedRenderer
        renderer != null && renderer.supportsDirectWindowCapture(window)
    }
    if (windows.isEmpty()) return null
    val trimmed = target.trim()
    if (trimmed.isEmpty()) return windows.first()

    trimmed.toIntOrNull()?.let { index ->
        return windows.getOrNull(index - 1)
    }

    return windows.filter { it.title.contains(trimmed, ignoreCase = true) }.singleOrNull()
}

internal fun resolveChatScreenshotBox(uid: String, sessionInfo: SessionInformation?): Box?

internal fun resolveChatScreenshotBox(uid: String, sessionInfo: SessionInformation?): Box? {
    val requestUid = uid.trim()
    if (requestUid.isBlank()) return sessionInfo?.root

    if (sessionInfo != null) {
        if (requestUid == "root" || requestUid == sessionInfo.root.id) return sessionInfo.root
        sessionInfo.boxes[requestUid]?.let { return it }
        return null
    }

    return runCatching { box(requestUid, createIfMissing = false) }.getOrNull()
}

internal suspend fun captureChatScreenshot

body — 46 lines, from line 116
internal suspend fun captureChatScreenshot(
    request: AiChatScreenshotRequest,
    sessionInfo: SessionInformation? = thisSessionInfo.get()
): AiChatScreenshotResponse {
    val requestUid = request.uid.trim()
    val box = resolveChatScreenshotBox(requestUid, sessionInfo)
    if (box == null && requestUid.isNotBlank()) {
        return AiChatScreenshotResponse(success = false, message = "Unknown box '$requestUid'; no fallback capture was taken.")
    }
    if (box == null && requestUid.isBlank() && openWindowsSnapshot().isEmpty() && lastWindowToOpen == null) {
        return AiChatScreenshotResponse(
            success = false,
            message = "No active Field document or capturable Field 4 render window for screenshot."
        )
    }

    val window = chooseChatScreenshotWindow(box, request.target, globalFallback = requestUid.isBlank()) ?: return AiChatScreenshotResponse(
        success = false,
        message = if (requestUid.isNotBlank() && box == null) {
            "No capturable Field 4 render window is visible; box '$requestUid' was not found in the active session."
        } else {
            "No capturable Field 4 render window is visible from this box."
        }
    )
    val renderer = window.associatedRenderer ?: return AiChatScreenshotResponse(
        success = false,
        message = "The selected Field 4 render window is not attached to a Renderer."
    )

    val out = Files.createTempFile("field4_ai_chat_screenshot_", ".jpg")
    try {
        renderer.captureWindowToFile(window, out.toString())
        val bytes = Files.readAllBytes(out)
        val dataUrl = "data:image/jpeg;base64," + Base64.getEncoder().encodeToString(bytes)
        return AiChatScreenshotResponse(
            success = true,
            dataUrl = dataUrl,
            message = "Captured ${window.title} (${window.width} x ${window.height}; JPEG ${bytes.size / 1024} KiB)",
            title = window.title,
            width = window.width,
            height = window.height
        )
    } finally {
        runCatching { Files.deleteIfExists(out) }
    }
}