Problem
Scripts that mirror numeric engine constants force every consumer to write opaque integers in attribute data. camera-frame.mjs is the motivating case — configuring it from data looks like this (from the Web Components ragdoll example):
<pc-script-instance name="cameraFrame" attributes='{
"rendering": { "toneMapping": 5, "samples": 4, "renderFormat": 12, "renderFormatFallback0": 18, "renderFormatFallback1": 7 },
"ssao": { "type": "combine", "blurEnabled": true, "intensity": 0.35, "radius": 0.6 }
}'></pc-script-instance>
"toneMapping": 5 and "renderFormat": 12 are unreadable and unwritable without a trip to the constants source. Note the contrast with "type": "combine" on the next line — the same script's SSAO enum is already a string enum, because the engine's SSAOTYPE_* constants are strings.
The same opacity affects everyone who touches these values as data: Editor scene JSON, script.create(CameraFrame, { properties: ... }) calls, Web Components markup, and documentation examples.
Why it happens
camera-frame.mjs declares two @enum {number} objects that mirror the engine's numeric constants — and hardcodes the values:
/** @enum {number} */
const RenderFormat = {
RGBA8: 7, // PIXELFORMAT_RGBA8
RG11B10: 18, // PIXELFORMAT_111110F
RGBA16: 12, // PIXELFORMAT_RGBA16F
RGBA32: 14 // PIXELFORMAT_RGBA32F
};
These are the only two @enum {number} declarations in the whole scripts/esm/ library — camera-controls.mjs, gsplat-flipbook.mjs and the other two enums in camera-frame.mjs itself all use string enums already.
The hardcoding is its own hazard: pixel-format values are positional implementation details, not stable API (PIXELFORMAT_R8 was appended later as 52). If formats are ever renumbered, this script — and the attribute data stored in every project that uses it — silently breaks.
Proposal
1. Fix camera-frame.mjs (bump to v1.3):
Declare the enums as strings:
/** @enum {string} */
const ToneMapping = {
LINEAR: 'linear',
FILMIC: 'filmic',
HEJL: 'hejl',
ACES: 'aces',
ACES2: 'aces2',
NEUTRAL: 'neutral'
};
/** @enum {string} */
const RenderFormat = {
RGBA8: 'rgba8',
RG11B10: 'rg11b10',
RGBA16: 'rgba16',
RGBA32: 'rgba32'
};
Resolve them in postUpdate() via lookup maps built from imported TONEMAP_* / PIXELFORMAT_* constants (removing the hardcoded numbers):
import { ..., TONEMAP_LINEAR, PIXELFORMAT_RGBA8, ... } from 'playcanvas';
const TONE_MAPPINGS = { linear: TONEMAP_LINEAR, filmic: TONEMAP_FILMIC, /* ... */ };
// numbers pass through for back-compat
const resolve = (map, value) => (typeof value === 'string' ? map[value] : value);
Notes:
- Back-compat: existing Editor projects store the chosen numeric values (5, 12, 18…) in scene data, and code users pass
TONEMAP_* constants — the typeof passthrough keeps all of that working. Unknown strings should warn once (it is a per-frame path) and fall back to the default.
- Editor: no changes needed — the attribute parser already handles
@enum {string}, as SsaoType and DebugType in this same file prove.
Attribute data then reads:
"rendering": { "toneMapping": "neutral", "renderFormat": "rgba16", "renderFormatFallback0": "rg11b10", "renderFormatFallback1": "rgba8" }
2. Codify the convention: script attribute enums in scripts/esm/ should use readable string values; where a script needs a numeric engine constant, it translates the string internally via imported constants. This is already the de facto pattern everywhere except these two enums, and matches the direction of the engine's own newer APIs (SSAOTYPE_*, DITHER_* are string constants). A line in AGENTS.md would prevent regressions.
(The legacy scripts/ directory has a few numeric attributes.add enums too — e.g. fly-camera.js mode: 0/1 — but those are Editor-dropdown-driven and legacy, so arguably out of scope.)
Problem
Scripts that mirror numeric engine constants force every consumer to write opaque integers in attribute data.
camera-frame.mjsis the motivating case — configuring it from data looks like this (from the Web Components ragdoll example):"toneMapping": 5and"renderFormat": 12are unreadable and unwritable without a trip to the constants source. Note the contrast with"type": "combine"on the next line — the same script's SSAO enum is already a string enum, because the engine'sSSAOTYPE_*constants are strings.The same opacity affects everyone who touches these values as data: Editor scene JSON,
script.create(CameraFrame, { properties: ... })calls, Web Components markup, and documentation examples.Why it happens
camera-frame.mjsdeclares two@enum {number}objects that mirror the engine's numeric constants — and hardcodes the values:These are the only two
@enum {number}declarations in the wholescripts/esm/library —camera-controls.mjs,gsplat-flipbook.mjsand the other two enums incamera-frame.mjsitself all use string enums already.The hardcoding is its own hazard: pixel-format values are positional implementation details, not stable API (
PIXELFORMAT_R8was appended later as52). If formats are ever renumbered, this script — and the attribute data stored in every project that uses it — silently breaks.Proposal
1. Fix
camera-frame.mjs(bump to v1.3):Declare the enums as strings:
Resolve them in
postUpdate()via lookup maps built from importedTONEMAP_*/PIXELFORMAT_*constants (removing the hardcoded numbers):Notes:
TONEMAP_*constants — thetypeofpassthrough keeps all of that working. Unknown strings should warn once (it is a per-frame path) and fall back to the default.@enum {string}, asSsaoTypeandDebugTypein this same file prove.Attribute data then reads:
2. Codify the convention: script attribute enums in
scripts/esm/should use readable string values; where a script needs a numeric engine constant, it translates the string internally via imported constants. This is already the de facto pattern everywhere except these two enums, and matches the direction of the engine's own newer APIs (SSAOTYPE_*,DITHER_*are string constants). A line inAGENTS.mdwould prevent regressions.(The legacy
scripts/directory has a few numericattributes.addenums too — e.g.fly-camera.jsmode: 0/1— but those are Editor-dropdown-driven and legacy, so arguably out of scope.)