├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── jsMain │ ├── kotlin │ │ ├── package-info.kt │ │ ├── util │ │ │ ├── GLMatrixT.kt │ │ │ ├── History.kt │ │ │ ├── DOMUtil.kt │ │ │ ├── ResizeTracker.kt │ │ │ ├── Color.kt │ │ │ └── GLMatrix.kt │ │ ├── main │ │ │ ├── RUtil.kt │ │ │ ├── Popup.kt │ │ │ ├── PolyStyle.kt │ │ │ ├── RenderUtil.kt │ │ │ ├── Main.js.kt │ │ │ ├── SvgPolygon.kt │ │ │ ├── RootPane.kt │ │ │ ├── ConfigPopup.kt │ │ │ └── ExportPopup.kt │ │ ├── common_util │ │ │ └── RunSynchronously.kt │ │ ├── components │ │ │ ├── PValueComponent.kt │ │ │ ├── PComponent.kt │ │ │ ├── PDropdown.kt │ │ │ ├── PCheckbox.kt │ │ │ ├── Dropdown.kt │ │ │ └── PSlider.kt │ │ ├── poly │ │ │ ├── LightingContext.kt │ │ │ ├── Indicator.kt │ │ │ ├── ExportStl.kt │ │ │ ├── EdgeProgram.kt │ │ │ ├── ViewContext.kt │ │ │ ├── DrawScene.kt │ │ │ ├── ViewBaseProgram.kt │ │ │ ├── ExportScad.kt │ │ │ ├── EdgeContext.kt │ │ │ ├── FaceProgram.kt │ │ │ └── TransformAnimation.kt │ │ ├── worker │ │ │ ├── WorkerTask.kt │ │ │ └── WorkerMain.kt │ │ ├── params │ │ │ ├── AnimationTracker.kt │ │ │ ├── Animation.kt │ │ │ └── Parser.kt │ │ └── glsl │ │ │ ├── GLUtil.kt │ │ │ ├── GLType.kt │ │ │ ├── GLBuffer.kt │ │ │ ├── GLBlock.kt │ │ │ ├── GLDecl.kt │ │ │ └── GLProgram.kt │ └── resources │ │ └── index.html ├── commonMain │ └── kotlin │ │ ├── package-info.kt │ │ ├── util │ │ ├── Tagged.kt │ │ ├── RunSynchronously.common.kt │ │ ├── OperationProgressContext.kt │ │ ├── PolarReciprociation.kt │ │ ├── Delegates.kt │ │ ├── LexicographicListComparator.kt │ │ ├── Vec2.kt │ │ ├── Fmt.kt │ │ ├── MathUtil.kt │ │ ├── CollectionUtil.kt │ │ ├── Mat3.kt │ │ ├── Plane.kt │ │ ├── IdMap.kt │ │ ├── Quat.kt │ │ └── Vec3.kt │ │ ├── poly │ │ ├── PolyUtil.kt │ │ ├── FEV.kt │ │ ├── Scale.kt │ │ ├── Topology.kt │ │ ├── PolyhedronSerializer.kt │ │ ├── MidPoint.kt │ │ ├── FaceRim.kt │ │ ├── PolygonProjection.kt │ │ ├── Validation.kt │ │ ├── Isomorphism.kt │ │ ├── Essence.kt │ │ └── Seed.kt │ │ └── transform │ │ ├── Truncate.kt │ │ ├── Bevel.kt │ │ ├── Cantellate.kt │ │ ├── Snub.kt │ │ ├── TransformCache.kt │ │ ├── Drop.kt │ │ ├── Canonical.kt │ │ ├── Chamfer.kt │ │ └── Transform.kt ├── jvmMain │ └── kotlin │ │ ├── common_util │ │ └── RunSynchronously.kt │ │ └── FindInvalidTransform.kt └── commonTest │ └── kotlin │ ├── TestUtil.kt │ ├── TreeMapTest.kt │ ├── QuatTest.kt │ └── ValidatePolyhedra.kt ├── settings.gradle.kts ├── gradle.properties ├── deploy.sh ├── README.md ├── gradlew.bat └── gradlew /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | build -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizarov/PolyhedraExplorer/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/jsMain/kotlin/package-info.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | // A workaround to tag a package for IDE 6 | package polyhedra.js -------------------------------------------------------------------------------- /src/commonMain/kotlin/package-info.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | // A workaround to tag a package for IDE 6 | package polyhedra.common 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | rootProject.name = "PolyhedraExplorer" 3 | 4 | pluginManagement { 5 | repositories { 6 | mavenCentral() 7 | maven { url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev/") } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/Tagged.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | interface Tagged { 8 | val tag: String 9 | } 10 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/RunSynchronously.common.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | expect fun runSynchronously(block: suspend () -> T): T -------------------------------------------------------------------------------- /src/jsMain/kotlin/util/GLMatrixT.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.util 6 | 7 | import org.khronos.webgl.* 8 | 9 | typealias quat_t = Float32Array 10 | 11 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/main/RUtil.kt: -------------------------------------------------------------------------------- 1 | package polyhedra.js.main 2 | 3 | import kotlinx.html.* 4 | import kotlinx.html.js.* 5 | import org.w3c.dom.events.* 6 | import react.dom.* 7 | 8 | fun RDOMBuilder.onClick(handler: (Event) -> Unit) { 9 | attrs { 10 | onClickFunction = handler 11 | } 12 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/OperationProgressContext.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | fun interface OperationProgressContext { 8 | // done percent from 0 to 100 9 | fun reportProgress(done: Int) 10 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/main/Popup.kt: -------------------------------------------------------------------------------- 1 | package polyhedra.js.main 2 | 3 | sealed class Popup { 4 | object Config : Popup() 5 | object Export : Popup() 6 | object Seed : Popup() 7 | object AddTransform : Popup() 8 | data class ModifyTransform(val index: Int) : Popup() 9 | object Faces : Popup() 10 | object Edges : Popup() 11 | object Vertices : Popup() 12 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/PolarReciprociation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | fun Plane.dualPoint(r: Double): Vec3 = 8 | (r * r / d) * this 9 | 10 | fun Vec3.dualPlane(r: Double): Plane { 11 | val n = norm 12 | return Plane(this / n, r * r / n) 13 | } 14 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/poly/PolyUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.poly 6 | 7 | fun Map.directedEdgeToFaceVertexMap(): Map> = 8 | entries 9 | .groupBy{ it.key.r } 10 | .mapValues { e -> 11 | e.value.associateBy({ it.key.a }, { it.value }) 12 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/common_util/RunSynchronously.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | @file:Suppress("PackageDirectoryMismatch") 6 | 7 | package polyhedra.common.util 8 | 9 | actual fun runSynchronously(block: suspend () -> T): T { 10 | throw UnsupportedOperationException("Not supported on JS") 11 | } 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/common_util/RunSynchronously.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | @file:Suppress("PackageDirectoryMismatch") 6 | 7 | package polyhedra.common.util 8 | 9 | import kotlinx.coroutines.* 10 | 11 | actual fun runSynchronously(block: suspend () -> T): T = 12 | runBlocking { 13 | block() 14 | } 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.mpp.enableGranularSourceSetsMetadata=true 3 | kotlin.native.enableDependencyPropagation=false 4 | kotlin.js.generate.executable.default=false 5 | 6 | kotlin.incremental.js.klib=false 7 | 8 | kotlin-serialization-version=1.2.1 9 | kotlin-coroutines-version=1.5.0 10 | kotlin-react-version=17.0.1-pre.148-kotlin-1.4.21 11 | react-version=17.0.1 12 | gl-matrix-version=3.3.0 13 | history-version=5.0.0 14 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/util/History.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | @file:JsModule("history") 6 | @file:JsNonModule 7 | package polyhedra.js.util 8 | 9 | external interface HashHistory { 10 | val location: Location 11 | fun push(path: String) 12 | } 13 | 14 | external interface Location { 15 | val pathname: String 16 | } 17 | 18 | external fun createHashHistory(): HashHistory 19 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e # exit on the first error 3 | ./gradlew jsBrowserDistribution 4 | rm -rf build/gh-pages 2> /dev/null 5 | git worktree add -f build/gh-pages gh-pages 6 | cp -r build/distributions/* build/gh-pages 7 | if [ -z $1 ] ; then 8 | echo "Use ./deploy.sh to deploy a specific version" 9 | else 10 | version=$1 11 | git tag -f $version 12 | pushd build/gh-pages 13 | git add * 14 | git commit -m "Version $version" 15 | git push origin gh-pages 16 | popd 17 | fi -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/Delegates.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | import kotlin.reflect.* 8 | 9 | class DelegateProvider(val factory: (name: String) -> R) { 10 | operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): R = factory(prop.name) 11 | } 12 | 13 | class ValueDelegate(val value: R) { 14 | operator fun getValue(thisRef: Any?, prop: KProperty<*>): R = value 15 | } 16 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/util/DOMUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.util 6 | 7 | import kotlinx.browser.* 8 | import org.w3c.dom.* 9 | import org.w3c.dom.events.* 10 | import kotlin.experimental.* 11 | 12 | fun HTMLElement.computedStyle() = 13 | window.document.defaultView!!.getComputedStyle(this) 14 | 15 | fun MouseEvent.isLeftButtonEvent() = 16 | button.toInt() == 0 17 | 18 | fun MouseEvent.isLeftButtonPressed() = 19 | (buttons and 1) != 0.toShort() -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/LexicographicListComparator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | class LexicographicListComparator(val comparator: Comparator) : Comparator> { 8 | override fun compare(a: List, b: List): Int { 9 | val n = minOf(a.size, b.size) 10 | for (i in 0 until n) { 11 | val c = comparator.compare(a[i], b[i]) 12 | if (c != 0) return c 13 | } 14 | if (a.size < b.size) return -1 15 | if (a.size > b.size) return 1 16 | return 0 17 | } 18 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/TestUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | fun testParameter(name: String, list: Iterable, block: (T) -> Unit) { 6 | for (value in list) { 7 | try { 8 | block(value) 9 | } catch (e: Throwable) { 10 | val msg = e.message 11 | val cause = if (e is TestParameterException) e.cause!! else e 12 | val sep = if (e is TestParameterException) "," else ":" 13 | throw TestParameterException("$name = $value$sep $msg", cause) 14 | } 15 | } 16 | } 17 | 18 | class TestParameterException(message: String, cause: Throwable) : Exception(message, cause) -------------------------------------------------------------------------------- /src/jsMain/kotlin/util/ResizeTracker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.util 6 | 7 | import kotlinx.browser.* 8 | 9 | object ResizeTracker { 10 | private val listeners = ArrayList<() -> Unit>() 11 | 12 | fun add(listener: () -> Unit) { 13 | if (listeners.isEmpty()) { 14 | window.onresize = { 15 | listeners.forEach { it() } 16 | } 17 | } 18 | listeners += listener 19 | } 20 | 21 | fun remove(listener: () -> Unit) { 22 | listeners -= listener 23 | if (listeners.isEmpty()) { 24 | window.onresize = null 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/Vec2.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | import kotlin.math.* 8 | 9 | interface Vec2 { 10 | val x: Double 11 | val y: Double 12 | } 13 | 14 | fun Vec2(x: Double, y: Double): Vec2 = MutableVec2(x, y) 15 | 16 | open class MutableVec2( 17 | override var x: Double = 0.0, 18 | override var y: Double = 0.0 19 | ) : Vec2 { 20 | constructor(v: Vec2) : this(v.x, v.y) 21 | override fun toString(): String = "[${x.fmt}, ${y.fmt}]" 22 | } 23 | 24 | fun norm(x: Double, y: Double): Double = 25 | sqrt(sqr(x) + sqr(y)) 26 | 27 | val Vec2.norm: Double 28 | get() = norm(x, y) 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/components/PValueComponent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.components 6 | 7 | import polyhedra.js.params.* 8 | import react.* 9 | 10 | external interface PValueComponentProps> : PComponentProps { 11 | var disabled: Boolean 12 | } 13 | 14 | external interface PValueComponentState : RState { 15 | var value: T 16 | } 17 | 18 | abstract class PValueComponent, P : PValueComponentProps, S : PValueComponentState>( 19 | props: P 20 | ) : PComponent(props) { 21 | override fun S.init(props: P) { 22 | value = props.params.targetValue 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Polyhedra Explorer 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/Fmt.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | import kotlin.math.* 8 | 9 | private const val defaultPrecision = 4 10 | 11 | fun Double.fmt(precision: Int): String { 12 | val p = 10.0.pow(precision) 13 | return ((this * p).roundToLong() / p).toString() 14 | } 15 | 16 | fun Float.fmt(precision: Int): String = toDouble().fmt(precision) 17 | 18 | val Double.fmt: String 19 | get() = fmt(defaultPrecision) 20 | 21 | fun Double.fmtFix(precision: Int): String { 22 | val p = 10.0.pow(precision) 23 | val m = (this * p).roundToLong().toString().padStart(precision + 1, '0') 24 | val i = m.length - precision 25 | return m.substring(0, i) + "." + m.substring(i) 26 | } 27 | 28 | val Double.fmtFix: String 29 | get() = fmtFix(defaultPrecision) 30 | 31 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/components/PComponent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.components 6 | 7 | import polyhedra.js.params.* 8 | import react.* 9 | 10 | external interface PComponentProps : RProps { 11 | var params: V 12 | } 13 | 14 | abstract class PComponent, S : RState>( 15 | props: P, 16 | private val tracksUpdateType: Param.UpdateType = Param.TargetValue 17 | ) : RComponent(props) { 18 | private lateinit var dependency: Param.Dependency 19 | 20 | abstract override fun S.init(props: P) 21 | 22 | final override fun componentDidMount() { 23 | dependency = props.params.onNotifyUpdated(tracksUpdateType) { setState { init(props) } } 24 | } 25 | 26 | final override fun componentWillUnmount() { 27 | dependency.destroy() 28 | } 29 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/poly/FEV.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.poly 6 | 7 | // Faces, Edge, Vertices (counts) 8 | data class FEV( 9 | val f: Int, 10 | val e: Int, 11 | val v: Int 12 | ) { 13 | override fun toString(): String = "F=$f, E=$e, V=$v" 14 | } 15 | 16 | fun Polyhedron.fev(): FEV = FEV(fs.size, es.size, vs.size) 17 | 18 | class TransformFEV( 19 | val ff: Int, val fe: Int, val fv: Int, 20 | val ef: Int, val ee: Int, val ev: Int, 21 | val vf: Int, val ve: Int, val vv: Int 22 | ) { 23 | companion object { 24 | val ID = TransformFEV( 25 | 1, 0, 0, 26 | 0, 1, 0, 27 | 0, 0, 1 28 | ) 29 | } 30 | } 31 | 32 | operator fun TransformFEV.times(p: FEV): FEV = with(p) { 33 | FEV( 34 | ff * f + fe * e + fv * v, 35 | ef * f + ee * e + ev * v, 36 | vf * f + ve * e + vv * v 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/poly/Scale.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.poly 6 | 7 | import polyhedra.common.transform.* 8 | import polyhedra.common.util.* 9 | 10 | enum class Scale(override val tag: String) : Tagged { 11 | Inradius("i"), 12 | Midradius("m"), 13 | Circumradius("c"); 14 | } 15 | 16 | val Scales: List by lazy { Scale.values().toList() } 17 | 18 | private object ScaledKey 19 | 20 | fun Polyhedron.scaled(factor: Double): Polyhedron = transformedPolyhedron(ScaledKey, factor) { 21 | for (v in vs) vertex(factor * v, v.kind) 22 | for (f in fs) face(f) 23 | 24 | } 25 | 26 | fun Polyhedron.scaled(scale: Scale?): Polyhedron { 27 | if (scale == null) return this 28 | val current = scaleDenominator(scale) 29 | if (current approx 1.0) return this // fast path, don't occupy cache slot 30 | return scaled(1 / current) 31 | } 32 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/poly/LightingContext.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.poly 6 | 7 | import polyhedra.js.glsl.* 8 | import polyhedra.js.params.* 9 | 10 | class LightningContext(params: LightingParams) : Param.Context(params) { 11 | private val ambientLight by { params.ambientLight.value } 12 | private val diffuseLight by { params.diffuseLight.value } 13 | private val specularLight by { params.specularLight.value } 14 | 15 | val specularLightPower by { params.specularPower.value } 16 | 17 | val ambientLightColor = float32Of(0.3, 0.3, 0.3) 18 | val diffuseLightColor = float32Of(1.0, 1.0, 1.0) 19 | val specularLightColor = float32Of(1.0, 1.0, 1.0) 20 | val lightPosition = float32Of(-1.2, 1.2, 4.0) 21 | 22 | init { setup() } 23 | 24 | override fun update() { 25 | ambientLightColor.fill(ambientLight) 26 | diffuseLightColor.fill(diffuseLight) 27 | specularLightColor.fill(specularLight) 28 | } 29 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/components/PDropdown.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.components 6 | 7 | import polyhedra.common.util.* 8 | import polyhedra.js.params.* 9 | import react.* 10 | 11 | fun RBuilder.pDropdown(param: EnumParam, disabled: Boolean = false) { 12 | child>, PDropdown> { 13 | attrs { 14 | this.params = param 15 | this.disabled = disabled 16 | } 17 | } 18 | } 19 | 20 | @Suppress("NON_EXPORTABLE_TYPE") 21 | @JsExport 22 | class PDropdown(props: PValueComponentProps>) : PValueComponent, PValueComponentProps>, PValueComponentState>(props) { 23 | override fun RBuilder.render() { 24 | dropdown { 25 | disabled = props.disabled 26 | value = props.params.value 27 | options = props.params.options 28 | onChange = { props.params.updateValue(it) } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/worker/WorkerTask.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.worker 6 | 7 | import kotlinx.serialization.* 8 | import polyhedra.common.poly.* 9 | import polyhedra.common.transform.* 10 | import polyhedra.common.util.* 11 | 12 | @Serializable 13 | sealed class WorkerTask> { 14 | abstract suspend fun invoke(progress: OperationProgressContext): R 15 | } 16 | 17 | @Serializable 18 | sealed class WorkerResult { 19 | abstract val value: T 20 | } 21 | 22 | @Serializable 23 | data class TransformTask( 24 | val poly: Polyhedron, 25 | val transform: Transform 26 | ) : WorkerTask() { 27 | override suspend fun invoke(progress: OperationProgressContext): PolyhedronResult { 28 | val value = when(val atx = transform.asyncTransform) { 29 | null -> poly.transformed(transform) 30 | else -> atx(poly, progress) 31 | } 32 | return PolyhedronResult(value) 33 | } 34 | } 35 | 36 | @Serializable 37 | data class PolyhedronResult(override val value: Polyhedron): WorkerResult() 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/poly/Topology.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.poly 6 | 7 | fun Polyhedron.hasSameTopology(other: Polyhedron): Boolean { 8 | val nv = vs.size 9 | if (nv != other.vs.size) return false 10 | val ne = es.size 11 | if (ne != other.es.size) return false 12 | val nf = fs.size 13 | if (nf != other.fs.size) return false 14 | for (i in 0 until nv) { 15 | if (vs[i].kind != other.vs[i].kind) return false 16 | } 17 | for (i in 0 until ne) { 18 | val e1 = es[i] 19 | val e2 = other.es[i] 20 | if (e1.a.id != e2.a.id) return false 21 | if (e1.b.id != e2.b.id) return false 22 | if (e1.l.id != e2.l.id) return false 23 | if (e1.r.id != e2.r.id) return false 24 | } 25 | for (i in 0 until nf) { 26 | val f1 = fs[i] 27 | val f2 = other.fs[i] 28 | if (f1.kind != f2.kind) return false 29 | val m = f1.size 30 | if (m != f2.size) return false 31 | for (j in 0 until m) { 32 | if (f1[j].id != f2[j].id) return false 33 | } 34 | } 35 | return true 36 | } 37 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/params/AnimationTracker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.params 6 | 7 | import kotlinx.browser.* 8 | import polyhedra.js.util.* 9 | 10 | private const val MAX_DT = 0.05 // 20 fps min 11 | 12 | class AnimationTracker(private val rootParams: Param) { 13 | private var prevTime = Double.NaN 14 | private var animationHandle = 0 15 | 16 | fun start() { 17 | rootParams.onNotifyUpdated(Param.AnyUpdate, ::requestAnimationFrame) 18 | requestAnimationFrame() 19 | } 20 | 21 | private fun requestAnimationFrame() { 22 | if (animationHandle != 0) return 23 | animationHandle = window.requestAnimationFrame(animationFun) 24 | } 25 | 26 | private val animationFun: (Double) -> Unit = af@{ nowTime -> 27 | animationHandle = 0 28 | val dt = if (prevTime.isNaN()) 0.0 else (nowTime - prevTime) / 1000 // in seconds 29 | prevTime = nowTime 30 | rootParams.performUpdate(null, dt.coerceAtMost(MAX_DT)) 31 | if (animationHandle == 0) { 32 | // no further request 33 | prevTime = Double.NaN 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/main/PolyStyle.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.main 6 | 7 | import polyhedra.common.poly.* 8 | import polyhedra.common.util.* 9 | import polyhedra.js.util.* 10 | 11 | private const val hue0 = 57.0 / 360 12 | private const val divisor = 4 13 | 14 | private fun paletteColor(id: Int): Color { 15 | var phase = 0.0 16 | var count = divisor 17 | var rem = id 18 | while (rem >= count) { 19 | rem -= count 20 | if (phase > 0) count *= 2 21 | phase += 0.5 / count 22 | } 23 | return hslColor(hue0 + phase + rem.toDouble() / count, 0.8, 0.5) 24 | } 25 | 26 | object PolyStyle { 27 | val edgeColor = hslColor(0.0, 0.0, 0.1) 28 | fun faceColor(f: Face): Color = 29 | paletteColor(f.kind.id) 30 | fun vertexColor(v: Vertex): Color = 31 | paletteColor(v.kind.id) 32 | } 33 | 34 | enum class Display(override val tag: String) : Tagged { 35 | All("a"), 36 | Faces("f"), 37 | Edges("e") 38 | } 39 | 40 | val Displays = Display.values().toList() 41 | 42 | fun Display.hasFaces() = this != Display.Edges 43 | fun Display.hasEdges() = this != Display.Faces 44 | 45 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/poly/Indicator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.poly 6 | 7 | import polyhedra.common.poly.* 8 | import polyhedra.common.transform.* 9 | 10 | class Indicator( 11 | val classes: String, 12 | val text: String, 13 | val tooltip: String 14 | ) 15 | 16 | class IndicatorMessage( 17 | val indicator: Indicator, 18 | val value: T 19 | ) 20 | 21 | operator fun Indicator.invoke(value: T) = IndicatorMessage(this, value) 22 | operator fun Indicator.invoke() = IndicatorMessage(this, Unit) 23 | 24 | val TransformFailed = Indicator("emoji", "❌", "{} Transformation has failed") 25 | val SomeFacesNotPlanar = Indicator("emoji", "⚠️", "Some faces are not planar, apply canonical transformation") 26 | val FaceNotPlanar = Indicator("emoji", "⚠️", "Face is not planar") 27 | val TransformIsId = Indicator("fa fa-recycle", "", "{} transformation is not doing anything here") 28 | val TransformNotApplicable = Indicator("emoji", "\uD83D\uDED1", "{} transformation is not applicable") 29 | val TooLarge = Indicator("fa fa-ban", "", "Polyhedron is too large to display ({})") -------------------------------------------------------------------------------- /src/jsMain/kotlin/main/RenderUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.main 6 | 7 | import kotlinx.html.* 8 | import polyhedra.js.poly.* 9 | import react.* 10 | import react.dom.* 11 | 12 | fun RBuilder.messageSpan(msg: IndicatorMessage) { 13 | span(msg.indicator.classes) { +msg.indicator.text } 14 | aside("tooltip-text") { 15 | +msg.indicator.tooltip.replace("{}", msg.value.toString()) 16 | } 17 | } 18 | 19 | fun RBuilder.groupHeader(text: String) { 20 | div("text-row") { 21 | div("header") { +text } 22 | } 23 | } 24 | 25 | fun RBuilder.tableBody(block: RDOMBuilder.() -> Unit) { 26 | table { 27 | tbody(block = block) 28 | } 29 | } 30 | 31 | fun RDOMBuilder.controlRow(label: String, block: RDOMBuilder.() -> Unit) { 32 | tr("control") { 33 | td { +label } 34 | td(block = block) 35 | } 36 | } 37 | 38 | fun RDOMBuilder.controlRow2( 39 | label: String, 40 | block1: RDOMBuilder.() -> Unit, 41 | block2: RDOMBuilder.() -> Unit 42 | ) { 43 | tr("control") { 44 | td { +label } 45 | td(block = block1) 46 | td(block = block2) 47 | } 48 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/MathUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | import kotlin.math.* 8 | 9 | const val EPS = 1e-10 10 | 11 | infix fun Double.approx(x: Double): Boolean = abs(this - x) < EPS 12 | 13 | object DoubleApproxComparator : Comparator { 14 | override fun compare(a: Double, b: Double): Int { 15 | val d = a - b 16 | return when { 17 | abs(d) < EPS -> 0 18 | d < 0 -> -1 19 | else -> 1 20 | } 21 | } 22 | } 23 | 24 | fun sqr(x: Double): Double = x * x 25 | 26 | fun frac(x: Double) = x - floor(x) 27 | 28 | infix fun Double.mod(m: Double): Double { 29 | val x = this / m 30 | return frac(x) * m 31 | } 32 | 33 | fun det( 34 | m11: Double, m12: Double, 35 | m21: Double, m22: Double 36 | ): Double = 37 | m11 * m22 - m12 * m21 38 | 39 | fun det( 40 | m11: Double, m12: Double, m13: Double, 41 | m21: Double, m22: Double, m23: Double, 42 | m31: Double, m32: Double, m33: Double 43 | ): Double = 44 | m11 * det(m22, m23, m32, m33) - 45 | m21 * det(m12, m13, m32, m33) + 46 | m31 * det(m12, m13, m22, m23) 47 | 48 | fun Double.toDegrees() = 49 | this * 180 / PI -------------------------------------------------------------------------------- /src/jvmMain/kotlin/FindInvalidTransform.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.jvm 6 | 7 | import polyhedra.common.poly.* 8 | import polyhedra.common.transform.* 9 | 10 | fun main() { 11 | val seeds = Seeds.filter { it.type == SeedType.Platonic } 12 | val transforms = listOf(Transform.Truncated, Transform.Dual, Transform.Rectified, Transform.Cantellated) 13 | fun found(d: Int, seed: Seed, ts: List, poly: Polyhedron): Boolean { 14 | if (d == 0) return try { 15 | poly.validate() 16 | false 17 | } catch(e: Exception) { 18 | println("Found invalid transform sequence") 19 | println("Seed = $seed") 20 | println("Transforms: $ts") 21 | true 22 | } 23 | for (t in transforms) { 24 | if (found(d - 1, seed, ts + t, poly.transformed(t))) { 25 | return true 26 | } 27 | } 28 | return false 29 | } 30 | var depth = 0 31 | loop@while (true) { 32 | depth++ 33 | for (s in seeds) { 34 | if (found(depth, s, listOf(), s.poly)) { 35 | break@loop 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/CollectionUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | inline fun Iterable.avgOf(selector: (T) -> Double): Double { 8 | var n = 0 9 | var sum = 0.0 10 | for (e in this) { 11 | n++ 12 | sum += selector(e) 13 | } 14 | return sum / n 15 | } 16 | 17 | fun List.updatedAt(index: Int, value: T): List { 18 | val result = toMutableList() 19 | result[index] = value 20 | return result 21 | } 22 | 23 | fun List.removedAt(index: Int): List { 24 | val result = toMutableList() 25 | result.removeAt(index) 26 | return result 27 | } 28 | 29 | inline fun Sequence.distinctIndexed(transform: (Int) -> R): Map { 30 | val result = mutableMapOf() 31 | var index = 0 32 | for (e in this) { 33 | if (e !in result) result[e] = transform(index++) 34 | } 35 | return result 36 | } 37 | 38 | inline fun Iterable.distinctIndexed(transform: (Int) -> R): Map { 39 | val result = mutableMapOf() 40 | var index = 0 41 | for (e in this) { 42 | if (e !in result) result[e] = transform(index++) 43 | } 44 | return result 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/Mat3.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | import kotlin.math.* 8 | 9 | data class Mat3( 10 | val x: Vec3, 11 | val y: Vec3, 12 | val z: Vec3 13 | ) { 14 | override fun toString(): String = "[$x, $y, $z]" 15 | 16 | companion object { 17 | val ID = Mat3( 18 | Vec3(1.0, 0.0, 0.0), 19 | Vec3(0.0, 1.0, 0.0), 20 | Vec3(0.0, 0.0, 1.0) 21 | ) 22 | } 23 | } 24 | 25 | operator fun Mat3.plus(m: Mat3): Mat3 = Mat3(x + m.x, y + m.y, z + m.z) 26 | 27 | operator fun Mat3.times(a: Double): Mat3 = Mat3(x * a, y * a, z * a) 28 | operator fun Double.times(m: Mat3): Mat3 = m * this 29 | 30 | operator fun Vec3.times(m: Mat3): Vec3 = Vec3( 31 | x * m.x.x + y * m.y.x + z * m.z.x, 32 | x * m.x.y + y * m.y.y + z * m.z.y, 33 | x * m.x.z + y * m.y.z + z * m.z.z 34 | ) 35 | 36 | fun Vec3.crossMat(): Mat3 = Mat3( 37 | Vec3(0.0, -z, y), 38 | Vec3(z, 0.0, -x), 39 | Vec3(-y, x, 0.0) 40 | ) 41 | 42 | infix fun Vec3.outerProd(u: Vec3) = Mat3(x * u, y * u, z * u) 43 | 44 | fun rotationMat(u: Vec3, a: Double) = 45 | cos(a) * Mat3.ID + sin(a) * u.crossMat() + (1 - cos(a)) * (u outerProd u) 46 | 47 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/TreeMapTest.kt: -------------------------------------------------------------------------------- 1 | import polyhedra.common.util.* 2 | import kotlin.random.* 3 | import kotlin.test.* 4 | 5 | /* 6 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 7 | */ 8 | 9 | class TreeMapTest { 10 | @Test 11 | fun testLinearAdds() { 12 | val n = 100 13 | val t = TreeMap() 14 | for (i in 0 until n) { 15 | t[i] = i.toString() 16 | check(t.keys.toList() == (0..i).toList()) 17 | check(t.values.toList() == (0..i).map { it.toString() }) 18 | } 19 | for (i in 0 until n) { 20 | check(i in t) 21 | } 22 | for (i in 0 until n) { 23 | check(t[i] == i.toString()) 24 | } 25 | } 26 | 27 | @Test 28 | fun testRandomAdds() { 29 | val n = 100 30 | val t = TreeMap() 31 | val r = (0 until n).shuffled(Random(1)) 32 | for (i in 0 until n) { 33 | t[r[i]] = r[i].toString() 34 | val keys = r.subList(0, i + 1).sorted() 35 | check(t.keys.toList() == keys) 36 | check(t.values.toList() == keys.map { it.toString() }) 37 | } 38 | for (i in 0 until n) { 39 | check(i in t) 40 | } 41 | for (i in 0 until n) { 42 | check(t[r[i]] == r[i].toString()) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/util/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.util 6 | 7 | import org.khronos.webgl.* 8 | import polyhedra.common.util.* 9 | import polyhedra.js.glsl.* 10 | import kotlin.math.* 11 | 12 | data class Color( 13 | val r: Float, 14 | val g: Float, 15 | val b: Float, 16 | val a: Float = 1.0f 17 | ) { 18 | override fun toString(): String = 19 | "Color(${r.fmt(3)}, ${g.fmt(3)}, ${b.fmt(3)}, ${a.fmt(3)})" 20 | } 21 | 22 | fun hslColor(h: Double, s: Double, l: Double, a: Double = 1.0): Color { 23 | val c = (1 - (2 * l - 1).absoluteValue) * s // chroma 24 | val p = frac(h) * 6 25 | val x = c * (1 - abs((p mod 2.0) - 1)) 26 | val m = (l - c / 2).toFloat() 27 | val cm = (c + m).toFloat() 28 | val xm = (x + m).toFloat() 29 | val af = a.toFloat() 30 | return when (p.toInt()) { 31 | 0 -> Color(cm, xm, m, af) 32 | 1 -> Color(xm, cm, m, af) 33 | 2 -> Color(m, cm, xm, af) 34 | 3 -> Color(m, xm, cm, af) 35 | 4 -> Color(xm, m, cm, af) 36 | else -> Color(cm, m, xm, af) 37 | } 38 | } 39 | 40 | fun Color.toFloat32Array4(): Float32Array = float32Of(r, g, b, a) 41 | 42 | fun Color.toRgbString() = 43 | "rgb(${r.intColor},${g.intColor},${b.intColor})" 44 | 45 | private val Float.intColor 46 | get() = (this * 255).roundToInt() -------------------------------------------------------------------------------- /src/jsMain/kotlin/components/PCheckbox.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.components 6 | 7 | import kotlinx.html.* 8 | import kotlinx.html.js.* 9 | import polyhedra.js.main.* 10 | import polyhedra.js.params.* 11 | import react.* 12 | import react.dom.* 13 | 14 | fun RBuilder.pCheckbox(param: BooleanParam, disabled: Boolean = false) { 15 | child(PCheckbox::class) { 16 | attrs { 17 | this.params = param 18 | this.disabled = disabled 19 | } 20 | } 21 | } 22 | 23 | @Suppress("NON_EXPORTABLE_TYPE") 24 | @JsExport 25 | class PCheckbox(props: PValueComponentProps) : PValueComponent, PValueComponentState>(props) { 26 | override fun RBuilder.render() { 27 | div("checkbox") { 28 | input(InputType.checkBox) { 29 | attrs { 30 | disabled = props.disabled 31 | // See https://github.com/JetBrains/kotlin-wrappers/issues/35 32 | this["checked"] = state.value 33 | onChangeFunction = { 34 | props.params.toggle() 35 | } 36 | } 37 | } 38 | span("checkmark") { 39 | onClick { props.params.toggle() } 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/poly/ExportStl.kt: -------------------------------------------------------------------------------- 1 | package polyhedra.js.poly 2 | 3 | import polyhedra.common.poly.* 4 | import polyhedra.common.util.* 5 | import kotlin.math.* 6 | 7 | fun FaceContext.exportSolidToStl(name: String, description: String, exportParams: FaceExportParams): String { 8 | val q = poly.rotationWithLargestFaceDown() 9 | val ofs = MutableVec3(0.0, 0.0, Double.POSITIVE_INFINITY) 10 | exportVertices(exportParams) { av -> 11 | val v = av.rotated(q) 12 | ofs.z = min(ofs.z, v.z) 13 | } 14 | return buildString { 15 | appendLine("solid $name ; $description") 16 | val normal = MutableVec3() 17 | exportTriangles(exportParams) { av1, av2, av3 -> 18 | val v1 = av1.rotated(q) 19 | val v2 = av2.rotated(q) 20 | val v3 = av3.rotated(q) 21 | normal.setToZero() 22 | crossCenteredAddTo(normal, v1, v2, v3) 23 | normal /= normal.norm 24 | appendLine(normal.toStl("facet normal")) 25 | appendLine("outer loop") 26 | appendLine((v1 - ofs).toStl("vertex")) 27 | appendLine((v2 - ofs).toStl("vertex")) 28 | appendLine((v3 - ofs).toStl("vertex")) 29 | appendLine("endloop") 30 | appendLine("endfacet") 31 | } 32 | appendLine("endsolid $name") 33 | } 34 | } 35 | 36 | private fun Vec3.toStl(lbl: String) = "$lbl ${x.fmt} ${y.fmt} ${z.fmt}" 37 | 38 | private fun Polyhedron.rotationWithLargestFaceDown(): Quat { 39 | val f = faceKinds.values.maxByOrNull { it.essence().area() }!! 40 | return rotationBetweenQuat(f, Vec3(0.0, 0.0, -1.0)) 41 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/poly/PolyhedronSerializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.poly 6 | 7 | import kotlinx.serialization.* 8 | import kotlinx.serialization.descriptors.* 9 | import kotlinx.serialization.encoding.* 10 | import polyhedra.common.util.* 11 | 12 | class PolyhedronSerializer : KSerializer { 13 | private val serializer = SerializedPolyhedron.serializer() 14 | override val descriptor: SerialDescriptor = serializer.descriptor 15 | 16 | override fun deserialize(decoder: Decoder): Polyhedron = 17 | decoder.decodeSerializableValue(serializer).toPolyhedron() 18 | 19 | override fun serialize(encoder: Encoder, value: Polyhedron) = 20 | encoder.encodeSerializableValue(serializer, value.toSerialized()) 21 | } 22 | 23 | @Serializable 24 | private class SerializedPolyhedron( 25 | val vs: List, 26 | val fs: List 27 | ) 28 | 29 | @Serializable 30 | private class SerializedVertex( 31 | override val x: Double, 32 | override val y: Double, 33 | override val z: Double, 34 | val kind: VertexKind 35 | ) : Vec3 36 | 37 | @Serializable 38 | private class SerializedFace( 39 | val fvs: List, 40 | val kind: FaceKind 41 | ) 42 | 43 | private fun Polyhedron.toSerialized() = SerializedPolyhedron( 44 | vs.map { v -> SerializedVertex(v.x, v.y, v.z, v.kind) }, 45 | fs.map { f -> SerializedFace(f.fvs.map { it.id }, f.kind) } 46 | ) 47 | 48 | private fun SerializedPolyhedron.toPolyhedron() = polyhedron { 49 | for (v in vs) vertex(v, v.kind) 50 | for (f in fs) face(f.fvs, f.kind) 51 | } 52 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/poly/MidPoint.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.poly 6 | 7 | import polyhedra.common.util.* 8 | 9 | enum class MidPoint { Tangent, Center, Closest } 10 | 11 | // returns 0.0 when tangent point is a, 1.0 -- when it is b, or a fraction in between 12 | fun tangentFraction(a: Vec3, b: Vec3): Double { 13 | val dx = b.x - a.x 14 | val dy = b.y - a.y 15 | val dz = b.z - a.z 16 | return -(a.x * dx + a.y * dy + a.z * dz) / (sqr(dx) + sqr(dy) + sqr(dz)) 17 | } 18 | 19 | // distance from origin to line A-B 20 | fun tangentDistance(a: Vec3, b: Vec3): Double = 21 | tangentFraction(a, b).distanceAtSegment(a, b) 22 | 23 | fun isTangentInSegment(a: Vec3, b: Vec3): Boolean = 24 | tangentFraction(a, b) in EPS..1 - EPS 25 | 26 | fun midPointFraction(a: Vec3, b: Vec3, midPoint: MidPoint): Double { 27 | if (midPoint == MidPoint.Center) return 0.5 28 | val f = tangentFraction(a, b) 29 | if (f !in EPS..1 - EPS) { // !isTangentInSegment 30 | if (midPoint == MidPoint.Tangent) return 0.5 31 | return if (a.norm < b.norm + EPS) 0.0 else 1.0 // closest 32 | } 33 | return f 34 | } 35 | 36 | fun Edge.isTangentInSegment(): Boolean = 37 | isTangentInSegment(a, b) 38 | 39 | fun Edge.midPointFraction(midPoint: MidPoint): Double = 40 | midPointFraction(a, b, midPoint) 41 | 42 | fun Edge.midPoint(midPoint: MidPoint): Vec3 = 43 | midPointFraction(midPoint).atSegment(a, b) 44 | 45 | fun Edge.tangentPoint(): Vec3 = 46 | tangentFraction(a, b).atSegment(a, b) 47 | 48 | fun Edge.tangentDistance(): Double = 49 | tangentFraction(a, b).distanceAtSegment(a, b) 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/components/Dropdown.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.components 6 | 7 | import kotlinx.html.js.* 8 | import org.w3c.dom.* 9 | import react.* 10 | import react.dom.* 11 | 12 | external interface DropdownProps : RProps { 13 | var disabled: Boolean 14 | var value: T 15 | var options: Collection 16 | var onChange: (T) -> Unit 17 | } 18 | 19 | fun RBuilder.dropdown(handler: DropdownProps.() -> Unit) { 20 | child, Dropdown> { 21 | attrs { 22 | disabled = false 23 | handler() 24 | } 25 | } 26 | } 27 | 28 | @Suppress("NON_EXPORTABLE_TYPE") 29 | @JsExport 30 | class Dropdown(props: DropdownProps) : RComponent, RState>(props) { 31 | override fun RBuilder.render() { 32 | div("select") { 33 | select { 34 | attrs { 35 | disabled = props.disabled 36 | this["value"] = props.value.toString() 37 | onChangeFunction = { event -> 38 | val valueString = (event.target as HTMLSelectElement).value 39 | val value = props.options.first { it.toString() == valueString } 40 | props.onChange(value) 41 | } 42 | } 43 | for (opt in props.options) { 44 | option { 45 | attrs { 46 | value = opt.toString() 47 | } 48 | +opt.toString() 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/poly/EdgeProgram.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.poly 6 | 7 | import polyhedra.js.glsl.* 8 | import org.khronos.webgl.WebGLRenderingContext as GL 9 | 10 | class EdgeProgram(gl: GL) : ViewBaseProgram(gl) { 11 | val uVertexColor by uniform(GLType.vec4, GLPrecision.lowp) 12 | 13 | val uTargetFraction by uniform(GLType.float) 14 | val uPrevFraction by uniform(GLType.float) 15 | 16 | val aPosition by attribute(GLType.vec3) 17 | val aNormal by attribute(GLType.vec3) 18 | 19 | val aPrevPosition by attribute(GLType.vec3) 20 | val aPrevNormal by attribute(GLType.vec3) 21 | 22 | private val vColorMul by varying(GLType.float) 23 | 24 | val fInterpolatedPosition by function(GLType.vec3) { 25 | aPosition * uTargetFraction + aPrevPosition * uPrevFraction 26 | } 27 | 28 | val fInterpolatedNormal by function(GLType.vec3) { 29 | aNormal * uTargetFraction + aPrevNormal * uPrevFraction 30 | } 31 | 32 | // world position of the current element 33 | val fPosition by function(GLType.vec4) { 34 | fViewPosition(fInterpolatedPosition(), fInterpolatedNormal()) 35 | } 36 | 37 | // world normal of the current element 38 | val fNormal by function(GLType.vec3) { 39 | uNormalMatrix * fInterpolatedNormal() 40 | } 41 | 42 | override val vertexShader = shader(ShaderType.Vertex) { 43 | val position by fPosition() 44 | gl_Position by uProjectionMatrix * position 45 | vColorMul by fCullMull(position, fNormal()) 46 | } 47 | 48 | override val fragmentShader = shader(ShaderType.Fragment) { 49 | gl_FragColor by uVertexColor * vColorMul 50 | } 51 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/params/Animation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.params 6 | 7 | import polyhedra.common.util.* 8 | 9 | abstract class ValueUpdateAnimation>( 10 | protected val param: P, 11 | private val duration: Double 12 | ) { 13 | init { require(duration > 0) } 14 | 15 | abstract val value: T 16 | 17 | private var position = 0.0 18 | 19 | protected val fraction: Double 20 | get() = (position / duration).coerceIn(0.0, 1.0) 21 | 22 | val isOver: Boolean 23 | get() = position >= duration 24 | 25 | fun update(dt: Double) { 26 | position += dt 27 | if (isOver) param.resetValueUpdateAnimation() 28 | } 29 | } 30 | 31 | class DoubleUpdateAnimation( 32 | param: DoubleParam, 33 | duration: Double, 34 | private val oldValue: Double 35 | ) : ValueUpdateAnimation(param, duration) { 36 | override val value: Double get() { 37 | val f = fraction 38 | return oldValue * (1 - f) + param.targetValue * f 39 | } 40 | } 41 | 42 | class RotationUpdateAnimation( 43 | param: RotationParam, 44 | duration: Double, 45 | private val oldValue: Quat 46 | ) : ValueUpdateAnimation(param, duration) { 47 | override val value: Quat get() { 48 | val f = fraction 49 | return (oldValue * (1 - f) + param.targetValue * f).unit 50 | } 51 | } 52 | 53 | class RotationAnimation( 54 | private val param: RotationParam, 55 | private val animation: RotationAnimationParams 56 | ) { 57 | fun update(dt: Double) { 58 | param.rotate((dt * animation.animatedRotationAngles).anglesToQuat(), Param.None) 59 | } 60 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/components/PSlider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.components 6 | 7 | import kotlinx.html.* 8 | import kotlinx.html.js.* 9 | import org.w3c.dom.* 10 | import polyhedra.common.util.* 11 | import polyhedra.js.params.* 12 | import react.* 13 | import react.dom.* 14 | import kotlin.math.* 15 | 16 | external interface PSliderProps : PValueComponentProps { 17 | var showValue: Boolean 18 | } 19 | 20 | fun RBuilder.pSlider(param: DoubleParam, disabled: Boolean = false, showValue: Boolean = true) { 21 | child(PSlider::class) { 22 | attrs { 23 | this.params = param 24 | this.disabled = disabled 25 | this.showValue = showValue 26 | } 27 | } 28 | } 29 | 30 | @Suppress("NON_EXPORTABLE_TYPE") 31 | @JsExport 32 | class PSlider(props: PSliderProps) : PValueComponent>(props) { 33 | private fun Double.intStr() = roundToInt().toString() 34 | 35 | override fun RBuilder.render() { 36 | input(InputType.range) { 37 | attrs { 38 | disabled = props.disabled 39 | with(props) { 40 | min = (params.min / params.step).intStr() 41 | max = (params.max / params.step).intStr() 42 | value = (state.value / params.step).intStr() 43 | } 44 | onChangeFunction = { event -> 45 | props.params.updateValue((event.target as HTMLInputElement).value.toInt() * props.params.step) 46 | } 47 | } 48 | } 49 | if (props.showValue) { 50 | span { +state.value.fmt } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/main/Main.js.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.main 6 | 7 | import kotlinx.browser.* 8 | import polyhedra.js.params.* 9 | import polyhedra.js.poly.* 10 | import polyhedra.js.util.* 11 | import polyhedra.js.worker.* 12 | import react.dom.* 13 | 14 | private const val historyPushThrottle = 500 15 | 16 | fun main() { 17 | if (runWorkerMain()) return 18 | // kotlinext.js.require("./css/style.css") 19 | window.onload = { onLoad() } 20 | } 21 | 22 | class RootParams : Param.Composite("") { 23 | val animationParams = using(ViewAnimationParams("a")) 24 | val render = using(RenderParams("", animationParams)) 25 | val export = using(ExportParams("e")) 26 | } 27 | 28 | private fun onLoad() { 29 | val rootParams = loadAndAutoSaveRootParams() 30 | AnimationTracker(rootParams).start() 31 | // Unit UI 32 | render(document.getElementById("root")) { 33 | child(RootPane::class) { 34 | attrs { 35 | params = rootParams 36 | } 37 | } 38 | } 39 | } 40 | 41 | private fun loadAndAutoSaveRootParams(): RootParams { 42 | val rootParams = RootParams() 43 | val history = createHashHistory() 44 | var historyPushTimeout = 0 45 | val path = decodeURI(history.location.pathname) 46 | rootParams.loadFromString(path.substringAfter('/', "")) 47 | rootParams.onNotifyUpdated(Param.TargetValue) { 48 | // throttle updates 49 | if (historyPushTimeout == 0) { 50 | historyPushTimeout = window.setTimeout({ 51 | historyPushTimeout = 0 52 | history.push("/$rootParams") 53 | }, historyPushThrottle) 54 | } 55 | } 56 | return rootParams 57 | } 58 | 59 | external fun decodeURI(s: String): String 60 | 61 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/main/SvgPolygon.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.main 6 | 7 | import kotlinx.html.* 8 | import polyhedra.common.poly.* 9 | import polyhedra.common.util.* 10 | import polyhedra.js.util.* 11 | import react.* 12 | import react.dom.* 13 | 14 | fun RBuilder.svgPolygon(classes: String, figure: PolygonProjection, stroke: Color, fill: Color) { 15 | val x0 = figure.vs.minOf { it.x } 16 | val y0 = figure.vs.minOf { it.y } 17 | val w0 = figure.vs.maxOf { it.x } - x0 18 | val h0 = figure.vs.maxOf { it.y } - y0 19 | val sw = maxOf(w0, h0) / 20 20 | svg( 21 | classes = classes, 22 | viewBox = "${(x0 - sw).fmt} ${(y0 - sw).fmt} ${(w0 + 2 * sw).fmt} ${(h0 + 2 * sw).fmt}", 23 | stroke = stroke.toRgbString(), 24 | strokeWidth = sw.fmt, 25 | fill = fill.toRgbString() 26 | ) { 27 | polygon(figure.vs.joinToString(" ") { "${it.x.fmt},${it.y.fmt}" }) 28 | } 29 | } 30 | 31 | inline fun RBuilder.svg( 32 | classes: String, 33 | viewBox: String, 34 | stroke: String, 35 | strokeWidth: String, 36 | fill: String, 37 | block: RDOMBuilder.() -> Unit 38 | ): ReactElement = 39 | tag(block) { SVG( 40 | attributesMapOf( 41 | "class", classes, 42 | "viewBox", viewBox, 43 | "stroke", stroke, 44 | "strokeWidth", strokeWidth, 45 | "fill", fill 46 | ), it) 47 | } 48 | 49 | open class POLYGON(initialAttributes : Map, override val consumer : TagConsumer<*>) : 50 | HTMLTag("polygon", consumer, initialAttributes, "http://www.w3.org/2000/svg", false, true) 51 | 52 | fun RDOMBuilder.polygon(points: String): ReactElement = 53 | child(RDOMBuilder { POLYGON(attributesMapOf("points", points), it) }.create()) 54 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/util/GLMatrix.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | @file:JsModule("gl-matrix") 6 | @file:JsNonModule 7 | package polyhedra.js.util 8 | 9 | import org.khronos.webgl.* 10 | 11 | external object quat { 12 | fun create(): quat_t 13 | fun setAxisAngle(out: quat_t, axis: Float32Array, rad: Number): quat_t 14 | fun multiply(out: quat_t, a: quat_t, b: quat_t): quat_t 15 | fun conjugate(out: quat_t, a: quat_t): quat_t 16 | } 17 | 18 | external object mat3 { 19 | fun create(): Float32Array 20 | 21 | fun fromQuat(out: Float32Array, q: quat_t): Float32Array 22 | fun invert(out: Float32Array, a: Float32Array): Float32Array 23 | fun transpose(out: Float32Array, a: Float32Array): Float32Array 24 | } 25 | 26 | external object mat4 { 27 | fun create(): Float32Array 28 | 29 | fun identity(out: Float32Array): Float32Array 30 | fun invert(out: Float32Array, a: Float32Array): Float32Array 31 | fun transpose(out: Float32Array, a: Float32Array): Float32Array 32 | fun translate(out: Float32Array, a: Float32Array, v: Float32Array): Float32Array 33 | fun rotate(out: Float32Array, a: Float32Array, rad: Number, axis: Float32Array): Float32Array 34 | fun rotateX(out: Float32Array, a: Float32Array, rad: Number) 35 | fun rotateY(out: Float32Array, a: Float32Array, rad: Number) 36 | fun rotateZ(out: Float32Array, a: Float32Array, rad: Number) 37 | fun perspective(out: Float32Array, fovy: Number, aspect: Number, near: Number, far: Number): Float32Array 38 | fun scale(out: Float32Array, a: Float32Array, v: Float32Array): Float32Array 39 | 40 | fun fromTranslation(out: Float32Array, v: Float32Array): Float32Array 41 | fun fromRotationTranslation(out: Float32Array, q: quat_t, v: Float32Array): Float32Array 42 | fun fromRotationTranslationScale(out: Float32Array, q: quat_t, v: Float32Array, s: Float32Array): Float32Array 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/poly/ViewContext.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.poly 6 | 7 | import org.khronos.webgl.* 8 | import polyhedra.js.glsl.* 9 | import polyhedra.js.params.* 10 | import polyhedra.js.util.* 11 | import kotlin.math.* 12 | 13 | class ViewContext(params: ViewParams) : Param.Context(params) { 14 | private val scale by { params.scale.value } 15 | private val rotate by { params.rotate.value } 16 | 17 | val expandFaces by { params.expandFaces.value } 18 | val transparentFaces by { params.transparentFaces.value } 19 | val faceWidth by { params.faceWidth.value } 20 | val faceRim by { params.faceRim.value } 21 | 22 | val cameraPosition = float32Of(0.0, 0.0, 4.0) 23 | val projectionMatrix = mat4.create() 24 | val modelMatrix = mat4.create() 25 | val normalMatrix = mat3.create() 26 | 27 | private val cameraFieldOfViewDegrees = 45.0 28 | 29 | private val modelTranslation = Float32Array(3) // model at origin 30 | private val modelScale = Float32Array(3) 31 | 32 | private val tmpQuat = quat.create() 33 | private val tmpVec3 = Float32Array(3) 34 | 35 | fun initProjection(width: Int, height: Int) { 36 | mat4.perspective( 37 | projectionMatrix, cameraFieldOfViewDegrees * PI / 180, 38 | width.toDouble() / height, 0.1, 30.0 39 | ) 40 | for (i in 0..2) tmpVec3[i] = -cameraPosition[i] 41 | mat4.translate(projectionMatrix, projectionMatrix, tmpVec3) 42 | } 43 | 44 | init { setup() } 45 | 46 | override fun update() { 47 | modelScale.fill(2.0.pow(scale)) 48 | val r = rotate 49 | tmpQuat[0] = r.x 50 | tmpQuat[1] = r.y 51 | tmpQuat[2] = r.z 52 | tmpQuat[3] = r.w 53 | mat4.fromRotationTranslationScale(modelMatrix, tmpQuat, modelTranslation, modelScale) 54 | 55 | quat.conjugate(tmpQuat, tmpQuat) 56 | mat3.fromQuat(normalMatrix, tmpQuat) 57 | mat3.transpose(normalMatrix, normalMatrix) 58 | } 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/poly/FaceRim.kt: -------------------------------------------------------------------------------- 1 | package polyhedra.common.poly 2 | 3 | import polyhedra.common.util.* 4 | 5 | class FaceRim(f: Face) { 6 | val maxRim: Double 7 | val rimDir: List 8 | 9 | init { 10 | val n = f.size 11 | // edge vectors 12 | val ev = List(n) { i -> 13 | val j = (i + 1) % n 14 | f[j] - f[i] 15 | } 16 | // edge unit vectors 17 | val evu = List(n) { i -> 18 | ev[i].unit 19 | } 20 | // angle bisectors 21 | val bis = List(n) { i -> 22 | val k = (i + n - 1) % n 23 | evu[i] - evu[k] 24 | } 25 | // pseudo-incenter 26 | val ic = run { 27 | val sum = MutableVec3() 28 | val tmp = MutableVec3() 29 | for (i in 0 until n) { 30 | val j = (i + 1) % n 31 | val a1 = bis[i] * bis[i] 32 | val b1 = -bis[i] * bis[j] 33 | val c1 = bis[i] * ev[i] 34 | val a2 = -b1 35 | val b2 = bis[j] * bis[j] 36 | val c2 = bis[j] * ev[i] 37 | val d = det(a1, b1, a2, b2) 38 | val t1 = det(c1, b1, c2, b2) / d 39 | val t2 = det(a1, c1, a2, c2) / d 40 | tmp.setToZero() 41 | tmp += f[i] 42 | tmp += bis[i] * t1 43 | tmp += f[j] 44 | tmp += bis[j] * t2 45 | tmp /= 2.0 46 | sum += tmp 47 | } 48 | sum /= n.toDouble() 49 | sum 50 | } 51 | // vector from vertex to ic 52 | val icd = List(n) { i -> 53 | ic - f[i] 54 | } 55 | // shortest distance from ic to edge 56 | maxRim = (0 until n).minOf { i -> 57 | val j = (i + 1) % n 58 | tangentDistance(icd[i], icd[j]) 59 | } 60 | // rescale so that rimDir reaches ic when multiplied by maxRim 61 | rimDir = List(n) { i -> 62 | icd[i] / maxRim 63 | } 64 | } 65 | 66 | val borderNorm = List(f.size) { i -> 67 | val j = (i + 1) % f.size 68 | (f[j] cross f[i]).unit 69 | } 70 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/poly/DrawScene.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.poly 6 | 7 | import kotlinext.js.* 8 | import org.w3c.dom.* 9 | import polyhedra.js.glsl.* 10 | import polyhedra.js.params.* 11 | import org.khronos.webgl.WebGLRenderingContext as GL 12 | 13 | class DrawContext( 14 | canvas: HTMLCanvasElement, 15 | params: RenderParams, 16 | private val onUpdate: () -> Unit, 17 | ) : Param.Context(params) { 18 | val transparentFaces by { params.view.transparentFaces.value } 19 | 20 | val gl: GL = canvas.getContext("webgl", js { 21 | premultipliedAlpha = false // Ask for non-premultiplied alpha 22 | } as Any) as GL 23 | 24 | val view = ViewContext(params.view) 25 | val lightning = LightningContext(params.lighting) 26 | val faces = FaceContext(gl, params) 27 | val edges = EdgeContext(gl, params) 28 | 29 | init { 30 | setup() 31 | initGL() 32 | } 33 | 34 | override fun updateAlways() { 35 | onUpdate() 36 | } 37 | } 38 | 39 | private fun DrawContext.initGL() { 40 | gl.blendFunc(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA); 41 | gl.depthFunc(GL.LEQUAL) 42 | gl.clearColor(0.0f, 0.0f, 0.0f, 0.0f) 43 | gl.clearDepth(1.0f) 44 | gl.getExtension("OES_element_index_uint") 45 | gl[GL.CULL_FACE] = true 46 | gl.cullFace(GL.BACK) 47 | } 48 | 49 | fun DrawContext.drawScene() { 50 | val width = gl.canvas.width 51 | val height = gl.canvas.height 52 | 53 | view.initProjection(width, height) 54 | gl.viewport(0, 0, width, height) 55 | gl.clear(GL.COLOR_BUFFER_BIT or GL.DEPTH_BUFFER_BIT) 56 | 57 | val transparentFaces = faces.drawFaces && transparentFaces != 0.0 58 | gl[GL.DEPTH_TEST] = !transparentFaces 59 | gl[GL.BLEND] = transparentFaces 60 | if (transparentFaces) { 61 | // special code for transparent faces - draw back "front faces", then front "front faces" 62 | faces.draw(view, lightning, 1) 63 | edges.draw(view, 1) 64 | faces.draw(view, lightning, -1) 65 | edges.draw(view, -1) 66 | } else { 67 | // regular draw faces 68 | faces.draw(view, lightning) 69 | edges.draw(view) 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/poly/PolygonProjection.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.poly 6 | 7 | import polyhedra.common.util.* 8 | 9 | class PolygonProjection( 10 | val vs: List 11 | ) : Comparable { 12 | override fun compareTo(other: PolygonProjection): Int = 13 | VertexListApproxComparator.compare(vs, other.vs) 14 | 15 | companion object { 16 | val Empty = PolygonProjection(emptyList()) 17 | } 18 | } 19 | 20 | infix fun PolygonProjection.approx(other: PolygonProjection) = 21 | vs.size == other.vs.size && 22 | vs.indices.all { i -> vs[i] approx other.vs[i] } 23 | 24 | val VertexListApproxComparator : Comparator> = 25 | LexicographicListComparator(Vec3ApproxComparator) 26 | 27 | // project face vertices using a given starting index 28 | private fun computeProjectionFigureAt(plane: Plane, vs: List, i: Int): PolygonProjection { 29 | val v0 = vs[i] 30 | val c = plane.tangentPoint 31 | if (v0 approx c) return PolygonProjection.Empty 32 | val n = vs.size 33 | val ux = (v0 - c).unit 34 | val list = ArrayList(n) 35 | for (j in 0 until n) { 36 | val v = vs[(i + j) % n] - c 37 | val x = ux * v 38 | val y = (ux cross v) * plane 39 | val z = ux * plane 40 | list += Vec3(x, y, z) 41 | } 42 | return PolygonProjection(list) 43 | } 44 | 45 | private fun computeProjectionFigure(plane: Plane, vs: List): PolygonProjection = 46 | vs.indices.maxOfOrNull { i -> computeProjectionFigureAt(plane, vs, i) }!! 47 | 48 | fun Face.computeProjectionFigure() = 49 | computeProjectionFigure(this, fvs) 50 | 51 | fun Face.computeProjectionFigureAt(v: Vertex) = 52 | computeProjectionFigureAt(this, fvs, fvs.indexOf(v)) 53 | 54 | // use dual to compute vertex figure 55 | fun Vertex.computeProjectFigure() = 56 | computeProjectionFigure( 57 | dualPlane(1.0), 58 | directedEdges.map { it.r.dualPoint(1.0) } 59 | ) 60 | 61 | fun Vertex.computeProjectionFigureAt(f: Face) = 62 | computeProjectionFigureAt( 63 | dualPlane(1.0), 64 | directedEdges.map { it.r.dualPoint(1.0) }, 65 | directedEdges.indexOfFirst { it.r == f } 66 | ) 67 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/transform/Truncate.kt: -------------------------------------------------------------------------------- 1 | package polyhedra.common.transform 2 | 3 | import polyhedra.common.poly.* 4 | import polyhedra.common.util.* 5 | import kotlin.math.* 6 | 7 | fun Polyhedron.rectified(): Polyhedron = transformedPolyhedron(Transform.Rectified) { 8 | // vertices from the original edges 9 | val ev = es.associateWith { e -> 10 | vertex(e.midPoint(edgesMidPointDefault), VertexKind(edgeKindsIndex[e.kind]!!)) 11 | } 12 | // faces from the original faces 13 | for (f in fs) { 14 | face(f.directedEdges.map { ev[it.normalizedDirection()]!! }, f.kind) 15 | } 16 | // faces from the original vertices 17 | val kindOfs = faceKinds.size 18 | for (v in vs) { 19 | face(v.directedEdges.map { ev[it.normalizedDirection()]!! }, FaceKind(kindOfs + v.kind.id)) 20 | } 21 | for (vk in vertexKinds.keys) faceKindSource(FaceKind(kindOfs + vk.id), vk) 22 | mergeIndistinguishableKinds() 23 | } 24 | 25 | // ea == PI / face_size 26 | fun regularTruncationRatio(ea: Double): Double = 1 / (1 + cos(ea)) 27 | 28 | fun Polyhedron.regularTruncationRatio(faceKind: FaceKind = FaceKind(0)): Double { 29 | val f = faceKinds[faceKind]!! // take representative face of this kind 30 | return regularTruncationRatio(PI / f.size) 31 | } 32 | 33 | fun Polyhedron.truncated( 34 | tr: Double = regularTruncationRatio(), 35 | scale: Scale? = null, 36 | forceFaceKinds: List? = null 37 | ): Polyhedron = transformedPolyhedron(Transform.Truncated, tr, scale, forceFaceKinds) { 38 | // vertices from the original directed edges 39 | val ev = directedEdges.associateWith { e -> 40 | val t = tr * e.midPointFraction(edgesMidPointDefault) 41 | vertex(t.atSegment(e.a, e.b), VertexKind(directedEdgeKindsIndex[e.kind]!!)) 42 | } 43 | // faces from the original faces 44 | for (f in fs) { 45 | val fvs = f.directedEdges.flatMap { 46 | listOf(ev[it]!!, ev[it.reversed]!!) 47 | } 48 | face(fvs, f.kind) 49 | } 50 | // faces from the original vertices 51 | val kindOfs = faceKinds.size 52 | for (v in vs) { 53 | face(v.directedEdges.map { ev[it]!! }, FaceKind(kindOfs + v.kind.id)) 54 | } 55 | for (vk in vertexKinds.keys) faceKindSource(FaceKind(kindOfs + vk.id), vk) 56 | mergeIndistinguishableKinds() 57 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/poly/ViewBaseProgram.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.poly 6 | 7 | import polyhedra.js.glsl.* 8 | import org.khronos.webgl.WebGLRenderingContext as GL 9 | 10 | abstract class ViewBaseProgram(gl: GL) : GLProgram(gl) { 11 | val uCameraPosition by uniform(GLType.vec3) 12 | val uProjectionMatrix by uniform(GLType.mat4) 13 | val uModelMatrix by uniform(GLType.mat4) 14 | val uNormalMatrix by uniform(GLType.mat3) 15 | val uExpand by uniform(GLType.float) 16 | val uColorAlpha by uniform(GLType.float, GLPrecision.lowp) 17 | val uFaceWidth by uniform(GLType.float) 18 | val uFaceRim by uniform(GLType.float) 19 | val uCullMode by uniform(GLType.float) // 0 - no, 1 - cull front, -1 - cull back 20 | 21 | val fViewPosition by function( 22 | GLType.vec4, 23 | "position", GLType.vec3, 24 | "expandDir", GLType.vec3 25 | ) { position, expandDir -> 26 | // todo: optimize when not expanded? 27 | uModelMatrix * vec4(position + expandDir * uExpand, 1.0) 28 | } 29 | 30 | // face direction: > 0 - front-face, < 0 - back-face 31 | val fFaceDirection by function( 32 | GLType.float, 33 | "position", GLType.vec4, 34 | "normal", GLType.vec3 35 | ) { position, normal -> 36 | dot((position.xyz - uCameraPosition), normal) 37 | } 38 | 39 | val fCullMull by function( 40 | GLType.float, 41 | "position", GLType.vec4, 42 | "normal", GLType.vec3 43 | ) { position, normal -> 44 | select( 45 | uCullMode eq 0.0.literal, 46 | 1.0.literal, 47 | select(fFaceDirection(position, normal) * uCullMode ge 0.0.literal, 1.0.literal, 0.0.literal) 48 | ) 49 | } 50 | 51 | fun assignView(view: ViewContext, cullMode: Int = 0) { 52 | with(view) { 53 | uCameraPosition by cameraPosition 54 | uProjectionMatrix by projectionMatrix 55 | uModelMatrix by modelMatrix 56 | uNormalMatrix by normalMatrix 57 | uExpand by expandFaces 58 | uColorAlpha by 1.0 - transparentFaces 59 | uFaceWidth by faceWidth 60 | uFaceRim by faceRim 61 | uCullMode by cullMode.toDouble() 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/poly/Validation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.poly 6 | 7 | import polyhedra.common.util.* 8 | 9 | fun Polyhedron.validate() { 10 | validateGeometry() 11 | validateKinds() 12 | } 13 | 14 | fun Polyhedron.validateGeometry() { 15 | // Validate edges 16 | for (e in es) { 17 | require((e.a - e.b).norm > EPS) { 18 | "$e non-degenerate" 19 | } 20 | } 21 | // Validate faces 22 | for (f in fs) { 23 | require(f.d > 0) { 24 | "Face normal does not point outwards: $f $f " 25 | } 26 | for (v in f.fvs) 27 | require(f.isPlanar) { 28 | "Face is not planar: $f" 29 | } 30 | for (i in 0 until f.size) { 31 | val a = f[i] 32 | val b = f[(i + 1) % f.size] 33 | val c = f[(i + 2) % f.size] 34 | val rot = (c - a) cross (b - a) 35 | require(rot * f > -EPS) { 36 | "Face is not clockwise: $f, vertices $a $b $c" 37 | } 38 | } 39 | } 40 | } 41 | 42 | fun Polyhedron.validateKinds() { 43 | // Validate face kinds 44 | for ((fk, fs) in fs.groupBy { it.kind }) { 45 | fs.validateUnique("$fk faces", FaceKindEssence::approx) { it.essence() } 46 | } 47 | check(contiguousFaceKinds()) { "Face kinds must be contiguously numbered" } 48 | // Validate vertex kinds 49 | for ((vk, vs) in vs.groupBy { it.kind }) { 50 | vs.validateUnique("$vk vertices", VertexKindEssence::approx) { it.essence() } 51 | } 52 | check(contiguousVertexKinds()) { "Vertex kinds must be contiguously numbered" } 53 | // Validate edge kinds 54 | for ((ek, es) in es.groupBy { it.kind }) { 55 | es.validateUnique("$ek edges", EdgeKindEssence::approx) { it.essence() } 56 | } 57 | } 58 | 59 | private fun List.validateUnique(msg: String, approx: (K, K) -> Boolean, selector: (T) -> K) { 60 | val first = first() 61 | val firstKey = selector(first) 62 | for (i in 1 until size) { 63 | val cur = get(i) 64 | val curKey = selector(cur) 65 | require(approx(firstKey, curKey)) { 66 | "$msg are different:\n" + 67 | " $first -- $firstKey\n" + 68 | " $cur -- $curKey" 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/params/Parser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.params 6 | 7 | sealed class ParsedParam { 8 | data class Value(val value: String) : ParsedParam() 9 | data class Composite(val map: Map) : ParsedParam() 10 | } 11 | 12 | fun Param.loadFromString(str: String) { 13 | val parsed = ParamParser(str).parse() 14 | val updated = ArrayList() 15 | loadFrom(parsed) { updated += it } 16 | updated.forEach { 17 | // mark loaded values for repaint in the next animation frame 18 | it.notifyUpdated(Param.LoadedValue) 19 | // eagerly recompute derived values just like on TargetValue change 20 | it.computeDerivedTargetValues() 21 | } 22 | } 23 | 24 | private class ParamParser(private val str: String) { 25 | private var pos = 0 26 | private var cur = parseNextToken() 27 | 28 | private enum class Type { End, Value, Open, Close } 29 | private data class Token(val type: Type, val value: String) 30 | 31 | private fun separator(ch: Char): Type? = when (ch) { 32 | '(' -> Type.Open 33 | ')' -> Type.Close 34 | else -> null 35 | } 36 | 37 | private fun parseNextToken(): Token { 38 | if (pos >= str.length) return Token(Type.End, "") 39 | val start = pos++ 40 | separator(str[start])?.let { return Token(it, str[start].toString()) } 41 | while(pos < str.length && separator(str[pos]) == null) pos++ 42 | return Token(Type.Value, str.substring(start, pos)) 43 | } 44 | 45 | fun parse(): ParsedParam = when(cur.type) { 46 | Type.End, Type.Open, Type.Close -> ParsedParam.Value("") 47 | else -> { 48 | var value = cur.value 49 | cur = parseNextToken() 50 | if (cur.type == Type.Open) { 51 | val map = mutableMapOf() 52 | while (cur.type == Type.Open) { 53 | cur = parseNextToken() 54 | map[value] = parse() 55 | if (cur.type != Type.Close) break 56 | cur = parseNextToken() 57 | if (cur.type != Type.Value) break 58 | value = cur.value 59 | cur = parseNextToken() 60 | } 61 | ParsedParam.Composite(map) 62 | } else { 63 | ParsedParam.Value(value) 64 | } 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/poly/ExportScad.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.poly 6 | 7 | import kotlinx.browser.* 8 | import org.w3c.dom.* 9 | import polyhedra.common.* 10 | import polyhedra.common.poly.* 11 | import polyhedra.common.util.* 12 | 13 | fun Polyhedron.exportGeometryToScad(name: String, description: String): String = buildString { 14 | appendLine("// polyhedron($name[0], $name[1]);") 15 | appendLine("// $description") 16 | appendLine() 17 | appendLine("// Elements of the $name array") 18 | appendLine("// 0 - vertices coordinates") 19 | appendLine("// 1 - face descriptions clockwise") 20 | appendLine("// 2 - vertex kinds") 21 | appendLine("// 3 - face kinds") 22 | appendLine("$name = [[") 23 | for ((i, v) in vs.withIndex()) { 24 | append(" ${v.toPreciseString()}") 25 | appendSeparator(i, vs.size) 26 | appendLine(" // ${v.id} ${v.kind} vertex") 27 | } 28 | appendLine("], [") 29 | for ((i, f) in fs.withIndex()) { 30 | append(" [${f.fvs.joinToString { it.id.toString() }}]") 31 | appendSeparator(i, fs.size) 32 | appendLine(" // ${f.id} ${f.kind} face") 33 | } 34 | appendLine("], [") 35 | appendLine(vs.joinToStringRows(" ") { it.kind.id.toString() }) 36 | appendLine("], [") 37 | appendLine(fs.joinToStringRows(" ") { it.kind.id.toString() }) 38 | appendLine("]];") 39 | } 40 | 41 | private fun List.joinToStringRows(prefix: String, transform: (T) -> String): String = buildString { 42 | for ((i, e) in this@joinToStringRows.withIndex()) { 43 | val rowStart = i % 20 == 0 44 | if (i > 0) { 45 | append(",") 46 | if (rowStart) appendLine() else append(' ') 47 | } 48 | if (rowStart) append(prefix) 49 | append(transform(e)) 50 | } 51 | } 52 | 53 | private fun StringBuilder.appendSeparator(i: Int, size: Int) { 54 | if (i < size - 1) append(',') 55 | } 56 | 57 | fun download(filename: String, content: String) { 58 | val body = document.body!! 59 | val node = (document.createElement("a") as HTMLAnchorElement).apply { 60 | setAttribute("style", "download") 61 | setAttribute("download", filename) 62 | setAttribute("href", "data:text/plain;charset=utf-8,${encodeURIComponent(content)}") 63 | } 64 | body.appendChild(node) 65 | node.click() 66 | body.removeChild(node) 67 | 68 | } 69 | 70 | external fun encodeURIComponent(content: String): String 71 | 72 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/Plane.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | import polyhedra.common.poly.* 8 | 9 | interface Plane : Vec3 { 10 | val d: Double 11 | } 12 | 13 | open class MutablePlane( 14 | x: Double, 15 | y: Double, 16 | z: Double, 17 | override var d: Double 18 | ) : Plane, MutableVec3(x, y, z) { 19 | constructor(p: Plane) : this(p.x, p.y, p.z, p.d) 20 | override fun toString(): String = 21 | "Plane(n=${super.toString()}, d=${d.fmt})" 22 | } 23 | 24 | // Note: n must be a unit vector 25 | fun Plane(n: Vec3, d: Double): Plane = 26 | MutablePlane(n.x, n.y, n.z, d) 27 | 28 | // Plane through a point with a given normal 29 | fun planeByNormalAndPoint(n: Vec3, p: Vec3): Plane { 30 | val u = n.unit 31 | return Plane(u, p * u) 32 | } 33 | 34 | // Plane through 3 points 35 | fun plane3(a: Vec3, b: Vec3, c: Vec3): Plane = 36 | planeByNormalAndPoint(((c - a) cross (b - a)), a) 37 | 38 | // Intersect 3 planes 39 | fun planeIntersection(p: Plane, q: Plane, r: Plane): Vec3 { 40 | val d = det( 41 | p.x, p.y, p.z, 42 | q.x, q.y, q.z, 43 | r.x, r.y, r.z 44 | ) 45 | val x = det( 46 | p.d, p.y, p.z, 47 | q.d, q.y, q.z, 48 | r.d, r.y, r.z 49 | ) 50 | val y = det( 51 | p.x, p.d, p.z, 52 | q.x, q.d, q.z, 53 | r.x, r.d, r.z 54 | ) 55 | val z = det( 56 | p.x, p.y, p.d, 57 | q.x, q.y, q.d, 58 | r.x, r.y, r.d 59 | ) 60 | return Vec3(x / d, y / d, z / d) 61 | } 62 | 63 | operator fun Plane.contains(v: Vec3): Boolean = 64 | this * v approx d 65 | 66 | // Projection of origin onto the plane 67 | val Plane.tangentPoint: Vec3 68 | get() = this * d 69 | 70 | // Intersection of a plane with a given vector 71 | // Resulting vector is in the plane 72 | fun Plane.intersection(v: Vec3) = 73 | v * (d / (this * v)) 74 | 75 | // Average plane via given points, outside pointing normal 76 | fun List.averagePlane(): Plane { 77 | require(size >= 3) { "Needs at least 3 points, found $size" } 78 | val center = MutableVec3() 79 | // find centroid of points 80 | for (i in 0 until size) center += this[i] 81 | center /= size 82 | // find sum cross-product of all angles -> normal of the "average" plane 83 | val normSum = MutableVec3() 84 | for (i in 0 until size) { 85 | val a = this[i] 86 | val b = this[(i + 1) % size] 87 | crossCenteredAddTo(normSum, b, a, center) 88 | } 89 | return planeByNormalAndPoint(normSum, center) 90 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/glsl/GLUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.glsl 6 | 7 | import org.khronos.webgl.* 8 | import polyhedra.common.util.* 9 | import polyhedra.js.util.* 10 | import org.khronos.webgl.WebGLRenderingContext as GL 11 | 12 | fun float32Of(vararg a: Float) = Float32Array(a.size).apply { 13 | for (i in a.indices) this[i] = a[i] 14 | } 15 | 16 | fun float32Of(vararg a: Double) = Float32Array(a.size).apply { 17 | for (i in a.indices) this[i] = a[i].toFloat() 18 | } 19 | 20 | fun uint16Of(vararg a: Int) = Uint16Array(a.size).apply { 21 | for (i in a.indices) this[i] = a[i].toShort() 22 | } 23 | 24 | inline operator fun Uint8Array.set(i: Int, x: Int) { 25 | set(i, x.toByte()) 26 | } 27 | 28 | inline operator fun Float32Array.set(i: Int, x: Double) { 29 | set(i, x.toFloat()) 30 | } 31 | 32 | fun Float32Array.fill(x: Double) { 33 | for (i in 0 until length) set(i, x.toFloat()) 34 | } 35 | 36 | operator fun Float32Array.set(i: Int, v: Vec3) { 37 | set(i, v.x) 38 | set(i + 1, v.y) 39 | set(i + 2, v.z) 40 | } 41 | 42 | fun Float32Array.setRGB(i: Int, c: Color) { 43 | set(i, c.r) 44 | set(i + 1, c.g) 45 | set(i + 2, c.b) 46 | } 47 | 48 | fun Vec3.toFloat32Array(): Float32Array = Float32Array(3).apply { 49 | set(0, x) 50 | set(1, y) 51 | set(2, z) 52 | } 53 | 54 | inline operator fun Uint16Array.set(i: Int, x: Int) { 55 | set(i, x.toShort()) 56 | } 57 | 58 | fun loadShader(gl: GL, type: Int, source: String): WebGLShader { 59 | val shader = gl.createShader(type)!! 60 | gl.shaderSource(shader, source) 61 | gl.compileShader(shader) 62 | if (gl.getShaderParameter(shader, GL.COMPILE_STATUS) != true) { 63 | val error = gl.getShaderInfoLog(shader) 64 | println("Error while compiling shader: $error") 65 | println("// --- begin source ---") 66 | println(source) 67 | println("// --- end source ---") 68 | error("Shader compilation error: $error") 69 | } 70 | return shader 71 | } 72 | 73 | fun initShaderProgram(gl: GL, vs: WebGLShader, fs: WebGLShader): WebGLProgram { 74 | val shaderProgram = gl.createProgram()!! 75 | gl.attachShader(shaderProgram, vs) 76 | gl.attachShader(shaderProgram, fs) 77 | gl.linkProgram(shaderProgram) 78 | if (gl.getProgramParameter(shaderProgram, GL.LINK_STATUS) != true) 79 | error("Shader program error: ${gl.getProgramInfoLog(shaderProgram)}") 80 | return shaderProgram 81 | } 82 | 83 | operator fun GL.set(cap: Int, value: Boolean) { 84 | if (value) 85 | enable(cap) 86 | else 87 | disable(cap) 88 | } 89 | 90 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/transform/Bevel.kt: -------------------------------------------------------------------------------- 1 | package polyhedra.common.transform 2 | 3 | import polyhedra.common.poly.* 4 | import polyhedra.common.util.* 5 | import kotlin.math.* 6 | 7 | data class BevellingRatio(val cr: Double, val tr: Double) { 8 | override fun toString(): String = "(cr=${cr.fmt}, tr=${tr.fmt})" 9 | } 10 | 11 | fun Polyhedron.regularBevellingRatio(edgeKind: EdgeKind? = null): BevellingRatio { 12 | val (ea, da) = regularFaceGeometry(edgeKind) 13 | val tr = regularTruncationRatio(ea) 14 | val cr = (1 - tr) / (1 + sin(da / 2) / tan(ea) - tr) 15 | return BevellingRatio(cr, tr) 16 | } 17 | 18 | fun Polyhedron.bevelled( 19 | br: BevellingRatio = regularBevellingRatio(), 20 | scale: Scale? = null, 21 | forceFaceKinds: List? = null 22 | ): Polyhedron = transformedPolyhedron(Transform.Bevelled, br, scale, forceFaceKinds) { 23 | val (cr, tr) = br 24 | val rr = dualReciprocationRadius 25 | // vertices from the face-directed edges 26 | val fev = fs.associateWith { f -> 27 | val c = f.dualPoint(rr) // for regular polygons -- face center 28 | f.directedEdges.flatMap { e -> 29 | val kind = directedEdgeKindsIndex[e.kind]!! 30 | val a = e.a 31 | val b = e.b 32 | val ac = cr.atSegment(a, c) 33 | val bc = cr.atSegment(b, c) 34 | val mf = e.midPointFraction(edgesMidPointDefault) 35 | val t1 = tr * mf 36 | val t2 = tr * (1 - mf) 37 | listOf( 38 | e to vertex(t1.atSegment(ac, bc), VertexKind(2 * kind)), 39 | e.reversed to vertex(t2.atSegment(bc, ac), VertexKind(2 * kind + 1)) 40 | ) 41 | }.associate { it } 42 | } 43 | // faces from the original faces 44 | for (f in fs) { 45 | face(fev[f]!!.values, f.kind) 46 | } 47 | // faces from the original vertices 48 | var kindOfs = faceKinds.size 49 | for (v in vs) { 50 | val fvs = v.directedEdges.flatMap { e -> 51 | listOf(fev[e.l]!![e]!!, fev[e.r]!![e]!!) 52 | } 53 | face(fvs, FaceKind(kindOfs + v.kind.id)) 54 | } 55 | for (vk in vertexKinds.keys) faceKindSource(FaceKind(kindOfs + vk.id), vk) 56 | // 4-faces from the original edges 57 | kindOfs += vertexKinds.size 58 | for (e in es) { 59 | val er = e.reversed 60 | val fvs = listOf( 61 | fev[e.r]!![e]!!, 62 | fev[e.l]!![e]!!, 63 | fev[e.l]!![er]!!, 64 | fev[e.r]!![er]!! 65 | ) 66 | face(fvs, FaceKind(kindOfs + edgeKindsIndex[e.kind]!!)) 67 | } 68 | for ((ek, id) in edgeKindsIndex) faceKindSource(FaceKind(kindOfs + id), ek) 69 | mergeIndistinguishableKinds() 70 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polyhedra Explorer 2 | 3 | **🚧 Work in process.** 4 | 5 | Interactive polyhedra explorer with animated transformations. This project is focused on regular convex 6 | polyhedra and derivation of larger polyhedra via 7 | [Conway polyhedron notation](https://en.wikipedia.org/wiki/Conway_polyhedron_notation). 8 | All transformations are symmetry-preserving and all resulting elements (faces, edges, vertices) 9 | are grouped into rotation orbits and are colored by default with respect to them. 10 | 11 | Prototype is deployed at [http://polyhedron.me](http://polyhedron.me) 12 | 13 | ## Building & running 14 | 15 | ```shell 16 | gradlew jsBrowserDevelopmentRun 17 | ``` 18 | 19 | ## Roadmap / TODO 20 | 21 | * UI/UX 22 | * [ ] Animate seed changes with fly in/out 23 | * [ ] Better progress bar display 24 | * [ ] Show/kind faces by kind with point and click on the polyhedron 25 | * [ ] Mark experimental features in UI 26 | * [ ] Better slider UI on mobile devices 27 | * Export/Share 28 | * [x] Solid to STL 29 | * [x] Geometry to OpenSCAD 30 | * [ ] Picture to SVG 31 | * [ ] Share link 32 | * Polyhedra 33 | * [ ] Bigger library of seeds 34 | * [x] Platonic solids 35 | * [x] Arhimedean solids 36 | * [x] Catalan solids 37 | * [ ] Infinite families of prisms/antiprisms 38 | * [ ] Johnson solids 39 | * [ ] Identify names of well-know polyhedra 40 | * Rendering 41 | * [ ] Render nicer edges and vertices 42 | * [ ] Render better-looking (physical) materials 43 | * [ ] Custom faces coloring: by orbit with reflections, by geometry, by size 44 | * [ ] Nicer-looking transparent views (only transparent front) 45 | * Polyhedron info 46 | * [ ] Show edge geometry (two faces) 47 | * [ ] Show face areas 48 | * [ ] Sort by selected column (kind/distance/length/area) 49 | * Transformations 50 | * [ ] Redesign truncation algorithm so that it always works 51 | * [ ] Rectification solution for non-regular polyhedra 52 | * [ ] Stellation 53 | * [ ] Better canonical algorithm 54 | * [ ] Long-term caching of canonical geometry keyed by topology 55 | * [ ] Improve transformation performance 56 | * Custom transformations 57 | * [ ] Truncate specific vertices 58 | * [ ] Cantellate specific edges 59 | * [ ] Augment specific faces 60 | * [ ] Improve dropping of selected vertices/faces/edges 61 | * Infrastructure 62 | * [ ] Embed CSS into WebPack 63 | * [ ] Drop gl-matrix 64 | * [ ] Switch from React to Compose 65 | * [ ] Benchmarking 66 | * [ ] Software gl impl: render polyhedra picture by params on backend 67 | 68 | ## License 69 | 70 | Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 71 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/transform/Cantellate.kt: -------------------------------------------------------------------------------- 1 | package polyhedra.common.transform 2 | 3 | import polyhedra.common.poly.* 4 | import polyhedra.common.util.* 5 | import kotlin.math.* 6 | 7 | data class RegularFaceGeometry( 8 | val ea: Double, // PI / face_size 9 | val da: Double // dihedral angle 10 | ) 11 | 12 | fun Polyhedron.regularFaceGeometry(edgeKind: EdgeKind? = null): RegularFaceGeometry { 13 | val ek = edgeKind ?: edgeKinds.keys.first() // min edge kind by default 14 | val e = edgeKinds[ek]!! // representative edge 15 | val f = e.r // primary face 16 | val g = e.l // secondary face 17 | val n = f.size // primary face size 18 | val ea = PI / n 19 | val da = PI - acos(f * g) // dihedral angle 20 | return RegularFaceGeometry(ea, da) 21 | } 22 | 23 | fun Polyhedron.regularCantellationRatio(edgeKind: EdgeKind? = null): Double { 24 | val (ea, da) = regularFaceGeometry(edgeKind) 25 | return 1 / (1 + sin(da / 2) / tan(ea)) 26 | } 27 | 28 | fun Polyhedron.cantellated( 29 | cr: Double = regularCantellationRatio(), 30 | scale: Scale? = null, 31 | forceFaceKinds: List? = null 32 | ): Polyhedron = transformedPolyhedron(Transform.Cantellated, cr, scale, forceFaceKinds) { 33 | val rr = dualReciprocationRadius 34 | // vertices from the directed edges 35 | val ev = directedEdges.associateWith { e -> 36 | val a = e.a // vertex for cantellation 37 | val f = e.r // primary face for cantellation 38 | val c = f.dualPoint(rr) // for regular polygons -- face center 39 | vertex(cr.atSegment(a, c), VertexKind(directedEdgeKindsIndex[e.kind]!!)) 40 | } 41 | val fvv = ev.directedEdgeToFaceVertexMap() 42 | // faces from the original faces 43 | for (f in fs) { 44 | val fvs = f.directedEdges.map { ev[it]!! } 45 | face(fvs, f.kind) 46 | } 47 | // faces from the original vertices 48 | var kindOfs = faceKinds.size 49 | for (v in vs) { 50 | face(v.directedEdges.map { ev[it]!! }, FaceKind(kindOfs + v.kind.id)) 51 | } 52 | for (vk in vertexKinds.keys) faceKindSource(FaceKind(kindOfs + vk.id), vk) 53 | // 4-faces from the original edges 54 | kindOfs += vertexKinds.size 55 | for (e in es) { 56 | val fvs = listOf( 57 | fvv[e.r]!![e.a]!!, 58 | fvv[e.l]!![e.a]!!, 59 | fvv[e.l]!![e.b]!!, 60 | fvv[e.r]!![e.b]!! 61 | ) 62 | face(fvs, FaceKind(kindOfs + edgeKindsIndex[e.kind]!!)) 63 | } 64 | for ((ek, id) in edgeKindsIndex) faceKindSource(FaceKind(kindOfs + id), ek) 65 | mergeIndistinguishableKinds() 66 | } 67 | 68 | fun Polyhedron.dual(): Polyhedron = transformedPolyhedron(Transform.Dual) { 69 | val rr = dualReciprocationRadius 70 | // vertices from the original faces 71 | val fv = fs.associateWith { f -> 72 | vertex(f.dualPoint(rr), VertexKind(f.kind.id)) 73 | } 74 | // faces from the original vertices 75 | for (v in vs) { 76 | face(v.directedEdges.map { fv[it.r]!! }, FaceKind(v.kind.id)) 77 | } 78 | for (vk in vertexKinds.keys) faceKindSource(FaceKind(vk.id), vk) 79 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/QuatTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | import polyhedra.common.util.* 6 | import kotlin.math.* 7 | import kotlin.random.* 8 | import kotlin.test.* 9 | 10 | class QuatTest { 11 | private val ux = Vec3(1.0, 0.0, 0.0) 12 | private val uy = Vec3(0.0, 1.0, 0.0) 13 | private val uz = Vec3(0.0, 0.0, 1.0) 14 | 15 | private val ra = PI / 2 // 90 deg 16 | private val rb = PI / 3 // 60 deg 17 | private val rc = PI / 4 // 45 deg 18 | private val rd = PI / 6 // 30 deg 19 | 20 | private val testAngles = listOf(0.0, ra, rb, rc, rd, -ra, -rb, -rc, -rd) 21 | 22 | @Test 23 | fun testRotationAround() { 24 | assertApprox(ux, ux.rotated(ux.toRotationAroundQuat(ra))) 25 | assertApprox(-uz, ux.rotated(uy.toRotationAroundQuat(ra))) 26 | assertApprox(uy, ux.rotated(uz.toRotationAroundQuat(ra))) 27 | assertApprox(uz, uy.rotated(ux.toRotationAroundQuat(ra))) 28 | assertApprox(uy, uy.rotated(uy.toRotationAroundQuat(ra))) 29 | assertApprox(-ux, uy.rotated(uz.toRotationAroundQuat(ra))) 30 | assertApprox(-uy, uz.rotated(ux.toRotationAroundQuat(ra))) 31 | assertApprox(ux, uz.rotated(uy.toRotationAroundQuat(ra))) 32 | assertApprox(uz, uz.rotated(uz.toRotationAroundQuat(ra))) 33 | } 34 | 35 | @Test 36 | fun testRotationBetween() { 37 | assertApprox(uz.toRotationAroundQuat(ra), rotationBetweenQuat(ux, uy)) 38 | assertApprox(ux.toRotationAroundQuat(ra), rotationBetweenQuat(uy, uz)) 39 | assertApprox(uy.toRotationAroundQuat(ra), rotationBetweenQuat(uz, ux)) 40 | } 41 | 42 | @Test 43 | fun testToAngles() { 44 | for (r in testAngles) { 45 | assertApprox(r * ux, ux.toRotationAroundQuat(r).toAngles()) 46 | assertApprox(r * uy, uy.toRotationAroundQuat(r).toAngles()) 47 | assertApprox(r * uz, uz.toRotationAroundQuat(r).toAngles()) 48 | } 49 | } 50 | 51 | @Test 52 | fun testAnglesToQuat() { 53 | for (r in testAngles) { 54 | assertApprox(ux.toRotationAroundQuat(r), (ux * r).anglesToQuat()) 55 | assertApprox(uy.toRotationAroundQuat(r), (uy * r).anglesToQuat()) 56 | assertApprox(uz.toRotationAroundQuat(r), (uz * r).anglesToQuat()) 57 | } 58 | } 59 | 60 | @Test 61 | fun testToAnglesAndBack() { 62 | val rnd = Random(1) 63 | repeat(10) { 64 | val original = rotationAroundQuat( 65 | rnd.nextDouble(), rnd.nextDouble(), rnd.nextDouble(), rnd.nextDouble() * 2 * PI 66 | ) 67 | val angles = original.toAngles() 68 | val quat = angles.anglesToQuat() 69 | assertApprox(original, quat) 70 | } 71 | } 72 | 73 | private fun assertApprox(expect: Quat, actual: Quat) { 74 | if (expect approx actual) return 75 | fail("Expected: $expect, actual: $actual") 76 | } 77 | 78 | private fun assertApprox(expect: Vec3, actual: Vec3) { 79 | if (expect approx actual) return 80 | fail("Expected: $expect, actual: $actual") 81 | } 82 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/glsl/GLType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.glsl 6 | 7 | import org.khronos.webgl.* 8 | 9 | enum class GLPrecision { lowp, mediump, highp } 10 | 11 | @Suppress("ClassName") 12 | interface GLType> { 13 | val bufferSize: Int 14 | 15 | interface Comparable> : GLType 16 | 17 | interface Numbers> : GLType 18 | 19 | interface Floats> : Numbers 20 | 21 | interface VecOrMatrixFloats> : Floats { 22 | val uniformFloat32Array: (WebGLRenderingContext, WebGLUniformLocation, Float32Array) -> Unit 23 | } 24 | 25 | interface NonMatrixFloats> : Floats 26 | 27 | interface VecFloats> : VecOrMatrixFloats, NonMatrixFloats 28 | 29 | interface MatrixFloats> : VecOrMatrixFloats 30 | 31 | object void : GLTypeBase() 32 | 33 | object bool : GLTypeBase( 34 | bufferSize = 1 35 | ) 36 | 37 | object int : Numbers, Comparable, GLTypeBase( 38 | bufferSize = 1 39 | ) 40 | 41 | object float : NonMatrixFloats, Comparable, GLTypeBase( 42 | bufferSize = 1 43 | ) 44 | 45 | object vec2 : VecFloats, GLTypeBase( 46 | bufferSize = 2, 47 | uniformFloat32Array = { gl, loc, a -> gl.uniform2fv(loc, a) } 48 | ) 49 | 50 | object vec3 : VecFloats, GLTypeBase( 51 | bufferSize = 3, 52 | uniformFloat32Array = { gl, loc, a -> gl.uniform3fv(loc, a) } 53 | ) 54 | 55 | object vec4 : VecFloats, GLTypeBase( 56 | bufferSize = 4, 57 | uniformFloat32Array = { gl, loc, a -> gl.uniform4fv(loc, a) } 58 | ) 59 | 60 | object mat2 : MatrixFloats, GLTypeBase( 61 | bufferSize = 4, 62 | uniformFloat32Array = { gl, loc, a -> gl.uniformMatrix2fv(loc, false, a) } 63 | ) 64 | 65 | object mat3 : MatrixFloats, GLTypeBase( 66 | bufferSize = 9, 67 | uniformFloat32Array = { gl, loc, a -> gl.uniformMatrix3fv(loc, false, a) } 68 | ) 69 | 70 | object mat4 : MatrixFloats, GLTypeBase( 71 | bufferSize = 16, 72 | uniformFloat32Array = { gl, loc, a -> gl.uniformMatrix4fv(loc, false, a) } 73 | ) 74 | 75 | // function type, cannot be manipulated by arithmetics, only called 76 | data class fun0>(val resultType: T) : GLTypeBase>() 77 | data class fun1, P1 : GLType>(val resultType: T) : GLTypeBase>() 78 | data class fun2, P1 : GLType, P2 : GLType>(val resultType: T) : GLTypeBase>() 79 | } 80 | 81 | abstract class GLTypeBase>( 82 | override val bufferSize: Int = 0, 83 | val uniformFloat32Array: (WebGLRenderingContext, WebGLUniformLocation, Float32Array) -> Unit = 84 | { _, _, _ -> error("cannot be set from Float32Array") } 85 | ) : GLType { 86 | override fun toString(): String = this::class.simpleName!! 87 | } 88 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/main/RootPane.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.main 6 | 7 | import kotlinx.html.js.* 8 | import polyhedra.js.components.* 9 | import polyhedra.js.params.* 10 | import polyhedra.js.poly.* 11 | import react.* 12 | import react.dom.* 13 | 14 | external interface RootPaneState : RState { 15 | var popup: Popup? 16 | var faces: FaceContext? 17 | } 18 | 19 | @Suppress("NON_EXPORTABLE_TYPE") 20 | @JsExport 21 | class RootPane(props: PComponentProps) : RComponent, RootPaneState>(props) { 22 | override fun RootPaneState.init(props: PComponentProps) { 23 | popup = null 24 | faces = null 25 | } 26 | 27 | private inner class Context(params: RootParams) : Param.Context(params, Param.TargetValue + Param.Progress) { 28 | val poly by { params.render.poly.poly } 29 | 30 | init { setup() } 31 | 32 | override fun update() { 33 | forceUpdate() 34 | } 35 | } 36 | 37 | private val ctx = Context(props.params) 38 | 39 | override fun RBuilder.render() { 40 | polyCanvas("poly") { 41 | params = props.params.render 42 | poly = ctx.poly 43 | faceContextSink = { setState { faces = it } } 44 | resetPopup = ::resetPopup 45 | } 46 | controlPane { 47 | params = props.params.render.poly 48 | popup = state.popup 49 | togglePopup = ::togglePopup 50 | } 51 | polyInfo { 52 | params = props.params.render 53 | popup = state.popup 54 | togglePopup = ::togglePopup 55 | } 56 | div("btn config" + activeWhen(Popup.Config)) { 57 | button(classes = "square") { 58 | attrs { onClickFunction = { togglePopup(Popup.Config) } } 59 | i("fa fa-cog") {} 60 | } 61 | } 62 | if (state.popup != Popup.Config) { 63 | div("btn export" + activeWhen(Popup.Export)) { 64 | button(classes = "square") { 65 | attrs { onClickFunction = { togglePopup(Popup.Export) } } 66 | i("fa fa-share-square-o") {} 67 | } 68 | } 69 | } 70 | when (state.popup) { 71 | Popup.Config -> { 72 | aside("drawer config") { 73 | configPopup(props.params) 74 | } 75 | } 76 | Popup.Export -> { 77 | aside("drawer export") { 78 | exportPopup(props.params, state.faces) 79 | } 80 | } 81 | } 82 | } 83 | 84 | private fun activeWhen(popup: Popup): String = if (state.popup == popup) " active" else "" 85 | 86 | private fun togglePopup(popup: Popup?) { 87 | setPopup(if(state.popup == popup) null else popup) 88 | } 89 | 90 | private fun resetPopup() { 91 | setPopup(null) 92 | } 93 | 94 | private fun setPopup(popup: Popup?) { 95 | if (state.popup != popup) setState { this.popup = popup } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/glsl/GLBuffer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.glsl 6 | 7 | import org.khronos.webgl.* 8 | import polyhedra.common.util.* 9 | import polyhedra.js.util.* 10 | import org.khronos.webgl.WebGLRenderingContext as GL 11 | 12 | fun > createBuffer(gl: GL, type: T): Float32Buffer = 13 | Float32Buffer(type, gl.createBuffer()!!) 14 | 15 | fun createUint8Buffer(gl: GL): Uint8Buffer = Uint8Buffer(gl.createBuffer()!!) 16 | fun createUint16Buffer(gl: GL): Uint16Buffer = Uint16Buffer(gl.createBuffer()!!) 17 | fun createUint32Buffer(gl: GL): Uint32Buffer = Uint32Buffer(gl.createBuffer()!!) 18 | 19 | abstract class GLBuffer, D : BufferDataSource>( 20 | val type: T, 21 | val glBuffer: WebGLBuffer, 22 | ) { 23 | @Suppress("LeakingThis") 24 | var data: D = allocate(128) 25 | 26 | protected abstract val capacity: Int 27 | protected abstract fun allocate(capacity: Int): D 28 | 29 | fun ensureCapacity(length: Int) { 30 | val capacity = capacity 31 | val size = length * type.bufferSize 32 | if (capacity < size) data = allocate(maxOf(size, capacity * 2)) 33 | } 34 | } 35 | 36 | fun GLBuffer<*, *>.bindBufferData(gl: GL, target: Int = GL.ARRAY_BUFFER) { 37 | gl.bindBuffer(target, glBuffer) 38 | gl.bufferData(target, data, GL.STATIC_DRAW) 39 | } 40 | 41 | class Float32Buffer>(type: T, glBuffer: WebGLBuffer) : GLBuffer(type, glBuffer) { 42 | override val capacity: Int get() = data.length 43 | override fun allocate(capacity: Int): Float32Array = Float32Array(capacity) 44 | } 45 | 46 | class Uint8Buffer(glBuffer: WebGLBuffer) : GLBuffer(GLType.int, glBuffer) { 47 | override val capacity: Int get() = data.length 48 | override fun allocate(capacity: Int): Uint8Array = Uint8Array(capacity) 49 | } 50 | 51 | class Uint16Buffer(glBuffer: WebGLBuffer) : GLBuffer(GLType.int, glBuffer) { 52 | override val capacity: Int get() = data.length 53 | override fun allocate(capacity: Int): Uint16Array = Uint16Array(capacity) 54 | } 55 | 56 | class Uint32Buffer(glBuffer: WebGLBuffer) : GLBuffer(GLType.int, glBuffer) { 57 | override val capacity: Int get() = data.length 58 | override fun allocate(capacity: Int): Uint32Array = Uint32Array(capacity) 59 | } 60 | 61 | operator fun Float32Buffer.set(i: Int, x: Double) { 62 | data[i] = x 63 | } 64 | 65 | operator fun Float32Buffer.set(i: Int, v: Vec3) { 66 | data[3 * i] = v 67 | } 68 | 69 | operator fun Float32Buffer.get(i: Int, j: Int): Double = data[3 * i + j].toDouble() 70 | 71 | operator fun Float32Buffer.set(i: Int, c: Color) { 72 | data.setRGB(3 * i, c) 73 | } 74 | 75 | operator fun Uint8Buffer.set(i: Int, x: Int) { 76 | data[i] = x 77 | } 78 | 79 | operator fun Uint8Buffer.get(i: Int): Int = data[i].toInt() 80 | 81 | operator fun Uint16Buffer.set(i: Int, x: Int) { 82 | data[i] = x 83 | } 84 | 85 | operator fun Uint32Buffer.set(i: Int, x: Int) { 86 | data[i] = x 87 | } 88 | 89 | operator fun Uint32Buffer.get(i: Int): Int = data[i] 90 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/poly/Isomorphism.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.poly 6 | 7 | import polyhedra.common.util.* 8 | 9 | class EdgeEquivalenceClass(e: Edge) : Comparable { 10 | val pa = e.a.computeProjectionFigureAt(e.r) 11 | val pb = e.b.computeProjectionFigureAt(e.l) 12 | val pl = e.l.computeProjectionFigureAt(e.b) 13 | val pr = e.r.computeProjectionFigureAt(e.a) 14 | 15 | override fun compareTo(other: EdgeEquivalenceClass): Int { 16 | val ca = pa.compareTo(other.pa) 17 | if (ca != 0) return ca 18 | val cb = pb.compareTo(other.pb) 19 | if (cb != 0) return cb 20 | val cl = pl.compareTo(other.pl) 21 | if (cl != 0) return cl 22 | return pr.compareTo(other.pr) 23 | } 24 | } 25 | 26 | enum class IsoDir { L, R } 27 | 28 | class IsoEdge( 29 | val kind: EdgeKind, 30 | val eq: EdgeEquivalenceClass 31 | ) { 32 | lateinit var lNext: IsoEdge 33 | lateinit var rNext: IsoEdge 34 | 35 | operator fun get(dir: IsoDir): IsoEdge = when(dir) { 36 | IsoDir.L -> lNext 37 | IsoDir.R -> rNext 38 | } 39 | 40 | override fun toString(): String = kind.toString() 41 | } 42 | 43 | private class Block(val es: MutableList) { 44 | var lq = false 45 | var rq = false 46 | operator fun get(dir: IsoDir): Boolean = when (dir) { 47 | IsoDir.L -> lq 48 | IsoDir.R -> rq 49 | } 50 | operator fun set(dir: IsoDir, value: Boolean) = when (dir) { 51 | IsoDir.L -> lq = value 52 | IsoDir.R -> rq = value 53 | } 54 | } 55 | 56 | private class Task(val block: Block, val dir: IsoDir) 57 | 58 | // A V*log(V) Algorithm for Isomorphism of Triconnected Planar Graphs 59 | // J. E. Hopcroft and R. E. Tarjan 60 | // Modified to find geometrical isomorphism by starting with geometrical equivalence classes 61 | fun List.groupIndistinguishable(): List> { 62 | val queue = ArrayList() 63 | val blocks: ArrayList = groupByTo(TreeMap()) { it.eq } 64 | .values 65 | .mapTo(ArrayList()) { list -> Block(list) } 66 | val edgeBlock = HashMap() 67 | for (b in blocks) { 68 | for (e in b.es) edgeBlock[e] = b 69 | } 70 | fun enqueue(block: Block, dir: IsoDir) { 71 | queue + Task(block, dir) 72 | block[dir] = true 73 | } 74 | for (b in blocks) { 75 | enqueue(b, IsoDir.L) 76 | enqueue(b, IsoDir.R) 77 | } 78 | var qh = 0 79 | while (qh < queue.size) { 80 | val task = queue[qh++] 81 | task.block[task.dir] = false 82 | val move = task.block.es.mapTo(HashSet()) { e -> e[task.dir] } 83 | val split = move.mapTo(HashSet()) { edgeBlock[it]!! } 84 | for (b in split) { 85 | val newEs = b.es.filterTo(ArrayList()) { it in move } 86 | if (newEs.size == b.es.size) continue 87 | b.es.removeAll(move) 88 | val newBlock = Block(newEs) 89 | blocks += newBlock 90 | for (e in newEs) edgeBlock[e] = newBlock 91 | for (dir in IsoDir.values()) { 92 | when { 93 | b[dir] -> enqueue(newBlock, dir) 94 | newEs.size <= b.es.size -> enqueue(newBlock, dir) 95 | else -> enqueue(b, dir) 96 | } 97 | } 98 | } 99 | } 100 | return blocks.map { it.es } 101 | } 102 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/poly/EdgeContext.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.poly 6 | 7 | import org.khronos.webgl.* 8 | import polyhedra.common.poly.* 9 | import polyhedra.js.glsl.* 10 | import polyhedra.js.main.* 11 | import polyhedra.js.params.* 12 | import polyhedra.js.util.* 13 | import org.khronos.webgl.WebGLRenderingContext as GL 14 | 15 | class EdgeContext(val gl: GL, params: RenderParams) : Param.Context(params) { 16 | val drawEdges by { params.view.display.value.hasEdges() } 17 | val poly by { params.poly.targetPoly } 18 | val animation by { params.poly.transformAnimation } 19 | 20 | val program = EdgeProgram(gl) 21 | 22 | lateinit var color: Float32Array 23 | var indexSize = 0 24 | val indexBuffer = createUint16Buffer(gl) 25 | val target = EdgeBuffers() 26 | val prev = EdgeBuffers() // only filled when animation != null 27 | 28 | init { setup() } 29 | 30 | override fun update() { 31 | if (!drawEdges) return 32 | program.use() 33 | color = PolyStyle.edgeColor.toFloat32Array4() 34 | indexSize = target.update(poly, indexBuffer) 35 | animation?.let { prev.update(it.prevPoly) } 36 | } 37 | 38 | inner class EdgeBuffers { 39 | val positionBuffer = createBuffer(gl, GLType.vec3) 40 | val normalBuffer = createBuffer(gl, GLType.vec3) 41 | 42 | fun update(poly: Polyhedron, indexBuffer: Uint16Buffer? = null): Int { 43 | val bufferSize = poly.es.size * 2 44 | val indexSize = poly.es.size * 4 45 | positionBuffer.ensureCapacity(bufferSize) 46 | normalBuffer.ensureCapacity(bufferSize) 47 | indexBuffer?.ensureCapacity(indexSize) 48 | var bufOfs = 0 49 | var idxOfs = 0 50 | for (f in poly.fs) { 51 | for (i in 0 until f.size) { 52 | positionBuffer[bufOfs + i] = f[i] 53 | normalBuffer[bufOfs + i] = f 54 | } 55 | if (indexBuffer != null) { 56 | for (i in 0 until f.size) { 57 | val j = (i + 1) % f.size 58 | indexBuffer[idxOfs++] = bufOfs + i 59 | indexBuffer[idxOfs++] = bufOfs + j 60 | } 61 | } 62 | bufOfs += f.size 63 | } 64 | positionBuffer.bindBufferData(gl) 65 | normalBuffer.bindBufferData(gl) 66 | indexBuffer?.bindBufferData(gl, GL.ELEMENT_ARRAY_BUFFER) 67 | check(bufOfs == bufferSize) 68 | if (indexBuffer != null) check(idxOfs == indexSize) 69 | return indexSize 70 | } 71 | } 72 | } 73 | 74 | // cullMode: 0 - no, 1 - cull front, -1 - cull back 75 | fun EdgeContext.draw(view: ViewContext, cullMode: Int = 0) { 76 | if (!drawEdges) return 77 | val animation = animation 78 | val prevOrTarget = if (animation != null) prev else target 79 | program.use { 80 | assignView(view, cullMode) 81 | 82 | uVertexColor by color 83 | 84 | uTargetFraction by (animation?.targetFraction ?: 1.0) 85 | uPrevFraction by (animation?.prevFraction ?: 0.0) 86 | 87 | aPosition by target.positionBuffer 88 | aNormal by target.normalBuffer 89 | aPrevPosition by prevOrTarget.positionBuffer 90 | aPrevNormal by prevOrTarget.normalBuffer 91 | } 92 | gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, indexBuffer.glBuffer) 93 | gl.drawElements(GL.LINES, indexSize, GL.UNSIGNED_SHORT, 0) 94 | } 95 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/main/ConfigPopup.kt: -------------------------------------------------------------------------------- 1 | package polyhedra.js.main 2 | 3 | import polyhedra.common.util.* 4 | import polyhedra.js.components.* 5 | import polyhedra.js.params.* 6 | import react.* 7 | import react.dom.* 8 | 9 | fun RBuilder.configPopup(params: RootParams) { 10 | child(ConfigPopup::class) { 11 | attrs { 12 | this.params = params 13 | } 14 | } 15 | } 16 | 17 | @Suppress("NON_EXPORTABLE_TYPE") 18 | @JsExport 19 | class ConfigPopup(props: PComponentProps) : RComponent, RState>(props) { 20 | private inner class Context(params: RootParams) : Param.Context(params, Param.TargetValue) { 21 | val hasFaces by { params.render.view.display.value.hasFaces() } 22 | val animateUpdates by { params.animationParams.animateValueUpdates.value } 23 | val rotate by { params.animationParams.animatedRotation.value } 24 | 25 | val scale by { params.export.size.targetValue / 2 } 26 | val faceWidth by { params.render.view.faceWidth.targetValue } 27 | val faceRim by { params.render.view.faceRim.targetValue } 28 | 29 | init { setup() } 30 | 31 | override fun update() { 32 | forceUpdate() 33 | } 34 | } 35 | 36 | private val ctx = Context(props.params) 37 | 38 | override fun componentWillUnmount() { 39 | ctx.destroy() 40 | } 41 | 42 | override fun RBuilder.render() { 43 | groupHeader("View") 44 | tableBody { 45 | controlRow("Base scale") { pDropdown(props.params.render.poly.baseScale) } 46 | controlRow("View scale") { pSlider(props.params.render.view.scale) } 47 | controlRow("Expand") { pSlider(props.params.render.view.expandFaces) } 48 | controlRow("Display") { pDropdown(props.params.render.view.display) } 49 | } 50 | 51 | groupHeader("Faces") 52 | tableBody { 53 | controlRow("Transparent") { pSlider(props.params.render.view.transparentFaces, !ctx.hasFaces) } 54 | controlRow("Width") { 55 | pSlider(props.params.render.view.faceWidth, !ctx.hasFaces, showValue = false) 56 | span { +"${(ctx.scale * ctx.faceWidth).fmt(1)} (mm)" } 57 | } 58 | controlRow("Rim") { 59 | pSlider(props.params.render.view.faceRim, !ctx.hasFaces, showValue = false) 60 | span { +"${(ctx.scale * ctx.faceRim).fmt(1)} (mm)" } 61 | } 62 | } 63 | 64 | groupHeader("Animation") 65 | tableBody { 66 | controlRow2("Rotation", { pCheckbox(props.params.animationParams.animatedRotation) }) { 67 | pSlider(props.params.animationParams.rotationSpeed, !ctx.rotate) 68 | } 69 | controlRow2("Angle", {}, { 70 | pSlider(props.params.animationParams.rotationAngle, !ctx.rotate) 71 | }) 72 | controlRow2("Updates", { pCheckbox(props.params.animationParams.animateValueUpdates) }) { 73 | pSlider(props.params.animationParams.animationDuration, !ctx.animateUpdates) 74 | } 75 | } 76 | 77 | groupHeader("Lighting") 78 | tableBody { 79 | controlRow("Ambient") { pSlider(props.params.render.lighting.ambientLight, !ctx.hasFaces) } 80 | controlRow("Diffuse") { pSlider(props.params.render.lighting.diffuseLight, !ctx.hasFaces) } 81 | controlRow("Specular") { pSlider(props.params.render.lighting.specularLight, !ctx.hasFaces) } 82 | controlRow("Shininess") { pSlider(props.params.render.lighting.specularPower, !ctx.hasFaces) } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/glsl/GLBlock.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.glsl 6 | 7 | import polyhedra.common.util.* 8 | 9 | class GLBlockBuilder>( 10 | private val resultType: T, 11 | private val name: String, 12 | private val indent: String 13 | ) { 14 | private val locals = mutableSetOf>() 15 | private val deps = mutableSetOf>() 16 | private val body = ArrayList() 17 | 18 | operator fun String.unaryPlus() { body.add("$indent$this") } 19 | 20 | fun build(factory: (name: String, deps: Set>, body: List) -> F): F = 21 | factory(name, deps, body) 22 | 23 | infix fun > GLDecl.by(expr: GLExpr) { 24 | using(this) 25 | using(expr) 26 | +"$this = $expr;" 27 | } 28 | 29 | fun > using(expr: GLExpr) = expr.visitDecls { decl -> 30 | when (decl) { 31 | is GLLocal<*> -> if (locals.add(decl)) { 32 | +decl.emitDeclaration() 33 | } 34 | else -> deps += decl 35 | } 36 | } 37 | } 38 | 39 | fun functionVoid(builder: GLBlockBuilder.() -> Unit): DelegateProvider> = 40 | DelegateProvider { name -> 41 | GLBlockBuilder(GLType.void, name, "\t").run { 42 | builder() 43 | build { name, deps, body -> 44 | GLFun0(GLType.void, name, deps, body) 45 | } 46 | } 47 | } 48 | 49 | fun > function( 50 | resultType: T, 51 | builder: GLBlockBuilder.() -> GLExpr 52 | ): DelegateProvider> = 53 | functionX(resultType, builder, 54 | factory = { name, deps, body -> 55 | GLFun0(resultType, name, deps, body) 56 | } 57 | ) 58 | 59 | fun , P1 : GLType> function( 60 | resultType: T, 61 | param1Name: String, param1Type: P1, 62 | builder: GLBlockBuilder.(p1 : GLParameter) -> GLExpr 63 | ): DelegateProvider> { 64 | val p1 = GLParameter(null, param1Type, param1Name) 65 | return functionX(resultType, 66 | builder = { builder(p1) }, 67 | factory = { name, deps, body -> 68 | GLFun1(resultType, name, deps, body, p1) 69 | } 70 | ) 71 | } 72 | 73 | fun , P1 : GLType, P2 : GLType> function( 74 | resultType: T, 75 | param1Name: String, param1Type: P1, 76 | param2Name: String, param2Type: P2, 77 | builder: GLBlockBuilder.(p1 : GLParameter, p2 : GLParameter) -> GLExpr 78 | ): DelegateProvider> { 79 | val p1 = GLParameter(null, param1Type, param1Name) 80 | val p2 = GLParameter(null, param2Type, param2Name) 81 | return functionX(resultType, 82 | builder = { builder(p1, p2) }, 83 | factory = { name, deps, body -> 84 | GLFun2(resultType, name, deps, body, p1, p2) 85 | } 86 | ) 87 | } 88 | 89 | private fun , F> functionX( 90 | resultType: T, 91 | builder: GLBlockBuilder.() -> GLExpr, 92 | factory: (name: String, deps: Set>, body: List) -> F 93 | ): DelegateProvider = 94 | DelegateProvider { name -> 95 | GLBlockBuilder(resultType, name, "\t").run { 96 | val result = builder() 97 | using(result) 98 | +"return $result;" 99 | build(factory) 100 | } 101 | } 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/poly/Essence.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.poly 6 | 7 | import polyhedra.common.util.* 8 | import kotlin.math.* 9 | 10 | data class VertexFaceKind( 11 | val vk: VertexKind, 12 | val fk: FaceKind 13 | ) : Comparable { 14 | override fun compareTo(other: VertexFaceKind): Int { 15 | if (vk != other.vk) return vk.compareTo(other.vk) 16 | return fk.compareTo(other.fk) 17 | } 18 | 19 | override fun toString(): String = "$vk $fk" 20 | } 21 | 22 | class FaceKindEssence( 23 | val kind: FaceKind, 24 | val dist: Double, 25 | val isPlanar: Boolean, 26 | val vfs: List, 27 | val figure: PolygonProjection 28 | ) { 29 | fun approx(other: FaceKindEssence): Boolean = 30 | kind == other.kind && 31 | dist approx other.dist && 32 | vfs == other.vfs && 33 | figure approx other.figure 34 | 35 | override fun toString() = 36 | "distance ${dist.fmt}, " + 37 | "adj ${vfs.size} [${vfs.joinToString(" ")}]" 38 | } 39 | 40 | fun Face.essence(): FaceKindEssence { 41 | val fes = directedEdges 42 | val size = fes.size 43 | val vfs = List(size) { VertexFaceKind(fes[it].a.kind, fes[it].l.kind) } 44 | return FaceKindEssence(kind, d, isPlanar, vfs.minCycle(), computeProjectionFigure()) 45 | } 46 | 47 | fun FaceKindEssence.area(): Double { 48 | var sum = 0.0 49 | val vs = figure.vs 50 | val n = vs.size 51 | for (i in 0 until n) { 52 | val j = (i + 1) % n 53 | sum += (vs[j].x - vs[i].x) * (vs[j].y + vs[i].y) 54 | } 55 | return sum / 2 56 | } 57 | 58 | class VertexKindEssence( 59 | val kind: VertexKind, 60 | val dist: Double, 61 | val vfs: List, 62 | val figure: PolygonProjection 63 | ) { 64 | fun approx(other: VertexKindEssence): Boolean = 65 | kind == other.kind && 66 | dist approx other.dist && 67 | vfs == other.vfs && 68 | figure approx other.figure 69 | 70 | override fun toString() = 71 | "distance ${dist.fmt}, " + 72 | "adj ${vfs.size} [${vfs.joinToString(" ")}]" 73 | } 74 | 75 | fun Vertex.essence(): VertexKindEssence { 76 | val ves = directedEdges 77 | val size = ves.size 78 | val vfs = List(size) { VertexFaceKind(ves[it].b.kind, ves[it].r.kind) } 79 | return VertexKindEssence(kind, norm, vfs.minCycle(), computeProjectFigure()) 80 | } 81 | 82 | class EdgeKindEssence( 83 | val kind: EdgeKind, 84 | val dist: Double, 85 | val len: Double, 86 | val dihedralAngle: Double 87 | ) { 88 | fun approx(other: EdgeKindEssence): Boolean = 89 | kind == other.kind && 90 | dist approx other.dist && 91 | len approx other.len && 92 | dihedralAngle approx other.dihedralAngle 93 | } 94 | 95 | fun Edge.essence(): EdgeKindEssence { 96 | val dist = midPoint(MidPoint.Closest).norm 97 | val dihedralAngle = PI - acos(l * r) 98 | return EdgeKindEssence(kind, dist, len, dihedralAngle) 99 | } 100 | 101 | fun > List.minCycle(): List { 102 | var min = 0 103 | for (i in 1 until size) { 104 | var cmp = 0 105 | for (j in 0 until size) { 106 | val c = get((i + j) % size).compareTo(get((min + j) % size)) 107 | if (c != 0) { 108 | cmp = c 109 | break 110 | } 111 | } 112 | if (cmp < 0) min = i 113 | } 114 | return List(size) { get((min + it) % size) } 115 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/main/ExportPopup.kt: -------------------------------------------------------------------------------- 1 | package polyhedra.js.main 2 | 3 | import kotlinx.html.js.* 4 | import polyhedra.common.util.* 5 | import polyhedra.js.components.* 6 | import polyhedra.js.params.* 7 | import polyhedra.js.poly.* 8 | import react.* 9 | import react.dom.* 10 | 11 | fun RBuilder.exportPopup(params: RootParams, faces: FaceContext?) { 12 | child(ExportPopup::class) { 13 | attrs { 14 | this.params = params 15 | this.faces = faces 16 | } 17 | } 18 | } 19 | 20 | external interface ExportPopupProps : PComponentProps { 21 | var faces: FaceContext? 22 | } 23 | 24 | @Suppress("NON_EXPORTABLE_TYPE") 25 | @JsExport 26 | class ExportPopup(props: ExportPopupProps) : RComponent(props) { 27 | private inner class Context(params: RootParams) : Param.Context(params, Param.TargetValue) { 28 | val poly by { params.render.poly.poly } 29 | val polyName by { params.render.poly.polyName } 30 | val hasFaces by { params.render.view.display.value.hasFaces() } 31 | val scale by { params.export.size.targetValue / 2 } 32 | val faceWidth by { params.render.view.faceWidth.targetValue } 33 | val faceRim by { params.render.view.faceRim.targetValue } 34 | val expandFaces by { params.render.view.expandFaces.targetValue } 35 | 36 | init { setup() } 37 | 38 | override fun update() { 39 | forceUpdate() 40 | } 41 | } 42 | 43 | private val ctx = Context(props.params) 44 | 45 | override fun componentWillUnmount() { 46 | ctx.destroy() 47 | } 48 | 49 | override fun RBuilder.render() { 50 | groupHeader("Export size") 51 | tableBody { 52 | controlRow("Width") { 53 | pSlider(props.params.render.view.faceWidth, !ctx.hasFaces, showValue = false) 54 | span { +"${(ctx.scale * ctx.faceWidth).fmt(1)} (mm)" } 55 | } 56 | controlRow("Rim") { 57 | pSlider(props.params.render.view.faceRim, !ctx.hasFaces, showValue = false) 58 | span { +"${(ctx.scale * ctx.faceRim).fmt(1)} (mm)" } 59 | } 60 | controlRow("Overall size") { 61 | pSlider(props.params.export.size, !ctx.hasFaces) 62 | span("suffix") { +"(mm)" } 63 | } 64 | } 65 | 66 | groupHeader("Export solid") 67 | div("control row") { 68 | button { 69 | attrs { 70 | disabled = !ctx.hasFaces 71 | onClickFunction = lambda@{ 72 | val name = exportName() 73 | val description = props.params.toString() 74 | val exportParams = FaceExportParams(ctx.scale, ctx.faceWidth, ctx.faceRim, ctx.expandFaces) 75 | val faces = props.faces ?: return@lambda 76 | download("$name.stl", 77 | faces.exportSolidToStl(name, description, exportParams) 78 | ) 79 | } 80 | } 81 | +"Export to STL" 82 | } 83 | } 84 | 85 | groupHeader("Export geometry") 86 | div("control row") { 87 | button { 88 | attrs { 89 | onClickFunction = { 90 | val name = exportName() 91 | val description = props.params.toString() 92 | download("$name.scad", ctx.poly.exportGeometryToScad(name, description)) 93 | } 94 | } 95 | +"Export to SCAD" 96 | } 97 | } 98 | } 99 | 100 | private fun exportName() = ctx.polyName.replace(' ', '_').lowercase() 101 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/transform/Snub.kt: -------------------------------------------------------------------------------- 1 | package polyhedra.common.transform 2 | 3 | import polyhedra.common.poly.* 4 | import polyhedra.common.util.* 5 | import kotlin.math.* 6 | 7 | // a * x^2 + b * x + c = 0 8 | private fun solve3(a: Double, b: Double, c: Double) = 9 | (-b + sqrt(sqr(b) - 4 * a * c)) / (2 * a) 10 | 11 | private fun snubComputeSA(ea: Double, da: Double, cr: Double): Double { 12 | val rf = 1 - cr 13 | val cm = (1 - cos(da)) / 2 14 | val cp = (1 + cos(da)) / 2 15 | val cosGA = solve3(cp * sqr(rf), 2 * cm * rf * cos(ea), -sqr(cos(ea)) * (cm + sqr(rf))) 16 | return ea - acos(cosGA) 17 | } 18 | 19 | private fun snubComputeA(ea: Double, da: Double, cr: Double, sa: Double): Double { 20 | val h = 1 / (2 * tan(ea)) 21 | val ga = ea - sa 22 | val rf = 1 - cr 23 | val t = rf / (2 * sin(ea)) 24 | return Vec3( 25 | 2 * t * sin(ga), 26 | (h - t * cos(ga)) * (cos(da) - 1), 27 | (h - t * cos(ga)) * sin(da) 28 | ).norm 29 | } 30 | 31 | private fun snubComputeB(ea: Double, da: Double, cr: Double, sa: Double): Double { 32 | val h = 1 / (2 * tan(ea)) 33 | val ga = ea - sa 34 | val ha = ea + sa 35 | val rf = 1 - cr 36 | val t = rf / (2 * sin(ea)) 37 | return Vec3( 38 | t * (sin(ga) - sin(ha)), 39 | (h - t * cos(ga)) * cos(da) - (h - t * cos(ha)), 40 | (h - t * cos(ga)) * sin(da) 41 | ).norm 42 | } 43 | 44 | private fun snubComputeCR(ea: Double, da: Double): Double { 45 | var crL = 0.0 46 | var crR = 1.0 47 | while (true) { 48 | val cr = (crL + crR) / 2 49 | if (cr <= crL || cr >= crR) return cr // result precision is an ULP 50 | val sa = snubComputeSA(ea, da, cr) 51 | // error goes from positive to negative to NaN as cr goes from 0 to 1 52 | val rf = 1 - cr 53 | val err = snubComputeB(ea, da, cr, sa) - rf 54 | if (err <= 0) 55 | crL = cr else 56 | crR = cr 57 | } 58 | } 59 | 60 | data class SnubbingRatio(val cr: Double, val sa: Double) { 61 | override fun toString(): String = "(cr=${cr.fmt}, sa=${sa.fmt})" 62 | } 63 | 64 | fun Polyhedron.regularSnubbingRatio(edgeKind: EdgeKind? = null): SnubbingRatio { 65 | val (ea, da) = regularFaceGeometry(edgeKind) 66 | val cr = snubComputeCR(ea, da) 67 | val sa = snubComputeSA(ea, da, cr) 68 | return SnubbingRatio(cr, sa) 69 | } 70 | 71 | fun Polyhedron.snub( 72 | sr: SnubbingRatio = regularSnubbingRatio(), 73 | scale: Scale? = null, 74 | forceFaceKinds: List? = null 75 | ) = transformedPolyhedron(Transform.Snub, sr, scale, forceFaceKinds) { 76 | val (cr, sa) = sr 77 | val rr = dualReciprocationRadius 78 | // vertices from the face-vertices (directed edges) 79 | val fvv = fs.associateWith { f -> 80 | val c = f.dualPoint(rr) // for regular polygons -- face center 81 | val r = f.toRotationAroundQuat(-sa) 82 | f.directedEdges.associateBy({ it.a }, { e -> 83 | vertex(c + ((1 - cr) * (e.a - c)).rotated(r), VertexKind(directedEdgeKindsIndex[e.kind]!!)) 84 | }) 85 | } 86 | // faces from the original faces 87 | for (f in fs) { 88 | face(fvv[f]!!.values, f.kind) 89 | } 90 | // faces from the original vertices 91 | var kindOfs = faceKinds.size 92 | for (v in vs) { 93 | val fvs = v.directedEdges.map { fvv[it.r]!![v]!! } 94 | face(fvs, FaceKind(kindOfs + v.kind.id)) 95 | } 96 | for (vk in vertexKinds.keys) faceKindSource(FaceKind(kindOfs + vk.id), vk) 97 | // 3-faces from the directed edges 98 | kindOfs += vertexKinds.size 99 | for (e in directedEdges) { 100 | val fvs = listOf( 101 | fvv[e.l]!![e.a]!!, 102 | fvv[e.l]!![e.b]!!, 103 | fvv[e.r]!![e.a]!! 104 | ) 105 | face(fvs, FaceKind(kindOfs + directedEdgeKindsIndex[e.kind]!!)) 106 | } 107 | for ((ek, id) in directedEdgeKindsIndex) faceKindSource(FaceKind(kindOfs + id), ek) 108 | mergeIndistinguishableKinds() 109 | } 110 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/poly/FaceProgram.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.poly 6 | 7 | import polyhedra.js.glsl.* 8 | import org.khronos.webgl.WebGLRenderingContext as GL 9 | 10 | class FaceProgram(gl: GL) : ViewBaseProgram(gl) { 11 | val uAmbientLightColor by uniform(GLType.vec3) 12 | val uDiffuseLightColor by uniform(GLType.vec3) 13 | val uSpecularLightColor by uniform(GLType.vec3) 14 | val uSpecularLightPower by uniform(GLType.float) 15 | val uLightPosition by uniform(GLType.vec3) 16 | 17 | val uTargetFraction by uniform(GLType.float) 18 | val uPrevFraction by uniform(GLType.float) 19 | 20 | val aPosition by attribute(GLType.vec3) 21 | val aLightNormal by attribute(GLType.vec3) 22 | val aExpandDir by attribute(GLType.vec3) 23 | val aRimDir by attribute(GLType.vec3) 24 | val aRimMax by attribute(GLType.float) 25 | val aColor by attribute(GLType.vec3, GLPrecision.lowp) 26 | 27 | val aPrevPosition by attribute(GLType.vec3) 28 | val aPrevLightNormal by attribute(GLType.vec3) 29 | val aPrevExpandDir by attribute(GLType.vec3) 30 | val aPrevRimDir by attribute(GLType.vec3) 31 | val aPrevRimMax by attribute(GLType.float) 32 | val aPrevColor by attribute(GLType.vec3, GLPrecision.lowp) 33 | 34 | val aInner by attribute(GLType.float, GLPrecision.lowp) 35 | val aFaceMode by attribute(GLType.float, GLPrecision.lowp) 36 | 37 | private val vNormal by varying(GLType.vec3) 38 | private val vToCamera by varying(GLType.vec3) 39 | private val vToLight by varying(GLType.vec3) 40 | private val vColor by varying(GLType.vec3, GLPrecision.lowp) 41 | private val vColorAlpha by varying(GLType.float, GLPrecision.lowp) 42 | 43 | val fInterpolatedPosition by function(GLType.vec3) { 44 | val pos by aPosition * uTargetFraction + aPrevPosition * uPrevFraction 45 | val rd by aRimDir * min(uFaceRim, aRimMax) * uTargetFraction + aPrevRimDir * min(uFaceRim, aPrevRimMax) * uPrevFraction 46 | val diLen by aInner * uFaceWidth 47 | val posLen by length(pos) 48 | val di by pos * diLen / posLen 49 | pos - di + rd * (posLen - diLen) / posLen 50 | } 51 | 52 | val fInterpolatedLightNormal by function(GLType.vec3) { 53 | aLightNormal * uTargetFraction + aPrevLightNormal * uPrevFraction 54 | } 55 | 56 | val fInterpolatedExpandDir by function(GLType.vec3) { 57 | aExpandDir * uTargetFraction + aPrevExpandDir * uPrevFraction 58 | } 59 | 60 | // world position of the current element 61 | val fPosition by function(GLType.vec4) { 62 | fViewPosition(fInterpolatedPosition(), fInterpolatedExpandDir()) 63 | } 64 | 65 | // world normal of the current element 66 | val fLightNormal by function(GLType.vec3) { 67 | uNormalMatrix * fInterpolatedLightNormal() 68 | } 69 | 70 | val fInterpolatedColor by function(GLType.vec3) { 71 | aColor * uTargetFraction + aPrevColor * uPrevFraction 72 | } 73 | 74 | override val vertexShader = shader(ShaderType.Vertex) { 75 | // position 76 | val position by fPosition() 77 | gl_Position by uProjectionMatrix * position 78 | // lighting & color 79 | vNormal by fLightNormal() 80 | vToCamera by uCameraPosition - position.xyz 81 | vToLight by uLightPosition - position.xyz 82 | vColor by fInterpolatedColor() * aFaceMode 83 | vColorAlpha by uColorAlpha * fCullMull(position, uNormalMatrix * fInterpolatedExpandDir()) 84 | } 85 | 86 | override val fragmentShader = shader(ShaderType.Fragment) { 87 | val normToCamera by normalize(vToCamera) 88 | val normToLight by normalize(vToLight) 89 | val halfVector by normalize(normToCamera + normToLight) 90 | val light by uAmbientLightColor + uDiffuseLightColor * max(dot(vNormal, normToLight), 0.0) 91 | val specular by uSpecularLightColor * pow(max(dot(vNormal, halfVector), 0.0), uSpecularLightPower) 92 | gl_FragColor by vec4(vColor * light + specular, vColorAlpha) 93 | } 94 | } 95 | 96 | const val FACE_NORMAL = 1 97 | const val FACE_SELECTED = 2 -------------------------------------------------------------------------------- /src/jsMain/kotlin/poly/TransformAnimation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.poly 6 | 7 | import polyhedra.common.poly.* 8 | import polyhedra.common.transform.* 9 | import polyhedra.common.util.* 10 | 11 | interface TransformAnimation { 12 | val prevPoly: Polyhedron 13 | val prevFraction: Double 14 | val targetPoly: Polyhedron 15 | val targetFraction: Double 16 | val isOver: Boolean 17 | fun update(dt: Double) 18 | } 19 | 20 | class TransformAnimationStep( 21 | private val duration: Double, 22 | private val prev: TransformKeyframe, 23 | private val target: TransformKeyframe, 24 | ) : TransformAnimation { 25 | init { require(duration > 0) } 26 | 27 | private var position = 0.0 28 | 29 | override val isOver: Boolean 30 | get() = position >= duration 31 | 32 | override fun update(dt: Double) { 33 | position += dt 34 | } 35 | 36 | private val fraction: Double 37 | get() = (position / duration).coerceIn(0.0, 1.0) 38 | 39 | override val prevPoly = prev.poly 40 | 41 | override val prevFraction: Double 42 | get() = (fraction - target.fraction) / (prev.fraction - target.fraction) 43 | 44 | override val targetPoly: Polyhedron = target.poly 45 | 46 | override val targetFraction: Double 47 | get() = (fraction - prev.fraction) / (target.fraction - prev.fraction) 48 | 49 | override fun toString(): String = buildString { 50 | append("${position.fmt}/${duration.fmt}(${target.poly}: ") 51 | append(prevPoly.faceKinds.entries.joinToString { (fk, fl) -> 52 | val f = fl[0] 53 | "$fk -> ${targetPoly.fs[f.id].kind} (${fl.size} faces)" 54 | }) 55 | append(")") 56 | } 57 | } 58 | 59 | data class TransformKeyframe( 60 | val poly: Polyhedron, 61 | val fraction: Double 62 | ) 63 | 64 | class TransformAnimationList(private vararg val animations: TransformAnimation) : TransformAnimation { 65 | private var index = 0 66 | 67 | private val at: TransformAnimation 68 | get() = animations[index] 69 | 70 | override val prevPoly: Polyhedron 71 | get() = at.prevPoly 72 | 73 | override val prevFraction: Double 74 | get() = at.prevFraction 75 | 76 | override val targetPoly: Polyhedron 77 | get() = at.targetPoly 78 | 79 | override val targetFraction: Double 80 | get() = at.targetFraction 81 | 82 | override val isOver: Boolean 83 | get() = index == animations.lastIndex && at.isOver 84 | 85 | override fun update(dt: Double) { 86 | if (isOver) return 87 | at.update(dt) 88 | if (at.isOver && index < animations.lastIndex) index++ 89 | } 90 | 91 | override fun toString(): String = 92 | "$index/${animations.size}[${animations.joinToString()}]" 93 | } 94 | 95 | private const val GAP = 1e-4 96 | 97 | fun prevFractionGap(ratio: Double): Double = 98 | if (ratio <= 0 || ratio >= 1) GAP else 0.0 99 | 100 | fun curFractionGap(ratio: Double): Double = 101 | if (ratio <= 0 || ratio >= 1) 1 - GAP else 1.0 102 | 103 | fun Double.interpolate(prev: Double, target: Double): Double = 104 | (1 - this) * prev + this * target 105 | 106 | fun prevFractionGap(ratio: BevellingRatio): Double = 107 | if (ratio.cr <= 0 || ratio.cr >= 1 || ratio.tr <= 0 || ratio.tr >= 1) GAP else 0.0 108 | 109 | fun curFractionGap(ratio: BevellingRatio): Double = 110 | if (ratio.cr <= 0 || ratio.cr >= 1 || ratio.tr <= 0 || ratio.tr >= 1) 1 - GAP else 1.0 111 | 112 | fun Double.interpolate(prev: BevellingRatio, target: BevellingRatio): BevellingRatio = 113 | BevellingRatio( 114 | (1 - this) * prev.cr + this * target.cr, 115 | (1 - this) * prev.tr + this * target.tr 116 | ) 117 | 118 | fun prevFractionGap(ratio: SnubbingRatio): Double = 119 | if (ratio.cr <= 0 || ratio.cr >= 1) GAP else 0.0 120 | 121 | fun curFractionGap(ratio: SnubbingRatio): Double = 122 | if (ratio.cr <= 0 || ratio.cr >= 1) 1 - GAP else 1.0 123 | 124 | fun Double.interpolate(prev: SnubbingRatio, target: SnubbingRatio): SnubbingRatio = 125 | SnubbingRatio( 126 | (1 - this) * prev.cr + this * target.cr, 127 | (1 - this) * prev.sa + this * target.sa 128 | ) 129 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/IdMap.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | interface Id { val id: Int } 8 | 9 | inline fun Iterable.associateById(keyTransform: (T) -> K, valueTransform: (T) -> V): IdMap { 10 | val result = ArrayIdMap() 11 | for (e in this) { 12 | result[keyTransform(e)] = valueTransform(e) 13 | } 14 | return result 15 | } 16 | 17 | inline fun Iterable.associateById(keyTransform: (T) -> K): IdMap = 18 | associateById(keyTransform, { it }) 19 | 20 | inline fun Iterable.groupById(keyTransform: (T) -> K, valueTransform: (T) -> V): IdMap> { 21 | val result = ArrayIdMap>() 22 | for (e in this) { 23 | val k = keyTransform(e) 24 | val l = result.getOrPut(k) { ArrayList() } 25 | l.add(valueTransform(e)) 26 | } 27 | return result 28 | } 29 | 30 | inline fun Iterable.groupById(keyTransform: (T) -> K): IdMap> = 31 | groupById(keyTransform, { it }) 32 | 33 | public inline fun IdMap.mapValues(transform: (Map.Entry) -> R): IdMap { 34 | val result = ArrayIdMap() 35 | for (e in entries) { 36 | result[e.key] = transform(e) 37 | } 38 | return result 39 | } 40 | 41 | interface IdMap : Map 42 | 43 | @Suppress("UNCHECKED_CAST") 44 | class ArrayIdMap(capacity: Int = 8) : AbstractMutableMap(), IdMap { 45 | public override var size: Int = 0 46 | private set 47 | private var ks = arrayOfNulls(capacity) 48 | private var vs = arrayOfNulls(capacity) 49 | 50 | override val keys: MutableSet 51 | get() = ViewSet { ks[it] as K } 52 | 53 | override val values: MutableCollection 54 | get() = ViewCollection { vs[it] as V } 55 | 56 | override val entries: MutableSet> 57 | get() = ViewSet { Entry(it) } 58 | 59 | override fun put(key: K, value: V): V? { 60 | if (key.id >= ks.size) ensureCapacity(key.id + 1) 61 | if (ks[key.id] == null) size++ 62 | val old = vs[key.id] as V? 63 | ks[key.id] = key 64 | vs[key.id] = value 65 | return old 66 | } 67 | 68 | override fun get(key: K): V? = vs.getOrNull(key.id) as V? 69 | 70 | fun ensureCapacity(capacity: Int) { 71 | val size = capacity.coerceAtLeast(ks.size * 2) 72 | ks = ks.copyOf(size) 73 | vs = vs.copyOf(size) 74 | } 75 | 76 | private inner class ViewIterator(val view: (Int) -> T) : MutableIterator { 77 | private var i = -1 78 | private fun moveToNext(): Int { 79 | if (i >= ks.size) return i 80 | val prev = i++ 81 | while (i < ks.size && ks[i] == null) i++ 82 | return prev 83 | } 84 | init { 85 | moveToNext() 86 | } 87 | 88 | override fun hasNext(): Boolean = i < ks.size 89 | override fun next(): T = view(moveToNext()) 90 | override fun remove() = throw UnsupportedOperationException() 91 | } 92 | 93 | private inner class ViewCollection(val view: (Int) -> T) : AbstractMutableCollection() { 94 | override val size: Int 95 | get() = this@ArrayIdMap.size 96 | override fun iterator(): MutableIterator = ViewIterator(view) 97 | override fun add(element: T): Boolean = throw UnsupportedOperationException() 98 | } 99 | 100 | private inner class ViewSet(val view: (Int) -> T) : AbstractMutableSet() { 101 | override val size: Int 102 | get() = this@ArrayIdMap.size 103 | override fun iterator(): MutableIterator = ViewIterator(view) 104 | override fun add(element: T): Boolean = throw UnsupportedOperationException() 105 | } 106 | 107 | private inner class Entry(val index: Int) : MutableMap.MutableEntry { 108 | override val key: K 109 | get() = ks[index] as K 110 | override val value: V 111 | get() = vs[index] as V 112 | override fun setValue(newValue: V): V = 113 | (vs[index] as V).also { vs[index] = newValue } 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/Quat.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | import kotlin.js.* 8 | import kotlin.math.* 9 | 10 | interface Quat { 11 | val x: Double 12 | val y: Double 13 | val z: Double 14 | val w: Double 15 | 16 | companion object { 17 | val ID: Quat = MutableQuat() 18 | } 19 | } 20 | 21 | data class MutableQuat( 22 | override var x: Double, 23 | override var y: Double, 24 | override var z: Double, 25 | override var w: Double, 26 | ) : Quat { 27 | override fun toString(): String = 28 | "Quat[${x.fmt}, ${y.fmt}, ${z.fmt}, ${w.fmt}]" 29 | } 30 | 31 | @JsName("MutableQuatId") 32 | fun MutableQuat(): MutableQuat = MutableQuat(0.0, 0.0, 0.0, 1.0) 33 | 34 | fun Quat.toMutableQuat(): MutableQuat = 35 | MutableQuat(x, y, z, w) 36 | 37 | fun Quat(x: Double, y: Double, z: Double, w: Double): Quat = 38 | MutableQuat(x, y, z, w) 39 | 40 | infix fun MutableQuat.by(q: Quat) = by(q.x, q.y, q.z, q.w) 41 | 42 | fun MutableQuat.by(x: Double, y: Double, z: Double, w: Double) { 43 | this.x = x 44 | this.y = y 45 | this.z = z 46 | this.w = w 47 | } 48 | 49 | fun norm(x: Double, y: Double, z: Double, w: Double): Double = 50 | sqrt(sqr(x) + sqr(y) + sqr(z) + sqr(w)) 51 | 52 | val Quat.norm: Double 53 | get() = norm(x, y, z, w) 54 | 55 | val Quat.unit: Quat get() { 56 | val n = norm 57 | if (n < EPS) return MutableQuat() 58 | return Quat(x / n, y / n, z / n, w / n) 59 | } 60 | 61 | operator fun Quat.plus(q: Quat): Quat = 62 | Quat(x + q.x, y + q.y, z + q.z, w + q.w) 63 | 64 | operator fun Quat.times(q: Quat): Quat = 65 | q.toMutableQuat().also { it.multiplyFront(this) } 66 | 67 | operator fun Quat.times(a: Double): Quat = 68 | Quat(x * a, y * a, z * a, w * a) 69 | 70 | operator fun Double.times(q: Quat): Quat = q * this 71 | 72 | operator fun MutableQuat.divAssign(a: Double) { 73 | x /= a 74 | y /= a 75 | z /= a 76 | w /= a 77 | } 78 | 79 | fun Vec3.toRotationAroundQuat(angle: Double): Quat = 80 | rotationAroundQuat(x, y, z, angle) 81 | 82 | fun rotationAroundQuat(x: Double, y: Double, z: Double, angle: Double): Quat { 83 | val s = sin(angle * 0.5) / norm(x, y, z) 84 | val w = cos(angle * 0.5) 85 | return Quat(s * x, s * y, s * z, w) 86 | } 87 | 88 | fun rotationBetweenQuat(v1: Vec3, v2: Vec3): Quat { 89 | val c = v1 cross v2 90 | if (c approx Vec3.ZERO) return Quat.ID 91 | val q = MutableQuat(c.x, c.y, c.z, v1.norm * v2.norm + v1 * v2) 92 | q /= q.norm 93 | return q 94 | } 95 | 96 | fun MutableQuat.multiplyFront(x: Double, y: Double, z: Double, w: Double): Unit = by( 97 | w * this.x + x * this.w + y * this.z - z * this.y, 98 | w * this.y - x * this.z + y * this.w + z * this.x, 99 | w * this.z + x * this.y - y * this.x + z * this.w, 100 | w * this.w - x * this.x - y * this.y - z * this.z 101 | ) 102 | 103 | fun MutableQuat.multiplyFront(q: Quat): Unit = 104 | multiplyFront(q.x, q.y, q.z, q.w) 105 | 106 | // multiplies by a pure quaternion of the given vector 107 | fun MutableQuat.multiplyFront(v: Vec3): Unit = by( 108 | +v.x * w + v.y * z - v.z * y, 109 | -v.x * z + v.y * w + v.z * x, 110 | +v.x * y - v.y * x + v.z * w, 111 | -v.x * x - v.y * y - v.z * z 112 | ) 113 | 114 | fun Vec3.rotated(q: Quat): Vec3 { 115 | val r = MutableQuat(-q.x, -q.y, -q.z, q.w) 116 | r.multiplyFront(this) 117 | r.multiplyFront(q) 118 | return Vec3(r.x, r.y, r.z) 119 | } 120 | 121 | fun Quat.toAngles(): Vec3 { 122 | val sy = 2 * (w * y - z * x) 123 | if (sy.absoluteValue >= 1 - EPS) 124 | return Vec3(0.0, PI * 0.5 * sy.sign, 0.0) 125 | val sx = 1 - 2 * (x * x + y * y) 126 | val cx = 2 * (w * x + y * z) 127 | val sz = 1 - 2 * (y * y + z * z) 128 | val cz = 2 * (w * z + x * y) 129 | return Vec3(atan2(cx, sx), asin(sy), atan2(cz, sz)) 130 | } 131 | 132 | fun Vec3.anglesToQuat(): Quat = 133 | anglesToQuat(x, y, z) 134 | 135 | fun anglesToQuat(x: Double, y: Double, z: Double): Quat { 136 | val cy = cos(z * 0.5); 137 | val sy = sin(z * 0.5); 138 | val cp = cos(y * 0.5); 139 | val sp = sin(y * 0.5); 140 | val cr = cos(x * 0.5); 141 | val sr = sin(x * 0.5); 142 | return Quat( 143 | sr * cp * cy - cr * sp * sy, 144 | cr * sp * cy + sr * cp * sy, 145 | cr * cp * sy - sr * sp * cy, 146 | cr * cp * cy + sr * sp * sy 147 | ) 148 | } 149 | 150 | infix fun Quat.approx(q: Quat): Boolean = 151 | w approx q.w && x approx q.x && y approx q.y && z approx q.z || 152 | w approx -q.w && x approx -q.x && y approx -q.y && z approx -q.z 153 | 154 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/util/Vec3.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.util 6 | 7 | import kotlinx.serialization.* 8 | import polyhedra.common.poly.* 9 | import kotlin.math.* 10 | 11 | interface Vec3 { 12 | val x: Double 13 | val y: Double 14 | val z: Double 15 | 16 | companion object { 17 | val ZERO = MutableVec3() 18 | } 19 | } 20 | 21 | @Serializable 22 | open class MutableVec3( 23 | override var x: Double = 0.0, 24 | override var y: Double = 0.0, 25 | override var z: Double = 0.0 26 | ) : Vec3 { 27 | constructor(v: Vec3) : this(v.x, v.y, v.z) 28 | override fun toString(): String = "[${x.fmt}, ${y.fmt}, ${z.fmt}]" 29 | } 30 | 31 | fun MutableVec3.setToZero() { 32 | x = 0.0 33 | y = 0.0 34 | z = 0.0 35 | } 36 | 37 | fun MutableVec3.set(x: Double, y: Double, z: Double) { 38 | this.x = x 39 | this.y = y 40 | this.z = z 41 | } 42 | 43 | fun Vec3.toPreciseString(): String = 44 | "[$x, $y, $z]" 45 | 46 | fun Vec3(x: Double, y: Double, z: Double): Vec3 = MutableVec3(x, y, z) 47 | 48 | fun Vec3.toMutableVec3(): MutableVec3 = MutableVec3(x, y, z) 49 | 50 | fun norm(x: Double, y: Double, z: Double): Double = 51 | sqrt(sqr(x) + sqr(y) + sqr(z)) 52 | 53 | val Vec3.norm: Double 54 | get() = norm(x, y, z) 55 | 56 | val Vec3.unit: Vec3 57 | get() { 58 | val norm = norm 59 | return if (abs(norm) < EPS) this else this / norm 60 | } 61 | 62 | operator fun MutableVec3.timesAssign(a: Double) { 63 | x *= a 64 | y *= a 65 | z *= a 66 | } 67 | 68 | operator fun MutableVec3.divAssign(a: Double) { 69 | x /= a 70 | y /= a 71 | z /= a 72 | } 73 | 74 | operator fun MutableVec3.divAssign(a: Int) { 75 | x /= a 76 | y /= a 77 | z /= a 78 | } 79 | 80 | operator fun MutableVec3.plusAssign(u: Vec3) { 81 | x += u.x 82 | y += u.y 83 | z += u.z 84 | } 85 | 86 | fun MutableVec3.plusAssignMul(u: Vec3, a: Double) { 87 | x += u.x * a 88 | y += u.y * a 89 | z += u.z * a 90 | } 91 | 92 | operator fun MutableVec3.minusAssign(u: Vec3) { 93 | x -= u.x 94 | y -= u.y 95 | z -= u.z 96 | } 97 | 98 | operator fun Vec3.plus(u: Vec3): Vec3 = Vec3(x + u.x, y + u.y, z + u.z) 99 | operator fun Vec3.minus(u: Vec3): Vec3 = Vec3(x - u.x, y - u.y, z - u.z) 100 | 101 | operator fun Vec3.times(u: Vec3): Double = x * u.x + y * u.y + z * u.z 102 | operator fun Vec3.times(a: Double): Vec3 = Vec3(x * a, y * a, z * a) 103 | operator fun Double.times(u: Vec3): Vec3 = u * this 104 | 105 | operator fun Vec3.div(d: Double): Vec3 = Vec3(x / d, y / d, z / d) 106 | operator fun Vec3.unaryMinus(): Vec3 = Vec3(-x, -y, -z) 107 | 108 | infix fun Vec3.cross(u: Vec3) = Vec3( 109 | y * u.z - z * u.y, 110 | -x * u.z + z * u.x, 111 | x * u.y - y * u.x 112 | ) 113 | 114 | // dest += (a - c) cross (b - c) 115 | fun crossCenteredAddTo(dest: MutableVec3, a: Vec3, b: Vec3, c: Vec3): Vec3 { 116 | val ax = a.x - c.x 117 | val ay = a.y - c.y 118 | val az = a.z - c.z 119 | val bx = b.x - c.x 120 | val by = b.y - c.y 121 | val bz = b.z - c.z 122 | dest.x += ay * bz - az * by 123 | dest.y += -ax * bz + az * bx 124 | dest.z += ax * by - ay * bx 125 | return dest 126 | } 127 | 128 | // when this == 0.0 -> result is a 129 | // when this == 1.0 -> result is b 130 | fun Double.atSegmentTo(dest: MutableVec3, a: Vec3, b: Vec3): Vec3 { // a + this * (b - a) 131 | val f = this 132 | dest.set(a.x + f * (b.x - a.x), a.y + f * (b.y - a.y), a.z + f * (b.z - a.z)) 133 | return dest 134 | } 135 | 136 | // when this == 0.0 -> result is a 137 | // when this == 1.0 -> result is b 138 | fun Double.atSegment(a: Vec3, b: Vec3): Vec3 = // a + this * (b - a) 139 | atSegmentTo(MutableVec3(), a, b) 140 | 141 | // when this == 0.0 -> result is a.norm 142 | // when this == 1.0 -> result is b.norm 143 | fun Double.distanceAtSegment(a: Vec3, b: Vec3): Double { // norm(a + this * (b - a)) 144 | val f = this 145 | return norm(a.x + f * (b.x - a.x), a.y + f * (b.y - a.y), a.z + f * (b.z - a.z)) 146 | } 147 | 148 | infix fun Vec3.approx(u: Vec3): Boolean = 149 | x approx u.x && y approx u.y && z approx u.z 150 | 151 | object Vec3ApproxComparator : Comparator { 152 | override fun compare(a: Vec3, b: Vec3): Int { 153 | val x = DoubleApproxComparator.compare(a.x, b.x) 154 | if (x != 0) return x 155 | val y = DoubleApproxComparator.compare(a.y, b.y) 156 | if (y != 0) return y 157 | return DoubleApproxComparator.compare(a.z, b.z) 158 | } 159 | } 160 | 161 | // distance from this point to a line A-B 162 | fun Vec3.distanceToLine(a: Vec3, b: Vec3): Double { 163 | val a0 = a - this 164 | val b0 = b - this 165 | return tangentDistance(a0, b0) 166 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/glsl/GLDecl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.glsl 6 | 7 | import kotlin.reflect.* 8 | 9 | enum class GLDeclKind(val isGlobal: Boolean = false) { 10 | builtin, 11 | uniform(true), 12 | attribute(true), 13 | varying(true), 14 | function(true), 15 | parameter, 16 | local; 17 | } 18 | 19 | open class GLDecl, SELF: GLDecl>( 20 | val kind: GLDeclKind, 21 | val precision: GLPrecision?, 22 | override val type: T, 23 | val name: String 24 | ) : GLExpr { 25 | @Suppress("UNCHECKED_CAST") 26 | operator fun getValue(thisRef: Any?, prop: KProperty<*>): SELF = this as SELF 27 | 28 | override fun visitDecls(visitor: (GLDecl<*, *>) -> Unit) { 29 | visitor(this) 30 | } 31 | 32 | override fun toString(): String = name 33 | 34 | open fun emitDeclaration(): String = 35 | if (precision == null) "$kind $type $name;" else "$kind $precision $type $name;" 36 | } 37 | 38 | private class FunctionCall>( 39 | override val type: T, 40 | val function: GLDecl<*, *>, 41 | val name: String, 42 | vararg val a: GLExpr<*> 43 | ) : GLExpr { 44 | override fun visitDecls(visitor: (GLDecl<*, *>) -> Unit) { 45 | function.visitDecls(visitor) 46 | a.forEach { it.visitDecls(visitor) } 47 | } 48 | 49 | override fun toString(): String = "$name(${a.joinToString(", ")})" 50 | } 51 | 52 | abstract class GLFunX, F : GLType, SELF : GLFunX>( 53 | val resultType: T, 54 | funType: F, 55 | name: String, 56 | private val deps: Set>, 57 | private val body: List, 58 | private vararg val params: GLParameter<*> 59 | ) : GLDecl(GLDeclKind.function, null, funType, name) { 60 | override fun emitDeclaration() = buildString { 61 | appendLine("$resultType $name(${params.joinToString { it.emitDeclaration() }}) {") 62 | body.forEach { appendLine(it) } 63 | append("}") 64 | } 65 | 66 | override fun visitDecls(visitor: (GLDecl<*, *>) -> Unit) { 67 | deps.forEach { visitor(it) } 68 | visitor(this) 69 | } 70 | } 71 | 72 | class GLFun0>( 73 | resultType: T, 74 | name: String, 75 | deps: Set>, 76 | body: List 77 | ) : GLFunX, GLFun0>(resultType, GLType.fun0(resultType), name, deps, body) { 78 | operator fun invoke(): GLExpr = FunctionCall(resultType, this, name) 79 | } 80 | 81 | class GLFun1, P1 : GLType>( 82 | resultType: T, 83 | name: String, 84 | deps: Set>, 85 | body: List, 86 | param1: GLParameter, 87 | ) : GLFunX, GLFun1>(resultType, GLType.fun1(resultType), name, deps, body, param1) { 88 | operator fun invoke(p1: GLExpr): GLExpr = FunctionCall(resultType,this, name, p1) 89 | } 90 | 91 | class GLFun2, P1 : GLType, P2 : GLType>( 92 | resultType: T, 93 | name: String, 94 | deps: Set>, 95 | body: List, 96 | param1: GLParameter, 97 | param2: GLParameter, 98 | ) : GLFunX, GLFun2>(resultType, GLType.fun2(resultType), name, deps, body, param1, param2) { 99 | operator fun invoke(p1: GLExpr, p2: GLExpr): GLExpr = FunctionCall(resultType,this, name, p1, p2) 100 | } 101 | 102 | class GLParameter>( 103 | precision: GLPrecision?, 104 | type: T, 105 | name: String 106 | ) : GLDecl>(GLDeclKind.parameter, precision, type, name) { 107 | override fun visitDecls(visitor: (GLDecl<*, *>) -> Unit) { 108 | visitor(this) 109 | } 110 | 111 | override fun emitDeclaration(): String = buildString { 112 | append(if (precision == null) "$type $name" else "$precision $type $name") 113 | } 114 | } 115 | 116 | class GLLocal>( 117 | precision: GLPrecision?, 118 | type: T, 119 | name: String, 120 | val value: GLExpr 121 | ) : GLDecl>(GLDeclKind.local, precision, type, name) { 122 | override fun visitDecls(visitor: (GLDecl<*, *>) -> Unit) { 123 | value.visitDecls(visitor) 124 | visitor(this) 125 | } 126 | 127 | override fun emitDeclaration(): String = buildString { 128 | append(if (precision == null) "$type $name" else "$precision $type $name") 129 | append(" = ") 130 | append(value) 131 | append(";") 132 | } 133 | } 134 | 135 | operator fun > GLExpr.provideDelegate(thisRef: Any?, prop: KProperty<*>): LocalProvider = 136 | LocalProvider(prop.name, this) 137 | 138 | class LocalProvider>(private val name: String, private val value: GLExpr) { 139 | private val local = GLLocal(null, value.type, name, value) 140 | operator fun getValue(thisRef: Any?, prop: KProperty<*>): GLLocal = local 141 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/glsl/GLProgram.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.glsl 6 | 7 | import org.khronos.webgl.* 8 | import polyhedra.common.* 9 | import polyhedra.common.util.* 10 | import polyhedra.js.poly.* 11 | import polyhedra.js.util.* 12 | import kotlin.properties.* 13 | import kotlin.reflect.* 14 | import org.khronos.webgl.WebGLRenderingContext as GL 15 | 16 | abstract class GLProgram(val gl: GL) { 17 | val gl_Position by builtin(GLType.vec4) 18 | val gl_FragColor by builtin(GLType.vec4) 19 | 20 | abstract val vertexShader: Shader 21 | abstract val fragmentShader: Shader 22 | 23 | val program by lazy { 24 | initShaderProgram(gl, vertexShader.glShader, fragmentShader.glShader) 25 | } 26 | 27 | fun shader(type: S, builder: GLBlockBuilder.() -> Unit): Shader { 28 | val main by functionVoid(builder) 29 | return Shader(loadShader(gl, type.glType, shaderSource(main))) 30 | } 31 | 32 | inner class Uniform>( 33 | precision: GLPrecision?, type: T, name: String 34 | ) : GLDecl>(GLDeclKind.uniform, precision, type, name) { 35 | val location by lazy { gl.getUniformLocation(program, name)!! } 36 | var isUsed = false 37 | 38 | override fun emitDeclaration(): String { 39 | isUsed = true 40 | return super.emitDeclaration() 41 | } 42 | } 43 | 44 | infix fun Uniform.by(value: Int) { 45 | if (isUsed) { 46 | gl.uniform1i(location, value) 47 | } 48 | } 49 | 50 | infix fun Uniform.by(value: Double) { 51 | if (isUsed) { 52 | gl.uniform1f(location, value.toFloat()) 53 | } 54 | } 55 | 56 | infix fun > Uniform.by(value: Float32Array) { 57 | if (isUsed) { 58 | type.uniformFloat32Array(gl, location, value) 59 | } 60 | } 61 | 62 | inner class Attribute>( 63 | precision: GLPrecision?, type: T, name: String 64 | ) : GLDecl>(GLDeclKind.attribute, precision, type, name) { 65 | val gl: GL get() = this@GLProgram.gl 66 | val location by lazy { gl.getAttribLocation(program, name) } 67 | } 68 | 69 | infix fun > Attribute.by(buffer: Uint8Buffer) { 70 | gl.bindBuffer(GL.ARRAY_BUFFER, buffer.glBuffer) 71 | gl.vertexAttribPointer(location, type.bufferSize, GL.UNSIGNED_BYTE, false, 0, 0) 72 | gl.enableVertexAttribArray(location) 73 | } 74 | 75 | infix fun > Attribute.by(buffer: Float32Buffer) { 76 | gl.bindBuffer(GL.ARRAY_BUFFER, buffer.glBuffer) 77 | gl.vertexAttribPointer(location, type.bufferSize, GL.FLOAT, false, 0, 0) 78 | gl.enableVertexAttribArray(location) 79 | } 80 | 81 | inner class Varying>( 82 | precision: GLPrecision?, type: T, name: String 83 | ) : GLDecl>(GLDeclKind.varying, precision, type, name) 84 | 85 | inner class Builtin>( 86 | type: T, name: String 87 | ) : GLDecl>(GLDeclKind.builtin, null, type, name) { 88 | override fun visitDecls(visitor: (GLDecl<*, *>) -> Unit) {} 89 | } 90 | 91 | inner class Shader( 92 | val glShader: WebGLShader 93 | ) 94 | 95 | private fun shaderSource(main: GLFun0): String = buildString { 96 | val decls = mutableSetOf>() 97 | main.visitDecls { decl -> 98 | if (decl.kind.isGlobal) decls += decl 99 | } 100 | appendLine("precision mediump float;") // default precision 101 | val sd = decls.sortedBy { it.kind } 102 | for (d in sd) appendLine(d.emitDeclaration()) 103 | } 104 | 105 | fun > uniform(type: T, precision: GLPrecision? = null): DelegateProvider> = 106 | DelegateProvider { Uniform(precision, type, it) } 107 | 108 | // GLSL: The attribute qualifier can be used only with the data types float, vec2, vec3, vec4, mat2, mat3, and mat4. 109 | fun > attribute(type: T, precision: GLPrecision? = null): DelegateProvider> = 110 | DelegateProvider { Attribute(precision, type, it) } 111 | 112 | fun > varying(type: T, precision: GLPrecision? = null): DelegateProvider> = 113 | DelegateProvider { Varying(precision, type, it) } 114 | 115 | private fun > builtin(type: T): DelegateProvider> = 116 | DelegateProvider { Builtin(type, it) } 117 | } 118 | 119 | fun

