# X_ITE_R browser API — supported and unsupported

Reference: X_ITE 16.5.0 (`oracle/x_ite-pin`, commit `34cdf9f97`). Names are X_ITE's.
"Compared" means a browser test checks the value against a pinned X_ITE recording: `oracle/web-browser-01-baseline` (runtime 01), `oracle/web-browser-02-baseline` (runtime 02, 30 cases), `oracle/web-browser-fallback-01-baseline` (10 cases) or `oracle/web-browser-loadurl-01-baseline` (21 cases), `oracle/web-browser-options-01-baseline` (browser options, properties, rendering properties, print, notification, browser keys; 11 cases), `oracle/web-browser-attributes-01-baseline` (canvas attributes and the options they set, contentScale, maximumFrameRate, update; 10 cases) or `oracle/web-browser-entry-01-baseline` (X3D entry points and `captureStream`; 12 cases).

This is a small subset. It is not full X_ITE API compatibility.
The Rust engine does all X3D work: parsing, events, sensors, picking, navigation and rendering. The JavaScript adapter only forwards page events and calls.

## Supported

### `<x3d-canvas>`

| member | state |
| --- | --- |
| `browser` | supported, compared; read-only like X_ITE's |
| `src` property and attribute | supported, compared |
| `url` property (MFString) and attribute (`"a.wrl", "b.wrl"`) | supported, tested: the first URL that loads |
| `contentScale` attribute | supported, compared: a number, or `auto` for `devicePixelRatio`; the default is 1. The framebuffer is clamped to the device's maximum texture size, like X_ITE's `limitFramebufferSize` |
| `scripts` attribute | X_ITE_R only, default off: `scripts="true"` lets the Script nodes of the worlds that start after it run, with the privileges of the page (see "Script nodes") |
| events `initialized`, `load`, `error`, `shutdown` | supported, order compared (`shutdown initialized load` for a world, `error` for a failure); also for a world an Anchor loads |
| `oninitialized`, `onshutdown` properties | supported (called before the event, like X_ITE). The attribute forms are not read |
| `captureStream (frameRate)` | supported, compared (`oracle/web-browser-entry-01-baseline`): the canvas's `captureStream`, a `MediaStream` with one live `CanvasCaptureMediaStreamTrack` (video, no audio); frame rate 0 and 30 give a track of that rate, and the canvas's errors for -1, `NaN`, `Infinity` and a string are the same |
| `toDataURL`, `toBlob` | present; a WebGPU canvas can read back empty where the browser does not composite it |
| `Symbol.toStringTag` | `"X3DCanvasElement"`, compared |

### Input (the canvas)

| input | state |
| --- | --- |
| mouse move, press, release (Pointer Events) | supported, compared: TouchSensor `isOver`, `isActive`, `hitPoint_changed`, `touchTime`; the nearest of two overlapping sensors; PlaneSensor drags |
| drag outside the canvas | supported: the pointer is captured while a button is down |
| wheel | supported, compared (EXAMINE zoom). `preventDefault ()` only when the viewer navigates |
| double click | forwarded to the viewer (X_ITE's `dblclick`) |
| keyboard (`keydown`, `keyup` on the element) | supported, compared: KeySensor while the canvas has the focus, nothing from other elements; the browser's own keys, see "Browser keys" below. The canvas is focusable (`tabIndex` 0) and takes the focus on a press |
| cursor | supported, compared: `pointer` over a sensor or Anchor, `grabbing` while a drag sensor is active, `move` while the viewer navigates, `default` otherwise |
| Anchor | supported, compared: a `#viewpoint` URL binds the viewpoint; a world URL replaces the world (with the browser events); a page URL (`.html`, `.htm`, `.xhtml`) or a `target` parameter opens it with `window .open` or `location`, without a fetch |
| EXAMINE navigation | supported, compared: rotation (mouse, one finger), zoom (wheel, two-finger pinch). The other viewers of the engine also get the events |
| touch (Pointer Events with `pointerType` touch) | supported, compared: tap on a TouchSensor or Anchor, one-finger rotation, two-finger pinch. Touch moves go to the engine once per frame with all touches, as X_ITE's TouchEvent handlers get them |
| coordinates | compared: X_ITE's `getPointerFromEvent` (framebuffer pixels, origin at the bottom left) at any canvas size, offset and contentScale; there is no DPR 1 assumption |
| `touch-action` | `none` while the world's viewer navigates, `auto` for NavigationInfo `NONE`: the page scrolls and zooms only where the canvas does not take the gesture |

### `X3D` (default export of `x_ite_r.mjs`, also `globalThis.X3D` when free)

| member | state |
| --- | --- |
| `X3D (onfulfilled, onrejected)` | supported, compared: `X3D` is a function of length 2 that returns a Promise, which resolves (with `undefined`) once the document is loaded and every `<x3d-canvas>` has a browser. Both callbacks are optional (non-functions are ignored); an `onfulfilled` that throws reaches `onrejected`. All calls share one inner Promise, but each call returns a new Promise, as in X_ITE. X_ITE_R's own `onrejected` path (a canvas without a browser) cannot happen, so only the throwing `onfulfilled` is compared. `X3D` also has `toString ()` and the tag `"X3D"` |
| `X3D.noConflict ()` | supported, compared: returns `X3D`; when `window.X3D` is `X3D` it restores the value from before the module loaded (or deletes the property when there was none); a second call, or a call while `window.X3D` holds something else, changes nothing. The module sets `globalThis.X3D` only when it is free, so with a prior value it never replaces it |
| `X3D.createBrowser ([url [, parameter]])` | supported, compared: a new `<x3d-canvas>` that is not connected and has a browser; with arguments `browser.loadURL (...args)` is called at once, whether or not the element is attached, and its rejection is not handled (a bad URL or a non-MFString is an unhandled rejection of the page). The element is not added to the page |
| `getBrowser ([selector or element])` | supported, compared; the same lookup as X_ITE (`undefined` when nothing matches) |
| `X3DConstants` | X_ITE's: the access types, the browser events (`CONNECTION_ERROR` is 1001, as in X_ITE), the load states, the field types and the node types of the Core profile; the node types of an external component (Geospatial, Text, Script, NURBS, ...) are added when a world loads the component (its `PROFILE`, a `COMPONENT` statement, or a dependency) or a node of it is first seen, as in X_ITE, whose module of a component adds them as it loads; the pinned X_ITE does not have them all at once (a page that loaded no world has 264 constants, the Full profile adds 150), compared by name (`fieldConstants.constants`; X_ITE numbers them in the order the modules load, which varies from run to run, so the values are not compared) |
| every field class (`SFBool` ... `MFVec4f`, `SFNode`, `MFNode`, `SFImage`, `MFImage`, `SFMatrix3d/3f/4d/4f`, `SFQuaternion`, ...), `X3DField`, `X3DArrayField`, `X3DFieldDefinition`, `FieldDefinitionArray` | X_ITE's own classes, with all their constructors and methods: the generated files of the Script prelude (`crates/x3d-script/src/prelude`, verbatim X_ITE 16.5.0) as one module, `pkg/fields.mjs`; compared |
| `ProfileInfo`, `ComponentInfo`, `ProfileInfoArray`, `ComponentInfoArray` | supported, compared (`tests/browser/scenecreate01.test.mjs`: `profileInfo`, `componentInfo`, `supportedLists`): `name`, `level`, `title`, `providerURL` (enumerable, read-only), `external`, `dependencies`, `providerUrl`, `getTypeName ()`, `typeName`, `toVRMLString ()`; the arrays are read-only and array-like (index, `length`, iteration, `get (name)`, `has (name)`, the non-destructive Array functions; `map`, `filter` and `slice` make an array of the same type of new values, so `map` to anything but info objects throws like X_ITE's). `toXMLString ()` and `toJSONString ()` of these objects are not supported |

### `X3DBrowser`

| member | state |
| --- | --- |
| `name`, `getName ()` | `"X_ITE_R"` (X_ITE: `"X_ITE"`) |
| `version`, `getVersion ()` | `"0.1.0"` (X_ITE: `"16.5.0"`) |
| `description`, `getDescription ()`, `setDescription (value)` | supported, compared (`options01`, case `notification`): the text of the notification, see "Notifications" |
| `currentScene` | supported, compared |
| `addRoute (...)`, `deleteRoute (...)` | supported, compared: those of the current scene, with its errors, returning nothing, as X_ITE. `deleteRoute (route)` with a route throws `Bad ROUTE specification: sourceNode must be of type X3DNode.`: the browser passes four arguments on. Not X_ITE members: `browser .getRoute`, `browser .routes` |
| `getWorldURL ()`, `baseURL` | supported |
| `getBrowserOption (name)`, `setBrowserOption (name, value)` | supported, compared (`oracle/web-browser-options-01-baseline`): the 35 options of X_ITE's `BrowserOptions` in typed fields, so the defaults, the coercion (`SFBool`, `SFInt32`, `SFFloat`, `SFDouble`, `SFString`), the `set_name`, `name_changed` and `AntiAliased` names and the error text of an unknown name are X_ITE's. Some options have an effect (`ContentScale`, `MaximumFrameRate` and `AutoUpdate`, see "Canvas attributes", and those listed in "Browser options the renderer does not honor"); the others are stored and read back, see that list |
| `getBrowserProperty (name)` | supported, compared: the eight constants of `BrowserProperties` (`BINARY_ENCODING` is false, the others true) and the unknown-name error |
| `getRenderingProperty (name)` | `LogarithmicDepthBuffer` (the option's value) and the four derived properties `Shading`, `Antialiased` (alias `AntiAliased`), `ContentScale` and `Multisampling`, compared (`oracle/web-browser-options-01-baseline`): `Shading` is the mapped name (`POINTSET` gives `POINT`, an unknown name `GOURAUD`); `Multisampling` is the option clamped to the samples the device supports (the highest count the WebGPU adapter supports: 4 at the WebGPU baseline, where X_ITE's recording GPU allowed 8; `MaxSamples` itself is not implemented) and 0 while `Antialiased` is false; `Antialiased` is true while the clamped count is above 0, whatever the option says; `ContentScale` is the option, or the device pixel ratio for `auto`. The WebGL-context names throw `X_ITE_R: the rendering property '...' is not implemented.`, see "Not implemented". An unknown name gives X_ITE's error |
| `print (...args)`, `println (...args)` | supported, compared: the arguments joined with a space and logged with `console.log`. X_ITE also appends the text to its `<x3d-console>` elements; X_ITE_R has none |
| `loadURL (url [, parameter])` | supported, compared: MFString type check, Promise, rejection text, world kept on failure; a newer `loadURL` aborts an older one, which then never replaces the world. Each candidate that fails is reported with X_ITE's FileLoader messages (`Couldn't load URL '...', trying URL n of m. ...`, `Couldn't load any of the n URLs, tried in this order:`), in order, by the engine. An abort ends the request in flight, which is reported (`AbortError: signal is aborted without reason`). An empty MFString, or one of empty URLs only, rejects with `Loading of X3D file aborted.`, fires no event and aborts no older `loadURL`, like X_ITE. `parameter` is checked and then ignored |
| `createX3DFromString (x3dSyntax)` | supported, compared: a Promise of the scene after its Inline, texture and EXTERNPROTO loads; the scene is not the world; its `worldURL` is the page's base URL; a syntax error rejects; compared (`sceneCreate.x3dString`, issue #134): the argument is `String (x3dSyntax)` (`undefined`, `null`, numbers and objects are text that no parser takes and reject with `Couldn't parse X3D. No suitable file handler found for '...'.`), and each text form and failure (empty, no header, VRML 1.0, BOM, CRLF, XML, JSON, glTF text, unknown nodes and fields, bad `ROUTE`, an unclosed node) with the rejection text and the logged lines |
| `createScene ([profile, component, ...])` | supported, compared: a Promise of a new empty scene that is not the world (`worldURL` is the page's URL); `replaceWorld` takes it. With no argument the scene has no profile (`null`) and no components; with a `ProfileInfo` (and `ComponentInfo` objects) the scene's `profile` and `components` are the objects given, a component of the same name replacing the one before in place (`updateComponent`). Any other first argument rejects with `Couldn't create scene: profile must be of type ProfileInfo.`, any other component with `Couldn't create scene: component must be of type ComponentInfo.` |
| `getProfile (name)`, `getSupportedProfile (name)`, `supportedProfiles`, `getSupportedProfiles ()` | supported, compared: the `ProfileInfo` objects of the pinned X_ITE's `SupportedProfiles.js` (the engine's table), `MPEG-4` an alias of `MPEG4Interactive`; the same object on each call; X_ITE's errors (`Profile 'x' is not supported.`, `Supported profile 'x' does not exists.`). Needs the engine: call them after `ready ()` |
| `getComponent (name [, level])`, `getSupportedComponent (name)`, `supportedComponents`, `getSupportedComponents ()` | supported, compared: `getComponent` gives a new `ComponentInfo` with the level clamped to 1 ... the supported level (0 or none: the supported level), X_ITE's aliases (`H-Anim`, `TextureProjector`, `ProjectiveTextureMapping`) and its error (`Component 'x' at level 'n' is not supported.`). `supportedComponents` has one component more than X_ITE's, see the known differences. Needs the engine: call them after `ready ()` |
| `createX3DFromURL (url)` | supported, compared: a Promise of the scene of the first url that loads; the world stays, no event fires; the failed candidates are reported with the messages of `loadURL`; every candidate failing rejects with `Couldn't load X3D file.`; a fragment stays in `worldURL`; a url that is not an `MFString` throws at once (not a rejection) |
| `createX3DFromURL (url, node, event)`, `createVrmlFromURL (url, node, event)` | supported, compared (`vrmlURLArguments`, `vrmlURLLoad`, `vrmlURLNodes`): the argument errors are thrown at once (`url must be of type MFString`, `node must be of type X3DNode`, `Unknown field`, `must be a input field`, `must be of type MFNode`), the call returns `undefined`. For a node of the world the scene loads in the world's own runtime (FileLoader of the X3DWorld: the candidates resolve against `baseURL`, each candidate it reaches is requested, the failures are reported with the messages of `loadURL`); after it is loaded (its EXTERNPROTO and Inline loads too) its root nodes are set on the field, so its events and field callbacks come, and the nodes are the world's own. A url that does not load changes nothing. For a node of a scene that is not the world, see the known differences |
| `createVrmlFromString (vrmlSyntax)` | supported, compared: the root nodes as an `MFNode`, made at once (`#VRML V2.0 utf8` is put in front); the parser's warnings and errors are logged and an error is thrown, like X_ITE. The nodes are the current world's, so they can be put into a field of the world (X_ITE's belong to a scene of their own). Needs the engine: call it after `ready ()` |
| `importJS (json)` | supported, compared: a Promise of the scene of X3D JSON (a string or an object); a value that is not X3D JSON gives an empty scene (no rejection), like X_ITE |
| `importDocument (dom)` | supported, compared for an XML string, an `XMLDocument` and an XML element; XML that does not parse, or is not an `X3D` document, gives an empty scene like X_ITE's; `undefined` throws X_ITE's `TypeError`. An `<X3D>` HTML element is read as X_ITE's HTML parser reads it (see "DOM integration"); another element gives an empty scene |
| `replaceWorld (scene)` | supported, compared: `null` or `undefined` gives an empty world; the event order `SHUTDOWN_EVENT shutdown INITIALIZED_EVENT initialized load`, the shutdown in the next frame; a newer `replaceWorld` rejects an older one with `Replacing world aborted.` |
| `firstViewpoint ([layerNode])`, `previousViewpoint ([layerNode])`, `nextViewpoint ([layerNode])`, `lastViewpoint ([layerNode])` | supported, compared (`tests/browser/viewpoints01.test.mjs`): the user viewpoints (those with a description) of the layer, bound in the next frame. `layerNode` is a Layer or LayoutLayer node of the world; a value that is no layer node (a Box, a number, `null`) means the active layer, as `X3DCast ... ?? getActiveLayer ()` |
| `changeViewpoint ([layerNode,] name)` | supported, compared: any viewpoint of the layer; an unknown name, or the name of a viewpoint of another layer, does nothing |
| `viewAll ([layerNode,] [transitionTime = 1])` | supported, compared: with `layerNode` the view of that layer is changed |
| `bindViewpoint (layerNode, viewpointNode)` | supported, compared: binds in the next frame; both arguments are type checked (`Browser.bindViewpoint: layerNode must be of type X3DLayerNode.`, `Browser.bindViewpoint: viewpointNode must be of type X3DViewpointNode.`; a `ViewpointGroup` is not a viewpoint); the layer is only type checked, the viewpoint binds in the layer that holds it, as in X_ITE |
| `isLive ()`, `setLive (value)`, `beginUpdate ()`, `endUpdate ()` | supported, compared: `endUpdate ()` is `setLive (false)`, `beginUpdate ()` is `setLive (true)`. A browser that is not live pauses the TimeSensors of the world (their `isActive` stays `true`, no more `time` or `fraction_changed` events) and the other nodes that follow the live state; field assignments of the page still send their events and the routes still run, and viewpoints still bind. The state outlives `loadURL`: the next world starts paused until `beginUpdate ()`. The canvas keeps drawing. `endUpdate ()` lets go of the buttons and touches held (X_ITE: `getViewer () .disconnect ()`) |
| `activeViewpoint` | supported, compared: the bound viewpoint, `null` for the default viewpoint |
| `addBrowserCallback`, `removeBrowserCallback`, `getBrowserCallbacks` | supported, compared (`routes01.test.mjs`, `routesApi.browserEvent`, issue #135) for `INITIALIZED_EVENT`, `SHUTDOWN_EVENT`, `INITIALIZED_ERROR` and `CONNECTION_ERROR` (a lost WebGPU device, like X_ITE's `webglcontextlost`); `BROWSER_EVENT` never fires, in X_ITE neither (its `callBrowserCallbacks` is never called with it): a callback of every type across `loadURL`, `replaceWorld`, viewpoint binding and `setLive` / `beginUpdate` / `endUpdate` records the same events in both. The registry follows X_ITE's argument counts: `addBrowserCallback` with 2 or 3 arguments only, `removeBrowserCallback` with 1 or 2 only (a 2-argument remove of an unknown event type throws a `TypeError`), a 3-argument add of an unknown event type adds it, and `getBrowserCallbacks ()` with no argument is a `Map` of every registration |
| `getCanvas ()` | supported |
| `dispose ()` | supported: stops the frame loop, removes the input listeners, ends the requests of its worlds and frees the engine |
| `ready ()` | X_ITE_R only: a Promise of the engine and its WebGPU device |

### `X3DScene`

| member | state |
| --- | --- |
| `worldURL`, `getWorldURL ()` | supported, compared |
| `rootNodes`, `getRootNodes ()` | supported, compared (`tests/browser/rootnodes01.test.mjs`): the scene's live `MFNode` field (`rootNodes`, inputOutput), the same object on every read. `push`, `unshift`, `splice`, `pop`, `shift`, a `length` change and `assign` change the world at once (its text forms, and the drawn world from the next frame); its nodes are the `SFNode` objects `getNamedNode` gives. `addFieldCallback`: each change sends one `rootNodes` event in the next frame, in order with the events of node fields; changes before a frame are one event; the callbacks get the live field |
| `rootNodes = value`, `setRootNodes (value)` | supported, compared: `rootNodes .assign (value)`; a value that is not a field (a plain array) throws X_ITE's `TypeError` (`field.getValue is not a function`) and changes nothing |
| `addRootNode (node)`, `removeRootNode (node)` | supported, compared: `addRootNode` pushes a node that is not a root node yet (nothing happens, no event, for one that is); `removeRootNode` assigns the root nodes without every copy of the node, which sends an event also when the node was not there. NULL, `undefined` or a value that is not a node throws X_ITE's `TypeError: Invalid value used as weak map key` |
| `getNamedNode (name)` | supported, compared, including the error text |
| `createNode (typeName)` | supported, compared (`fieldEarly.createNode*`): a new node of a concrete node type, made in the scene and part of nothing until it is added; `Unknown node type 'x'.` for a name that is not a node type or that belongs to an external component the world has not loaded (as in X_ITE). Needs the engine (`browser .ready ()`). Differences: the text form of a node put into the world writes it as `DEF _1 ...` (the node is also the root node of a scene of its own in the engine, so it counts two holders), and a type that only another node holds (`GeoOrigin`) throws `X_ITE_R: a GeoOrigin node cannot be made on its own.` |
| `profile`, `components`, `getProfile ()`, `getComponents ()` | supported, compared (`sceneProfile`, `createSceneInfo`): the scene's `PROFILE` and `COMPONENT` statements (classic VRML, XML, JSON; a VRML97 file is the Interchange profile with the components X_ITE adds, an X3D file without a profile `Full`), the profile as the browser's own `ProfileInfo`, the components as a `ComponentInfoArray` that stays the same object; for a scene of `createScene` the objects it was given |
| `routes` | supported, compared: a live, read-only `RouteArray` (`length`, index, iteration, `has (id)`, `get (id)`, `getTypeName ()`; `X3D .RouteArray`), the routes in the order they were added, the parsed ones first. The same scene gives the same array |
| `addRoute (sourceNode, sourceField, destinationNode, destinationField)` | supported, compared (`tests/browser/routes01.test.mjs`): returns the `X3DRoute`; a route that exists (any spelling of the field names: `translation`, `translation_changed`, `set_translation`) returns the route it is. A bad node throws `Bad ROUTE specification: source node must be of type X3DNode.` (or `destination node`); a field that does not exist throws `Unknown field 'x' in node class T.`; a type mismatch or a source that is no output or a destination that is no input throws `Bad ROUTE specification: ...`. Nothing is logged. Events over the route go in the same frames, and in the same order, as those of a parsed `ROUTE`. Extra arguments are ignored |
| `getRoute (sourceNode, sourceField, destinationNode, destinationField)` | supported, compared: the route or `undefined`; checks as `addRoute`, with `sourceNode must be of type X3DNode.` (no space) for a bad node |
| `deleteRoute (route)`, `deleteRoute (sourceNode, sourceField, destinationNode, destinationField)` | supported, compared: with four arguments the route is found as `getRoute` finds it, and the errors are `getRoute`'s; anything that is not a route of this scene (another scene's route, a deleted one, a node, `undefined`) does nothing. The route object of a deleted route keeps its nodes and prints the field names it was given. Returns `undefined` |
| `protos`, `externprotos`, `getProtoDeclarations ()`, `getExternProtoDeclarations ()` | supported, compared (`tests/browser/fields05.test.mjs`, issue #137): live `ProtoDeclarationArray` and `ExternProtoDeclarationArray` (`length`, index, iteration, `has (name)`, `get (name)`, `getTypeName ()`), the same object on every read, in the order of the scene; `X3D .ProtoDeclarationArray` and `X3D .ExternProtoDeclarationArray` |
| `getProtoDeclaration (name)`, `getExternProtoDeclaration (name)` | supported, compared, including the error texts; the declaration object is the same one on every call and in the array |
| `addProtoDeclaration (name, proto)`, `updateProtoDeclaration (name, proto)`, `removeProtoDeclaration (name)` and the `ExternProto` forms | supported, compared: the argument checks and error texts of `X3DExecutionContext.js`, the warnings of an add or update (a name that a built-in node has, an extern proto it overrides) in the console, a removed declaration stays usable, `update` renames in place |
| `getUniqueProtoName (name)`, `getUniqueExternProtoName (name)` | supported, compared |
| `X3DProtoDeclaration`, `X3DExternProtoDeclaration` objects | supported, compared (`X3D .X3DProtoDeclaration`, `X3D .X3DExternProtoDeclaration`): `name`, `isExternProto`, `fields` / `getFieldDefinitions ()`, `getUserDefinedFields ()`, `getField (name)`, `getType ()`, `getTypeName ()`, `getExecutionContext ()`, `getBrowser ()`, `appInfo`, `documentation`, `canUserDefinedFields ()`, `getCloneCount ()`, `newInstance ()`, `createInstance (scene)`, `requestUpdateInstances ()`, `updateInstances ()`, `addUserDefinedField (accessType, name, field)`, `removeUserDefinedField (name)`, `toVRMLString ()`, `toXMLString ()`, `toJSONString ()`; a PROTO also `getBody ()`, `getProtoDeclaration ()`; an EXTERNPROTO also `urls`, `loadState`, `loadNow ()` (a Promise), `unloadNow ()`, `getProtoDeclaration ()` (the loaded PROTO, or `undefined`) |

### `X3DRoute`

| member | state |
| --- | --- |
| `sourceNode`, `destinationNode`, `sourceField`, `destinationField` | supported, compared: the nodes as `SFNode`, the fields as `translation_changed` / `set_translation` for an inputOutput field and as declared otherwise; after `deleteRoute` the names as given to `addRoute` or the `ROUTE` statement. Enumerable accessors on the prototype, read-only |
| `getTypeName ()`, `getId ()`, `getSourceNode ()`, `getSourceField ()`, `getDestinationNode ()`, `getDestinationField ()`, `getExecutionContext ()`, `dispose ()` | supported (the types are compared; `getId ()` is the engine's id of the route, a number; `getSourceNode ()` and `getDestinationNode ()` give the `SFNode`, where X_ITE's give the internal node; `dispose ()` is `deleteRoute`) |
| `toVRMLString ()`, `toXMLString ()`, `toJSONString ()` | throw `Source node does not exist in scene graph.`, as X_ITE's do for a route alone, compared |
| `X3D .X3DRoute` | supported (`instanceof` works) |
| `toVRMLString ()`, `toXMLString ()`, `toJSONString ()` | supported, compared as exact text (`tests/browser/scenetext01.test.mjs`): the scene of `createX3DFromString`, the `currentScene` after `loadURL`, and the empty world of a canvas before any load (no PROFILE line in the VRML text, `Full` in XML and JSON). No options (see below) |

### `SFNode`

| member | state |
| --- | --- |
| `getNodeTypeName ()`, `getNodeName ()` | supported, compared |
| the SFNode | X_ITE's own SFNode and its proxy (`getNodeTypeName`, `getNodeName`, `getNodeType`, `getFieldDefinitions`, `getField`, `equals`, ...); compared |
| `node.field` read | supported, compared: `field .valueOf ()` as in X_ITE: numbers, booleans and strings as JavaScript values, every other type as the node's own field object, which is live. An inputOnly field reads `undefined` |
| `node.field = value` | supported for every field type, compared: X3DField.setValue. The setter returns `false` for an outputOnly field, like X_ITE's proxy |
| live field objects | supported, compared: one object per field; a changed component or element (`node .diffuseColor .r = 1`, `node .point [1] .x = 5`, `push`, `length`) reaches the node and its routes in the next frame; the engine's changes reach the object after each frame |
| `getField (name)` | X_ITE's X3DField with all its methods, and X_ITE's deprecation warning; compared |
| `addFieldCallback`, `removeFieldCallback`, `getFieldCallbacks` | supported, compared: each event of the field calls the callbacks with the value of that event, in event order. Inside a callback that runs in the route chain these work: the field operations, `rootNodes` changes, `setLive`/`beginUpdate`/`endUpdate` (the engine's live state changes when the chain ends) and `dispose ()` (the engine is freed when the chain ends); other browser calls throw (issue #202, `tests/browser/reentry01.test.mjs`) |
| `getFieldDefinitions ()`, `getFieldDefinition (name)` | supported, compared: a `FieldDefinitionArray`; a predefined field's definition holds the class's default value, a user-defined field's definition the field |
| `toString ()` | `TypeName { }`, compared |
| `toVRMLString ()`, `toXMLString ()`, `toJSONString ()` | the node in full, also for `MFNode` and `X3DScene .rootNodes`, with DEF and USE; compared (`tests/browser/fields02.test.mjs`) |
| `fromString`, `fromVRMLString`, `fromXMLString` of every field class | supported, compared (`tests/browser/fields02.test.mjs`): values, errors (`Couldn't read value for field '...'.`) and the nodes of a value |

### Resource loading

The world of `loadURL`, Inline, ImageTexture, EXTERNPROTO, FontStyle and FontLibrary font files, MovieTexture, AudioClip and BufferAudioSource files load with the page's `fetch` (`cache: "default"`, an `AbortSignal`), without blocking the page. A `data:` URL is decoded by the engine, with no request, as the browser's `fetch` decodes it for every kind of file: `;base64` in any letter case, percent escapes, and a fetch error (`TypeError: Failed to fetch`) for base64 that does not decode. Raw tab, LF and CR characters follow Chrome 156: they are kept (as `%09`, `%0A`, `%0D`) when the URL starts with `data:` in any letter case, after any leading tabs and newlines, and removed otherwise (a leading space, a tab in the scheme), as the URL standard removes them in every other URL. Chromium 151 kept them only after an exact lower-case `data:`. A world or Inline document that is not base64 is read as X_ITE's FileLoader reads it: only a lower-case `;base64` is base64, and one malformed escape keeps the text undecoded.
The engine runs every fallback chain, also the world's; the JavaScript adapter only prints the engine's messages.
A world waits for its loads before it replaces the current world, like X_ITE's scene `loadCount`; the current world runs meanwhile.
Compared: relative URLs, HTTP errors (`Error: Not Found`), data that is no X3D or image, MFString fallbacks in order, cross-origin requests without CORS (`TypeError: Failed to fetch`), EXTERNPROTO fallbacks and waiting, and the console messages and their levels.
Tested: requests that end after a newer `loadURL`, after `dispose ()` or after the element left the document never reach another world.
Compared (`tests/browser/fallback01.test.mjs`): after a candidate that downloads but does not load (no parser takes it, it does not parse, an image does not decode, a glTF buffer fails), the next candidate is fetched then, in order, with X_ITE's messages; a candidate after the one that loads is never fetched. The external buffers of a glTF file are requested together and resolve against the glTF file. A load waits for its responses and never blocks the page.
Compared (`tests/browser/loadurl01.test.mjs`): the candidates of the world-level `loadURL` and their messages, also when a newer `loadURL`, `replaceWorld` or `dispose ()` comes while one is in flight; a FontStyle font file that is missing or no font, then one that loads; a MovieTexture GIF or video that fails, then a GIF; an AudioClip file that does not decode, then one that does, and when none does. Each of these waits for the response of the next candidate, also when it is held.

MovieTexture media (issue #139): a GIF or an APNG is decoded by the engine (`x3d-runtime`, the `image` crate, the same code as the native build). A candidate that is no GIF or PNG (X_ITE's test is the same: the path ends in `.gif` or `.png`, or a `data:image/gif` or `data:image/png` URL) goes to a `<video>` element of the page (`web/video_mirror.mjs`; the engine asks with `WebBrowser.takeVideoOps`, the page answers `videoLoaded`, `videoFailed` and `videoFrame`). The element makes the request (X_ITE's `loadVideo`: `crossOrigin = "anonymous"`, `preload = "auto"`, `playsInline`), so a file that is no video, or that is missing, fails on the element's `error` or `abort` event with X_ITE's messages (`Error loading movie '<url>': Couldn't load video '<url>': error.`), and the next candidate is requested only then. The node's `width`, `height`, `colorDepth` (3), `hasSound` and `duration_changed` come from the element; `set_start` sets `currentTime = 0` and plays, `set_pause` and `set_stop` pause, `loop` and `speed` set the element's. Compared (`tests/browser/movie01.test.mjs`): the color of a GIF, an APNG, a WebM and an MP4 early in each of four four-second frames (and one loop later; a sample counts only when its whole capture was inside its frame; the two video frame cases are skipped when `CI` is set, because the hosted runner's video decode exceeds the window, and run locally), a GIF that does not loop and one at half speed, and the candidates after a video that is no video, that is missing, and that plays.

### Sound

Compared (`tests/browser/sound01.test.mjs`, `oracle/web-browser-sound-01-baseline`, issue #140): an AudioClip that plays through a Sound node. The audio graph of the engine's Web Audio port (`x3d-runtime/src/sound`) has the same nodes, channel settings, gains, buffer, loop attributes, `start` arguments and connections as the Web Audio graph of X_ITE in the page. The engine gives this graph to tests as JSON (`audioGraph ()` of the wasm object, `Symbol.for ("X_ITE_R.engine")`).

Also compared in the same suite: a clip stopped by `stopTime` (`isActive` FALSE, the source stopped); `loop` turned off while it plays (the clip ends); the order of the `isActive`, `isPaused` and `duration_changed` events over start, pause, resume and the end; an AudioClip whose url does not load (the console messages, the fields and the graph); a world replaced while the clip plays (the source leaves the graph, the page keeps its context); and the Sound fields `intensity`, `location` and `spatialize` (the gains of the graph).

The page makes an `AudioContext` when the first Sound node of a world makes the engine's audio context (X3DSoundContext.getAudioContext), and calls `resume ()`, as X_ITE does. It calls `resume ()` again while the context waits: on the canvas events of X_ITE's list (a user gesture unlocks it under the browser's autoplay policy) and when a sound source starts or resumes its media (X3DSoundContext.startAudioElement). Before each frame the engine takes the context's `currentTime` as its audio clock; the engine keeps the latest one, and a world that `replaceWorld` installs while the canvas is detached starts with it, not with 0. The same `AudioContext` serves every world of the browser, and `dispose ()` leaves it running (X_ITE does not close it either). So a clip that does not loop ends when the audio clock passes its duration, a paused clip resumes at its position, and while the autoplay policy holds the context, the clock and the clips wait, as in X_ITE. The engine records each call it makes on a Web Audio node (`x3d-runtime` `sound/ops.rs`); the page makes the same call on a real node of its AudioContext (`web/audio_mirror.mjs`), after each frame, `replaceWorld` and option change, so the nodes are browser nodes that reach the context's destination and the page plays the sound, as X_ITE's. Slot 0 of each engine is the page's one `destination`. A world that goes away (`replaceWorld`, a discarded load) stops and disconnects its nodes. An AudioBuffer that the engine decoded from a file goes to the page as that file, and the page decodes it itself (`decodeAudioData`), so it is resampled once, to the page context's rate, as in X_ITE (copying the engine's 48 kHz samples made a looping clip about 5% quieter in a 44.1 kHz context); a buffer made by `createBuffer` goes as its samples. The engine's own graph stays the record of the calls (`audioGraph ()`, the tests); it renders nothing in a page.

Also compared with a pinned recording (`tests/browser/sound03.test.mjs`, `oracle/web-browser-sound-03-baseline`, 23 cases, 15 identical X_ITE 16.5.0 runs in three processes): `Mute` and `SoundIntensity` (the gain of the sound destination after each change, and set before the world loads), an AudioClip's `pitch` (also changed while it plays; negative gives rate 0), the Sound ranges (`minFront`/`maxFront`, `minBack`/`maxBack`, on the limits, spatialized), `priority` (stored; the graph does not change), the audio listener (default, ListenerPointSource fixed and tracking the view), the page's AudioContext over a load, a gesture, two `replaceWorld` calls and `dispose ()`, an AudioDestination node, and a world replaced by another playing world. The start offset of the replacement clip is a race in X_ITE (0 or a few samples), so that field is not compared. `tests/browser/sound02.test.mjs` keeps the checks of the engine against values worked out from the X_ITE source, and the checks of the page's clock and `dispose ()`. These tests read the graph; nothing in them is heard.

**Audible output (issue #140).** `tests/browser/sound04.test.mjs` (`oracle/web-browser-sound-04-baseline`, 11 cases, 8 pinned X_ITE runs) taps the page's AudioContext destination and compares the level with X_ITE within 0.005 (the largest spread of X_ITE's own runs was 0.003). The level depends on the machine (the AudioContext's sample rate follows the audio output), so each environment has its own pinned reference: the owner's machine `sound04-baseline.json`, the GitHub Actions runner `sound04-runner-baseline.json` (pinned X_ITE measured there): a looping clip, `Mute` (after the start and before the world loads), `SoundIntensity`, `Mute` cleared, a clip stopped and started again, a world replaced by an empty and by a playing one, and an AudioDestination with an OscillatorSource. The graph (`audioGraph ()`) is compared as before. Not mirrored on the page: the data of an AnalyserNode (its `get*Data` fields still come from the engine's own, unrendered analyser), the microphone, and the `MediaStream` of a StreamAudioDestination.

## Known differences inside the supported part

Each difference below is listed in `KNOWN_DIFFERENCES` of `tests/browser/api.test.mjs` or `tests/browser/runtime02_harness.mjs` when a compared case shows it.

- Field callbacks run after the frame that processed their events, not inside its event cascade. A callback gets the value of its event, as in X_ITE; a field it sets sends its event in the next frame, where X_ITE's goes on in the same cascade. A callback that throws is an uncaught page error, as in X_ITE: the callbacks and events after it never run, and the engine processes no event again (the frames go on). Registering a callback while another runs, and removing one in place, follow X_ITE's Map semantics. Compared in `tests/browser/fields04.test.mjs`. Refs #137.
- A node can only be assigned to a field of a node of the same world (a scene of `createX3DFromString` is a world of its own). X_ITE allows it: the node keeps its own scene, and the field holds it. X_ITE_R refuses the assignment, prints the warning `X_ITE_R: a node can only be used in the world it belongs to.`, and leaves the field as it was. The same holds for a node list (`MFNode`), which is refused as a whole, and for the scene's `rootNodes` (`push`, assignment, `addRootNode`, `setRootNodes`) with a node of another world: the warning is printed, the page's field keeps the value it was given, and the world keeps the root nodes it had (`s04`). As in X_ITE, an `SFNode` setter keeps a plain JavaScript property when it fails, so the page reads the node back from that property. The text forms and the field value do not show it. An operation on the field after a refused one goes on (the engine keeps its old value, and a text form shows it, see `s13`). A node of a replaced world is refused as stale, where X_ITE keeps it usable. Refs #137, #145. Compared in `tests/browser/fields03.test.mjs` (12 compared scenarios; the cross-scene `ROUTE` scenario, s06, is compared in `routes01.test.mjs` with `addRoute` of #135).
- A route belongs to one world (the engine runs one world at a time). X_ITE puts no limit on the scenes of the nodes of a route: `scene .addRoute (nodeOfThisScene, ..., nodeOfAnotherScene, ...)` works and carries events between the scenes, and `other .addRoute (nodeOfThisScene, ...)` puts a route of the other scene on nodes of this one. X_ITE_R refuses each of these (`addRoute`, `getRoute`, `deleteRoute`, `browser .addRoute`) with the warning `X_ITE_R: a node can only be used in the world it belongs to.` and the same error as a node of another world in a field, so a page that routes between scenes does not work. This closes the deferred cross-scene `ROUTE` scenario (s06) of `fields03`: compared in `tests/browser/routes01.test.mjs` (`routesApi.crossScene`). X_ITE also runs the events of a scene that is not the world (a `createX3DFromString` scene), so a route inside such a scene carries events; X_ITE_R steps only the world, and the routes of another scene carry none (`routesApi.scenes`, `events`). The routes of a replaced world: `routes`, `addRoute` and `getRoute` of its `X3DScene` throw the stale-world error, where X_ITE keeps them usable (`routesApi.browser`). `X3DScene .toVRMLString ()` is not supported, so a world's `ROUTE` statements cannot be printed. Refs #135.
- The text forms of nodes and scenes take no options: `toVRMLString ()`, `toXMLString ()` and `toJSONString ()` (of an `X3DScene` too) print with X_ITE's default generator options, and the options object (`style`, `indent`, ...) is ignored. `toString ()` needs none: X_ITE's `X3DNode .toStream` prints `TypeName { }`.
- `X3DField .fromVRMLString` and `fromXMLString` take an `X3DScene` of this adapter as `scene`; the nodes of the value are made in that scene's world. `fromJSONString` calls `fromVRMLString`, as in X_ITE. A value read without a scene works for every type that needs no node, in a page where the engine is loaded (use `browser .ready ()`). Before that it throws `X_ITE_R: the engine is not loaded yet; wait for browser .ready ().`.
- Removing the element from the document stops the frame loop and releases the buttons and touches that are down; adding it again restarts the loop. X_ITE keeps drawing, and its document `mouseup` listener releases later.
- A `pointercancel` (for example a touch the browser takes for scrolling) releases the touch: the sensor or the viewer ends its drag. X_ITE has no `touchcancel` handler, and its TouchSensor stays active.
- After the element is removed and added again while the pointer did not move, a TouchSensor stays `isOver`. X_ITE ends `isOver`, because its release checks the pointer against the detached canvas (an empty rectangle).
- A hidden page (`visibilityState` hidden) cancels the frame loop, and showing the page starts it again. X_ITE keeps its loop scheduled and relies on the browser to pause `requestAnimationFrame`.
- A URL is requested once per world, also when several nodes use it. X_ITE sends one request per node (the HTTP cache may answer).
- A `data:` URL whose base64 does not decode gives the same message as in X_ITE, without Chromium's own `Failed to load resource: net::ERR_INVALID_URL` line before it: X_ITE_R makes no request for it.
- An empty document (`data:model/vrml,`, or an empty file) is an empty world. X_ITE rejects it; in the recording page it fails with `ReferenceError: CharLS is not defined` (DICOMParser).
- An image that does not decode fails with `Couldn't load image '<file URL>': error.`. X_ITE names the blob URL of the data.
- A world with an Inline whose glTF file has a buffer that fails finishes loading. In X_ITE the world never does, also when the Inline loads its next candidate: GLTF2Parser does not remove the failed buffer from the scene's loading objects, so `loadURL` never settles.
- A newer `loadURL` ends the loads of the older world. X_ITE's Inline FileLoaders of the older world go on: a candidate that responds after the replacement and does not parse is reported, and the next candidate is fetched.
- After `dispose ()` X_ITE draws a frame and throws `Couldn't create frame buffer.`; X_ITE_R's frame loop has stopped.
- `loadURL` rejects with `Loading of X3D file aborted.` after `dispose ()`. X_ITE's never settles. When a `loadURL` is in flight, `dispose ()` sends the `shutdown` event (callbacks and DOM event) one frame later, as X_ITE's `replaceWorld` does when the file arrives (compared, `loadURL.disposedWhilePending`); with no load in flight neither engine sends it. `dispose ()` unloads the world: its Scripts get `shutdown ()` (see "Script `shutdown`" below).
- A scene does not wait for its FontStyle font files or for an AudioClip's decode: it can go live while a font file or the next audio candidate is still in flight. The FontStyle and the AudioClip wait for their responses and complete later. In X_ITE these nodes hold the scene's loadCount, so `createX3DFromString` and `loadURL` settle only after them.
- An Anchor or `loadURL` never opens a `javascript:`, `vbscript:`, `ecmascript:`, `vrmlscript:` or `data:` URL in a window or as the page location, in any letter case; it logs `X_ITE_R: Anchor URL '...' was not opened.`. X_ITE checks for script URLs in lower case only and opens the others. A new window opens with `noopener`.
- The browser fires its first `initialized load` (the empty world) when the engine is ready, which is asynchronous (WebAssembly and the WebGPU device). Use `browser.ready ()` or wait for the `load` of your world. X_ITE fires nothing for its first empty world, so the events of an element made by `X3D.createBrowser ()` start with this `initialized load` in X_ITE_R and are only the world's own in X_ITE (also with a URL, a bad URL and a wrong argument; recorded in `entryPoints.createBrowser*`).
- A node handle belongs to one world, and so does an `X3DScene` object. After the world is replaced (also by an Anchor), using an old node or scene throws `X_ITE_R: the node belongs to a world that was replaced.`; `currentScene` gives the new scene. In X_ITE the old objects stay usable but are no longer the world.
- The engine releases a node when its scene is disposed (an Inline unloads) and nothing refers to it any more, and a later node can take its id. Using a node object of a released node then throws `X_ITE_R: the node was released; it is no longer in the world.`, and its field callbacks get no more events; a node that takes the id is a new node object. In X_ITE the old node object stays usable.
- Field values the page assigns are checked: data of the wrong shape (too few components, a number for a boolean) or a node that is not in the world throws instead of being filled in.
- `ready ()` is not an X_ITE name. X_ITE starts synchronously and needs no such call.
- Before `browser .ready ()` (a browser from `X3D .createBrowser ()`, attached or not): X_ITE works at once, X_ITE_R's engine is asynchronous. Without the engine the page's classes work (`new X3D .SFVec3f`, `X3D .SFVec3f .fromString`, `X3DConstants`, a scene's `rootNodes`, `currentScene`, the options, `createScene`, `loadURL`, `toVRMLString`, the same results as X_ITE), and `createNode`, `createVrmlFromString` and a node-field assignment throw `X_ITE_R: the browser is not ready; await browser .ready () first.` (`createVrmlFromString`, `createNode`) or a TypeError on the missing node (the lines of the case that use the node it returned). `X3D .SFNode .fromString` throws X_ITE's `createNode` TypeError in both. Recorded in `fieldEarly.detached` and `fieldEarly.attached`; the calls after `ready ()` are equal. Large difference: a page must `await browser .ready ()` before `createNode`.
- PROTO and EXTERNPROTO declarations (issue #137, `x_ite_r.mjs`): a declaration belongs to the world of its scene and stops working with it (`X_ITE_R: the node belongs to a world that was replaced.`). A declaration of another scene passed to `add...` or `update...` is refused with X_ITE's "does not belong to this execution context" text. The `appInfo` and `documentation` of a declaration are kept by the page and written into its XML and JSON text. The field objects of `getField (name)` and `getUserDefinedFields ()` are copies of the declaration's defaults: writing one does not change the declaration. `getBody ()` is a read-only view (its `rootNodes` and `namedNodes` are copies, its text is the body's alone); the body is not an editable `X3DExecutionContext`. `addUserDefinedField` and `removeUserDefinedField` on an EXTERNPROTO do nothing (X_ITE changes the extern proto's own interface). After an interface change the instances of the declaration take it in the call (`addUserDefinedField` updates them at once) or with `updateInstances ()` / `requestUpdateInstances ()`; the compared case reads the instances' new fields right after the add.
- `scene .addImportedNode`, `updateImportedNode`, `renameImportedNode`, `removeImportedNode`, `getImportedNode`, `getImportedNodes ()` / `importedNodes`, `getLocalNode` and `getUniqueImportName` are supported and compared (`fieldEarly.importedNodes`, issue #137, a world with two Inline nodes that load `data:` files with EXPORT statements): the argument checks and the error texts in X_ITE's order, the record of a replaced name going to the end of the list, `IMPORT` lines in `toVRMLString ()`, a live `ImportedNodesArray` of `X3DImportedNode` items (`inlineNode`, `exportedName`, `exportedNode`, `importedName`, `instance`, `description` and its setter, `getExecutionContext`; an update makes a new item, a rename keeps it), and the instance node, which delegates `getNodeTypeName`, `getField`, field reads and writes and `getFieldDefinitions` to the exported node of its Inline and keeps its own name. `X3DImportedNode.getInlineNode ()`, `getExportedNode ()` and `getInstance ()` give the SFNode where X_ITE gives its internal node, and `ImportedNodesArray`'s private mutators are not provided. Not compared: routes that end at an imported instance from the page (the native suites cover them), and an Inline that is not loaded yet (`exportedNode` throws `Exported node '...' not found.` as in X_ITE, but its timing is not recorded). The PROTO and EXTERNPROTO declaration methods (`getProtoDeclaration`, `addProtoDeclaration`, `updateProtoDeclaration`, `removeProtoDeclaration`, `getProtoDeclarations`, `protos`, and the EXTERNPROTO forms with `getUniqueProtoName`) are not in X_ITE_R's `X3DScene`: they need `X3DProtoDeclaration` and `X3DExternProtoDeclaration` objects (interface fields, `body`, `isExternProto`, `loadState`, `newInstance`, `toVRMLString`) and the `ProtoDeclarationArray` and `ExternProtoDeclarationArray` lists, none of which exist in the page, and their `add` and `update` take declarations that only a page-built `X3DProtoDeclaration` could supply; that is a separate lane. `addNamedNode`, `updateNamedNode` and `removeNamedNode` are supported and compared (`fieldEarly.namedNodes`); so are `addExportedNode`, `updateExportedNode`, `removeExportedNode` and `getExportedNode` (`fieldEarly.exportedNodes`). `createProto (name)` makes an instance of a PROTO or EXTERNPROTO the scene declares (`fieldEarly.createProto`; the unknown-name error is X_ITE's). `getNamedNodes ()` / `namedNodes` and `getExportedNodes ()` / `exportedNodes` give live `NamedNodesArray` and `ExportedNodesArray` objects (the same object each call, index access, `length`, iteration, the non-destructive Array methods, `has`/`get`, read-only; `filter`, `map`, `slice`, `toReversed`... give a new list of the same class) with `X3DExportedNode` items (`exportedName`, `localNode`, `description` and its setter, `getExecutionContext`, `toVRMLString`, `toXMLString`, `toJSONString`), compared in `fieldEarly.namedLists`. `X3DExportedNode.getLocalNode ()` (X_ITE returns its internal node) and the lists' private mutators (`add`, `update`, `remove`, `clear`, `assign`) are not provided. X_ITE's check for an imported-node instance in `updateExportedNode` and `addNamedNode` is now reachable from a page (an instance from `getImportedNode`); the engine refuses it for `updateExportedNode` with X_ITE's text, `addNamedNode` and `updateNamedNode` do not check it yet and are not compared with an instance.
- The `layerNode` of the viewpoint functions is a node of the current world: a layer node of another world, or of a replaced one, counts as no layer node (the active layer; `bindViewpoint` throws its layer error). X_ITE casts a node of any scene.
- Sound (issue #293, compared in sound01 `replace` and sound03 `lifecycle`, `audioDestination`; no difference left): after `replaceWorld` the page's audio context stays with the destination node alone in its graph, as in X_ITE (`audioGraph ()` gives that destination-only graph while the page has a context and the new world has no Sound node of its own). `dispose ()` leaves the page's AudioContext running, as X_ITE does (its browser has no step that closes it; the nodes of the worlds are disconnected). An AudioDestination node feeds a `MediaStreamAudioDestinationNode` whose stream an `<audio>` element plays, and starting it calls the context's `resume ()` again, as X_ITE's `startAudioElements` does while the first call is pending. Cost of the left-open context: a page that removes and makes many `<x3d-canvas>` elements keeps one AudioContext per element that made sound, as it does with X_ITE; browsers cap the number of live contexts.
- AudioClip start timing (issue #262, compared in `soundMore.loadTimeEvents` and `soundMore.routeStart`, no difference): for a clip with `loop TRUE` that starts while its world loads, a Script on routes sees `isActive true`, `duration_changed`, `elapsedTime` in that order, in X_ITE and in X_ITE_R (5 identical runs). A clip that does not loop and is started by a TimeSensor route (`cycleTime` to `startTime`) becomes active and ends (`isActive true`, `isActive false`) in both. The elapsedTime gap of the two-canvas native cases came from the harness (the virtual `Date.now` was installed after the load); see `crates/x3d-viewer/tests/multi_viewer_b08.rs`. ISO/IEC 19775-1 V4.0 (local) has no clause for the order of these events across a decode; ISO/IEC 19775-2 was not available (unverified).
- GaussianSplats sort timing (issue #69, a documented difference, not parity; the engineering decision is to keep the synchronous sort; compared in `splatSort.flip`, `burst` and `back`, recording `oracle/web-browser-splat-sort-01-baseline`, 5 identical runs): X_ITE sorts the splat indices in a worker, one sort in flight, and draws the frame of a view change with the last sorted order; when the worker answers it uploads the indices and draws again (recorded trace: `post sort`, `draw 2`, `recv sorted`, `draw 2`). X_ITE_R sorts in the frame that draws, so that frame has the new order. The pictures once the sorts are done are equal (0 levels in all six, for a flip of two overlapping translucent splats, a burst of ten view changes in ten frames and a turn there and back). Observable effect: a script that reads a frame of a view change sees the old order in X_ITE and the new order in X_ITE_R for that one frame (measured with 2, 441, 3969 and 30000 splats, three identical recordings of each, `oracle/web-browser-splat-sort-01-baseline/splatsort01-trace-counts.json`, recorded by `oracle/x_ite-splat-sort-trace.mjs`: in every count one draw with the old order, then the worker's answer and a second draw; the worker answered within that one frame on the RTX machine). The difference that stays is the one transient frame (it may be longer for a cloud above 30000 splats or on a slower machine, where the worker takes longer; not measured), whose length depends on the worker's run time and cannot be reproduced; it is not matched: emulating a one-frame lag would show the stale order on purpose. A world with GaussianSplats also waits for the first sort before X_ITE shows it (`addLoadingObject`); X_ITE_R's first frame is sorted too. The node text of ISO/IEC 19775-1 V4.0 has no GaussianSplats (the node is of X3D 4.1), so no clause applies; ISO/IEC 19775-2 was not available (unverified).
- Sound: the AudioDestinationNode of the engine's Web Audio port has `numberOfOutputs` 1 (the Web Audio specification and the web-audio-api crate); Chromium's has 0. X_ITE shows its "no sound" button while the AudioContext waits for a gesture; X_ITE_R has no such button. A MovieTexture does not make the page's AudioContext (X_ITE's does), and AudioDestination and StreamAudioDestination media were not compared.
- GaussianSplats never-finished load (issue #69, measured with `oracle/x_ite-shot-msaa.mjs` on the pinned 16.5.0 page): a GaussianSplats node that is never displayed keeps its scene from finishing the load in X_ITE. `GaussianSplatsShape.js` adds a loading object when its sort worker answers "ready" (`browser .loading () .then (...)`) and removes it only on the first "sorted" answer, which only a `display ()` call asks for. A node with `visible FALSE`, an empty node, and a node whose explicit `bboxSize`/`bboxCenter` culls it (measured: the 4x4x4 box of `gs-bbox` moved to `bboxCenter 100 0 0`) therefore never leave the loading state: the pinned shot page times out after 45 s with no picture (`gs-empty`, `gs-visible-false` and the moved box, at Multisampling 0 and 4, and with LogarithmicDepthBuffer). A visible, uncullable node loads normally (`gs-bbox`). X_ITE_R finishes loading in all these cases and draws the rest of the world; the stuck loading state is not emulated. ISO/IEC 19775-1 V4.0 has no GaussianSplats clause (standards library `new-items/reference/standards`); 19775-2: unverified.
- Canvas attributes: see "Canvas attributes" below. `baseURL` and `extensions` have no effect.
- Notifications (issue #141, `x_ite_r.mjs` `X3DBrowser.setDescription`): `browser.description = text` shows X_ITE's notification: a `div.x_ite-private-notification` with a `span` in the canvas element's shadow DOM, which shows the text, takes the width of the text and hides after 5000 ms (a new text clears the timer; an empty text, or `Notifications` false, changes nothing). The compared parts are the text, the class, the inline `visibility`, `width` and `transition`, the timer delay and the clearing, the coercion of the set value and the computed position (`options01`, case `notification`, with a controlled 5000 ms timer). Differences: the element sits in a zero-height box before the canvas (`x_ite-private-notification-box`) instead of X_ITE's surface; the change is handled in the next microtask, where X_ITE handles it in its next event cycle (once for any number of sets in both); the rules are in the shadow `<style>` with the light theme's colors, so there is no dark theme; X_ITE's logo (an asset) is not drawn though its space is kept; the font `X_ITE PT Sans` is not loaded, so the width is that of the fallback font. The descriptions X_ITE sends itself are wired (issue #141, `ui01`, case `descriptions`): a bound viewpoint (`X3DViewpointNode.set_bound__`), an over pointing-device sensor (`set_over__`), the context menu's entries, and `""` when the world is replaced. The loading messages of `X3DNetworkingContext` are wired (issue #141, `ui02`, case `loading`): when the browser is not loading, a change of the number of loads in progress (Inlines, textures, Scripts, EXTERNPROTOs of the world and of its Inlines) says "Loading 1 file", "Loading n files" or "Loading done" through `setDescription`. The number is read when a frame starts and when the frame's events are over, as X_ITE processes the `_loadCount` event once per event cycle; while the browser loads (the splash screen) nothing is said. The percent text of the splash screen is not shown.
- Removing an EXTERNPROTO declaration (`removeExternProtoDeclaration`, `externremove01.test.mjs`, issue #58): only the name leaves the scene's table, as in pinned X_ITE 16.5.0. The declaration is not disposed and its load is not cancelled: a load in flight finishes and rebuilds the bodies of the instances, and a declaration the page still holds keeps its state, can `loadNow ()` again and makes instances. The `Cache` browser option (`WebBrowser.setEngineOption`) also drives the page now: with it off, each `loadNow ()` of a declaration asks for the file again (X3DExternProtoDeclaration.loadData gives `cacheScene: false` to its FileLoader). The standards do not say what happens to a load when a declaration is removed (ISO/IEC 14772-1 4.9 only defines the declaration; the text of ISO/IEC 19775 is not in this repository), so no clause is departed from. One difference remains, compared in `externRemove.replaced`: when the world is replaced while such a load is in flight, X_ITE's old scene still gets the response and the Script of its instance prints, while the old world of X_ITE_R is gone with its requests (see the node handles bound to one world).
- An Inline whose request is still in flight when its `url` is set again (`inlinehold01.test.mjs`, ADR 0014 section 5, an owner-approved difference): pinned X_ITE 16.5.0 shows the older request's scene when its response comes later, over the newer scene or over the url that was set back to the first file (`Inline.loadData` calls `this .fileLoader ?.abort ()`, but `FileLoader.createX3DFromURL` returns what `loadDocument` returns, which is nothing, so `fileLoader` is always undefined and nothing is aborted: the last response to arrive wins). X_ITE_R never shows it: a result whose request is not the current one is dropped, and the older Script does not initialize (`inline_request_is_current`). Compared as known differences in `runtime02_harness.mjs` (`inlineHold.*`: the scene shown and its Script's print after the release). Two more differences in the case that sets the url back to the first file while the other request is held: X_ITE asks for the file again and X_ITE_R takes the one it already fetched, and X_ITE_R's Script of that file prints twice in that step (unexplained, only with a held request and a file it already fetched).
- Browser options the renderer does not honor: `setBrowserOption` stores a value that `getBrowserOption` reads back. Eleven options drive the page: `Gravity` (`WebBrowser.setEngineOption`: the WALK viewer's fall acceleration, read in every frame as X3DRenderObject.gravitate does; with 0 it does not fall; compared by the resting position after the fall, `browserOptions.gravity`; the speed during the fall depends on the frame rate and is not compared), `StraightenHorizon` (`WebBrowser.setEngineOption`: the engine straightens a bound view while it is TRUE, setting it TRUE straightens the active view over one second, and `viewAll` straightens its result, as BrowserOptions.set_StraightenHorizon__ and X3DBrowser.viewAll; compared through a ProximitySensor, `browserOptions.straightenHorizon`), `EnableInlineViewpoints` (`WebBrowser.setEngineOption`: the viewpoints of Inline scenes are left out of `firstViewpoint`, `previousViewpoint`, `nextViewpoint` and `lastViewpoint` while it is FALSE, as X3DLayerNode.getUserViewpoints; compared, `browserOptions.enableInlineViewpoints`), `LoadUrlObjects` (`WebBrowser.setEngineOption`: with FALSE an Inline of the world does not start to load, as X3DUrlObject.requestImmediateLoad; compared, `browserOptions.loadUrlObjects`), `ContentScale`, `MaximumFrameRate` and `AutoUpdate` (see "Canvas attributes"), `Shading` (`Renderer::set_shading`), `OrderIndependentTransparency` (`Renderer::set_order_independent_transparency`: the transparent shapes of the main frame are not sorted and are drawn into the weighted blended accumulation targets, then composed over the frame, as X3DRenderObject.drawShapes and MultiSampleFramebuffer do; compared natively with the pinned page at Multisampling 0 and 4, `oracle/rea-oit-render-items-baseline`; the web build shares the renderer but this option is not compared on the page), and `Antialiased` and `Multisampling` (`Renderer::set_sample_count`, clamped to the adapter's supported counts). The other options are stored only, because the WebGPU renderer and the wasm layer expose nothing to apply them to, or the feature does not exist in the browser build: `SplashScreen`, `Dashboard`, `Rubberband`, `TextureQuality`, `PrimitiveQuality`, `QualityWhenMoving`, `MotionBlur`, `AlwaysUpdateGeometries`, `Cache`, `ColorSpace`, `ContextMenu` (no context menu), `Debug` (no debug keys), `Exposure`, `ToneMapping` and `DisplayColorSpace` (no such renderer setting is exposed), `LogarithmicDepthBuffer` (read back by `getRenderingProperty`, no effect on depth), `MetadataReference`, `Mute` and `SoundIntensity` (they set the gain of the engine's sound destinations, but the browser build has no audio output), `TextCompression`, `Timings` (no timings display), `WallFriction` and `XRSessionMode` (no XR). X_ITE also keeps `PrimitiveQuality`, `Rubberband`, `StraightenHorizon`, `TextureQuality` and `Timings` in `localStorage` between pages; X_ITE_R does not.
- Browser keys (issue #141, `x_ite_r.mjs` `#browserKeyDown`, `#browserKeyUp`; X3DCoreContext `[_keydown]`): on the element, with the state of the Ctrl key kept from the keyCode of keydown and keyup as X_ITE does. Home, End, Page Up and Page Down call `firstViewpoint`, `lastViewpoint`, `previousViewpoint` and `nextViewpoint` of the active layer and call `preventDefault`; Ctrl+Plus (keyCode 171 or 187) toggles the `Timings` option and calls `preventDefault`; with the `Debug` option set, Ctrl+1 to Ctrl+5 set `Shading` to POINT, WIREFRAME, FLAT, GOURAUD and PHONG and Ctrl+S ends or begins the update (`isLive`), each with `preventDefault`. Without Ctrl, or without Debug for those six, nothing happens and the key is not claimed. Compared (`options01`, case `debugKeys`, keys sent with Playwright to the focused canvas): the claimed keys, the bound viewpoint, `Timings`, `Shading`, the rendering property, `isLive`, and what an enabled KeySensor gets. Known differences: X_ITE's KeySensor calls `preventDefault` for every key while it is enabled, X_ITE_R's does not (the case compares the key codes only there). The `setDescription` notification that X_ITE shows with the shading and update keys is shown only where the browser has `description` (issue #141, notifications). Shift+F8 (copies the active Viewpoint, OrthoViewpoint or GeoViewpoint with its user position, orientation, center of rotation and field of view to the clipboard as text, then shows "Viewpoint copied to clipboard."; compared in `ui01`, case `shiftF8`, and `ui02`, cases `shiftF8Geo` and `shiftF8Encodings`). A GeoViewpoint is copied with its `geoSystem`, a copy of its `GeoOrigin` and the position and center of rotation as geographic coordinates (`getGeoCoord`); the digits are X_ITE's exactly, including its matrix noise (10 20 1000000 comes out as 9.99999999999995 20 999999.999999998, from the Geodetic iteration, and the orientation from the two `set_activeViewpoint__` passes), compared in full. The text is in the encoding of the scene, `scene .encoding` (also new: `VRML`, `XML`, `JSON`, `GLTF` and so on, `SCRIPTED` for `createScene` and `replaceWorld (null)`): `VRML` and `ASCII` give VRML, `JSON` gives JSON, any other gives XML, as X_ITE's `getEncoding ()` switch. The field of view is the node's `fieldOfView`, not the user's after a view-all. A Viewpoint put into the `rootNodes` of a scene from `createScene` is bound when that scene replaces the world (X_ITE's BindableList.getBound skips only bindables of Inline scenes), so Shift+F8 copies it (case `shiftF8Encodings`).
- `createX3DFromURL (url, node, event)` and `createVrmlFromURL` with a node of a scene that is not the world (a scene of `createX3DFromString`, `createScene` or the promise form, which has no frames of its own in X_ITE_R): the scene of the url loads as a scene of its own, and the field refuses its nodes (see the known difference of a node of another world above), with the warning `X_ITE_R: a node can only be used in the world it belongs to.`; the field keeps its value. For a node of the world the nodes go into the field, as in X_ITE. Not compared: the recorded cases use nodes of the world.
- `browser .supportedComponents` has the Annotation component (level 2) after X_ITE's 39, so 40 components: X_ITE 16.5.0 ships its classes but does not register it, and X_ITE_R does (REA batch 04 owner decision, `crates/x3d-scene/src/nodes.rs`). Compared in `supportedLists` (the known difference of `tests/browser/runtime02_harness.mjs`).
- `browser .addSupportedProfile`, `updateSupportedProfile`, `removeSupportedProfile` and the same for components are not supported: the supported lists are those of the pinned X_ITE. The `toXMLString ()` and `toJSONString ()` of `ProfileInfo`, `ComponentInfo` and their arrays are not supported. The `providerURL` of an external component is `assets/components/<name>Component.js` next to `x_ite_r.mjs` (X_ITE: next to its own script); the file part is compared.
- The profile and components of a scene of `createScene` are kept by the page's `X3DScene` object; the engine's scene has neither (no text form prints them, since `X3DScene .toVRMLString ()` is not supported).
- `createVrmlFromString` before the first world (the page's empty one): the nodes belong to a scene of their own, which they keep alive, and not to the world (there is none to make them in). After a `loadURL` or `replaceWorld` they are the world's. The nodes of a world that was replaced are stale (see the known difference of a replaced world above).
- `importDocument (htmlElement)`: supported for an `<X3D>` element since #146 (see "DOM integration").
- `importJS`, `importDocument`: the scene's `worldURL` is the page's URL, like X_ITE's. Its Scripts run only when the page lets them (see "Script nodes").

## Script nodes

**Status: PARTIAL.** (This entry replaces "Script nodes: not implemented".) Script nodes run in the browser build, off by default. The three acceptance criteria of #133 are met: the engine design is approved (the page's own engine), a Script with fields, routes, `initialize` and `eventsProcessed` runs, and the native Script oracle cases that need no DOM pass in the browser build (below). The node stays PARTIAL because the kept differences below are open, and the Script life cycle cases of `tests/browser/scriptlife01.test.mjs` list the ones that a pinned recording shows.

Refs #133. A Script node runs in the page's own JavaScript engine, as in X_ITE. The engine and its Script implementation are the native viewer's (`crates/x3d-script`, the X_ITE 16.5.0 prelude), so field access, routing, event order and the Script lifecycle are the same.

**Security.** A Script runs with the privileges of the page. It can read and change the document, call the page's functions and send requests as the page. X_ITE_R gives no isolation from the host page; treat Script code as code of the page.

**The switch.** The Scripts do not run by default. The page lets them run with the `scripts="true"` attribute of `<x3d-canvas>`, which is read when a world starts (`loadURL`, `src`, `createX3DFromString`, `createX3DFromURL`, `importJS`, `importDocument`). Any other value, or no attribute, keeps them off: a Script then logs `Couldn't load URL ... no script engine`, and the rest of the world runs. The attribute does not check where a Script's code comes from: a world from another server, an Inline or an external `.js` file runs with the page's privileges as well. Set it only for content you trust.

**Content Security Policy.** The Script engine evaluates source text with `new Function` and `eval`, so a page that lets Scripts run and has a Content Security Policy needs `'unsafe-eval'` in its `script-src`. Give that only to the page that embeds X_ITE_R for trusted content, not to unrelated applications. X_ITE_R itself needs only `'wasm-unsafe-eval'` for its WebAssembly. Without `'unsafe-eval'` the Scripts of a world do not run: the engine logs `X_ITE_R: the Scripts of this world cannot run: EvalError: ...`, each Script logs `no script engine`, and the rest of the world runs.

Compared with pinned X_ITE 16.5.0 in `tests/browser/script01.test.mjs`: the 48 worlds of the four clean-room Script sets of the native suite: `oracle/synthetic-x6` (8), `oracle/synthetic-inline-scripts` (18), `oracle/synthetic-externproto` (12) and `oracle/synthetic-followers` (10) (scene text and the `error` and `warn` console lines, the recordings `oracle/<set>-baseline.json`; what a Script prints with `Browser.print` or `console.log` is not in them). The reload sets `oracle/synthetic-inline-reload` and `oracle/synthetic-externproto-reload` are step scenarios that an embedding application drives with files published while the world runs; the native suites compare them frame by frame. A page cannot step frames, so `scriptlife01.test.mjs` (group `scriptReload`, issue #202) drives the scenarios whose actions a page can do in both engines (`url`, `load`, `remove`, `publish` on a named Inline, 25 of the 37 Inline scenarios, with no two actions in adjacent frames; of the EXTERNPROTO set two scenarios without actions: X_ITE_R's page has no `loadNow`, `unloadNow`, EXTERNPROTO declaration objects or the Inlines of an Inline) and compares what is settled after each frame with actions: the `warn` and `error` lines, the `initialize`, `shutdown` and `created` prints, whether a clock ran, and the `url` and `load` of the Inline. The browser build calls no `shutdown` when an Inline unloads, as X_ITE (it did before issue #202). The differences that stay are in `KNOWN_DIFFERENCES`: `c29-script-interests` (a Script of a replaced scene gets no more `prepareEvents`), as in the native known list. An Inline whose url is set again forgets a failed response of its urls, so it loads a file published after a 404, as X_ITE does (its `fetch` runs again at each load); a failed url that nothing sets again is not asked for again, and an EXTERNPROTO keeps its failure, as X_ITE's scene cache does (`inlineRetry` cases). `c35-auto-refresh-after-failure` is not a case (its error count grows with wall-clock time in X_ITE); X_ITE_R does no automatic refresh after a failed load (X3D 4.0, 9.3.2), see the native known list. The recorded EXTERNPROTO scenarios with actions are not compared in the page; and the 11 Script cases of `oracle/web-browser-script-url-01-baseline`, including what the Script did. The same file tests the default, the policy, the lifecycle and event order, and the page's `rootNodes` field when a Script changes the root nodes.

Kept differences from X_ITE:

- `Browser.print` and the Script's console messages reach the page's console during the Script's call, as in X_ITE (compared in `scriptLife.printTiming`, issue #202): a Script that calls a page function between two prints (`window .mark`) gives `print a`, `mark`, `print b` in both, also over a route chain. The engine hands the line to the page's console writer (`setLogSink`) at once, when nothing waits before it: no earlier line of the engine's log that the page has not taken, and no event of a field the page observes (`addFieldCallback`) that came before it. Otherwise the line stays in the log, in order, and goes out after the callbacks of the events before it, as before. The page's own `Browser .print` and `console` calls, and the page's field callbacks, print in X_ITE's order (the lines made before an event, then its callbacks, then the lines its routes make; compared, `scriptLife.events`). The page's field callbacks run inside the route chain, at the point where X_ITE's X3DField.processEvent calls them (issue #202; compared in `scriptLife.printTiming` and `scriptLife.callbackInChain`, both equal to X_ITE 16.5.0 with no listed difference): the engine puts the lines made so far on the console, then calls the page (`setFieldEventSink`), so a callback that writes another field is seen by the next Script of the chain. While a callback runs the `WebBrowser` is borrowed by the call that runs the chain, so the page reaches the engine through the `chain*` functions (`crates/x3d-web/src/chain.rs`) for field reads and writes, `exists`, `worldGeneration`, `fieldNames`, `fieldData`, `setFieldData`, `takeFieldChanges` and `takeReleasedNodes`. Any other `Browser` call made inside such a callback (`createX3DFromString`, `replaceWorld`, `rootNodes` writes, and so on) throws an Error that says so. The `rootNodes` event is still called after the frame, in its turn. The same order holds for an observed input of a Script (its handler prints first, then the page's callback) and for the `rootNodes` event (it comes in its turn, after the lines of a handler that changed the root nodes, and after the lines of the Scripts' own `rootNodes` callbacks). Checked in `script01.test.mjs` (`order.*`) against pinned X_ITE 16.5.0 from a one-off run, with no new baseline. If a Script's `rootNodes` callback and another event print in one cascade, the page's `rootNodes` event follows both.
- The promise jobs of a Script run when the page's JavaScript stack is empty, after the engine's call, as in X_ITE (compared, `scriptLife.promiseJobs`, issue #202): a `Promise .resolve () .then (...)` in a handler runs after the handler returns and after the page's own microtask, a job that sets an eventOut sends its event, and an `async` handler continues after its `await` the same way. No difference. The ECMAScript job queue is not part of ISO/IEC 14772-1 or 19775-1, so there is no clause.
- A function or node object that a Script left in the page (`window .kept = ...`) goes on working after its world was replaced, as in X_ITE (compared, `scriptLife.staleFunction`, issue #202): it reads and writes the old nodes, sends events (the old world runs no frames, so they go nowhere), prints (the line is logged), and a node handle the Script kept reads its fields. Cost: when a world that ran Scripts is replaced, the engine keeps its session (the runtime with its scene, its node and field data and the Script host's table) instead of freeing it. It drops the frame caches, the GPU buffers of picking and collision, and the world's audio engine (`Viewer3d::shed_drawing`); what a retired runtime still holds beyond that, such as parsed images and meshes of its scene, is not measured, and releases it when the page's engine has collected every function and object of that world's Script realm (a `FinalizationRegistry` on the realm; a world with no Script context is not kept). The page's objects are the realm's only holders, so a page that keeps nothing frees the world at its next garbage collection, and a page that keeps one function keeps the whole old world until it drops it. A world with Scripts off, or with no Script that ran, is freed at once as before. Every world switch goes through the engine's `replace`, so each is covered; a page whose element is removed frees the engine and its kept worlds, and the functions throw `X_ITE_R: the Script's world is gone.` then (X_ITE keeps them working). A node object of a replaced world that the page got from the world's own `X3DScene` is still stale (see the replaced-world difference above); this entry is about what a Script made. Checked in `script01.test.mjs` (`scripts.retiredWorld`, with a function, a node object alone, and nothing kept).
- There is no time limit: a Script that does not return stops the page, as in X_ITE. The native viewer stops it after 10 s.
- `importJS` and `importDocument` with a Script that runs are compared (`scriptLife.importJsString`, `importJsObject`, `importXmlString`, `importXmlDocument`, `importXmlElement`, issue #202): the Script's `initialize` runs at the import, an event and a route work after `replaceWorld`, and the console lines and the page's callbacks are in X_ITE's order. No difference.
- A page callback that changes the world inside the route chain is compared (`scriptLife.chainRoots`, `scriptLife.chainUrl`, issue #202): a callback on `S .x_changed` that adds and removes root nodes (a node made with `fromVRMLString` in the callback too) is seen by the next Script of the chain (`R` reads the new root nodes, then `Q`), and one that gives `Q` new code gives `Q` `shutdown` and the new `initialize` after the chain, as in X_ITE. Fixed by this: `fromVRMLString` / `fromXMLString` with a scene (`WebBrowser.parseField`) threw `X_ITE_R: the browser call 'parseField' is not available inside a field callback` in a callback of the chain; it goes through `chainParseField` now. No difference left. The page callbacks are not in the standards; the order of the events of the chain follows ISO/IEC 14772-1 4.10.3 (an event is processed in the order it is sent).
- The output of an element that is disposed is compared (`scriptLife.disposeInCall`, issue #202): a handler that removes its own `<x3d-canvas>` and goes on prints its later lines, they reach the console, and the route to the next Script still runs, in X_ITE and here. No difference.
- A Script that is a root node is compared (`scriptLife.rootScript`, issue #202): `addRootNode`, `rootNodes .push`, `removeRootNode`, `rootNodes = ...` of the page and a Script's `addRootNode` with the Script itself at the root give the same root nodes, callbacks and text. The `array[i].set is not a function` of the issue did not appear in X_ITE 16.5.0 for these six operations (so it is not a difference). ISO/IEC 14772-1 Annex A.2: a `rootNodeStatement` is any `node`, so a Script is a valid root node.

Compared with pinned X_ITE 16.5.0 in `tests/browser/scriptlife01.test.mjs` (19 `scriptLife` cases and 27 `scriptReload` cases, synthetic worlds, `oracle/web-browser-script-life-01-baseline`, recorded 5 times with identical values): `initialize` (events sent from it, the order of the Scripts, `directOutput`), the events of Script fields (eventIns, eventOuts, routes, an interpolator, the page's events, several events in one task), `eventsProcessed`, `shutdown` when the Script's code is replaced and when the world is replaced, Script errors (in `initialize`, a handler, `eventsProcessed`, `shutdown`, at the top level, a syntax error, a thrown string or object), and the `rootNodes` of `Browser .currentScene` in a Script (a live `MFNode`, the same object on every read: `push`, `splice`, `assign`, `addRootNode`, `removeRootNode` and `setRootNodes` change the world, a callback runs once when the world starts and after each change, before the page's callbacks) and the callbacks of the page. A node that is a root node and a Script field value prints once with `DEF` and `USE`: the page's text forms read the clone counts as they are now. The cases that differ are listed in `KNOWN_DIFFERENCES` (`tests/browser/runtime02_harness.mjs`), with X_ITE's value checked against the recording:

- Script `shutdown`, for each life cycle event on its own (issue #292; compared in `tests/browser/shutdown01.test.mjs`, `oracle/web-browser-shutdown-01-baseline`, 13 cases, 5 identical runs). The standard text is ISO/IEC 14772-1:1997, 4.12.3: `shutdown()` "shall be invoked when the corresponding Script node is deleted or the world containing the Script node is unloaded or replaced by another world"; no other method is invoked after it; events it sends to nodes deleted by the same action are not delivered; 4.12.2 (a `set_url` for a script initialized for a different URL) and 4.10.3 step 2 add the removal from the scene. Only 14772-1 is in the repository (`wb-ct-scrape/specs/iso-14772-vrml97`); the 19775-1 Scripting text was not available, so the matrix rests on 14772-1.

  | event | standard | X_ITE 16.5.0 | X_ITE_R | decision |
  | --- | --- | --- | --- | --- |
  | (a) world replaced (`replaceWorld`, `loadURL`, an Anchor, an import) | `shutdown ()` of every Script of the world, Inline scenes included | none; the browser `shutdown` event only | `shutdown ()` after the browser `shutdown` event, in node order, also for the Scripts in Inline scenes (`replaceWorld`, `loadURL`, `replaceInline`) | standard; known differences |
  | (b1) Script node removed from the live scene (`removeRootNode`, `setRootNodes`) | "deleted": the node is not deleted while its name or the page's handle keeps it | none | none (`removeRootNode`, `inlineRemove`) | X_ITE; the engine does not count references, so deletion is seen only when a scene is dropped |
  | (b2) Inline unloaded or replaced (`load` false, a new `url`) | its scene is dropped, so its Scripts are deleted | none (`N` keeps running its callbacks) | `shutdown ()` of the dropped scene's Scripts (`inlineLoadFalse`, `inlineUrl`); on a new `url` the new scene's `initialize` comes first | standard; known differences |
  | (b3) element removed from the page (DOM integration, X_ITE `processRemovedNode`) | deleted | `node .dispose ()`: `Script .unloadData ()` calls `shutdown ()` | `shutdown ()` (`disposeNode` closes the Script first; `domRemove`) | X_ITE and standard agree; no difference |
  | (c) browser disposed (`dispose ()`) | the world is unloaded | none | `shutdown ()` of the current world's Scripts (`dispose`, `disposeReplacing`) | standard; known difference |
  | (c2) the page ends (`unload` on the window) | the world is unloaded | each initialized Script's `shutdown ()` (a `window` `unload` listener) | the same, once (`unload`) | X_ITE; no difference |
  | (c3) element removed from the document | not a disposal | none | none (`remove`) | X_ITE; no difference |
  | (d) code replaced (`url` of the Script) | `shutdown ()` of the old code, then `initialize ()` of the new | the same (`Script .unloadData`) | the same (`codeReplaced`, `scriptLife.shutdownUrl`) | X_ITE and standard; no difference |
  | (e) a Script that never started (load failed, `load` false, empty `url`), or a load cancelled | `initialize ()` never ran, so there is nothing to shut down | none | none (`idleReplace`) | X_ITE and standard; no difference |

  The browser `shutdown` event (and `onshutdown`) is X_ITE's, not the standard's: it is sent by `replaceWorld` before the old world goes, and by `dispose ()` only as the first bullet above says (compared, `disposeReplacing`: no event). The cases that print from a replaced world's Scripts in `scriptlife01.test.mjs` (`scriptLife.shutdown`, `errors`, the imports, and the `scriptReload.inline` cases) leave the `shutdown` print lines out on both sides (`SHUTDOWN_PRINTS`, `runtime02_harness.mjs`): `shutdown01.test.mjs` compares them. What a `shutdown ()` sends to nodes of the world that goes is not delivered.
- X_ITE 16.5.0 throws `array[i].set is not a function` for a change of the root nodes while a Script is a root node itself. The cases keep the Script in a Group, so X_ITE_R's behavior there is not compared.

## Browser user interface

Issue #141; `x_ite_r.mjs` `BrowserUI`, in the element's shadow DOM; compared with pinned X_ITE 16.5.0 in `tests/browser/ui01.test.mjs` (recording `oracle/web-browser-ui-01-baseline`, 5 identical runs; cases `descriptions`, `splash`, `timings`, `timingsTouch`, `menuEntries`, `menuActions`, `menuOpenClose`, `menuWorldInfo`, `shiftF8`, `cleanup`). The tests click the real entries and press the real keys and compare the resulting browser state; the DOM snapshots only describe what was clicked.

| part | state |
| --- | --- |
| splash screen (`SplashScreen`, attribute `splashScreen`) | shows while `loadURL` or `replaceWorld` loads (the canvas hidden), hides after `initialized`; "Failed loading world." when no URL loads; the option false keeps the canvas |
| timings panel (`Timings`, attribute `timings`, Ctrl+Plus, menu) | X_ITE's table, texts, "More/Less Properties" button (mouse click and touch), starts as LESS |
| context menu (`ContextMenu`, attribute `contextMenu`) | X_ITE's own menu (not the jQuery library): the `contextmenu` event of the element builds it (the event is always claimed); a mousedown or contextmenu on its layer closes it; an entry that is no checkbox or radio closes it; no menu keys (Escape, arrows and Enter do nothing). Entries: Viewpoints, Available Viewers, Straighten Horizon, Display Rubberband, Primitive Quality, Texture Quality, Shading (Debug), the user menu (`browser.getContextMenu ().userMenu`), Animations, Browser Timings, Fullscreen, Show World Info (WorldInfo and META), About X_ITE. A long tap (1200 ms, no sensor under the finger, within 7 pixels) opens it on a touch screen |
| `browser.getContextMenu ()`, `getElement ()` | `userMenu`, `getUserMenu`, `setUserMenu`, `hide`; the element |

Known differences (also `KNOWN_DIFFERENCES` of `runtime02_harness.mjs` for the recorded ones):

- Names: the menu title is "X_ITE_R v0.1.0" and the splash screen shows "X_ITE_R" (X_ITE's name and version are not ours).
- Assets are not drawn (logo, spinner image, icons, backgrounds): the spinner and the checkbox and radio marks are CSS shapes; the font `X_ITE PT Sans` is not loaded; only the light theme exists. Texts are English only (no gettext).
- Timings panel: X_ITE draws on demand, so its frame rate in an idle world is about 0; X_ITE_R draws every frame (the value is recorded in `detail`, not compared). The engine measures no speed, camera, picking, collision time or counts of primitives, shapes and sensors: those rows show 0; "Browser" and "X3D total" are the page's and the engine frame's time. The panel's type (More/Less) is not kept in localStorage.
- Splash screen: the text is "Loading 1 file" while loading and "Failed loading world." on failure; the engine reports no load counts or percentages.
- The viewpoint list, viewers, world info and META come from engine exports (`userViewpoints`, `availableViewers`, `setViewer`, `worldInfo`, `userPose`, `takeDescriptions`). "Available Viewers" in the menu names the viewers of the bound NavigationInfo.
- `dispose ()` removes the splash screen, the timings panel and any open menu; X_ITE leaves them in the shadow DOM. The 5000 ms notification timer is cleared by `dispose ()`.
- A tap on the canvas gives a `click` after the touch events; X_ITE's touch handlers cancel it (`ui01.timingsTouch`).
- Not verified against X_ITE: the long tap (headless Chromium gives X_ITE no `touchstart` for DevTools touches, so the case could not be recorded); the long tap uses the pointer cursor as "a sensor is under the finger".
- The keyboard: Shift+F8 needs the `clipboard-write` permission; without it nothing is copied.

## Canvas attributes

Compared with the pinned X_ITE in `oracle/web-browser-attributes-01-baseline`. An attribute sets its browser option when the element starts (the parser's attributes) and on every change, with X_ITE's rules: `true` and `false` are the only boolean values (any other text, also `TRUE` and the empty text, is the default of the attribute), a string attribute that is empty gives its default, a removed attribute gives that default as well (not the default of the option), and the number attributes (`exposure`, `maximumFrameRate`, `multisampling`, `contentScale`) pass the text to the option's number coercion (`x` gives NaN, `multisampling` gives an integer). The options never write an attribute back: setting an option leaves the attribute as it was, and a later attribute change replaces the option's value.

| attribute | option | effect |
| --- | --- | --- |
| `contentScale` | `ContentScale` | the framebuffer size is the CSS size times the option (`auto` is -1, the device pixel ratio, followed in every frame); 0, a negative or a NaN value gives a 1 x 1 framebuffer, like X_ITE. Setting the option does the same as the attribute. A stopped browser (see `update`) applies a new size when it starts |
| `maximumFrameRate` | `MaximumFrameRate` | a frame sooner than 1 / max (rate, 1) seconds after the last processed one is skipped whole (events and drawing), like `X3DTimeContext.advanceTime`; NaN does not limit. The interval is measured with the frame timestamp of `requestAnimationFrame` (X_ITE: `Date.now ()`): X_ITE draws on demand, and this browser draws every frame, where two `Date.now ()` values can be closer than the frame period |
| `update` | `AutoUpdate` | `auto` sets the option to true; any other value sets it false and starts (`true`, removed, or any text other than `false`) or stops (`false`) the browser. A stopped browser is not live (`endUpdate ()`): its TimeSensors and the like pause and the input held at the stop is let go; the events of the page and the routes still run, and `loadURL` and `replaceWorld` still finish. With `AutoUpdate` true a canvas that is hidden or scrolled out of view stops, and starts again if it was running before (setting the `AutoUpdate` option does the same watching as `update="auto"`). `update="auto"` on a stopped browser leaves it stopped |
| `oninitialized`, `onshutdown` | | the text becomes the event handler of the element, called before the event is dispatched |
| `antialiased`, `cache`, `colorSpace`, `contextMenu`, `debug`, `displayColorSpace`, `exposure`, `logarithmicDepthBuffer`, `multisampling`, `orderIndependentTransparency`, `splashScreen`, `textCompression`, `timings`, `toneMapping`, `xrSessionMode` | the option of the same name | the option is set and read back, compared; nothing else happens (see "Browser options the renderer does not honor") |
| `notifications` | `Notifications` | the option is set and read back; it decides whether `browser.description` shows a notification (see "Notifications") |
| `baseURL` | | not implemented: X_ITE sets the base of the relative URLs (`browser.baseURL`); X_ITE_R's `baseURL` is the page's |
| `extensions` | | not implemented: X_ITE observes it and has no effect on options either |
| `src`, `url` | | load the world (supported) |

The `update` attribute and the `AutoUpdate` option call `beginUpdate` and `endUpdate` (see the table of the browser functions): a stopped browser is not live. The introspection `browser [Symbol.for ("X_ITE_R.frameLoop")]` also reports `processed` (the frames not skipped) and `live`.

## Not implemented

- `getUserViewpoints` and the other X3DBrowserContext functions.
- `toVRMLString` and the other stream functions of the browser.
- The rendering properties of X_ITE's WebGL context: `getRenderingProperty` throws `X_ITE_R: the rendering property '...' is not implemented.` for `MaxTextureSize`, `TextureUnits`, `MaxLights`, `ColorDepth`, `TextureMemory`, `MaxAnisotropicDegree`, `MaxSamples`, `PixelsPerPoint`, `SoundDestinations` and `XRSession`. The WebGPU renderer has no such values (its limits are different quantities, and the recorded numbers belong to one GPU through ANGLE).
- DOM integration: the world follows the `<X3D>` child of the canvas (see "DOM integration"). The changes of the elements inside it follow it too, with the Inline children and the field and load CustomEvents (see "DOM integration"). The one known difference: the order of the first load events of the Inline elements (below).
- XR. The browser user interface is ported (see "Browser user interface"); not done there: the "Loading n files" notifications, the GeoViewpoint of Shift+F8, the engine values of the timings panel that the engine does not measure.
- Sound: the data of an AnalyserNode (`byteFrequencyData`, `floatTimeDomainData`, ...) is not read from the page's analyser; the microphone and the MediaStream of a StreamAudioDestination are not mirrored (see "Sound" above).
- MovieTexture video: the sound of a video plays through its own `<video>` element, not through the engine's Web Audio graph, so the node's `gain`, `pitch` (beyond `preservesPitch`), Sound spatialization and Mute do not reach it, and the page's AudioContext is not made for it (X_ITE routes the element through `createMediaElementSource`). The first frame the page has when `loadeddata` fires is the texture, and the page copies each new picture with a 2D canvas (`drawImage`, `getImageData`), so a large video costs a read-back for each frame (issue #139).
- A `replaceWorld` while a `loadURL` waits for its Inline and texture responses does not stop that load: like X_ITE, the older world then replaces the newer one.

## Graphics

The renderer is WebGPU only (wgpu 30 `Backends::BROWSER_WEBGPU`). There is no WebGL renderer.
Before it makes the renderer, the engine checks the adapter's limits against what the renderer needs. A shortfall stops the start with `X_ITE_R cannot start: ...` in the element and names the limit.
A lost device (driver reset, GPU process crash, a background tab the browser reclaims) stops the frame loop, calls the `CONNECTION_ERROR` callbacks and shows `X_ITE_R stopped: the WebGPU device is lost.`.

WebGPU compatibility mode (`requestAdapter ({ featureLevel: "compatibility" })`, for devices with OpenGL ES 3.1 class GPUs) is not used. wgpu 30's `RequestAdapterOptions` has no `featureLevel` field and its WebGPU backend always requests a core adapter, so the engine cannot ask for one. This is the blocker until wgpu has the option.

## Mobile support

Pointer Events give mouse, pen and touch input; the engine gets touch as X_ITE's TouchEvent handlers do. Rotation and resize change the framebuffer on the next frame.

The WebGPU column is the vendors' published state as known in mid-2026; this lane did not verify it on devices.

| browser | WebGPU (vendor documentation) | state |
| --- | --- | --- |
| Chrome for Android 121 and later, Android 12 and later, with a supported GPU (Qualcomm Adreno, ARM Mali) | yes, by default | expected to run; **not tested on a real device** |
| Safari on iOS and iPadOS 26 and later | yes, by default | expected to run; **not tested on a real device** |
| Safari on iOS and iPadOS 18 and earlier | no (a feature flag only) | the element shows its fallback message |
| Firefox for Android | no (not yet enabled by default) | the element shows its fallback message |
| Samsung Internet, other Chromium browsers | follows its Chromium version and the device's GPU | **not tested** |
| devices with only OpenGL ES 3.1 class GPUs | needs WebGPU compatibility mode | not supported (see Graphics) |

Tested in desktop Chrome emulation only (`tests/browser/runtime02_phone.test.mjs`, `tests/browser/runtime02_tablet.test.mjs`): a phone (390 × 844, DPR 3) and a tablet (820 × 1180, DPR 2, rotated to landscape), with touch input. These tests check input, coordinates, framebuffer size, rotation and cleanup. They are no proof of a mobile browser or a mobile GPU.

Measured on 2026-11-01 (Chromium 151.0.7922.34 under Playwright, headless, WebGPU adapter NVIDIA Ampere through Vulkan on a desktop): `runtime02_phone` 1 of 1, `runtime02_tablet` 1 of 1, `runtime02_input` 11 of 11 (touch tap and navigate, scale coordinates among them) and `runtime02_lifecycle` 10 of 10 passed, none skipped. The phone and tablet emulation set `isMobile` and `hasTouch` and send touch through the DevTools protocol (`Input.dispatchTouchEvent`); that is not a real touchscreen. Real-device testing stays open (issue #143). It needs:

- Chrome for Android 121 or later on one Android 12 or later phone with an Adreno or Mali GPU, and Safari on one iOS or iPadOS 26 device;
- the OS and browser versions, the device model, and the `GPUAdapterInfo` of the adapter recorded with the result;
- the runtime02 cases or a documented subset: one-finger rotate, two-finger pinch, tap on a TouchSensor, page scroll with `touch-action`, device rotation, tab hide and show, removal and `dispose ()`;
- the memory limits and the device-loss behavior under real memory pressure.

## replaceWorld arguments (issue #134)

Compared with pinned X_ITE 16.5.0 in `sceneCreate.replaceWorldArgs` and `sceneCreate.replaceWorldGuards` (`tests/browser/scenecreate01_case.mjs`): the Promise, `null`, `undefined`, no argument and an extra argument, and the three forms below. For each of them X_ITE_R gives the same result, the same root nodes and the same world as X_ITE, and also prints an error to the page console that says what X_ITE accepted silently. These lines are X_ITE_R's own (declared in `KNOWN_DIFFERENCES`, `sceneCreate.replaceWorldGuards`, `tests/browser/runtime02_harness.mjs`); X_ITE logs nothing.

- `replaceWorld (sameScene)` where the scene is already the world: resolves, the scene stays the world (`currentScene` is the same object), the `shutdown` and `initialized load` events run again. Console: `X_ITE_R: Browser.replaceWorld: the scene is already the world; it stays the world.`
- `replaceWorld (value)` where the value is no `X3DScene` of this browser (a number, string, object, array, node, function, `SFNode`, `MFString`, a scene of another browser, ...): resolves with an empty world. An `MFNode` gives a world with its nodes as root nodes (a `NULL` entry stays `NULL`; the nodes are made again in the new world with their field values, and their `DEF` names are not kept, as in X_ITE). Console: `X_ITE_R: Browser.replaceWorld: the argument is no X3DScene of this browser; the world is replaced by an empty world.` (or `... by a world with the root nodes of the MFNode.`).
- `replaceWorld (scene)` with a scene of a world that was replaced: resolves, and `scene` is the world again (`currentScene === scene`) with the root nodes and named nodes it had. The engine disposes a replaced world, so the scene is made again from its VRML text as it was when the world went (kept with the `X3DScene` object and released with it). Console: `X_ITE_R: Browser.replaceWorld: the scene belongs to a world that was replaced; the world is made again from the scene's text as it was then (nodes of the old world stay unusable).`

What still differs for a scene made again: node objects taken from the old world stay stale (see the replaced-world difference above), Scripts start again, Inlines and textures are fetched again, and no state outside the printed text (for example the time of a running TimeSensor) survives. A scene whose world an Anchor replaced inside the scene is not kept and still rejects with the stale-world error.

## DOM integration

Issue #146; `x_ite_r.mjs` `DOMIntegration` and `htmlToXML`; compared with pinned X_ITE 16.5.0 in `tests/browser/dom01.test.mjs` (recording `oracle/web-browser-dom-01-baseline` and, for the changes, `tests/browser/dom02.test.mjs` (`oracle/web-browser-dom-02-baseline`, 5 identical runs; cases `attributes`, `adding`, `removing`, `routes`, `declarations`, `live`); the first recording is 5 identical runs; cases `inlineWorld`, `names`, `structure`, `protos`, `script`, `wiring`, `values`, `envelope`, `replacing`, `importElement`). The tests put real markup into the canvas and compare the resulting scene.

Status: PARTIAL. #146 is split by what DOMIntegration.js does: the root element (part 1), the changes of the `<X3D>` element (part 2), the Inline child elements and the CustomEvents (part 3).

- The canvas observes its children. The last `<X3D>` child (`:scope > X3D:last-of-type`) is the world; it is imported with `importDocument` and replaces the world. No `<X3D>` child gives an empty world. Setting the same element again changes nothing. `dispose ()` stops the observer.
- `importDocument (htmlElement)` reads an `<X3D>` HTML element with the names of X_ITE's HTMLSupport: node type names exact or upper case (`TRANSFORM`), field names exact or lower case (`diffusecolor`), `DEF`, `USE` and `containerField` in either case, `ProtoDeclare`, `ExternProtoDeclare`, `ProtoInstance`, `IS`, `ROUTE`, `IMPORT`, `EXPORT` and the head elements as written or upper case, a PROTO's name as an element and its fields in lower case. The element is written as XML text on the page and the engine's XML parser does the rest, so the values and the warnings are the engine's (compared). The htmlSupport tables come from the engine's node table (`htmlSupport ()` of the wasm package).
- `<script>` elements are moved into child elements and CDATA sections like X_ITE's `XMLParser.scriptElement` does, so the element in the page changes as in X_ITE; text that is not well-formed XML logs `Error: Invalid XML.` and the node is skipped.
- Changes (part 2): the element is observed (attributes, child list, subtree). The engine tells the page what each XML element made (`xmlTargets`: a node, the scene or a route), which is X_ITE's `element [_node]`. An attribute is read into the field of its node with `XMLParser.nodeAttribute`'s rules and sends its event (`xmlAttribute`); an attribute of a child that is not a node (`fieldValue`, `field`) reads the child again into its parent node. An added element is parsed into the context of its parent node, or among the root nodes of the world (`xmlChild`), its ROUTEs are connected, and its fields send their events. A removed element's node is disposed like `X3DNode.dispose` (`disposeNode`: its name, its routes, the MFNode and SFNode fields and root node lists that hold it); its child elements keep their nodes, as in X_ITE. A removed ROUTE element deletes its route.
- Inline children and events (part 3): the Inline elements of the world (and of every document added to them) listen to their load state (`loadState`, a private field). Not started or failed: the child elements are removed. Complete: they are removed, then the XML document of the loaded scene is appended as the child of the Inline element (`scene [_x3d]`; `inlineXml`), its elements are bound to their nodes (so attribute changes inside the child reach the nodes of the Inline's scene, `xmlAttribute` and `xmlChild` with the context of its `Scene` element), and the Inline elements and fields inside it are processed in turn. Then a `load` (complete) or `error` (failed) CustomEvent with `detail.node` is sent from the element. A file that is not XML gives no child, as in X_ITE. Each output field (`outputOnly`, `inputOutput`) of every node element sends a CustomEvent named like the field, with `detail.node` and `detail.value`; USE elements and ROUTE elements send none. A `field` element of a node whose attribute changed (or a child added or removed) makes the page's field objects of that node new, so the next read of the field gives the new object, and, as in X_ITE, the new objects send no CustomEvent. A removed `Scene` element disposes its context (`disposeContext`: routes, names, imports, declarations, root nodes), like `X3DExecutionContext.dispose`. Compared in `tests/browser/dom03.test.mjs` (4 cases, `oracle/web-browser-dom-03-baseline`, recorded 5 times with identical values). Known difference (`domEvents.inline`, in `KNOWN_DIFFERENCES`): at the first load X_ITE starts this code while its Inline loads still run, so a failed Inline sends `error` twice (the state read, then the pending field event) and the loaded one sends `load` after them; here the engine has finished every load of the world when this code starts, so each Inline sends one event, in document order. The later changes (`url`, `load`, an added Inline, an Inline inside an Inline) are equal.
- Known differences: the `field` element of an existing user-defined field of a node, changed by an attribute: the scene text has the new value, but the field object the page holds is not refreshed (X_ITE's is). An added or changed element in an Inline's scene, the output-field CustomEvents and `load`/`error` events of Inline elements are part 3. Open: a removed `Scene` element (X_ITE disposes the scene; here nothing happens).
