All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.Composition.kt Maven / Gradle / Ivy

package org.openrndr.extra.composition

import org.openrndr.draw.*
import org.openrndr.math.*
import org.openrndr.math.transforms.*
import org.openrndr.shape.Rectangle
import org.openrndr.shape.Shape
import org.openrndr.shape.ShapeContour
import org.openrndr.shape.bounds
import kotlin.math.*
import kotlin.reflect.*

/**
 * Describes a node in a composition
 */
sealed class CompositionNode {

    var id: String? = null

    var parent: CompositionNode? = null

    /** This CompositionNode's own style. */
    var style: Style = Style()

    /**
     * This CompositionNode's computed style.
     * Where every style attribute is obtained by
     * overwriting the Style in the following order:
     * 1. Default style attributes.
     * 2. Parent Node's computed style's inheritable attributes.
     * 3. This Node's own style attributes.
     */
    val effectiveStyle: Style
        get() = when (val p = parent) {
            is CompositionNode -> style inherit p.effectiveStyle
            else -> style
        }

    /**
     * Custom attributes to be applied to the Node in addition to the Style attributes.
     */
    var attributes = mutableMapOf()

    /**
     * a map that stores user data
     */
    val userData = mutableMapOf()

    /**
     * a [Rectangle] that describes the bounding box of the contents
     */
    abstract val bounds: Rectangle

    val effectiveStroke get() = effectiveStyle.stroke.value
    val effectiveStrokeOpacity get() = effectiveStyle.strokeOpacity.value
    val effectiveStrokeWeight get() = effectiveStyle.strokeWeight.value
    val effectiveMiterLimit get() = effectiveStyle.miterLimit.value
    val effectiveLineCap get() = effectiveStyle.lineCap.value
    val effectiveLineJoin get() = effectiveStyle.lineJoin.value
    val effectiveFill get() = effectiveStyle.fill.value
    val effectiveFillOpacity get() = effectiveStyle.fillOpacity.value
    val effectiveDisplay get() = effectiveStyle.display.value
    val effectiveOpacity get() = effectiveStyle.opacity.value
    val effectiveVisibility get() = effectiveStyle.visibility.value
    val effectiveShadeStyle get() = effectiveStyle.shadeStyle.value

    /** Calculates the absolute transformation of the current node. */
    val effectiveTransform: Matrix44
        get() = when (val p = parent) {
            is CompositionNode -> transform * p.effectiveTransform
            else -> transform
        }

    var stroke
        get() = style.stroke.value
        set(value) {
            style.stroke = when (value) {
                null -> Paint.None
                else -> Paint.RGB(value)
            }
        }
    var strokeOpacity
        get() = style.strokeOpacity.value
        set(value) {
            style.strokeOpacity = Numeric.Rational(value)
        }
    var strokeWeight
        get() = style.strokeWeight.value
        set(value) {
            style.strokeWeight = Length.Pixels(value)
        }
    var miterLimit
        get() = style.miterLimit.value
        set(value) {
            style.miterLimit = Numeric.Rational(value)
        }
    var lineCap
        get() = style.lineCap.value
        set(value) {
            style.lineCap = when (value) {
                org.openrndr.draw.LineCap.BUTT -> LineCap.Butt
                org.openrndr.draw.LineCap.ROUND -> LineCap.Round
                org.openrndr.draw.LineCap.SQUARE -> LineCap.Square
            }
        }
    var lineJoin
        get() = style.lineJoin.value
        set(value) {
            style.lineJoin = when (value) {
                org.openrndr.draw.LineJoin.BEVEL -> LineJoin.Bevel
                org.openrndr.draw.LineJoin.MITER -> LineJoin.Miter
                org.openrndr.draw.LineJoin.ROUND -> LineJoin.Round
            }
        }
    var fill
        get() = style.fill.value
        set(value) {
            style.fill = when (value) {
                null -> Paint.None
                else -> Paint.RGB(value)
            }
        }
    var fillOpacity
        get() = style.fillOpacity.value
        set(value) {
            style.fillOpacity = Numeric.Rational(value)
        }
    var opacity
        get() = style.opacity.value
        set(value) {
            style.opacity = Numeric.Rational(value)
        }
    var shadeStyle
        get() = style.shadeStyle.value
        set(value) {
            style.shadeStyle = Shade.Value(value)
        }
    var transform
        get() = style.transform.value
        set(value) {
            style.transform = Transform.Matrix(value)
        }
}