P.use() { 120 | gl.useProgram(program) 121 | } 122 | 123 | fun

P.use(block: P.() -> Unit) { 124 | use() 125 | block() 126 | } 127 | 128 | sealed class ShaderType(val glType: Int) { 129 | object Vertex : ShaderType(GL.VERTEX_SHADER) 130 | object Fragment : ShaderType(GL.FRAGMENT_SHADER) 131 | } 132 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/transform/TransformCache.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.common.transform 6 | 7 | import polyhedra.common.poly.* 8 | import polyhedra.common.util.* 9 | 10 | private const val POW2 = 7 11 | private const val SHIFT = 32 - POW2 12 | private const val HASH_CAPACITY = 1 shl POW2 13 | private const val CACHE_SIZE_LIMIT = HASH_CAPACITY / 2 14 | private const val HASH_MASK = (1 shl POW2) - 1 15 | private const val MAGIC = 2654435769L.toInt() // golden ratio 16 | 17 | @Suppress("RESULT_CLASS_IN_RETURN_TYPE") 18 | object TransformCache { 19 | private val hash = arrayOfNulls(HASH_CAPACITY) 20 | private var hashSize = 0 21 | private var lruFirst: Entry? = null 22 | private var lruLast: Entry? = null 23 | 24 | private class Entry( 25 | var hashIndex: Int, 26 | val poly: Polyhedron, 27 | val key: Any, 28 | val param: Any?, 29 | var result: Result 30 | ) { 31 | var lruPrev: Entry? = null 32 | var lruNext: Entry? = null 33 | } 34 | 35 | private fun hashIndex0(poly: Polyhedron, key: Any, param: Any?): Int = 36 | (((poly.hashCode() * 31 + key.hashCode()) * 31 + param.hashCode()) * MAGIC) ushr SHIFT 37 | 38 | private fun pullUpLru(entry: Entry) { 39 | if (entry.lruPrev == null) return // already first 40 | removeLru(entry) 41 | addLruFirst(entry) 42 | } 43 | 44 | private fun removeLru(entry: Entry) { 45 | val prev = entry.lruPrev 46 | val next = entry.lruNext 47 | if (prev == null) lruFirst = next else prev.lruNext = next 48 | if (next == null) lruLast = prev else next.lruPrev = prev 49 | } 50 | 51 | private fun addLruFirst(entry: Entry) { 52 | val first = lruFirst 53 | lruFirst = entry 54 | entry.lruPrev = null 55 | entry.lruNext = first 56 | if (first == null) lruLast = entry else first.lruPrev = entry 57 | } 58 | 59 | private fun dropOldest() { 60 | val last = lruLast ?: return 61 | val prev = last.lruPrev 62 | lruLast = prev 63 | prev?.lruNext = null 64 | hashRemove(last) 65 | } 66 | 67 | private fun hashRemove(entry: Entry) { 68 | hash[entry.hashIndex] = null 69 | hashSize-- 70 | var hole = entry.hashIndex 71 | var i = hole 72 | while (true) { 73 | if (i == 0) i = hash.size 74 | i-- 75 | val e = hash[i] ?: return 76 | val j = hashIndex0(e.poly, e.key, e.param) 77 | val entryDist = (j - i) and HASH_MASK 78 | val holeDist = (hole - i) and HASH_MASK 79 | if (holeDist <= entryDist) { 80 | e.hashIndex = hole 81 | hash[hole] = e 82 | hash[i] = null 83 | hole = i 84 | } 85 | } 86 | } 87 | 88 | operator fun get(poly: Polyhedron, key: Any, param: Any? = null): Polyhedron? { 89 | var i = hashIndex0(poly, key, param) 90 | while(true) { 91 | val e = hash[i] ?: return null 92 | if (e.poly == poly && e.key == key && e.param == param) { 93 | pullUpLru(e) 94 | return e.result.getOrThrow() 95 | } 96 | if (i == 0) i = hash.size 97 | i-- 98 | } 99 | } 100 | 101 | operator fun set(poly: Polyhedron, key: Any, param: Any? = null, result: Result) { 102 | var i = hashIndex0(poly, key, param) 103 | while(true) { 104 | val e = hash[i] ?: break 105 | if (e.poly == poly && e.key == key && e.param == param) { 106 | e.result = result 107 | pullUpLru(e) 108 | return 109 | } 110 | if (i == 0) i = hash.size 111 | i-- 112 | } 113 | if (hashSize >= CACHE_SIZE_LIMIT) dropOldest() 114 | val e = Entry(i, poly, key, param, result) 115 | hash[i] = e 116 | hashSize++ 117 | addLruFirst(e) 118 | } 119 | } 120 | 121 | fun Polyhedron.transformedPolyhedron( 122 | key: Any, 123 | param: Any? = null, 124 | scale: Scale? = null, 125 | forceFaceKinds: List? = null, 126 | block: PolyhedronBuilder.() -> Unit 127 | ): Polyhedron { 128 | TransformCache[this, key, param]?.let { cached -> 129 | // update cached copy as needed 130 | return cached.scaled(scale).forceFaceKinds(forceFaceKinds) 131 | } 132 | // Optimization: store polyhedron with the requested scale in cache 133 | val cache = runCatching { 134 | polyhedron { 135 | block() 136 | scale(scale) 137 | } 138 | } 139 | TransformCache[this, key, param] = cache 140 | return cache.getOrThrow().forceFaceKinds(forceFaceKinds) 141 | } 142 | 143 | private object ForceFaceKindsKey 144 | 145 | private fun Polyhedron.forceFaceKinds(forceFaceKinds: List?): Polyhedron { 146 | if (forceFaceKinds == null) return this 147 | return transformedPolyhedron(ForceFaceKindsKey, forceFaceKinds) { 148 | vertices(vs) 149 | faces(fs) 150 | faceKindSources(faceKindSources) 151 | forceFaceKinds(forceFaceKinds) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/worker/WorkerMain.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Roman Elizarov. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package polyhedra.js.worker 6 | 7 | import kotlinx.browser.* 8 | import kotlinx.coroutines.* 9 | import kotlinx.serialization.* 10 | import kotlinx.serialization.json.* 11 | import org.w3c.dom.* 12 | import polyhedra.common.util.* 13 | import kotlin.collections.HashMap 14 | import kotlin.collections.set 15 | import kotlin.coroutines.* 16 | 17 | private external val self: DedicatedWorkerGlobalScope 18 | private external var onmessage: (MessageEvent) -> Unit 19 | 20 | fun runWorkerMain(): Boolean { 21 | if (jsTypeOf(document) != "undefined") return false 22 | onmessage = ::onMessageInWorker 23 | return true 24 | } 25 | 26 | private val worker: Worker by lazy { createWorker() } 27 | private val json by lazy { Json { } } 28 | private var lastTaskId = 0L 29 | 30 | @Serializable 31 | private sealed class MessageToWorker { 32 | @Serializable 33 | data class Task>(val id: Long, val task: WorkerTask) : MessageToWorker() { 34 | suspend fun invoke(progress: OperationProgressContext): MessageToMain = 35 | try { 36 | MessageToMain.Result(id, task.invoke(progress)) 37 | } catch (e: Throwable) { 38 | if (e !is CancellationException) e.printStackTrace() 39 | MessageToMain.Failure(id, e.toString()) 40 | } 41 | } 42 | 43 | @Serializable 44 | data class Cancel(val id: Long) : MessageToWorker() 45 | } 46 | 47 | @Serializable 48 | private sealed class MessageToMain { 49 | @Serializable 50 | data class Progress(val id: Long, val done: Int) : MessageToMain() 51 | 52 | @Serializable 53 | data class Result(val id: Long, val result: WorkerResult) : MessageToMain() 54 | 55 | @Serializable 56 | data class Failure(val id: Long, val message: String) : MessageToMain() 57 | } 58 | 59 | @Suppress("UnsafeCastFromDynamic") 60 | @OptIn(ExperimentalSerializationApi::class) 61 | private fun serializeAndPostMessageToWorker(msg: MessageToWorker) { 62 | worker.postMessage(json.encodeToDynamic(MessageToWorker.serializer(), msg)) 63 | } 64 | 65 | @Suppress("UnsafeCastFromDynamic") 66 | @OptIn(ExperimentalSerializationApi::class) 67 | private fun serializeAndPostMessageToMain(msg: MessageToMain) { 68 | self.postMessage(json.encodeToDynamic(MessageToMain.serializer(), msg)) 69 | } 70 | 71 | private class ActiveTaskInMain( 72 | val progress: OperationProgressContext?, 73 | val cont: CancellableContinuation 74 | ) 75 | 76 | private val activeTasksInMain = HashMap() 77 | 78 | suspend fun > performWorkerTask( 79 | task: WorkerTask, 80 | progress: OperationProgressContext? = null 81 | ): T = 82 | suspendCancellableCoroutine { cont -> 83 | val id = ++lastTaskId 84 | @Suppress("UNCHECKED_CAST") 85 | activeTasksInMain[id] = ActiveTaskInMain(progress, cont as CancellableContinuation) 86 | serializeAndPostMessageToWorker(MessageToWorker.Task(id, task)) 87 | cont.invokeOnCancellation { 88 | serializeAndPostMessageToWorker(MessageToWorker.Cancel(id)) 89 | } 90 | } 91 | 92 | private class ActiveTaskInWorker(val id: Long) : OperationProgressContext { 93 | lateinit var job: Job 94 | override fun reportProgress(done: Int) = 95 | serializeAndPostMessageToMain(MessageToMain.Progress(id, done)) 96 | } 97 | 98 | private var activeTaskInWorker: ActiveTaskInWorker? = null 99 | 100 | @OptIn(ExperimentalSerializationApi::class) 101 | private fun onMessageInWorker(e: MessageEvent) { 102 | val msg = json.decodeFromDynamic(MessageToWorker.serializer(), e.data) 103 | when (msg) { 104 | is MessageToWorker.Task<*, *> -> { 105 | activeTaskInWorker?.job?.cancel() // cancel ongoing task (if any) on a new task 106 | val at = ActiveTaskInWorker(msg.id) 107 | activeTaskInWorker = at 108 | at.job = GlobalScope.launch { 109 | val resultMessage = msg.invoke(at) 110 | activeTaskInWorker = null 111 | serializeAndPostMessageToMain(resultMessage) 112 | } 113 | } 114 | is MessageToWorker.Cancel -> { 115 | activeTaskInWorker?.let { at -> 116 | if (at.id == msg.id) { 117 | activeTaskInWorker = null 118 | at.job.cancel() 119 | } 120 | } 121 | } 122 | } 123 | } 124 | 125 | @OptIn(ExperimentalSerializationApi::class) 126 | private fun onMessageInMain(e: MessageEvent) { 127 | val msg = json.decodeFromDynamic(MessageToMain.serializer(), e.data) 128 | when (msg) { 129 | is MessageToMain.Progress -> activeTasksInMain[msg.id]?.progress?.reportProgress(msg.done) 130 | is MessageToMain.Result<*> -> activeTasksInMain.remove(msg.id)?.cont?.resume(msg.result.value) 131 | is MessageToMain.Failure -> activeTasksInMain.remove(msg.id)?.cont?.resumeWithException(Exception(msg.message)) 132 | } 133 | } 134 | 135 | private fun createWorker(): Worker { 136 | val script = 137 | document.body?.firstElementChild as? HTMLScriptElement ?: error("The first element in must be