arkavo-org / arkavo-org/VRMMetalKit
VRMExpressionController: add public getter for preset/custom weights
- Dominant language
- Swift
- Stars
- 6
- Forks
- 2
- Avg merge
- 18h 51m
- Merged PRs (30d)
- 26
Description
## Summary
`VRMExpressionController` exposes `setExpressionWeight(_:weight:)` and `setCustomExpressionWeight(_:weight:)` publicly, but the corresponding read side is missing. The current weights live in `private var currentWeights: [VRMExpressionPreset: Float]` and `private var customCurrentWeights: [String: Float]` (`Sources/VRMMetalKit/Animation/VRMMorphTargets.swift:455-456`) with no public accessor.
## Why this is needed
Downstream tooling that drives expressions through `setExpressionWeight` and then needs to observe what VMK actually stored — including the post-clamp value, and including weights that `AnimationPlayer` writes during VRMA playback — currently has no way to read it back without subclassing or maintaining a duplicate mirror.
Concrete use case: [arkavo-org/vrm-conformance](https://github.com/arkavo-org/vrm-conformance) is wiring a `dump_expression_weights` adapter op so the conformance suite can verify the VRM 1.0 viseme `morphTargetBinds` weight coercion fix shipped in [0.15.2 / PR #272](https://github.com/arkavo-org/VRMMetalKit/pull/272) end-to-end. The verification model is "drive a viseme weight, dump back, compare across renderers" — three-vrm exposes `expressionManager.getValue(name)` for exactly this; VMK's symmetry gap forces an adapter-side mirror that loses fidelity when `AnimationPlayer` writes weights internally during VRMA playback.
## Proposed API
```swift
extension VRMExpressionController {
/// Current weight of a preset expression. Returns 0 when the preset
/// has not been set (matches the `init()` default).
public func weight(for preset: VRMExpressionPreset) -> Float {
currentWeights[preset] ?? 0
}
/// Current weight of a registered custom expression. Returns `nil`
/// when the name has not been registered via
/// `registerCustomExpression(_:name:)`.
public func weight(forCustom name: String) -> Float? {
customCurrentWeights[name]
}
}
```
Two methods, both pure reads, no behavioural change. Matches the existing setter symmetry (`setExpressionWeight` / `setCustomExpressionWeight`).
## Alternative considered
Exposing the dictionaries directly (`public private(set) var currentWeights`) is structurally simpler but commits to the storage shape as API. The accessor form lets `VRMExpressionController` re-implement storage (e.g. switch to an array indexed by `allCases`) without breaking callers.
## Severity
Low. Workaround exists (adapter-side mirror of `setExpressionWeight` calls), but the workaround is unsound when `AnimationPlayer.update(deltaTime:model:)` writes weights as part of VRMA playback — adapter sees neither the call nor the result.
## Test coverage
Single round-trip test in `VRMExpressionControllerTests` (or wherever the controller's existing tests live):
```swift
let c = VRMExpressionController()
c.setExpressionWeight(.aa, weight: 0.7)
XCTAssertEqual(c.weight(for: .aa), 0.7, accuracy: 1e-6)
c.setExpressionWeight(.aa, weight: 1.5) // clamps to 1.0
XCTAssertEqual(c.weight(for: .aa), 1.0, accuracy: 1e-6)
XCTAssertEqual(c.weight(for: .ih), 0.0) // unset → default
```
## Related
- [VRMMetalKit 0.15.2 release](https://github.com/arkavo-org/VRMMetalKit/releases/tag/0.15.2) — viseme weight coercion fix
- [arkavo-org/vrm-conformance#13](https://github.com/arkavo-org/vrm-conformance/issues/13) — adapter-side wiring task that surfaced this gap
Contributor guide
Research direction
Start in Sources/VRMMetalKit/Animation/VRMMorphTargets.swift around VRMExpressionController and its currentWeights and customCurrentWeights storage. Read the existing setter and registration behavior, then add the two read accessors described in the issue. Run the existing VRMExpressionControllerTests, adding the preset round-trip, clamping, and unset cases to verify the stored values.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- computer-graphics
- Issue type
- Feature
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100