Skip to main content

Shared packages

You can import these packages in your own code. Here is what you can use where, and what each package gives you.

Entities are objects

In code, the objects you see in the editor are called entities. They are the same thing.

Which packages you can import depends on where your code runs:

PackageWhat it gives youIn a scriptIn a pluginIn a component extension
@was/ecsentities, components, the world✓✓✓
@was/enginethe component classes✓✓✓
@was/signalsreactive values—✓✓
@was/svdtschemas and validation✓✓—
@was/utilssmall helpers✓✓—
@was/uithe editor's own component library—✓✓
@was/iconsthe editor's icon set—✓✓
reactReact 19—✓✓
In a script, use ctx.effect

A script cannot import @was/signals directly. Use ctx.effect instead. It works the same way, and it stops when its object goes away, so nothing is left running.

@was/signals: reactive values​

A signal is a value that knows who reads it. When it changes, only the things that depend on it update.

import { signal, computed, effect, batch, untracked } from '@was/signals';

const score = signal(0);
const doubled = computed(() => score.value * 2);

const stop = effect(() => {
render(score.value); // runs again only when score changes
});

batch(() => {
score.value += 1;
score.value += 1; // effects run once, not twice
});

stop();
FunctionWhat it does
signal(v)a value that can be watched; read and write .value
computed(fn)a derived value, recalculated only when what it reads changes
effect(fn)runs now, and again whenever what it read changes; returns a stop function
batch(fn)groups writes, so watchers run once at the end
untracked(fn)reads a value without depending on it
ref(obj)makes a whole object reactive, down to every nested value
snapshot(obj)a plain, non-reactive copy
raw(obj)the underlying object, without tracking
readonly(obj)a view that cannot be written to

For React panels, a companion package gives you useSignal, useComputed, useSignalEffect and useLiveSignal. A component then re-renders from a signal with no extra wiring.

@was/ui: editor components​

Build plugin panels and component extensions from the same components the editor uses. Your panels then look like part of the editor, instead of an embedded web page. The library has these components:

Accordion · Avatar · Button · Card · Checkbox · Chip · CloseButton
DimensionInput · Draggable · Dropdown · Flag · Icon · IconButton · Input
Menu · Modal · Outside · Panel · Popover · Portal · Render · Scroll
Search · Section · Segmented · Select · Skeleton · Switcher · Tabs
Toast · Toolbar · Tooltip

For icons, use @was/icons.

The stylesheet is prebuilt

Panels can use only the utility classes that ship with the library. Arbitrary values like text-[13px] are not included. For sizes outside the scale, use an inline style.

@was/ecs: entities and components​

This package has Entity, Component and Space. With it, a plugin reads and changes a scene through the full API, not a separate, limited one.

Space: the world​

CallReturns
getEntity(id)one entity, or nothing
hasEntity(id)whether it exists
getEntities()all of them
queryEntities(A, B, …)every entity that has all of those components
makeQueryEntities(A, B, …)the same query, built once for repeated use
createEntity(id?, components?)a new entity, added to the world
addEntity(…e) · removeEntity(…e)adds an entity to the world or removes it
getSystem(S) · hasSystem(S)a system, or whether it exists
execute()runs one frame
Use queryEntities to find entities

Asking for "everything with a light and a transform" this way is fast. getEntities() gives you the whole world, and you have to filter it yourself.

Entity: one object in the world​

CallWhat it does
getComponent(Type)returns one component, or null
getComponents(A, B)returns several at once, in that order
hasComponent(A, B)checks whether it has all of them
addComponent(…c)adds components; a type it already has is ignored
removeComponent(…c)removes components, by class or by instance
component(fn)runs fn for every component, now and in the future
clone() · clean()copies the entity, or removes all its components
.id · .componentsits id, and its components keyed by type

Component: the data​

CallWhat it does
.x or get('x')reads a field; inside an effect, it also subscribes to it
$datathe whole component as a plain object
$rawDatathe stored object, without subscribing; read-only
update({ … })writes data. This is the only way to write.
updateAt(path, value)writes deep inside a large component without re-validating all of it
version(field?)a counter that goes up when something changes, so you can depend on a field without reading it
reset(data?)goes back to the defaults
clone()makes a copy
Use version() for large lists

Reading a large value subscribes you to every item inside it. Depend on its version counter instead: you learn that it changed without watching every item. This is how a panel stays fast with a list of twenty thousand items.

@was/engine: component classes​

Every component type is a class: TransformComponent, MaterialComponent, RigidBodyComponent and the rest. Import the class and pass it to getComponent, hasComponent or query.

For the full list with every field, see the component reference.

@was/svdt: schemas​

Use this package to describe the shape of your data and check it.

import { s, compile } from '@was/svdt';

const schema = s.object({ speed: s.f64(1), name: s.string('') });
const codec = compile(schema); // compile once, where you declare it, never on every call
const value = codec.parse(input);

Describe a shape​

  • Numbers: s.f32 s.f64 s.i8 s.u8 s.i16 s.u16 s.i32 s.u32, each with a default.
  • Scalars: s.bool, s.string, s.color, s.literal, s.enum, s.unknown, s.ref.
  • Vectors: s.vec2, s.vec3, s.mat4.
  • Composites: s.object, s.variant, s.array, s.record, s.union, s.preprocess, s.lazy.

You can chain these on any of them: .default(v), .optional(), .nullable(), .min(n), .max(n), .int().

Use a compiled codec​

CallWhat it does
parse(input)validates and normalises; throws on bad input
safeParse(input)the same, but returns success or an error instead of throwing
parseAt(path, v)validates one field without touching the rest
equals(a, b)deep comparison
diff(a, b)returns what changed
apply(target, p)applies a diff
invert(p)reverses a diff
pack · unpackconverts to and from a compact binary form

To inspect a schema itself instead of data, use introspect, keys and requiredKeys.

@was/utils​

Small helpers: pick, omit, assign, keys, values, entries, capitalize, basename, extname, dispose (combines cleanup functions into one), and MIME-type helpers for checking file types.


Next: Glossary — the terms used across these docs.