Propagation of changes to observables can now be deferred until the end of a code block.

This commit is contained in:
Daan Vanden Bosch 2021-05-30 15:16:58 +02:00
parent 327dfe79bb
commit e5c1c81be3
33 changed files with 521 additions and 182 deletions

View File

@ -0,0 +1,10 @@
package world.phantasmal.observable
/**
* Defer propagation of changes to observables until the end of a code block. All changes to
* observables in a single change set won't be propagated to their dependencies until the change set
* is completed.
*/
fun change(block: () -> Unit) {
ChangeManager.inChangeSet(block)
}

View File

@ -0,0 +1,45 @@
package world.phantasmal.observable
object ChangeManager {
private var currentChangeSet: ChangeSet? = null
fun inChangeSet(block: () -> Unit) {
val existingChangeSet = currentChangeSet
val changeSet = existingChangeSet ?: ChangeSet().also {
currentChangeSet = it
}
try {
block()
} finally {
if (existingChangeSet == null) {
currentChangeSet = null
changeSet.complete()
}
}
}
fun changed(dependency: Dependency) {
val changeSet = currentChangeSet
if (changeSet == null) {
dependency.emitDependencyChanged()
} else {
changeSet.changed(dependency)
}
}
}
private class ChangeSet {
private val changedDependencies: MutableList<Dependency> = mutableListOf()
fun changed(dependency: Dependency) {
changedDependencies.add(dependency)
}
fun complete() {
for (dependency in changedDependencies) {
dependency.emitDependencyChanged()
}
}
}

View File

@ -11,4 +11,9 @@ interface Dependency {
* This method is not meant to be called from typical application code.
*/
fun removeDependent(dependent: Dependent)
/**
* This method is not meant to be called from typical application code.
*/
fun emitDependencyChanged()
}

View File

@ -1,6 +1,11 @@
package world.phantasmal.observable
open class ChangeEvent<out T>(val value: T) {
open class ChangeEvent<out T>(
/**
* The observable's new value
*/
val value: T,
) {
operator fun component1() = value
}

View File

@ -3,16 +3,30 @@ package world.phantasmal.observable
import world.phantasmal.core.disposable.Disposable
class SimpleEmitter<T> : AbstractDependency(), Emitter<T> {
private var event: ChangeEvent<T>? = null
override fun emit(event: ChangeEvent<T>) {
for (dependent in dependents) {
dependent.dependencyMightChange()
}
for (dependent in dependents) {
dependent.dependencyChanged(this, event)
}
this.event = event
ChangeManager.changed(this)
}
override fun observe(observer: Observer<T>): Disposable =
CallbackObserver(this, observer)
override fun emitDependencyChanged() {
if (event != null) {
try {
for (dependent in dependents) {
dependent.dependencyChanged(this, event)
}
} finally {
event = null
}
}
}
}

View File

@ -32,11 +32,13 @@ abstract class AbstractCell<T> : AbstractDependency(), Cell<T> {
}
}
protected fun emitChanged(event: ChangeEvent<T>?) {
protected fun emitDependencyChanged(event: ChangeEvent<*>?) {
if (mightChangeEmitted) {
mightChangeEmitted = false
for (dependent in dependents) {
dependent.dependencyChanged(this, event)
}
}
}
}

View File

@ -24,10 +24,16 @@ abstract class AbstractDependentCell<T> : AbstractCell<T>(), Dependent {
dependenciesChanged()
} else {
emitChanged(null)
emitDependencyChanged(null)
}
}
}
abstract fun dependenciesChanged()
override fun emitDependencyChanged() {
// Nothing to do because dependent cells emit dependencyChanged immediately. They don't
// defer this operation because they only change when there is no transaction or the current
// transaction is being committed.
}
protected abstract fun dependenciesChanged()
}

View File

@ -1,6 +1,7 @@
package world.phantasmal.observable.cell
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.ChangeManager
class DelegatingCell<T>(
private val getter: () -> T,
@ -16,7 +17,11 @@ class DelegatingCell<T>(
setter(value)
emitChanged(ChangeEvent(value))
ChangeManager.changed(this)
}
}
override fun emitDependencyChanged() {
emitDependencyChanged(ChangeEvent(value))
}
}

View File

@ -50,9 +50,9 @@ class DependentCell<T>(
if (newValue != _value) {
_value = newValue
emitChanged(ChangeEvent(newValue))
emitDependencyChanged(ChangeEvent(newValue))
} else {
emitChanged(null)
emitDependencyChanged(null)
}
}
}

