@pixi-ui-editor/schema 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,7 +6,8 @@ import { collectBindingKeys, collectBindingKeyScopes } from "./bindingKeys.js";
6
6
  import { collectLocalizationIssues, createDefaultLocalization, LocalizationSchema } from "./localization.js";
7
7
  export { collectBindingKeys, collectBindingKeyScopes, OWNER_BINDING_KEY_SCOPE } from "./bindingKeys.js";
8
8
  export { rewriteNodeReferences, rewritePrefabInstanceReferencePatches } from "./nodeReferences.js";
9
- export const CURRENT_SCHEMA_VERSION = 39;
9
+ export { clearNodeAssetReferences } from "./assetReferences.js";
10
+ export const CURRENT_SCHEMA_VERSION = 40;
10
11
  const Id = Type.String({ format: "uuid" });
11
12
  const Name = Type.String({ minLength: 1 });
12
13
  /**
@@ -109,18 +110,25 @@ const BitmapTextAlign = Type.Union([Type.Literal("left"), Type.Literal("center")
109
110
  // A bitmap font's glyphs come pre-rendered from its texture, so unlike `Text` there is no system
110
111
  // fallback: `assetId` is required (like Spine's), and the "Bitmap Text" add-node entry stays disabled
111
112
  // until at least one `bitmapFont` asset exists — a bitmap-text node with no asset has nothing to draw.
113
+ const BitmapTextStyle = Type.Object({
114
+ assetId: Id,
115
+ // The authored/preferred size. With `autoSize` on, this doubles as the upper bound the runtime
116
+ // shrinks down from — never grows past it — the same "Best Fit" contract Unity's legacy Text uses.
117
+ fontSize: Type.Number({ exclusiveMinimum: 0 }),
118
+ fill: Type.String({ pattern: "^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$" }),
119
+ align: BitmapTextAlign,
120
+ verticalAlign: Type.Union([Type.Literal("top"), Type.Literal("middle"), Type.Literal("bottom")]),
121
+ letterSpacing: Type.Number(),
122
+ wordWrap: Type.Boolean(),
123
+ });
124
+ const BitmapTextOverrides = Type.Partial(Type.Object({ desktop: Type.Partial(BitmapTextStyle), mobile: Type.Partial(BitmapTextStyle) }));
112
125
  const BitmapText = Type.Composite([NodeBase, Type.Object({
113
126
  type: Type.Literal("bitmap-text"),
127
+ // Content and runtime fit are shared. The visual font presentation follows the same base/mobile
128
+ // override convention as `Text.style`; text remains one localization/game-data value.
114
129
  text: Type.String(),
115
- assetId: Id,
116
- // The authored/preferred size. With `autoSize` on, this doubles as the upper bound the runtime
117
- // shrinks down from — never grows past it — the same "Best Fit" contract Unity's legacy Text uses.
118
- fontSize: Type.Number({ exclusiveMinimum: 0 }),
119
- fill: Type.String({ pattern: "^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$" }),
120
- align: BitmapTextAlign,
121
- verticalAlign: Type.Union([Type.Literal("top"), Type.Literal("middle"), Type.Literal("bottom")]),
122
- letterSpacing: Type.Number(),
123
- wordWrap: Type.Boolean(),
130
+ ...BitmapTextStyle.properties,
131
+ bitmapTextOverrides: Type.Optional(BitmapTextOverrides),
124
132
  // Omitted/false keeps the historical behavior: fixed `fontSize`, overflow simply clips/overruns.
125
133
  autoSize: Type.Optional(Type.Boolean()),
126
134
  // The floor `autoSize` won't shrink past; omitted defaults to 1 at the runtime/view layer, not here,
@@ -422,6 +430,12 @@ const structural = documentAjv.compile(ProjectDocumentSchema);
422
430
  const structuralNode = createAjv().compile(UINodeSchema);
423
431
  export function createStableId() { return crypto.randomUUID(); }
424
432
  const add = (issues, code, path, message) => issues.push({ code, path, message, severity: "error" });
433
+ // A missing asset reference is downgraded to a warning: deleting an asset must always be possible
434
+ // (see AssetPanel's delete action), and after deletion any node still pointing at it necessarily
435
+ // dangles. Blocking the whole document as invalid would make asset deletion silently no-op instead —
436
+ // exactly the confusing "it won't delete" experience this severity exists to avoid. A type mismatch
437
+ // (`INCOMPATIBLE_ASSET_REFERENCE`) stays an error: that is real corruption, not a dangling reference.
438
+ const addWarning = (issues, code, path, message) => issues.push({ code, path, message, severity: "warning" });
425
439
  /** Общая проверка владельца: одинаковые issue получают и сцена, и определение пресета. */
