Skip to content

Script attribute enums should be readable strings, not raw numeric constants #9210

Description

@willeastcott

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.)

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions