Skip to content

Fix/shaderlab#2983

Open
luzhuang wants to merge 94 commits into
dev/2.0from
fix/shaderlab
Open

Fix/shaderlab#2983
luzhuang wants to merge 94 commits into
dev/2.0from
fix/shaderlab

Conversation

@luzhuang
Copy link
Copy Markdown
Contributor

@luzhuang luzhuang commented May 9, 2026

Please check if the PR fulfills these requirements

  • The commit message follows our guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)

What is the current behavior? (You can also link to an open issue here)

What is the new behavior (if this is a feature change)?

Does this PR introduce a breaking change? (What changes might users need to make in their application due to this PR?)

Other information:

zhuxudong and others added 30 commits March 26, 2026 16:18
…tin functions

texture(sampler2D, vec2) returns GVec4 which was incorrectly resolved to
the sampler type instead of vec4, causing "No overload function type found"
when passing the result to user-defined functions like decode32(vec4).

Add resolveGenericReturnType() to correctly map GSampler* → GVec4:
  sampler2D/sampler3D/samplerCube → vec4
  isampler2D/isampler3D/...       → ivec4
  usampler2D/usampler3D/...       → uvec4
…exture2DLod signatures

- Simplify resolveGenericReturnType: remove genericParamType param, only
  check if return type is GVec4
- Fix textureCube/textureCubeLod return type: SAMPLER_CUBE → VEC4
- Add missing texture2DLod builtin function registration
- Add texture2DLod test cases to texture-generic.shader
* feat: implement HorizontalBillboard render mode
… type

When a builtin generic function (e.g. normalize) receives TypeAny args,
resolvedReturnType stays TypeAny. Previously the else branch returned
the raw EGenType enum value (200), which is neither a concrete type nor
a wildcard, causing downstream user-function overload matching to fail.
…tin functions

texture(sampler2D, vec2) returns GVec4 which was incorrectly resolved to
the sampler type instead of vec4, causing "No overload function type found"
when passing the result to user-defined functions like decode32(vec4).

Add resolveGenericReturnType() to correctly map GSampler* → GVec4:
  sampler2D/sampler3D/samplerCube → vec4
  isampler2D/isampler3D/...       → ivec4
  usampler2D/usampler3D/...       → uvec4
…exture2DLod signatures

- Simplify resolveGenericReturnType: remove genericParamType param, only
  check if return type is GVec4
- Fix textureCube/textureCubeLod return type: SAMPLER_CUBE → VEC4
- Add missing texture2DLod builtin function registration
- Add texture2DLod test cases to texture-generic.shader
… type

When a builtin generic function (e.g. normalize) receives TypeAny args,
resolvedReturnType stays TypeAny. Previously the else branch returned
the raw EGenType enum value (200), which is neither a concrete type nor
a wildcard, causing downstream user-function overload matching to fail.
…ing CodeGen

When #define values contain struct member access like `o.v_uv` (where `o` is
a Varyings/Attributes/MRT struct variable), the CodeGen now correctly transforms
them to just the property name (e.g. `v_uv`), matching the behavior of direct
struct member access in regular code. Closes #2944.
…uct-access

Verify the actual CodeGen output instead of just checking GLSL compilation:
- #define values with struct member access are correctly transformed
- varying/attribute declarations are emitted for referenced properties
…ions

