WebPublication.kt
src/jvmMain/kotlin/afield/document/workspace/WebPublication.kt · 88 lines
package afield.document.workspace
import afield.app.protocol.ExportDocumentResult
import afield.app.protocol.ExportWebDocumentResult
import afield.app.protocol.Set_output
import afield.app.server.SessionInformation
import afield.document.persistence.SavingContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import java.io.File
import afield.app.protocol.NativePublicationImage
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.util.Window
import afield.graphics.vulkan.util.openWindowsSnapshot
import java.util.Base64
import javax.imageio.ImageIO
import afield.tools.docs.raiseAndScreenshot
internal fun nativePublicationTargets(info: SessionInformation): List<Pair<String, Window>>
Resolve only this document's already running renderers, without creating a scripting engine.
internal fun nativePublicationTargets(info: SessionInformation): List<Pair<String, Window>> {
val open = openWindowsSnapshot().toSet()
return info.boxes.values.flatMap { box ->
(box.properties.values.toList() + box.liveKi?.context?.vars?.values.orEmpty().map { it.value })
.mapNotNull { value -> when (value) {
is Window -> value
is Renderer -> value.window
else -> null
} }.filter { it in open }.map { box.id to it }
}.distinctBy { it.second }
}
internal suspend fun captureNativePublication(source: ExportWebDocumentResult, targets: List<Pair<String, Window>>): ExportWebDocumentResult
Uses the same native-window screenshot operation as docs/backdoor captures. Runs off Runloop.
internal suspend fun captureNativePublication(source: ExportWebDocumentResult, targets: List<Pair<String, Window>>): ExportWebDocumentResult {
val images = mutableListOf<NativePublicationImage>()
val warnings = source.warnings.toMutableList()
targets.forEach { (uid, window) ->
val file = kotlin.io.path.createTempFile("field-publication-", ".png").toFile()
try {
window.raiseAndScreenshot(3, file.absolutePath, captureTimeoutMs = 10000)
require(file.length() <= 32 * 1024 * 1024) { "Native image exceeds 32 MB" }
val image = ImageIO.read(file) ?: error("Native capture produced no image")
images.add(NativePublicationImage(uid, "data:image/png;base64," + Base64.getEncoder().encodeToString(file.readBytes()), image.width, image.height))
} catch (error: Exception) {
warnings.add("Native window for $uid could not be captured: ${error.message}")
} finally { file.delete() }
}
return source.copy(nativeImages = images, warnings = warnings)
}
internal fun webPublicationAssets(): List<String>
Enumerate canonical assets so a publication includes Monaco's lazy modules and local fonts.
internal fun webPublicationAssets(): List<String> {
val common = File("src/commonMain/resources")
val js = File("src/jsMain/resources")
val paths = mutableListOf("offline.html", "offline-viewer.js", "offline-outputs.js", "edn-parser.js",
"iosevka.css", "outputareas.css", "plotly-2.24.1.min.js")
paths.addAll(listOf("threejs_viewer.html", "threejs_tools.js", "threejs_camera_link.js", "threejs_shot.js",
"threejs_export.js", "geometry_review_inbox.js", "geometry_review_controls.js"))
listOf("regular", "light", "lightitalic", "bold", "italic", "semicondensedlight", "semicondensedlightitalic")
.forEach { paths.add("woff2/iosevka-$it.woff2") }
listOf(File(js, "monaco/min") to "min", File(common, "vendor/three") to "vendor/three").forEach { (dir, prefix) ->
require(dir.isDirectory) { "Missing publication assets: $dir" }
dir.walkTopDown().filter { it.isFile }.forEach { file ->
paths.add("$prefix/${file.relativeTo(dir).invariantSeparatorsPath}")
}
}
paths.forEach { path ->
require(File(common, path).isFile || File(js, path).isFile || File(js, "monaco/$path").isFile) {
"Missing publication asset: $path"
}
}
return paths.sorted()
}
internal fun exportWebDocument(info: SessionInformation): ExportWebDocumentResult
Reads current state only: publication never evaluates a box or changes the saved document.
internal fun exportWebDocument(info: SessionInformation): ExportWebDocumentResult {
val boxes = info.boxes.values.toMutableSet().also { it.add(info.root) }
val outputs = boxes.flatMap { box ->
box.output.retainedAreas.snapshot(box.id).filter { it.name == "set-output" }.map {
Json.decodeFromJsonElement<Set_output>(it.payload!!)
}
}
return ExportWebDocumentResult(
ExportDocumentResult(SavingContext().exportEdnPackage(boxes, info.filename),
File(info.filename).nameWithoutExtension.ifBlank { "document" }),
outputs, webPublicationAssets()
)
}