Holoscripting™ 101

Start your journey in learning Holoscripting with these foundational documents as guides covering fundamentals and logic.

Holoscripting™ 101

Start your journey in learning Holoscripting with these foundational documents as guides covering fundamentals and logic.

Contents


Holoscripting: Getting Started

Welcome to Holoscripting! This document will get you through the steps needed to compile and deploy your first script.

The Holoscripting dev system is a set of utilities and libraries designed to help compile and deploy a Holoscripting to a Cavrnus space. You can write scripts manually if you wish, but using these tools should make for a better experience.

Holoscripting itself is interpreted javascript. For ease of development we opt to build scripts using Typescript. The types provided by the Cavrnus runtime are very helpful for reducing programming mistakes, and also provide code-level documentation for the capabilities of Holoscripting.

Prerequisites.

You will need:

  1. A Cavrnus account.
  2. Node.js (We are currently using version 16.15.1, but any LTS build should do!
  3. Yarn v1, which can be installed using:
npm install --global yarn

Optional but helpful:

Setting up the Cavrnus CLI Environment.

In your terminal of choice, run:

npx @cavrnus/cli login

This will prompt you to provide your Cavrnus login information, in order to work with your account.

If you are connecting to the Cavrnus staging environment, or an on-premises Cavrnus installation you will need to use:

npx @cavrnus/cli login -s

This will allow you to select the destination server.

For the staging environment, this is ‘api.stage.cavrn.us’, and continue.

You will be prompted for your Cavrnus customer id. If you log in to the website at ‘http://mycompany.cavrn.us ’, your id is ‘mycompany’.

Provide your username and password.

You can rerun this command to change accounts whenever you wish.

You can confirm your current login information by running:

npx @cavrnus/cli profile

Setting up a script environment.

Create a new folder as your scripting workspace, or use an existing one.

Open up Visual Studio Code in this empty folder.

In your terminal of choice, in your script project folder, run:

npx @cavrnus/cli new

If you’re targetting the dev or stage environments, use:

npx @cavrnus/cli new -d

This will run the Cavrnus CLI to build a new scripting project. It will ask you:

  • The script’s name: This should unique among your scripts. Avoid using spaces. We recommend something like ‘scriptingdemo-initialization’. Project-Purpose.
  • The script’s description: This is entirely up to you. The description has no programmatic effect.
  • A Cavrnus room name: The project will be set up with respect to a specific Cavrnus space. Deployment and dependency retrieval scripts work with respect to this space. NOTE: This can be changed at any time later by modifying the package.json file.

If this is your first run of the @cavrnus/cli, after setting up the new script package, the cli will prompt you to login, as per the above instructions.

After a moment or two the initialization should complete. You will see a new folder present, named the same as your script. Inside you will see:

node_modules, package.json, yarn.lock, tsconfig.json:

These are all familiar to typescript and node.js developers and contain libraries, dependencies, scripts and configuration data. For Holoscripting purposes these should not need to be changed.

script.ts: This is your script! By default it should contain:

import cav, { out } from '@cavrnus/runtime';out.print(`Welcome to Holoscripting!`);

Build and deploy your script to your selected room by running:

yarn deploy

or equivalently:

npm run deploy

If you are running the Cavrnus application and connected to the room, you will see the script appear in the objects list and your script will be live!

After deploying you may notice a .hs file produced in your script directory. This is a packaged and sharable version of the script, and can be uploaded directly inside the Cavrnus application just like any other content.

Your script is also available in your Cavrnus library. You can insert it just like any other object from the Cavrnus Insert > Content menu. As you deploy new versions, old versions will be archived. They are still usable in rooms where they are already present, but will not show up in your library.

See what you can do by inspecting the ‘cav’ object for functions and data accessors:

  • cav.beginOperations is used in all cases to make changes to the room.
  • cav.on is used to configure a trigger, or event hook.
  • cav.data.* is used to access information about the room.

Other helper scripts.

yarn upgrade

This is a standard node.js script to upgrade all dependencies. Run this when needed to update the dependencies and runtime types to match the Cavrnus Application.

yarn build

Compiles without incrementing the version or deploying the script.

yarn deployto

deployto will prompt you for a room to deploy the script to, instead of the default configured room.

yarn scriptenv

scriptenv will connect to the deployment room and download type information for any other scripts live in the room, and download them to your script’s library/ folder. Imports from these libraries can then be included in your script.ts source using:

import * as YourScriptName from 'yourscriptnamehere';

Holoscripting import does not support subfolder imports (e.g. import * as X from ‘yourscriptnamehere/script', so avoid that.

Deploying a script will not deploy its dependencies; if they are not present in the room then the script will not run. It will wait until the dependencies are present before starting up.

Notes.

@cavrnus/runtime describes with types, the capabilities of the Cavrnus application scripting system. At the moment there is no interactive debugging capability, nor can the runtime or script be run from within node.js. We hope to build these things in the future.

Importing scripts from other Holoscripting libraries must be done with a simple library name without subfolder/files specified.

This is acceptable:

import * as test from 'test'

This is not:

import * as testinternal from 'test/internalscript'

Consider instead with the assumption that the main script exports the contents of internalscript.

import {testinternal} from ‘test';

Deploying a script will not also upload its dependencies. They will need to be inserted into the space using their own project space, or directly in the Cavrnus application.


↑ Back to Contents

Cavrnus Systems: The Journal, Operations, and Transients

The Cavrnus system primarily consists of a set of spaces, each an individual environment a user can join, participate within, create, or modify.

The Journal.

The data that defines a space is called its Journal.

The Journal consists of a linear sequence of Operations. Operations are never deleted, the Journal only ever moves forward. Think of the Journal as a log of everything that ever occurred within the space, which when replayed, reconstructs the space in its current state.

When connected to a space, a user can submit an Operation to be included in the journal. These become part of the permanent journal. Common operations include:

  • Instantiating objects
  • Updating property values
  • Cancelling previous operations
  • Adding a chat message

A Transient, is a communication sent by a user which is not stored in the Journal. Transient communications involve either temporary or unsynchronized state changes. When a new user joins the space, they will not be transmitted any historical transient information; they only receive and process the permanent journal. Common transients include:

  • User metadata ( user entry and exit from the space, avatar position, camera position, mute state, streaming state )
  • Client to client application communications needed to synchronize certain systems.
  • Client pings to get other users attention.
  • Requests for other users to follow.

But the most important transient is most likely:

  • Operations in progress.

As an example, while a user is manipulating an object’s position, they will frequently send transient operations as they change the object’s values. When they release the object they will submit a finalized Operation. Or if they instead hit Escape or otherwise cancel, they will send a transient event cancelling the in-progress process.

Lastly, transient events are often echoed locally; they take effect immediately within the executing client’s application. Operations require guaranteed ordering, so they only ever are applied after a round trip to the Cavrnus API server. Since latency makes for bad UX, transients are applied locally immediately. The journalling system will ensure that the correct synchronized state is reached despite a potential order difference when multiple users manipulate the same fields simultaneously.

In short:

Operations are permanent changes.

Transients are temporary state transmissions and coordination systems.

A Journal example.

For example, consider the following sequence of abstract operations:

1. Create a Sphere called 'A'.

2. Move 'A' to (10,1,0).

3. Create a Box called 'B'.

This results in a space with two objects, A and B. If we wish to remove 'B' from the space, rather than delete anything from the Journal, we instead:

4. Cancel operation #3.

Which will result in 'B' not being present, as its creation is now ignored.

Consider a user wishing to ‘Undo’ the move of 'A', operation #2. In this case, we just:

5. Cancel operation #2.

Rather than track previous location and update it, we simply mark the operation as ‘cancelled’.

Oops, now we want to redo that operation!

6. Cancel operation #5.

Now the cancellation is cancelled. Operation #2 is now live once again.

Another example.

Consider a user ‘A' dragging a slider for ‘x' from 0, through 1, then releasing on 2. What events occur for user 'A’ and 'B’?

  1. 'A': Local transient: Update 'x' to 0
  2. 'A': Send transient: Update 'x' to 0
  3. ‘B' only: Recv and apply transient: Update 'x' to 0 (client 'A’ will ignore this update as it knows it has already been applied)
  4. 'A': Local transient: Update 'x' to 1
  5. 'A': Send transient: Update ‘x' to 1
  6. ‘B' only: Recv and apply transient: Update 'x' to 1. Client 'A’ will ignore this update as it knows it has already been applied.
  7. 'A': Local transient: Update 'x' to 2
  8. 'A': Send transient: Update ‘x' to 2
  9. ‘B' only: Recv and apply transient: Update 'x' to 1. Client 'A’ will ignore this update as it knows it has already been applied.
  10. 'A': Send operation: Update 'x' to 2, noting this operation concludes the above transient sequence
  11. ‘A' and ‘B’: Recv operation: Update ‘x' to 2, with transient conclusion. Clients will mark the transient events completed and reorder them to match the operation’s ordering, determined by the API server. In most cases, the live state will not need to be updated, as it is already correct.

↑ Back to Contents

Property Contexts and Link Properties

The property system describes a tree structure, with properties as the leaves of the tree, very much like a file system of folders and files. Unlike a file system, there is some (optional) implied meaning behind the paths. Within the Cavrnus implementation we call this the Context of the property path.

This document describes why this exists, how it is useful, and how it is implemented.

Why and What?

Properties is a generic system. Properties can be created of any property type at any path.

However, properties generally are grouped by their purpose. This purpose provides useful information when navigating the properties tree. Furthermore there are occasions where this purpose is necessary to completely resolve a property value (this is rare, but relevant!). This purpose, we encapsulate in an object called IPropertyContainerContext. Within all PropertySetManager, the node object of the property system, there is a collection of these contexts, accessible to integration code.

The property system does not ever set up these contexts; they are run-time only objects, attached generally during initialization. There is no protocol or serialization for these types. As such they are generally application specific.

There is also a second aspect to these contexts. As of Cavrnus 2022.2.1, the Unity application establishes a set of read-only properties, in all nodes with contexts, that provide property-level information about the context.

Let’s have an example:

An example:

When the unity application receives an OpCreateObject operation, it begins the process of loading the object. It decides on the property path for the object given the object’s ObjectId, using PropertyDefs::ObjectContainer. It then adds a context to that node, which in the case of the Unity app, provides access to the runtime object that manages that object. This allows property related extensions to access application-specific information. This is never accessed generically using properties; it is always specific to the integration.

Secondly, it writes the well-known, read-only, identifier properties into the property tree, this consists of:

The context’s type (in this case the string ‘object’)

The expected types here are available at PropertyDefs.PropertyContext_Type_*

  • As of 2023.1.0, ‘object’, ‘part’, ‘user’, and ‘assets’.

The context’s specific type (in this case, the type of the object; for example, ‘video’)

Likewise, at PropertyDefs.PropertyContext_ObjectType_*

  • As of 2022.2.2, for objects: ‘whiteboard’, ‘streamboard’, ‘userboard’, ‘holo’, ‘image’, ‘pdf’, ‘video’, ‘textboard’, ‘artracker’, ‘script’, and ‘unknown’.
  • As of 2022.2.2, for parts: ‘holoroot’, ‘importtransform’, ‘transformpart’, ‘meshpart’, ‘light’, ‘animation’, ‘text’, ‘particles’, 'material’, ‘texture’.

Usually, a name.

Most loaded Holo components generate their own node and context, identifying their type alongside their properties.

For what purpose?

All of the above definition and data doesn’t actually do anything. But it supports secondary systems, in particular, Holoscripting!

Holoscripting generally works at a very low level within the Cavrnus system; it works with the property tree directly rather than with any application specific information. All of this metadata provides Holoscripting methods to understand, search, and navigate the scene. If Holoscripting wishes to turn off all the lights, it can search for light contexts and manipulate their properties. The search is only possible using the context provided data.

Link Properties.

In 2022.2.3 we introduced a new type of property value. This property is a pointer to a node in the property tree. It provides a way to point, generically, to an asset or other loaded content.

The first property to use this system is /room/skybox. Previously this was a string property that contained the contentId for an object stored on the server. After 2022.2.3 this property is a link property to a loaded asset. The context of this target is used to find the runtime texture needed to render the skybox. This extra level of indirection is not terribly useful for skyboxes, but is a substantial component of the subsequent update to the materials system, in 2022.2.4 or soon afterwards.

Alongside this change of property data, there are new OpCreateObject types. In particular, a new option for ObjectType, ‘ContentIdAsset’, which is used to load an asset (texture, until materials and other systems leverage this feature), into the room. This is different than just-'ContentId', which would take an image asset and place it on a board. These assets are set up with a property context and information, then wait for some other system to require them, at which point they are downloaded, initialized, cached, and provided to the consumer.

The most basic update for /room/skybox would require handling the ‘ContentIdAsset’ loaded textures, retaining at a minimum the objectId for that asset (which is used to establish the property path and context), as well as traversing the value of a link property to its context, and then to the actual asset to be used.

This same system will be used shortly for materials and textures.

A small example.

You might see the following operation sequence:

  • OpCreateObject, ContentIdAsset = {content id for an hdr}, NewObjectId = ‘abcdeexample’
  • UpdatePropertyValue, /room/skybox <= /objects/abcdeexample

↑ Back to Contents

Properties : Sub-property values

Properties all contain a set of internal sub-properties, which can be accessed using a . (dot) notation.

Sub-properties are assigned automatically based on the type of the property. Some properties have explicitly added sub-properties. Sub-properties can be referenced by expressions the same as the root properties.

Sub-properties are not computed unless some expression or script is dependent upon it. Their value updates immediately after their source property updates.

Sub-properties are meant to be interpretations of the source value, though this is not a hard constraint.

As an example, if you wish to set up an expression that incorporates the current day of the month, you could use the following reference:

/date.day

This accesses the ‘/date’ property’s ‘day’ sub-property.

This document describes the built-in sub-properties available first by property type, then by property, for those that have additional assigned.

Scalar properties.

None are established at this time.

Vector properties.

.x, .y, .z, .w

Number sub-fields which extract the components of the Vector.

Color properties.

.r, .b, .g, .a

Number sub-fields which extract the components of the Color.

Boolean properties.

.not

Presents the negation of the boolean property value.

  • Added in 2022.2.2

String properties.

None are established at this time.

Json properties.

Json properties set up sub-properties to reflect the contents of the json value. If the property value was:

{ a: 10, b: 20, c: { nest: 30 } }

Then there will be sub-properties .a, .b, .c. a and b will be scalar values, while c will also be a Json property type. In fact, this sub-property can further be sub-propertied: referencing .c.nest is valid.

Transform properties.

.pos

A vector sub-property that presents the translational component of the Transform

.rot

A vector sub-property that presents the euler-rotation component of the Transform. Note this does not account for quaternion rotations whatsoever, but does include lookat rotations.

.scaling

A scalar-valued sub-property that presents the overall scale factor of the transform. In the case of uniform scales this value will be correct, but in nonuniform scaling transforms, it will be computed as the determinant of the final transform, or as the cube root of the multiplication of the independent axes' scales.

.scale

A vector-valued sub-property that presents the scaling of the transform, per axis.

.forward

A vector-valued directional vector that presents the forward orientation of the transform. Forward, for an identity transform, will be the positive Z-Axis.

.up

A vector-valued directional vector representing the up-vector. By default, the Y-Axis.

.right

A vector-valued directional vector representing the right-vector. By default, the X-Axis.

The following sub-properties are included only with the Unity client, and not set up by the SDK systems alone:

.worldUp, .worldForward, .worldRight, .worldEuler, .worldQuat, .worldScale, .worldPos

These are vector-valued sub-properties that present the relevant values transformed into world coordinates.

Specific properties with custom sub-fields.

/date
.isoday

The current date in yyyy-MM-dd form, as a string.

.year

The number of the current year.

.month

The number of the current month.

.day

The number of the day of the month

.dow

The name of the current day of the week.

.hour

The number of the current hour, from 0 to 23.

.minute
.second
.millisecond

The millisecond field does not update every millisecond and will be quite a bit more coarse.

Numerical Derivatives.

The properties system includes a method for computing numerical derivatives of scalar, vector, and color properties. These derivates can be computed with respect to any scalar-valued dependency. These derivatives will not take changes to property assignments into account; they can only account for animated property generators.

As an example, consider a fictitious scalar property /room/brightness, set to an expression Math.cos(ref('t')). This property will animate as a cosine wave over time.

The derivative of this scalar property can be referenced using the id /room/brightness.dd|time. This value will follow a sin curve, in this instance.

The general form of the sub-property is .dd followed by the property id, encoded, of the variable with which to derivate with respect to. The property id is encoded by replacing its / with |. .dd|time thereby means the derivative with respect to real time.

Derivatives are computed numerically by reevaluating values using an offset for the ‘time’ value. The offset value is quite large to avoid numerical instability.

The primary use of this system is to understand playback speed for animations, videos, sound clips, etc, but it a general purpose system and available for use.


↑ Back to Contents

Tutorial: Expressions

Almost all property types allow the user to assign an Expression to compute the value. Expressions allow assigning complicated functions, which can depend on the values of other properties, or be animated and change over time.

Expressions are interpreted as javascript code, and have a set of automatically available types ready for use. The intrinsic javascript Math namespace is available, as well as the types included in Holoscripting Definitions: Math Types. The Holoscripting runtime '@cavrnus/runtime' is not available.

Basic expressions.

Number values
  • The expression should produce a number:
1 + Math.cos(0)
String values
  • The expression should produce a string:
“Object A"
`Object ${somevariable}`
Vector values
  • The expression should produce a Float4T, from the link above:
{ x:0, y:10, z:5 }
Color values
  • The expression should produce a ColorT, also from the link above:
{ r:0, g:1, b:.5, a:.8 }
Boolean values
  • The expression should produce a boolean:
true
0 === 1
Transform values
  • The expression should produce a TransformCompleteT, defined in the transform document above:
{data:Transform.lookat(Float3.new(5,5,0),Float3.new(0,1,0))}
Json values
  • The expression can yield any valid json object.
{ something: 'is this', wat : 2 }

Referencing other properties.

Available only in expressions, is the function ref(propertyId : string). The ref function will resolve other property values, and yield an appropriate type. Scalar property deferencing yields a number, etc.

Expressions search for properties relative to the property on which they are assigned. If the expression is assigned on the property  /room/skyboxIntensity, which has a sibling property /room/skyboxRotation, the following expression for the former is valid:

10 + ref('skyboxRotation')

Equivalently a non-relative, absolute, path can be used if needed:

10 + ref('/room/skyboxRotation')

References are tracked, and will ensure that the expression based property will update whenever the dependent property value changes. In the above case, this means that whenever skyboxRotation changes, skyboxIntensity would immediately be recomputed, as well.

Also note that references can resolve sub-properties. See the sub-properties document to understand what is available for use.

Dynamic references.

The referenced property can also be a reference! Let’s build an example:

Let’s create a new string property selectedObject, which will contain the propertyId path of a selected object (e.g. /objects/objectidhere) Let’s define our expression to be normally 10, and if the selected object is visible, 50.

10 + ref(`${ref('selectedObject')}/vis`) ? 40 : 0

Breaking this down, first let’s resolve the internal reference:

`${ref('selectedObject')}.vis` => “/objects/objectidhere" + "/vis" => “/objects/objectidhere/vis"

This fetches the visible boolean of the selected object, which is a boolean property.

In this expression, the produced value will be updated whenever either selectedObject changes, OR when the current selectedObject’s vis changes.

Dynamic references can only be nested once. ref(ref(ref('arg'))) will show warnings when assigned, and always produce the default value for the property type. If anyone reading this document finds a use for triply nested dereferencing, please let us know precisely why.

Animating / Referencing ‘time’.

There are two methods for incorporating time into an expression.

Expressions provide a ‘fake’ property to reference, called t. t always means ‘time since this expression was applied’. t will be synchronized among all participants in the space. If a user rejoins the space after a day, t will still be ‘seconds since this expression was applied’, which will be a large large value.

As a simple example, to set a simple occilating scalar value:

10 + Math.cos(ref('t'))

Or to assign a rotating vector:

Float3.fromPolar(ref('t'), 0, 2)

Time.

The /time property also exists. time is defined to be ‘seconds since the room was joined’. This value will be different for all users and is not synchronized. The time value should probably be not directly consumed. It primarily exists in order to handle synchronizing t.

Custom time values.

If you wish to synchronize multiple animations, it is recommended to create a new property to reflect this new time. This animation-time can be dependent upon t, or use a built-in playback generator type to make it easy. Then each animation can be dependent upon animation-time instead.


↑ Back to Contents

Holoscripting Definitions: Global Utility Types

From Holoscripting, these types are defined within the ‘@cavrnus/runtime’ module, and the function collections must be imported from this module.

For example:

import { out} from '@cavrnus/runtime';
out.warn(`Something is wrong!`);

Types are defined as they are using Typescript notation.

out : ConsoleT

‘out’ defines a set of output functions, useful for showing warnings and errors, or writing to the log or other consoles.

All message types will be written to the log, in additional to their listed behavior.

log(msg : any)

Write a message to the log file.

print(msg : any)

Write a message to the application console (available at the moment only in internal and staging builds, using F7)

debug(msg : any)

Write a message to the application console (available at the moment only in internal and staging builds, using F7)

warn(msg : any)

Write a message to the console, and, in internal and staging builds, present a dialog to the user with the contents of the warning.

warning(msg : any)

Alias for warn()

error(msg : any)

Write a message to the console, and present a dialog to the user with the contents of the message, regardless of build type.

verbose(msg : any)

No additional behavior.


↑ Back to Contents

Holoscripting Definitions: Math Types

The scripting and expression systems both provide access to a number of important underlying types and functions.

From Holoscripting, these types are defined within the ‘@cavrnus/runtime’ module, and the function collections must be imported from this module.

For example:

import { Float3 } from '@cavrnus/runtime';
const vector = Float3.new(1,2,3);

While writing expressions, no imports are possible. All of the types contained within this document are already imported and available for use.

For example:

Float3.new(Math.sin(Math.PI), 2, 4);

Also note, the Math namespace intrinsic to javascript is fully available.

Types.

Types are defined as they are using Typescript notation. In general, types are defined a simple interface, with a separate class of ‘static’ functions to operate or construct those interfaces. User code can construct an object to implement the interfaces itself if it wishes to.

Float3T.

A Float3T is a mathematical vector type, defined as Float3T, as:

export interface Float3T{x: number;    y: number;    z: number;}

Float3 contains the following functions.

Float3.new(x : number, y : number, z : number) : Float3T

Constructs a new Float3T. Equivalent to {x, y, z};

Float3.add(a : Float3T, b : Float3T) : Float3T

Adds the vectors, component-wise.

Float3.subtract(a : Float3T, b : Float3T) : Float3T

Subtracts the vectors, component-wise.

Float3.dot(a : Float3T, b : Float3T) : number

Computes the dot product, the sum of component-wise multiplication.

Float3.cross(a : Float3T, b : Float3T) : Float3T

Computes the cross product, yielding a vector perpendicular to both the inputs, with magnitude based on the product of the magnitude of the original vectors, and the sin of the angle between them.

Float3.scale(a : Float3T, b : number | Float3T) : Float3T

Scales/Multiplies the vector a by either a constant number b, or component-wise by a vector b.

Float3.rotate(a : Float3T, axis : Float3T, radians : number) : Float3T

Rotates the vector a around the axis axis, an angle of radians radians.

Float3.angleBetween(a : Float3T, b : Float3T) : number

Computes the angle between the vectors a and b, using the inverse cos of their normalized dot product.

Float3.fromPolar(theta : number, psi: number, dist : number) : Float3T

Computes a Float3 vector from the given angles theta (rotation around the up-vector/y-axis), and psi (tilt, around right-vector/x-axis), with magnitude dist.

= { x : -Math.sin(theta) * Math.cos(psi) * dist, y : Math.sin(psi) * dist, z : Math.cos(theta) * Math.cos(psi) * dist) }
Float3.magnitude(a : Float3T) : number