426
440
  function checkBindingKeys(document, owner, path, issues) {
427
441
  const graph = resolveOwnerNodeGraph(document, owner);
@@ -571,14 +585,14 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
571
585
  if ((node.type === "image" || node.type === "spine") && node.assetId !== undefined) {
572
586
  const asset = assets.get(node.assetId);
573
587
  if (!asset)
574
- add(issues, "MISSING_ASSET_REFERENCE", `${nodePath}/assetId`, `Asset '${node.assetId}' does not exist.`);
588
+ addWarning(issues, "MISSING_ASSET_REFERENCE", `${nodePath}/assetId`, `Asset '${node.assetId}' does not exist.`);
575
589
  else if (asset.type !== node.type)
576
590
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", `${nodePath}/assetId`, `A ${node.type} node requires a ${node.type} asset.`);
577
591
  }
578
592
  if (isLayoutGroup(node) && node.backgroundAssetId !== undefined) {
579
593
  const asset = assets.get(node.backgroundAssetId);
580
594
  if (!asset)
581
- add(issues, "MISSING_ASSET_REFERENCE", `${nodePath}/backgroundAssetId`, `Background asset '${node.backgroundAssetId}' does not exist.`);
595
+ addWarning(issues, "MISSING_ASSET_REFERENCE", `${nodePath}/backgroundAssetId`, `Background asset '${node.backgroundAssetId}' does not exist.`);
582
596
  else if (asset.type !== "image")
583
597
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", `${nodePath}/backgroundAssetId`, "A layout group background requires an image asset.");
584
598
  }
@@ -586,7 +600,7 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
586
600
  if (node.type === "button")
587
601
  BUTTON_STATE_KEYS.forEach((state) => { const field = `${state}AssetId`; const assetId = node.states[field]; if (assetId === undefined)
588
602
  return; const asset = assets.get(assetId), path = `${nodePath}/states/${field}`; if (!asset)
589
- add(issues, "MISSING_ASSET_REFERENCE", path, `Asset '${assetId}' does not exist.`);
603
+ addWarning(issues, "MISSING_ASSET_REFERENCE", path, `Asset '${assetId}' does not exist.`);
590
604
  else if (asset.type !== "image")
591
605
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", path, `A button '${state}' state requires an image asset.`); });
592
606
  if (node.type === "button")
@@ -596,7 +610,7 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
596
610
  continue;
597
611
  const asset = assets.get(assetId), soundPath = `${nodePath}/sounds/${field}`;
598
612
  if (!asset)
599
- add(issues, "MISSING_ASSET_REFERENCE", soundPath, `Asset '${assetId}' does not exist.`);
613
+ addWarning(issues, "MISSING_ASSET_REFERENCE", soundPath, `Asset '${assetId}' does not exist.`);
600
614
  else if (asset.type !== "sound")
601
615
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", soundPath, `A button ${field} requires a sound asset.`);
602
616
  }
@@ -608,7 +622,7 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
608
622
  stageIds.add(stage.id);
609
623
  BUTTON_STATE_KEYS.forEach((state) => { const field = `${state}AssetId`; const assetId = stage.states[field]; if (assetId === undefined)
610
624
  return; const asset = assets.get(assetId), statePath = `${nodePath}/stages/${stageIndex}/states/${field}`; if (!asset)
611
- add(issues, "MISSING_ASSET_REFERENCE", statePath, `Asset '${assetId}' does not exist.`);
625
+ addWarning(issues, "MISSING_ASSET_REFERENCE", statePath, `Asset '${assetId}' does not exist.`);
612
626
  else if (asset.type !== "image")
613
627
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", statePath, `A staged button stage '${state}' state requires an image asset.`); });
614
628
  });
@@ -620,7 +634,7 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
620
634
  continue;
621
635
  const asset = assets.get(assetId), soundPath = `${nodePath}/sounds/${field}`;
622
636
  if (!asset)
623
- add(issues, "MISSING_ASSET_REFERENCE", soundPath, `Asset '${assetId}' does not exist.`);
637
+ addWarning(issues, "MISSING_ASSET_REFERENCE", soundPath, `Asset '${assetId}' does not exist.`);
624
638
  else if (asset.type !== "sound")
625
639
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", soundPath, `A staged button ${field} requires a sound asset.`);
626
640
  }
@@ -628,7 +642,7 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
628
642
  if (node.type === "checkbox")
629
643
  CHECKBOX_STATE_KEYS.forEach((state) => { const field = `${state}AssetId`; const assetId = node.states[field]; if (assetId === undefined)
630
644
  return; const asset = assets.get(assetId), path = `${nodePath}/states/${field}`; if (!asset)
631
- add(issues, "MISSING_ASSET_REFERENCE", path, `Asset '${assetId}' does not exist.`);
645
+ addWarning(issues, "MISSING_ASSET_REFERENCE", path, `Asset '${assetId}' does not exist.`);
632
646
  else if (asset.type !== "image")
633
647
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", path, `A checkbox '${state}' state requires an image asset.`); });
634
648
  if (node.type === "checkbox")
@@ -638,16 +652,25 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
638
652
  continue;
639
653
  const asset = assets.get(assetId), soundPath = `${nodePath}/sounds/${field}`;
640
654
  if (!asset)
641
- add(issues, "MISSING_ASSET_REFERENCE", soundPath, `Asset '${assetId}' does not exist.`);
655
+ addWarning(issues, "MISSING_ASSET_REFERENCE", soundPath, `Asset '${assetId}' does not exist.`);
642
656
  else if (asset.type !== "sound")
643
657
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", soundPath, `A checkbox ${field} requires a sound asset.`);
644
658
  }
645
659
  if (node.type === "bitmap-text") {
646
- const asset = assets.get(node.assetId);
647
- if (!asset)
648
- add(issues, "MISSING_ASSET_REFERENCE", `${nodePath}/assetId`, `Asset '${node.assetId}' does not exist.`);
649
- else if (asset.type !== "bitmapFont")
650
- add(issues, "INCOMPATIBLE_ASSET_REFERENCE", `${nodePath}/assetId`, "A bitmap text node requires a bitmap font asset.");
660
+ const fontReferences = [
661
+ [node.assetId, `${nodePath}/assetId`],
662
+ [node.bitmapTextOverrides?.desktop?.assetId, `${nodePath}/bitmapTextOverrides/desktop/assetId`],
663
+ [node.bitmapTextOverrides?.mobile?.assetId, `${nodePath}/bitmapTextOverrides/mobile/assetId`],
664
+ ];
665
+ for (const [assetId, assetPath] of fontReferences) {
666
+ if (assetId === undefined)
667
+ continue;
668
+ const asset = assets.get(assetId);
669
+ if (!asset)
670
+ addWarning(issues, "MISSING_ASSET_REFERENCE", assetPath, `Asset '${assetId}' does not exist.`);
671
+ else if (asset.type !== "bitmapFont")
672
+ add(issues, "INCOMPATIBLE_ASSET_REFERENCE", assetPath, "A bitmap text node requires a bitmap font asset.");
673
+ }
651
674
  }
652
675
  if (node.type === "text") {
653
676
  const fontReferences = [
@@ -660,7 +683,7 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
660
683
  continue;
661
684
  const asset = assets.get(fontAssetId);
662
685
  if (!asset)
663
- add(issues, "MISSING_ASSET_REFERENCE", fontPath, `Asset '${fontAssetId}' does not exist.`);
686
+ addWarning(issues, "MISSING_ASSET_REFERENCE", fontPath, `Asset '${fontAssetId}' does not exist.`);
664
687
  else if (asset.type !== "font")
665
688
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", fontPath, "A text node fontAssetId requires a font asset.");
666
689
  }
@@ -668,14 +691,14 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
668
691
  if (node.type === "input" && node.backgroundAssetId !== undefined) {
669
692
  const asset = assets.get(node.backgroundAssetId);
670
693
  if (!asset)
671
- add(issues, "MISSING_ASSET_REFERENCE", `${nodePath}/backgroundAssetId`, `Background asset '${node.backgroundAssetId}' does not exist.`);
694
+ addWarning(issues, "MISSING_ASSET_REFERENCE", `${nodePath}/backgroundAssetId`, `Background asset '${node.backgroundAssetId}' does not exist.`);
672
695
  else if (asset.type !== "image")
673
696
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", `${nodePath}/backgroundAssetId`, "An input background requires an image asset.");
674
697
  }
675
698
  if (node.type === "input" && node.textStyle.fontAssetId !== undefined) {
676
699
  const asset = assets.get(node.textStyle.fontAssetId), path = `${nodePath}/textStyle/fontAssetId`;
677
700
  if (!asset)
678
- add(issues, "MISSING_ASSET_REFERENCE", path, `Asset '${node.textStyle.fontAssetId}' does not exist.`);
701
+ addWarning(issues, "MISSING_ASSET_REFERENCE", path, `Asset '${node.textStyle.fontAssetId}' does not exist.`);
679
702
  else if (asset.type !== "font")
680
703
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", path, "An input node textStyle fontAssetId requires a font asset.");
681
704
  }
@@ -687,7 +710,7 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
687
710
  const asset = assets.get(assetId);
688
711
  const assetPath = `${nodePath}/${field}`;
689
712
  if (!asset)
690
- add(issues, "MISSING_ASSET_REFERENCE", assetPath, `Asset '${assetId}' does not exist.`);
713
+ addWarning(issues, "MISSING_ASSET_REFERENCE", assetPath, `Asset '${assetId}' does not exist.`);
691
714
  else if (asset.type !== "image")
692
715
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", assetPath, `A ${node.type} ${field} requires an image asset.`);
693
716
  }
@@ -700,7 +723,7 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
700
723
  if (node.valueTextStyle?.fontAssetId !== undefined) {
701
724
  const asset = assets.get(node.valueTextStyle.fontAssetId), fontPath = `${nodePath}/valueTextStyle/fontAssetId`;
702
725
  if (!asset)
703
- add(issues, "MISSING_ASSET_REFERENCE", fontPath, `Asset '${node.valueTextStyle.fontAssetId}' does not exist.`);
726
+ addWarning(issues, "MISSING_ASSET_REFERENCE", fontPath, `Asset '${node.valueTextStyle.fontAssetId}' does not exist.`);
704
727
  else if (asset.type !== "font")
705
728
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", fontPath, "A slider valueTextStyle fontAssetId requires a font asset.");
706
729
  }
@@ -718,7 +741,7 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
718
741
  }
719
742
  const asset = assets.get(assetId);
720
743
  if (!asset)
721
- add(issues, "MISSING_ASSET_REFERENCE", assetPath, `Asset '${assetId}' does not exist.`);
744
+ addWarning(issues, "MISSING_ASSET_REFERENCE", assetPath, `Asset '${assetId}' does not exist.`);
722
745
  else if (asset.type !== "image")
723
746
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", assetPath, `A pagination ${field} requires an image asset.`);
724
747
  }
