Whether this renderable is drawn into a shadow map (§49, §69). Defaults
to true.
floor.castShadow = false; // a ground plane occludes nothing useful
true by default, unlike the light's own castShadow, and the asymmetry
is the point: switching a light on is a decision to pay for a whole extra
pass, while switching a node off is a per-object exclusion. With the
light's flag at false — every scene that predates §69 — these two never
run at all, so the default costs nothing until an author asks for shadows,
and when they do the answer is "everything casts", which is what an author
who just enabled a sun expects to see.
Honoured by the shadow pass for the three surface pipelines (unlit, lit,
standard). A §55 Sprite never casts whatever this says: a depth-only pass
writes geometry, not alpha, so a textured quad would cast its rectangle —
§69's transparent shadow masks are what fix that, and they are staged (see
@fourjs/scene's DirectionalLightShadow).
Whether this node's own drawn shape clips its subtree (§67; R-23,
2026-08-21). Default false.
panel.clip = true; // children are masked to the panel's shape
panel.add(content); // …including everything below them
The mask is the node's own geometry, drawn with colour writes off, so a
§50 Shape clips to its path, a rectangle clips to a rectangle, and a
Sprite clips to its quad — §67's "path masks" and "UI overflow clipping"
with no second authoring surface and nothing new to build a region out of.
@fourjs/render's clip.ts states the tier and what it deliberately leaves
to a later packet (alpha masks, 3D clipping planes, true scissor
rectangles).
The clip gates this node's descendants and not this node, which is the
exact mirror of layers above (§46 gates the node and not its subtree).
That is what a UI panel needs — it paints its own background and border and
then contains its children — and it is why the two fields sit next to each
other with opposite scopes rather than sharing a spelling.
Nested clips intersect: a clipped subtree that contains another clip
draws only where both shapes cover. Eight clips can be live in one frame,
because a stencil bit plane is spent on each and every stencil buffer WebGL
2 can allocate is eight bits deep; the ninth is dropped with a §67
diagnostic, and its subtree spills rather than vanishing (see
ClipPlaneAllocator).
Two conditions make this field inert, both warned about in a development
build and both failing toward drawing rather than toward an empty screen:
a node with no geometry to draw (a plain Group) has no shape to mask
with, and a renderer whose drawing buffer carries no stencil buffer
(new WebglRenderer({ stencil: true }), R-7) has nothing to write a mask
into.
A plain field, like visible and layers and for the same reason: the
render list reads it once per node per frame and there is no value it can
hold that §85 would refuse.
Whether this node participates in simulation and updates (§6). Default true.
Whether this renderable may be dropped from a view whose frustum its world
bounds lie entirely outside (§49, §87). Defaults to true.
skybox.frustumCulled = false; // its bounds do not describe where it draws
true by default because culling is an optimisation that is invisible when
it is right: a view drops draws it could not have shown, and every pixel is
where it was. The flag exists for the cases where the bounds are the
thing that is wrong — geometry a vertex shader displaces, a skybox drawn at
the origin but meant to fill the view, a node whose geometry is a
placeholder for something drawn elsewhere. Setting it false says "trust
me, draw this", and the engine does, in every view.
The bound tested is a world-space sphere derived from §53's cached
local box (see bounds.ts), so it is conservative: an item is dropped only
when that sphere lies wholly beyond one of the six planes. A geometry that
cannot state its bounds — an empty one, or a structurally-typed geometry
with no computeBounds — is drawn whatever this says.
Honoured by view-list.ts's buildViewRenderList, which is what the
WebGL 2 backend derives each view's draws with. §36's particle systems are
exempt whatever their node says, and the reason is on
RenderItem.frustumCulled.
Which §71 hit-test strategy answers for this node, or null — the
default — for §71's own default rule: "the engine should select the
cheapest valid method" (A-11; the field ships with the analytic tier per
the adopted RFC 0005 Q3, 2026-08-29).
mesh.hitTestMode = "geometry"; // exact ray/triangle, never just the box
decal.hitTestMode = "pixel"; // a hit must land on a present texel
hull.hitTestMode = "bounds"; // the box alone, whatever data is attached
boid.hitTestMode = "gpu"; // the id-buffer pass answers, not the ray
The node carries only the choice; the data each strategy needs rides
the Pickable candidate (@fourjs/input may not import @fourjs/geometry or
a render type, plan §3.1 — the recorded reason picking takes a structural
candidate list). Under null, pick() runs the bounding-volume test and
refines it by whatever the candidate carries — triangles, an alpha mask,
or nothing — which is the cheapest method that is valid for that
candidate, and is byte-for-byte the pre-field behaviour. An explicit mode
selects exactly one strategy and requires its data (§85: a candidate
that asked for precision and silently fell back would pick where the
author said not to); "gpu" excludes the node from the ray tier
altogether, because RFC 0005's id-buffer pass is that strategy's
implementation and one pointer event must not resolve the node twice.
The mode gates this node only, not its subtree — the layers scope,
not the clip scope: every descendant is its own picking candidate with
its own mode, exactly as it is its own layer member.
§71's "custom" value is deliberately absent from the union: no
custom-callback strategy exists yet, and a selector value with nothing
behind it would be a silent no-op. It joins HitTestMode with that
strategy.
A plain field, like visible, layers and clip, and for their reason:
pick() reads it once per candidate per pick, and the union is the check —
there is no assignable value §85 would refuse (a §79 document's value is
filtered on read instead, restoring null for anything unrecognized).
ReadonlyidStable identity (§6). Engine-assigned at construction from a monotonic
counter, formatted node-<n>; ids are unique within a process and
ascending in construction order.
readonly, as §6 requires. §79 additionally requires that a deserialized
node keep the id it was saved with ("the engine assigns ids only to newly
created objects"): pass NodeOptions.id to restore one, which also
reserves it against the counter (2026-08-06, A-17 — the note that used to
stand here deferred this to the serialization packet, which shipped without
it).
Which §46 layers this node belongs to, as a bit mask (R-38, 2026-08-08).
Defaults to DEFAULT_LAYER_MASK — the layer named "default", and
nothing else — so a scene that never mentions layers is seen by every
camera and every viewport, whose own masks default to ALL_LAYERS.
node.layers = layerMask("ui"); // one layer
node.layers = layerMask("ui", "debug"); // two
applyLayers(panel, layerMask("ui")); // this node *and* its subtree
The mask gates this node only, not its subtree: a child on another
layer is unaffected by what its parent is on, which is the opposite of
visible/enabled and is a recorded decision — see layers.ts for the
reasoning and for the subtree spelling.
A plain field, like visible and tags, rather than a validated accessor:
the render list reads it once per node per frame, and §85's checks are made
where a mask is built (layerMask) and where one is accepted from
an author (assertLayerMask), not on every read of a hot field.
Not to be confused with Renderable.renderLayer (§49), which is §66's sort
ordinal rather than a membership mask.
Surface appearance (§57). Shared, not owned — see the module header.
Typed by the class's M parameter, which defaults to
SurfaceMaterial — so a plain Renderable still carries exactly the
UnlitMaterial | LitMaterial it carried before §57's base landed, and
renderable.material.setColor(…) still compiles. A subclass that draws
through a different pipeline narrows the parameter instead of
re-declaring the field: §55's Sprite is a Renderable<SpriteMaterial>,
and a family member a consumer writes is a Renderable<TheirMaterial>.
The material's own kind discriminant — not an instanceof, and not the
node's class — decides which pipeline draws it (see render-list.ts).
That is what makes the parameter safe to widen: the render list learns the
pipeline from the material, so a new material type does not need a new
node type.
Free-form user data (§6). Its own object per node.
Human-readable name (§6); not unique and not an identity. Empty by default.
Opacity in [0, 1] (§6). Default 1. Not clamped here.
Whether this renderable is darkened where a shadow map says it is occluded
(§49, §69). Defaults to true, for Renderable.castShadow's
reason.
Honoured by the two shaded pipelines — §57's LitMaterial and §59's
StandardMaterial — because a shadow attenuates a lighting term and an
unlit surface has none to attenuate. Setting it on an unlit or sprite
renderable is therefore accepted and inert, which is the honest reading of
§49 putting the flag on the family root: it is a property of the node, and
whether a given pipeline can act on it is the pipeline's business.
Symbolic drawing group (§46, §66 sort key 1). The primary sort key of the render list: everything in layer 0 draws before anything in layer 1.
A plain number for now. §46 requires human-readable layer names that "compile to efficient masks internally", and that mapping — shared with camera visibility, physics interaction groups, and picking — is a packet of its own; this field is the numeric slot it will resolve to.
Explicit ordering within a layer (§66 sort key 5). Lower draws first; ties
keep scene-graph order (see buildRenderList).
§67 rectangular scissor — a per-draw axis-aligned rectangle in
drawing-buffer pixels, or null to inherit the view scissor only.
Default-off so a scene that never names one issues the same backend
scissor calls it issued before the field existed. The backend intersects
this rectangle with view.rect and restores the view rect after the
draw. Stencil clip = true is a different mechanism and composes: a
node can punch a path mask and restrict its pixels to this rectangle.
Free-form tags, queried by Scene.findByTag (§46). Empty by default.
ReadonlytransformLocal transform (§7). Never replaced — write into it.
Which system owns this node's transform (§42). Default "manual".
§42 spells this as a plain assignable field —
node.transformAuthority = "physics" — and that is exactly what it is:
read it, write it, one value at a time. Every value of
TransformAuthority assigns, "blended" included: it was guarded
by a FourError("NOT_IMPLEMENTED") between WP-2.3 and WP-7.3, because
nothing implemented §19's physics-animation pipeline and a node claiming
it would have been owned by a system that did not exist. PhysicsWorld
now runs that pipeline (@fourjs/physics, plan P7-4), so the guard is gone
and the value means what §42 says it means.
A "blended" node is driven by §19's pipeline, which needs two more things
this field cannot check for — a RigidBody registered with a
PhysicsWorld and a PoseTarget on the node. The first step that
tries to blend a node with no target throws FourError naming it; see
PhysicsWorld.step. Assigning the authority is never itself an error,
exactly as "kinematic" on a node no controller drives is not.
Enforcement lives in the writing systems (see warnAuthorityConflict), not
here: Node records ownership, systems honour it.
Whether this node renders (§6). Default true.
Children in insertion order (§6).
The live array is returned as a readonly view rather than a copy, so
reading children allocates nothing on hot paths. It is a view, not a
snapshot: it reflects later mutations, and casting the readonly away to
mutate it corrupts the parent pointers this class maintains.
The components attached to this node, in attach order (§6a, 2026-08-06 A-15).
A live iterator over the registry's own map, so reading it allocates
nothing and reflects the node's current state; do not attach or detach
while iterating (the underlying Map would be mutated mid-walk). Copy it
first — [...node.components] — for a snapshot.
Enumeration exists for §79. Without it a serializer cannot walk what a node
has; it can only probe for what it knows about, and a component whose
type has no registered serializer is then dropped from the document with
nobody able to notice — a save that silently loses state, which is the one
failure mode that design could not warn about. @fourjs/serialization walks
this getter and refuses an unserializable component by name.
ComponentHost (§6a, @fourjs/core) does not declare it: the host contract
is what a component sees of its owner, and a component enumerating its
siblings is not something §6a sanctions.
Whether Shape2D.dispose has run.
What fills this shape's interior (§50 "fill and stroke", §58) — a
Paint, "inherit" for the material's own colour, or "none".
// §50's own example, in this repository's colour convention
const panel = new Rectangle({
width: 2,
height: 1,
radius: 0.12,
material: new UnlitMaterial({ vertexColors: true }),
fill: { kind: "solid", color: [0.27, 0.4, 1, 1] },
stroke: { width: 0.03, paint: { kind: "solid", color: [1, 1, 1, 1] } },
});
Validated and copied on assignment (§85); assigning rebuilds. "none"
skips the tessellation entirely, which is what makes an outline-only
rectangle cost no fill triangles rather than invisible ones.
A shape whose path is open — Line, Polyline,
Arc, or a PathShape over an unclosed path — fills as if it
were closed, SVG's and Canvas's rule, if you ask it to; the three
stroke-only primitives simply default to "none" so that nobody asks by
accident.
The fill and stroke this shape draws (§53), rebuilt on read whenever a parameter has changed since the last one.
Owned by the shape — do not dispose it yourself, and do not hand it to a
Renderable, which would then draw a geometry that changes under it.
Reading it is cheap: the rebuild is skipped unless something moved.
Overrides Renderable.geometry with a read-only accessor, like
Sprite: a shape derives its geometry from its parameters, so there is
nothing sensible to assign.
Whether this shape derives its material from its §58 paints — true
exactly when it was constructed without a material (see
Shape2DOptions.material). Fixed for the shape's lifetime: the two
tiers resolve different mistakes to different refusals, and a shape that
silently switched tier on a paint write would move its material out from
under the author. The §79 writer reads it to decide whether a material
key exists to write.
This node's parent, or null when it is a root (§6).
Assignment is supported and is defined in terms of the hierarchy methods:
node.parent = other is other.add(node) (including its cycle check and
its detach-from-the-old-parent step) and node.parent = null is
oldParent.remove(node). It is never a bare field write, because a bare
write could not keep the two children arrays consistent.
Alias for this.transform.position — the same live Vector3, never
a copy, so node.position.set(1, 2, 3) and
node.transform.position.set(1, 2, 3) are one and the same write and both
run the transform's change hook (plan D3: the version bumps, dependent
caches invalidate).
The spec writes this spelling throughout — Four.animate(node.position)
(§15), camera.position.set(0, 2, 6) and label.position.set(0, 1.2, 0)
(§97) — while Transform is where the value actually lives (§7). This
getter makes both spellings work, resolving the WP-3.1 ergonomics flag
(TODO: Node.position/rotation/scale aliases onto transform.*).
Getter only, deliberately: Transform.position is a readonly property
holding a mutable vector — replacing the instance would silently drop the
change hook — and the alias mirrors exactly that contract. Assignment
(node.position = v) is not the API; write into the vector with
.set(...) / .copy(...).
Transform authority (§42) is unmoved by the spelling: a write through this alias is a write to the node's transform, and a system that is not the node's Node.transformAuthority must not make it either way.
Semi-axis along the ellipse's own X; strictly positive (§85).
Semi-axis along the ellipse's own Y; strictly positive (§85).
Alias for this.transform.rotation — the same live Quaternion,
never a copy. Radians semantics per §7a; getter only, for the reasons
documented on Node.position (mutate via quaternion methods, never
by assignment). Writes through it bump the transform version exactly as
transform.rotation writes do, and §42 authority applies unchanged.
Alias for this.transform.scale — the same live Vector3, never a
copy. Getter only, for the reasons documented on Node.position
(mutate via .set(...) / .copy(...), never by assignment). Writes
through it bump the transform version exactly as transform.scale writes
do, and §42 authority applies unchanged.
Counter-clockwise tilt of the ellipse's own frame, in radians (§7a, §7b) —
equivalently, where its outline begins, measured from +X. See the class
documentation for why it is not called rotation.
How this shape's outline is drawn (§50, §58's StrokeStyle), or null
for no stroke.
Validated and copied on assignment (§85): what comes back is a fresh
record with every optional field resolved, so shape.stroke.lineJoin
reads "miter" rather than undefined and an in-place edit of the object
you passed cannot desynchronise the geometry from the style that built it.
Assigning rebuilds; there is deliberately no way to mutate a stroke in
place, which is the one place this family does not follow
Polygon.points' announce-it-yourself contract — a stroke is six
fields that have to agree, not one array.
The band is expanded by §52's expandStroke at this shape's
Shape2D.tolerance, from the same flattening the fill uses.
Greatest distance, in world units, between this shape's curves and the polyline that stands in for them (§51 "flatten", §52 "adaptive curve subdivision"). Smaller is smoother and costs more vertices; assigning rebuilds the fill.
It is a world-space length, not a screen-space one: a shape that is
scaled up by its transform, or drawn by a camera that zooms in, does not
re-tessellate. Screen-space tolerance needs a per-view render list (R-8)
and a rebuild inside the frame, which §61 forbids throwing in and §33
forbids making frame-rate dependent.
Attaches nodes as children, in argument order, appending each to the end
of Node.children. Returns this.
Rules:
FourError("INVALID_SCENE_GRAPH"). Nothing is
attached for that argument.removed on the node before added fires.add silently reorder siblings and
emit a spurious removed/added pair; callers that want a new index can
remove then add.)Releases the geometry this shape owns (§83). Idempotent.
The material is not disposed: it is shared, and §83 puts disposal on
whoever created it. After disposal the geometry has no vertices, so a
backend meeting this shape in a render list skips it — the behaviour a
disposed BufferGeometry already produces for a Renderable.
Delivers event to the listeners registered for type in registration
order. Re-entrant emission of type is deferred (rule 4).
The attached component of type, or undefined (§6a).
Writes the world-space unit vector this node faces — its −Z axis — into
out and returns it (§7b's out-parameter convention; nothing allocates).
−Z is the engine's one forward direction, for every node rather than for
cameras alone: a camera looks down it (Camera.viewMatrix,
Matrix4.setPerspective), a directional or spot light shines along it
(§68), and Node.lookAt aims it. The inverse of lookAt, and the
cheapest way to ask where something ended up pointing.
World transforms are resolved on demand (resolveWorldTransform(this)),
exactly as Camera.updateViewMatrix resolves — O(depth), version-cached,
so the render path's prior resolve pass makes this a few comparisons. The
basis column is normalized, so ancestor scale does not stretch the
direction.
A degenerate node — zero scale somewhere up the chain — yields the zero
vector rather than NaN (Vector3.normalize's documented zero-length
behaviour): like a degenerate camera (§47), it poisons nothing downstream
and raises no error on a per-frame path.
Number of live registrations for type (diagnostics, §83).
Number of live listener registrations across every event type (§83).
Used by teardown diagnostics — a detached node that still holds listeners pins its subtree until those listeners are removed.
Turns this node so that its −Z axis points at target, with its +Y
axis as close to up as the aim allows (§44/§47's orientation helper —
"aiming a camera or a light" without hand-composing quaternions). Returns
this.
camera.position.set(0, 2, 6);
camera.lookAt(new Vector3(0, 0.5, 0)); // §7a's world +Y is the default up
sun.lookAt(target, new Vector3(0, 0, 1)); // a light is aimed the same way
target is in world spaceAlways, whatever this node is parented to (decision, R-36). A world-space target is the contract that makes the call mean the same thing when a node is later reparented onto a moving rig — the case §44's follow rigs and spring arms exist for — and it is the only one under which "point the camera at the player" is a single call. The local rotation is derived from it: this node's world position comes from its resolved world matrix, the world rotation is built from the aim, and the parent's world rotation is divided out
localRotation = conjugate(parentWorldRotation) · worldRotation
so a node under a rotated, translated, or uniformly scaled parent aims
correctly. A parent with non-uniform scale has no exact rotation to
divide out; Matrix4.decompose's closest-rotation reading is used, with
the shear it cannot represent left in — the same documented limitation
every decomposition in the engine carries. A parent with a zero scale
component decomposes to the identity, so the aim lands in world terms.
The node's own scale and pivot are irrelevant to the result: only the
rotation is written, through transform.rotation.copy(...), so the version
bumps exactly once (plan D3) and dependent caches invalidate exactly as
they do for any other transform write.
This is an ordinary application transform write: it always lands, the
same as node.rotation.setFromAxisAngle(...). A DEV build warns once if
Node.transformAuthority is not "manual" (warnApplicationTransformWrite);
production is silent. The write is never refused. A rig that wants to aim a
node every frame under a non-manual authority has the same obligation as
any other writer: claim the authority, or wrap with runOwnedTransformWrite.
FourError("INVALID_SCENE_GRAPH") is thrown, and nothing is written, when
target coincides with this node's world position (or either is
non-finite): there is no direction to face; andup is zero, parallel to the aim, or non-finite: the aim leaves the roll
about it completely undetermined — the top-down lookAt from directly
above with the default +Y up is exactly this case, and wants an
explicit up such as world −Z.Refusing rather than substituting is §85's "detect singular transforms"
applied at the point of authoring, reported under the same code
Node.add uses for §85's scene-graph rule so one catch covers both.
Silently picking a fallback up — the usual engine behaviour — would
rewrite the orientation the caller asked for and hide the mistake behind a
plausible-looking roll; leaving the node unturned would be indistinguishable
from a frozen rig. A rig that can reach a degenerate configuration (an orbit
at zero radius, a follow target that catches its camera) must guard the
call, exactly as it must guard a division by its own radius.
Allocates nothing: the aim, the world rotation, and the parent's rotation live in module scratch.
Announces a parameter change the shape could not see — a write into a point
record handed to Polygon, or into the Path a PathShape
was built from. The geometry is rebuilt on the next read of
Shape2D.geometry, not here, so a burst of edits in one frame costs
one rebuild rather than one per edit.
Removes the earliest still-registered registration of listener for
type. Removing a listener that is not registered is a no-op.
Registers listener for type. Returns a function that removes it.
The same listener may be registered more than once; each registration is independent and is delivered once per dispatch.
Registers listener for a single delivery. Returns an unsubscriber.
Detaches nodes from this node, in argument order. Returns this.
Removing a node that is not a child of this node is a no-op — no throw, no event (decision: removal is idempotent, so teardown paths can call it without first testing parentage).
Removes every listener, or every listener of one type. Used by teardown paths that must not leave detached nodes retaining listeners (§83).
Optionaltype: KDetaches component, running its onDetach; returns false when it is
not the component currently attached for its type. Detaching does not
dispose (§6a).
This shape as a §51 path, in the node's own local space — a new path on every call, owned by the caller.
const outline = new Star({ points: 5, innerRadius: 0.4, outerRadius: 1, material })
.toPath();
outline.length(); // perimeter in world units
outline.closestPoint(hit); // §51's query, on the real curve
This is the family's one polymorphic operation and the seam §50's "SVG
import/export compatibility" (R-26) and §51's boolean operations both
need: a consumer of a shape gets vector source data rather than a triangle
soup, and never has to learn which shape it is holding.
A fresh path rather than a cached one because a Path is mutable: handing
out the shape's own would let a caller edit a rectangle into something that
is not the rectangle the shape's parameters describe, silently, until the
next rebuild threw it away.
Calls visitor on this node, then on every descendant, depth-first in
insertion order (§6).
Children are read live by index rather than from a snapshot, so the walk
allocates nothing; the price is that structurally mutating the hierarchy
from inside visitor is not supported (removing the current node's
successor skips it). Collect first, mutate after.
Recursive, so depth is bounded by the JS stack — scene graphs thousands of levels deep are out of scope.
A filled ellipse centred on the node origin (§49, §50 "ellipse").
The tilt is the ellipse's own, about its centre, and is deliberately kept beside the radii rather than pushed onto the node transform: it is a parameter of the shape (§51's
ellipsecommand carries it), so a shape that is a child of something else can be tilted without disturbing the transform §42 says exactly one system owns.It is spelled
startAngle, notrotation, and that is not taste: §6'sNodepublishesrotationas the live alias of its transform's quaternion (the §15/§97 idiom), so a shape parameter of that name would shadow the node's own orientation — the compiler refuses it outright, which is how this was found.startAngleis the family's one name for "where the outline begins, measured from +X": an ellipse's outline begins on its rotated X semi-axis, a regular polygon's at vertex 0, a star's at outer point 0, and a sector's arc at exactly that angle.