Part of the Wrangle reference. Everything on this page goes in the node's Code field, and runs once per element of the current Run Over mode.
Run once: Detail
Run Mode = Detail.
What it is
Set Run Mode to Detail and the code runs exactly once for the whole mesh, single threaded, instead of once per vertex/triangle/texel - elemnum is 0 and numelem is 1. This is what makes side-effecting writes (landmarks, setoutput, setpointpos) safe: there is only ever one run, so there is nothing for two elements to race. The subject Mesh parameter is optional, exactly like Texel: undeclared or unwired, the code still runs; wired, it passes through (or is rewritten when setpointpos is used) and doubles as the "Mesh" donor slot.
clearlandmarks(); addlandmark(pointpos("Mesh", 0), pointpos("Mesh", 0) + pointnormal("Mesh", 0))
Not available
Everything that describes a current mesh element is a positioned compile error here, because Detail has none: P, N, uv, Cd, wmap(...) writes, mask, area, ptnum/numpt/primnum/numprim, @attributes (read one with pointattr("Mesh", "name", i) or surfattr("Mesh", "name", p) instead), foreach over neighbours, and neighbourcount()/neighbour()/pointnormal()/isboundary() (the no-argument form). pointpos(i) and isboundary(p) are legal here so foreach-over-points can read the working copy.
P, N, Cd, wmap(...) writes, mask, @name - all errors
Stays legal
Parameters (bare reads and ch()/chf()/chv()/chi()/chb()), detail constants (need the subject Mesh parameter wired), every donor-mesh function and every texture function - their pins are independent of the run-over, same as in Texel. Every landmark read function above. setoutput() and setpointpos() below. foreach (i in points()). addpoint()/addprim()/removeprim()/removepoint() and foreach (t in prims()) - see the Topology creation/deletion section below.
float t = pointattr("Mesh", "thickness", 0)
setpointpos(i, v)
Write vertex i's position on the Detail working copy. Later pointpos(i) / pointpos("Mesh", i) in the same run see the write - that is what makes foreach (i in points()) { setpointpos(i, ...) } work. Statement-OK. Detail only (Vertex/Triangle are parallel and refuse it). After a successful run, dirty positions are applied to the Mesh output.
foreach (i in points()) { setpointpos(i, pointpos(i) + vec3(0,0,1)) }
clearlandmarks()
Empties the working landmark set's pairs. Splines are untouched. Usable as a bare statement (its return value, if any, is simply discarded).
clearlandmarks()
addlandmark(src, dst)
Appends a pair to the working set and returns its new index. A cap of 16,777,216 pairs makes this a no-op (a counted LimitGuarded guard, not an error) past that point.
float i = addlandmark(pointpos("Mesh", 0), pointpos("Mesh", 0) + pointnormal("Mesh", 0) * 2)
setlandmarksrc(i, v)
Overwrites pair i's source position. Out-of-range i is a counted no-op.
setlandmarksrc(0, pointpos("Mesh", 0))
setlandmarkdst(i, v)
Overwrites pair i's target position. Same out-of-range convention.
setlandmarkdst(0, pointpos("Mesh", 0) + pointnormal("Mesh", 0))
removelandmark(i)
Removes pair i, preserving the order of the rest. Same out-of-range convention.
removelandmark(numlandmarks("Guide") - 1)
Working set
The working set starts as a copy of the first wired Landmarks parameter (Parameters-list order) - its splines carry through unchanged - or empty if none is wired. After a successful run it publishes on this node's primary Landmarks output if one exists (see primary on the Parameters page) - Create pins from code (or the pin editor) adds one when the expression writes landmarks; switching Run Mode does not. The default landmark viewport rendering picks it up automatically. The output rebuilds its map from the flat pair list; since a map keys on the source position, two pairs that end up with the same source position collapse to one - the last write for that position wins, so order your addlandmark/setlandmarksrc calls with that in mind.
setoutput("Name", value)
Writes a Parameters-list value output ("User Outputs", Float/Vector/Int/Bool) by name - Detail mode's counterpart to a named WeightMap output write (wmap("Name") = ...) in the vertex/corner domain. The value must match the declared type: scalar for Float/Int/Bool (Int truncates toward zero, Bool becomes 0/1 by !=0, the same rules chi()/chb() use on the read side), vector for Vector. Writing a name that is not declared is a compile error listing the declared outputs. Multiple writes: the last one wins. Usable as a bare statement.
setoutput("Count", numlandmarks("Guide")); setoutput("Centroid", centroid())
Unwritten outputs
After a successful run, every declared output publishes its pin's own default value if the expression never wrote it - and the node reports a warning naming which ones. Nothing is ever left stale from a previous run.
Topology creation/deletion
Detail Run Mode only.
What it is
addpoint()/addprim()/removeprim()/removepoint() edit the Detail working copy's topology - not just a position or an attribute - immediately, inside the same run. Vertex/Triangle/Corner refuse all four with a positioned compile error: those run-overs are parallel, so a new element's id would depend on what every other element did and could not be the same from one run to the next.
n = addpoint(P + N * 2)
addpoint(v)
Appends a new vertex at v and returns its new id. A cap of 100,000,000 new points per run (counted from the pre-run mesh, not the mesh's own size) makes this a no-op (a counted LimitGuarded guard, not an error) past that point - the addlandmark() precedent. Statement-OK.
float n = addpoint(pointpos(0) + vec3(0, 0, 1))
addprim(a, b, c)
Appends a new triangle over three existing vertex ids and returns its new id, or -1. Any corner that does not name a live vertex, or two corners naming the same vertex, is a counted OutOfRangeIndex and adds nothing. A triangle that would be non-manifold (an edge already shared by two faces) or that already exists is a counted TopologyRefused and adds nothing. A cap of 100,000,000 new triangles per run is a counted LimitGuarded, the addpoint() twin. No group argument - a new triangle always lands in TriangleGroups' default group; assign a real one downstream if you use polygroups. Statement-OK.
float t = addprim(a, b, c)
Winding
The new triangle's face normal follows normalize(cross(Pc - Pa, Pb - Pa)) - the same left-handed convention every other triangle in this engine uses. Wind it like the triangle you copied the corners from; if the result renders inside-out, swap b and c.
float t = addprim(a, c, b) // flipped on purpose
Overlay fill
A freshly appended triangle's UV/normal/colour elements are unset by the engine (every downstream bake silently skips an unset triangle), so addprim() fills them itself: each corner reuses that vertex's first existing overlay element if it has one, or gets a fresh neutral one otherwise (UV zero; normal = the new triangle's own face normal; tangent/bitangent layers, if present, zero - they need a real downstream tangent recompute; colour white).
removeprim(t) / removeprim(t, keep_points)
Removes triangle t; returns 1, or 0 if t already was not a triangle (a legitimate "nothing to remove" answer, the removedelta() precedent - not counted). The one-argument form also removes any corner left referencing no other triangle (which is also what a "blast a region" script's usual intent); keep_points != 0 leaves those corners in place as free points, which round-trip through the Mesh output pin same as any other isolated vertex. Statement-OK.
removeprim(t); removeprim(t2, 1)
removepoint(i)
Removes vertex i and every triangle still touching it; returns 1, or 0 if i already was not a vertex (same "nothing to remove", not counted). No keep_prims form exists: a mesh cannot hold a triangle that references a dangling vertex, so removing the vertex necessarily removes its triangles too. Statement-OK.
removepoint(i)
clearmesh()
Empties every point and triangle from the working copy, one at a time - not the same as an empty mesh from Mesh.Clear(): every overlay's layout (UV/normal layer count, colour layer presence) survives with zero elements in it, so a from-scratch addpoint()/addprim() pass right after this call still has overlays to fill. Cleared points/triangles are counted into the same totals removepoint()/removeprim() report. Has no per-element form (unlike every other function in this section, it refuses with its own message rather than suggesting a foreach loop). Statement-OK.
clearmesh(); n = addpoint(vec3(0,0,0))
foreach (t in prims()) { ... }
Iterate every live triangle id, Detail only - the required companion to removeprim()/addprim(): without it, a removal script has nothing but a raw for (t = 0; t < maxprims(); ...) loop over a ceiling that keeps moving if the body itself adds triangles. The element set is snapshotted once at loop entry, exactly like foreach (i in points()) - a body that adds or removes triangles does not retroactively grow or shrink the current loop. No mesh-pin argument: a donor mesh is never mutated, so there is nothing to iterate there.
foreach (t in prims()) { if (primarea(t) < 0.01) { removeprim(t) } }
Live counts
npoints()/nprims()/maxpoints()/maxprims()'s self forms (a bare call, or an explicit "Mesh" pin name) read the current count/ceiling after any addpoint()/addprim()/removeprim()/removepoint() earlier in the same run - what makes removeprim(t); setoutput("Count", nprims()) tell the truth.
setoutput("PointCount", npoints())
Ids do not survive publish
A vertex/triangle id returned by addpoint()/addprim() (or read via npoints()/pointpos(i)/etc.) is only meaningful for this run. A mesh leaving any pin is always compacted if it was not already, which renumbers every id - so an id you saved into a User Output and expect to feed into a later node's own point-index math will not necessarily name the same element anymore. An isolated point (removeprim's keep_points, or any point with no triangle) is not discarded by that compaction - it round-trips through the Mesh output pin's own orphan-vertex block, just potentially under a different id.
Interactions
Deltas: if this run wrote a Delta entry (setdelta) and also changed the topology and the mesh was not already compact, the node posts a warning - the delta's key no longer names the same vertex once the mesh is renumbered on publish. Skin: points added by addpoint() carry no skin weights - a note recommends Transfer Weights or Bind Skin downstream when the mesh has a skin-weights attribute.