// TODO: Deprecate this?
operator fun KMutableProperty0.setValue(thisRef: Style, property: KProperty<*>, value: ShadeStyle) {
    this.set(Shade.Value(value))
}

fun transform(node: CompositionNode): Matrix44 =
    (node.parent?.let { transform(it) } ?: Matrix44.IDENTITY) * node.transform

/**
 * a [CompositionNode] that holds a single image [ColorBuffer]
 */
class ImageNode(var image: ColorBuffer, var x: Double, var y: Double, var width: Double, var height: Double) :
    CompositionNode() {
    override val bounds: Rectangle
        get() = Rectangle(0.0, 0.0, width, height).contour.transform(transform(this)).bounds
}

/**
 * a [CompositionNode] that holds a single [Shape]
 */
class ShapeNode(var shape: Shape) : CompositionNode() {
    override val bounds: Rectangle
        get() {
            val t = effectiveTransform
            return if (t === Matrix44.IDENTITY) {
                shape.bounds
            } else {
                shape.bounds.contour.transform(t).bounds
            }
        }

    /**
     * apply transforms of all ancestor nodes and return a new detached org.openrndr.shape.ShapeNode with conflated transform
     */
    fun conflate(): ShapeNode {
        return ShapeNode(shape).also {
            it.id = id
            it.parent = parent
            it.style = style
            it.transform = transform(this)
            it.attributes = attributes
        }
    }


    /**
     * apply transforms of all ancestor nodes and return a new detached shape node with identity transform and transformed Shape
     * @param composition use viewport transform
     */
    fun flatten(composition: Composition? = null): ShapeNode {

        val viewport = composition?.calculateViewportTransform() ?: Matrix44.IDENTITY

        return ShapeNode(shape.transform(viewport * transform(this))).also {
            it.id = id
            it.parent = parent
            it.style = effectiveStyle
            it.attributes = attributes
        }
    }

    fun copy(
        id: String? = this.id,
        parent: CompositionNode? = null,
        style: Style = this.style,
        attributes: MutableMap = this.attributes,
        shape: Shape = this.shape
    ): ShapeNode {
        return ShapeNode(shape).also {
            it.id = id
            it.parent = parent
            it.style = style
            it.attributes = attributes
        }
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is ShapeNode) return false
        if (shape != other.shape) return false
        return true
    }

    override fun hashCode(): Int {
        return shape.hashCode()
    }

    /**
     * the local [Shape] with the [effectiveTransform] applied to it
     */
    val effectiveShape
        get() = shape.transform(effectiveTransform)
}

/**
 * a [CompositionNode] that holds a single text
 */
data class TextNode(var text: String, var contour: ShapeContour?) : CompositionNode() {
    // TODO: This should not be Rectangle.EMPTY
    override val bounds: Rectangle
        get() = Rectangle.EMPTY
}

/**
 * A [CompositionNode] that functions as a group node
 */
open class GroupNode(open val children: MutableList = mutableListOf()) : CompositionNode() {
    override val bounds: Rectangle
        get() {
            return children.map { it.bounds }.bounds
        }

    fun copy(
        id: String? = this.id,
        parent: CompositionNode? = null,
        style: Style = this.style,
        children: MutableList = this.children
    ): GroupNode {
        return GroupNode(children).also {
            it.id = id
            it.parent = parent
            it.style = style
            it.attributes = attributes
        }
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is GroupNode) return false

        if (children != other.children) return false
        return true
    }

    override fun hashCode(): Int {
        return children.hashCode()
    }
}

data class CompositionDimensions(val x: Length, val y: Length, val width: Length, val height: Length) {
    val position = Vector2((x as Length.Pixels).value, (y as Length.Pixels).value)
    val dimensions = Vector2((width as Length.Pixels).value, (height as Length.Pixels).value)

    constructor(rectangle: Rectangle) : this(
        rectangle.corner.x.pixels,
        rectangle.corner.y.pixels,
        rectangle.dimensions.x.pixels,
        rectangle.dimensions.y.pixels
    )

    override fun toString(): String = "$x $y $width $height"

    // I'm not entirely sure why this is needed but
    // but otherwise equality checks will never succeed
    override fun equals(other: Any?): Boolean {
        return other is CompositionDimensions
                && x.value == other.x.value
                && y.value == other.y.value
                && width.value == other.width.value
                && height.value == other.height.value
    }