@@ -728,7 +751,7 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
728
751
  const asset = assets.get(node.numberStyle.fontAssetId);
729
752
  const fontPath = `${nodePath}/numberStyle/fontAssetId`;
730
753
  if (!asset)
731
- add(issues, "MISSING_ASSET_REFERENCE", fontPath, `Asset '${node.numberStyle.fontAssetId}' does not exist.`);
754
+ addWarning(issues, "MISSING_ASSET_REFERENCE", fontPath, `Asset '${node.numberStyle.fontAssetId}' does not exist.`);
732
755
  else if (asset.type !== "font")
733
756
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", fontPath, "A pagination numberStyle fontAssetId requires a font asset.");
734
757
  }
@@ -736,7 +759,7 @@ function hierarchy(owner, path, assets, prefabs, effects, issues) {
736
759
  const asset = assets.get(node.numberActiveBackgroundAssetId);
737
760
  const bgPath = `${nodePath}/numberActiveBackgroundAssetId`;
738
761
  if (!asset)
739
- add(issues, "MISSING_ASSET_REFERENCE", bgPath, `Asset '${node.numberActiveBackgroundAssetId}' does not exist.`);
762
+ addWarning(issues, "MISSING_ASSET_REFERENCE", bgPath, `Asset '${node.numberActiveBackgroundAssetId}' does not exist.`);
740
763
  else if (asset.type !== "image")
741
764
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", bgPath, "A pagination numberActiveBackgroundAssetId requires an image asset.");
742
765
  }