Computes the length of the vector a.

Float3.magnitude2(a : Float3T) : number

Computes the square of the length of the vector a.

Float3.toString(a : Float3T) : string

Produces a standard format string of the vector a.

Float4T.

A Float4T is a mathematical vector type, defined as Float4T, as:

export interface Float4T{x: number; y: number; z: number; w: number;}

Float4T thereby implicitly implements Float3T and can be used in any Float3 functions. 4-component vectors are used commonly when interacting with scripting and properties, but generally 3d vector math works off of 3-component vectors. Float4 accordingly has noticeably fewer functions.

Note you can build a Float4T from a Float3T concisely using the spread operator: {...f3, w:1}

Float4 contains the following functions:

Float4.new(x : number, y : number, z : number, w: number) : Float4T

Constructs a new Float3T. Equivalent to {x, y, z, w};

Float4.add(a : Float4T, b : Float4T) : Float4T

Adds the vectors, component-wise.

Float4.subtract(a : Float4T, b : Float4T) : Float4T

Subtracts the vectors, component-wise.

ColorT.

ColorT represents an RGBA color, and is used frequently in the properties and materials systems. Unlike Float3T and Float4T colors treat the fourth component (alpha) differently than the vector w component. It is defined as:

export interface ColorT{r: number; g: number; b: number; a: number;}