    override fun hashCode(): Int {
        var result = x.hashCode()
        result = 31 * result + y.hashCode()
        result = 31 * result + width.hashCode()
        result = 31 * result + height.hashCode()
        return result
    }
}

val defaultCompositionDimensions = CompositionDimensions(0.0.pixels, 0.0.pixels, 768.0.pixels, 576.0.pixels)


class GroupNodeStop(children: MutableList) : GroupNode(children)

/**
 * A vector composition.
 * @param root the root node of the composition
 * @param bounds the dimensions of the composition
 */
class Composition(val root: CompositionNode, var bounds: CompositionDimensions = defaultCompositionDimensions) {
    constructor(root: CompositionNode, bounds: Rectangle) : this(root, CompositionDimensions(bounds))

    /** SVG/XML namespaces */
    val namespaces = mutableMapOf()

    var style: Style = Style()

    /**
     * The style attributes affecting the whole document, such as the viewBox area and aspect ratio.
     */
    var documentStyle: DocumentStyle = DocumentStyle()

    init {
        val (x, y, width, height) = bounds
        style.x = x
        style.y = y
        style.width = width
        style.height = height
    }

    fun findShapes() = root.findShapes()
    fun findShape(id: String): ShapeNode? {
        return (root.find { it is ShapeNode && it.id == id }) as? ShapeNode
    }

    fun findImages() = root.findImages()
    fun findImage(id: String): ImageNode? {
        return (root.find { it is ImageNode && it.id == id }) as? ImageNode
    }

    fun findGroups(): List = root.findGroups()
    fun findGroup(id: String): GroupNode? {
        return (root.find { it is GroupNode && it.id == id }) as? GroupNode
    }

    fun clear() = (root as? GroupNode)?.children?.clear()

    /** Calculates the equivalent of `1%` in pixels. */
    internal fun normalizedDiagonalLength(): Double = sqrt(bounds.dimensions.squaredLength / 2.0)

    /**
     * Calculates effective viewport transformation using [viewBox] and [preserveAspectRatio].
     * As per [the SVG 2.0 spec](https://svgwg.org/svg2-draft/single-page.html#coords-ComputingAViewportsTransform).
     */
    fun calculateViewportTransform(): Matrix44 {
        return when (documentStyle.viewBox) {
            ViewBox.None -> Matrix44.IDENTITY
            is ViewBox.Value -> {
                when (val vb = (documentStyle.viewBox as ViewBox.Value).value) {
                    Rectangle.EMPTY -> {
                        // The intent is to not display the element
                        Matrix44.ZERO
                    }

                    else -> {
                        val vbCorner = vb.corner
                        val vbDims = vb.dimensions
                        val eCorner = bounds.position
                        val eDims = bounds.dimensions
                        val (align, meetOrSlice) = documentStyle.preserveAspectRatio

                        val scale = (eDims / vbDims).let {
                            if (align != Align.NONE) {
                                if (meetOrSlice == MeetOrSlice.MEET) {
                                    Vector2(min(it.x, it.y))
                                } else {
                                    Vector2(max(it.x, it.y))
                                }
                            } else {
                                it
                            }
                        }

                        val translate = (eCorner - (vbCorner * scale)).let {
                            val cx = eDims.x - vbDims.x * scale.x
                            val cy = eDims.y - vbDims.y * scale.y
                            it + when (align) {
                                // TODO: This first one probably doesn't comply with the spec
                                Align.NONE -> Vector2.ZERO
                                Align.X_MIN_Y_MIN -> Vector2.ZERO
                                Align.X_MID_Y_MIN -> Vector2(cx / 2, 0.0)
                                Align.X_MAX_Y_MIN -> Vector2(cx, 0.0)
                                Align.X_MIN_Y_MID -> Vector2(0.0, cy / 2)
                                Align.X_MID_Y_MID -> Vector2(cx / 2, cy / 2)
                                Align.X_MAX_Y_MID -> Vector2(cx, cy / 2)
                                Align.X_MIN_Y_MAX -> Vector2(0.0, cy)
                                Align.X_MID_Y_MAX -> Vector2(cx / 2, cy)
                                Align.X_MAX_Y_MAX -> Vector2(cx, cy)
                            }
                        }

                        buildTransform {
                            translate(translate)
                            scale(scale.x, scale.y, 1.0)
                        }
                    }
                }
            }
        }
    }
}