View File

@ -82,9 +82,9 @@ class FlatteningDependentCell<T>(
if (newValue != _value) {
_value = newValue
emitChanged(ChangeEvent(newValue))
emitDependencyChanged(ChangeEvent(newValue))
} else {
emitChanged(null)
emitDependencyChanged(null)
}
}
}

View File

@ -1,6 +1,7 @@
package world.phantasmal.observable.cell
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.ChangeManager
class SimpleCell<T>(value: T) : AbstractCell<T>(), MutableCell<T> {
override var value: T = value
@ -10,7 +11,11 @@ class SimpleCell<T>(value: T) : AbstractCell<T>(), MutableCell<T> {
field = value
emitChanged(ChangeEvent(value))
ChangeManager.changed(this)
}
}
override fun emitDependencyChanged() {
emitDependencyChanged(ChangeEvent(value))
}
}

View File

@ -16,4 +16,8 @@ class StaticCell<T>(override val value: T) : AbstractDependency(), Cell<T> {
}
override fun observe(observer: Observer<T>): Disposable = nopDisposable()
override fun emitDependencyChanged() {
error("StaticCell can't change.")
}
}

View File

@ -57,7 +57,7 @@ abstract class AbstractDependentListCell<E> :
computeElements()
emitChanged(
emitDependencyChanged(
ListChangeEvent(elements, listOf(ListChange.Structural(0, oldElements, elements)))
)
}

View File

@ -179,15 +179,21 @@ class FilteredListCell<E>(
}
if (filteredChanges.isEmpty()) {
emitChanged(null)
emitDependencyChanged(null)
} else {
emitChanged(ListChangeEvent(elements, filteredChanges))
emitDependencyChanged(ListChangeEvent(elements, filteredChanges))
}
} else {
emitChanged(null)
emitDependencyChanged(null)
}
}
override fun emitDependencyChanged() {
// Nothing to do because FilteredListCell emits dependencyChanged immediately. We don't
// defer this operation because FilteredListCell only changes when there is no transaction
// or the current transaction is being committed.
}
private fun recompute() {
val newElements = mutableListOf<E>()
indexMap.clear()

View File

@ -5,6 +5,7 @@ import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Dependency
import world.phantasmal.observable.Dependent
import world.phantasmal.observable.ChangeManager
typealias DependenciesExtractor<E> = (element: E) -> Array<Dependency>
@ -27,7 +28,7 @@ class SimpleListCell<E>(
*/
private val elementDependents = mutableListOf<ElementDependent>()
private var changingElements = 0
private var elementListChanges = mutableListOf<ListChange.Element<E>>()
private var changes = mutableListOf<ListChange<E>>()
override var value: List<E>
get() = elements
@ -50,12 +51,8 @@ class SimpleListCell<E>(
elementDependents[index] = ElementDependent(index, element)
}
emitChanged(
ListChangeEvent(
elements,
listOf(ListChange.Structural(index, listOf(removed), listOf(element))),
),
)
changes.add(ListChange.Structural(index, listOf(removed), listOf(element)))
ChangeManager.changed(this)
return removed
}
@ -180,6 +177,14 @@ class SimpleListCell<E>(
}
}
override fun emitDependencyChanged() {
try {
emitDependencyChanged(ListChangeEvent(elements, changes))
} finally {
changes = mutableListOf()
}
}
private fun checkIndex(index: Int, maxIndex: Int) {
if (index !in 0..maxIndex) {
throw IndexOutOfBoundsException(
@ -206,12 +211,8 @@ class SimpleListCell<E>(
}
}
emitChanged(
ListChangeEvent(
elements,
listOf(ListChange.Structural(index, removed, inserted)),
),
)
changes.add(ListChange.Structural(index, removed, inserted))
ChangeManager.changed(this)
}
private inner class ElementDependent(
@ -249,19 +250,11 @@ class SimpleListCell<E>(
if (--changingDependencies == 0) {
if (dependenciesActuallyChanged) {
dependenciesActuallyChanged = false
elementListChanges.add(ListChange.Element(index, element))
changes.add(ListChange.Element(index, element))
}
if (--changingElements == 0) {
try {
if (elementListChanges.isNotEmpty()) {
emitChanged(ListChangeEvent(value, elementListChanges))
} else {
emitChanged(null)
}
} finally {
elementListChanges = mutableListOf()
}
ChangeManager.changed(this@SimpleListCell)
}
}
}

View File

@ -45,4 +45,8 @@ class StaticListCell<E>(private val elements: List<E>) : AbstractDependency(), L
return unsafeAssertNotNull(firstOrNull)
}
override fun emitDependencyChanged() {
error("StaticListCell can't change.")
}
}

