-
Notifications
You must be signed in to change notification settings - Fork 557
Adding a spline joint to the joint collection #2784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MegMll
wants to merge
168
commits into
stack-of-tasks:devel
Choose a base branch
from
MegMll:topic/spline_joint
base: devel
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
168 commits
Select commit
Hold shift + click to select a range
a10ce5a
Joints - Add a first implementation of joint spline
MegMll 2a6edbe
Unittest - Add TU for spline joint
MegMll 222c07a
Bindings - add bindings for spline joint
MegMll b4fd816
Spatial - Weird fix to make spline joint work for now (to remove)
MegMll fd21eba
Unittest - update joint unittests to include spline joints
MegMll 18b3b92
SplineJoint - apply pre-commit
MegMll 74c1e65
Spline - use the true cumulative form of the spline
MegMll fb53cdd
Spline - Add isEqual Method
MegMll c3c65b4
Spline - Add TU with finite difference
MegMll 53d341a
Unittest - fix tests for spline joint
MegMll e88b923
Spline joint - change if/else to be casadi compatible
MegMll e12aa61
Joint Spline - Remove useless comments
MegMll 588131a
Joint Spline - Fix indexing
MegMll 6580fac
Joint Spline - Eigen No Malloc checked and passed
MegMll 1f43b93
Spline joints - fix casadi joints tests
MegMll 09e89fa
Joint Spline - Add a build method
MegMll 2e51cc5
Unittest - Fix joint spline's unittests
MegMll 534c69d
Model-graph - Add spline joint to model graph
MegMll c72f2c5
JointSpline - make some correction to the joint
MegMll d5d163c
TU - apply new jointSpline constructor
MegMll 4d49886
ModelGraph - use new JointSpline method
MegMll 72d162b
Serialization - Add attributes from spline model and data
MegMll 9b6d290
Unittest - use std vector instead of se3
MegMll 34b0d4e
JointSpline - Diverse Changes
MegMll fff96bb
Casadi - Add a separated file for findSpan to be able to do specializ…
MegMll 0a07743
Bindings - modify joint spline binding
MegMll 470ba33
JointSpline - Remove findSpan
MegMll 3cf9774
Unittest - remove buildJoint function
MegMll 2a4074b
ModelGraph - finish adding jointSpline to model graph + unittest
MegMll f092510
cppadcg: Fix cppadcg cast test with spline joint
jorisv 6a961ee
multiprecision: Fix multiprecision cast test with Spline joint
jorisv 3bdd00c
multiprecision: Add missing header
jorisv 8250c0a
lint: Run linter
jorisv 55d3eaa
JointSpline - Removed comments
MegMll 315dfe1
JointSpline - move bspline functions in public to be able to test it
MegMll 28c2fba
Unittest - add more test for joint spline
MegMll 19742de
Bindings - Add another constructor for joint spline
MegMll e9e2a4a
Unittest - put full knee trajectory
MegMll db1fbf4
Unittest - fix getTrajectory function
MegMll df7bb40
Bindings - add modelgraph bindings constant
MegMll c911491
JointSpline - Fix indexing
MegMll c709e01
JointSpline - Add a doc
MegMll 0987757
JointSpline - Fix bias computation
MegMll dbc51a9
Unittest - add a finite difference test for S and bias c for JointSpline
MegMll 71b4101
JointSpline - Add comments and complete isEqual function
MegMll 29c6fc4
Unittest - Try to add tolerance to see if it fixes macos-intel problem
MegMll 2f6f0fb
Unittest - fix compilation error
MegMll 109ce2f
JointSpline - add assert for configuration to be between 0 and 1
MegMll 0d42a74
Bindings - Add graph spline joints bindings
MegMll 8224e91
Algo/Splines - remove comments
MegMll 3a9a244
Unittest - add find span test for q = 0
MegMll 77dfab7
Changelog - update
MegMll 3eea325
JointSpline - remove SE3 and use Transformation_t
MegMll 5f5c210
Unittest - remove comments
MegMll 365d826
JointSpline - Use transformation_t and motion_t
MegMll adaa24d
Examples - Add a small examples on what the spline joint can do
MegMll 5c003e8
Unittest - Specialize for joint spline
MegMll 87591d1
Examples - add spline joint examples
MegMll 9e5649e
Unittest - Fix joint generic test for joint spline
MegMll db514f5
Examples - Fix some linting errors
MegMll 8becc17
JointSpline - Disable assert
MegMll c526591
core: Apply small changes
jorisv 174470a
Algo/splines - add doc
MegMll 1933556
Unittest - Avoid code duplication for randomConfig vector generation
MegMll 8244f2d
Unittest - more complex and complete tests
MegMll dce59cb
Unittest/cppad - use degree 3 spline joint
MegMll 96f6fab
unittest - change name of struct to be more generic
MegMll 7db87e4
unittest - avoid code duplication
MegMll 539813a
unittest - avoid code duplication
MegMll 60a1bed
Unittest - fix casadi/joints test
MegMll e315f77
Unittest - add forgotten helical unaligned joint
MegMll 8a3df20
Unittest - Fix casadi test for universal joints
MegMll 4ae2d28
Mode-graph - use new function for joint spline
MegMll 1057886
Linting
MegMll 9575d2e
unittest - fix new joint
MegMll 022126e
JointSpline - Fix bias computation
MegMll 3dc2652
Unittest - Fix finite-difference test
MegMll d5f3c01
unittest - use config vector of joints
MegMll 1fae00c
unittest - remove useless lines
MegMll 6c1e84d
Unittest - Add a utils file to centralize init of joints with parameters
MegMll 0294c22
Run pre-commit
MegMll eb41d3c
unittest/utils - Fix mimic init
MegMll fcc53be
Core - update spline to allow spans to be between min and max, instea…
MegMll f9b983d
core - move joint spline definition
MegMll 8fcc9a1
core - update splines algorithms
MegMll 20baa07
core - add spline joint
MegMll 4b7ec2f
parser - add spline joint
MegMll bd6fa64
core - fix some errors and warnings (some errors due to rebase still …
MegMll 34ab05f
unittest - use jointModelWithParameters
MegMll 5a70e46
unittest - use std::vector
MegMll efe4c1d
remove useless file
MegMll b05323e
unittest - fix compilation warnings
MegMll c81ec80
unittest - add spline joint to serialization test
MegMll ff32ecf
unittest - update serialization test to use jointModelWithParameters
MegMll 88186fc
unittest - clean, remove comments
MegMll be6d1b8
fix cmake: @lucasJoseph
Ipuch cce666a
fix: splines.hpp to sources.cmake
Ipuch 9dc293f
core : factorize code calc functions
MegMll 954e47f
core : finish adding min/max for the knot vector + fix knot vector co…
MegMll 0b66bd2
unittest : modify knot unittest to test min/max
MegMll 1f6901d
core : add min/max for spline joint
MegMll 6352ef7
refactor: decoupling the knotvector scaling
Ipuch 7da5f68
tests: joint-generic bounding q with joint limits
Ipuch b9c2643
spline: Apply new header convention
jorisv 10e86da
spline: Fix build
jorisv dbc38fb
core - add a new struct JointSplineBuilder
MegMll fd961e4
unittest - update tests to use new builder struct
MegMll e53faca
model-graph - update jointSpline
MegMll 84ee9a0
bindings - update jointSpline bindings
MegMll 2352f3e
graph: Explain why we can't reverse the joint
jorisv 52903fc
core - move splines algos into internal and/or private
MegMll 3d377e1
core - make test casadi compatible
MegMll 4cc4b5b
unittest - Fix degree
MegMll 719fe67
unittest - Make tset compatible with new internal api
MegMll 5f204b8
unittest: Fix joint-init.hpp build
jorisv 2e3a900
unittest: Use joint-init.hpp in visitor.cpp
jorisv 6abe11f
unittest: Use joint-init.hpp in joint-motion-subspace.cpp
jorisv 0396b77
unittest: Use joint-init.hpp in all-joints.cpp
jorisv 3f17fbf
unittest: Remove dead code
jorisv 2bb46e9
mpfr: Remove useless include
jorisv 10c41e3
changelog: Set in Unreleased and add example link
jorisv 55c905a
casadi: Fix scalar casting
jorisv 7c0b85d
casadi: Fix expression evaluation
jorisv e9d59b1
graph: Add JointSplineBuilder
jorisv a0f3a96
joint: Remove moved methods
jorisv 1b68377
joint: Remove one warning
jorisv 83b2af7
casadi: Fix build
jorisv 6c5def0
python: Fix binding build and add graph::JointSplineBuilder
jorisv 5fce325
examples: Use coal and run when running ctest
jorisv 1f72d6c
python: Add JointModelSplineBuilder binding
jorisv 03d98fd
lint: Apply clang-format
jorisv 51e9302
joint: Remove warning
jorisv b061b3e
unittest: Use builder to build joint
jorisv e4294e1
unittest: Fix cppad-joints build
jorisv a55eb74
unittest - rewrite most tests to use internal functions directly
MegMll 8a794f1
core - write correct test for range inside basis function computation
MegMll 26da239
example: knee with spline joint.
Ipuch 55f4611
example: spline knee joint with better curves.
Ipuch 5211976
example: spline knee joint make it minimal
Ipuch bb6850e
example: parabola in spline-joint.py
Ipuch c77dec8
spline: Fix FindSpan and improve tests
jorisv fc163ef
test: Relax precision on finite-differences test
jorisv 5ceeac4
test: fix some assert and remove warnings
jorisv ea3a114
spline: Remove more warnings
jorisv 5011384
unittest: Use an open uniform spline by default to help numerical pre…
jorisv fbd0bf6
unittest: Add a bound test to bsplineBasis
jorisv 696acb7
python: Expose JointDataSpline specialized members
jorisv 4398c72
warning: Remove a bunch of clang warning
jorisv dbd7b9d
core - add Continuity check over knot vector
MegMll e83306a
unittest - add test for basis function continuity
MegMll 220ac50
unittest - fix unittest for basis function derivatives (finite differ…
MegMll 5f8ef41
examples - apply linting
MegMll c901379
spline: Add deBoor algorithm to evaluate the BSpline
jorisv 9643d8e
spline: Remove warnings
jorisv 5f738ea
spline: Implement deBoorCumBasisSparse
jorisv 7375020
spline: Rename deBoorBasis into deBoor
jorisv 64e9aa9
spline: Add bsplineBasis to compute all basis function with minimal c…
jorisv ec7039b
spline: Remove other deBoor like algorithm
jorisv 6ad5593
spline: Add comments
jorisv c50af03
spline: First working version with deBoorBasis
jorisv f90968f
spline: Add standalone function to compute cumulative bspline derivat…
jorisv c84a983
spline: Move bsplineBasis in unit tests and test cumulativeBasisDeriv…
jorisv dd672f7
spline: Fix build
jorisv c67c201
spline: Add deBoorFullBasis function
jorisv cbd4841
spline: Improve doc and comments
jorisv 55604f2
spline: Add Full variant to support casadi
jorisv 0c2cc04
spline: Refactor code to implement spline transformation on minimal a…
jorisv 66ff657
spline: Add computeSplineKinematicsFull
jorisv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| """Knee spline joint driven by its physical flexion angle. | ||
|
|
||
| This example uses the SE(3) spline joint (``pin.JointModelSpline`` / | ||
| ``pin.JointModelSplineBuilder``) to reproduce experimental knee kinematics: the | ||
| joint constrains its child (tibia) frame to a cumulative B-spline on SE(3) | ||
| defined by a list of control frames, a knot vector and a degree | ||
|
|
||
| source: Lee, S. H., & Terzopoulos, D. (2008). | ||
| Spline joints for multibody dynamics. In ACM SIGGRAPH 2008 papers (pp. 1-8). | ||
|
|
||
| The 8 control frames below were fit offline (Gauss-Newton, degree=3, n_ctrl=8) | ||
| to a planar OpenSim knee, where the tx/ty translations the rotation | ||
| about Z (knee flexion) are driven by knee flexion itself. | ||
| """ | ||
|
|
||
| import time | ||
| from pathlib import Path | ||
|
|
||
| import coal | ||
| import meshcat.geometry as mg | ||
| import numpy as np | ||
| import pinocchio as pin | ||
| from pinocchio.visualize import MeshcatVisualizer | ||
|
|
||
| # Bones: mesh folder, OpenSim (subject-specific) scale factors and display color. | ||
| MESH_DIR = Path(__file__).resolve().parent.parent / "models" / "biomechanics" | ||
| FEMUR_SCALE, TIBIA_SCALE = 1.17378, 1.12373 | ||
| BONE_COLOR = np.array([0.96, 0.94, 0.86, 0.7]) # bone ivory, semi-transparent. | ||
|
|
||
| # Recenter so the femoral condyle (knee) sits at the world origin instead of the | ||
| # hip: the fixed femur and the spline joint are both lifted by the knee offset. | ||
| KNEE_FROM_HIP = np.array([-0.0046, -0.4644, 0.0]) | ||
| RECENTER = pin.SE3(np.eye(3), -KNEE_FROM_HIP) | ||
|
|
||
| # Control frames from the offline 2D-knee regression, as (yaw [rad], tx, ty [m]). | ||
| CONTROL_FRAMES = [ | ||
| (-2.094400, -0.003842, -0.496205), | ||
| (-1.815147, 0.001278, -0.489246), | ||
| (-1.256640, 0.006243, -0.480186), | ||
| (-0.418880, -0.002220, -0.466444), | ||
| (0.418880, -0.008427, -0.463391), | ||
| (1.256640, -0.005740, -0.461613), | ||
| (1.815147, -0.006127, -0.464906), | ||
| (2.094400, -0.006285, -0.464416), | ||
| ] | ||
|
|
||
|
|
||
| def mesh_object(name, parent_joint, stl, scale, placement): | ||
| """Build a mesh GeometryObject fixed to ``parent_joint`` at ``placement``.""" | ||
| mesh_scale = scale * np.ones(3) | ||
| geom = coal.MeshLoader().load(str(stl), mesh_scale) | ||
| obj = pin.GeometryObject(name, parent_joint, placement, geom, str(stl), mesh_scale) | ||
| obj.meshColor = BONE_COLOR | ||
| return obj | ||
|
|
||
|
|
||
| class PointTracer: | ||
| """Trace a fixed point in Meshcat, keeping the dotted trail.""" | ||
|
|
||
| def __init__(self, viz, name, point=None, color=0xFF3030, size=0.004): | ||
| self._world_to_femur = RECENTER.inverse() | ||
| self._point = np.zeros(3) if point is None else np.asarray(point, float) | ||
| self._material = mg.PointsMaterial(size=size, color=color) | ||
| self._trail = [] | ||
| self._node = viz.viewer[f"tibia_traces/{name}"] | ||
| self._node.set_transform(RECENTER.homogeneous) | ||
|
|
||
| def add(self, tibia_pose): | ||
| self._trail.append(self._world_to_femur.act(tibia_pose.act(self._point))) | ||
| pts = np.asarray(self._trail, dtype=np.float32).T | ||
| self._node.set_object(mg.Points(mg.PointsGeometry(pts), self._material)) | ||
|
|
||
|
|
||
| def main(): | ||
|
|
||
| control_frames = [ | ||
| pin.SE3(pin.rpy.rpyToMatrix(0.0, 0.0, yaw), np.array([tx, ty, 0.0])) | ||
| for (yaw, tx, ty) in CONTROL_FRAMES | ||
| ] | ||
|
|
||
| # Knee-angle domain of the fitted spline (in degree). | ||
| THETA_MIN_DEG, THETA_MAX_DEG = -120.0, 120.0 | ||
|
|
||
| knee = ( | ||
| pin.JointModelSplineBuilder() | ||
| .withDegree( | ||
| 3 | ||
| ) # degree of the splines, degree 3 means acceleration is continuous. | ||
| .withControlFrameVector(control_frames) # list of SE3 transforms | ||
| .withOpenUniformKnots( | ||
| THETA_MIN_DEG, THETA_MAX_DEG | ||
| ) # Map knee flexion angle (degrees) to spline parameter q in [0, 1]. | ||
| .build() | ||
| ) | ||
| model = pin.Model() | ||
| joint_id = model.addJoint(0, knee, RECENTER, "knee") | ||
|
|
||
| # visual only code | ||
| try: | ||
| visual_model = pin.GeometryModel() | ||
| visual_model.addGeometryObject( | ||
| mesh_object("femur", 0, MESH_DIR / "femur_r.stl", FEMUR_SCALE, RECENTER) | ||
| ) | ||
| visual_model.addGeometryObject( | ||
| mesh_object( | ||
| "tibia", | ||
| joint_id, | ||
| MESH_DIR / "tibia_r.stl", | ||
| TIBIA_SCALE, | ||
| pin.SE3.Identity(), | ||
| ) | ||
| ) | ||
|
|
||
| viz = MeshcatVisualizer(model, None, visual_model) | ||
| viz.initViewer(open=True) | ||
| viz.loadViewerModel() | ||
|
|
||
| except ImportError as e: | ||
| print("Error while initializing the viewer.") | ||
| print(e) | ||
| return | ||
|
|
||
| # Trace tibia-frame points and show the tibia frame as a triad, updated every step. | ||
| plateau = np.array([0.03100708, -0.03819558, -0.00347008]) | ||
| tracers = [ | ||
| PointTracer(viz, "origin", color=0xFF3030), | ||
| PointTracer(viz, "plateau", plateau, color=0x3060FF), | ||
| ] | ||
| tibia_frame = viz.viewer["tibia_frame"] | ||
| tibia_frame.set_object(mg.triad(0.08)) | ||
|
|
||
| # Sweep the physical knee angle (+10 deg hyperextension to -120 deg flexion), | ||
| angles = np.linspace(10.0, -120.0, 120) | ||
| angles = np.concatenate([angles, angles[::-1]]) | ||
| for _ in range(5): | ||
| for theta_deg in angles: | ||
| viz.display(np.array([theta_deg])) | ||
| tibia_pose = viz.data.oMi[joint_id] | ||
| for tracer in tracers: | ||
| tracer.add(tibia_pose) | ||
| tibia_frame.set_transform(tibia_pose.homogeneous) | ||
| time.sleep(0.015) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import time | ||
|
|
||
| import meshcat.geometry as mg | ||
| import numpy as np | ||
| import pinocchio as pin | ||
| from pinocchio.visualize import MeshcatVisualizer | ||
|
|
||
|
|
||
| class PointTracer: | ||
| """Trace a fixed point in Meshcat, keeping the dotted trail.""" | ||
|
|
||
| def __init__(self, viz): | ||
| self._material = mg.PointsMaterial(size=0.004, color=0xFF3030) | ||
| self._trail = [] | ||
| self._node = viz.viewer["solid_pose"] | ||
|
|
||
| def add(self, solid_pose): | ||
| self._trail.append(solid_pose.translation.copy()) | ||
| pts = np.asarray(self._trail, dtype=np.float32).T | ||
|
|
||
| line = mg.Line( | ||
| geometry=mg.PointsGeometry(pts), | ||
| material=mg.LineBasicMaterial(color=0xFF3030), | ||
| ) | ||
| self._node.set_object(geometry=line) | ||
|
|
||
|
|
||
| def main(): | ||
|
|
||
| PARABOLA_CONTROL_FRAMES = [ | ||
| (-1.000000, 1.000000), | ||
| (-0.888889, 0.777778), | ||
| (-0.666667, 0.407407), | ||
| (-0.333333, 0.074074), | ||
| (0.000000, -0.037037), | ||
| (0.333333, 0.074074), | ||
| (0.666667, 0.407407), | ||
| (0.888889, 0.777778), | ||
| (1.000000, 1.000000), | ||
| ] | ||
|
|
||
| control_frames = [ | ||
| pin.SE3(np.eye(3), np.array([0, ty, tz])) | ||
| for (ty, tz) in PARABOLA_CONTROL_FRAMES | ||
| ] | ||
|
|
||
| # Create a Pinocchio model with a single free-flyer joint | ||
| model = pin.Model() | ||
| spline_joint = ( | ||
| pin.JointModelSplineBuilder() | ||
| .withDegree(3) | ||
| .withControlFrameVector(control_frames) | ||
| .withOpenUniformKnots(-1, 1) | ||
| .build() | ||
| ) | ||
| joint_id = model.addJoint(0, spline_joint, pin.SE3.Identity(), "spline-joint") | ||
|
|
||
| # adding some inertia to drop the solid in the parabola | ||
| box_size = 1.0 # edge length of each cube [m] | ||
| box_mass = 1.0 # mass of each cube [kg] | ||
| box_inertia = pin.Inertia.FromBox(box_mass, box_size, box_size, box_size) | ||
| model.appendBodyToJoint(joint_id, box_inertia, pin.SE3.Identity()) | ||
|
|
||
| # visual only code | ||
| try: | ||
| visual_model = pin.GeometryModel() | ||
|
|
||
| viz = MeshcatVisualizer(model, None, visual_model) | ||
| viz.initViewer(open=True) | ||
| viz.loadViewerModel() | ||
|
|
||
| except ImportError as e: | ||
| print("Error while initializing the viewer.") | ||
| print(e) | ||
| return | ||
|
|
||
| tracers = [PointTracer(viz)] | ||
| solid_frame = viz.viewer["solid_frame"] | ||
| solid_frame.set_object(mg.triad(0.2)) | ||
|
|
||
| dt = 0.01 | ||
| qs, _ = sim_loop(model, dt) | ||
|
|
||
| for theta in qs: | ||
| viz.display(np.array([theta])) | ||
| solid_pose = viz.data.oMi[joint_id] | ||
| for tracer in tracers: | ||
| tracer.add(solid_pose) | ||
| solid_frame.set_transform(solid_pose.homogeneous) | ||
| time.sleep(dt) | ||
|
|
||
|
|
||
| def sim_loop(model, dt=0.01, nsteps=800): | ||
|
|
||
| qs = [np.array([1.0])] | ||
| vs = [np.array([0])] | ||
| data = model.createData() | ||
| for i in range(nsteps): | ||
| q = qs[i] | ||
| v = vs[i] | ||
| tau = -1 * v # a little bit of damping | ||
| a1 = pin.aba(model, data, q, v, tau) | ||
| vnext = v + dt * a1 | ||
| qnext = pin.integrate(model, q, dt * vnext) | ||
| qs.append(qnext) | ||
| vs.append(vnext) | ||
| return qs, vs | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.