Add assertions for macro usage in expressions (not just #define transformation):
- Macro as RHS in multiplication, as LHS in assignment
- Macro as function argument in dot(), texture2D()
- Multiple varying properties (v_uv, v_normal) referenced via #define
…ransform

Build a combined _globalStructVarMap in visitShaderProgram by scanning
both vertex and fragment entry functions, so global #define values like
`attr.POSITION` or `o.v_uv` are correctly transformed in all stages.

Rewrite define-struct-access tests to use snapshot file comparison
against expected/ GLSL outputs for clearer verification.
…cro scanning

- Suppress `uniform` output for global struct-typed variables (e.g. `Varyings o;`)
- Register global struct vars in both per-function and cross-stage maps
- Unify macro member access scanning into callback-based _forEachMacroMemberAccess
- Add registerStructVar() encapsulation in VisitorContext
- Add Cocos VSOutput pattern test (global-varying-var)
…version

GLES100 visitJumpStatement converted `return expr;` to `gl_FragColor = expr`
without a trailing semicolon, causing WebGL compilation errors. Only triggered
when fragment entry returns vec4 (Cocos pattern), not void (standard Galacean).
…eprocessor

#if !0 and similar expressions now work correctly, matching C/GLSL preprocessor behavior.
…atrix

The viewMatrix getter intentionally ignores the camera entity's world scale
(using Matrix.rotationTranslation), but _getInvViewProjMat() was using the
full entity.transform.worldMatrix which includes inherited scale. This
inconsistency causes screenPointToRay to produce incorrect world-space rays
when the camera inherits scale from a parent entity (e.g. UICanvas in
ScreenSpaceCamera mode with camera as a child of canvas).

Closes #2748

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify that screenPointToRay and viewport-world round-trip produce
correct results when the camera inherits non-identity scale from a
parent entity. Without the fix, the round-trip deviates by the
inherited scale factor (e.g. 105 -> 107.5 at scale 1.5).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ReflectionParser 解析 IComponentRef(entityPath + componentType)时,
目标组件可能在 GLB clone 的子 entity 上而非 clone 根 entity 自身。
getComponents 找不到时 fallback 到 getComponentsIncludeChildren。
CloneManager: 当 source 和 target 属性是同类型 Object 实例(如 Vector4)时,
自动升级为 DeepClone,避免 prefab 模板的引用覆盖克隆体的独立实例。
新增 Map/Set 类型的 deep clone 支持。

ModelMesh: throw string 改为 throw Error 以获得正确堆栈。
AnimatorState.speed is part of the shared AnimatorController asset.
Modifying it at runtime pollutes all Animator instances sharing the
same controller, causing animation speed corruption after cloning.

- Add speed field to AnimatorStatePlayData, initialized from AnimatorState.speed on reset
- Add proxy properties (name/clip/wrapMode/transitions/addStateMachineScript)
- Change speed calculation to playData.speed * animator.speed
- findAnimatorState now returns per-instance AnimatorStatePlayData
- Export AnimatorStatePlayData for consumer code
When sizeMode is set to Automatic, the UITransform size is automatically
synchronized to the sprite's natural dimensions when the sprite changes.
This matches Cocos Creator's Sprite.SizeMode.TRIMMED behavior.

- Add SpriteSizeMode enum (Custom / Automatic)
- Add sizeMode property to Image with getter/setter
- Sync UITransform.size in set sprite and _onSpriteChange
- Export SpriteSizeMode from component index
GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

luzhuang added 3 commits May 14, 2026 20:05
Four independent bugs all share the same root pattern — "JS field is the source
of truth, native handle synced via setter side-effects" — fails whenever a code
path bypasses the setter:

- **R0** kinematic + CCD warning: setRigidBodyFlag(eENABLE_CCD,true) on a
  kinematic actor prints a PhysX warning and the flag becomes undefined when
  switching back to dynamic. Cache `_isKinematic` and `_collisionDetectionMode`;
  apply CCD flags only in dynamic state; on kinematic→dynamic transition,
  reapply the cached mode.

- **R5** addForce/addTorque silently ignored on sleeping actors: PhysX wasm
  wrapper omits the `autowake` parameter (C++ default true, wasm default false).
  Explicitly `wakeUp()` before adding force/torque; guard kinematic to avoid
  triggering a separate PhysX warning.

- **R6** MeshColliderShape async cook & prefab clone: `_cookMesh` can fail
  transiently when mesh data is not yet accessible; the cloned shape's
  `_nativeShape` is `@ignoreClone` so prefab instantiation produces a useless
  shape. Add `_pendingNativeShapeCreation` flag; retry every physics tick via
  new `ColliderShape._onPhysicsUpdate` hook (driven by `Collider._onUpdate`);
  override `_cloneTo` to cook a fresh native shape from already-cloned buffers.

- **R8** PhysicsMaterial clone bypasses setters: CloneManager deep-copies
  `_bounciness/_friction/_combine` fields directly without invoking setters,
  so the target's freshly-constructed PxMaterial keeps its default values
  while JS fields show source values. Result: PhysX uses default `b=0`
  while logs and `material.bounciness` read source values — fully invisible
  divergence. Mark `_nativeMaterial` `@ignoreClone`; add `_cloneTo` →
  `_syncNative()` that re-writes all 5 fields to native via setter API.

  Side fix: PhysicsMaterial constructor was passing `bounceCombine` and
  `frictionCombine` to `createPhysicsMaterial` in reverse order — the bug was
  hidden because R8 kept native stuck at default Average (both reversed
  arguments are 0) and users only ever read JS fields. Fixed in the same patch.

Test: `PhysicsMaterial.test.ts > cloned collider shape material keeps native
values` verifies a cloned dynamic body bounces off a wall by simulating
40 frames and asserting vx flips negative — fails without the R8 fix.
Add Unity-style applyForceAtPosition(force, worldPosition) to apply a force
at an arbitrary world-space point, producing both linear acceleration through
the center of mass and angular acceleration about it.

Internally decomposes into the textbook (F, τ = r × F) pair:
  worldCoM = entity.worldPos + entity.worldRot · localCoM (from native)
  r = worldPosition - worldCoM
  addForce(F); addTorque(r × F)

Equivalent to Cocos `RigidBody.applyForce(force, relativePoint)` (Bullet's
implementation is the same cross product); enables direct migration of
billiards-style "hit at an offset point" gameplay without manual r × F math.

Pure TS-layer composition over existing addForce/addTorque — no design
interface change, no native binding change (physX.js untouched).

Tests (6 cases, orthogonal coverage of all worldCoM computation terms):
1. force at CoM → only linear acceleration
2. r ≠ 0 produces torque = r × F, validated via differential vs applyTorque(τ)
3. CoM offset + force at world CoM → r = 0 → no torque
4. Entity rotated 90° + CoM offset → worldRot correctly transforms localCoM
5. (position + rotation + CoM offset + r ≠ 0) — full stress test catching any
   missing term in worldCoM = pos + rot·localCoM
6. After entity.clone() — defends prefab instantiation path (R6/R8 territory)
… root cause

Previous commit 273d181 claimed "PhysX wasm wrapper's addForce doesn't pass
the autowake parameter, so calls on sleeping actors are silently ignored". This
claim is incorrect — the wasm binding explicitly passes autowake=true:

  body.addForce(force, PxForceMode::eFORCE, true);

PhysX 4.1.1 doc confirms: "If true, this call wakes up the actor if it is
sleeping." Two new tests verify autowake works as documented in wasm:

- "applyForce on sleeping actor must wake up and apply force"
  → sleep() → applyForce (no explicit wakeUp) → force is applied, actor wakes
- "applyForce after kinematic→dynamic switch (mimic billiards game break flow)"
  → reproduces the exact game scenario where 'force seemed lost' originally

Both pass without the explicit wakeUp() call. The original 'applyForce lost'
symptom in the billiards game was actually caused by other concurrent bugs
(R1 collisionMatrix, R8 PhysicsMaterial, fixedTimeStep mismatch) that were
each masking the real cause, not by sleeping-actor behavior.

Keep the `_isKinematic` guard: PhysX makes addForce a no-op on kinematic
actors (documented), but the guard avoids the wasm boundary cross.
GuoLei1990

This comment was marked as outdated.

luzhuang added 2 commits May 14, 2026 20:54
…ysX scene queries)

These tests were red on dev/2.0 before any of the recent physics work landed.
Diagnosed and fixed at the root, no test-skipping or assertion weakening.

**LitePhysicsMaterial — 1 failure**

`ColliderShape Lite > clone` broke after R8 added `PhysicsMaterial._cloneTo →
_syncNative()` which now calls every native setter on a freshly-cloned material.
Lite's setters threw "Physics-lite don't support physics material", violating
the no-op-with-log convention already used by `LiteColliderShape.setMaterial /
setIsTrigger / setContactOffset`. Change all 5 setters to silent no-op + a
one-time `console.log` warning, matching the existing convention.

**PhysXPhysicsScene — 7 failures (raycast/boxCast/sphereCast/capsuleCast +
overlapBoxAll/overlapSphereAll/overlapCapsuleAll)**

Two underlying problems both surface in the JS filter-callback layer:

1. `_pxFilterData` had `POST_FILTER` flag enabled but `_overlapMultiple` reused
   it with a callback object that only defined `preFilter`. When PhysX tried to
   invoke `postFilter`, the wasm boundary errored with
   `Emval.toValue(...)[getStringOrSymbol(...)] is not a function`.

2. The 4 cast tests asserted the old "origin-inside-collider returns true with
   distance=0" behavior. The wasm side was already switched to the Unity-style
   "skip initial overlap" design (commit bdf5490 / PR #2998 on dev branch
   ahead of us), but the tests still encoded the rejected behavior.

Mirror PR #2998's JS-side fix already merged on dev:

- Split filter data into `_pxFilterData` (PRE only, for overlap) and
  `_pxRaycastSweepFilterData` (PRE + POST, for raycast/sweep).
- Replace per-call `PxQueryFilterCallback.implement` with a single persistent
  `_pxQueryCallback` defining both `preFilter` and `postFilter`. User callbacks
  dispatch via a `_currentOnQuery` slot saved/restored around each native call
  for reentrancy safety.
- Cache the `PxSweepHit` instance instead of creating per-call.
- Add `QueryHitType` enum.

Update 4 stale test assertions in PhysicsScene.test.ts to match the
already-shipped "skip initial overlap" wasm contract. Use strictly-inside origin
(2.9,2.9,2.9) for the raycast case to avoid the corner-tolerance flake that PR
\#2998 already addressed.

Verification: 11 physics test files, 213/213 passing locally.
…lone

After fixing R0/R5/R6/R8 in commits 273d181 and 9c32650, run a RED-GREEN
verification on each: temporarily revert the fix, run the test, confirm it
fails; restore the fix, confirm it passes. Outcome reshaped what we thought
the root causes were.

R6 (MeshColliderShape clone) — real bug fix, RED verified

`R6: cloned MeshColliderShape rebuilds its native PhysX shape` —
With `_cloneTo` override removed, `clonedShape._nativeShape` is null and the
cloned ground entity is not physically present (sphere falls through). With
the fix, sphere lands on the cloned ground (sphereY > -1).

R0 (kinematic + CCD) — defensive, not a bug fix

`R0: CCD mode survives kinematic toggle (PhysX rejects CCD on kinematic)` and
`R0: setCollisionDetectionMode in kinematic state defers application` —
Both pass even with the R0 fix reverted. PhysX 4.1.1 already manages the
CCD↔kinematic flag relation correctly on its own. R0's real value is:
  1. Suppressing the PhysX warning that fires when CCD-on actors switch to
     kinematic (or vice-versa).
  2. Preserving user intent — `collisionDetectionMode` getter returns the
     last user-set mode even while temporarily kinematic; on dynamic restore,
     CCD is reapplied without the user having to remember.

These tests are kept as contract tests, not red-green regression tests.

R5's wakeUp call was already removed in 9c32650 after its red test showed
PhysX wasm `addForce(force, eFORCE, autowake=true)` wakes sleeping actors on
its own — no separate test needed here.

Verification: full physics suite 216/216 passing.
GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

luzhuang and others added 2 commits May 15, 2026 18:30
…ansform clone dirty flag

Two latent bugs surfaced jointly while fixing kinematic-pair contact callbacks (3D billiard
aim-line):