View File

@ -36,6 +36,11 @@ interface CellWithDependenciesTests : CellTests {
val publicDependents: List<Dependent> = dependents
override val value: Int = 5
override fun emitDependencyChanged() {
// Not going to change.
throw NotImplementedError()
}
}
val cell = p.createWithDependencies(dependency)

View File

@ -0,0 +1,37 @@
package world.phantasmal.observable.cell
import world.phantasmal.observable.change
import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
class ChangeTests : ObservableTestSuite {
@Test
fun exceptions_during_a_change_set_are_allowed() = test {
val dependency = mutableCell(7)
val dependent = dependency.map { 2 * it }
var dependentObservedValue: Int? = null
disposer.add(dependent.observe { dependentObservedValue = it.value })
assertFails {
change {
dependency.value = 11
throw Exception()
}
}
// The change to dependency is still propagated because it happened before the exception.
assertEquals(22, dependentObservedValue)
assertEquals(22, dependent.value)
// The machinery behind change is still in a valid state.
change {
dependency.value = 13
}
assertEquals(26, dependentObservedValue)
assertEquals(26, dependent.value)
}
}

View File

@ -1,5 +1,9 @@
package world.phantasmal.observable.cell
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Dependency
import world.phantasmal.observable.Dependent
import world.phantasmal.observable.change
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
@ -25,12 +29,121 @@ interface MutableCellTests<T : Any> : CellTests {
assertEquals(newValue, observedValue)
}
/**
* Modifying mutable cells in a change set doesn't result in calls to
* [Dependent.dependencyChanged] of their dependents until the change set is completed.
*/
@Test
fun cell_changes_in_change_set_dont_immediately_produce_dependencyChanged_calls() = test {
val dependencies = (1..5).map { createProvider() }
var dependencyMightChangeCount = 0
var dependencyChangedCount = 0
val dependent = object : Dependent {
override fun dependencyMightChange() {
dependencyMightChangeCount++
}
override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
dependencyChangedCount++
}
}
for (dependency in dependencies) {
dependency.observable.addDependent(dependent)
}
change {
for (dependency in dependencies) {
dependency.observable.value = dependency.createValue()
}
// Calls to dependencyMightChange happen immediately.
assertEquals(dependencies.size, dependencyMightChangeCount)
// Calls to dependencyChanged happen later.
assertEquals(0, dependencyChangedCount)
}
assertEquals(dependencies.size, dependencyMightChangeCount)
assertEquals(dependencies.size, dependencyChangedCount)
}
/**
* Modifying a mutable cell multiple times in one change set results in a single call to
* [Dependent.dependencyMightChange] and [Dependent.dependencyChanged].
*/
@Test
fun multiple_changes_to_one_cell_in_change_set() = test {
val dependency = createProvider()
var dependencyMightChangeCount = 0
var dependencyChangedCount = 0
val dependent = object : Dependent {
override fun dependencyMightChange() {
dependencyMightChangeCount++
}
override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
dependencyChangedCount++
}
}
dependency.observable.addDependent(dependent)
// Change the dependency multiple times in a transaction.
change {
repeat(5) {
dependency.observable.value = dependency.createValue()
}
// Calls to dependencyMightChange happen immediately.
assertEquals(1, dependencyMightChangeCount)
// Calls to dependencyChanged happen later.
assertEquals(0, dependencyChangedCount)
}
assertEquals(1, dependencyMightChangeCount)
assertEquals(1, dependencyChangedCount)
}
/**
* Modifying two mutable cells in a change set results in a single recomputation of their
* dependent.
*/
@Test
fun modifying_two_cells_together_results_in_one_recomputation() = test {
val dependency1 = createProvider()
val dependency2 = createProvider()
var computeCount = 0
val dependent = DependentCell(dependency1.observable, dependency2.observable) {
computeCount++
Unit
}
// Observe dependent to ensure it gets recomputed when its dependencies change.
disposer.add(dependent.observe {})
// DependentCell's compute function is called once when we start observing.
assertEquals(1, computeCount)
change {
dependency1.observable.value = dependency1.createValue()
dependency2.observable.value = dependency2.createValue()
}
assertEquals(2, computeCount)
}
interface Provider<T : Any> : CellTests.Provider {
override val observable: MutableCell<T>
/**
* Returns a value that can be assigned to [observable] and that's different from
* [observable]'s current value.
* [observable]'s current and all previous values.
*/
fun createValue(): T
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.actions
import world.phantasmal.observable.change
import world.phantasmal.web.core.actions.Action
import world.phantasmal.web.questEditor.models.QuestEntityModel
import world.phantasmal.web.questEditor.models.QuestModel
@ -12,9 +13,11 @@ class CreateEntityAction(
override val description: String = "Add ${entity.type.name}"
override fun execute() {
change {
quest.addEntity(entity)
setSelectedEntity(entity)
}
}
override fun undo() {
quest.removeEntity(entity)

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.actions
import world.phantasmal.observable.change
import world.phantasmal.web.core.actions.Action
import world.phantasmal.web.questEditor.models.QuestEventModel
import world.phantasmal.web.questEditor.models.QuestModel
@ -13,12 +14,16 @@ class CreateEventAction(
override val description: String = "Add event ${event.id.value}"
override fun execute() {
change {
quest.addEvent(index, event)
setSelectedEvent(event)
}
}
override fun undo() {
change {
setSelectedEvent(null)
quest.removeEvent(event)
}
}
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.actions
import world.phantasmal.observable.change
import world.phantasmal.web.core.actions.Action
import world.phantasmal.web.questEditor.models.QuestEventActionModel
import world.phantasmal.web.questEditor.models.QuestEventModel
@ -16,12 +17,16 @@ class CreateEventActionAction(
"Add ${action.shortName} action to event ${event.id.value}"
override fun execute() {
change {
event.addAction(action)
setSelectedEvent(event)
}
}
override fun undo() {
change {
event.removeAction(action)
setSelectedEvent(event)
}
}
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.actions
import world.phantasmal.observable.change
import world.phantasmal.web.core.actions.Action
import world.phantasmal.web.questEditor.models.QuestEntityModel
import world.phantasmal.web.questEditor.models.QuestModel
@ -8,7 +9,7 @@ class DeleteEntityAction(
private val setSelectedEntity: (QuestEntityModel<*, *>) -> Unit,
private val quest: QuestModel,
private val entity: QuestEntityModel<*, *>,
) :Action{
) : Action {
override val description: String = "Delete ${entity.type.name}"
override fun execute() {
@ -16,7 +17,9 @@ class DeleteEntityAction(
}
override fun undo() {
change {
quest.addEntity(entity)
setSelectedEntity(entity)
}
}
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.actions
import world.phantasmal.observable.change
import world.phantasmal.web.core.actions.Action
import world.phantasmal.web.questEditor.models.QuestEventModel
import world.phantasmal.web.questEditor.models.QuestModel
@ -13,12 +14,16 @@ class DeleteEventAction(
override val description: String = "Delete event ${event.id.value}"
override fun execute() {
change {
setSelectedEvent(null)
quest.removeEvent(event)
}
}
override fun undo() {
change {
quest.addEvent(index, event)
setSelectedEvent(event)
}
}
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.actions
import world.phantasmal.observable.change
import world.phantasmal.web.core.actions.Action
import world.phantasmal.web.questEditor.models.QuestEventActionModel
import world.phantasmal.web.questEditor.models.QuestEventModel
@ -17,12 +18,16 @@ class DeleteEventActionAction(
"Remove ${action.shortName} action from event ${event.id.value}"
override fun execute() {
change {
setSelectedEvent(event)
event.removeAction(action)
}
}
override fun undo() {
change {
setSelectedEvent(event)
event.addAction(index, action)
}
}
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.actions
import world.phantasmal.observable.change
import world.phantasmal.web.core.actions.Action
import world.phantasmal.web.questEditor.models.QuestEntityModel
import world.phantasmal.web.questEditor.models.QuestEntityPropModel
@ -14,12 +15,16 @@ class EditEntityPropAction(
override val description: String = "Edit ${entity.type.simpleName} ${prop.name}"
override fun execute() {
change {
setSelectedEntity(entity)
prop.setValue(newValue)
}
}
override fun undo() {
change {
setSelectedEntity(entity)
prop.setValue(oldValue)
}
}
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.actions
import world.phantasmal.observable.change
import world.phantasmal.web.core.actions.Action
import world.phantasmal.web.questEditor.models.QuestEventModel
@ -12,12 +13,16 @@ class EditEventPropertyAction<T>(
private val oldValue: T,
) : Action {
override fun execute() {
change {
setSelectedEvent(event)
setter(newValue)
}
}
override fun undo() {
change {
setSelectedEvent(event)
setter(oldValue)
}
}
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.actions
import world.phantasmal.observable.change
import world.phantasmal.web.core.actions.Action
import world.phantasmal.web.externals.three.Euler
import world.phantasmal.web.questEditor.models.QuestEntityModel
@ -14,6 +15,7 @@ class RotateEntityAction(
override val description: String = "Rotate ${entity.type.simpleName}"
override fun execute() {
change {
setSelectedEntity(entity)
if (world) {
@ -22,8 +24,10 @@ class RotateEntityAction(
entity.setRotation(newRotation)
}
}
}
override fun undo() {
change {
setSelectedEntity(entity)
if (world) {
@ -32,4 +36,5 @@ class RotateEntityAction(
entity.setRotation(oldRotation)
}
}
}
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.actions
import world.phantasmal.observable.change
import world.phantasmal.web.core.actions.Action
import world.phantasmal.web.externals.three.Vector3
import world.phantasmal.web.questEditor.models.QuestEntityModel
@ -16,18 +17,22 @@ class TranslateEntityAction(
override val description: String = "Move ${entity.type.simpleName}"
override fun execute() {
change {
setSelectedEntity(entity)
newSection?.let(setEntitySection)
entity.setPosition(newPosition)
}
}
override fun undo() {
change {
setSelectedEntity(entity)
oldSection?.let(setEntitySection)
entity.setPosition(oldPosition)
}
}
}

View File

@ -7,6 +7,7 @@ import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.list.ListCell
import world.phantasmal.observable.cell.list.listCell
import world.phantasmal.observable.cell.mutableCell
import world.phantasmal.observable.change
import world.phantasmal.web.core.minus
import world.phantasmal.web.core.rendering.conversion.vec3ToEuler
import world.phantasmal.web.core.rendering.conversion.vec3ToThree
@ -60,6 +61,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
})
open fun setSectionId(sectionId: Int) {
change {
entity.sectionId = sectionId.toShort()
_sectionId.value = sectionId
@ -67,6 +69,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
_section.value = null
}
}
}
fun setSectionInitialized() {
_sectionInitialized.value = true
@ -81,6 +84,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
"Quest entities can't be moved across areas."
}
change {
entity.sectionId = section.id.toShort()
_sectionId.value = section.id
@ -100,8 +104,10 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
setSectionInitialized()
}
}
fun setPosition(pos: Vector3) {
change {
entity.setPosition(pos.x.toFloat(), pos.y.toFloat(), pos.z.toFloat())
_position.value = pos
@ -112,8 +118,10 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
if (section == null) pos
else pos.clone().applyEuler(section.rotation).add(section.position)
}
}
fun setWorldPosition(pos: Vector3) {
change {
val section = section.value
val relPos =
@ -125,8 +133,10 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
_worldPosition.value = pos
_position.value = relPos
}
}
fun setRotation(rot: Euler) {
change {
floorModEuler(rot)
entity.setRotation(rot.x.toFloat(), rot.y.toFloat(), rot.z.toFloat())
@ -143,8 +153,10 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
_worldRotation.value = floorModEuler(q1.toEuler())
}
}
}
fun setWorldRotation(rot: Euler) {
change {
floorModEuler(rot)
val section = section.value
@ -163,6 +175,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
_worldRotation.value = rot
_rotation.value = relRot
}
}
companion object {
// These quaternions are used as temporary variables to avoid memory allocation.

View File

@ -15,6 +15,7 @@ import world.phantasmal.lib.fileFormats.parseAreaCollisionGeometry
import world.phantasmal.lib.fileFormats.parseAreaRenderGeometry
import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.mutableCell
import world.phantasmal.observable.change
import world.phantasmal.web.core.files.cursor
import world.phantasmal.web.viewer.stores.NinjaGeometry
import world.phantasmal.web.viewer.stores.ViewerStore
@ -75,11 +76,11 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
val result = PwResult.build<Unit>(logger)
var success = false
try {
var ninjaGeometry: NinjaGeometry? = null
var textures: List<XvrTexture>? = null
var ninjaMotion: NjMotion? = null
try {
for (file in files) {
val extension = file.extension()?.toLowerCase()
@ -92,7 +93,8 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
fileResult = njResult
if (njResult is Success) {
ninjaGeometry = njResult.value.firstOrNull()?.let(NinjaGeometry::Object)
ninjaGeometry =
njResult.value.firstOrNull()?.let(NinjaGeometry::Object)
}
}
@ -101,7 +103,8 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
fileResult = xjResult
if (xjResult is Success) {
ninjaGeometry = xjResult.value.firstOrNull()?.let(NinjaGeometry::Object)
ninjaGeometry =
xjResult.value.firstOrNull()?.let(NinjaGeometry::Object)
}
}
@ -157,16 +160,18 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
success = true
}
}
ninjaGeometry?.let(store::setCurrentNinjaGeometry)
textures?.let(store::setCurrentTextures)
ninjaMotion?.let(store::setCurrentNinjaMotion)
} catch (e: Exception) {
result.addProblem(Severity.Error, "Couldn't parse files.", cause = e)
}
change {
ninjaGeometry?.let(store::setCurrentNinjaGeometry)
textures?.let(store::setCurrentTextures)
ninjaMotion?.let(store::setCurrentNinjaMotion)
setResult(if (success) result.success(Unit) else result.failure())
}
}
fun dismissResultDialog() {
_resultDialogVisible.value = false

View File

@ -14,6 +14,7 @@ import world.phantasmal.observable.cell.and
import world.phantasmal.observable.cell.list.ListCell
import world.phantasmal.observable.cell.list.mutableListCell
import world.phantasmal.observable.cell.mutableCell
import world.phantasmal.observable.change
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.rendering.conversion.PSO_FRAME_RATE
import world.phantasmal.web.core.stores.UiStore
@ -163,6 +164,7 @@ class ViewerStore(
}
fun setCurrentNinjaGeometry(geometry: NinjaGeometry?) {
change {
if (_currentCharacterClass.value != null) {
_currentCharacterClass.value = null
_currentTextures.clear()
@ -172,6 +174,7 @@ class ViewerStore(
_currentNinjaMotion.value = null
_currentNinjaGeometry.value = geometry
}
}
fun setCurrentTextures(textures: List<XvrTexture>) {
_currentTextures.replaceAll(textures)
@ -200,9 +203,11 @@ class ViewerStore(
}
fun setCurrentNinjaMotion(njm: NjMotion) {
change {
_currentNinjaMotion.value = njm
_animationPlaying.value = true
}
}
suspend fun setCurrentAnimation(animation: AnimationModel?) {
_currentAnimation.value = animation
@ -244,13 +249,13 @@ class ViewerStore(
val char = currentCharacterClass.value
?: return
try {
val sectionId = currentSectionId.value
val body = currentBody.value
try {
val ninjaObject = characterClassAssetLoader.loadNinjaObject(char)
val textures = characterClassAssetLoader.loadXvrTextures(char, sectionId, body)
change {
if (clearAnimation) {
_currentAnimation.value = null
_currentNinjaMotion.value = null
@ -258,20 +263,27 @@ class ViewerStore(
_currentNinjaGeometry.value = NinjaGeometry.Object(ninjaObject)
_currentTextures.replaceAll(textures)
}
} catch (e: Exception) {
logger.error(e) { "Couldn't load Ninja model for $char." }
change {
_currentAnimation.value = null
_currentNinjaMotion.value = null
_currentNinjaGeometry.value = null
_currentTextures.clear()
}
}
}
private suspend fun loadAnimation(animation: AnimationModel) {
try {
_currentNinjaMotion.value = animationAssetLoader.loadAnimation(animation.filePath)
val ninjaMotion = animationAssetLoader.loadAnimation(animation.filePath)
change {
_currentNinjaMotion.value = ninjaMotion
_animationPlaying.value = true
}
} catch (e: Exception) {
logger.error(e) {
"Couldn't load Ninja motion for ${animation.name} (path: ${animation.filePath})."

View File

@ -63,6 +63,10 @@ class HTMLElementSizeCell(element: HTMLElement? = null) : AbstractCell<Size>() {
}
}
override fun emitDependencyChanged() {
error("HTMLElementSizeCell emits dependencyChanged immediately.")
}
private fun getSize(): Size =
element
?.let { Size(it.offsetWidth.toDouble(), it.offsetHeight.toDouble()) }
@ -78,7 +82,7 @@ class HTMLElementSizeCell(element: HTMLElement? = null) : AbstractCell<Size>() {
if (newValue != _value) {
emitMightChange()
_value = newValue
emitChanged(ChangeEvent(newValue))
emitDependencyChanged(ChangeEvent(newValue))
}
}
}