/**
 * remove node from its parent [CompositionNode]
 */
fun CompositionNode.remove() {
    require(parent != null) { "parent is null" }
    val parentGroup = (parent as? GroupNode)
    if (parentGroup != null) {
        val filtered = parentGroup.children.filter {
            it != this
        }
        parentGroup.children.clear()
        parentGroup.children.addAll(filtered)
    }
    parent = null
}

fun CompositionNode.findTerminals(filter: (CompositionNode) -> Boolean): List {
    val result = mutableListOf()
    fun find(node: CompositionNode) {
        when (node) {
            is GroupNode -> node.children.forEach { find(it) }
            else -> if (filter(node)) {
                result.add(node)
            }
        }
    }
    find(this)
    return result
}

fun CompositionNode.findAll(filter: (CompositionNode) -> Boolean): List {
    val result = mutableListOf()
    fun find(node: CompositionNode) {
        if (filter(node)) {
            result.add(node)
        }
        if (node is GroupNode) {
            node.children.forEach { find(it) }
        }
    }
    find(this)
    return result
}

/**
 * Finds first [CompositionNode] to match the given [predicate].
 */
fun CompositionNode.find(predicate: (CompositionNode) -> Boolean): CompositionNode? {
    if (predicate(this)) {
        return this
    } else if (this is GroupNode) {
        val deque: ArrayDeque = ArrayDeque(children)
        while (deque.isNotEmpty()) {
            val node = deque.removeFirst()
            if (predicate(node)) {
                return node
            } else if (node is GroupNode) {
                deque.addAll(node.children)
            }
        }
    }
    return null
}

/**
 * find all descendant [ShapeNode] nodes, including potentially this node
 * @return a [List] of [ShapeNode] nodes
 */
fun CompositionNode.findShapes(): List = findTerminals { it is ShapeNode }.map { it as ShapeNode }

/**
 * find all descendant [ImageNode] nodes, including potentially this node
 * @return a [List] of [ImageNode] nodes
 */
fun CompositionNode.findImages(): List = findTerminals { it is ImageNode }.map { it as ImageNode }

/**
 * find all descendant [GroupNode] nodes, including potentially this node
 * @return a [List] of [GroupNode] nodes
 */
fun CompositionNode.findGroups(): List = findAll { it is GroupNode }.map { it as GroupNode }

/**
 * visit this [CompositionNode] and all descendant nodes and execute [visitor]
 */
fun CompositionNode.visitAll(visitor: (CompositionNode.() -> Unit)) {
    visitor()
    if (this is GroupNode) {
        for (child in children) {
            child.visitAll(visitor)
        }
    }
}

/**
 * org.openrndr.shape.UserData delegate
 */
class UserData(
    val name: String, val initial: T
) {
    @Suppress("UNCHECKED_CAST")
    operator fun getValue(node: CompositionNode, property: KProperty<*>): T {
        val value: T? = node.userData[name] as? T
        return value ?: initial
    }

    operator fun setValue(stylesheet: CompositionNode, property: KProperty<*>, value: T) {
        stylesheet.userData[name] = value
    }
}

fun CompositionNode.filter(filter: (CompositionNode) -> Boolean): CompositionNode? {
    val f = filter(this)

    if (!f) {
        return null
    }

    if (this is GroupNode) {
        val copies = mutableListOf()
        children.forEach {
            val filtered = it.filter(filter)
            if (filtered != null) {
                when (filtered) {
                    is ShapeNode -> {
                        copies.add(filtered.copy(parent = this))
                    }

                    is GroupNode -> {
                        copies.add(filtered.copy(parent = this))
                    }

                    else -> {

                    }
                }
            }
        }
        return GroupNode(children = copies)
    } else {
        return this
    }
}

fun CompositionNode.map(mapper: (CompositionNode) -> CompositionNode): CompositionNode {
    val r = mapper(this)
    return when (r) {
        is GroupNodeStop -> {
            r.copy().also { copy ->
                copy.children.forEach {
                    it.parent = copy
                }
            }
        }

        is GroupNode -> {
            val copy = r.copy(children = r.children.map { it.map(mapper) }.toMutableList())
            copy.children.forEach {
                it.parent = copy
            }
            copy
        }

        else -> r
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy