@routevn/creator-model 1.3.0 → 1.3.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/model.js +1394 -198
package/src/model.js CHANGED
@@ -49,7 +49,16 @@ const TAG_SCOPE_BASE_KEYS = [
49
49
  "sounds",
50
50
  "videos",
51
51
  "characters",
52
+ "fonts",
52
53
  "transforms",
54
+ "colors",
55
+ "textStyles",
56
+ "variables",
57
+ "layouts",
58
+ "controls",
59
+ "animations",
60
+ "particles",
61
+ "spritesheets",
53
62
  ];
54
63
  const CHARACTER_SPRITE_TAG_SCOPE_PREFIX = "characterSprites:";
55
64
  const createEmptyTagsState = () => ({
@@ -57,7 +66,16 @@ const createEmptyTagsState = () => ({
57
66
  sounds: createEmptyCollectionState(),
58
67
  videos: createEmptyCollectionState(),
59
68
  characters: createEmptyCollectionState(),
69
+ fonts: createEmptyCollectionState(),
60
70
  transforms: createEmptyCollectionState(),
71
+ colors: createEmptyCollectionState(),
72
+ textStyles: createEmptyCollectionState(),
73
+ variables: createEmptyCollectionState(),
74
+ layouts: createEmptyCollectionState(),
75
+ controls: createEmptyCollectionState(),
76
+ animations: createEmptyCollectionState(),
77
+ particles: createEmptyCollectionState(),
78
+ spritesheets: createEmptyCollectionState(),
61
79
  });
62
80
  const isCharacterSpriteTagScopeKey = (value) =>
63
81
  isNonEmptyString(value) &&
@@ -1024,6 +1042,7 @@ const validateSpritesheetItems = ({ items, path, errorFactory }) => {
1024
1042
  "type",
1025
1043
  "name",
1026
1044
  "description",
1045
+ "tagIds",
1027
1046
  "thumbnailFileId",
1028
1047
  "fileId",
1029
1048
  "sheetWidth",
@@ -1071,6 +1090,18 @@ const validateSpritesheetItems = ({ items, path, errorFactory }) => {
1071
1090
  }
1072
1091
 
1073
1092
  if (item.type === "spritesheet") {
1093
+ {
1094
+ const result = validateOptionalUniqueIdArray({
1095
+ value: item.tagIds,
1096
+ path: `${itemPath}.tagIds`,
1097
+ errorFactory,
1098
+ allowEmpty: false,
1099
+ });
1100
+ if (result?.valid === false) {
1101
+ return result;
1102
+ }
1103
+ }
1104
+
1074
1105
  if (
1075
1106
  item.thumbnailFileId !== undefined &&
1076
1107
  !isNonEmptyString(item.thumbnailFileId)
@@ -1970,7 +2001,7 @@ const validateAnimationItems = ({ items, path, errorFactory }) => {
1970
2001
  allowedKeys:
1971
2002
  item.type === "folder"
1972
2003
  ? ["id", "type", "name", "description"]
1973
- : ["id", "type", "name", "description", "animation"],
2004
+ : ["id", "type", "name", "description", "tagIds", "animation"],
1974
2005
  path: itemPath,
1975
2006
  errorFactory,
1976
2007
  });
@@ -2008,6 +2039,18 @@ const validateAnimationItems = ({ items, path, errorFactory }) => {
2008
2039
  }
2009
2040
 
2010
2041
  if (item.type === "animation") {
2042
+ {
2043
+ const result = validateOptionalUniqueIdArray({
2044
+ value: item.tagIds,
2045
+ path: `${itemPath}.tagIds`,
2046
+ errorFactory,
2047
+ allowEmpty: false,
2048
+ });
2049
+ if (result?.valid === false) {
2050
+ return result;
2051
+ }
2052
+ }
2053
+
2011
2054
  {
2012
2055
  const result = validateAnimationDefinition({
2013
2056
  animation: item.animation,
@@ -2044,6 +2087,7 @@ const validateFontItems = ({ items, path, errorFactory }) => {
2044
2087
  "type",
2045
2088
  "name",
2046
2089
  "description",
2090
+ "tagIds",
2047
2091
  "fileId",
2048
2092
  "fontFamily",
2049
2093
  ],
@@ -2084,6 +2128,18 @@ const validateFontItems = ({ items, path, errorFactory }) => {
2084
2128
  }
2085
2129
 
2086
2130
  if (item.type === "font") {
2131
+ {
2132
+ const result = validateOptionalUniqueIdArray({
2133
+ value: item.tagIds,
2134
+ path: `${itemPath}.tagIds`,
2135
+ errorFactory,
2136
+ allowEmpty: false,
2137
+ });
2138
+ if (result?.valid === false) {
2139
+ return result;
2140
+ }
2141
+ }
2142
+
2087
2143
  if (!isNonEmptyString(item.fileId)) {
2088
2144
  return invalidFromErrorFactory(
2089
2145
  errorFactory,
@@ -2097,7 +2153,6 @@ const validateFontItems = ({ items, path, errorFactory }) => {
2097
2153
  `${itemPath}.fontFamily must be a non-empty string`,
2098
2154
  );
2099
2155
  }
2100
-
2101
2156
  }
2102
2157
  }
2103
2158
  };
@@ -2119,7 +2174,7 @@ const validateColorItems = ({ items, path, errorFactory }) => {
2119
2174
  allowedKeys:
2120
2175
  item.type === "folder"
2121
2176
  ? ["id", "type", "name", "description"]
2122
- : ["id", "type", "name", "description", "hex"],
2177
+ : ["id", "type", "name", "description", "tagIds", "hex"],
2123
2178
  path: itemPath,
2124
2179
  errorFactory,
2125
2180
  });
@@ -2156,11 +2211,25 @@ const validateColorItems = ({ items, path, errorFactory }) => {
2156
2211
  );
2157
2212
  }
2158
2213
 
2159
- if (item.type === "color" && !isHexColor(item.hex)) {
2160
- return invalidFromErrorFactory(
2161
- errorFactory,
2162
- `${itemPath}.hex must be a #RRGGBB string`,
2163
- );
2214
+ if (item.type === "color") {
2215
+ {
2216
+ const result = validateOptionalUniqueIdArray({
2217
+ value: item.tagIds,
2218
+ path: `${itemPath}.tagIds`,
2219
+ errorFactory,
2220
+ allowEmpty: false,
2221
+ });
2222
+ if (result?.valid === false) {
2223
+ return result;
2224
+ }
2225
+ }
2226
+
2227
+ if (!isHexColor(item.hex)) {
2228
+ return invalidFromErrorFactory(
2229
+ errorFactory,
2230
+ `${itemPath}.hex must be a #RRGGBB string`,
2231
+ );
2232
+ }
2164
2233
  }
2165
2234
  }
2166
2235
  };
@@ -2321,6 +2390,7 @@ const validateParticleItems = ({ items, path, errorFactory }) => {
2321
2390
  "type",
2322
2391
  "name",
2323
2392
  "description",
2393
+ "tagIds",
2324
2394
  "width",
2325
2395
  "height",
2326
2396
  "seed",
@@ -2366,6 +2436,18 @@ const validateParticleItems = ({ items, path, errorFactory }) => {
2366
2436
  continue;
2367
2437
  }
2368
2438
 
2439
+ {
2440
+ const result = validateOptionalUniqueIdArray({
2441
+ value: item.tagIds,
2442
+ path: `${itemPath}.tagIds`,
2443
+ errorFactory,
2444
+ allowEmpty: false,
2445
+ });
2446
+ if (result?.valid === false) {
2447
+ return result;
2448
+ }
2449
+ }
2450
+
2369
2451
  if (!isFiniteNumber(item.width) || item.width <= 0) {
2370
2452
  return invalidFromErrorFactory(
2371
2453
  errorFactory,
@@ -2448,6 +2530,7 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
2448
2530
  "type",
2449
2531
  "name",
2450
2532
  "description",
2533
+ "tagIds",
2451
2534
  "scope",
2452
2535
  "default",
2453
2536
  "value",
@@ -2489,6 +2572,18 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
2489
2572
  }
2490
2573
 
2491
2574
  if (variableType !== "folder") {
2575
+ {
2576
+ const result = validateOptionalUniqueIdArray({
2577
+ value: item.tagIds,
2578
+ path: `${itemPath}.tagIds`,
2579
+ errorFactory,
2580
+ allowEmpty: false,
2581
+ });
2582
+ if (result?.valid === false) {
2583
+ return result;
2584
+ }
2585
+ }
2586
+
2492
2587
  if (!VARIABLE_SCOPE_KEYS.includes(item.scope)) {
2493
2588
  return invalidFromErrorFactory(
2494
2589
  errorFactory,
@@ -2544,6 +2639,7 @@ const validateTextStyleItems = ({ items, path, errorFactory }) => {
2544
2639
  "type",
2545
2640
  "name",
2546
2641
  "description",
2642
+ "tagIds",
2547
2643
  "fontId",
2548
2644
  "colorId",
2549
2645
  "fontSize",
@@ -2596,6 +2692,18 @@ const validateTextStyleItems = ({ items, path, errorFactory }) => {
2596
2692
  }
2597
2693
 
2598
2694
  if (item.type === "textStyle") {
2695
+ {
2696
+ const result = validateOptionalUniqueIdArray({
2697
+ value: item.tagIds,
2698
+ path: `${itemPath}.tagIds`,
2699
+ errorFactory,
2700
+ allowEmpty: false,
2701
+ });
2702
+ if (result?.valid === false) {
2703
+ return result;
2704
+ }
2705
+ }
2706
+
2599
2707
  if (!isNonEmptyString(item.fontId)) {
2600
2708
  return invalidFromErrorFactory(
2601
2709
  errorFactory,
@@ -3589,6 +3697,7 @@ const validateCharacterItems = ({ items, path, errorFactory }) => {
3589
3697
  "name",
3590
3698
  "description",
3591
3699
  "tagIds",
3700
+ "spriteGroups",
3592
3701
  "shortcut",
3593
3702
  "fileId",
3594
3703
  "sprites",
@@ -3642,6 +3751,19 @@ const validateCharacterItems = ({ items, path, errorFactory }) => {
3642
3751
  }
3643
3752
  }
3644
3753
 
3754
+ {
3755
+ const result = validateCharacterSpriteGroups({
3756
+ value: item.spriteGroups,
3757
+ path: `${itemPath}.spriteGroups`,
3758
+ errorFactory,
3759
+ allowEmpty: false,
3760
+ allowMissingId: true,
3761
+ });
3762
+ if (result?.valid === false) {
3763
+ return result;
3764
+ }
3765
+ }
3766
+
3645
3767
  if (item.shortcut !== undefined && !isString(item.shortcut)) {
3646
3768
  return invalidFromErrorFactory(
3647
3769
  errorFactory,
@@ -3777,10 +3899,7 @@ const validateTagCreateData = ({
3777
3899
  }
3778
3900
 
3779
3901
  if (data?.type !== "tag") {
3780
- return invalidFromErrorFactory(
3781
- errorFactory,
3782
- `${path}.type must be 'tag'`,
3783
- );
3902
+ return invalidFromErrorFactory(errorFactory, `${path}.type must be 'tag'`);
3784
3903
  }
3785
3904
 
3786
3905
  if (!isNonEmptyString(data.name)) {
@@ -3847,7 +3966,10 @@ const validateTagsRoot = ({ state, tags, path, errorFactory }) => {
3847
3966
  }
3848
3967
 
3849
3968
  for (const scopeKey of Object.keys(tags)) {
3850
- if (!isBaseTagScopeKey(scopeKey) && !isCharacterSpriteTagScopeKey(scopeKey)) {
3969
+ if (
3970
+ !isBaseTagScopeKey(scopeKey) &&
3971
+ !isCharacterSpriteTagScopeKey(scopeKey)
3972
+ ) {
3851
3973
  return invalidFromErrorFactory(
3852
3974
  errorFactory,
3853
3975
  `${path}.${scopeKey} is not allowed`,
@@ -3992,6 +4114,39 @@ const validateTagIdsAgainstScope = ({
3992
4114
  return VALID_RESULT;
3993
4115
  };
3994
4116
 
4117
+ const validateCharacterSpriteGroupsAgainstScope = ({
4118
+ state,
4119
+ spriteGroups,
4120
+ scopeKey,
4121
+ path,
4122
+ details = {},
4123
+ errorFactory = createPreconditionValidationError,
4124
+ }) => {
4125
+ if (spriteGroups === undefined) {
4126
+ return VALID_RESULT;
4127
+ }
4128
+
4129
+ for (const [index, spriteGroup] of spriteGroups.entries()) {
4130
+ const result = validateTagIdsAgainstScope({
4131
+ state,
4132
+ tagIds: spriteGroup.tags,
4133
+ scopeKey,
4134
+ path: `${path}[${index}].tags`,
4135
+ details: {
4136
+ ...details,
4137
+ spriteGroupIndex: index,
4138
+ spriteGroupName: spriteGroup.name,
4139
+ },
4140
+ errorFactory,
4141
+ });
4142
+ if (!result.valid) {
4143
+ return result;
4144
+ }
4145
+ }
4146
+
4147
+ return VALID_RESULT;
4148
+ };
4149
+
3995
4150
  const validateUniqueTagNameInScope = ({
3996
4151
  collection,
3997
4152
  name,
@@ -4029,6 +4184,12 @@ const assignOptionalTagIds = ({ target, tagIds }) => {
4029
4184
  }
4030
4185
  };
4031
4186
 
4187
+ const assignOptionalCharacterSpriteGroups = ({ target, spriteGroups }) => {
4188
+ if (Array.isArray(spriteGroups) && spriteGroups.length > 0) {
4189
+ target.spriteGroups = structuredClone(spriteGroups);
4190
+ }
4191
+ };
4192
+
4032
4193
  const applyTagIdsUpdate = ({ currentItem, data }) => {
4033
4194
  const nextData = structuredClone(data);
4034
4195
  if (nextData.tagIds === undefined) {
@@ -4051,12 +4212,31 @@ const applyTagIdsUpdate = ({ currentItem, data }) => {
4051
4212
  return nextItem;
4052
4213
  };
4053
4214
 
4215
+ const applyCharacterUpdate = ({ currentItem, data }) => {
4216
+ const nextItem = applyTagIdsUpdate({
4217
+ currentItem,
4218
+ data,
4219
+ });
4220
+
4221
+ if (data.spriteGroups !== undefined) {
4222
+ if (Array.isArray(data.spriteGroups) && data.spriteGroups.length > 0) {
4223
+ nextItem.spriteGroups = structuredClone(data.spriteGroups);
4224
+ } else {
4225
+ delete nextItem.spriteGroups;
4226
+ }
4227
+ }
4228
+
4229
+ return nextItem;
4230
+ };
4231
+
4054
4232
  const stripDeletedTagIdsFromItem = ({ item, deletedTagIds }) => {
4055
4233
  if (!Array.isArray(item?.tagIds) || item.tagIds.length === 0) {
4056
4234
  return;
4057
4235
  }
4058
4236
 
4059
- const remainingTagIds = item.tagIds.filter((tagId) => !deletedTagIds.has(tagId));
4237
+ const remainingTagIds = item.tagIds.filter(
4238
+ (tagId) => !deletedTagIds.has(tagId),
4239
+ );
4060
4240
  if (remainingTagIds.length === item.tagIds.length) {
4061
4241
  return;
4062
4242
  }
@@ -4069,7 +4249,62 @@ const stripDeletedTagIdsFromItem = ({ item, deletedTagIds }) => {
4069
4249
  item.tagIds = remainingTagIds;
4070
4250
  };
4071
4251
 
4072
- const stripDeletedTagIdsFromScopeItems = ({ state, scopeKey, deletedTagIds }) => {
4252
+ const stripDeletedTagIdsFromCharacterSpriteGroups = ({
4253
+ item,
4254
+ deletedTagIds,
4255
+ }) => {
4256
+ if (!Array.isArray(item?.spriteGroups) || item.spriteGroups.length === 0) {
4257
+ return;
4258
+ }
4259
+
4260
+ const nextSpriteGroups = [];
4261
+ let didChange = false;
4262
+
4263
+ for (const spriteGroup of item.spriteGroups) {
4264
+ const currentTags = Array.isArray(spriteGroup?.tags)
4265
+ ? spriteGroup.tags
4266
+ : [];
4267
+ const remainingTags = currentTags.filter(
4268
+ (tagId) => !deletedTagIds.has(tagId),
4269
+ );
4270
+
4271
+ if (remainingTags.length !== currentTags.length) {
4272
+ didChange = true;
4273
+ }
4274
+
4275
+ if (remainingTags.length === 0) {
4276
+ didChange = true;
4277
+ continue;
4278
+ }
4279
+
4280
+ if (remainingTags.length === currentTags.length) {
4281
+ nextSpriteGroups.push(spriteGroup);
4282
+ continue;
4283
+ }
4284
+
4285
+ nextSpriteGroups.push({
4286
+ ...spriteGroup,
4287
+ tags: remainingTags,
4288
+ });
4289
+ }
4290
+
4291
+ if (!didChange) {
4292
+ return;
4293
+ }
4294
+
4295
+ if (nextSpriteGroups.length === 0) {
4296
+ delete item.spriteGroups;
4297
+ return;
4298
+ }
4299
+
4300
+ item.spriteGroups = nextSpriteGroups;
4301
+ };
4302
+
4303
+ const stripDeletedTagIdsFromScopeItems = ({
4304
+ state,
4305
+ scopeKey,
4306
+ deletedTagIds,
4307
+ }) => {
4073
4308
  if (deletedTagIds.size === 0) {
4074
4309
  return;
4075
4310
  }
@@ -4119,16 +4354,105 @@ const stripDeletedTagIdsFromScopeItems = ({ state, scopeKey, deletedTagIds }) =>
4119
4354
  return;
4120
4355
  }
4121
4356
 
4357
+ if (scopeKey === "fonts") {
4358
+ for (const item of Object.values(state.fonts.items)) {
4359
+ if (item?.type === "font") {
4360
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4361
+ }
4362
+ }
4363
+ return;
4364
+ }
4365
+
4366
+ if (scopeKey === "colors") {
4367
+ for (const item of Object.values(state.colors.items)) {
4368
+ if (item?.type === "color") {
4369
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4370
+ }
4371
+ }
4372
+ return;
4373
+ }
4374
+
4375
+ if (scopeKey === "textStyles") {
4376
+ for (const item of Object.values(state.textStyles.items)) {
4377
+ if (item?.type === "textStyle") {
4378
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4379
+ }
4380
+ }
4381
+ return;
4382
+ }
4383
+
4384
+ if (scopeKey === "variables") {
4385
+ for (const item of Object.values(state.variables.items)) {
4386
+ if (item?.type !== "folder") {
4387
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4388
+ }
4389
+ }
4390
+ return;
4391
+ }
4392
+
4393
+ if (scopeKey === "layouts") {
4394
+ for (const item of Object.values(state.layouts.items)) {
4395
+ if (item?.type === "layout") {
4396
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4397
+ }
4398
+ }
4399
+ return;
4400
+ }
4401
+
4402
+ if (scopeKey === "controls") {
4403
+ for (const item of Object.values(state.controls.items)) {
4404
+ if (item?.type === "control") {
4405
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4406
+ }
4407
+ }
4408
+ return;
4409
+ }
4410
+
4411
+ if (scopeKey === "animations") {
4412
+ for (const item of Object.values(state.animations.items)) {
4413
+ if (item?.type === "animation") {
4414
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4415
+ }
4416
+ }
4417
+ return;
4418
+ }
4419
+
4420
+ if (scopeKey === "particles") {
4421
+ for (const item of Object.values(state.particles.items)) {
4422
+ if (item?.type === "particle") {
4423
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4424
+ }
4425
+ }
4426
+ return;
4427
+ }
4428
+
4429
+ if (scopeKey === "spritesheets") {
4430
+ for (const item of Object.values(state.spritesheets.items)) {
4431
+ if (item?.type === "spritesheet") {
4432
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4433
+ }
4434
+ }
4435
+ return;
4436
+ }
4437
+
4122
4438
  if (!isCharacterSpriteTagScopeKey(scopeKey)) {
4123
4439
  return;
4124
4440
  }
4125
4441
 
4126
4442
  const characterId = getCharacterSpriteTagScopeCharacterId(scopeKey);
4443
+ const characterItem = state.characters?.items?.[characterId];
4127
4444
  const collection = getCharacterSpriteCollection({
4128
4445
  state,
4129
4446
  characterId,
4130
4447
  });
4131
4448
 
4449
+ if (characterItem?.type === "character") {
4450
+ stripDeletedTagIdsFromCharacterSpriteGroups({
4451
+ item: characterItem,
4452
+ deletedTagIds,
4453
+ });
4454
+ }
4455
+
4132
4456
  for (const item of Object.values(collection?.items || {})) {
4133
4457
  if (item?.type === "image") {
4134
4458
  stripDeletedTagIdsFromItem({ item, deletedTagIds });
@@ -4204,6 +4528,7 @@ const validateLayoutItems = ({ items, path, errorFactory }) => {
4204
4528
  "type",
4205
4529
  "name",
4206
4530
  "description",
4531
+ "tagIds",
4207
4532
  "layoutType",
4208
4533
  "isFragment",
4209
4534
  "thumbnailFileId",
@@ -4268,6 +4593,18 @@ const validateLayoutItems = ({ items, path, errorFactory }) => {
4268
4593
  }
4269
4594
 
4270
4595
  if (item.type === "layout") {
4596
+ {
4597
+ const result = validateOptionalUniqueIdArray({
4598
+ value: item.tagIds,
4599
+ path: `${itemPath}.tagIds`,
4600
+ errorFactory,
4601
+ allowEmpty: false,
4602
+ });
4603
+ if (result?.valid === false) {
4604
+ return result;
4605
+ }
4606
+ }
4607
+
4271
4608
  if (!LAYOUT_TYPE_KEYS.includes(item.layoutType)) {
4272
4609
  return invalidFromErrorFactory(
4273
4610
  errorFactory,
@@ -4323,6 +4660,7 @@ const validateControlItems = ({ items, path, errorFactory }) => {
4323
4660
  "type",
4324
4661
  "name",
4325
4662
  "description",
4663
+ "tagIds",
4326
4664
  "thumbnailFileId",
4327
4665
  "preview",
4328
4666
  "elements",
@@ -4387,9 +4725,21 @@ const validateControlItems = ({ items, path, errorFactory }) => {
4387
4725
 
4388
4726
  if (item.type === "control") {
4389
4727
  {
4390
- const result = validateNestedCollection({
4391
- collection: item.elements,
4392
- path: `${itemPath}.elements`,
4728
+ const result = validateOptionalUniqueIdArray({
4729
+ value: item.tagIds,
4730
+ path: `${itemPath}.tagIds`,
4731
+ errorFactory,
4732
+ allowEmpty: false,
4733
+ });
4734
+ if (result?.valid === false) {
4735
+ return result;
4736
+ }
4737
+ }
4738
+
4739
+ {
4740
+ const result = validateNestedCollection({
4741
+ collection: item.elements,
4742
+ path: `${itemPath}.elements`,
4393
4743
  itemValidator: validateLayoutElementItems,
4394
4744
  treeValidator: validateLayoutElementTreeOwnership,
4395
4745
  treeNodeLabel: "control element",
@@ -5501,6 +5851,22 @@ export const assertInvariants = ({ state }) => {
5501
5851
  return result;
5502
5852
  }
5503
5853
 
5854
+ {
5855
+ const tagResult = validateTagIdsAgainstScope({
5856
+ state,
5857
+ tagIds: spritesheet.tagIds,
5858
+ scopeKey: "spritesheets",
5859
+ path: "spritesheet.tagIds",
5860
+ details: {
5861
+ spritesheetId,
5862
+ },
5863
+ errorFactory: createInvariantValidationError,
5864
+ });
5865
+ if (!tagResult.valid) {
5866
+ return tagResult;
5867
+ }
5868
+ }
5869
+
5504
5870
  if (spritesheet.thumbnailFileId === undefined) {
5505
5871
  continue;
5506
5872
  }
@@ -5624,6 +5990,22 @@ export const assertInvariants = ({ state }) => {
5624
5990
  continue;
5625
5991
  }
5626
5992
 
5993
+ {
5994
+ const result = validateTagIdsAgainstScope({
5995
+ state,
5996
+ tagIds: font.tagIds,
5997
+ scopeKey: "fonts",
5998
+ path: "font.tagIds",
5999
+ details: {
6000
+ fontId,
6001
+ },
6002
+ errorFactory: createInvariantValidationError,
6003
+ });
6004
+ if (!result.valid) {
6005
+ return result;
6006
+ }
6007
+ }
6008
+
5627
6009
  const result = validateFileReference({
5628
6010
  state,
5629
6011
  fileId: font.fileId,
@@ -5643,6 +6025,22 @@ export const assertInvariants = ({ state }) => {
5643
6025
  continue;
5644
6026
  }
5645
6027
 
6028
+ {
6029
+ const tagResult = validateTagIdsAgainstScope({
6030
+ state,
6031
+ tagIds: animation.tagIds,
6032
+ scopeKey: "animations",
6033
+ path: "animation.tagIds",
6034
+ details: {
6035
+ animationId,
6036
+ },
6037
+ errorFactory: createInvariantValidationError,
6038
+ });
6039
+ if (!tagResult.valid) {
6040
+ return tagResult;
6041
+ }
6042
+ }
6043
+
5646
6044
  const result = validateAnimationMaskImageReferences({
5647
6045
  state,
5648
6046
  animation: animation.animation,
@@ -5691,6 +6089,22 @@ export const assertInvariants = ({ state }) => {
5691
6089
  }
5692
6090
  }
5693
6091
 
6092
+ {
6093
+ const result = validateCharacterSpriteGroupsAgainstScope({
6094
+ state,
6095
+ spriteGroups: character.spriteGroups,
6096
+ scopeKey: `${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${characterId}`,
6097
+ path: "character.spriteGroups",
6098
+ details: {
6099
+ characterId,
6100
+ },
6101
+ errorFactory: createInvariantValidationError,
6102
+ });
6103
+ if (!result.valid) {
6104
+ return result;
6105
+ }
6106
+ }
6107
+
5694
6108
  for (const [spriteId, sprite] of Object.entries(
5695
6109
  character.sprites?.items || {},
5696
6110
  )) {
@@ -5771,25 +6185,153 @@ export const assertInvariants = ({ state }) => {
5771
6185
  }
5772
6186
  }
5773
6187
 
6188
+ for (const [particleId, particle] of Object.entries(state.particles.items)) {
6189
+ if (particle.type !== "particle") {
6190
+ continue;
6191
+ }
6192
+
6193
+ {
6194
+ const result = validateTagIdsAgainstScope({
6195
+ state,
6196
+ tagIds: particle.tagIds,
6197
+ scopeKey: "particles",
6198
+ path: "particle.tagIds",
6199
+ details: {
6200
+ particleId,
6201
+ },
6202
+ errorFactory: createInvariantValidationError,
6203
+ });
6204
+ if (!result.valid) {
6205
+ return result;
6206
+ }
6207
+ }
6208
+ }
6209
+
6210
+ for (const [colorId, color] of Object.entries(state.colors.items)) {
6211
+ if (color.type !== "color") {
6212
+ continue;
6213
+ }
6214
+
6215
+ {
6216
+ const result = validateTagIdsAgainstScope({
6217
+ state,
6218
+ tagIds: color.tagIds,
6219
+ scopeKey: "colors",
6220
+ path: "color.tagIds",
6221
+ details: {
6222
+ colorId,
6223
+ },
6224
+ errorFactory: createInvariantValidationError,
6225
+ });
6226
+ if (!result.valid) {
6227
+ return result;
6228
+ }
6229
+ }
6230
+ }
6231
+
6232
+ for (const [textStyleId, textStyle] of Object.entries(
6233
+ state.textStyles.items,
6234
+ )) {
6235
+ if (textStyle.type !== "textStyle") {
6236
+ continue;
6237
+ }
6238
+
6239
+ {
6240
+ const result = validateTagIdsAgainstScope({
6241
+ state,
6242
+ tagIds: textStyle.tagIds,
6243
+ scopeKey: "textStyles",
6244
+ path: "textStyle.tagIds",
6245
+ details: {
6246
+ textStyleId,
6247
+ },
6248
+ errorFactory: createInvariantValidationError,
6249
+ });
6250
+ if (!result.valid) {
6251
+ return result;
6252
+ }
6253
+ }
6254
+ }
6255
+
5774
6256
  for (const [layoutId, layout] of Object.entries(state.layouts.items)) {
5775
- if (layout.type !== "layout" || layout.thumbnailFileId === undefined) {
6257
+ if (layout.type !== "layout") {
5776
6258
  continue;
5777
6259
  }
5778
6260
 
5779
- const result = validateFileReference({
5780
- state,
5781
- fileId: layout.thumbnailFileId,
5782
- path: "layout.thumbnailFileId",
5783
- details: { layoutId, thumbnailFileId: layout.thumbnailFileId },
5784
- errorFactory: createInvariantValidationError,
5785
- });
5786
- if (!result.valid) {
5787
- return result;
6261
+ {
6262
+ const result = validateTagIdsAgainstScope({
6263
+ state,
6264
+ tagIds: layout.tagIds,
6265
+ scopeKey: "layouts",
6266
+ path: "layout.tagIds",
6267
+ details: {
6268
+ layoutId,
6269
+ },
6270
+ errorFactory: createInvariantValidationError,
6271
+ });
6272
+ if (!result.valid) {
6273
+ return result;
6274
+ }
6275
+ }
6276
+
6277
+ if (layout.thumbnailFileId !== undefined) {
6278
+ const result = validateFileReference({
6279
+ state,
6280
+ fileId: layout.thumbnailFileId,
6281
+ path: "layout.thumbnailFileId",
6282
+ details: { layoutId, thumbnailFileId: layout.thumbnailFileId },
6283
+ errorFactory: createInvariantValidationError,
6284
+ });
6285
+ if (!result.valid) {
6286
+ return result;
6287
+ }
6288
+ }
6289
+ }
6290
+
6291
+ for (const [variableId, variable] of Object.entries(state.variables.items)) {
6292
+ if (variable.type === "folder") {
6293
+ continue;
6294
+ }
6295
+
6296
+ {
6297
+ const result = validateTagIdsAgainstScope({
6298
+ state,
6299
+ tagIds: variable.tagIds,
6300
+ scopeKey: "variables",
6301
+ path: "variable.tagIds",
6302
+ details: {
6303
+ variableId,
6304
+ },
6305
+ errorFactory: createInvariantValidationError,
6306
+ });
6307
+ if (!result.valid) {
6308
+ return result;
6309
+ }
5788
6310
  }
5789
6311
  }
5790
6312
 
5791
6313
  for (const [controlId, control] of Object.entries(state.controls.items)) {
5792
- if (control.type !== "control" || control.thumbnailFileId === undefined) {
6314
+ if (control.type !== "control") {
6315
+ continue;
6316
+ }
6317
+
6318
+ {
6319
+ const result = validateTagIdsAgainstScope({
6320
+ state,
6321
+ tagIds: control.tagIds,
6322
+ scopeKey: "controls",
6323
+ path: "control.tagIds",
6324
+ details: {
6325
+ controlId,
6326
+ },
6327
+ errorFactory: createInvariantValidationError,
6328
+ });
6329
+ if (!result.valid) {
6330
+ return result;
6331
+ }
6332
+ }
6333
+
6334
+ if (control.thumbnailFileId === undefined) {
5793
6335
  continue;
5794
6336
  }
5795
6337
 
@@ -6494,6 +7036,97 @@ const validateOptionalUniqueIdArray = ({
6494
7036
  }
6495
7037
  };
6496
7038
 
7039
+ const validateCharacterSpriteGroups = ({
7040
+ value,
7041
+ path,
7042
+ errorFactory,
7043
+ allowEmpty = true,
7044
+ allowMissingId = false,
7045
+ }) => {
7046
+ if (value === undefined) {
7047
+ return VALID_RESULT;
7048
+ }
7049
+
7050
+ if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) {
7051
+ return invalidFromErrorFactory(
7052
+ errorFactory,
7053
+ allowEmpty
7054
+ ? `${path} must be an array when provided`
7055
+ : `${path} must be a non-empty array when provided`,
7056
+ );
7057
+ }
7058
+
7059
+ const seenIds = new Set();
7060
+
7061
+ for (const [index, spriteGroup] of value.entries()) {
7062
+ const itemPath = `${path}[${index}]`;
7063
+
7064
+ if (!isPlainObject(spriteGroup)) {
7065
+ return invalidFromErrorFactory(
7066
+ errorFactory,
7067
+ `${itemPath} must be an object`,
7068
+ );
7069
+ }
7070
+
7071
+ {
7072
+ const result = validateAllowedKeys({
7073
+ value: spriteGroup,
7074
+ allowedKeys: ["id", "name", "tags"],
7075
+ path: itemPath,
7076
+ errorFactory,
7077
+ });
7078
+ if (result?.valid === false) {
7079
+ return result;
7080
+ }
7081
+ }
7082
+
7083
+ if (spriteGroup.id === undefined) {
7084
+ if (!allowMissingId) {
7085
+ return invalidFromErrorFactory(
7086
+ errorFactory,
7087
+ `${itemPath}.id must be a non-empty string`,
7088
+ );
7089
+ }
7090
+ } else {
7091
+ if (!isNonEmptyString(spriteGroup.id)) {
7092
+ return invalidFromErrorFactory(
7093
+ errorFactory,
7094
+ `${itemPath}.id must be a non-empty string`,
7095
+ );
7096
+ }
7097
+
7098
+ if (seenIds.has(spriteGroup.id)) {
7099
+ return invalidFromErrorFactory(
7100
+ errorFactory,
7101
+ `${itemPath}.id must be unique within spriteGroups`,
7102
+ );
7103
+ }
7104
+
7105
+ seenIds.add(spriteGroup.id);
7106
+ }
7107
+
7108
+ if (!isNonEmptyString(spriteGroup.name)) {
7109
+ return invalidFromErrorFactory(
7110
+ errorFactory,
7111
+ `${itemPath}.name must be a non-empty string`,
7112
+ );
7113
+ }
7114
+
7115
+ {
7116
+ const result = validateRequiredUniqueIdArray({
7117
+ value: spriteGroup.tags,
7118
+ path: `${itemPath}.tags`,
7119
+ errorFactory,
7120
+ });
7121
+ if (result?.valid === false) {
7122
+ return result;
7123
+ }
7124
+ }
7125
+ }
7126
+
7127
+ return VALID_RESULT;
7128
+ };
7129
+
6497
7130
  const validateSectionCreateData = ({ data, errorFactory }) => {
6498
7131
  {
6499
7132
  const result = validateExactKeys({
@@ -6899,6 +7532,7 @@ const validateSpritesheetCreateData = ({ data, errorFactory }) => {
6899
7532
  "type",
6900
7533
  "name",
6901
7534
  "description",
7535
+ "tagIds",
6902
7536
  "thumbnailFileId",
6903
7537
  "fileId",
6904
7538
  "sheetWidth",
@@ -6932,6 +7566,17 @@ const validateSpritesheetCreateData = ({ data, errorFactory }) => {
6932
7566
  }
6933
7567
 
6934
7568
  if (data.type === "spritesheet") {
7569
+ {
7570
+ const result = validateOptionalUniqueIdArray({
7571
+ value: data.tagIds,
7572
+ path: "payload.data.tagIds",
7573
+ errorFactory,
7574
+ });
7575
+ if (result?.valid === false) {
7576
+ return result;
7577
+ }
7578
+ }
7579
+
6935
7580
  if (
6936
7581
  data.thumbnailFileId !== undefined &&
6937
7582
  !isNonEmptyString(data.thumbnailFileId)
@@ -6991,6 +7636,7 @@ const validateSpritesheetUpdateData = ({ data, errorFactory }) => {
6991
7636
  allowedKeys: [
6992
7637
  "name",
6993
7638
  "description",
7639
+ "tagIds",
6994
7640
  "thumbnailFileId",
6995
7641
  "fileId",
6996
7642
  "sheetWidth",
@@ -7030,13 +7676,24 @@ const validateSpritesheetUpdateData = ({ data, errorFactory }) => {
7030
7676
  );
7031
7677
  }
7032
7678
 
7033
- if (
7034
- data.thumbnailFileId !== undefined &&
7035
- !isNonEmptyString(data.thumbnailFileId)
7036
- ) {
7037
- return invalidFromErrorFactory(
7038
- errorFactory,
7039
- "payload.data.thumbnailFileId must be a non-empty string when provided",
7679
+ {
7680
+ const result = validateOptionalUniqueIdArray({
7681
+ value: data.tagIds,
7682
+ path: "payload.data.tagIds",
7683
+ errorFactory,
7684
+ });
7685
+ if (result?.valid === false) {
7686
+ return result;
7687
+ }
7688
+ }
7689
+
7690
+ if (
7691
+ data.thumbnailFileId !== undefined &&
7692
+ !isNonEmptyString(data.thumbnailFileId)
7693
+ ) {
7694
+ return invalidFromErrorFactory(
7695
+ errorFactory,
7696
+ "payload.data.thumbnailFileId must be a non-empty string when provided",
7040
7697
  );
7041
7698
  }
7042
7699
 
@@ -7467,13 +8124,7 @@ const validateFontCreateData = ({ data, errorFactory }) => {
7467
8124
  allowedKeys:
7468
8125
  data.type === "folder"
7469
8126
  ? ["type", "name", "description"]
7470
- : [
7471
- "type",
7472
- "name",
7473
- "description",
7474
- "fileId",
7475
- "fontFamily",
7476
- ],
8127
+ : ["type", "name", "description", "tagIds", "fileId", "fontFamily"],
7477
8128
  path: "payload.data",
7478
8129
  errorFactory,
7479
8130
  });
@@ -7497,6 +8148,17 @@ const validateFontCreateData = ({ data, errorFactory }) => {
7497
8148
  }
7498
8149
 
7499
8150
  if (data.type === "font") {
8151
+ {
8152
+ const result = validateOptionalUniqueIdArray({
8153
+ value: data.tagIds,
8154
+ path: "payload.data.tagIds",
8155
+ errorFactory,
8156
+ });
8157
+ if (result?.valid === false) {
8158
+ return result;
8159
+ }
8160
+ }
8161
+
7500
8162
  if (!isNonEmptyString(data.fileId)) {
7501
8163
  return invalidFromErrorFactory(
7502
8164
  errorFactory,
@@ -7510,7 +8172,6 @@ const validateFontCreateData = ({ data, errorFactory }) => {
7510
8172
  "payload.data.fontFamily must be a non-empty string",
7511
8173
  );
7512
8174
  }
7513
-
7514
8175
  }
7515
8176
  };
7516
8177
 
@@ -7518,12 +8179,7 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
7518
8179
  {
7519
8180
  const result = validateAllowedKeys({
7520
8181
  value: data,
7521
- allowedKeys: [
7522
- "name",
7523
- "description",
7524
- "fileId",
7525
- "fontFamily",
7526
- ],
8182
+ allowedKeys: ["name", "description", "tagIds", "fileId", "fontFamily"],
7527
8183
  path: "payload.data",
7528
8184
  errorFactory,
7529
8185
  });
@@ -7553,6 +8209,17 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
7553
8209
  );
7554
8210
  }
7555
8211
 
8212
+ {
8213
+ const result = validateOptionalUniqueIdArray({
8214
+ value: data.tagIds,
8215
+ path: "payload.data.tagIds",
8216
+ errorFactory,
8217
+ });
8218
+ if (result?.valid === false) {
8219
+ return result;
8220
+ }
8221
+ }
8222
+
7556
8223
  if (data.fileId !== undefined && !isNonEmptyString(data.fileId)) {
7557
8224
  return invalidFromErrorFactory(
7558
8225
  errorFactory,
@@ -7566,7 +8233,6 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
7566
8233
  "payload.data.fontFamily must be a non-empty string when provided",
7567
8234
  );
7568
8235
  }
7569
-
7570
8236
  };
7571
8237
 
7572
8238
  const validateFileCreateData = ({ data, errorFactory }) => {
@@ -7690,7 +8356,7 @@ const validateColorCreateData = ({ data, errorFactory }) => {
7690
8356
  allowedKeys:
7691
8357
  data.type === "folder"
7692
8358
  ? ["type", "name", "description"]
7693
- : ["type", "name", "description", "hex"],
8359
+ : ["type", "name", "description", "tagIds", "hex"],
7694
8360
  path: "payload.data",
7695
8361
  errorFactory,
7696
8362
  });
@@ -7713,11 +8379,24 @@ const validateColorCreateData = ({ data, errorFactory }) => {
7713
8379
  );
7714
8380
  }
7715
8381
 
7716
- if (data.type === "color" && !isHexColor(data.hex)) {
7717
- return invalidFromErrorFactory(
7718
- errorFactory,
7719
- "payload.data.hex must be a #RRGGBB string",
7720
- );
8382
+ if (data.type === "color") {
8383
+ {
8384
+ const result = validateOptionalUniqueIdArray({
8385
+ value: data.tagIds,
8386
+ path: "payload.data.tagIds",
8387
+ errorFactory,
8388
+ });
8389
+ if (result?.valid === false) {
8390
+ return result;
8391
+ }
8392
+ }
8393
+
8394
+ if (!isHexColor(data.hex)) {
8395
+ return invalidFromErrorFactory(
8396
+ errorFactory,
8397
+ "payload.data.hex must be a #RRGGBB string",
8398
+ );
8399
+ }
7721
8400
  }
7722
8401
  };
7723
8402
 
@@ -7725,7 +8404,7 @@ const validateColorUpdateData = ({ data, errorFactory }) => {
7725
8404
  {
7726
8405
  const result = validateAllowedKeys({
7727
8406
  value: data,
7728
- allowedKeys: ["name", "description", "hex"],
8407
+ allowedKeys: ["name", "description", "tagIds", "hex"],
7729
8408
  path: "payload.data",
7730
8409
  errorFactory,
7731
8410
  });
@@ -7755,6 +8434,17 @@ const validateColorUpdateData = ({ data, errorFactory }) => {
7755
8434
  );
7756
8435
  }
7757
8436
 
8437
+ {
8438
+ const result = validateOptionalUniqueIdArray({
8439
+ value: data.tagIds,
8440
+ path: "payload.data.tagIds",
8441
+ errorFactory,
8442
+ });
8443
+ if (result?.valid === false) {
8444
+ return result;
8445
+ }
8446
+ }
8447
+
7758
8448
  if (data.hex !== undefined && !isHexColor(data.hex)) {
7759
8449
  return invalidFromErrorFactory(
7760
8450
  errorFactory,
@@ -7784,7 +8474,7 @@ const validateAnimationCreateData = ({ data, errorFactory }) => {
7784
8474
  allowedKeys:
7785
8475
  data.type === "folder"
7786
8476
  ? ["type", "name", "description"]
7787
- : ["type", "name", "description", "animation"],
8477
+ : ["type", "name", "description", "tagIds", "animation"],
7788
8478
  path: "payload.data",
7789
8479
  errorFactory,
7790
8480
  });
@@ -7808,6 +8498,17 @@ const validateAnimationCreateData = ({ data, errorFactory }) => {
7808
8498
  }
7809
8499
 
7810
8500
  if (data.type === "animation") {
8501
+ {
8502
+ const result = validateOptionalUniqueIdArray({
8503
+ value: data.tagIds,
8504
+ path: "payload.data.tagIds",
8505
+ errorFactory,
8506
+ });
8507
+ if (result?.valid === false) {
8508
+ return result;
8509
+ }
8510
+ }
8511
+
7811
8512
  {
7812
8513
  const result = validateAnimationDefinition({
7813
8514
  animation: data.animation,
@@ -7825,7 +8526,7 @@ const validateAnimationUpdateData = ({ data, errorFactory }) => {
7825
8526
  {
7826
8527
  const result = validateAllowedKeys({
7827
8528
  value: data,
7828
- allowedKeys: ["name", "description", "animation"],
8529
+ allowedKeys: ["name", "description", "tagIds", "animation"],
7829
8530
  path: "payload.data",
7830
8531
  errorFactory,
7831
8532
  });
@@ -7855,6 +8556,17 @@ const validateAnimationUpdateData = ({ data, errorFactory }) => {
7855
8556
  );
7856
8557
  }
7857
8558
 
8559
+ {
8560
+ const result = validateOptionalUniqueIdArray({
8561
+ value: data.tagIds,
8562
+ path: "payload.data.tagIds",
8563
+ errorFactory,
8564
+ });
8565
+ if (result?.valid === false) {
8566
+ return result;
8567
+ }
8568
+ }
8569
+
7858
8570
  if (data.animation !== undefined) {
7859
8571
  {
7860
8572
  const result = validateAnimationDefinition({
@@ -7981,6 +8693,7 @@ const validateParticleCreateData = ({ data, errorFactory }) => {
7981
8693
  "type",
7982
8694
  "name",
7983
8695
  "description",
8696
+ "tagIds",
7984
8697
  "width",
7985
8698
  "height",
7986
8699
  "seed",
@@ -8012,6 +8725,17 @@ const validateParticleCreateData = ({ data, errorFactory }) => {
8012
8725
  return;
8013
8726
  }
8014
8727
 
8728
+ {
8729
+ const result = validateOptionalUniqueIdArray({
8730
+ value: data.tagIds,
8731
+ path: "payload.data.tagIds",
8732
+ errorFactory,
8733
+ });
8734
+ if (result?.valid === false) {
8735
+ return result;
8736
+ }
8737
+ }
8738
+
8015
8739
  if (!isFiniteNumber(data.width) || data.width <= 0) {
8016
8740
  return invalidFromErrorFactory(
8017
8741
  errorFactory,
@@ -8056,6 +8780,7 @@ const validateParticleUpdateData = ({ data, errorFactory }) => {
8056
8780
  allowedKeys: [
8057
8781
  "name",
8058
8782
  "description",
8783
+ "tagIds",
8059
8784
  "width",
8060
8785
  "height",
8061
8786
  "seed",
@@ -8090,6 +8815,17 @@ const validateParticleUpdateData = ({ data, errorFactory }) => {
8090
8815
  );
8091
8816
  }
8092
8817
 
8818
+ {
8819
+ const result = validateOptionalUniqueIdArray({
8820
+ value: data.tagIds,
8821
+ path: "payload.data.tagIds",
8822
+ errorFactory,
8823
+ });
8824
+ if (result?.valid === false) {
8825
+ return result;
8826
+ }
8827
+ }
8828
+
8093
8829
  if (
8094
8830
  data.width !== undefined &&
8095
8831
  (!isFiniteNumber(data.width) || data.width <= 0)
@@ -8230,7 +8966,15 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
8230
8966
  allowedKeys:
8231
8967
  data.type === "folder"
8232
8968
  ? ["type", "name", "description"]
8233
- : ["type", "name", "description", "scope", "default", "value"],
8969
+ : [
8970
+ "type",
8971
+ "name",
8972
+ "description",
8973
+ "tagIds",
8974
+ "scope",
8975
+ "default",
8976
+ "value",
8977
+ ],
8234
8978
  path: "payload.data",
8235
8979
  errorFactory,
8236
8980
  });
@@ -8254,6 +8998,18 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
8254
8998
  }
8255
8999
 
8256
9000
  if (data.type !== "folder") {
9001
+ {
9002
+ const result = validateOptionalUniqueIdArray({
9003
+ value: data.tagIds,
9004
+ path: "payload.data.tagIds",
9005
+ errorFactory,
9006
+ allowEmpty: false,
9007
+ });
9008
+ if (result?.valid === false) {
9009
+ return result;
9010
+ }
9011
+ }
9012
+
8257
9013
  if (!VARIABLE_SCOPE_KEYS.includes(data.scope)) {
8258
9014
  return invalidFromErrorFactory(
8259
9015
  errorFactory,
@@ -8290,7 +9046,14 @@ const validateVariableUpdateData = ({ data, errorFactory }) => {
8290
9046
  {
8291
9047
  const result = validateAllowedKeys({
8292
9048
  value: data,
8293
- allowedKeys: ["name", "description", "scope", "default", "value"],
9049
+ allowedKeys: [
9050
+ "name",
9051
+ "description",
9052
+ "tagIds",
9053
+ "scope",
9054
+ "default",
9055
+ "value",
9056
+ ],
8294
9057
  path: "payload.data",
8295
9058
  errorFactory,
8296
9059
  });
@@ -8326,6 +9089,18 @@ const validateVariableUpdateData = ({ data, errorFactory }) => {
8326
9089
  "payload.data.scope must be 'context', 'device', or 'account' when provided",
8327
9090
  );
8328
9091
  }
9092
+
9093
+ {
9094
+ const result = validateOptionalUniqueIdArray({
9095
+ value: data.tagIds,
9096
+ path: "payload.data.tagIds",
9097
+ errorFactory,
9098
+ allowEmpty: false,
9099
+ });
9100
+ if (result?.valid === false) {
9101
+ return result;
9102
+ }
9103
+ }
8329
9104
  };
8330
9105
 
8331
9106
  const validateTextStyleCreateData = ({ data, errorFactory }) => {
@@ -8353,6 +9128,7 @@ const validateTextStyleCreateData = ({ data, errorFactory }) => {
8353
9128
  "type",
8354
9129
  "name",
8355
9130
  "description",
9131
+ "tagIds",
8356
9132
  "fontId",
8357
9133
  "colorId",
8358
9134
  "fontSize",
@@ -8391,6 +9167,17 @@ const validateTextStyleCreateData = ({ data, errorFactory }) => {
8391
9167
  }
8392
9168
 
8393
9169
  if (data.type === "textStyle") {
9170
+ {
9171
+ const result = validateOptionalUniqueIdArray({
9172
+ value: data.tagIds,
9173
+ path: "payload.data.tagIds",
9174
+ errorFactory,
9175
+ });
9176
+ if (result?.valid === false) {
9177
+ return result;
9178
+ }
9179
+ }
9180
+
8394
9181
  {
8395
9182
  const result = validateTextStyleItems({
8396
9183
  items: {
@@ -8416,6 +9203,7 @@ const validateTextStyleUpdateData = ({ data, errorFactory }) => {
8416
9203
  allowedKeys: [
8417
9204
  "name",
8418
9205
  "description",
9206
+ "tagIds",
8419
9207
  "fontId",
8420
9208
  "colorId",
8421
9209
  "fontSize",
@@ -8460,6 +9248,17 @@ const validateTextStyleUpdateData = ({ data, errorFactory }) => {
8460
9248
  );
8461
9249
  }
8462
9250
 
9251
+ {
9252
+ const result = validateOptionalUniqueIdArray({
9253
+ value: data.tagIds,
9254
+ path: "payload.data.tagIds",
9255
+ errorFactory,
9256
+ });
9257
+ if (result?.valid === false) {
9258
+ return result;
9259
+ }
9260
+ }
9261
+
8463
9262
  for (const key of ["fontId", "colorId", "strokeColorId"]) {
8464
9263
  if (data[key] !== undefined && !isNonEmptyString(data[key])) {
8465
9264
  return invalidFromErrorFactory(
@@ -8728,6 +9527,7 @@ const validateCharacterCreateData = ({ data, errorFactory }) => {
8728
9527
  "name",
8729
9528
  "description",
8730
9529
  "tagIds",
9530
+ "spriteGroups",
8731
9531
  "shortcut",
8732
9532
  "fileId",
8733
9533
  "sprites",
@@ -8766,6 +9566,17 @@ const validateCharacterCreateData = ({ data, errorFactory }) => {
8766
9566
  }
8767
9567
  }
8768
9568
 
9569
+ {
9570
+ const result = validateCharacterSpriteGroups({
9571
+ value: data.spriteGroups,
9572
+ path: "payload.data.spriteGroups",
9573
+ errorFactory,
9574
+ });
9575
+ if (result?.valid === false) {
9576
+ return result;
9577
+ }
9578
+ }
9579
+
8769
9580
  if (data.shortcut !== undefined && !isString(data.shortcut)) {
8770
9581
  return invalidFromErrorFactory(
8771
9582
  errorFactory,
@@ -8812,6 +9623,7 @@ const validateCharacterUpdateData = ({ data, errorFactory }) => {
8812
9623
  "name",
8813
9624
  "description",
8814
9625
  "tagIds",
9626
+ "spriteGroups",
8815
9627
  "shortcut",
8816
9628
  "fileId",
8817
9629
  ],
@@ -8862,13 +9674,23 @@ const validateCharacterUpdateData = ({ data, errorFactory }) => {
8862
9674
  }
8863
9675
  }
8864
9676
 
9677
+ {
9678
+ const result = validateCharacterSpriteGroups({
9679
+ value: data.spriteGroups,
9680
+ path: "payload.data.spriteGroups",
9681
+ errorFactory,
9682
+ });
9683
+ if (result?.valid === false) {
9684
+ return result;
9685
+ }
9686
+ }
9687
+
8865
9688
  if (data.fileId !== undefined && !isNonEmptyString(data.fileId)) {
8866
9689
  return invalidFromErrorFactory(
8867
9690
  errorFactory,
8868
9691
  "payload.data.fileId must be a non-empty string when provided",
8869
9692
  );
8870
9693
  }
8871
-
8872
9694
  };
8873
9695
 
8874
9696
  const validateLayoutCreateData = ({ data, errorFactory }) => {
@@ -8896,6 +9718,7 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
8896
9718
  "type",
8897
9719
  "name",
8898
9720
  "description",
9721
+ "tagIds",
8899
9722
  "layoutType",
8900
9723
  "isFragment",
8901
9724
  "thumbnailFileId",
@@ -8946,6 +9769,17 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
8946
9769
  }
8947
9770
 
8948
9771
  if (data.type === "layout") {
9772
+ {
9773
+ const result = validateOptionalUniqueIdArray({
9774
+ value: data.tagIds,
9775
+ path: "payload.data.tagIds",
9776
+ errorFactory,
9777
+ });
9778
+ if (result?.valid === false) {
9779
+ return result;
9780
+ }
9781
+ }
9782
+
8949
9783
  if (!LAYOUT_TYPE_KEYS.includes(data.layoutType)) {
8950
9784
  return invalidFromErrorFactory(
8951
9785
  errorFactory,
@@ -8982,6 +9816,7 @@ const validateLayoutUpdateData = ({ data, errorFactory }) => {
8982
9816
  allowedKeys: [
8983
9817
  "name",
8984
9818
  "description",
9819
+ "tagIds",
8985
9820
  "layoutType",
8986
9821
  "isFragment",
8987
9822
  "thumbnailFileId",
@@ -9016,6 +9851,17 @@ const validateLayoutUpdateData = ({ data, errorFactory }) => {
9016
9851
  );
9017
9852
  }
9018
9853
 
9854
+ {
9855
+ const result = validateOptionalUniqueIdArray({
9856
+ value: data.tagIds,
9857
+ path: "payload.data.tagIds",
9858
+ errorFactory,
9859
+ });
9860
+ if (result?.valid === false) {
9861
+ return result;
9862
+ }
9863
+ }
9864
+
9019
9865
  if (
9020
9866
  data.thumbnailFileId !== undefined &&
9021
9867
  !isNonEmptyString(data.thumbnailFileId)
@@ -9080,6 +9926,7 @@ const validateControlCreateData = ({ data, errorFactory }) => {
9080
9926
  "type",
9081
9927
  "name",
9082
9928
  "description",
9929
+ "tagIds",
9083
9930
  "thumbnailFileId",
9084
9931
  "preview",
9085
9932
  "elements",
@@ -9129,6 +9976,18 @@ const validateControlCreateData = ({ data, errorFactory }) => {
9129
9976
  }
9130
9977
 
9131
9978
  if (data.type === "control") {
9979
+ {
9980
+ const result = validateOptionalUniqueIdArray({
9981
+ value: data.tagIds,
9982
+ path: "payload.data.tagIds",
9983
+ errorFactory,
9984
+ allowEmpty: false,
9985
+ });
9986
+ if (result?.valid === false) {
9987
+ return result;
9988
+ }
9989
+ }
9990
+
9132
9991
  {
9133
9992
  const result = validateNestedCollection({
9134
9993
  collection: data.elements,
@@ -9162,6 +10021,7 @@ const validateControlUpdateData = ({ data, errorFactory }) => {
9162
10021
  allowedKeys: [
9163
10022
  "name",
9164
10023
  "description",
10024
+ "tagIds",
9165
10025
  "keyboard",
9166
10026
  "thumbnailFileId",
9167
10027
  "preview",
@@ -9226,6 +10086,18 @@ const validateControlUpdateData = ({ data, errorFactory }) => {
9226
10086
  return result;
9227
10087
  }
9228
10088
  }
10089
+
10090
+ {
10091
+ const result = validateOptionalUniqueIdArray({
10092
+ value: data.tagIds,
10093
+ path: "payload.data.tagIds",
10094
+ errorFactory,
10095
+ allowEmpty: false,
10096
+ });
10097
+ if (result?.valid === false) {
10098
+ return result;
10099
+ }
10100
+ }
9229
10101
  };
9230
10102
 
9231
10103
  const validateLayoutElementCreateData = ({ data, errorFactory }) => {
@@ -10374,59 +11246,68 @@ const COMMAND_DEFINITIONS = [
10374
11246
  itemLabel: "spritesheet item",
10375
11247
  createDataValidator: validateSpritesheetCreateData,
10376
11248
  updateDataValidator: validateSpritesheetUpdateData,
10377
- createItem: ({ payload }) => ({
10378
- id: payload.spritesheetId,
10379
- type: payload.data.type,
10380
- name: payload.data.name,
10381
- ...(payload.data.description !== undefined
10382
- ? {
10383
- description: payload.data.description,
10384
- }
10385
- : {}),
10386
- ...(payload.data.type === "spritesheet"
10387
- ? {
10388
- fileId: payload.data.fileId,
10389
- ...(payload.data.thumbnailFileId !== undefined
10390
- ? {
10391
- thumbnailFileId: payload.data.thumbnailFileId,
10392
- }
10393
- : {}),
10394
- ...(payload.data.sheetWidth !== undefined
10395
- ? {
10396
- sheetWidth: payload.data.sheetWidth,
10397
- }
10398
- : {}),
10399
- ...(payload.data.sheetHeight !== undefined
10400
- ? {
10401
- sheetHeight: payload.data.sheetHeight,
10402
- }
10403
- : {}),
10404
- ...(payload.data.frameCount !== undefined
10405
- ? {
10406
- frameCount: payload.data.frameCount,
10407
- }
10408
- : {}),
10409
- ...(payload.data.width !== undefined
10410
- ? {
10411
- width: payload.data.width,
10412
- }
10413
- : {}),
10414
- ...(payload.data.height !== undefined
10415
- ? {
10416
- height: payload.data.height,
10417
- }
10418
- : {}),
10419
- jsonData: structuredClone(payload.data.jsonData),
10420
- animations: structuredClone(payload.data.animations),
10421
- }
10422
- : {}),
10423
- }),
11249
+ createItem: ({ payload }) => {
11250
+ const item = {
11251
+ id: payload.spritesheetId,
11252
+ type: payload.data.type,
11253
+ name: payload.data.name,
11254
+ };
11255
+
11256
+ if (payload.data.description !== undefined) {
11257
+ item.description = payload.data.description;
11258
+ }
11259
+
11260
+ if (payload.data.type !== "spritesheet") {
11261
+ return item;
11262
+ }
11263
+
11264
+ item.fileId = payload.data.fileId;
11265
+
11266
+ if (payload.data.thumbnailFileId !== undefined) {
11267
+ item.thumbnailFileId = payload.data.thumbnailFileId;
11268
+ }
11269
+
11270
+ if (payload.data.sheetWidth !== undefined) {
11271
+ item.sheetWidth = payload.data.sheetWidth;
11272
+ }
11273
+
11274
+ if (payload.data.sheetHeight !== undefined) {
11275
+ item.sheetHeight = payload.data.sheetHeight;
11276
+ }
11277
+
11278
+ if (payload.data.frameCount !== undefined) {
11279
+ item.frameCount = payload.data.frameCount;
11280
+ }
11281
+
11282
+ if (payload.data.width !== undefined) {
11283
+ item.width = payload.data.width;
11284
+ }
11285
+
11286
+ if (payload.data.height !== undefined) {
11287
+ item.height = payload.data.height;
11288
+ }
11289
+
11290
+ assignOptionalTagIds({
11291
+ target: item,
11292
+ tagIds: payload.data.tagIds,
11293
+ });
11294
+
11295
+ item.jsonData = structuredClone(payload.data.jsonData);
11296
+ item.animations = structuredClone(payload.data.animations);
11297
+
11298
+ return item;
11299
+ },
11300
+ updateItem: ({ currentItem, payload }) =>
11301
+ applyTagIdsUpdate({
11302
+ currentItem,
11303
+ data: payload.data,
11304
+ }),
10424
11305
  validateCreateState: ({ state, payload }) => {
10425
11306
  if (payload.data.type !== "spritesheet") {
10426
11307
  return;
10427
11308
  }
10428
11309
 
10429
- return validateReferencedFilesInData({
11310
+ const fileResult = validateReferencedFilesInData({
10430
11311
  state,
10431
11312
  data: payload.data,
10432
11313
  fields: ["fileId", "thumbnailFileId"],
@@ -10434,6 +11315,19 @@ const COMMAND_DEFINITIONS = [
10434
11315
  spritesheetId: payload.spritesheetId,
10435
11316
  },
10436
11317
  });
11318
+ if (!fileResult.valid) {
11319
+ return fileResult;
11320
+ }
11321
+
11322
+ return validateTagIdsAgainstScope({
11323
+ state,
11324
+ tagIds: payload.data.tagIds,
11325
+ scopeKey: "spritesheets",
11326
+ path: "payload.data.tagIds",
11327
+ details: {
11328
+ spritesheetId: payload.spritesheetId,
11329
+ },
11330
+ });
10437
11331
  },
10438
11332
  validateUpdateState: ({ state, payload, currentItem }) => {
10439
11333
  if (
@@ -10448,7 +11342,7 @@ const COMMAND_DEFINITIONS = [
10448
11342
  }
10449
11343
 
10450
11344
  if (currentItem.type === "spritesheet") {
10451
- return validateReferencedFilesInData({
11345
+ const fileResult = validateReferencedFilesInData({
10452
11346
  state,
10453
11347
  data: payload.data,
10454
11348
  fields: ["fileId", "thumbnailFileId"],
@@ -10456,6 +11350,19 @@ const COMMAND_DEFINITIONS = [
10456
11350
  spritesheetId: payload.spritesheetId,
10457
11351
  },
10458
11352
  });
11353
+ if (!fileResult.valid) {
11354
+ return fileResult;
11355
+ }
11356
+
11357
+ return validateTagIdsAgainstScope({
11358
+ state,
11359
+ tagIds: payload.data.tagIds,
11360
+ scopeKey: "spritesheets",
11361
+ path: "payload.data.tagIds",
11362
+ details: {
11363
+ spritesheetId: payload.spritesheetId,
11364
+ },
11365
+ });
10459
11366
  }
10460
11367
  },
10461
11368
  }),
@@ -13140,6 +14047,16 @@ const COMMAND_DEFINITIONS = [
13140
14047
  if (!result.valid) {
13141
14048
  return result;
13142
14049
  }
14050
+
14051
+ return validateTagIdsAgainstScope({
14052
+ state,
14053
+ tagIds: payload.data.tagIds,
14054
+ scopeKey: "animations",
14055
+ path: "payload.data.tagIds",
14056
+ details: {
14057
+ animationId: payload.animationId,
14058
+ },
14059
+ });
13143
14060
  }
13144
14061
  },
13145
14062
  reduce: ({ state, payload }) => {
@@ -13154,6 +14071,10 @@ const COMMAND_DEFINITIONS = [
13154
14071
  }
13155
14072
 
13156
14073
  if (payload.data.type === "animation") {
14074
+ assignOptionalTagIds({
14075
+ target: nextAnimation,
14076
+ tagIds: payload.data.tagIds,
14077
+ });
13157
14078
  nextAnimation.animation = structuredClone(payload.data.animation);
13158
14079
  }
13159
14080
 
@@ -13213,32 +14134,45 @@ const COMMAND_DEFINITIONS = [
13213
14134
 
13214
14135
  if (
13215
14136
  currentAnimation.type === "folder" &&
13216
- payload.data.animation !== undefined
14137
+ Object.keys(payload.data).some(
14138
+ (key) => key !== "name" && key !== "description",
14139
+ )
13217
14140
  ) {
13218
14141
  return invalidPrecondition(
13219
14142
  "folder animation items cannot update animation fields",
13220
14143
  );
13221
14144
  }
13222
14145
 
13223
- if (payload.data.animation !== undefined) {
13224
- const result = validateAnimationMaskImageReferences({
14146
+ if (currentAnimation.type === "animation") {
14147
+ if (payload.data.animation !== undefined) {
14148
+ const result = validateAnimationMaskImageReferences({
14149
+ state,
14150
+ animation: payload.data.animation,
14151
+ path: "payload.data.animation",
14152
+ details: { animationId: payload.animationId },
14153
+ errorFactory: createPreconditionValidationError,
14154
+ });
14155
+ if (!result.valid) {
14156
+ return result;
14157
+ }
14158
+ }
14159
+
14160
+ return validateTagIdsAgainstScope({
13225
14161
  state,
13226
- animation: payload.data.animation,
13227
- path: "payload.data.animation",
14162
+ tagIds: payload.data.tagIds,
14163
+ scopeKey: "animations",
14164
+ path: "payload.data.tagIds",
13228
14165
  details: { animationId: payload.animationId },
13229
14166
  errorFactory: createPreconditionValidationError,
13230
14167
  });
13231
- if (!result.valid) {
13232
- return result;
13233
- }
13234
14168
  }
13235
14169
  },
13236
14170
  reduce: ({ state, payload }) => {
13237
14171
  const currentAnimation = state.animations.items[payload.animationId];
13238
- state.animations.items[payload.animationId] = {
13239
- ...structuredClone(currentAnimation),
13240
- ...structuredClone(payload.data),
13241
- };
14172
+ state.animations.items[payload.animationId] = applyTagIdsUpdate({
14173
+ currentItem: currentAnimation,
14174
+ data: payload.data,
14175
+ });
13242
14176
  return state;
13243
14177
  },
13244
14178
  },
@@ -13532,6 +14466,21 @@ const COMMAND_DEFINITIONS = [
13532
14466
  }
13533
14467
 
13534
14468
  if (payload.data.type === "font") {
14469
+ {
14470
+ const result = validateTagIdsAgainstScope({
14471
+ state,
14472
+ tagIds: payload.data.tagIds,
14473
+ scopeKey: "fonts",
14474
+ path: "payload.data.tagIds",
14475
+ details: {
14476
+ fontId: payload.fontId,
14477
+ },
14478
+ });
14479
+ if (!result.valid) {
14480
+ return result;
14481
+ }
14482
+ }
14483
+
13535
14484
  const result = validateReferencedFilesInData({
13536
14485
  state,
13537
14486
  data: payload.data,
@@ -13557,6 +14506,10 @@ const COMMAND_DEFINITIONS = [
13557
14506
  }
13558
14507
 
13559
14508
  if (payload.data.type === "font") {
14509
+ assignOptionalTagIds({
14510
+ target: nextFont,
14511
+ tagIds: payload.data.tagIds,
14512
+ });
13560
14513
  nextFont.fileId = payload.data.fileId;
13561
14514
  nextFont.fontFamily = payload.data.fontFamily;
13562
14515
  }
@@ -13617,7 +14570,8 @@ const COMMAND_DEFINITIONS = [
13617
14570
 
13618
14571
  if (
13619
14572
  currentFont.type === "folder" &&
13620
- (payload.data.fileId !== undefined ||
14573
+ (payload.data.tagIds !== undefined ||
14574
+ payload.data.fileId !== undefined ||
13621
14575
  payload.data.fontFamily !== undefined)
13622
14576
  ) {
13623
14577
  return invalidPrecondition(
@@ -13626,6 +14580,21 @@ const COMMAND_DEFINITIONS = [
13626
14580
  }
13627
14581
 
13628
14582
  if (currentFont.type === "font") {
14583
+ {
14584
+ const result = validateTagIdsAgainstScope({
14585
+ state,
14586
+ tagIds: payload.data.tagIds,
14587
+ scopeKey: "fonts",
14588
+ path: "payload.data.tagIds",
14589
+ details: {
14590
+ fontId: payload.fontId,
14591
+ },
14592
+ });
14593
+ if (!result.valid) {
14594
+ return result;
14595
+ }
14596
+ }
14597
+
13629
14598
  const result = validateReferencedFilesInData({
13630
14599
  state,
13631
14600
  data: payload.data,
@@ -13641,10 +14610,10 @@ const COMMAND_DEFINITIONS = [
13641
14610
  },
13642
14611
  reduce: ({ state, payload }) => {
13643
14612
  const currentFont = state.fonts.items[payload.fontId];
13644
- state.fonts.items[payload.fontId] = {
13645
- ...structuredClone(currentFont),
13646
- ...structuredClone(payload.data),
13647
- };
14613
+ state.fonts.items[payload.fontId] = applyTagIdsUpdate({
14614
+ currentItem: currentFont,
14615
+ data: payload.data,
14616
+ });
13648
14617
  return state;
13649
14618
  },
13650
14619
  },
@@ -13936,6 +14905,18 @@ const COMMAND_DEFINITIONS = [
13936
14905
  );
13937
14906
  }
13938
14907
  }
14908
+
14909
+ if (payload.data.type === "color") {
14910
+ return validateTagIdsAgainstScope({
14911
+ state,
14912
+ tagIds: payload.data.tagIds,
14913
+ scopeKey: "colors",
14914
+ path: "payload.data.tagIds",
14915
+ details: {
14916
+ colorId: payload.colorId,
14917
+ },
14918
+ });
14919
+ }
13939
14920
  },
13940
14921
  reduce: ({ state, payload }) => {
13941
14922
  const nextColor = {
@@ -13950,6 +14931,10 @@ const COMMAND_DEFINITIONS = [
13950
14931
 
13951
14932
  if (payload.data.type === "color") {
13952
14933
  nextColor.hex = payload.data.hex;
14934
+ assignOptionalTagIds({
14935
+ target: nextColor,
14936
+ tagIds: payload.data.tagIds,
14937
+ });
13953
14938
  }
13954
14939
 
13955
14940
  state.colors.items[payload.colorId] = nextColor;
@@ -14006,18 +14991,33 @@ const COMMAND_DEFINITIONS = [
14006
14991
  );
14007
14992
  }
14008
14993
 
14009
- if (currentColor.type === "folder" && payload.data.hex !== undefined) {
14994
+ if (
14995
+ currentColor.type === "folder" &&
14996
+ (payload.data.tagIds !== undefined || payload.data.hex !== undefined)
14997
+ ) {
14010
14998
  return invalidPrecondition(
14011
14999
  "folder color items cannot update color fields",
14012
15000
  );
14013
15001
  }
15002
+
15003
+ if (currentColor.type === "color") {
15004
+ return validateTagIdsAgainstScope({
15005
+ state,
15006
+ tagIds: payload.data.tagIds,
15007
+ scopeKey: "colors",
15008
+ path: "payload.data.tagIds",
15009
+ details: {
15010
+ colorId: payload.colorId,
15011
+ },
15012
+ });
15013
+ }
14014
15014
  },
14015
15015
  reduce: ({ state, payload }) => {
14016
15016
  const currentColor = state.colors.items[payload.colorId];
14017
- state.colors.items[payload.colorId] = {
14018
- ...structuredClone(currentColor),
14019
- ...structuredClone(payload.data),
14020
- };
15017
+ state.colors.items[payload.colorId] = applyTagIdsUpdate({
15018
+ currentItem: currentColor,
15019
+ data: payload.data,
15020
+ });
14021
15021
  return state;
14022
15022
  },
14023
15023
  },
@@ -14239,6 +15239,10 @@ const COMMAND_DEFINITIONS = [
14239
15239
  item.width = payload.data.width;
14240
15240
  item.height = payload.data.height;
14241
15241
  item.modules = structuredClone(payload.data.modules);
15242
+ assignOptionalTagIds({
15243
+ target: item,
15244
+ tagIds: payload.data.tagIds,
15245
+ });
14242
15246
 
14243
15247
  if (payload.data.seed !== undefined && payload.data.seed !== null) {
14244
15248
  item.seed = payload.data.seed;
@@ -14247,10 +15251,10 @@ const COMMAND_DEFINITIONS = [
14247
15251
  return item;
14248
15252
  },
14249
15253
  updateItem: ({ currentItem, payload }) => {
14250
- const nextItem = {
14251
- ...structuredClone(currentItem),
14252
- ...structuredClone(payload.data),
14253
- };
15254
+ const nextItem = applyTagIdsUpdate({
15255
+ currentItem,
15256
+ data: payload.data,
15257
+ });
14254
15258
 
14255
15259
  if (payload.data.seed === null) {
14256
15260
  delete nextItem.seed;
@@ -14269,6 +15273,33 @@ const COMMAND_DEFINITIONS = [
14269
15273
  "folder particle items cannot update particle fields",
14270
15274
  );
14271
15275
  }
15276
+
15277
+ if (currentItem.type === "particle") {
15278
+ return validateTagIdsAgainstScope({
15279
+ state,
15280
+ tagIds: payload.data.tagIds,
15281
+ scopeKey: "particles",
15282
+ path: "payload.data.tagIds",
15283
+ details: {
15284
+ particleId: payload.particleId,
15285
+ },
15286
+ });
15287
+ }
15288
+ },
15289
+ validateCreateState: ({ state, payload }) => {
15290
+ if (payload.data.type !== "particle") {
15291
+ return;
15292
+ }
15293
+
15294
+ return validateTagIdsAgainstScope({
15295
+ state,
15296
+ tagIds: payload.data.tagIds,
15297
+ scopeKey: "particles",
15298
+ path: "payload.data.tagIds",
15299
+ details: {
15300
+ particleId: payload.particleId,
15301
+ },
15302
+ });
14272
15303
  },
14273
15304
  }),
14274
15305
  ...createFolderedCollectionCommandDefinitions({
@@ -14360,23 +15391,37 @@ const COMMAND_DEFINITIONS = [
14360
15391
  itemLabel: "variable item",
14361
15392
  createDataValidator: validateVariableCreateData,
14362
15393
  updateDataValidator: validateVariableUpdateData,
14363
- createItem: ({ payload }) => ({
14364
- id: payload.variableId,
14365
- type: payload.data.type,
14366
- name: payload.data.name,
14367
- ...(payload.data.description !== undefined
14368
- ? {
14369
- description: payload.data.description,
14370
- }
14371
- : {}),
14372
- ...(payload.data.type === "folder"
14373
- ? {}
14374
- : {
14375
- scope: payload.data.scope,
14376
- default: payload.data.default,
14377
- value: payload.data.value,
14378
- }),
14379
- }),
15394
+ createItem: ({ payload }) => {
15395
+ const data = structuredClone(payload.data);
15396
+ if (!Array.isArray(data.tagIds) || data.tagIds.length === 0) {
15397
+ delete data.tagIds;
15398
+ }
15399
+
15400
+ return {
15401
+ id: payload.variableId,
15402
+ ...data,
15403
+ };
15404
+ },
15405
+ updateItem: ({ currentItem, payload }) =>
15406
+ applyTagIdsUpdate({
15407
+ currentItem,
15408
+ data: payload.data,
15409
+ }),
15410
+ validateCreateState: ({ state, payload }) => {
15411
+ if (payload.data.type === "folder") {
15412
+ return;
15413
+ }
15414
+
15415
+ return validateTagIdsAgainstScope({
15416
+ state,
15417
+ tagIds: payload.data.tagIds,
15418
+ scopeKey: "variables",
15419
+ path: "payload.data.tagIds",
15420
+ details: {
15421
+ variableId: payload.variableId,
15422
+ },
15423
+ });
15424
+ },
14380
15425
  validateUpdateState: ({ state, payload, currentItem }) => {
14381
15426
  if (
14382
15427
  currentItem.type === "folder" &&
@@ -14390,6 +15435,21 @@ const COMMAND_DEFINITIONS = [
14390
15435
  }
14391
15436
 
14392
15437
  if (currentItem.type !== "folder") {
15438
+ {
15439
+ const result = validateTagIdsAgainstScope({
15440
+ state,
15441
+ tagIds: payload.data.tagIds,
15442
+ scopeKey: "variables",
15443
+ path: "payload.data.tagIds",
15444
+ details: {
15445
+ variableId: payload.variableId,
15446
+ },
15447
+ });
15448
+ if (!result.valid) {
15449
+ return result;
15450
+ }
15451
+ }
15452
+
14393
15453
  if (payload.data.default !== undefined) {
14394
15454
  {
14395
15455
  const result = validateVariableTypedValue({
@@ -14427,16 +15487,43 @@ const COMMAND_DEFINITIONS = [
14427
15487
  itemLabel: "text style item",
14428
15488
  createDataValidator: validateTextStyleCreateData,
14429
15489
  updateDataValidator: validateTextStyleUpdateData,
14430
- createItem: ({ payload }) => ({
14431
- id: payload.textStyleId,
14432
- ...structuredClone(payload.data),
14433
- }),
15490
+ createItem: ({ payload }) => {
15491
+ const data = structuredClone(payload.data);
15492
+ if (!Array.isArray(data.tagIds) || data.tagIds.length === 0) {
15493
+ delete data.tagIds;
15494
+ }
15495
+
15496
+ return {
15497
+ id: payload.textStyleId,
15498
+ ...data,
15499
+ };
15500
+ },
15501
+ updateItem: ({ currentItem, payload }) =>
15502
+ applyTagIdsUpdate({
15503
+ currentItem,
15504
+ data: payload.data,
15505
+ }),
14434
15506
  validateCreateState: ({ state, payload }) => {
14435
15507
  const data = payload.data;
14436
15508
  if (data.type !== "textStyle") {
14437
15509
  return;
14438
15510
  }
14439
15511
 
15512
+ {
15513
+ const result = validateTagIdsAgainstScope({
15514
+ state,
15515
+ tagIds: payload.data.tagIds,
15516
+ scopeKey: "textStyles",
15517
+ path: "payload.data.tagIds",
15518
+ details: {
15519
+ textStyleId: payload.textStyleId,
15520
+ },
15521
+ });
15522
+ if (!result.valid) {
15523
+ return result;
15524
+ }
15525
+ }
15526
+
14440
15527
  for (const field of ["fontId", "colorId", "strokeColorId"]) {
14441
15528
  if (data[field] === undefined) {
14442
15529
  continue;
@@ -14463,6 +15550,23 @@ const COMMAND_DEFINITIONS = [
14463
15550
  );
14464
15551
  }
14465
15552
 
15553
+ if (currentItem.type === "textStyle") {
15554
+ {
15555
+ const result = validateTagIdsAgainstScope({
15556
+ state,
15557
+ tagIds: payload.data.tagIds,
15558
+ scopeKey: "textStyles",
15559
+ path: "payload.data.tagIds",
15560
+ details: {
15561
+ textStyleId: payload.textStyleId,
15562
+ },
15563
+ });
15564
+ if (!result.valid) {
15565
+ return result;
15566
+ }
15567
+ }
15568
+ }
15569
+
14466
15570
  for (const field of ["fontId", "colorId", "strokeColorId"]) {
14467
15571
  if (payload.data[field] === undefined) {
14468
15572
  continue;
@@ -14512,6 +15616,10 @@ const COMMAND_DEFINITIONS = [
14512
15616
  target: item,
14513
15617
  tagIds: payload.data.tagIds,
14514
15618
  });
15619
+ assignOptionalCharacterSpriteGroups({
15620
+ target: item,
15621
+ spriteGroups: payload.data.spriteGroups,
15622
+ });
14515
15623
 
14516
15624
  item.sprites =
14517
15625
  payload.data.sprites === undefined
@@ -14521,7 +15629,7 @@ const COMMAND_DEFINITIONS = [
14521
15629
  return item;
14522
15630
  },
14523
15631
  updateItem: ({ currentItem, payload }) =>
14524
- applyTagIdsUpdate({
15632
+ applyCharacterUpdate({
14525
15633
  currentItem,
14526
15634
  data: payload.data,
14527
15635
  }),
@@ -14559,6 +15667,21 @@ const COMMAND_DEFINITIONS = [
14559
15667
  }
14560
15668
  }
14561
15669
 
15670
+ {
15671
+ const result = validateCharacterSpriteGroupsAgainstScope({
15672
+ state,
15673
+ spriteGroups: payload.data.spriteGroups,
15674
+ scopeKey: `${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${payload.characterId}`,
15675
+ path: "payload.data.spriteGroups",
15676
+ details: {
15677
+ characterId: payload.characterId,
15678
+ },
15679
+ });
15680
+ if (!result.valid) {
15681
+ return result;
15682
+ }
15683
+ }
15684
+
14562
15685
  for (const [spriteId, sprite] of Object.entries(
14563
15686
  payload.data.sprites?.items || {},
14564
15687
  )) {
@@ -14634,11 +15757,26 @@ const COMMAND_DEFINITIONS = [
14634
15757
  return result;
14635
15758
  }
14636
15759
 
14637
- return validateTagIdsAgainstScope({
15760
+ {
15761
+ const tagResult = validateTagIdsAgainstScope({
15762
+ state,
15763
+ tagIds: payload.data.tagIds,
15764
+ scopeKey: "characters",
15765
+ path: "payload.data.tagIds",
15766
+ details: {
15767
+ characterId: payload.characterId,
15768
+ },
15769
+ });
15770
+ if (!tagResult.valid) {
15771
+ return tagResult;
15772
+ }
15773
+ }
15774
+
15775
+ return validateCharacterSpriteGroupsAgainstScope({
14638
15776
  state,
14639
- tagIds: payload.data.tagIds,
14640
- scopeKey: "characters",
14641
- path: "payload.data.tagIds",
15777
+ spriteGroups: payload.data.spriteGroups,
15778
+ scopeKey: `${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${payload.characterId}`,
15779
+ path: "payload.data.spriteGroups",
14642
15780
  details: {
14643
15781
  characterId: payload.characterId,
14644
15782
  },
@@ -14651,9 +15789,7 @@ const COMMAND_DEFINITIONS = [
14651
15789
  continue;
14652
15790
  }
14653
15791
 
14654
- delete state.tags[
14655
- `${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${characterId}`
14656
- ];
15792
+ delete state.tags[`${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${characterId}`];
14657
15793
  }
14658
15794
  },
14659
15795
  }),
@@ -14675,6 +15811,12 @@ const COMMAND_DEFINITIONS = [
14675
15811
  : {}),
14676
15812
  ...(payload.data.type === "layout"
14677
15813
  ? {
15814
+ ...(Array.isArray(payload.data.tagIds) &&
15815
+ payload.data.tagIds.length > 0
15816
+ ? {
15817
+ tagIds: structuredClone(payload.data.tagIds),
15818
+ }
15819
+ : {}),
14678
15820
  layoutType: payload.data.layoutType,
14679
15821
  isFragment: payload.data.isFragment,
14680
15822
  ...(payload.data.thumbnailFileId !== undefined
@@ -14691,11 +15833,31 @@ const COMMAND_DEFINITIONS = [
14691
15833
  }
14692
15834
  : {}),
14693
15835
  }),
15836
+ updateItem: ({ currentItem, payload }) =>
15837
+ applyTagIdsUpdate({
15838
+ currentItem,
15839
+ data: payload.data,
15840
+ }),
14694
15841
  validateCreateState: ({ state, payload }) => {
14695
15842
  if (payload.data.type !== "layout") {
14696
15843
  return;
14697
15844
  }
14698
15845
 
15846
+ {
15847
+ const result = validateTagIdsAgainstScope({
15848
+ state,
15849
+ tagIds: payload.data.tagIds,
15850
+ scopeKey: "layouts",
15851
+ path: "payload.data.tagIds",
15852
+ details: {
15853
+ layoutId: payload.layoutId,
15854
+ },
15855
+ });
15856
+ if (!result.valid) {
15857
+ return result;
15858
+ }
15859
+ }
15860
+
14699
15861
  return validateReferencedFilesInData({
14700
15862
  state,
14701
15863
  data: payload.data,
@@ -14718,6 +15880,21 @@ const COMMAND_DEFINITIONS = [
14718
15880
  }
14719
15881
 
14720
15882
  if (currentItem.type === "layout") {
15883
+ {
15884
+ const result = validateTagIdsAgainstScope({
15885
+ state,
15886
+ tagIds: payload.data.tagIds,
15887
+ scopeKey: "layouts",
15888
+ path: "payload.data.tagIds",
15889
+ details: {
15890
+ layoutId: payload.layoutId,
15891
+ },
15892
+ });
15893
+ if (!result.valid) {
15894
+ return result;
15895
+ }
15896
+ }
15897
+
14721
15898
  return validateReferencedFilesInData({
14722
15899
  state,
14723
15900
  data: payload.data,
@@ -14736,41 +15913,42 @@ const COMMAND_DEFINITIONS = [
14736
15913
  itemLabel: "control item",
14737
15914
  createDataValidator: validateControlCreateData,
14738
15915
  updateDataValidator: validateControlUpdateData,
14739
- createItem: ({ payload }) => ({
14740
- id: payload.controlId,
14741
- type: payload.data.type,
14742
- name: payload.data.name,
14743
- ...(payload.data.description !== undefined
14744
- ? {
14745
- description: payload.data.description,
14746
- }
14747
- : {}),
14748
- ...(payload.data.type === "control"
14749
- ? {
14750
- ...(payload.data.thumbnailFileId !== undefined
14751
- ? {
14752
- thumbnailFileId: payload.data.thumbnailFileId,
14753
- }
14754
- : {}),
14755
- ...(payload.data.preview !== undefined
14756
- ? {
14757
- preview: structuredClone(payload.data.preview),
14758
- }
14759
- : {}),
14760
- elements: structuredClone(payload.data.elements),
14761
- ...(payload.data.keyboard !== undefined
14762
- ? {
14763
- keyboard: structuredClone(payload.data.keyboard),
14764
- }
14765
- : {}),
14766
- }
14767
- : {}),
14768
- }),
15916
+ createItem: ({ payload }) => {
15917
+ const data = structuredClone(payload.data);
15918
+ if (!Array.isArray(data.tagIds) || data.tagIds.length === 0) {
15919
+ delete data.tagIds;
15920
+ }
15921
+
15922
+ return {
15923
+ id: payload.controlId,
15924
+ ...data,
15925
+ };
15926
+ },
15927
+ updateItem: ({ currentItem, payload }) =>
15928
+ applyTagIdsUpdate({
15929
+ currentItem,
15930
+ data: payload.data,
15931
+ }),
14769
15932
  validateCreateState: ({ state, payload }) => {
14770
15933
  if (payload.data.type !== "control") {
14771
15934
  return;
14772
15935
  }
14773
15936
 
15937
+ {
15938
+ const result = validateTagIdsAgainstScope({
15939
+ state,
15940
+ tagIds: payload.data.tagIds,
15941
+ scopeKey: "controls",
15942
+ path: "payload.data.tagIds",
15943
+ details: {
15944
+ controlId: payload.controlId,
15945
+ },
15946
+ });
15947
+ if (!result.valid) {
15948
+ return result;
15949
+ }
15950
+ }
15951
+
14774
15952
  return validateReferencedFilesInData({
14775
15953
  state,
14776
15954
  data: payload.data,
@@ -14793,6 +15971,21 @@ const COMMAND_DEFINITIONS = [
14793
15971
  }
14794
15972
 
14795
15973
  if (currentItem.type === "control") {
15974
+ {
15975
+ const result = validateTagIdsAgainstScope({
15976
+ state,
15977
+ tagIds: payload.data.tagIds,
15978
+ scopeKey: "controls",
15979
+ path: "payload.data.tagIds",
15980
+ details: {
15981
+ controlId: payload.controlId,
15982
+ },
15983
+ });
15984
+ if (!result.valid) {
15985
+ return result;
15986
+ }
15987
+ }
15988
+
14796
15989
  return validateReferencedFilesInData({
14797
15990
  state,
14798
15991
  data: payload.data,
@@ -14947,7 +16140,10 @@ const COMMAND_DEFINITIONS = [
14947
16140
  id: payload.spriteId,
14948
16141
  ...structuredClone(payload.data),
14949
16142
  };
14950
- if (payload.data.tagIds !== undefined && payload.data.tagIds.length === 0) {
16143
+ if (
16144
+ payload.data.tagIds !== undefined &&
16145
+ payload.data.tagIds.length === 0
16146
+ ) {
14951
16147
  delete nextSprite.tagIds;
14952
16148
  }
14953
16149