{"id":15031983,"url":"https://github.com/donbright/earcutr","last_synced_at":"2025-04-09T21:22:44.495Z","repository":{"id":39406958,"uuid":"160102709","full_name":"donbright/earcutr","owner":"donbright","description":"port of MapBox's earcut triangulation code to Rust language","archived":false,"fork":false,"pushed_at":"2024-05-12T17:14:40.000Z","size":1175,"stargazers_count":60,"open_issues_count":0,"forks_count":10,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-04T15:55:08.768Z","etag":null,"topics":["computer-graphics","earcut","geometry-library","mapbox","polygon","rust","rust-language","triangulation","triangulation-library"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/donbright.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-12-02T22:29:22.000Z","updated_at":"2025-02-21T13:32:59.000Z","dependencies_parsed_at":"2024-05-12T18:27:05.654Z","dependency_job_id":"5decd5d3-6fd2-4da0-9ff1-52f958f46f6b","html_url":"https://github.com/donbright/earcutr","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/donbright%2Fearcutr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/donbright%2Fearcutr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/donbright%2Fearcutr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/donbright%2Fearcutr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/donbright","download_url":"https://codeload.github.com/donbright/earcutr/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248112892,"owners_count":21049746,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["computer-graphics","earcut","geometry-library","mapbox","polygon","rust","rust-language","triangulation","triangulation-library"],"created_at":"2024-09-24T20:17:02.991Z","updated_at":"2025-04-09T21:22:44.455Z","avatar_url":"https://github.com/donbright.png","language":"Rust","readme":"\n[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua)\n\n# Earcutr\n\nThis is a port of the MapBox company's Earcut computer code ( see \nhttps://github.com/mourner, Volodymyr Agafonkin) , which triangulates \npolygons. Please see https://github.com/mapbox/earcut for more\ninformation about the original javascript code. This port is to the\nRust computer language, and is single-threaded.\n\nThis port is not affiliated with MapBox in any way and no endorsement is \nimplied.  Also please note MapBox has their own Rust port of this code, \nno claim is made this is better than theirs. The goal is to have identical \noutput to MapBox's Javascript code however several updates have been\nmade to MapBox js and this Rust port is a few years behind.\n \nPlease also note someone made this into a Crate (not me) for \nconvenience, please check crates.io\n\n![image showing an outline of a circle with a hole inside of it, with \n!triangles inside of it](viz/circle.png \"circle, earcut\")\n\n\n\n## Usage\n\n```rust\nextern crate earcutr;\nvar triangles = earcutr::earcut(\u0026vec![10,0, 0,50, 60,60, 70,10],\u0026vec![],2);\nprintln!(\"{:?}\",triangles);  // [1, 0, 3, 3, 2, 1]\n```\n\nSignature: \n\n`earcut(vertices:\u0026vec\u003cf64\u003e, hole_indices:\u0026vec\u003cusize\u003e, dimensions:usize)`.\n\n* `vertices` is a flat array of vertex coordinates like `[x0,y0, x1,y1, x2,y2, ...]`.\n* `holes` is an array of hole _indices_ if any\n  (e.g. `[5, 8]` for a 12-vertex input would mean one hole with vertices 5\u0026ndash;7 and another with 8\u0026ndash;11).\n* `dimensions` is the number of coordinates per vertex in the input array. Dimensions must be 2.\n\nEach group of three vertex indices in the resulting array forms a triangle.\n\n```rust\n// triangulating a polygon with a hole\nearcutr::earcut(\n   \u0026vec![0.,0., 100.,0., 100.,100., 0.,100.,  20.,20., 80.,20., 80.,80., 20.,80.],\n   \u0026vec![4] , 2 );\n// result: [3,0,4, 5,4,0, 3,4,7, 5,0,1, 2,3,7, 6,5,1, 2,7,6, 6,1,2]\n```\n\nIf you pass a single vertex as a hole, Earcut treats it as a Steiner point. \nSee the 'steiner' test under ./tests/fixtures for an example input,\nand the test visualization under ./viz.\n\nAfter getting a triangulation, you can verify its correctness with \n`earcutr.deviation`:\n\n```rust\nlet deviation = earcutr.deviation(\u0026data.vertices, \u0026data.holes, data.dimensions, \u0026triangles);\n```\n\nDeviation returns the relative difference between the total area of \ntriangles and the area of the input polygon. `0` means the triangulation \nis fully correct.\n\n## Flattened vs multi-dimensional data\n\nIf your input is a multi-dimensional array you can convert it to the \nformat expected by Earcut with `earcut.flatten`. For example:\n\n```rust \nlet v = vec![\n  vec![vec![0.,0.],vec![1.,0.],vec![1.,1.],vec![0.,1.]], // outer ring\n  vec![vec![1.,1.],vec![3.,1.],vec![3.,3.]]        // hole ring\n];\nlet (vertices,holes,dimensions) = earcutr::flatten( \u0026v );\nlet triangles = earcutr::earcut(\u0026vertices, \u0026holes, dimensions);\n``` \n\nThe [GeoJSON Polygon](http://geojson.org/geojson-spec.html#polygon) format uses \nmulti-dimensional data in a text based JSON format. There is example code under \ntests/integration_test.rs on how to parse JSON data. The test/fixtures test\nfiles are all multi-dimensional .json files.\n\n## How it works: The algorithm\n\nThe library implements a modified ear slicing algorithm,\noptimized by [z-order curve](http://en.wikipedia.org/wiki/Z-order_curve) hashing\nand extended to handle holes, twisted polygons, degeneracies and self-intersections\nin a way that doesn't _guarantee_ correctness of triangulation,\nbut attempts to always produce acceptable results for practical data.\n\nIt's based on ideas from\n[FIST: Fast Industrial-Strength Triangulation of Polygons](http://www.cosy.sbg.ac.at/~held/projects/triang/triang.html) by Martin Held\nand [Triangulation by Ear Clipping](http://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf) by David Eberly.\n\n### Visual example\n\nFor example a rectangle could be given in GeoJSON format like so:\n\n    [ [ [0,0],[7,0],[7,4],[0,4] ] ]\n\nThis has a single contour, or ring, with four points. The way\nthe points are listed, it looks 'counter-clockwise' or 'anti-clockwise'\non the page. This is the 'winding' and signifies that it is an 'outer'\nring, or 'body' of the shape.\n\n     _______\n     |     |\n     |     |\n     |     |\n     |_____|\n \nNow let's add a hole to the square.: \n\n    [ \n      [ [0,0],[7,0],[7,4],[0,4] ],   \n      [ [1,1],[3,1],[3,3] ] \n    ]\n\nThis has two contours (rings), the first with four points, the second \nwith three points. The second has 'clockwise' winding, signifying it is \na 'hole'. \n\n    _______\n    |     |\n    |  /| |\n    | /_| |\n    |_____|\n\nAfter 'flattening', we end up with a single array:\n\n    data [ 0,0,7,0,7,4,0,4,1,1,3,1,3,3  ]\n    holeindexes: [ 4 ]\n    dimensions: 2\n\nThe program will interpret this sequence of data into two separate \"rings\",\nthe outside ring and the 'hole'. The rings are stored using a circular\ndoubly-linked list. \n\nThe program then \"removes\" the hole, by essentially adding a \"cut\" between\nthe hole and the polygon, so that there is only a single \"ring\" cycle.\n\n         _______\n         |     |\n         |  /| |\n    cut\u003e |_/_| |\n         |_____|\n\nThe program also automatically 'corrects' the winding of the points \nduring input so they are all counter-clockwise.\n\nThen, an \"ear cutting\" algorithm is applied. But not just any earcutting\nalgorithm. \n\nNormally, an ear-cutter algorithm works by finding a potential ear, \nor a \"candidate\" ear, by looking at three consecutive points on the \npolygon. Then, it must make sure there are no other points \"inside\"\nthe ear.\n\nIn order to do this, it must iterate through every point in the polygon,\nso if your polygon has 15,000 points then it must go through all of them\nlooking to see if each one is inside the potential ear. Each ear check \ntakes a dozen or two calculations, typically using a test like the\nwedge product between each side of the ear, and the point to check - if\nthe point is on the right-hand-side (Wedge is less than zero) of each\nside, it's inside the ear, and so the ear cannot be cut. The algorithm\nthen moves on to the next potential ear.\n\n#### Z-order curve\n\nZ-order hashing allows the number of 'is in ear' checks to be drastically \ncut down. How? Instead of running the \"is in ear\" code on each other point\nin the polygon, it is able to only check points 'nearby' the ear. This is \naccomplished in the following manner:\n\nStep 1: before earcut, each point of the polygon is given a coordinate\non the z-order (Morton) curve through the space of the bounding box \nof the polygon. This is a type of space-filling curve strategy that \nhas been explored in many geometry algorithms. Pleas see \nhttps://en.wikipedia.org/wiki/Z-order_curve\n\nStep 2: The clever bit is that if you want to search within a limited\nrectangle inside the space filled by the Z-order curve, you can\nrelatively easily figure out which points are inside that box by\nlooking at their position on the z-order curve.. in other words\ntheir z index. The code stores the index as \".z\" in each node of the\nlinked list that represents a vertex of the polygon.\n\nTo be more specific, Z-order curves have a special thing about them,\nwhen you pick a limited rectangle inside, you can iterate along the\nz-curve from the \"lowest\" corner to the \"highest\" corner and be\nguaranteed to hit every 2d point inside that rectangle. \n\nFor example, in a 4x4 morton square, there are 16 Morton codes.\n\n     x-----0--1--2--3--\u003e\n    y|0    0  1  4  5\n     |1    2  3  6  7\n     |2    8  9 12 13\n     |3   10 11 14 15\n    \\|/\n\nGoing from z-code 0 to z-code 6, 0 1 2 3 4 5 6, takes us through \nevery 2 dimensional point between 0,0, where 0 lives, and 2,1, \nwhere 6 lives. It also goes through 3,0 but nothing is perfect.\n\nLet's say you pick 2,2 and 3,3. The z code at the lowest point\nis 12, and the highest is 15. So your z-iteration would be \n12, 13, 14, 15, which covers the rectangle from 2,2 to 3,3.\n\nSo, that is how it gets away without checking every point in the polygon\nto see if they are inside the ear. It draws a rectangle around the ear,\nit finds the lowest and highest corner of that rectangle, and iterates\nthrough the z-curve to check every 2-d point that is 'near' the polygon\near.\n\nAs you can imagine, if 14,000 of your points in your polygon are outside\nthis box, and only 1000 are in the box, thats quite a bit less math \nand calculation to be done than if you had to iterate through 15,000 points.\n\n#### Additional massaging\n\nIf the straightforward earcutting fails, it also does some simple fixes, \n\n- Filtering points - removing some collinear and equal points\n\n- Self intersections - removing points that only tie a tiny little knot\n  in the polygon without contributing to its overall shape (and also\n  make it not-simple)\n\n- Split bridge - actually split the polygon into two separate ones,\n  and try to earcut each.\n\nData examples are included under tests/fixtures in json files.\nVisualization of test results is generated under viz/testoutput and can\nbe viewed in a web browser by opening viz/viz.html\n\n### Coordinate number type\n\nThe coordinate type in this code is 64-bit floating point. Note that \n32-bit floating point will fail the tests because the test data files \nhave numbers that cannot be held with full precision in 32 bits, like \nthe base 10 number 537629.886026485, which gets rounded to 537629.875 \nduring conversion from base 10 to 32-bit base 2.\n\n\n### Tradeoffs\n\nThis triangulator is built primarily as an exercise in porting \njavascript to Rust. However some minor details of the implementation \nhave been modified for speed optimization. The code is supposed to \nproduce exacly the same output as the javascript version, by using the \nlarge amount of test data supplied with the original javascript code. \nThe speed is comparable with Mapbox's C++ version of earcut, earcut.hpp, \nexcept for input of very small polygons where the speed is much slower. \nSee the benchmarking section for more detail.\n\nIf you want to get correct triangulation even on very bad data with lots \nof self-intersections and earcutr is not precise enough, take a look at \n[libtess.js](https://github.com/brendankenny/libtess.js).\n\nYou may also want to consider pre-processing the polygon data with \n[Angus J's Clipper](http://angusj.com/delphi/clipper.php) which uses \nVatti's Algorithm to clean up 'polygon soup' type of data.\n\n### These algorithms are based on linked lists, is that difficult in Rust?\n\nYes. [A. Beinges's \"Too Many Lists\"](https://cglab.ca/~abeinges/blah/too-many-lists/book/) \nshows how to do Linked Lists in Rust. Rust also has a 'linked list' type\nbuiltin, which could be made Circular in theory by calling iter().cycle().\n\nHowever this code implements a Circular Doubly Linked List entirely on \ntop of a Rust Vector, without any unsafe blocks. This does not use Rc, \nBox, Arc, etc. The pointers in normal Linked List Node code have been \nreplaced by integers which index into a single Vector of Nodes stored in \nLinkedLists struct. It will still crash if you use an index out of bounds\nbut the RUST_BACKTRACE=1 will tell you exactly where it happened.\n\nIt also uses Extension Traits to basically extend the type usize itself, \nso you can have methods on indexes even though the indexes are just integers.\nFor example:\n\nIn C++ pointers imagine the following expression\n\n    start.prev.next = i\n\nThis takes the start node, finds the previous node, then sets the next \nnode of the previous node to some other node. start, prev, next,\nand i are all pointers into memory, which if set incorrectly could lead to\nsegmentation faults.\n\nIn this Rust implementation with Extension Traits we express the same idea as \nfollows. \n\n    start.prev(fll).set_next(fll,i);\n\nIn this version, we replaced pointers by integers (type usize) then\nuse Trait Extensions on usize to allow functions to be called 'on' the\nintegers. \n\n    fll is a reference to a struct, which has a vector of nodes. (fake linked list)\n    start is a usize, i is a usize, both indexes into fll's vector\n   \nThe trick here mentally is to separate the idea of a node from the idea \nof a pointer. So once we do that, we can comprehend that the \nidea of a Node is separate from the idea of it's index, which is \nbasically it's pointer but the pointer is limited to be within the \nVector inside of the 'fll' structure. So since a lot of linked list code \nis just shuffling pointers around, in this Rust version, the code is \njust shuffling indexes around. Since indexes are just integers, and we \ncan do Trait Extensions of integers, this allows us to mimic the syntax \nof pointers a little bit.\n\nNow we can also do some silly tricks with Nodes themselves, implementing\n'next' method for Node, that just calls the trait extension method\non the nodes own index. Then if we need to get the next Node and we have\na Node, we can just call next() on the node. \n\n     fll: LinkedList\n     a: Node = Node::new(blah blah blah);\n     fll.add(\u0026node);\n     calculate_something(a,a.next(fll));\n\nSo basically we have two concepts of next() here. \n\n     i: usize    // i is an index into some linked list fll\n     i.next(fll) // calling next() on an integer i which is an index into fll\n                 // this gives us the index of the next node in fll.\n\n     n: Node     // plain old struct Node\n     n.next(fll) // calling next() on a Node which gives us the next Node\n\nOne theoretical benefit of this setup is that if you want to transform\nevery single point in your dataset, you can just iterate thru the vector\nof nodes, rather than iterating thru a linked list. In theory\nthis could be faster than iterating through a linked list, since\nmany pieces of the machine are designed to quickly process arrays. \n\nThis might seem like a source of a slowdown - to bounds check every\narray access. However in practice the difference is barely measurable. \nIn fact, the code is built so it is relatively easy to switch to\n\"unchecked_get\" to test vector access without bounds checking. \n\n## Tests, Benchmarks\n\nTo run tests:\n\n```bash\n$ git clone github.com/donbright/earcutr\n$ cd earcutr\n$ cargo test             # normal Rust tests. Also outputs visualization data\n$ cd viz                 # which is stored under viz/testoutput. you can\n$ firefox viz.html       # view in your favorite web browser (circa 2018)\n```\n\nTo run benchmarks:\n\n```bash\n$ cargo bench\n...\ntest bench_water                ... bench:   1,860,385 ns/iter (+/- 21,188)\ntest bench_water2               ... bench:   1,477,185 ns/iter (+/- 10,294)\ntest bench_water3               ... bench:      63,800 ns/iter (+/- 3,809)\ntest bench_water3b              ... bench:       5,751 ns/iter (+/- 18)\ntest bench_water4               ... bench:     473,971 ns/iter (+/- 5,950)\ntest bench_water_huge           ... bench:  26,770,194 ns/iter (+/- 532,784)\ntest bench_water_huge2          ... bench:  53,256,089 ns/iter (+/- 1,208,028)\n```\n\nBench note: As of this writing, benchmarking is not in Stable Rust, so \nthis project uses an alternative, https://docs.rs/bencher/0.1.5/bencher/\n\n### Speed of this Rust code vs earcut.hpp C++ code\n\nFollowing is a rough table based on testing of Earcut's C++ code, \nEarcut's C++ test of LibTess' C++ code, and finally this Rust port of Earcut.\n\n```\n____polygon______earcut.hpp_-O2__libtessc++_-O2___Rust_earcutr_release\n| water      |  1,831,501 ns/i  |  9,615,384 ns/i |   1,860,385 ns/i |\n| water2     |  1,626,016 ns/i  |  1,694,915 ns/i |   1,477,185 ns/i |\n| water3     |     53,140 ns/i  |    153,869 ns/i |      63,800 ns/i |\n| water3b    |      4,183 ns/i  |     20,143 ns/i |       5,751 ns/i |\n| water4     |    475,511 ns/i  |    871,839 ns/i |     473,971 ns/i |\n| water_huge | 26,315,789 ns/i  | 26,315,789 ns/i |  26,770,194 ns/i |\n| water_huge2| 55,555,555 ns/i  | 20,000,000 ns/i |  53,256,089 ns/i |\n----------------------------------------------------------------------\nns/i = nanoseconds per iteration\n```\n\nThis Rust code appears to be about 20-40% slower than the C++ version of \nEarcut for tiny shapes. However with bigger shapes, it is either within\nthe error margin, or maybe a bit faster.\n\n#### Method for comparing Earcut C++ / Earcutr Rust\n\nMapbox has a C++ port of earcut.hpp, with a built in benchmarker, \nmeasured in 'ops per second'. It also compares against a c++ version of \nlibtess. However by default it builds without optimization, which \nhampers comparison. We can fix this.  Editing the .hpp CMakeLists.txt \nfile for the C compiler flags lets us turn on optimization,\n\n    add_compile_options(\"-g\" \"-O2\" ....\n\nNow, Rust bench measures in nanoseconds per iteration.\nC++ Earcut measures in iterations per second. To convert:\n18 ops in 1 second, is \n18 iterations in 1,000,000,000 nanoseconds. \n1,000,000,000 / 18 -\u003e 55,555,555 nanoseconds/iteration\nThis way, a conversion can be done. \n\n#### Profiling\n\n- http://www.codeofview.com/fix-rs/2017/01/24/how-to-optimize-rust-programs-on-linux/\n\n- Valgrind 's callgrind: (see Cargo.toml, set debug=yes)\n\n```bash\nsudo apt install valgrind\ncargo bench water2 # find the binary name \"Running: target/release/...\"\nvalgrind --tool=callgrind target/release/deps/speedtest-bc0e4fb32ac081fc water2\ncallgrind_annotate callgrind.out.6771\nkcachegrind callgrind.out.6771\n```\n\n- CpuProfiler \n\nFrom AtheMathmo https://github.com/AtheMathmo/cpuprofiler\n\n- Perf\n\nhttps://perf.wiki.kernel.org/index.php/Tutorial\n\n```bash\ncargo bench water2 # find the binary name \"Running: target/release/...\"\nsudo perf stat target/release/deps/speedtest-bc0e4fb32ac081fc  water2\nsudo perf record  target/release/deps/speedtest-bc0e4fb32ac081fc  water2\nsudo perf report\n```\n\n#### Profiling results\n\nPlease see [OPTO.md] if you wish a long description of the optimization\nprocess. Here are a few other highlights:\n\n* is_earcut_hashed() is hot\n\nProfilers reveal that on bigger shapes the vast majority of time is \nspent inside is_earcut_hashed(), which is determining whether an ear is \n\"really an ear\" so that it may be cut without damaging the polygon.\n\n* Zorder is also hot\n\nThe second major speed boost comes from Callgrind/kcachegrind in \nparticular revealed that the zorder() function was a source of some a \nlot of work by the CPU. In particular the conversion from floating point \n64 bit numbers in the input arguments, to the 32 bit integer, can be \nimportant to improving speed.\n\n* inline by-hand is important\n\nMost of the time in C++ you can assume the compiler figures out \ninlining. In Rust, however, the point_in_triangle and area function \ninside ear_checker wont get inlined unless specifically indicated with \nthe inline macro.\n\n* Vector [Indexing] and bounds checking in Rust doesn't hurt speed here\n\nAs mentioned, this code is implemented as a double-linked list sitting on\ntop of a vector, an the 'pointers' are actually indexes into the vector.\nThere are several macros used that represent the normal linked list\nlanguage, such as next!(index) prev!(index), which take index integers\nas input, and return a Node or Reference to Node. Each index uses Rust's\nbuilt in vector indexing, which uses 'bounds checking' so it will panic\nimmediately if memory outside the vector range is accessed on accident.\nThe panic and backtrace will report exactly what line the access occured\nand the value of the index that was too large to use.\n\nTheoretically this slows down the program. In practice, it does not.\nThis has been tested extensively because the macros like next!() and prev!()\nhave been written in a way that it is extremely easy to switch back and\nforth between bounds-checked vector indexing, and unsafe vector indexing\nusing get_unchecked(), and re-run 'cargo bench water' to compare them.\n\nThe benchmark of water shapes shows the difference is within error, \nexcept for tiny shapes like water3b, where the benefit is so tiny\nas to not be worth it for most usage.\n\n* Iteration vs loops\n\nThis code has converted several javascript for loops into Rust \niteration. In theory this is slower. In practice, it is not, and in some \ncases it is actually faster, especially in find_hole_bridge. In theory\niterators are easier to read and write, take up less code, and have less\nbugs.\n\n#### It could be faster\n\nThe goal is to have identical output to MapBox's Javascript code. So if \nthere are optimizations that break that compatability they will not be \nused. But there is of course always room for improvement.\n\n## This triangulator in other languages\n\n- [mapbox/earcut](https://github.com/mapbox/earcut) MapBox Original javascript\n- [mapbox/earcut.hpp](https://github.com/mapbox/earcut.hpp) MapBox C++11\n\n\nThanks\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdonbright%2Fearcutr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdonbright%2Fearcutr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdonbright%2Fearcutr/lists"}