@pixi-ui-editor/runtime-pixi
v0.27.0
Published
PixiJS runtime that renders Pixi UI Editor scene documents in a game.
Downloads
3,023
Readme
@pixi-ui-editor/runtime-pixi
PixiJS runtime that loads and renders scene documents produced by Pixi UI Editor inside a game.
Consumes documents validated against @pixi-ui-editor/schema.
Install
pnpm add @pixi-ui-editor/runtime-pixiReparent live views
Use reparentPreservingWorld for a live Pixi Container created by game code or taken from a
mounted scene. It recalculates the ancestor chain instead of reading Pixi's render-pass cache, so the
view does not jump even before the first frame:
import { reparentPreservingWorld } from "@pixi-ui-editor/runtime-pixi";
if (!reparentPreservingWorld(symbolView, bonusLayer, 0)) {
// World transform could not be preserved: the destination parent, or the view's own current
// world matrix, is singular (for example scale.x === 0). The tree move still happened.
}containerWorldMatrix, applyWorldTransform, and matchWorldTransform expose the same live-view
layer for lower-level use. applyWorldTransform returns false and leaves the local transform
untouched when either the target world matrix or the current parent is singular; on its successful
branch it also clears any stale view.skew so it cannot shear the result.
Use NodeTransformSpace instead when moving serialized document nodes: it additionally resolves
layout profiles, anchors, Canvas scaling, and layout-managed rectangles.
Spawn a preset
import { loadPrefabView } from "@pixi-ui-editor/runtime-pixi";
import { materializedPrefabNodeId } from "@pixi-ui-editor/schema";
const configured = scene.nodes.find(
(node) => node.type === "prefab-instance" && node.bindingKey === "rewardCardTemplate",
);
if (configured?.type !== "prefab-instance") throw new Error("Configured preset instance is missing.");
const labelDefinitionNodeId = document.prefabs
.find((prefab) => prefab.id === configured.prefabId)!
.nodes.find((node) => node.bindingKey === "label")!.id;
const mounted = await loadPrefabView(
document,
configured.prefabId,
"desktop",
resolveFileUrl,
{ interaction: "runtime" },
{
propertyOverrides: configured.propertyOverrides,
locallyAddedNodes: configured.locallyAddedNodes,
},
);
gameLayer.addChild(mounted.root);
const labelView = mounted.nodeViews.get(
materializedPrefabNodeId(mounted.instanceId, labelDefinitionNodeId),
);Omit the last argument to instantiate the prefab definition defaults. nodeViews contains the outer
instance plus deterministic instance-scoped IDs for inherited children and stable IDs for local children.
The lookup above is two-step on purpose, and it is the contract: a bindingKey is unique within the scope
it resolves against, not across the whole document. A key on a scene node — including a preset instance
itself — is unique within that scene. A key inside a preset definition is unique within that definition and
resolves against one instance, which is what makes it usable for presets you spawn many times: a hundred
symbol views can each carry "spine" without colliding. So you find the instance by its scene-level key,
then find the node inside it by its definition-level key.
Reuse parsed assets across loads
loadPrefabView and loadSceneView fetch and parse every asset a scene references. Spawning presets one
by one — a symbol view per reel stop, a card per deal — repeats that work for each call: the skeleton JSON
is re-parsed, the atlas is rebuilt, and every texture becomes a separate GPU upload of the same file.
Pass an assetCache you own to share the parsed results. Its lifetime is yours: keep one per scene session
and drop it with the scene.
import { createSceneAssetCache, loadPrefabView } from "@pixi-ui-editor/runtime-pixi";
const assetCache = createSceneAssetCache();
const mounted = await loadPrefabView(document, prefabId, "desktop", resolveFileUrl, {
interaction: "runtime",
assetCache,
});Each map is keyed by the asset file's URI, so the same file assigned to different assets loads once. Without
assetCache every call starts from empty maps, which is fine for a single scene load and wasteful for
anything spawned repeatedly. The lower-level loadSceneTextures / loadSceneSpines / loadSceneFonts /
loadSceneSounds take the individual maps directly if you drive loading yourself.
Scene viewport
buildSceneView and loadSceneView return a runtime-owned SceneViewport as root. With
interaction: "runtime", that object clips content to the authored reference viewport and owns the
only contain-fit calculation used by Preview and consuming applications:
const { root } = await loadSceneView(document, sceneId, profile, resolveFileUrl, {
interaction: "runtime",
});
app.stage.addChild(root);
root.resize({ width: app.screen.width, height: app.screen.height });Call root.resize(...) after the Pixi renderer changes size. It applies one uniform scale and centers
the unchanged authored viewport; letterbox and pillarbox space never reveal off-viewport content.
Hosts must not reproduce the fit formula or add their own viewport mask. Spawned prefab views stay
unclipped because they are scene content rather than player-screen boundaries.
Use resolveViewport for physical host measurements. It returns null while the host is collapsed or
non-finite, preventing a transient 0×0 measurement from selecting desktop and applying a singular
transform. resolveProfileForViewport and fitViewportToHost remain available as low-level primitives;
passing the live profile to the former enables a five-percent breakpoint dead zone.
const resolved = resolveViewport(
document.settings,
scene.layout.referenceViewports,
host,
mounted.profile,
);
if (resolved === null) return; // keep the last usable profile and transform
if (resolved.profile !== mounted.profile) applyProfile(mounted, document, sceneId, resolved.profile);
mounted.root.resize(host);Crossing the breakpoint does not require rebuilding or reloading assets. SceneViewResult.profile is
the source of truth for the live tree, and applyProfile updates its reference viewport, Canvas scale,
node transforms/styles, layout-managed children, page groups, item tables, scroll content, and fixed-grid
line breaks in place. Runtime state such as the current button/page, capacity override, and scroll offset
is retained; applying the already-live profile is a no-op.
Localization
buildSceneView, loadSceneView, buildPrefabView and loadPrefabView return a SceneLocalization
next to root and nodeViews. A Text node that carries a textKey already renders its base-locale
translation without any extra call; a document without keys behaves exactly as before.
const { root, localization } = await loadSceneView(document, sceneId, "desktop", resolveFileUrl, {
interaction: "runtime",
});
localization.setLocale("pt-BR");
// Values ship as a flat Record<string, string> in locales/<locale>.json, so any dictionary works.
localization.setTranslate((key, { locale }) => dictionaries[locale]?.[key]);
// `{amount}` inside a translation is a runtime parameter, not a second key.
localization.setParams({ amount: 48 });
// An immutable editor/game store hands over a new document copy without rebuilding the scene.
localization.syncDocument(nextDocument);Resolution order is translate → requested locale → base locale → Text.text → the key itself; a
translate returning undefined keeps the chain going. Every call mutates the existing TextNodeView
objects: the scene root, NodeView identity and live control state are preserved, and re-rendering the
same string does no work at all.
