Skip to content

copy.kt

src/jvmMain/kotlin/afield/document/workspace/copy.kt · 156 lines

package afield.document.workspace

import afield.data.table.div
import afield.data.table.get
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.document.graph.box
import afield.document.graph.nested
import afield.runtime.execution.plus
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem

import afield.audio.synth.*
import afield.document.graph.Box
import afield.document.graph.collectAllNestedBoxes
import afield.document.graph.isTrue
import afield.document.graph.makeKI
import afield.document.graph.needsResync
import afield.document.graph.nestedIn
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.ui.output.templates.filter
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

Duplicating boxes inside a document: copyBox clones one box, copyBoxes clones a selection and rewires its connections, and nextCopyName names the results.

copyBox copies the persistent properties except the graph edges (parents, children, nested, nestedIn), gives the copy a fresh uuid, a (copy) or (copy n) name and its own engine, connects it under root, and by default offsets its frame by 80 in x and y. copyBoxes also takes every box nested inside the selection and skips tombstoned ones. Connections between copied boxes point at the copies; connections to boxes outside the selection are kept, so a copy stays wired to the same outside parents and children. Only the top-level boxes of the selection are offset, so nested copies keep their place inside their copied container.

private val copyRegex = Regex("""^(.*) \(copy(?: (\d+))?\)$""")

fun nextCopyName(name: String): String

Returns the next copy-style name for name.

Existing suffixes like " (copy)" or " (copy 3)" are advanced; otherwise " (copy)" is appended.

fun nextCopyName(name: String): String {
    val match = copyRegex.matchEntire(name)

    return if (match != null) {
        val base = match.groupValues[1]
        val numberPart = match.groupValues[2]

        val nextNumber = if (numberPart.isEmpty()) {
            2
        } else {
            numberPart.toInt() + 1
        }

        "$base (copy $nextNumber)"
    } else {
        "$name (copy)"
    }
}

fun copyBoxes(boxes: List<Box>): List<Box>

Copies boxes and everything nested in them, as described above, and returns the new boxes.

body — 50 lines, from line 70
fun copyBoxes(boxes: List<Box>): List<Box> {
    val sourceBoxes = collectAllNestedBoxes(
        boxes.filter { !it.getPropertyRaw("_tombstone").isTrue() }.toSet()
    ).filter { !it.getPropertyRaw("_tombstone").isTrue() }

    if (sourceBoxes.isEmpty()) return emptyList()

    val sourceSet = sourceBoxes.toSet()
    val topLevelSources = sourceSet.filter { it.nestedIn !in sourceSet }.toSet()

    val oldToNew = sourceBoxes.associateWith { source ->
        copyBox(source, offsetFrame = source in topLevelSources)
    }

    oldToNew.entries.forEach { (older, newer) ->
        older.parents.forEach { parent ->
            if (parent.getPropertyRaw("_tombstone").isTrue()) return@forEach
            when (parent) {
                newer.root -> {}
                in oldToNew -> {
                    oldToNew[parent]!!.connect(newer)
                }

                else -> {
                    parent.connect(newer)
                }
            }
        }
        older.children.forEach { child ->
            if (child.getPropertyRaw("_tombstone").isTrue()) return@forEach
            when (child) {
                in oldToNew -> {
                    newer.connect(oldToNew[child]!!)
                }

                else -> {
                    newer.connect(child)
                }
            }
        }
    }

    oldToNew.entries.forEach { (older, newer) ->
        val oldNestedParent = older.nestedIn ?: return@forEach
        if (oldNestedParent.getPropertyRaw("_tombstone").isTrue()) return@forEach
        newer.nestedIn = oldToNew[oldNestedParent] ?: oldNestedParent
    }

    return oldToNew.values.toList()
}

fun copyBox(b: Box, offsetFrame: Boolean = true): Box

A copy of b in the same document, offset by 80 in x and y when offsetFrame is true.

@OptIn(ExperimentalUuidApi::class)
fun copyBox(b: Box, offsetFrame: Boolean = true): Box {
    val newBox = Box(id = Uuid.random().toString())

    val exclude = setOf("parents", "children", "nested", "nestedIn")
    Box.ioPersistentProperties.forEach { name ->
        if (name in b.properties && name !in exclude) {
            newBox.properties[name] = b.properties[name]
            if (Box.uiResyncProperties.any { it.matches(name) }) {
                needsResync.add(newBox to name)
            }
        }
    }

    val newName = nextCopyName(b.name)
    newBox.properties["name"] = newName
    newBox.name = newName
    newBox.inside = b.inside
    newBox.frame = if (offsetFrame) {
        b.frame.copy(x = b.frame.x + 80, y = b.frame.y + 80)
    } else {
        b.frame.copy()
    }

    b.root.connect(newBox)
    b.inside.boxes[newBox.id] = newBox
    // newBox's own engine: `ki` is a per-box delegate, never among the properties copied above, so
    // nothing here can be the source box's engine.
    newBox.replaceKi(makeKI(newBox, reason = "copyBox"))
    newBox.name = newName


    return newBox
}