@@ -801,7 +824,7 @@ function semantic(document) {
801
824
  add(issues, "INVALID_PARTICLE_RANGE", `${p}/${field}`, "Particle range min must not exceed max."); }); if (e.rate === 0 && e.bursts.length === 0)
802
825
  add(issues, "EMPTY_PARTICLE_EMISSION", `${p}/emission`, "Particle emission needs a rate or a burst."); e.bursts.forEach((burst, j) => { if (burst.time > e.duration)
803
826
  add(issues, "PARTICLE_BURST_OUTSIDE_DURATION", `${p}/emission/bursts/${j}/time`, "Particle burst time must be inside emission duration."); }); const source = v.source; particleAssetIds(effect).forEach((id, j) => { const asset = assets.get(id), sourcePath = source.type === "single" ? `${p}/particle/visual/source/assetId` : `${p}/particle/visual/source/assetIds/${j}`; if (!asset)
804
- add(issues, "MISSING_ASSET_REFERENCE", sourcePath, `Asset '${id}' does not exist.`);
827
+ addWarning(issues, "MISSING_ASSET_REFERENCE", sourcePath, `Asset '${id}' does not exist.`);
805
828
  else if (asset.type !== "image")
806
829
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", sourcePath, "Particle visual source requires an image asset or atlas frame."); }); });
807
830
  document.prefabs.forEach((prefab, i) => { register(prefab.id, `/prefabs/${i}/id`); prefabs.add(prefab.id); });
@@ -856,7 +879,7 @@ function semantic(document) {
856
879
  validateInstance(node, `/scenes/${i}/nodes/${j}`); }); const musicAssetId = scene.audio?.backgroundMusicAssetId; if (musicAssetId !== undefined) {
857
880
  const asset = assets.get(musicAssetId), musicPath = `/scenes/${i}/audio/backgroundMusicAssetId`;
858
881
  if (!asset)
859
- add(issues, "MISSING_ASSET_REFERENCE", musicPath, `Asset '${musicAssetId}' does not exist.`);
882
+ addWarning(issues, "MISSING_ASSET_REFERENCE", musicPath, `Asset '${musicAssetId}' does not exist.`);
860
883
  else if (asset.type !== "sound")
861
884
  add(issues, "INCOMPATIBLE_ASSET_REFERENCE", musicPath, "Background music requires a sound asset.");
862
885
  } checkBindingKeys(document, scene, `/scenes/${i}`, issues); checkNodeReferences(document, scene, `/scenes/${i}`, issues); });