Color

Color.new(r : number, g : number, b : number, a? : number) : ColorT

Trivial constructor, equivalent to:

{ r, g, b, a : a ?? 1 }
Color.parse(hexstring : string) : ColorT

Interprets a color definition string with 3, 4, or 8 hex characters. Leading # or 0x are ignored.

  • #ff0 → yellow
  • #aaa0 → grey and transparent
  • #b00100ff → red and opaque
Color.add(a : ColorT, b : ColorT) : ColorT

Component-wise sum.

Color.subtract(a : ColorT, b : ColorT) : ColorT

Component-wise subtraction

Color.scale(a : ColorT, b : number | ColorT) : ColorT

If b is a number, scale the rgb components of a only.

If b is a ColorT, component-wise multiplication.

Color.hsv(hue : number, sat : number, val : number, alpha? : number) : ColorT

Computes a new color given hue (range 0 to 360), saturation (0 to 1), and value (0 to 1), and optionally included alpha, which defaults to 1.

Rand

Rand is a collection of random value generators.

Rand.random(a : number, b? : number) : number

If two args, generate a random number between a and b.

If one arg, generate a random number between 0 and a.

Rand.randomf(a : number, b? : number ) : number

Alias for random. f for float.

Rand.randomi(a : number, b? : number) : number