1. setGlobalPose vs setKinematicTarget routing (PhysX 4.x API misuse)

   PhysX docs require setKinematicTarget for moving kinematic actors if collision detection
   is needed; setGlobalPose is teleport and skips contact detection. SceneBinding already
   sets kineKineFilteringMode = staticKineFilteringMode = eKEEP, but every transform sync
   went through setWorldTransform → setGlobalPose, silently short-circuiting that intent.

   Split Collider's native-sync responsibility into two virtual hooks:
   - _teleportToEntityTransform(pos, rot): init/clone path, always setGlobalPose
   - _syncEntityTransformToNative(pos, rot): per-frame runtime path
   DynamicCollider overrides the runtime hook to call nativeCollider.move() (=
   setKinematicTarget) when _isKinematic. CharacterController overrides the teleport hook
   to use setWorldPosition (its native API lacks setWorldTransform).
   PhysXDynamicCollider.move() also normalizes the rotation quaternion (PhysX requirement).

2. Transform._cloneTo missed world-flag dispatch

   Earlier components on a cloned entity (e.g. DynamicCollider's native ctor) may query
   transform.worldPosition, which clears WorldPosition / WorldMatrix dirty flags as a side
   effect of caching. _cloneTo then writes new local values but never re-dirties or
   dispatches the world flag set, so subsequent worldPosition reads return stale (0,0,0)
   and registered listeners (Collider._updateFlag, etc.) never fire. Added
   _worldAssociatedChange(WmWpWeWqWsWus) at the end of _cloneTo.

   This bug was hidden under the old setGlobalPose path (first _onUpdate teleported the
   actor regardless of cache freshness). Switching to setKinematicTarget exposed it as
   Joint/HingeJoint/SpringJoint clone regressions where box2 flew at 299 m/s (implied
   velocity from native actor at default (0,0,0) being kinematic-animated to entity's
   true (0,5,0) over one 1/60s substep).

Test coverage:
- tests/src/core/Transform.test.ts: 2 new cases targeting _cloneTo dirty-flag dispatch
  (worldPosition cache invalidation + listener fire). Both fail without the Transform.ts
  fix.
- tests/src/core/physics/Collision.test.ts: 9 new cases covering kine-kine / kine-dyn /
  dyn-dyn / dyn+frozen pairs via transform.setPosition. Confirms callback firing under
  the new routing.
- tests/src/core/physics/PhysicsScene.test.ts: updated 2 assertions ("Dynamic Kinematic
  vs Static", "Dynamic Trigger Kinematic vs Dynamic Kinematic") from expecting 0
  callbacks to expecting callbacks fire — aligning with SceneDesc.kineKineFilteringMode
  = eKEEP design intent.
- tests/src/core/physics/DynamicCollider.test.ts: additional kinematic-mode coverage.

All 231 physics + Transform tests pass. Joint / HingeJoint / SpringJoint / CharacterController
clone tests all GREEN.
Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查(2026-05-15)

总结

本轮新增两个 commit(5ec7e3f + ab190ac):

  • 5ec7e3f fix(physics): route kinematic transform sync through setKinematicTarget + fix Transform._cloneTo world-flag dispatch — 将 per-frame transform 同步路径拆分为 _teleportToEntityTransform(init/clone)和 _syncEntityTransformToNative(每帧),DynamicCollider 覆盖后者,kinematic 时走 move()(即 setKinematicTarget)而非 setGlobalPose,解决 kine-kine/kine-dynamic onCollisionEnter 不触发问题;同时修复 Transform._cloneTo 漏发 world-flag 导致 clone 后 collider 位置 stale 的 bug。
  • ab190ac chore: release — 版本号 game.11 → game.12,无逻辑改动

方向完全正确,PhysX setKinematicTarget vs setGlobalPose 的区别是核心问题所在,修复直指根因。Transform dirty-flag 传播修复也准确。


问题

[P1] Transform._cloneTo 测试绕过公开 entity.clone() 链路

// tests/src/core/Transform.test.ts
// @ts-ignore
source.transform._cloneTo(target.transform);  // 直接调用私有方法
expect(target.transform.worldPosition).deepEqual({ x: 1, y: 2, z: 3 });

这个 bug 的触发链路是:entity.clone() → 组件 ctor 提前读 worldPosition(清 dirty flag) → _cloneTo 写新值 → Collider 读到 stale (0,0,0)。测试却直接调 _cloneTo,绕过了 entity.clone() 的完整 clone 流程(包括组件的 _cloneFrom 顺序)。

这带来两个问题:

  1. 如果有人重构 entity.clone() 内部调度顺序,导致 bug 复现,测试仍然绿——因为它不走 entity.clone() 路径。
  2. 测试的实质是验证"直接调 _cloneTo 后 worldPosition 正确",而非"clone 后 collider 不读到 stale position"。

建议改为从公开 API 测:

// 源 entity 在 (1,2,3)
const src = rootEntity.createChild("src");
src.transform.setPosition(1, 2, 3);
src.addComponent(DynamicCollider);  // DynamicCollider ctor 会读 worldPosition

const cloned = src.clone();
// 断言 cloned 上的 DynamicCollider 收到正确的 worldPosition 通知
// (可通过检查 native actor 位置或注册 worldChangeFlag 来断言)

[P2] Collision.test.ts"frozen + teleport" 测试只有 resolvereject,失败机制是 expect.fail() 意外 throw

it("dynamic + frozen-6 + teleport: ...", function () {
  return new Promise<void>((resolve) => {  // ← 缺少 reject 参数
    // ...
    if (!fired) {
      expect.fail("expected onCollisionEnter to fire...");  // ← throw 导致 Promise 隐式 reject
    }
  });
});

当前偶然能工作(expect.fail() throw 被 Promise 构造器捕获后隐式 reject),但这不是标准写法。正确形式:

return new Promise<void>((resolve, reject) => {
  // ...
  if (!fired) {
    reject(new Error("expected onCollisionEnter to fire for dynamic-frozen pair after teleport"));
  }
});

与同文件中其他 probeKinematicCallback 测试(显式传 reject)保持一致。


[P0] e2e/config.ts 删除后 e2e/tests/index.spec.ts 仍然 import "../config" — CI 必然报错(已提 2 轮,仍未修复)

PR diff 删除 e2e/config.ts,但 e2e/tests/index.spec.tsimport { E2E_CONFIG } from "../config"(硬依赖,无降级),合并后 playwright suite 无法启动。


[P1] UIPointerEventEmitter._bubble — 冒泡结束后 currentTarget 未清空(已提 5 轮,仍未修复)

for (let i = 0; i < length; i++) {
  eventData.currentTarget = path[i];
  fireEvent(path[i], eventData);
}
// 缺少:eventData.currentTarget = null;

[P1] Collider._onUpdate_onPhysicsUpdate 每帧对所有 shape 无条件调用(已提 3 轮,仍未修复)

_onUpdate(): void {
  if (this._updateFlag.flag) {
    // ... transform sync ...
  }
  for (let i = 0, n = shapes.length; i < n; i++) {  // ← 在 guard 块外,每帧无条件调用
    shapes[i]._onPhysicsUpdate();
  }
}

[P1] findAnimatorState 返回类型 breaking change 且语义错误(已提 2 轮,仍未修复)

state 未在播放时,返回的是当前播放的 srcPlayData(其 .state 是另一个动画),语义错误。应返回 null


[P2] PointerEventData.target / currentTarget 类型声明不含 null(已提 5 轮,仍未修复)

target: Entity = null;        // 应为 Entity | null
currentTarget: Entity = null; // 应为 Entity | null

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

说明

PR #2983(Fix/shaderlab)是一个大型综合 PR,包含多个方向的改动(ShaderLab、动画、GLTF 加载器、物理、UI 等)。从 PR 描述来看,其中的动画+GLTF 修复已被抽离到 #2999,物理 raycast/sweep 修复已被抽离到 #2998

建议继续将剩余改动拆分成独立 PR 再逐一合入 dev/2.0,而不是合并这个全量 PR。原因:

  1. 混合多个功能域的 PR 难以做 code review(注意力稀释)
  2. 如果其中某个方向有问题,回滚代价高
  3. PR 描述模板中的 checklist 全部未填写,缺少变更说明

如果后续有独立拆分的 PR,请在各 PR 上发起 review。此 PR 暂不做详细 diff 审查。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants