SolidCraft.kt
src/jvmMain/kotlin/afield/geometry/solid/solidcraft/SolidCraft.kt · 187 lines
@file:afield.runtime.completion.CompletionCategory("Graphics")
package afield.geometry.solid.solidcraft
import afield.geometry.mesh.TriMesh
import org.json.JSONObject
import java.lang.foreign.Arena
import java.lang.foreign.FunctionDescriptor
import java.lang.foreign.Linker
import java.lang.foreign.MemorySegment
import java.lang.foreign.SymbolLookup
import java.lang.foreign.ValueLayout.ADDRESS
import java.lang.foreign.ValueLayout.JAVA_BYTE
import java.lang.foreign.ValueLayout.JAVA_DOUBLE
import java.lang.foreign.ValueLayout.JAVA_INT
import java.lang.foreign.ValueLayout.JAVA_LONG
import java.lang.invoke.MethodHandle
import java.nio.file.Files
import java.nio.file.Path
An optional bridge to the SolidCraft B-rep document engine, whose model stays in native Rust code.
SolidCraftLibrary locates the native library and reports whether it is available. SolidCraftDesign.open loads a design, SolidCraftDesign.setParameter edits it, and SolidCraftDesign.mesh copies a body out as a SolidCraftMesh for display.
object SolidCraftLibrary
Optional SolidCraft B-rep/document engine. Its model remains in Rust; Field copies display meshes.
object SolidCraftLibrary {
private val fileName: String get() = when {
System.getProperty("os.name").contains("win", ignoreCase = true) -> "field_solidcraft_jvm.dll"
System.getProperty("os.name").contains("mac", ignoreCase = true) -> "libfield_solidcraft_jvm.dylib"
else -> "libfield_solidcraft_jvm.so"
}
val searchedPaths: List<Path> get()
val searchedPaths: List<Path> get() = listOfNotNull(
System.getProperty("afield.solidcraft.library")?.takeIf(String::isNotBlank),
System.getenv("AFIELD_SOLIDCRAFT_LIBRARY")?.takeIf(String::isNotBlank),
Path.of("build", "native", "solidcraft", fileName).toString(),
).map(Path::of)
val resolvedPath: Path? get() = searchedPaths.firstOrNull { Files.isRegularFile(it) }
val isAvailable: Boolean get() = resolvedPath != null
fun describe(): String
body — 58 lines, from line 45
fun describe(): String = resolvedPath?.let { "SolidCraft bridge at $it" }
?: "SolidCraft bridge unavailable. Run ./gradlew buildSolidCraftNative or set AFIELD_SOLIDCRAFT_LIBRARY."
}
private object Native {
private val path = SolidCraftLibrary.resolvedPath
?: throw IllegalStateException(SolidCraftLibrary.describe())
private val lookup = SymbolLookup.libraryLookup(path.toAbsolutePath(), Arena.global())
private val linker = Linker.nativeLinker()
private fun method(name: String, descriptor: FunctionDescriptor): MethodHandle {
val symbol = lookup.find("field_solidcraft_$name")
.orElseThrow { IllegalStateException("SolidCraft bridge lacks $name at $path") }
return linker.downcallHandle(symbol, descriptor)
}
val version = method("abi_version", FunctionDescriptor.of(JAVA_INT))
val lastError = method("error", FunctionDescriptor.of(ADDRESS))
val open = method("open", FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_LONG))
val close = method("close", FunctionDescriptor.ofVoid(ADDRESS))
val setParameter = method("set_parameter", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS))
val summary = method("summary", FunctionDescriptor.of(ADDRESS, ADDRESS))
val mesh = method("mesh", FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_LONG, JAVA_DOUBLE))
val meshClose = method("mesh_close", FunctionDescriptor.ofVoid(ADDRESS))
val meshVertexCount = method("mesh_vertex_count", FunctionDescriptor.of(JAVA_LONG, ADDRESS))
val meshTriangleCount = method("mesh_triangle_count", FunctionDescriptor.of(JAVA_LONG, ADDRESS))
val meshPositions = method("mesh_positions", FunctionDescriptor.of(ADDRESS, ADDRESS))
val meshTriangles = method("mesh_triangles", FunctionDescriptor.of(ADDRESS, ADDRESS))
val meshFaceIds = method("mesh_face_ids", FunctionDescriptor.of(ADDRESS, ADDRESS))
val export = method("export", FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_INT))
val bytesLen = method("bytes_len", FunctionDescriptor.of(JAVA_LONG, ADDRESS))
val bytesPtr = method("bytes_ptr", FunctionDescriptor.of(ADDRESS, ADDRESS))
val bytesClose = method("bytes_close", FunctionDescriptor.ofVoid(ADDRESS))
init {
val actual = version.invokeWithArguments() as Int
require(actual == 1) { "SolidCraft bridge ABI $actual; Field needs 1 at $path" }
}
fun failure(): Nothing {
val pointer = lastError.invokeWithArguments() as MemorySegment
val message = if (pointer.address() == 0L) "unknown native error" else pointer.reinterpret(16_384).getString(0)
throw IllegalStateException("SolidCraft: $message")
}
fun bytes(handle: MemorySegment): ByteArray {
if (handle.address() == 0L) failure()
try {
val length = bytesLen.invokeWithArguments(handle) as Long
require(length in 0..(256L shl 20)) { "SolidCraft returned an oversized buffer: $length" }
if (length == 0L) return ByteArray(0)
val pointer = bytesPtr.invokeWithArguments(handle) as MemorySegment
require(pointer.address() != 0L) { "SolidCraft returned a null data pointer" }
return pointer.reinterpret(length).toArray(JAVA_BYTE)
} finally {
bytesClose.invokeWithArguments(handle)
}
}
}
data class SolidCraftMesh(val positions: DoubleArray, val indices: IntArray, val faceIds: IntArray)
Double-precision SolidCraft tessellation plus B-rep face IDs, copied out of Rust memory.
fun toTriMesh(): TriMesh
fun toTriMesh(): TriMesh = TriMesh(FloatArray(positions.size) { positions[it].toFloat() }, indices.copyOf())
}
class SolidCraftDesign private constructor(private var handle: MemorySegment) : AutoCloseable
A saved SolidCraft design with an incremental evaluator. Close it when finished.
class SolidCraftDesign private constructor(private var handle: MemorySegment) : AutoCloseable {
private fun current(): MemorySegment {
check(handle.address() != 0L) { "SolidCraft design is closed" }
return handle
}
@Synchronized fun summary(): JSONObject
@Synchronized fun summary(): JSONObject = JSONObject(String(Native.bytes(Native.summary.invokeWithArguments(current()) as MemorySegment), Charsets.UTF_8))
fun setParameter(name: String, expression: String)
An invalid expression or failed feature leaves the previous design and mesh intact.
@Synchronized
fun setParameter(name: String, expression: String) {
Arena.ofConfined().use { arena ->
val ok = Native.setParameter.invokeWithArguments(current(), arena.allocateFrom(name), arena.allocateFrom(expression)) as Int
if (ok != 1) Native.failure()
}
}
@Synchronized fun mesh(bodyIndex: Int = 0, tolerance: Double = 0.05): SolidCraftMesh
@Synchronized fun mesh(bodyIndex: Int = 0, tolerance: Double = 0.05): SolidCraftMesh {
require(bodyIndex >= 0)
val mesh = Native.mesh.invokeWithArguments(current(), bodyIndex.toLong(), tolerance) as MemorySegment
if (mesh.address() == 0L) Native.failure()
try {
val vertices = Native.meshVertexCount.invokeWithArguments(mesh) as Long
val triangles = Native.meshTriangleCount.invokeWithArguments(mesh) as Long
require(vertices in 1..10_000_000 && triangles in 1..20_000_000) {
"SolidCraft mesh too large: $vertices vertices, $triangles triangles"
}
val pos = Native.meshPositions.invokeWithArguments(mesh) as MemorySegment
val tri = Native.meshTriangles.invokeWithArguments(mesh) as MemorySegment
val faces = Native.meshFaceIds.invokeWithArguments(mesh) as MemorySegment
val p = pos.reinterpret(vertices * 3 * 8)
val t = tri.reinterpret(triangles * 3 * 4)
val f = faces.reinterpret(triangles * 4)
return SolidCraftMesh(
DoubleArray((vertices * 3).toInt()) { p.getAtIndex(JAVA_DOUBLE, it.toLong()) },
IntArray((triangles * 3).toInt()) { t.getAtIndex(JAVA_INT, it.toLong()) },
IntArray(triangles.toInt()) { f.getAtIndex(JAVA_INT, it.toLong()) },
)
} finally {
Native.meshClose.invokeWithArguments(mesh)
}
}
@Synchronized fun exportStep(): ByteArray
@Synchronized fun exportStep(): ByteArray = Native.bytes(Native.export.invokeWithArguments(current(), 1) as MemorySegment)
@Synchronized fun exportStl(): ByteArray
@Synchronized fun exportStl(): ByteArray = Native.bytes(Native.export.invokeWithArguments(current(), 2) as MemorySegment)
@Synchronized fun saveDesign(): ByteArray
@Synchronized fun saveDesign(): ByteArray = Native.bytes(Native.export.invokeWithArguments(current(), 3) as MemorySegment)
@Synchronized override fun close()
@Synchronized override fun close() {
if (handle.address() != 0L) {
Native.close.invokeWithArguments(handle)
handle = MemorySegment.NULL
}
}
companion object
companion object {
fun openBundledExample(): SolidCraftDesign
The small box, fillet and hole design shipped with Field's SolidCraft starting point.
fun openBundledExample(): SolidCraftDesign {
val path = "/afield/solidcraft/m0-box-fillet-hole.solidcraft"
val stream = SolidCraftDesign::class.java.getResourceAsStream(path)
?: throw IllegalStateException("Missing bundled SolidCraft example: $path")
return stream.use { open(it.readBytes()) }
}
fun open(bytes: ByteArray): SolidCraftDesign
fun open(bytes: ByteArray): SolidCraftDesign {
require(bytes.isNotEmpty()) { "empty SolidCraft design" }
Arena.ofConfined().use { arena ->
val input = arena.allocate(bytes.size.toLong())
input.copyFrom(MemorySegment.ofArray(bytes))
val handle = Native.open.invokeWithArguments(input, bytes.size.toLong()) as MemorySegment
if (handle.address() == 0L) Native.failure()
return SolidCraftDesign(handle)
}
}