Generate a random integer from a to b, inclusively.

If a single arg, 0 to a, inclusively.

Rand.randomb() : boolean

Random boolean. Coin flip.

Rand.randomDisc(radius : number) : Float3T

Generate a random vector in the XZ plane, within radius distance of the origin. Y will = 0.

Rand.randomAnnulus(inner : number, outer : number) : Float3T

Like randomDisc, but with a minimum distance inner from the origin as well.

Rand.randomSphere(radius : number) : Float3T

Generate a random position within a radius radius sphere, of the origin.

Rand.randomShell(radius : number) : Float3T

Generate a random position on the surface of a sphere, of radius radius.

Rand.randomCube(radius : number) : Float3T

Generate a random position within a cube with each dimension radius.

Rand.randomGaussian(mean : number, stddev : number) : number

Generate a normally-distributed value, given mean mean, and standard deviation stddev.

Rand.randomColor(alpha? : number) : ColorT

Generate a random color with given alpha. RGB will be generated with Rand.randomf(0,1)

Shape.

A Shape represents a simple geometric shape. This type is used primarily to set up intersection tests. Shape is defined as a union type SphereT | AABBT, defined subsequently.

Geometry.

Geometry contains a few functions to compute information on Shapes.

Geometry.distance(a : Shape, b : Shape) : number

