Attributes.kt
src/jvmMain/kotlin/afield/geometry/polytope/Attributes.kt · 105 lines
@file:afield.runtime.completion.CompletionCategory("Polytopes")
package afield.geometry.polytope
Untyped per-element data, carried alongside the topology and propagated through transforms.
An attribute is how a caller writes something onto one vertex, edge or face and gets it back later:
a per-face height for a non-uniform kis, a colour that survives into the FLine and triangle
output, a tag a selector can test with ElementSelectors.attributeEquals. What crosses a Conway
operator is decided by an AttributePropagationPolicy rather than by each operator, and the
default — PreserveCommonAttributes — keeps only the entries every source element agreed on.
Values are typed Any?: these are Field's ordinary property values, so a box can put an
Int, a Vec3 or a lambda in one. Two consequences follow: equality is
structural == on Any?, so 1 and 1.0 are different attributes, and nothing here validates a
value against a schema. The two extension functions PolyhedralObject.withAttribute and
PolyhedralObject.withAttributes are the intended entry points, because unlike
PolytopeAttributes itself they check that the element exists.
class PolytopeAttributes private constructor
Immutable element-keyed attributes. Attribute values follow Field's ordinary property model and
are therefore typed as Any; maps are copied, while mutable values remain owned by the caller.
class PolytopeAttributes private constructor(
values: Map<ElementId, Map<String, Any?>>,
) {
private val stored = values.mapValues { (_, attributes) -> attributes.toMap() }
val elementIds: Set<ElementId> get() = stored.keys.toSet()
The elements that have an entry, as a fresh set. An element appears as soon as any name has been set on it — including a name set to null — and there is no way to remove it again.
operator fun get(element: ElementId): Map<String, Any?> = stored[element].orEmpty().toMap()
This element's attributes, as a fresh copy.
An element with no entry returns an empty map rather than null, so a read cannot distinguish "no attributes" from "not part of this topology at all".
fun with(element: ElementId, name: String, value: Any?): PolytopeAttributes
One more name on one element, as a new value. A null value is stored, not treated as a
removal, and it makes element appear in elementIds.
fun with(element: ElementId, name: String, value: Any?): PolytopeAttributes {
require(name.isNotBlank()) { "attribute name must not be blank" }
val next = stored.toMutableMap()
next[element] = stored[element].orEmpty() + (name to value)
return PolytopeAttributes(next)
}
fun with(element: ElementId, attributes: Map<String, Any?>): PolytopeAttributes
Merge a map over one element's existing attributes; later names win.
An empty attributes returns this value untouched and creates no entry — which is why an
element that has only ever been given an empty map does not count towards
PolyhedralObjectSummary.attributedElements.
fun with(element: ElementId, attributes: Map<String, Any?>): PolytopeAttributes {
if (attributes.isEmpty()) return this
require(attributes.keys.none { it.isBlank() }) { "attribute names must not be blank" }
val next = stored.toMutableMap()
next[element] = stored[element].orEmpty() + attributes
return PolytopeAttributes(next)
}
companion object
val EMPTY = PolytopeAttributes(emptyMap())
fun of(values: Map<ElementId, Map<String, Any?>>): PolytopeAttributes
Wrap an existing map. Element ids are not checked against any topology here; that check
belongs to PolyhedralObject's own constructor and to the withAttribute extensions.
fun of(values: Map<ElementId, Map<String, Any?>>): PolytopeAttributes =
if (values.isEmpty()) EMPTY else PolytopeAttributes(values)
}
}
fun PolyhedralObject.withAttribute(element: ElementId, name: String, value: Any?): PolyhedralObject
Write one attribute onto one element, returning a new object — the ordinary way to paint a scalar field or a tag onto a solid before deploying a selective transform.
Rejects an element that is not in this topology, so a stale ElementId from before a transform fails here rather than silently attaching data nothing will ever read.
fun PolyhedralObject.withAttribute(element: ElementId, name: String, value: Any?): PolyhedralObject {
require(topology.element(element) != null) { "element ${element.value} is not in this topology" }
return copy(attributes = attributes.with(element, name, value))
}
fun PolyhedralObject.withAttributes(element: ElementId, values: Map<String, Any?>): PolyhedralObject
withAttribute for several names at once; existing names not mentioned in values survive.
An empty values still validates the element and still allocates a new object, but the attributes
are the same value.
fun PolyhedralObject.withAttributes(element: ElementId, values: Map<String, Any?>): PolyhedralObject {
require(topology.element(element) != null) { "element ${element.value} is not in this topology" }
return copy(attributes = attributes.with(element, values))
}