@@ -1271,11 +1294,15 @@ function migrateV32ToV33(document) {
1271
1294
  normalizeLegacySpineConnectors(document);
1272
1295
  normalizeLegacyPagination(document);
1273
1296
  }
1297
+ /** v40 adds sparse bitmap-text presentation overrides; existing values remain the shared base. */
1298
+ function migrateV39ToV40(_document) { }
1274
1299
  export function migrateProjectDocument(input) {
1275
1300
  if (typeof input !== "object" || input === null || !Object.hasOwn(input, "schemaVersion"))
1276
1301
  throw new ProjectDocumentMigrationError("A schemaVersion is required for migration.");
1277
1302
  const version = input.schemaVersion;
1278
- if (typeof version !== "number" || !Number.isInteger(version) || version < 1 || version > CURRENT_SCHEMA_VERSION)
1303
+ if (typeof version === "number" && Number.isInteger(version) && version > CURRENT_SCHEMA_VERSION)
1304
+ throw new ProjectDocumentMigrationError(`This project was saved with schema version ${version}, which is newer than schema version ${CURRENT_SCHEMA_VERSION} supported by this editor. Update the editor to open it.`);
1305
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1)
1279
1306
  throw new ProjectDocumentMigrationError(`Unsupported schemaVersion '${String(version)}'.`);
1280
1307
  const migrated = structuredClone(input);
1281
1308
  if (version <= 1)
@@ -1324,6 +1351,8 @@ export function migrateProjectDocument(input) {
1324
1351
  migrateV31ToV32(migrated);
1325
1352
  if (version <= 32)
1326
1353
  migrateV32ToV33(migrated);
1354
+ if (version <= 39)
1355
+ migrateV39ToV40(migrated);
1327
1356
  // v36 → v37 (position multiplier у spine-коннекторов) не требует переписывания данных:
1328
1357
  // добавлены только опциональные поля коннектора, и любой документ v36 уже валиден как v37.
1329
1358
  // v34 → v35 (`states.normalAssetId` стал опциональным у button/staged-button) не требует
@@ -1338,6 +1367,7 @@ export function migrateProjectDocument(input) {
1338
1367
  // v15 хранить не мог, поэтому запрещённых значений в старых документах не бывает.
1339
1368
  // v38 → v39 (per-state button tint/opacity) не требует переписывания данных: новые appearance
1340
1369
  // fields optional and therefore resolve to white / 1 for every existing button.
1370
+ // v39 → v40 (bitmap-text presentation overrides) likewise adds only optional sparse fields.
1341
1371
  // v37 → v38 (page-group carousel arrows) не требует переписывания данных: добавлены только
1342
1372
  // опциональные поля `pageUpButtonNodeId`/`pageDownButtonNodeId`, любой документ v37 уже валиден как v38.
1343
1373
  migrated.schemaVersion = CURRENT_SCHEMA_VERSION;