Computes the minimum spanning distance between the two Shapes.

Geometry.distanceBetween(a : Shape, b : Shape) : number

An alias for distance.

SphereT.

Represents a Sphere, generally used for bounds testing.

export interface SphereT{center: Float3T; radius: number;}

Sphere.

Sphere.new(center : Float3T, radius : number) : SphereT

Trivial constructor, equivalent to { center, radius };

AABBT.

Represents an Axis-Aligned Bounding Box, generally used for bounds testing.

export interface AABBT{min: Float3T;max: Float3T;}

AABB.

AABB.new(min : Float3T, max: Float3T) : AABBT

Trivial constructor, equivalent to: { min, max }


↑ Back to Contents

Holoscripting Definitions: Transforms

Transforms represent a position, orientation, and often scale, of an object. Transforms are an intrinsic type of the properties system, and are used for every object loaded into Cavrnus. Rather than represent transforms by a 3 by 4 matrix, Cavrnus represents a transform with a generic component-based system, so that each component can leverage other features of the properties system, such as expressions and cross referencing.

This document describes the utility types and functions that represent a transform from within Holoscripting. The same system describes the internal usage within the SDK, but the types will be named slightly differently.

From Holoscripting, these types are defined within the ‘@cavrnus/runtime’ module, and the function collections must be imported from this module.

