Browser-based visual editor for building WebGL, WebGPU, WebXR apps
| Files | Last commit | Last update |
|---|---|---|
chore(deps): update actions/setup-node action to v7 (#2150) | 2 months ago | |
fix: added images folder back | 1 year ago | |
fix: prevent simple tooltips from intercepting pointer events (#2229) | 12 days ago | |
feat: align Editor writes with complete ShareDB schemas (#2238) * feat: repair incomplete ShareDB documents * refactor: delegate ShareDB backfill to the backend * fix: preserve texture atlas inspector across schema catalogs | 8 days ago | |
[FIX] Improve How Do I tip on transforming entities (#1637) * [FIX] Improve How Do I tip on transforming entities * More link fixes | 8 months ago | |
chore(deps-dev): bump js-yaml from 4.3.0 to 4.3.2 in /test-suite (#2204) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.0 to 4.3.2. - [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.2/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.3.0...4.3.2) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.3.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> | 12 days ago | |
feat: align Editor writes with complete ShareDB schemas (#2238) * feat: repair incomplete ShareDB documents * refactor: delegate ShareDB backfill to the backend * fix: preserve texture atlas inspector across schema catalogs | 8 days ago | |
[FEAT] Test suite config (#1782) * feat: add support to loading multiple accounts * refactor: Changed PC_COOKIE_VALUES to PC_COOKIE_VALUE * feat: remove cloudfront limiter and inject headers for PlayCanvas API requests * feat: add PC_HEADER_NAME and PC_HEADER_VALUE environment variables for test execution * feat: add PC_HEADER_NAME and PC_HEADER_VALUE to .env.template files * feat: add '--disable-web-security' argument to Chrome launch options in Playwright config * feat: replace .env.template with a symlink to test-suite/.env.template | 6 months ago | |
feat: generate fonts in the Editor as referenced JSON + atlas assets (#2190) * feat(assets): client-side font generation foundation - add @playcanvas/font-tools devDependency - font-generate worker (msdfgen wasm + canvas backend) + served wasm - fonts:generate editor method driver - build wiring: wasm copy to dist/js, worker import.meta define + node stubs - font-tools ambient type shim Groundwork for referenced font import (folder + json/texture mirror assets). v1 skips fontkit kerning to keep the worker bundle polyfill-free. * feat(assets): pass noConvert through assetCreate lets the editor upload a source asset (e.g. a client-side-generated font's .otf) without triggering the server-side conversion pipeline * feat(assets): client-side referenced font import - fonts:import — on font upload (gated by hasClientFontImport flag), create a folder + source .otf (noConvert) + JSON/Texture mirror assets + a runtime font baked from them (v3->v2), with data.jsonAsset/textureAssets refs - point the font's file at the page-0 texture mirror (single-page dedup) - fonts:rebake + mirror watch — re-bake the font when a mirror's file changes - fonts:reprocess — Process Font on a referenced font regenerates client-side and refreshes the mirrors instead of hitting the server pipeline - font inspector: SOURCE FILES panel with JSON + Texture pickers - noConvert now flows through assetUpdate too * wip: font gen * feat(fonts): add ReferencedFontHandler resolving json+texture references * fix(fonts): add proper type annotations to ReferencedFontHandler * chore(fonts): drop unused Asset import * feat(fonts): register ReferencedFontHandler in editor viewport and launch Swap the stock pc.FontHandler for ReferencedFontHandler in both engine contexts so client-referenced fonts resolve their json+texture asset data at load time, matching the server-rendered editor viewport and the launch preview. * feat(fonts): rework import to reference-only font; reload on ref change Stop baking v2 font data into the runtime font asset. importFont now creates the font with data = { jsonAsset, textureAssets } and a tiny placeholder file (needed only to trigger AssetRegistry.load() so ReferencedFontHandler's async reference resolution runs); the atlas is no longer duplicated into a third copy. Removes the now-dead toV2/rebake/fonts:rebake/baking machinery. The assets:add watcher that used to trigger a rebake on referenced-file change or repoint now reloads the font's engine resource instead, so the handler re-resolves and the viewport/thumbnail pick up the change. * fix(fonts): reload referenced font from engine signals, not observer events The reload watcher fired on the editor observer's data/file changes, but assets-registry syncs those into the engine asset on a deferred tick — so the handler re-resolved against stale engine data (repoint) or stale mirror resources (json/atlas edit). React to engine-side signals instead: a mirror's load:${id} (content edits) and the font engine asset's data change (repoints). * feat(fonts): hide intensity panel for referenced fonts data.intensity is inert for reference-only fonts (the handler builds pc.Font from the json descriptor and FontHandler.patch never applies it), so gate the PROPERTIES panel on the referenced discriminator like SOURCE FILES. * fix(fonts): make the reference font a usable target, not a source The reference font's placeholder file was named `${base}.font`; the backend marks a font-with-file as a source asset unless the file extension is a target extension (asset.ts: source = type!=='binary' && !TARGET_EXTENSIONS[ext]), so it showed as 'Font (source)' with no usable font, and source:true also forced preload off. Name the placeholder `${base}.json` (a target extension, honest for its {} content) so it becomes a preloadable target font. Regression from the reference-only rework, which changed the placeholder from the atlas .png. * fix(fonts): render referenced MSDF atlas as linear, not srgb Texture assets default to srgb (assets-srgb), and the engine samples the atlas through sRGB gamma, corrupting the MSDF median distance reconstruction (broken corners/thin strokes). Force each resolved atlas texture to srgb=false when building the pc.Font, matching the stock font handler (raw non-srgb atlas). * fix(fonts): rebuild referenced font on repoint via observer events The reload watcher keyed repoint off the engine asset's 'change' event, which only fires when the data object identity actually changes and proved unreliable. Trigger off the observer data:set events instead and push the fresh refs onto the engine asset before rebuilding, so a repointed json/texture updates the viewport and thumbnail. Mirror-content edits still reload via the mirror's engine 'load'. * fix(fonts): don't corrupt referenced font resource on ref change The engine text element's _onFontChange does this._font.data = asset.data on any font-asset data change, assuming asset.data is a descriptor. For a referenced font asset.data is refs ({jsonAsset,textureAssets}) with no chars, so it crashed (_getPxRange -> Object.keys(undefined)) when a picker was cleared/repointed. Stop the refs-only data from reaching the engine asset's data setter: skip the assets-registry data sync for referenced fonts, and in the reload watcher set the engine asset's refs via _data (bypassing the setter) before unload/load. * feat: convert legacy fonts to referenced assets * fix(fonts): resolve font references outside the load call stack Defer the reference lookup so it runs once the caller's synchronous work has settled, rather than inline while AssetRegistry.load() is still on the stack. * fix(fonts): keep meta and rebuild reliably when references change Converting a legacy font wrote `meta` before uploading the placeholder file, but the server clears `meta` on any asset file update — normally the conversion repopulates it, and that is exactly what is skipped here. The character set was lost, so the Characters field came back empty and the next regenerate produced a font with no glyphs. Write `meta` after the upload. Repointing the json and atlas references in the same tick started two overlapping rebuilds that could resolve out of order, leaving the new descriptor paired with the previous atlas. Coalesce rebuild requests into one per tick, and clear the loader cache for the font url, which never changes for a reference-only font and so would otherwise return the previously built font. * chore(fonts): use type aliases for the font-tools declarations `consistent-type-definitions` rejects `interface`, which failed lint. * fix(fonts): validate the referenced descriptor and expose every atlas page The json and texture refs are user-editable by design — the point of unpacking them is that you can supply your own pre-converted assets — so the descriptor reaching pc.Font is untrusted. - Validate it in the handler. pc.Font's data setter rewrites info.maps from info.width/height whenever version is missing or below 2, which NaNs every glyph UV of an otherwise valid file; pin the version so it leaves it alone, and reject descriptors missing info.maps or chars, or keyed by codepoint rather than letter. - Re-apply the atlas options the stock handler forces. These are plain texture assets carrying the user's own settings, and mipmaps bleed glyphs together at small sizes just as srgb corrupts the distance reconstruction. - Expose data.textureAssets as an ordered list instead of only index 0. A hand-supplied atlas may pack into a different number of pages, and page order is what chars[].map indexes into. - Report the states the handler soft-fails on in the SOURCE FILES panel, so a bad pairing reads as an explanation instead of blank text. Also pin @playcanvas/font-tools exactly, matching the other @playcanvas deps, and promote @playcanvas/msdfgen-wasm to a direct dependency since the build copies its wasm out of node_modules by path. * test: drop the frontend unit tests added on this branch This repo does not take unit tests for editor changes. * fix(fonts): unbind referenced-asset error handlers once a load settles The handler registered a `once('error')` listener per referenced asset on every load and never removed it on the success path, so one leaked per asset per rebuild. Worse, rebuilds clear the loader cache and reload under the same key, so a stale handler firing later called the loader callback for an already finished load and could fail the request that was in flight at the time. Route every exit through a single `settle()` that unbinds the listeners it registered, and bind them in a separate pass before any `ready()` call, since `ready()` fires synchronously for an already-loaded asset and could otherwise settle the load before the remaining handlers were bound. * fix(fonts): report generation and asset failures instead of hanging Three paths could leave a promise pending forever, which stranded the `updating` set, permanently blocked later rebuilds of that font and left the inspector button disabled reading "REGENERATING FONT ASSETS…" until a reload: - `fonts:generate` bound its 'error' callback inside the 'ready' handler, so a worker that 404s or throws at top level fired 'error' with nobody listening. It also never caught the rejection from the async `start()`. - `getObserver` waited on `assets:add[id]` with no timeout. - `reprocess` fed an unchecked `fetch` response to the generator, so an error page was parsed as font bytes. Bind the worker error handler before start, funnel every exit through one `settle()` so `stop()` is never skipped, bound the asset wait, and check `res.ok` before reading the source file. Also surface orphaned atlas pages when a regenerated descriptor packs into fewer pages than before, rather than silently leaving unreferenced assets in the folder, and mark a font watched only after the viewport app is known to exist so it can still be watched later if it is not. * fix(fonts): read referenced font intensity from the descriptor A referenced font's `data` holds only the json/texture references, so it has no `intensity`. Two places got that wrong: - `migrateFont` only checked `source` and `data`, so it wrote `data.intensity = 0` onto every referenced font on each project open until the value persisted — a realtime op adding a field the referenced path never reads. - The thumbnail renderer read `data.intensity` off the observer, so it picked up whatever the migration had backfilled rather than the descriptor's own value. Every other read in that function already used the loaded resource. Skip the migration for referenced fonts and take intensity from `resource.data.intensity`, which pc.Font guarantees for both font types. * feat(fonts): confirm before converting a legacy font Converting a legacy font rewrites `data`, replaces the asset file and drops the generated descriptor, none of it through history — so it cannot be undone. The button that used to be an idempotent "PROCESS FONT" now performs that one-way conversion, so gate it behind a confirm dialog. Regenerating an already referenced font is unchanged and still a single click. Also reset the processing flag before touching the panel in the reprocess continuation and bail if the inspector was unlinked meanwhile, otherwise selecting another asset mid-regenerate wrote to a stale element and left the button permanently disabled on relink. Document the source-file pickers via the reference tooltips, including that the referenced atlas textures are reconfigured for MSDF sampling when the font loads, and explain why the registry drops a queued `data` flush. * chore: lint fix * chore(fonts): share isRefPath and record the handler's duplication `isRefPath` was written out twice, in assets-font-import and the font inspector, both needing the same three-clause predicate over `data`, `data.jsonAsset` and `data.textureAssets`. Export it from the shared handler module the inspector already imports `normalizeFontJson` from. Also record two invariants that currently hold only by luck: - EMPTY_FONT is shared by every soft-failed font, and pc.Font's data setter writes into whatever `data` it is handed, inserting `info` when absent and rewriting `info.maps` when version < 2. This shape reaches neither branch, so both `version: 3` and the present `info` are load-bearing. - The handler is hand-mirrored into pipeline/templates/start.js and pipeline/templates/esm/start.mjs, which have no build step and cannot import it. Name all three copies in each, so a behaviour change cannot quietly land in one and render a font differently in a published build. * fix(fonts): count referenced font mirrors as asset usages `keys` had no `font` entry, so `data.jsonAsset` and `data.textureAssets` were invisible to the usage index. The json descriptor and atlas pages of a font in active use were therefore drawn with CLASS_ASSET_NOT_REFERENCED in the asset panel, deleting one raised no in-use warning, and assets-srgb's `findUsages` could not see them either. The failure mode was quiet and plausible: a user tidying up assets the editor itself labelled "not referenced" deleted them, and the font soft-failed to a blank one. Index the json ref through `keys.font` and the atlas pages through a `font` handler pair modelled on `model`/`model-insert`/`model-remove`. The `cubemap` entry is no use as a template because it relies on six fixed slots, whereas the page list is variable length and its order is what `chars[].map` indexes into. Count exactly one level per write. The observer emits the leaf events for a write and then the redundant container events above them — 'data.textureAssets.0', then 'data.textureAssets', then 'data' — so counting more than one level counts every ref twice; against a live project that drove counts to -2 and 3. For the page list the container is the level that works: a shrinking array emits no leaf event for the index it dropped, so leaves alone leak that ref. A `data` write always emits its children first and unsets the keys it drops, so the `data` container itself adds nothing. * fix(fonts): tear down referenced font watchers when the asset is removed `watchFont` bound four observer handlers plus one registry handler per mirror and never unbound any of them, and `watched` held the asset id forever. Every deleted referenced font therefore leaked those handlers, and any later re-add of that id — a merge, a realtime resync — was treated as already watched and silently stopped rebuilding the font on a ref change. The stale `pending` and `queued` entries were worse: assets-registry unloads the engine asset and drops it from the registry on removal, so a retry armed in `rebuildFont` never fires, and the leftover id then short-circuits every later rebuild of that font. Hold the teardown per id and run it on `assets:remove`, clearing the transient sets at the same time. Also unbind the `assets:add[id]` handler when `getObserver` times out rather than leaving it bound, take `isRefPath` from the shared module, and note why the json mirror is not preloaded — the font's own load resolves it either way. * fix(fonts): read font intensity off the resource, not its data 4d7c5f4b9 moved the thumbnail's intensity read to `resource.data.intensity` on the grounds that pc.Font guarantees it for both font types. It does not. What pc.Font guarantees is `resource.intensity`, which its constructor initialises to 0 and its data setter only overwrites when the descriptor actually carries the field. A descriptor that omits `intensity` leaves `data.intensity` undefined, and EMPTY_FONT — the blank font the handler soft-fails to when a reference does not resolve — has no `intensity` at all, so the shader parameter was being set to undefined. Confirmed against a font with a cleared json reference: `resource.data.intensity` is undefined there while `resource.intensity` is 0. * fix(fonts): refresh the regenerate button for the font linked now 7c86c2ada reset `_processing` before touching the panel and bailed when the inspector had been unlinked meanwhile, which stopped the write to a stale element — but nothing then re-rendered the button for the asset selected in the interim. The typed asset inspectors are cached instances reused through link/unlink, so selecting font B while font A was regenerating left B's button disabled reading "REGENERATING FONT ASSETS…" until the user selected away and back. Refresh the button for whatever is linked once the flag clears, ahead of the unlinked-asset bail. Also use the shared `isRefPath` for the source-file ref filter. * fix(fonts): name the failure when the font generator cannot load There is no server-side fallback for font import, so a worker or msdfgen.wasm that fails to load has to say what actually went wrong. Reporting it as the generic "font generation failed" reads as a problem with the font file, sending the user after the wrong thing. Also record two ceilings in the worker: the inversion probe samples a single corner pixel of the first glyph that generates, so it can misread when pxrange is large relative to size; and glyph source disposal is deliberately not in a finally, because the client stops the worker on every settled path and the wasm heap goes with it. * chore(fonts): clarify the font comments and drop the duplicated msdf defaults Comment pass over the font work: - Remove three `ponytail:` markers. That prefix is a working-mode convention from outside this repo and means nothing to anyone reading it here; the notes they carried are kept as plain prose. - Replace the winding probe's raw codepoint list with PROBE_CHARS = 'HXAoe'. `[72, 88, 65, 111, 101]` gave no hint that these were characters, let alone which ones. - Take the probe's size and pxrange fallbacks from font-tools' exported GLYPH_SIZE and PXRANGE instead of repeating 64 and 8. They were copies of that package's own defaults, so the probe would have silently run at different settings from generateFont if the package ever changed them. - Replace the "1/2)", "3/4)", "5)" step numbering in importFont, which described a sequence the reader could not see, and split the long step-5 note into the two things about it that are actually load-bearing. - Trim the rest to the house style of short and lowercase, and make several vaguer lines say what they mean: what the registry sync guard is protecting against, and why the usage index counts font mirrors at all. * fix(fonts): keep font asset retries working when swapping the handler Replacing the stock font handler silently disabled retrying for every font in every project, legacy ones included. Two separate causes: - ReferencedFontHandler wrapped a private FontHandler instance. ResourceLoader's enableRetry() assigns maxRetries to the object registered in the loader, so the wrapper absorbed it as an inert property while the inner handler kept the maxRetries of 0 from its own constructor — and that inner value is what FontHandler.load() passes to http.get. Subclass FontHandler instead, so the registered object is the one that reads it. That also restores `app` and the parser registry, which the wrapper did not forward, and lets patch() and the ResourceHandler casts at both registration sites go away. - The editor viewport swaps the handler after the app has already called enableRetry(), which only touches handlers registered at that moment, so the replacement started at 0 regardless. Carry the outgoing handler's retry count across the swap. Confirmed in the editor: the font handler now reports the same maxRetries as every other handler, and a failing font request retries five times with the expected backoff instead of failing outright. A legacy-shaped asset still routes to the inherited implementation, which is now the engine's own code rather than a re-implementation. * chore: ignore playwright mcp scratch output Driving the browser MCP from the repo root writes snapshots, console logs and screenshots to .playwright-mcp/, which was not ignored and is easy to commit by accident. * fix(fonts): keep referenced fonts out of asset bundles A bundle can only ever carry a member's own file, so a referenced font would contribute its {} placeholder and none of its atlas pages. Worse, the engine's BundleRegistry derives a bundled font's extra page urls from data.info.maps, which a referenced font does not have — it holds only jsonAsset/textureAssets. _getAssetFileUrls therefore throws mid-loop while indexing the bundle, which abandons the rest of that bundle's url index, so every other member silently stops resolving from the tar too. Reproduced on the launch page: TypeError: Cannot read properties of undefined (reading 'maps') at _getAssetFileUrls -> _indexAssetFileUrls -> _onAssetAdd -> add The editor viewport happens not to throw today, but by timing rather than safety: _onAssetAdd runs before the engine asset's file is populated, so getFileUrl() is null and _getAssetFileUrls returns before reaching the font branch. Calling it on a loaded referenced font throws on demand, so any re-add would hit it. Three changes, none of which touch user data: - isAssetValid refuses a referenced font, so it cannot be added to a bundle in the first place. Legacy fonts are unaffected: the guard keys off data.jsonAsset, which a server-pipeline font never has. - The launch page and the editor registry both filter the member list on the copy taken from asset.json() before handing it to the engine, so a project that was already bundled this way loads instead of throwing. The observer keeps its original data.assets. Published builds are handled at export time instead, since they never see the live config. Verified in the editor and on the launch page against a bundle holding a legacy font plus a force-added referenced font: the engine sees only the legacy member, the observer still lists both, the console is clean, and the font itself still loads its 95 glyphs and one atlas page. A forced re-index that previously threw now returns cleanly. * fix(fonts): keep the server font pipeline off referenced fonts and their mirrors Re-Import is offered for any target asset with a resolvable source, and a referenced font qualifies: it is a target and its source_asset_id points at the imported .otf. Running it sent the font through the server pipeline, which rewrote data to a generated descriptor and replaced the {} placeholder with an atlas png, dropping jsonAsset/textureAssets and orphaning the json and texture assets the user can still see in the folder. Silent — no error, no confirmation. The mirrors carry the same source_asset_id, so they were exposed too: Re-Import appeared on them as well, and the texture compressor would set a compress task's source to a font file it cannot re-derive a texture from. Both guards key off the source asset being a font, which leaves every other source type alone, and a legacy font still shows Re-Import because it is a font target whose data carries no jsonAsset. Verified in the editor by driving the real context menu: hidden for a referenced font and for both its json and texture mirrors, still shown for a legacy server-pipeline font. The matching server-side guard lands in the monorepo, so this holds for direct API callers too. * fix(fonts): move a referenced font's mirrors to the store alongside it assets:move-to-store sends exactly the ids that were selected, and nothing on either side expands them — the store move endpoint moves precisely the assets its query names. A referenced font moved on its own therefore arrived in the store without the json descriptor or atlas pages it points at, and a clone of that store item resolves both refs to null, leaving a blank font with the inspector's "No JSON asset referenced" warning and no indication of where the assets went. Pull the mirrors into the selection instead. Deduped against the existing ids so selecting the font together with its folder contents, or two fonts sharing an atlas, does not send an id twice. Verified in the editor: moving the two-page font alone reports "Moving 4 assets to store" and sends assetIds [235, 233, 234, 242] — the font, its descriptor and both atlas pages. * style(fonts): satisfy prettier in the font import path * docs(fonts): correct why the build bootstraps carry no referenced font handler | 1 month ago | |
Migrate to @playcanvas/eslint-config v3 (/typescript) (#2076) * Migrate to @playcanvas/eslint-config v3 (/typescript) * chore: add prettier config and split lint/format scripts - add prettier.config.mjs re-exporting @playcanvas/eslint-config/prettier - add .prettierignore (sass stays owned by stylelint) - add fmt/fmt:fix scripts and prettier devDependency - rename type:check script to typecheck and update callers - ci: separate lint and fmt steps * style: apply prettier formatting and fix eslint-config v3 errors - format entire codebase with prettier (627 files) - fix ~1300 auto-fixable eslint errors (eslint --fix) - fix 688 mechanical rule violations across 241 files (prefer-for-of, no-prototype-builtins→Object.hasOwn, consistent-type-imports, typedef, no-unsafe-function-type, no-extraneous-class, etc.) - defer no-explicit-any and no-unused-vars to warn during migration - disable @typescript-eslint/no-unused-expressions for test files (chai) - bump tsconfig lib to es2023 (Node 22 LTS) for Object.hasOwn eslint: 0 errors | prettier: clean | build: passes | tests: 157 passing * chore: fix lint * chore: bump eslint config to beta 8 * chore: fix lint * chore: fix lint errors * fix: preserve lint cleanup initialization order * fix: avoid Object.hasOwn browser compatibility regression * perf: restore indexed loops | 2 months ago | |
chore(deps): update node.js to v22.23.2 (#2185) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> | 1 month ago | |
Migrate to @playcanvas/eslint-config v3 (/typescript) (#2076) * Migrate to @playcanvas/eslint-config v3 (/typescript) * chore: add prettier config and split lint/format scripts - add prettier.config.mjs re-exporting @playcanvas/eslint-config/prettier - add .prettierignore (sass stays owned by stylelint) - add fmt/fmt:fix scripts and prettier devDependency - rename type:check script to typecheck and update callers - ci: separate lint and fmt steps * style: apply prettier formatting and fix eslint-config v3 errors - format entire codebase with prettier (627 files) - fix ~1300 auto-fixable eslint errors (eslint --fix) - fix 688 mechanical rule violations across 241 files (prefer-for-of, no-prototype-builtins→Object.hasOwn, consistent-type-imports, typedef, no-unsafe-function-type, no-extraneous-class, etc.) - defer no-explicit-any and no-unused-vars to warn during migration - disable @typescript-eslint/no-unused-expressions for test files (chai) - bump tsconfig lib to es2023 (Node 22 LTS) for Object.hasOwn eslint: 0 errors | prettier: clean | build: passes | tests: 157 passing * chore: fix lint * chore: bump eslint config to beta 8 * chore: fix lint * chore: fix lint errors * fix: preserve lint cleanup initialization order * fix: avoid Object.hasOwn browser compatibility regression * perf: restore indexed loops | 2 months ago | |
Open sourcing of PlayCanvas Editor Frontend (#1346) * feat: removed all old files * feat: added init files * fix: update repository references from editor-ui to editor in various files * fix: update service and package names to use playcanvas prefix * fix: recreated lockfile * fix: update Docker build step to use build-push-action and add caching for auth * refactor: remove unused CustomSettingsPanel and related code | 1 year ago | |
Migrate to @playcanvas/eslint-config v3 (/typescript) (#2076) * Migrate to @playcanvas/eslint-config v3 (/typescript) * chore: add prettier config and split lint/format scripts - add prettier.config.mjs re-exporting @playcanvas/eslint-config/prettier - add .prettierignore (sass stays owned by stylelint) - add fmt/fmt:fix scripts and prettier devDependency - rename type:check script to typecheck and update callers - ci: separate lint and fmt steps * style: apply prettier formatting and fix eslint-config v3 errors - format entire codebase with prettier (627 files) - fix ~1300 auto-fixable eslint errors (eslint --fix) - fix 688 mechanical rule violations across 241 files (prefer-for-of, no-prototype-builtins→Object.hasOwn, consistent-type-imports, typedef, no-unsafe-function-type, no-extraneous-class, etc.) - defer no-explicit-any and no-unused-vars to warn during migration - disable @typescript-eslint/no-unused-expressions for test files (chai) - bump tsconfig lib to es2023 (Node 22 LTS) for Object.hasOwn eslint: 0 errors | prettier: clean | build: passes | tests: 157 passing * chore: fix lint * chore: bump eslint config to beta 8 * chore: fix lint * chore: fix lint errors * fix: preserve lint cleanup initialization order * fix: avoid Object.hasOwn browser compatibility regression * perf: restore indexed loops | 2 months ago | |
chore: added CLAUDE.md symlink | 6 months ago | |
[REFACTOR] Migrate editor build from Rollup to Vite + esbuild (#1776) * feat: replaced esbuild for dev build * refactor: remove esbuild-dev and rollup configuration files, update build scripts to use esbuild.config.mjs * chore: lint fix * refactor: simplify watch log output in esbuild configuration * fix: update esbuild configuration to exclude additional paths from loading and change sourcemap option to 'linked' * feat: replaced orchestrator with vite * chore: update package dependencies and clean up package-lock.json * chore: update linting scripts for improved efficiency and clarity * refactor: remove compile-sass script and integrate SASS compilation into vite configuration * chore: remove unused dependencies from package-lock.json to reduce bloat * fix: reverted whats new element * refactor: consolidate CSS and JS build steps into a single command and update engine version message | 6 months ago | |
Update copyright year to 2026 | 8 months ago | |
chore: refactored README | 1 month ago | |
Migrate to @playcanvas/eslint-config v3 (/typescript) (#2076) * Migrate to @playcanvas/eslint-config v3 (/typescript) * chore: add prettier config and split lint/format scripts - add prettier.config.mjs re-exporting @playcanvas/eslint-config/prettier - add .prettierignore (sass stays owned by stylelint) - add fmt/fmt:fix scripts and prettier devDependency - rename type:check script to typecheck and update callers - ci: separate lint and fmt steps * style: apply prettier formatting and fix eslint-config v3 errors - format entire codebase with prettier (627 files) - fix ~1300 auto-fixable eslint errors (eslint --fix) - fix 688 mechanical rule violations across 241 files (prefer-for-of, no-prototype-builtins→Object.hasOwn, consistent-type-imports, typedef, no-unsafe-function-type, no-extraneous-class, etc.) - defer no-explicit-any and no-unused-vars to warn during migration - disable @typescript-eslint/no-unused-expressions for test files (chai) - bump tsconfig lib to es2023 (Node 22 LTS) for Object.hasOwn eslint: 0 errors | prettier: clean | build: passes | tests: 157 passing * chore: fix lint * chore: bump eslint config to beta 8 * chore: fix lint * chore: fix lint errors * fix: preserve lint cleanup initialization order * fix: avoid Object.hasOwn browser compatibility regression * perf: restore indexed loops | 2 months ago | |
Migrate to @playcanvas/eslint-config v3 (/typescript) (#2076) * Migrate to @playcanvas/eslint-config v3 (/typescript) * chore: add prettier config and split lint/format scripts - add prettier.config.mjs re-exporting @playcanvas/eslint-config/prettier - add .prettierignore (sass stays owned by stylelint) - add fmt/fmt:fix scripts and prettier devDependency - rename type:check script to typecheck and update callers - ci: separate lint and fmt steps * style: apply prettier formatting and fix eslint-config v3 errors - format entire codebase with prettier (627 files) - fix ~1300 auto-fixable eslint errors (eslint --fix) - fix 688 mechanical rule violations across 241 files (prefer-for-of, no-prototype-builtins→Object.hasOwn, consistent-type-imports, typedef, no-unsafe-function-type, no-extraneous-class, etc.) - defer no-explicit-any and no-unused-vars to warn during migration - disable @typescript-eslint/no-unused-expressions for test files (chai) - bump tsconfig lib to es2023 (Node 22 LTS) for Object.hasOwn eslint: 0 errors | prettier: clean | build: passes | tests: 157 passing * chore: fix lint * chore: bump eslint config to beta 8 * chore: fix lint * chore: fix lint errors * fix: preserve lint cleanup initialization order * fix: avoid Object.hasOwn browser compatibility regression * perf: restore indexed loops | 2 months ago | |
2.31.5 | 8 days ago | |
2.31.5 | 8 days ago | |
Migrate to @playcanvas/eslint-config v3 (/typescript) (#2076) * Migrate to @playcanvas/eslint-config v3 (/typescript) * chore: add prettier config and split lint/format scripts - add prettier.config.mjs re-exporting @playcanvas/eslint-config/prettier - add .prettierignore (sass stays owned by stylelint) - add fmt/fmt:fix scripts and prettier devDependency - rename type:check script to typecheck and update callers - ci: separate lint and fmt steps * style: apply prettier formatting and fix eslint-config v3 errors - format entire codebase with prettier (627 files) - fix ~1300 auto-fixable eslint errors (eslint --fix) - fix 688 mechanical rule violations across 241 files (prefer-for-of, no-prototype-builtins→Object.hasOwn, consistent-type-imports, typedef, no-unsafe-function-type, no-extraneous-class, etc.) - defer no-explicit-any and no-unused-vars to warn during migration - disable @typescript-eslint/no-unused-expressions for test files (chai) - bump tsconfig lib to es2023 (Node 22 LTS) for Object.hasOwn eslint: 0 errors | prettier: clean | build: passes | tests: 157 passing * chore: fix lint * chore: bump eslint config to beta 8 * chore: fix lint * chore: fix lint errors * fix: preserve lint cleanup initialization order * fix: avoid Object.hasOwn browser compatibility regression * perf: restore indexed loops | 2 months ago | |
chore(deps): guard against unpublished patch updates (#2183) | 1 month ago | |
Migrate to @playcanvas/eslint-config v3 (/typescript) (#2076) * Migrate to @playcanvas/eslint-config v3 (/typescript) * chore: add prettier config and split lint/format scripts - add prettier.config.mjs re-exporting @playcanvas/eslint-config/prettier - add .prettierignore (sass stays owned by stylelint) - add fmt/fmt:fix scripts and prettier devDependency - rename type:check script to typecheck and update callers - ci: separate lint and fmt steps * style: apply prettier formatting and fix eslint-config v3 errors - format entire codebase with prettier (627 files) - fix ~1300 auto-fixable eslint errors (eslint --fix) - fix 688 mechanical rule violations across 241 files (prefer-for-of, no-prototype-builtins→Object.hasOwn, consistent-type-imports, typedef, no-unsafe-function-type, no-extraneous-class, etc.) - defer no-explicit-any and no-unused-vars to warn during migration - disable @typescript-eslint/no-unused-expressions for test files (chai) - bump tsconfig lib to es2023 (Node 22 LTS) for Object.hasOwn eslint: 0 errors | prettier: clean | build: passes | tests: 157 passing * chore: fix lint * chore: bump eslint config to beta 8 * chore: fix lint * chore: fix lint errors * fix: preserve lint cleanup initialization order * fix: avoid Object.hasOwn browser compatibility regression * perf: restore indexed loops | 2 months ago | |
feat: generate fonts in the Editor as referenced JSON + atlas assets (#2190) * feat(assets): client-side font generation foundation - add @playcanvas/font-tools devDependency - font-generate worker (msdfgen wasm + canvas backend) + served wasm - fonts:generate editor method driver - build wiring: wasm copy to dist/js, worker import.meta define + node stubs - font-tools ambient type shim Groundwork for referenced font import (folder + json/texture mirror assets). v1 skips fontkit kerning to keep the worker bundle polyfill-free. * feat(assets): pass noConvert through assetCreate lets the editor upload a source asset (e.g. a client-side-generated font's .otf) without triggering the server-side conversion pipeline * feat(assets): client-side referenced font import - fonts:import — on font upload (gated by hasClientFontImport flag), create a folder + source .otf (noConvert) + JSON/Texture mirror assets + a runtime font baked from them (v3->v2), with data.jsonAsset/textureAssets refs - point the font's file at the page-0 texture mirror (single-page dedup) - fonts:rebake + mirror watch — re-bake the font when a mirror's file changes - fonts:reprocess — Process Font on a referenced font regenerates client-side and refreshes the mirrors instead of hitting the server pipeline - font inspector: SOURCE FILES panel with JSON + Texture pickers - noConvert now flows through assetUpdate too * wip: font gen * feat(fonts): add ReferencedFontHandler resolving json+texture references * fix(fonts): add proper type annotations to ReferencedFontHandler * chore(fonts): drop unused Asset import * feat(fonts): register ReferencedFontHandler in editor viewport and launch Swap the stock pc.FontHandler for ReferencedFontHandler in both engine contexts so client-referenced fonts resolve their json+texture asset data at load time, matching the server-rendered editor viewport and the launch preview. * feat(fonts): rework import to reference-only font; reload on ref change Stop baking v2 font data into the runtime font asset. importFont now creates the font with data = { jsonAsset, textureAssets } and a tiny placeholder file (needed only to trigger AssetRegistry.load() so ReferencedFontHandler's async reference resolution runs); the atlas is no longer duplicated into a third copy. Removes the now-dead toV2/rebake/fonts:rebake/baking machinery. The assets:add watcher that used to trigger a rebake on referenced-file change or repoint now reloads the font's engine resource instead, so the handler re-resolves and the viewport/thumbnail pick up the change. * fix(fonts): reload referenced font from engine signals, not observer events The reload watcher fired on the editor observer's data/file changes, but assets-registry syncs those into the engine asset on a deferred tick — so the handler re-resolved against stale engine data (repoint) or stale mirror resources (json/atlas edit). React to engine-side signals instead: a mirror's load:${id} (content edits) and the font engine asset's data change (repoints). * feat(fonts): hide intensity panel for referenced fonts data.intensity is inert for reference-only fonts (the handler builds pc.Font from the json descriptor and FontHandler.patch never applies it), so gate the PROPERTIES panel on the referenced discriminator like SOURCE FILES. * fix(fonts): make the reference font a usable target, not a source The reference font's placeholder file was named `${base}.font`; the backend marks a font-with-file as a source asset unless the file extension is a target extension (asset.ts: source = type!=='binary' && !TARGET_EXTENSIONS[ext]), so it showed as 'Font (source)' with no usable font, and source:true also forced preload off. Name the placeholder `${base}.json` (a target extension, honest for its {} content) so it becomes a preloadable target font. Regression from the reference-only rework, which changed the placeholder from the atlas .png. * fix(fonts): render referenced MSDF atlas as linear, not srgb Texture assets default to srgb (assets-srgb), and the engine samples the atlas through sRGB gamma, corrupting the MSDF median distance reconstruction (broken corners/thin strokes). Force each resolved atlas texture to srgb=false when building the pc.Font, matching the stock font handler (raw non-srgb atlas). * fix(fonts): rebuild referenced font on repoint via observer events The reload watcher keyed repoint off the engine asset's 'change' event, which only fires when the data object identity actually changes and proved unreliable. Trigger off the observer data:set events instead and push the fresh refs onto the engine asset before rebuilding, so a repointed json/texture updates the viewport and thumbnail. Mirror-content edits still reload via the mirror's engine 'load'. * fix(fonts): don't corrupt referenced font resource on ref change The engine text element's _onFontChange does this._font.data = asset.data on any font-asset data change, assuming asset.data is a descriptor. For a referenced font asset.data is refs ({jsonAsset,textureAssets}) with no chars, so it crashed (_getPxRange -> Object.keys(undefined)) when a picker was cleared/repointed. Stop the refs-only data from reaching the engine asset's data setter: skip the assets-registry data sync for referenced fonts, and in the reload watcher set the engine asset's refs via _data (bypassing the setter) before unload/load. * feat: convert legacy fonts to referenced assets * fix(fonts): resolve font references outside the load call stack Defer the reference lookup so it runs once the caller's synchronous work has settled, rather than inline while AssetRegistry.load() is still on the stack. * fix(fonts): keep meta and rebuild reliably when references change Converting a legacy font wrote `meta` before uploading the placeholder file, but the server clears `meta` on any asset file update — normally the conversion repopulates it, and that is exactly what is skipped here. The character set was lost, so the Characters field came back empty and the next regenerate produced a font with no glyphs. Write `meta` after the upload. Repointing the json and atlas references in the same tick started two overlapping rebuilds that could resolve out of order, leaving the new descriptor paired with the previous atlas. Coalesce rebuild requests into one per tick, and clear the loader cache for the font url, which never changes for a reference-only font and so would otherwise return the previously built font. * chore(fonts): use type aliases for the font-tools declarations `consistent-type-definitions` rejects `interface`, which failed lint. * fix(fonts): validate the referenced descriptor and expose every atlas page The json and texture refs are user-editable by design — the point of unpacking them is that you can supply your own pre-converted assets — so the descriptor reaching pc.Font is untrusted. - Validate it in the handler. pc.Font's data setter rewrites info.maps from info.width/height whenever version is missing or below 2, which NaNs every glyph UV of an otherwise valid file; pin the version so it leaves it alone, and reject descriptors missing info.maps or chars, or keyed by codepoint rather than letter. - Re-apply the atlas options the stock handler forces. These are plain texture assets carrying the user's own settings, and mipmaps bleed glyphs together at small sizes just as srgb corrupts the distance reconstruction. - Expose data.textureAssets as an ordered list instead of only index 0. A hand-supplied atlas may pack into a different number of pages, and page order is what chars[].map indexes into. - Report the states the handler soft-fails on in the SOURCE FILES panel, so a bad pairing reads as an explanation instead of blank text. Also pin @playcanvas/font-tools exactly, matching the other @playcanvas deps, and promote @playcanvas/msdfgen-wasm to a direct dependency since the build copies its wasm out of node_modules by path. * test: drop the frontend unit tests added on this branch This repo does not take unit tests for editor changes. * fix(fonts): unbind referenced-asset error handlers once a load settles The handler registered a `once('error')` listener per referenced asset on every load and never removed it on the success path, so one leaked per asset per rebuild. Worse, rebuilds clear the loader cache and reload under the same key, so a stale handler firing later called the loader callback for an already finished load and could fail the request that was in flight at the time. Route every exit through a single `settle()` that unbinds the listeners it registered, and bind them in a separate pass before any `ready()` call, since `ready()` fires synchronously for an already-loaded asset and could otherwise settle the load before the remaining handlers were bound. * fix(fonts): report generation and asset failures instead of hanging Three paths could leave a promise pending forever, which stranded the `updating` set, permanently blocked later rebuilds of that font and left the inspector button disabled reading "REGENERATING FONT ASSETS…" until a reload: - `fonts:generate` bound its 'error' callback inside the 'ready' handler, so a worker that 404s or throws at top level fired 'error' with nobody listening. It also never caught the rejection from the async `start()`. - `getObserver` waited on `assets:add[id]` with no timeout. - `reprocess` fed an unchecked `fetch` response to the generator, so an error page was parsed as font bytes. Bind the worker error handler before start, funnel every exit through one `settle()` so `stop()` is never skipped, bound the asset wait, and check `res.ok` before reading the source file. Also surface orphaned atlas pages when a regenerated descriptor packs into fewer pages than before, rather than silently leaving unreferenced assets in the folder, and mark a font watched only after the viewport app is known to exist so it can still be watched later if it is not. * fix(fonts): read referenced font intensity from the descriptor A referenced font's `data` holds only the json/texture references, so it has no `intensity`. Two places got that wrong: - `migrateFont` only checked `source` and `data`, so it wrote `data.intensity = 0` onto every referenced font on each project open until the value persisted — a realtime op adding a field the referenced path never reads. - The thumbnail renderer read `data.intensity` off the observer, so it picked up whatever the migration had backfilled rather than the descriptor's own value. Every other read in that function already used the loaded resource. Skip the migration for referenced fonts and take intensity from `resource.data.intensity`, which pc.Font guarantees for both font types. * feat(fonts): confirm before converting a legacy font Converting a legacy font rewrites `data`, replaces the asset file and drops the generated descriptor, none of it through history — so it cannot be undone. The button that used to be an idempotent "PROCESS FONT" now performs that one-way conversion, so gate it behind a confirm dialog. Regenerating an already referenced font is unchanged and still a single click. Also reset the processing flag before touching the panel in the reprocess continuation and bail if the inspector was unlinked meanwhile, otherwise selecting another asset mid-regenerate wrote to a stale element and left the button permanently disabled on relink. Document the source-file pickers via the reference tooltips, including that the referenced atlas textures are reconfigured for MSDF sampling when the font loads, and explain why the registry drops a queued `data` flush. * chore: lint fix * chore(fonts): share isRefPath and record the handler's duplication `isRefPath` was written out twice, in assets-font-import and the font inspector, both needing the same three-clause predicate over `data`, `data.jsonAsset` and `data.textureAssets`. Export it from the shared handler module the inspector already imports `normalizeFontJson` from. Also record two invariants that currently hold only by luck: - EMPTY_FONT is shared by every soft-failed font, and pc.Font's data setter writes into whatever `data` it is handed, inserting `info` when absent and rewriting `info.maps` when version < 2. This shape reaches neither branch, so both `version: 3` and the present `info` are load-bearing. - The handler is hand-mirrored into pipeline/templates/start.js and pipeline/templates/esm/start.mjs, which have no build step and cannot import it. Name all three copies in each, so a behaviour change cannot quietly land in one and render a font differently in a published build. * fix(fonts): count referenced font mirrors as asset usages `keys` had no `font` entry, so `data.jsonAsset` and `data.textureAssets` were invisible to the usage index. The json descriptor and atlas pages of a font in active use were therefore drawn with CLASS_ASSET_NOT_REFERENCED in the asset panel, deleting one raised no in-use warning, and assets-srgb's `findUsages` could not see them either. The failure mode was quiet and plausible: a user tidying up assets the editor itself labelled "not referenced" deleted them, and the font soft-failed to a blank one. Index the json ref through `keys.font` and the atlas pages through a `font` handler pair modelled on `model`/`model-insert`/`model-remove`. The `cubemap` entry is no use as a template because it relies on six fixed slots, whereas the page list is variable length and its order is what `chars[].map` indexes into. Count exactly one level per write. The observer emits the leaf events for a write and then the redundant container events above them — 'data.textureAssets.0', then 'data.textureAssets', then 'data' — so counting more than one level counts every ref twice; against a live project that drove counts to -2 and 3. For the page list the container is the level that works: a shrinking array emits no leaf event for the index it dropped, so leaves alone leak that ref. A `data` write always emits its children first and unsets the keys it drops, so the `data` container itself adds nothing. * fix(fonts): tear down referenced font watchers when the asset is removed `watchFont` bound four observer handlers plus one registry handler per mirror and never unbound any of them, and `watched` held the asset id forever. Every deleted referenced font therefore leaked those handlers, and any later re-add of that id — a merge, a realtime resync — was treated as already watched and silently stopped rebuilding the font on a ref change. The stale `pending` and `queued` entries were worse: assets-registry unloads the engine asset and drops it from the registry on removal, so a retry armed in `rebuildFont` never fires, and the leftover id then short-circuits every later rebuild of that font. Hold the teardown per id and run it on `assets:remove`, clearing the transient sets at the same time. Also unbind the `assets:add[id]` handler when `getObserver` times out rather than leaving it bound, take `isRefPath` from the shared module, and note why the json mirror is not preloaded — the font's own load resolves it either way. * fix(fonts): read font intensity off the resource, not its data 4d7c5f4b9 moved the thumbnail's intensity read to `resource.data.intensity` on the grounds that pc.Font guarantees it for both font types. It does not. What pc.Font guarantees is `resource.intensity`, which its constructor initialises to 0 and its data setter only overwrites when the descriptor actually carries the field. A descriptor that omits `intensity` leaves `data.intensity` undefined, and EMPTY_FONT — the blank font the handler soft-fails to when a reference does not resolve — has no `intensity` at all, so the shader parameter was being set to undefined. Confirmed against a font with a cleared json reference: `resource.data.intensity` is undefined there while `resource.intensity` is 0. * fix(fonts): refresh the regenerate button for the font linked now 7c86c2ada reset `_processing` before touching the panel and bailed when the inspector had been unlinked meanwhile, which stopped the write to a stale element — but nothing then re-rendered the button for the asset selected in the interim. The typed asset inspectors are cached instances reused through link/unlink, so selecting font B while font A was regenerating left B's button disabled reading "REGENERATING FONT ASSETS…" until the user selected away and back. Refresh the button for whatever is linked once the flag clears, ahead of the unlinked-asset bail. Also use the shared `isRefPath` for the source-file ref filter. * fix(fonts): name the failure when the font generator cannot load There is no server-side fallback for font import, so a worker or msdfgen.wasm that fails to load has to say what actually went wrong. Reporting it as the generic "font generation failed" reads as a problem with the font file, sending the user after the wrong thing. Also record two ceilings in the worker: the inversion probe samples a single corner pixel of the first glyph that generates, so it can misread when pxrange is large relative to size; and glyph source disposal is deliberately not in a finally, because the client stops the worker on every settled path and the wasm heap goes with it. * chore(fonts): clarify the font comments and drop the duplicated msdf defaults Comment pass over the font work: - Remove three `ponytail:` markers. That prefix is a working-mode convention from outside this repo and means nothing to anyone reading it here; the notes they carried are kept as plain prose. - Replace the winding probe's raw codepoint list with PROBE_CHARS = 'HXAoe'. `[72, 88, 65, 111, 101]` gave no hint that these were characters, let alone which ones. - Take the probe's size and pxrange fallbacks from font-tools' exported GLYPH_SIZE and PXRANGE instead of repeating 64 and 8. They were copies of that package's own defaults, so the probe would have silently run at different settings from generateFont if the package ever changed them. - Replace the "1/2)", "3/4)", "5)" step numbering in importFont, which described a sequence the reader could not see, and split the long step-5 note into the two things about it that are actually load-bearing. - Trim the rest to the house style of short and lowercase, and make several vaguer lines say what they mean: what the registry sync guard is protecting against, and why the usage index counts font mirrors at all. * fix(fonts): keep font asset retries working when swapping the handler Replacing the stock font handler silently disabled retrying for every font in every project, legacy ones included. Two separate causes: - ReferencedFontHandler wrapped a private FontHandler instance. ResourceLoader's enableRetry() assigns maxRetries to the object registered in the loader, so the wrapper absorbed it as an inert property while the inner handler kept the maxRetries of 0 from its own constructor — and that inner value is what FontHandler.load() passes to http.get. Subclass FontHandler instead, so the registered object is the one that reads it. That also restores `app` and the parser registry, which the wrapper did not forward, and lets patch() and the ResourceHandler casts at both registration sites go away. - The editor viewport swaps the handler after the app has already called enableRetry(), which only touches handlers registered at that moment, so the replacement started at 0 regardless. Carry the outgoing handler's retry count across the swap. Confirmed in the editor: the font handler now reports the same maxRetries as every other handler, and a failing font request retries five times with the expected backoff instead of failing outright. A legacy-shaped asset still routes to the inherited implementation, which is now the engine's own code rather than a re-implementation. * chore: ignore playwright mcp scratch output Driving the browser MCP from the repo root writes snapshots, console logs and screenshots to .playwright-mcp/, which was not ignored and is easy to commit by accident. * fix(fonts): keep referenced fonts out of asset bundles A bundle can only ever carry a member's own file, so a referenced font would contribute its {} placeholder and none of its atlas pages. Worse, the engine's BundleRegistry derives a bundled font's extra page urls from data.info.maps, which a referenced font does not have — it holds only jsonAsset/textureAssets. _getAssetFileUrls therefore throws mid-loop while indexing the bundle, which abandons the rest of that bundle's url index, so every other member silently stops resolving from the tar too. Reproduced on the launch page: TypeError: Cannot read properties of undefined (reading 'maps') at _getAssetFileUrls -> _indexAssetFileUrls -> _onAssetAdd -> add The editor viewport happens not to throw today, but by timing rather than safety: _onAssetAdd runs before the engine asset's file is populated, so getFileUrl() is null and _getAssetFileUrls returns before reaching the font branch. Calling it on a loaded referenced font throws on demand, so any re-add would hit it. Three changes, none of which touch user data: - isAssetValid refuses a referenced font, so it cannot be added to a bundle in the first place. Legacy fonts are unaffected: the guard keys off data.jsonAsset, which a server-pipeline font never has. - The launch page and the editor registry both filter the member list on the copy taken from asset.json() before handing it to the engine, so a project that was already bundled this way loads instead of throwing. The observer keeps its original data.assets. Published builds are handled at export time instead, since they never see the live config. Verified in the editor and on the launch page against a bundle holding a legacy font plus a force-added referenced font: the engine sees only the legacy member, the observer still lists both, the console is clean, and the font itself still loads its 95 glyphs and one atlas page. A forced re-index that previously threw now returns cleanly. * fix(fonts): keep the server font pipeline off referenced fonts and their mirrors Re-Import is offered for any target asset with a resolvable source, and a referenced font qualifies: it is a target and its source_asset_id points at the imported .otf. Running it sent the font through the server pipeline, which rewrote data to a generated descriptor and replaced the {} placeholder with an atlas png, dropping jsonAsset/textureAssets and orphaning the json and texture assets the user can still see in the folder. Silent — no error, no confirmation. The mirrors carry the same source_asset_id, so they were exposed too: Re-Import appeared on them as well, and the texture compressor would set a compress task's source to a font file it cannot re-derive a texture from. Both guards key off the source asset being a font, which leaves every other source type alone, and a legacy font still shows Re-Import because it is a font target whose data carries no jsonAsset. Verified in the editor by driving the real context menu: hidden for a referenced font and for both its json and texture mirrors, still shown for a legacy server-pipeline font. The matching server-side guard lands in the monorepo, so this holds for direct API callers too. * fix(fonts): move a referenced font's mirrors to the store alongside it assets:move-to-store sends exactly the ids that were selected, and nothing on either side expands them — the store move endpoint moves precisely the assets its query names. A referenced font moved on its own therefore arrived in the store without the json descriptor or atlas pages it points at, and a clone of that store item resolves both refs to null, leaving a blank font with the inspector's "No JSON asset referenced" warning and no indication of where the assets went. Pull the mirrors into the selection instead. Deduped against the existing ids so selecting the font together with its folder contents, or two fonts sharing an atlas, does not send an id twice. Verified in the editor: moving the two-page font alone reports "Moving 4 assets to store" and sends assetIds [235, 233, 234, 242] — the font, its descriptor and both atlas pages. * style(fonts): satisfy prettier in the font import path * docs(fonts): correct why the build bootstraps carry no referenced font handler | 1 month ago | |
feat: generate fonts in the Editor as referenced JSON + atlas assets (#2190) * feat(assets): client-side font generation foundation - add @playcanvas/font-tools devDependency - font-generate worker (msdfgen wasm + canvas backend) + served wasm - fonts:generate editor method driver - build wiring: wasm copy to dist/js, worker import.meta define + node stubs - font-tools ambient type shim Groundwork for referenced font import (folder + json/texture mirror assets). v1 skips fontkit kerning to keep the worker bundle polyfill-free. * feat(assets): pass noConvert through assetCreate lets the editor upload a source asset (e.g. a client-side-generated font's .otf) without triggering the server-side conversion pipeline * feat(assets): client-side referenced font import - fonts:import — on font upload (gated by hasClientFontImport flag), create a folder + source .otf (noConvert) + JSON/Texture mirror assets + a runtime font baked from them (v3->v2), with data.jsonAsset/textureAssets refs - point the font's file at the page-0 texture mirror (single-page dedup) - fonts:rebake + mirror watch — re-bake the font when a mirror's file changes - fonts:reprocess — Process Font on a referenced font regenerates client-side and refreshes the mirrors instead of hitting the server pipeline - font inspector: SOURCE FILES panel with JSON + Texture pickers - noConvert now flows through assetUpdate too * wip: font gen * feat(fonts): add ReferencedFontHandler resolving json+texture references * fix(fonts): add proper type annotations to ReferencedFontHandler * chore(fonts): drop unused Asset import * feat(fonts): register ReferencedFontHandler in editor viewport and launch Swap the stock pc.FontHandler for ReferencedFontHandler in both engine contexts so client-referenced fonts resolve their json+texture asset data at load time, matching the server-rendered editor viewport and the launch preview. * feat(fonts): rework import to reference-only font; reload on ref change Stop baking v2 font data into the runtime font asset. importFont now creates the font with data = { jsonAsset, textureAssets } and a tiny placeholder file (needed only to trigger AssetRegistry.load() so ReferencedFontHandler's async reference resolution runs); the atlas is no longer duplicated into a third copy. Removes the now-dead toV2/rebake/fonts:rebake/baking machinery. The assets:add watcher that used to trigger a rebake on referenced-file change or repoint now reloads the font's engine resource instead, so the handler re-resolves and the viewport/thumbnail pick up the change. * fix(fonts): reload referenced font from engine signals, not observer events The reload watcher fired on the editor observer's data/file changes, but assets-registry syncs those into the engine asset on a deferred tick — so the handler re-resolved against stale engine data (repoint) or stale mirror resources (json/atlas edit). React to engine-side signals instead: a mirror's load:${id} (content edits) and the font engine asset's data change (repoints). * feat(fonts): hide intensity panel for referenced fonts data.intensity is inert for reference-only fonts (the handler builds pc.Font from the json descriptor and FontHandler.patch never applies it), so gate the PROPERTIES panel on the referenced discriminator like SOURCE FILES. * fix(fonts): make the reference font a usable target, not a source The reference font's placeholder file was named `${base}.font`; the backend marks a font-with-file as a source asset unless the file extension is a target extension (asset.ts: source = type!=='binary' && !TARGET_EXTENSIONS[ext]), so it showed as 'Font (source)' with no usable font, and source:true also forced preload off. Name the placeholder `${base}.json` (a target extension, honest for its {} content) so it becomes a preloadable target font. Regression from the reference-only rework, which changed the placeholder from the atlas .png. * fix(fonts): render referenced MSDF atlas as linear, not srgb Texture assets default to srgb (assets-srgb), and the engine samples the atlas through sRGB gamma, corrupting the MSDF median distance reconstruction (broken corners/thin strokes). Force each resolved atlas texture to srgb=false when building the pc.Font, matching the stock font handler (raw non-srgb atlas). * fix(fonts): rebuild referenced font on repoint via observer events The reload watcher keyed repoint off the engine asset's 'change' event, which only fires when the data object identity actually changes and proved unreliable. Trigger off the observer data:set events instead and push the fresh refs onto the engine asset before rebuilding, so a repointed json/texture updates the viewport and thumbnail. Mirror-content edits still reload via the mirror's engine 'load'. * fix(fonts): don't corrupt referenced font resource on ref change The engine text element's _onFontChange does this._font.data = asset.data on any font-asset data change, assuming asset.data is a descriptor. For a referenced font asset.data is refs ({jsonAsset,textureAssets}) with no chars, so it crashed (_getPxRange -> Object.keys(undefined)) when a picker was cleared/repointed. Stop the refs-only data from reaching the engine asset's data setter: skip the assets-registry data sync for referenced fonts, and in the reload watcher set the engine asset's refs via _data (bypassing the setter) before unload/load. * feat: convert legacy fonts to referenced assets * fix(fonts): resolve font references outside the load call stack Defer the reference lookup so it runs once the caller's synchronous work has settled, rather than inline while AssetRegistry.load() is still on the stack. * fix(fonts): keep meta and rebuild reliably when references change Converting a legacy font wrote `meta` before uploading the placeholder file, but the server clears `meta` on any asset file update — normally the conversion repopulates it, and that is exactly what is skipped here. The character set was lost, so the Characters field came back empty and the next regenerate produced a font with no glyphs. Write `meta` after the upload. Repointing the json and atlas references in the same tick started two overlapping rebuilds that could resolve out of order, leaving the new descriptor paired with the previous atlas. Coalesce rebuild requests into one per tick, and clear the loader cache for the font url, which never changes for a reference-only font and so would otherwise return the previously built font. * chore(fonts): use type aliases for the font-tools declarations `consistent-type-definitions` rejects `interface`, which failed lint. * fix(fonts): validate the referenced descriptor and expose every atlas page The json and texture refs are user-editable by design — the point of unpacking them is that you can supply your own pre-converted assets — so the descriptor reaching pc.Font is untrusted. - Validate it in the handler. pc.Font's data setter rewrites info.maps from info.width/height whenever version is missing or below 2, which NaNs every glyph UV of an otherwise valid file; pin the version so it leaves it alone, and reject descriptors missing info.maps or chars, or keyed by codepoint rather than letter. - Re-apply the atlas options the stock handler forces. These are plain texture assets carrying the user's own settings, and mipmaps bleed glyphs together at small sizes just as srgb corrupts the distance reconstruction. - Expose data.textureAssets as an ordered list instead of only index 0. A hand-supplied atlas may pack into a different number of pages, and page order is what chars[].map indexes into. - Report the states the handler soft-fails on in the SOURCE FILES panel, so a bad pairing reads as an explanation instead of blank text. Also pin @playcanvas/font-tools exactly, matching the other @playcanvas deps, and promote @playcanvas/msdfgen-wasm to a direct dependency since the build copies its wasm out of node_modules by path. * test: drop the frontend unit tests added on this branch This repo does not take unit tests for editor changes. * fix(fonts): unbind referenced-asset error handlers once a load settles The handler registered a `once('error')` listener per referenced asset on every load and never removed it on the success path, so one leaked per asset per rebuild. Worse, rebuilds clear the loader cache and reload under the same key, so a stale handler firing later called the loader callback for an already finished load and could fail the request that was in flight at the time. Route every exit through a single `settle()` that unbinds the listeners it registered, and bind them in a separate pass before any `ready()` call, since `ready()` fires synchronously for an already-loaded asset and could otherwise settle the load before the remaining handlers were bound. * fix(fonts): report generation and asset failures instead of hanging Three paths could leave a promise pending forever, which stranded the `updating` set, permanently blocked later rebuilds of that font and left the inspector button disabled reading "REGENERATING FONT ASSETS…" until a reload: - `fonts:generate` bound its 'error' callback inside the 'ready' handler, so a worker that 404s or throws at top level fired 'error' with nobody listening. It also never caught the rejection from the async `start()`. - `getObserver` waited on `assets:add[id]` with no timeout. - `reprocess` fed an unchecked `fetch` response to the generator, so an error page was parsed as font bytes. Bind the worker error handler before start, funnel every exit through one `settle()` so `stop()` is never skipped, bound the asset wait, and check `res.ok` before reading the source file. Also surface orphaned atlas pages when a regenerated descriptor packs into fewer pages than before, rather than silently leaving unreferenced assets in the folder, and mark a font watched only after the viewport app is known to exist so it can still be watched later if it is not. * fix(fonts): read referenced font intensity from the descriptor A referenced font's `data` holds only the json/texture references, so it has no `intensity`. Two places got that wrong: - `migrateFont` only checked `source` and `data`, so it wrote `data.intensity = 0` onto every referenced font on each project open until the value persisted — a realtime op adding a field the referenced path never reads. - The thumbnail renderer read `data.intensity` off the observer, so it picked up whatever the migration had backfilled rather than the descriptor's own value. Every other read in that function already used the loaded resource. Skip the migration for referenced fonts and take intensity from `resource.data.intensity`, which pc.Font guarantees for both font types. * feat(fonts): confirm before converting a legacy font Converting a legacy font rewrites `data`, replaces the asset file and drops the generated descriptor, none of it through history — so it cannot be undone. The button that used to be an idempotent "PROCESS FONT" now performs that one-way conversion, so gate it behind a confirm dialog. Regenerating an already referenced font is unchanged and still a single click. Also reset the processing flag before touching the panel in the reprocess continuation and bail if the inspector was unlinked meanwhile, otherwise selecting another asset mid-regenerate wrote to a stale element and left the button permanently disabled on relink. Document the source-file pickers via the reference tooltips, including that the referenced atlas textures are reconfigured for MSDF sampling when the font loads, and explain why the registry drops a queued `data` flush. * chore: lint fix * chore(fonts): share isRefPath and record the handler's duplication `isRefPath` was written out twice, in assets-font-import and the font inspector, both needing the same three-clause predicate over `data`, `data.jsonAsset` and `data.textureAssets`. Export it from the shared handler module the inspector already imports `normalizeFontJson` from. Also record two invariants that currently hold only by luck: - EMPTY_FONT is shared by every soft-failed font, and pc.Font's data setter writes into whatever `data` it is handed, inserting `info` when absent and rewriting `info.maps` when version < 2. This shape reaches neither branch, so both `version: 3` and the present `info` are load-bearing. - The handler is hand-mirrored into pipeline/templates/start.js and pipeline/templates/esm/start.mjs, which have no build step and cannot import it. Name all three copies in each, so a behaviour change cannot quietly land in one and render a font differently in a published build. * fix(fonts): count referenced font mirrors as asset usages `keys` had no `font` entry, so `data.jsonAsset` and `data.textureAssets` were invisible to the usage index. The json descriptor and atlas pages of a font in active use were therefore drawn with CLASS_ASSET_NOT_REFERENCED in the asset panel, deleting one raised no in-use warning, and assets-srgb's `findUsages` could not see them either. The failure mode was quiet and plausible: a user tidying up assets the editor itself labelled "not referenced" deleted them, and the font soft-failed to a blank one. Index the json ref through `keys.font` and the atlas pages through a `font` handler pair modelled on `model`/`model-insert`/`model-remove`. The `cubemap` entry is no use as a template because it relies on six fixed slots, whereas the page list is variable length and its order is what `chars[].map` indexes into. Count exactly one level per write. The observer emits the leaf events for a write and then the redundant container events above them — 'data.textureAssets.0', then 'data.textureAssets', then 'data' — so counting more than one level counts every ref twice; against a live project that drove counts to -2 and 3. For the page list the container is the level that works: a shrinking array emits no leaf event for the index it dropped, so leaves alone leak that ref. A `data` write always emits its children first and unsets the keys it drops, so the `data` container itself adds nothing. * fix(fonts): tear down referenced font watchers when the asset is removed `watchFont` bound four observer handlers plus one registry handler per mirror and never unbound any of them, and `watched` held the asset id forever. Every deleted referenced font therefore leaked those handlers, and any later re-add of that id — a merge, a realtime resync — was treated as already watched and silently stopped rebuilding the font on a ref change. The stale `pending` and `queued` entries were worse: assets-registry unloads the engine asset and drops it from the registry on removal, so a retry armed in `rebuildFont` never fires, and the leftover id then short-circuits every later rebuild of that font. Hold the teardown per id and run it on `assets:remove`, clearing the transient sets at the same time. Also unbind the `assets:add[id]` handler when `getObserver` times out rather than leaving it bound, take `isRefPath` from the shared module, and note why the json mirror is not preloaded — the font's own load resolves it either way. * fix(fonts): read font intensity off the resource, not its data 4d7c5f4b9 moved the thumbnail's intensity read to `resource.data.intensity` on the grounds that pc.Font guarantees it for both font types. It does not. What pc.Font guarantees is `resource.intensity`, which its constructor initialises to 0 and its data setter only overwrites when the descriptor actually carries the field. A descriptor that omits `intensity` leaves `data.intensity` undefined, and EMPTY_FONT — the blank font the handler soft-fails to when a reference does not resolve — has no `intensity` at all, so the shader parameter was being set to undefined. Confirmed against a font with a cleared json reference: `resource.data.intensity` is undefined there while `resource.intensity` is 0. * fix(fonts): refresh the regenerate button for the font linked now 7c86c2ada reset `_processing` before touching the panel and bailed when the inspector had been unlinked meanwhile, which stopped the write to a stale element — but nothing then re-rendered the button for the asset selected in the interim. The typed asset inspectors are cached instances reused through link/unlink, so selecting font B while font A was regenerating left B's button disabled reading "REGENERATING FONT ASSETS…" until the user selected away and back. Refresh the button for whatever is linked once the flag clears, ahead of the unlinked-asset bail. Also use the shared `isRefPath` for the source-file ref filter. * fix(fonts): name the failure when the font generator cannot load There is no server-side fallback for font import, so a worker or msdfgen.wasm that fails to load has to say what actually went wrong. Reporting it as the generic "font generation failed" reads as a problem with the font file, sending the user after the wrong thing. Also record two ceilings in the worker: the inversion probe samples a single corner pixel of the first glyph that generates, so it can misread when pxrange is large relative to size; and glyph source disposal is deliberately not in a finally, because the client stops the worker on every settled path and the wasm heap goes with it. * chore(fonts): clarify the font comments and drop the duplicated msdf defaults Comment pass over the font work: - Remove three `ponytail:` markers. That prefix is a working-mode convention from outside this repo and means nothing to anyone reading it here; the notes they carried are kept as plain prose. - Replace the winding probe's raw codepoint list with PROBE_CHARS = 'HXAoe'. `[72, 88, 65, 111, 101]` gave no hint that these were characters, let alone which ones. - Take the probe's size and pxrange fallbacks from font-tools' exported GLYPH_SIZE and PXRANGE instead of repeating 64 and 8. They were copies of that package's own defaults, so the probe would have silently run at different settings from generateFont if the package ever changed them. - Replace the "1/2)", "3/4)", "5)" step numbering in importFont, which described a sequence the reader could not see, and split the long step-5 note into the two things about it that are actually load-bearing. - Trim the rest to the house style of short and lowercase, and make several vaguer lines say what they mean: what the registry sync guard is protecting against, and why the usage index counts font mirrors at all. * fix(fonts): keep font asset retries working when swapping the handler Replacing the stock font handler silently disabled retrying for every font in every project, legacy ones included. Two separate causes: - ReferencedFontHandler wrapped a private FontHandler instance. ResourceLoader's enableRetry() assigns maxRetries to the object registered in the loader, so the wrapper absorbed it as an inert property while the inner handler kept the maxRetries of 0 from its own constructor — and that inner value is what FontHandler.load() passes to http.get. Subclass FontHandler instead, so the registered object is the one that reads it. That also restores `app` and the parser registry, which the wrapper did not forward, and lets patch() and the ResourceHandler casts at both registration sites go away. - The editor viewport swaps the handler after the app has already called enableRetry(), which only touches handlers registered at that moment, so the replacement started at 0 regardless. Carry the outgoing handler's retry count across the swap. Confirmed in the editor: the font handler now reports the same maxRetries as every other handler, and a failing font request retries five times with the expected backoff instead of failing outright. A legacy-shaped asset still routes to the inherited implementation, which is now the engine's own code rather than a re-implementation. * chore: ignore playwright mcp scratch output Driving the browser MCP from the repo root writes snapshots, console logs and screenshots to .playwright-mcp/, which was not ignored and is easy to commit by accident. * fix(fonts): keep referenced fonts out of asset bundles A bundle can only ever carry a member's own file, so a referenced font would contribute its {} placeholder and none of its atlas pages. Worse, the engine's BundleRegistry derives a bundled font's extra page urls from data.info.maps, which a referenced font does not have — it holds only jsonAsset/textureAssets. _getAssetFileUrls therefore throws mid-loop while indexing the bundle, which abandons the rest of that bundle's url index, so every other member silently stops resolving from the tar too. Reproduced on the launch page: TypeError: Cannot read properties of undefined (reading 'maps') at _getAssetFileUrls -> _indexAssetFileUrls -> _onAssetAdd -> add The editor viewport happens not to throw today, but by timing rather than safety: _onAssetAdd runs before the engine asset's file is populated, so getFileUrl() is null and _getAssetFileUrls returns before reaching the font branch. Calling it on a loaded referenced font throws on demand, so any re-add would hit it. Three changes, none of which touch user data: - isAssetValid refuses a referenced font, so it cannot be added to a bundle in the first place. Legacy fonts are unaffected: the guard keys off data.jsonAsset, which a server-pipeline font never has. - The launch page and the editor registry both filter the member list on the copy taken from asset.json() before handing it to the engine, so a project that was already bundled this way loads instead of throwing. The observer keeps its original data.assets. Published builds are handled at export time instead, since they never see the live config. Verified in the editor and on the launch page against a bundle holding a legacy font plus a force-added referenced font: the engine sees only the legacy member, the observer still lists both, the console is clean, and the font itself still loads its 95 glyphs and one atlas page. A forced re-index that previously threw now returns cleanly. * fix(fonts): keep the server font pipeline off referenced fonts and their mirrors Re-Import is offered for any target asset with a resolvable source, and a referenced font qualifies: it is a target and its source_asset_id points at the imported .otf. Running it sent the font through the server pipeline, which rewrote data to a generated descriptor and replaced the {} placeholder with an atlas png, dropping jsonAsset/textureAssets and orphaning the json and texture assets the user can still see in the folder. Silent — no error, no confirmation. The mirrors carry the same source_asset_id, so they were exposed too: Re-Import appeared on them as well, and the texture compressor would set a compress task's source to a font file it cannot re-derive a texture from. Both guards key off the source asset being a font, which leaves every other source type alone, and a legacy font still shows Re-Import because it is a font target whose data carries no jsonAsset. Verified in the editor by driving the real context menu: hidden for a referenced font and for both its json and texture mirrors, still shown for a legacy server-pipeline font. The matching server-side guard lands in the monorepo, so this holds for direct API callers too. * fix(fonts): move a referenced font's mirrors to the store alongside it assets:move-to-store sends exactly the ids that were selected, and nothing on either side expands them — the store move endpoint moves precisely the assets its query names. A referenced font moved on its own therefore arrived in the store without the json descriptor or atlas pages it points at, and a clone of that store item resolves both refs to null, leaving a blank font with the inspector's "No JSON asset referenced" warning and no indication of where the assets went. Pull the mirrors into the selection instead. Deduped against the existing ids so selecting the font together with its folder contents, or two fonts sharing an atlas, does not send an id twice. Verified in the editor: moving the two-page font alone reports "Moving 4 assets to store" and sends assetIds [235, 233, 234, 242] — the font, its descriptor and both atlas pages. * style(fonts): satisfy prettier in the font import path * docs(fonts): correct why the build bootstraps carry no referenced font handler | 1 month ago |
PlayCanvas Editor
PlayCanvas Editor 是一个可视化编辑环境,用于构建 WebGL/WebGPU/WebXR 应用程序。可通过 https://playcanvas.com 访问。

您可以在 PlayCanvas 网站 上查看更多使用该编辑器构建的项目。
Tip
AI 辅助开发: 编辑器内置了 Model Context Protocol (MCP) 集成,允许 AI 助手通过打开的编辑器会话直接检查和修改项目。
本地开发
要初始化编辑器前端的本地开发环境,请确保已安装 Node.js 18 或更高版本。请按照以下步骤操作:
-
克隆仓库:
git clone https://github.com/playcanvas/editor.git cd editor -
安装依赖:
npm install -
构建编辑器前端并启动本地 Web 服务器:
npm run develop -
在 playcanvas.com 打开编辑器,并附加查询参数
use_local_frontend以加载开发构建版本:https://playcanvas.com/editor/scene/<YOUR_SCENE_ID>?use_local_frontend这将从
http://localhost:3487加载编辑器前端,而非生产构建版本。
Note
代码编辑器和启动页面也支持此查询参数
库集成测试
编辑器基于以下开源库构建:
| 库 | 详情 |
|---|---|
| PlayCanvas Engine | 为编辑器的 3D 视图和启动页面提供支持 |
| Observer | 数据绑定和历史记录 |
| PCUI | 前端组件库 |
| PCUI-Graph | 用于渲染基于节点的图形的 PCUI 插件 |
要测试这些库的集成,请使用 npm link。请按照以下步骤操作:
-
从源代码创建全局链接
cd <library> npm link -
创建指向全局链接的链接
cd editor npm link <library>
测试套件
PlayCanvas 编辑器的测试套件使用 Playwright 进行端到端测试。

测试
测试分为两类:
test-suite/test/api- 编辑器 API 行为测试test-suite/test/ui- 编辑器 UI 行为测试
运行测试
要运行测试套件,请确保已安装 Docker。请按照以下步骤操作:
-
基于模板创建
.env文件PC_HOST=playcanvas.com PC_LOGIN_HOST=login.playcanvas.com PC_LAUNCH_HOST=launch.playcanvas.com PC_LOCAL_FRONTEND=<true|false> PC_COOKIE_NAME=pc_auth PC_COOKIE_VALUE=<playcanvas-cookie>
Important
所使用的账户必须是现有账户。可在此创建一个账户
-
使用 Docker Compose 运行容器
docker compose up -
使用 Docker Compose 停止容器
docker compose down --remove-orphans
Note
若要从源代码构建镜像而不是从注册表拉取,请附加 --build 标志
测试套件本地开发
若要创建新测试,请确保已安装 Node.js 18 或更高版本。请按照以下步骤操作:
-
安装依赖:
cd test-suite npx playwright install --with-deps npm install -
创建测试并将其放入相应的文件夹中。若要动态录制测试,请运行以下命令:
npm run codegen
Note
运行 npm run 可查看完整的 npm 脚本列表
Introduction
基于浏览器的可视化编辑器,用于构建 WebGL、WebGPU、WebXR 应用程序【此简介由AI生成】