For example:

import { Transform } from '@cavrnus/runtime';
Transform.srt(...);

Types are defined as they are using Typescript notation. In general, types are defined a simple interface, with a separate class of ‘static’ functions to operate or construct those interfaces. User code can construct an object to implement the interfaces itself if it wishes to.

TransformCompleteT

export interface TransformCompleteT{data : TransformDataT;updates : TransformUpdateT[];}

TransformCompleteT represents the complete state of a transform. A transform being in world-space or in local-space depends on the property usage, and cannot be inferred from the transform value by itself.

data : TransformDataT

Is the defining value for a transform. It will be either an SRT, SQT, or LookAt based transform data, defined below.

updates : TransformUpdateT[]

Updates are processes added to the transform after beginning with the data component. Updates do not present to the UI and are intended to be temporary overlay effects, such a small pulse to gather attention to an object.

Transform

Transform contains a set of type-narrowing functions to determine the nature of a TransformDataT, or a TransformUpdateT.

It also contains constructor functions for those types. Rather than detail each function we will just include the prototypes here:

srt(scale : Float3T, euler : Float3T, translation : Float3T) : TransformDataSRT;
sqt(scale : Float3T, quat : Float4T, translation : Float3T) : TransformDataSQT;
lookat(eye : Float3T, lookAt : Float3T, nominalUp? : Float3T | undefined) : TransformDataLookAt;
isSrt(data : TransformDataT) : data is TransformDataSRT;6isSqt(data : TransformDataT) : data is TransformDataSQT;
isLookAt(data : TransformDataT) : data is TransformDataLookAt;
shiftUpdate(moveBy : Float3T) : TransformUpdateShift;
lookAtUpdate(rotateToFacePt : Float3T, percentageToMove : number) : TransformUpdateLookAt;
scaleUpdate(scale : number) : TransformUpdateScaleUniform;
scaleNonuniformUpdate(scale : Float3T) : TransformUpdateScaleNonuniform;
rotateEulerUpdate(euler : Float3T) : TransformUpdateRotateEuler;
rotateQuatUpdate(quat : Float4T) : TransformUpdateRotateQuat;
toEulerUpdate(rotateToEuler : Float3T, percentageToMove : number) : TransformUpdateToEuler;
toQuatUpdate(rotateToQuat : Float4T, percentageToMove : number) : TransformUpdateToQuat;
isShiftUpdate(data : TransformUpdateT) : data is TransformUpdateShift;
isLookAtUpdate(data : TransformUpdateT) : data is TransformUpdateLookAt;
isScaleUpdate(data : TransformUpdateT) : data is TransformUpdateScaleUniform;
isScaleNonuniformUpdate(data : TransformUpdateT) : data is TransformUpdateScaleNonuniform;
isRotateEulerUpdate(data : TransformUpdateT) : data is TransformUpdateRotateEuler;
isRotateQuatUpdate(data : TransformUpdateT) : data is TransformUpdateRotateQuat;
isToEulerUpdate(data : TransformUpdateT) : data is TransformUpdateToEuler;
isToQuatUpdate(data : TransformUpdateT) : data is TransformUpdateToQuat;

TransformDataT

export type TransformDataT = TransformDataSRT | TransformDataSQT | TransformDataLookAt;

To narrow the type of a TransformDataT use the is* functions of Transform, defined above.

TransformDataSRT

export interface TransformDataSRT{scale : Float3T; euler : Float3T; translation : Float3T;}

TransformDataSQT

export interface TransformDataSQT{scale : Float3T; quat : Float4T; translation : Float3T;}

TransformDataLookAt

export interface TransformDataLookAt{ eye : Float3T; lookAt : Float3T; nominalUp? : Float3T;}

TransformUpdateT

export type TransformUpdateT = TransformUpdateShift | TransformUpdateLookAt | TransformUpdateScaleUniform | TransformUpdateScaleNonuniform | TransformUpdateRotateEuler | TransformUpdateRotateQuat | TransformUpdateToEuler | TransformUpdateToQuat;

TransformUpdateShift

Offsets the transform position without adjusting orientation.

export interface TransformUpdateShift{moveBy : Float3T;}

TransformUpdateLookAt

Partially rotate a transform to reorient it toward the given point.

export interface TransformUpdateLookAt{rotateToFacePt : Float3T; percentageToMove : Float3;}

TransformUpdateScaleUniform

Scale up a transform uniformly. Any changes to position updated after this will end up multiplied.

export interface TransformUpdateScaleUniform{scale : number;}

TransformUpdateScaleNonuniform

Scale up a transform non-uniformly, by each axis. This can produce skews in some circumstances, which may have unexpected side-effects.

export interface TransformUpdateScaleNonuniform{scale : Float3T;}

TransformUpdateRotateEuler

Incrementally rotate a transform, using euler angles.

export interface TransformUpdateRotateEuler{euler : Float3T;}

TransformUpdateRotateQuat

Incrementally rotate a transform, using a quaternion

export interface TransformUpdateRotateQuat{quat : Float4T;}

TransformUpdateToEuler

Partially rotate a transform to orient with the given euler angles.

export interface TransformUpdateToEuler{rotateToEuler : Float3T; percentageToMove : number;}

TransformUpdateToQuat

Partially rotate a transform to orient with the given quaternion based rotation.

export interface TransformUpdateToQuat{rotateToQuat : Float4T; percentageToMove : number;}

↑ Back to Contents

Holoscripting Systems: beginOperations(), OpContext

This document describes the purpose and functionality of the root level function:

beginOperations(options? : OpContextOptions) : OpContext.

This function is the primary entry point used to enact changes to the state of the space. Before reading this further, it is recommended to get a basic understanding of the Journal, Operations, and Transients.

beginOperations constructs an object the script may use to invoke transients and operations within the space. In general this is the only method to do so. All updates will go through this object. The function prototype is as follows:

- beginOperations(options? : OpContextOptions) : OpContext

OpContext

The OpContext is the object by which changes can be made. An OpContext can be constructed at almost any time within script, and its lifecycle can continue beyond the constructing function. Once given context is either .commit()ed, or .cancel()ed, it should be considered destroyed and discarded. Further use will throw exceptions.

Functions
op(operation : Operation, opOptions? : OpOptions) : void

Op is an extremely powerful and generic function, used to submit any type of operation.

When called this function will immediately send a transient version of the operation.

  • This is suppressed if the OpContext Options' sendTransients is false.

This function provides extremely low level and powerful access to sending operations, but because of that it is quite complicated! It is our recommendation that this function only ever be called as a last resort. After reading the Properties section below, the reasoning should be more clear.

  • commit() : void
  • This function concludes the Operations block, submitting all previously sent transient operations as finalized operations. Redundant updates (based on their unique id, see ‘OpOptions’ below) will be reduced and only send the final update.
  • cancel() : void
  • This function broadcasts a cancellation transient, effectively undoing all operations sent to this OpContext.
Properties

Three properties of the OpContext provide the utility needed to post operations without using .op(). They each provide a subset of functionality specific to different types of operations.

prop : OpContextProperties
  • prop provides functions used to update property values, as well as declare new properties. Further definition below.
create : OpContextCreations
  • create provides functions used to instantiate new objects, annotations, or chats. It also aids in removing created objects.
user : OpContextUser

user provides functions specific to updating user control properties. These are transient-only properties that make it possible to move the user’s view, and control their avatar. Take care when doing so not to make your users nauseous.

OpContextOptions

This options object provides some controls governing an OpContext. These options will apply to all operations posted to the OpContext through .op() or any child methods.

sendTransients? : boolean
  • Default is true.
  • When false, disables sending transient operations immediately when posting an operation. In this case, no changes will take effect until calling .commit()
undoable? : boolean
  • (NYI)
  • Default is/will be false.
  • If false, this operation will be not be placed on the undo stack. Ctrl-Z or other undo commands will bypass these changes.
individualUndo? : boolean
  • (NYI)
  • Default is/will be false.
  • If true, each individual operation will be placed on the undo stack separately. If false, undo will cancel the entire batch of operations as one.

OpOptions

This options object can be supplied to .op(), or any child-property function that submits an operation.

transientUniqueId? : string
  • If present, this unique id will uniquely identify an posted operation. Subsequent operations posted with the same unique id will override the previous operations, implicitly cancelling them. When the operations are commit()ed, only the final version of each uniqueid will be sent.
bypassLocalApplication? : boolean
  • Default is false.
  • By default, when sending a transient operation, the event is applied locally with no latency. In rare cases this may be an issue. Setting this to true will not apply the event locally, waiting instead to apply the transient event returned by the API server. If for whatever reason you wish all clients to receive the events in more synchrony, or there is no need for immediate responsiveness, this option may be useful.
  • As a note, if this option is undefined or false, and sendTransients is false, this will result in applying submitted operations locally only, with no network traffic or any synchronization whatsoever until .commit().
  • As an example, if you wish to have an object that tracks the player user, this will be different for every client, so this change needs to avoid synchronization, and run locally-only.

↑ Back to Contents

Holoscripting Systems: OpPropertiesContext

This document describes the purpose and functionality of the prop property of the OpContext, retrieved through .beginOperations().

This object is used to update property value generators and declare new properties. Like all components of OpContext (this, OpContextCreations, and OpContextUser, at the time of writing), all functions contained here eventually make their changes via OpContext.op(). All of these functions are mere helpers! But they are very helpful helpers, so do use them. :)

It would be best to understand the basics of properties before reading through the remainder of this document.

Unlike cav.prop.declareProperty(), properties defined using an operation are permanently stored in the space’s journal. There is no mechanism for removing this definition; though it can be altered afterwards.

Functions

WIP
  • op(operation : Operation, opOptions? : OpOptions) : void
  • Op is an extremely powerful and generic function, used to submit any type of operation.