@pixi-ui-editor/runtime-pixi 0.8.0 → 0.9.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.
@@ -0,0 +1,100 @@
1
+ import { resolveItemTableVisibleCount } from "@pixi-ui-editor/schema";
2
+ import { NodeView } from "./NodeView.js";
3
+ /**
4
+ * Узел фиксированной вместимости: держит авторский список узлов (`items`, ссылки по адресу — их
5
+ * разрешает `scene.ts`, тем же приёмом, что цели spine-коннекторов) и показывает только первые
6
+ * `visibleCount` из них — остальные скрыты всегда, а не «на этой странице». Автор раскладывает
7
+ * элементы под максимум, а сколько их реально помещается, задаётся на профиль: в разной ориентации
8
+ * экрана на экран влезает разное число строк.
9
+ *
10
+ * Список именно авторский, а не дети: элемент обычно уже лежит там, где его выравнивает layout-группа.
11
+ *
12
+ * Узел намеренно ничего больше не делает. Он не страницы (`PageGroupNodeView` показывает ровно
13
+ * одного ребёнка и растягивает его на себя), не раскладка (позицию элементы сохраняют свою) и ничего не
14
+ * знает ни про какие данные: чем наполнить видимые элементы и когда, решает игровой код через
15
+ * generic API из `controls.ts` (`getItemTableViewVisibleItems`, `getItemTableViewItem`,
16
+ * `setItemTableViewVisibleCount`, `setItemTableViewItemsVisible`). Связка «переключили страницу →
17
+ * перезаполнили элементы» тоже принадлежит игре: `PaginationNodeView.onPageChange` и эта таблица
18
+ * ничего друг о друге не знают.
19
+ *
20
+ * Видимость складывается с авторской: элемент, скрытый в документе, не станет видимым из-за
21
+ * вместимости, а скрытый вместимостью держится флагом `NodeView.hiddenByOwner` (см. `applyVisibility`).
22
+ */
23
+ export class ItemTableNodeView extends NodeView {
24
+ itemViews = [];
25
+ authoredVisibleCount = 0;
26
+ /** Транзиентное переопределение от игрового кода; `undefined` — авторское значение профиля. */
27
+ runtimeVisibleCount;
28
+ /** Транзиентный «скрыть/показать всё» поверх вместимости; `undefined` — вместимость решает сама. */
29
+ forcedItemsVisible;
30
+ /**
31
+ * Разрешённые views авторского списка `items`, позиционно выровненные с ним (`scene.ts`). Ссылка,
32
+ * которая ни во что не разрешилась, сохраняет свою дырку `undefined`, чтобы остальные элементы не
33
+ * сдвинулись; сама висячая ссылка — ошибка валидации документа, а не забота этого вида.
34
+ */
35
+ setItems(views) {
36
+ // Выбывшие из списка возвращаются под управление документа: иначе узел, убранный из таблицы,
37
+ // остался бы скрытым её прежней вместимостью навсегда.
38
+ const next = new Set(views);
39
+ for (const previous of this.itemViews) {
40
+ if (previous instanceof NodeView && !next.has(previous)) {
41
+ previous.hiddenByOwner = false;
42
+ previous.visible = previous.documentVisible;
43
+ }
44
+ }
45
+ this.itemViews = [...views];
46
+ this.applyVisibility();
47
+ }
48
+ /** Элементы в авторском порядке; нерезолвенные ссылки пропущены. */
49
+ get items() {
50
+ return this.itemViews.filter((item) => item !== undefined);
51
+ }
52
+ get visibleCount() {
53
+ return this.runtimeVisibleCount ?? this.authoredVisibleCount;
54
+ }
55
+ /** Транзиентно меняет вместимость; `undefined` возвращает авторское значение профиля. */
56
+ set visibleCount(value) {
57
+ this.runtimeVisibleCount = value === undefined ? undefined : Math.max(0, Math.round(value));
58
+ this.applyVisibility();
59
+ }
60
+ /** Элементы, показанные прямо сейчас, в авторском порядке. */
61
+ get visibleItems() {
62
+ return this.items.filter((item) => item.visible);
63
+ }
64
+ /** Индекс — позиция в авторском списке, а не среди показанных: адресация не «плавает». */
65
+ itemAt(index) {
66
+ return this.itemViews[index];
67
+ }
68
+ /** Показывает или прячет все элементы разом; `undefined` возвращает управление вместимости. */
69
+ setItemsVisible(visible) {
70
+ this.forcedItemsVisible = visible;
71
+ this.applyVisibility();
72
+ }
73
+ /**
74
+ * Проход по элементам: влезающие включены, остальные выключены. Помимо самого `visible` ставится
75
+ * `hiddenByOwner`, потому что `visible` узла пересчитывается из документа на каждом обновлении —
76
+ * в том числе из колбэка layout-движка, который для элемента внутри layout-группы срабатывает
77
+ * позже прохода таблицы и без флага вернул бы скрытый элемент обратно.
78
+ */
79
+ applyVisibility() {
80
+ const count = this.visibleCount;
81
+ this.itemViews.forEach((item, index) => {
82
+ if (item === undefined)
83
+ return;
84
+ const enabled = this.forcedItemsVisible ?? index < count;
85
+ if (item instanceof NodeView) {
86
+ item.hiddenByOwner = !enabled;
87
+ item.visible = enabled && item.documentVisible;
88
+ return;
89
+ }
90
+ item.visible = enabled;
91
+ });
92
+ }
93
+ syncContent(node, _transform, profile) {
94
+ if (node.type !== "item-table")
95
+ return;
96
+ this.authoredVisibleCount = resolveItemTableVisibleCount(node, profile);
97
+ this.applyVisibility();
98
+ }
99
+ }
100
+ //# sourceMappingURL=ItemTableNodeView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ItemTableNodeView.js","sourceRoot":"","sources":["../../src/views/ItemTableNodeView.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolveItemTableVisibleCount, type LayoutProfileId, type UINode } from \"@pixi-ui-editor/schema\";\r\nimport { Container } from \"pixi.js\";\r\nimport { NodeView } from \"./NodeView.js\";\r\n\r\n/**\r\n * Узел фиксированной вместимости: держит авторский список узлов (`items`, ссылки по адресу — их\r\n * разрешает `scene.ts`, тем же приёмом, что цели spine-коннекторов) и показывает только первые\r\n * `visibleCount` из них — остальные скрыты всегда, а не «на этой странице». Автор раскладывает\r\n * элементы под максимум, а сколько их реально помещается, задаётся на профиль: в разной ориентации\r\n * экрана на экран влезает разное число строк.\r\n *\r\n * Список именно авторский, а не дети: элемент обычно уже лежит там, где его выравнивает layout-группа.\r\n *\r\n * Узел намеренно ничего больше не делает. Он не страницы (`PageGroupNodeView` показывает ровно\r\n * одного ребёнка и растягивает его на себя), не раскладка (позицию элементы сохраняют свою) и ничего не\r\n * знает ни про какие данные: чем наполнить видимые элементы и когда, решает игровой код через\r\n * generic API из `controls.ts` (`getItemTableViewVisibleItems`, `getItemTableViewItem`,\r\n * `setItemTableViewVisibleCount`, `setItemTableViewItemsVisible`). Связка «переключили страницу →\r\n * перезаполнили элементы» тоже принадлежит игре: `PaginationNodeView.onPageChange` и эта таблица\r\n * ничего друг о друге не знают.\r\n *\r\n * Видимость складывается с авторской: элемент, скрытый в документе, не станет видимым из-за\r\n * вместимости, а скрытый вместимостью держится флагом `NodeView.hiddenByOwner` (см. `applyVisibility`).\r\n */\r\nexport class ItemTableNodeView extends NodeView {\r\n private itemViews: (Container | undefined)[] = [];\r\n private authoredVisibleCount = 0;\r\n /** Транзиентное переопределение от игрового кода; `undefined` — авторское значение профиля. */\r\n private runtimeVisibleCount?: number;\r\n /** Транзиентный «скрыть/показать всё» поверх вместимости; `undefined` — вместимость решает сама. */\r\n private forcedItemsVisible?: boolean;\r\n\r\n /**\r\n * Разрешённые views авторского списка `items`, позиционно выровненные с ним (`scene.ts`). Ссылка,\r\n * которая ни во что не разрешилась, сохраняет свою дырку `undefined`, чтобы остальные элементы не\r\n * сдвинулись; сама висячая ссылка — ошибка валидации документа, а не забота этого вида.\r\n */\r\n setItems(views: readonly (Container | undefined)[]): void {\r\n // Выбывшие из списка возвращаются под управление документа: иначе узел, убранный из таблицы,\r\n // остался бы скрытым её прежней вместимостью навсегда.\r\n const next = new Set(views);\r\n for (const previous of this.itemViews) {\r\n if (previous instanceof NodeView && !next.has(previous)) { previous.hiddenByOwner = false; previous.visible = previous.documentVisible; }\r\n }\r\n this.itemViews = [...views];\r\n this.applyVisibility();\r\n }\r\n\r\n /** Элементы в авторском порядке; нерезолвенные ссылки пропущены. */\r\n get items(): Container[] {\r\n return this.itemViews.filter((item): item is Container => item !== undefined);\r\n }\r\n\r\n get visibleCount(): number {\r\n return this.runtimeVisibleCount ?? this.authoredVisibleCount;\r\n }\r\n\r\n /** Транзиентно меняет вместимость; `undefined` возвращает авторское значение профиля. */\r\n set visibleCount(value: number | undefined) {\r\n this.runtimeVisibleCount = value === undefined ? undefined : Math.max(0, Math.round(value));\r\n this.applyVisibility();\r\n }\r\n\r\n /** Элементы, показанные прямо сейчас, в авторском порядке. */\r\n get visibleItems(): Container[] {\r\n return this.items.filter((item) => item.visible);\r\n }\r\n\r\n /** Индекс — позиция в авторском списке, а не среди показанных: адресация не «плавает». */\r\n itemAt(index: number): Container | undefined {\r\n return this.itemViews[index];\r\n }\r\n\r\n /** Показывает или прячет все элементы разом; `undefined` возвращает управление вместимости. */\r\n setItemsVisible(visible: boolean | undefined): void {\r\n this.forcedItemsVisible = visible;\r\n this.applyVisibility();\r\n }\r\n\r\n /**\r\n * Проход по элементам: влезающие включены, остальные выключены. Помимо самого `visible` ставится\r\n * `hiddenByOwner`, потому что `visible` узла пересчитывается из документа на каждом обновлении —\r\n * в том числе из колбэка layout-движка, который для элемента внутри layout-группы срабатывает\r\n * позже прохода таблицы и без флага вернул бы скрытый элемент обратно.\r\n */\r\n private applyVisibility(): void {\r\n const count = this.visibleCount;\r\n this.itemViews.forEach((item, index) => {\r\n if (item === undefined) return;\r\n const enabled = this.forcedItemsVisible ?? index < count;\r\n if (item instanceof NodeView) {\r\n item.hiddenByOwner = !enabled;\r\n item.visible = enabled && item.documentVisible;\r\n return;\r\n }\r\n item.visible = enabled;\r\n });\r\n }\r\n\r\n protected syncContent(node: UINode, _transform: UINode[\"transform\"], profile: LayoutProfileId): void {\r\n if (node.type !== \"item-table\") return;\r\n this.authoredVisibleCount = resolveItemTableVisibleCount(node, profile);\r\n this.applyVisibility();\r\n }\r\n}\r\n"]}
@@ -22,6 +22,18 @@ export declare abstract class NodeView extends Container {
22
22
  /** Authored/computed RectTransform-like rectangle; unlike Pixi bounds it also exists without content. */
23
23
  protected layoutWidth: number;
24
24
  protected layoutHeight: number;
25
+ /**
26
+ * Скрыт ли узел владельцем-вместимостью (`ItemTableNodeView`) поверх авторской видимости.
27
+ *
28
+ * Флаг живёт здесь, а не только в самом владельце, потому что `visible` заново выводится из
29
+ * документа на каждом обновлении — в том числе в `updateManaged`, который для элемента внутри
30
+ * layout-группы дёргает сам layout-движок своим колбэком `onLayout` (см. `LayoutItemContainer`),
31
+ * то есть уже после того, как владелец закончил свой проход. Одно присваивание `visible` снаружи
32
+ * пережило бы ровно до следующего пересчёта раскладки; сложение с этим флагом переживает любой.
33
+ */
34
+ hiddenByOwner: boolean;
35
+ /** Visibility last resolved from the document, before `hiddenByOwner` is applied on top. */
36
+ private authoredVisible;
25
37
  constructor(textures?: ReadonlyMap<string, Texture>);
26
38
  protected textureFor(assetId: string | undefined): Texture | undefined;
27
39
  /**
@@ -49,6 +61,8 @@ export declare abstract class NodeView extends Container {
49
61
  /** Applies the rectangle owned by a direct parent's layout solver. */
50
62
  updateManaged(node: UINode, profile: LayoutProfileId, size: LayoutSize): void;
51
63
  get layoutRectangle(): LayoutSize;
64
+ /** Авторская видимость узла отдельно от `visible`, поверх которой применяется `hiddenByOwner`. */
65
+ get documentVisible(): boolean;
52
66
  getSpine(): Spine | undefined;
53
67
  /**
54
68
  * Rasterizes canvas-based content (`TextNodeView`) at a higher pixel density than the renderer's
@@ -1 +1 @@
1
- {"version":3,"file":"NodeView.d.ts","sourceRoot":"","sources":["../../src/views/NodeView.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iCAAiC,CAAC;AACxD,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAE,SAAS,EAAa,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAqD,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAElG;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,SAAS,CAAC;AAE3D;;;;GAIG;AACH,8BAAsB,QAAS,SAAQ,SAAS;IAC9C,SAAS,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC9B,4FAA4F;IAC5F,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACtE,6FAA6F;IAC7F,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmB;IAC5C,yGAAyG;IACzG,SAAS,CAAC,WAAW,SAAK;IAC1B,SAAS,CAAC,YAAY,SAAK;IAE3B,YAAY,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,EAGlD;IAED,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAErE;IAED;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAEvC;IAED,mGAAmG;IACnG,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI,CAI7C;IAED,kGAAkG;IAClG,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAE7G,OAAO,CAAC,sBAAsB;IAuB9B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAG5E;IAED,mGAAmG;IACnG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,EAAE,OAAO,UAAO,EAAE,OAAO,GAAE,eAA2B,GAAG,IAAI,CAEhH;IAED,sEAAsE;IACtE,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAG5E;IAED,IAAI,eAAe,IAAI,UAAU,CAEhC;IAED,QAAQ,IAAI,KAAK,GAAG,SAAS,CAE5B;IAED;;;;;;OAMG;IACH,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAG;CACnD"}
1
+ {"version":3,"file":"NodeView.d.ts","sourceRoot":"","sources":["../../src/views/NodeView.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iCAAiC,CAAC;AACxD,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAE,SAAS,EAAa,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAqD,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAElG;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,SAAS,CAAC;AAE3D;;;;GAIG;AACH,8BAAsB,QAAS,SAAQ,SAAS;IAC9C,SAAS,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC9B,4FAA4F;IAC5F,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACtE,6FAA6F;IAC7F,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmB;IAC5C,yGAAyG;IACzG,SAAS,CAAC,WAAW,SAAK;IAC1B,SAAS,CAAC,YAAY,SAAK;IAC3B;;;;;;;;OAQG;IACH,aAAa,UAAS;IACtB,4FAA4F;IAC5F,OAAO,CAAC,eAAe,CAAQ;IAE/B,YAAY,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,EAGlD;IAED,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAErE;IAED;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAEvC;IAED,mGAAmG;IACnG,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI,CAI7C;IAED,kGAAkG;IAClG,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAE7G,OAAO,CAAC,sBAAsB;IAwB9B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAG5E;IAED,mGAAmG;IACnG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,EAAE,OAAO,UAAO,EAAE,OAAO,GAAE,eAA2B,GAAG,IAAI,CAEhH;IAED,sEAAsE;IACtE,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAG5E;IAED,IAAI,eAAe,IAAI,UAAU,CAEhC;IAED,kGAAkG;IAClG,IAAI,eAAe,IAAI,OAAO,CAE7B;IAED,QAAQ,IAAI,KAAK,GAAG,SAAS,CAE5B;IAED;;;;;;OAMG;IACH,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAG;CACnD"}
@@ -15,6 +15,18 @@ export class NodeView extends Container {
15
15
  /** Authored/computed RectTransform-like rectangle; unlike Pixi bounds it also exists without content. */
16
16
  layoutWidth = 0;
17
17
  layoutHeight = 0;
18
+ /**
19
+ * Скрыт ли узел владельцем-вместимостью (`ItemTableNodeView`) поверх авторской видимости.
20
+ *
21
+ * Флаг живёт здесь, а не только в самом владельце, потому что `visible` заново выводится из
22
+ * документа на каждом обновлении — в том числе в `updateManaged`, который для элемента внутри
23
+ * layout-группы дёргает сам layout-движок своим колбэком `onLayout` (см. `LayoutItemContainer`),
24
+ * то есть уже после того, как владелец закончил свой проход. Одно присваивание `visible` снаружи
25
+ * пережило бы ровно до следующего пересчёта раскладки; сложение с этим флагом переживает любой.
26
+ */
27
+ hiddenByOwner = false;
28
+ /** Visibility last resolved from the document, before `hiddenByOwner` is applied on top. */
29
+ authoredVisible = true;
18
30
  constructor(textures) {
19
31
  super();
20
32
  this.textures = textures;
@@ -59,7 +71,8 @@ export class NodeView extends Container {
59
71
  this.position.set(managedByLayout ? pivotX : transform.x, managedByLayout ? pivotY : transform.y);
60
72
  this.rotation = transform.rotation;
61
73
  this.scale.set(transform.scaleX, transform.scaleY);
62
- this.visible = visible;
74
+ this.authoredVisible = visible;
75
+ this.visible = visible && !this.hiddenByOwner;
63
76
  this.layoutWidth = transform.width;
64
77
  this.layoutHeight = transform.height;
65
78
  // Тот же прямоугольник, что видит пользователь: grab-зона не зависит от содержимого ноды.
@@ -84,6 +97,10 @@ export class NodeView extends Container {
84
97
  get layoutRectangle() {
85
98
  return { width: this.layoutWidth, height: this.layoutHeight };
86
99
  }
100
+ /** Авторская видимость узла отдельно от `visible`, поверх которой применяется `hiddenByOwner`. */
101
+ get documentVisible() {
102
+ return this.authoredVisible;
103
+ }
87
104
  getSpine() {
88
105
  return this.content instanceof Spine ? this.content : undefined;
89
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NodeView.js","sourceRoot":"","sources":["../../src/views/NodeView.ts"],"names":[],"mappings":"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","sourcesContent":["import { Spine } from \"@esotericsoftware/spine-pixi-v8\";\r\nimport { type LayoutProfileId, type UINode } from \"@pixi-ui-editor/schema\";\r\nimport { Container, Rectangle, Texture, type PointData } from \"pixi.js\";\r\nimport { resolveAnchoredTransform, resolveProfileTransform, type LayoutSize } from \"../layout.js\";\r\n\r\n/**\r\n * Whether a built scene is an inert authoring surface or a live one.\r\n * The editor canvas builds `authoring` scenes so controls never swallow selection and drag\r\n * gestures; Preview and the consuming app build `runtime` scenes with real pointer handling.\r\n */\r\nexport type SceneInteractionMode = \"authoring\" | \"runtime\";\r\n\r\n/**\r\n * Base display object for every schema node type. The whole layout contract — profile overrides,\r\n * anchors, pivot, position, scale, visibility — lives here once; subclasses only synchronize their\r\n * type-specific content against the resolved layout rectangle.\r\n */\r\nexport abstract class NodeView extends Container {\r\n protected content?: Container;\r\n /** A mutable scene texture map: assets may be loaded after this stable view was created. */\r\n protected readonly textures: ReadonlyMap<string, Texture> | undefined;\r\n /** Node's own grab rectangle in local space, kept equal to its resolved layout rectangle. */\r\n private readonly grabRect = new Rectangle();\r\n /** Authored/computed RectTransform-like rectangle; unlike Pixi bounds it also exists without content. */\r\n protected layoutWidth = 0;\r\n protected layoutHeight = 0;\r\n\r\n constructor(textures?: ReadonlyMap<string, Texture>) {\r\n super();\r\n this.textures = textures;\r\n }\r\n\r\n protected textureFor(assetId: string | undefined): Texture | undefined {\r\n return assetId === undefined ? undefined : this.textures?.get(assetId);\r\n }\r\n\r\n /**\r\n * Makes every node selectable and draggable by its layout rectangle, whatever it renders.\r\n *\r\n * Lives here rather than in a subclass because a bare `Container` has no `containsPoint` of its\r\n * own: without this, Pixi could only find a node through whatever content the subclass happens to\r\n * draw, so nodes that draw nothing (container) or keep their content out of hit testing (button,\r\n * whose `@pixi/ui` view is inert while authoring) would silently stop being selectable.\r\n *\r\n * Deliberately `containsPoint` and NOT `hitArea`: Pixi's `hitPruneFn` drops a container together\r\n * with its whole subtree once the point falls outside its `hitArea`, which would make any child\r\n * reaching past its parent's rectangle visible but unclickable. `containsPoint` never clips\r\n * children — they are hit-tested first and independently, so a child always stays grabbable.\r\n */\r\n containsPoint(point: PointData): boolean {\r\n return this.grabRect.contains(point.x, point.y);\r\n }\r\n\r\n /** Заменяет содержимое ноды: прежнее снимается со сцены и уничтожается, чтобы не копить сироты. */\r\n protected setContent(content: Container): void {\r\n if (this.content !== undefined) { this.removeChild(this.content); this.content.destroy(); }\r\n this.content = content;\r\n this.addChild(content);\r\n }\r\n\r\n /** Syncs type-specific content to the resolved layout rectangle; layout math itself is shared. */\r\n protected abstract syncContent(node: UINode, transform: UINode[\"transform\"], profile: LayoutProfileId): void;\r\n\r\n private applyResolvedTransform(node: UINode, transform: UINode[\"transform\"], visible: boolean, managedByLayout: boolean, profile: LayoutProfileId): void {\r\n this.syncContent(node, transform, profile);\r\n\r\n const pivotX = (transform.pivotX ?? 0) * transform.width;\r\n const pivotY = (transform.pivotY ?? 0) * transform.height;\r\n this.pivot.set(pivotX, pivotY);\r\n // x/y are the pivot offset from the resolved anchor. Thus a centred anchor and pivot at\r\n // zero offsets place the visual centre exactly at the parent's centre.\r\n // A managed child is positioned by its technical Yoga item. Its own NodeView keeps only the\r\n // pivot offset inside that computed rectangle; authored x/y and anchors do not participate.\r\n this.position.set(managedByLayout ? pivotX : transform.x, managedByLayout ? pivotY : transform.y);\r\n this.rotation = transform.rotation;\r\n this.scale.set(transform.scaleX, transform.scaleY);\r\n this.visible = visible;\r\n this.layoutWidth = transform.width;\r\n this.layoutHeight = transform.height;\r\n // Тот же прямоугольник, что видит пользователь: grab-зона не зависит от содержимого ноды.\r\n this.grabRect.x = 0;\r\n this.grabRect.y = 0;\r\n this.grabRect.width = transform.width;\r\n this.grabRect.height = transform.height;\r\n }\r\n\r\n update(node: UINode, profile: LayoutProfileId, parentSize?: LayoutSize): void {\r\n const resolved = resolveProfileTransform(node, profile);\r\n this.applyResolvedTransform(node, resolveAnchoredTransform(resolved.transform, parentSize), resolved.visible, false, profile);\r\n }\r\n\r\n /** Applies a transient editor rectangle without deriving it from content-dependent Pixi bounds. */\r\n preview(node: UINode, transform: UINode[\"transform\"], visible = true, profile: LayoutProfileId = \"desktop\"): void {\r\n this.applyResolvedTransform(node, transform, visible, false, profile);\r\n }\r\n\r\n /** Applies the rectangle owned by a direct parent's layout solver. */\r\n updateManaged(node: UINode, profile: LayoutProfileId, size: LayoutSize): void {\r\n const resolved = resolveProfileTransform(node, profile);\r\n this.applyResolvedTransform(node, { ...resolved.transform, x: 0, y: 0, width: size.width, height: size.height }, resolved.visible, true, profile);\r\n }\r\n\r\n get layoutRectangle(): LayoutSize {\r\n return { width: this.layoutWidth, height: this.layoutHeight };\r\n }\r\n\r\n getSpine(): Spine | undefined {\r\n return this.content instanceof Spine ? this.content : undefined;\r\n }\r\n\r\n /**\r\n * Rasterizes canvas-based content (`TextNodeView`) at a higher pixel density than the renderer's\r\n * own backing resolution, so an authoring camera zoomed in doesn't magnify an already-rasterized\r\n * bitmap past its source resolution and read as blurred. A no-op base implementation: sprite/\r\n * graphics/bitmap-text content is either GPU-native (always crisp under zoom) or, for bitmap-text,\r\n * has its resolution fixed by the installed `BitmapFont` and can't be changed per instance.\r\n */\r\n setContentResolution(_resolution: number): void {}\r\n}\r\n"]}
1
+ {"version":3,"file":"NodeView.js","sourceRoot":"","sources":["../../src/views/NodeView.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iCAAiC,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,SAAS,EAA2B,MAAM,SAAS,CAAC;AACxE,OAAO,EAAE,wBAAwB,EAAE,uBAAuB,EAAmB,MAAM,cAAc,CAAC;AASlG;;;;GAIG;AACH,MAAM,OAAgB,QAAS,SAAQ,SAAS;IACpC,OAAO,CAAa;IAC9B,4FAA4F;IACzE,QAAQ,CAA2C;IACtE,6FAA6F;IAC5E,QAAQ,GAAG,IAAI,SAAS,EAAE,CAAC;IAC5C,yGAAyG;IAC/F,WAAW,GAAG,CAAC,CAAC;IAChB,YAAY,GAAG,CAAC,CAAC;IAC3B;;;;;;;;OAQG;IACH,aAAa,GAAG,KAAK,CAAC;IACtB,4FAA4F;IACpF,eAAe,GAAG,IAAI,CAAC;IAE/B,YAAY,QAAuC;QACjD,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAES,UAAU,CAAC,OAA2B;QAC9C,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACzE,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,KAAgB;QAC5B,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,mGAAmG;IACzF,UAAU,CAAC,OAAkB;QACrC,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QAAC,CAAC;QAC3F,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC;IAKO,sBAAsB,CAAC,IAAY,EAAE,SAA8B,EAAE,OAAgB,EAAE,eAAwB,EAAE,OAAwB;QAC/I,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QAE3C,MAAM,MAAM,GAAG,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC;QACzD,MAAM,MAAM,GAAG,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC;QAC1D,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,yFAAyF;QACzF,uEAAuE;QACvE,4FAA4F;QAC5F,4FAA4F;QAC5F,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAClG,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC;QACnC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;QACnD,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC;QAC9C,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC;QACnC,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC,MAAM,CAAC;QACrC,0FAA0F;QAC1F,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC;QACpB,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC;QACpB,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;QACtC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC;IAC1C,CAAC;IAED,MAAM,CAAC,IAAY,EAAE,OAAwB,EAAE,UAAuB;QACpE,MAAM,QAAQ,GAAG,uBAAuB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACxD,IAAI,CAAC,sBAAsB,CAAC,IAAI,EAAE,wBAAwB,CAAC,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IAChI,CAAC;IAED,mGAAmG;IACnG,OAAO,CAAC,IAAY,EAAE,SAA8B,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,GAAoB,SAAS;QACxG,IAAI,CAAC,sBAAsB,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACxE,CAAC;IAED,sEAAsE;IACtE,aAAa,CAAC,IAAY,EAAE,OAAwB,EAAE,IAAgB;QACpE,MAAM,QAAQ,GAAG,uBAAuB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACxD,IAAI,CAAC,sBAAsB,CAAC,IAAI,EAAE,EAAE,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACpJ,CAAC;IAED,IAAI,eAAe;QACjB,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC;IAChE,CAAC;IAED,kGAAkG;IAClG,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,eAAe,CAAC;IAC9B,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAClE,CAAC;IAED;;;;;;OAMG;IACH,oBAAoB,CAAC,WAAmB,IAAS,CAAC;CACnD","sourcesContent":["import { Spine } from \"@esotericsoftware/spine-pixi-v8\";\r\nimport { type LayoutProfileId, type UINode } from \"@pixi-ui-editor/schema\";\r\nimport { Container, Rectangle, Texture, type PointData } from \"pixi.js\";\r\nimport { resolveAnchoredTransform, resolveProfileTransform, type LayoutSize } from \"../layout.js\";\r\n\r\n/**\r\n * Whether a built scene is an inert authoring surface or a live one.\r\n * The editor canvas builds `authoring` scenes so controls never swallow selection and drag\r\n * gestures; Preview and the consuming app build `runtime` scenes with real pointer handling.\r\n */\r\nexport type SceneInteractionMode = \"authoring\" | \"runtime\";\r\n\r\n/**\r\n * Base display object for every schema node type. The whole layout contract — profile overrides,\r\n * anchors, pivot, position, scale, visibility — lives here once; subclasses only synchronize their\r\n * type-specific content against the resolved layout rectangle.\r\n */\r\nexport abstract class NodeView extends Container {\r\n protected content?: Container;\r\n /** A mutable scene texture map: assets may be loaded after this stable view was created. */\r\n protected readonly textures: ReadonlyMap<string, Texture> | undefined;\r\n /** Node's own grab rectangle in local space, kept equal to its resolved layout rectangle. */\r\n private readonly grabRect = new Rectangle();\r\n /** Authored/computed RectTransform-like rectangle; unlike Pixi bounds it also exists without content. */\r\n protected layoutWidth = 0;\r\n protected layoutHeight = 0;\r\n /**\r\n * Скрыт ли узел владельцем-вместимостью (`ItemTableNodeView`) поверх авторской видимости.\r\n *\r\n * Флаг живёт здесь, а не только в самом владельце, потому что `visible` заново выводится из\r\n * документа на каждом обновлении — в том числе в `updateManaged`, который для элемента внутри\r\n * layout-группы дёргает сам layout-движок своим колбэком `onLayout` (см. `LayoutItemContainer`),\r\n * то есть уже после того, как владелец закончил свой проход. Одно присваивание `visible` снаружи\r\n * пережило бы ровно до следующего пересчёта раскладки; сложение с этим флагом переживает любой.\r\n */\r\n hiddenByOwner = false;\r\n /** Visibility last resolved from the document, before `hiddenByOwner` is applied on top. */\r\n private authoredVisible = true;\r\n\r\n constructor(textures?: ReadonlyMap<string, Texture>) {\r\n super();\r\n this.textures = textures;\r\n }\r\n\r\n protected textureFor(assetId: string | undefined): Texture | undefined {\r\n return assetId === undefined ? undefined : this.textures?.get(assetId);\r\n }\r\n\r\n /**\r\n * Makes every node selectable and draggable by its layout rectangle, whatever it renders.\r\n *\r\n * Lives here rather than in a subclass because a bare `Container` has no `containsPoint` of its\r\n * own: without this, Pixi could only find a node through whatever content the subclass happens to\r\n * draw, so nodes that draw nothing (container) or keep their content out of hit testing (button,\r\n * whose `@pixi/ui` view is inert while authoring) would silently stop being selectable.\r\n *\r\n * Deliberately `containsPoint` and NOT `hitArea`: Pixi's `hitPruneFn` drops a container together\r\n * with its whole subtree once the point falls outside its `hitArea`, which would make any child\r\n * reaching past its parent's rectangle visible but unclickable. `containsPoint` never clips\r\n * children — they are hit-tested first and independently, so a child always stays grabbable.\r\n */\r\n containsPoint(point: PointData): boolean {\r\n return this.grabRect.contains(point.x, point.y);\r\n }\r\n\r\n /** Заменяет содержимое ноды: прежнее снимается со сцены и уничтожается, чтобы не копить сироты. */\r\n protected setContent(content: Container): void {\r\n if (this.content !== undefined) { this.removeChild(this.content); this.content.destroy(); }\r\n this.content = content;\r\n this.addChild(content);\r\n }\r\n\r\n /** Syncs type-specific content to the resolved layout rectangle; layout math itself is shared. */\r\n protected abstract syncContent(node: UINode, transform: UINode[\"transform\"], profile: LayoutProfileId): void;\r\n\r\n private applyResolvedTransform(node: UINode, transform: UINode[\"transform\"], visible: boolean, managedByLayout: boolean, profile: LayoutProfileId): void {\r\n this.syncContent(node, transform, profile);\r\n\r\n const pivotX = (transform.pivotX ?? 0) * transform.width;\r\n const pivotY = (transform.pivotY ?? 0) * transform.height;\r\n this.pivot.set(pivotX, pivotY);\r\n // x/y are the pivot offset from the resolved anchor. Thus a centred anchor and pivot at\r\n // zero offsets place the visual centre exactly at the parent's centre.\r\n // A managed child is positioned by its technical Yoga item. Its own NodeView keeps only the\r\n // pivot offset inside that computed rectangle; authored x/y and anchors do not participate.\r\n this.position.set(managedByLayout ? pivotX : transform.x, managedByLayout ? pivotY : transform.y);\r\n this.rotation = transform.rotation;\r\n this.scale.set(transform.scaleX, transform.scaleY);\r\n this.authoredVisible = visible;\r\n this.visible = visible && !this.hiddenByOwner;\r\n this.layoutWidth = transform.width;\r\n this.layoutHeight = transform.height;\r\n // Тот же прямоугольник, что видит пользователь: grab-зона не зависит от содержимого ноды.\r\n this.grabRect.x = 0;\r\n this.grabRect.y = 0;\r\n this.grabRect.width = transform.width;\r\n this.grabRect.height = transform.height;\r\n }\r\n\r\n update(node: UINode, profile: LayoutProfileId, parentSize?: LayoutSize): void {\r\n const resolved = resolveProfileTransform(node, profile);\r\n this.applyResolvedTransform(node, resolveAnchoredTransform(resolved.transform, parentSize), resolved.visible, false, profile);\r\n }\r\n\r\n /** Applies a transient editor rectangle without deriving it from content-dependent Pixi bounds. */\r\n preview(node: UINode, transform: UINode[\"transform\"], visible = true, profile: LayoutProfileId = \"desktop\"): void {\r\n this.applyResolvedTransform(node, transform, visible, false, profile);\r\n }\r\n\r\n /** Applies the rectangle owned by a direct parent's layout solver. */\r\n updateManaged(node: UINode, profile: LayoutProfileId, size: LayoutSize): void {\r\n const resolved = resolveProfileTransform(node, profile);\r\n this.applyResolvedTransform(node, { ...resolved.transform, x: 0, y: 0, width: size.width, height: size.height }, resolved.visible, true, profile);\r\n }\r\n\r\n get layoutRectangle(): LayoutSize {\r\n return { width: this.layoutWidth, height: this.layoutHeight };\r\n }\r\n\r\n /** Авторская видимость узла отдельно от `visible`, поверх которой применяется `hiddenByOwner`. */\r\n get documentVisible(): boolean {\r\n return this.authoredVisible;\r\n }\r\n\r\n getSpine(): Spine | undefined {\r\n return this.content instanceof Spine ? this.content : undefined;\r\n }\r\n\r\n /**\r\n * Rasterizes canvas-based content (`TextNodeView`) at a higher pixel density than the renderer's\r\n * own backing resolution, so an authoring camera zoomed in doesn't magnify an already-rasterized\r\n * bitmap past its source resolution and read as blurred. A no-op base implementation: sprite/\r\n * graphics/bitmap-text content is either GPU-native (always crisp under zoom) or, for bitmap-text,\r\n * has its resolution fixed by the installed `BitmapFont` and can't be changed per instance.\r\n */\r\n setContentResolution(_resolution: number): void {}\r\n}\r\n"]}
@@ -3,6 +3,17 @@ import { Texture } from "pixi.js";
3
3
  import { Signal } from "typed-signals";
4
4
  import { NodeView, type SceneInteractionMode } from "./NodeView.js";
5
5
  export type { PaginationNode };
6
+ /**
7
+ * Кнопка-стрелка пагинатора: ровно та часть `ButtonNodeView`/`StagedButtonNodeView`, которая нужна
8
+ * виджету, — нажатие и «доступна ли». Тип объявлен структурно, чтобы этот файл не зависел от обоих
9
+ * классов кнопок и не решал, какой из них «настоящий».
10
+ */
11
+ export type PaginationArrow = {
12
+ onPress: {
13
+ connect(handler: () => void): unknown;
14
+ };
15
+ enabled: boolean;
16
+ };
6
17
  /**
7
18
  * Standalone page indicator: N procedurally generated items arranged by `@pixi/ui`'s `List` (the same
8
19
  * approach `ScrollViewNodeView`/`scrollView.ts` use for arranging children) — this view never
@@ -18,12 +29,22 @@ export type { PaginationNode };
18
29
  * item count/footprint is a runtime fact about this control, not scene topology — it only rebuilds this
19
30
  * view's own item children, never the owning `NodeView` or the wider scene tree. The active pill itself
20
31
  * is not structural: switching pages only recreates the (one) background view that needs to move.
32
+ *
33
+ * With `visiblePageCount` set, the indicator becomes a fixed strip of that many slots with the active
34
+ * page always in the middle one (4 pages, page 3 active → 2-3-4). A slot pointing outside the real
35
+ * page range keeps its footprint and renders nothing, so the active page never shifts sideways.
36
+ *
37
+ * The prev/next arrows are ordinary authored button nodes placed wherever the layout needs them; the
38
+ * widget only takes their views (`scene.ts`'s `wireNodeReferences`, the same address → view resolution
39
+ * Spine connector targets use) and steps the page on press. It never draws an arrow itself.
21
40
  */
22
41
  export declare class PaginationNodeView extends NodeView {
23
42
  private readonly list;
24
43
  private readonly interaction;
25
44
  private readonly fonts;
26
45
  private items;
46
+ private visibleSlots;
47
+ private arrows?;
27
48
  private styleMode;
28
49
  private activeTexture;
29
50
  private inactiveTexture;
@@ -40,15 +61,30 @@ export declare class PaginationNodeView extends NodeView {
40
61
  readonly onPageChange: Signal<(page: number) => void>;
41
62
  constructor(node: PaginationNode, textures: ReadonlyMap<string, Texture> | undefined, interaction: SceneInteractionMode, fonts: ReadonlyMap<string, string> | undefined);
42
63
  private createBackgroundView;
64
+ /** How many indicator slots are drawn: the fixed authored window, or one slot per real page. */
65
+ private get slotCount();
66
+ /** The page a slot points at. Windowed slots slide with the current page, which keeps its middle seat. */
67
+ private pageForSlot;
43
68
  private createItem;
44
- /** Recreates every item for the current `styleMode`/`pageCountState`/`dotWidth`/`dotHeight`. */
69
+ /** Recreates every item for the current `styleMode`/`slotCount`/`dotWidth`/`dotHeight`. */
45
70
  private buildItems;
46
71
  /** Authoring canvas stays inert like every other control; a clickable item is only live at runtime. */
47
72
  private wireItem;
48
73
  private applyCurrentPage;
74
+ /**
75
+ * Подключает авторские кнопки-стрелки: нажатие шагает на страницу назад/вперёд и эмитит
76
+ * `onPageChange`, как клик по самому пункту. Кнопки остаются обычными нодами сцены — виджет только
77
+ * подписывается на них и гасит ту, за которой страниц больше нет.
78
+ */
79
+ setArrows(arrows: {
80
+ previous?: PaginationArrow;
81
+ next?: PaginationArrow;
82
+ }): void;
83
+ /** На краях диапазона соответствующая стрелка недоступна — она никуда не ведёт. */
84
+ private applyArrowState;
49
85
  protected syncContent(node: UINode): void;
50
86
  get pageCount(): number;
51
- /** Structural: rebuilds every item. Used both by document edits and `setPaginationViewPageCount`. */
87
+ /** Used by `setPaginationViewPageCount` once game code knows the real content length. */
52
88
  set pageCount(value: number);
53
89
  get page(): number;
54
90
  /** External sync from game code (or editor preview): highlights an item without emitting `onPageChange`. */
@@ -1 +1 @@
1
- {"version":3,"file":"PaginationNodeView.d.ts","sourceRoot":"","sources":["../../src/views/PaginationNodeView.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,cAAc,EAA4B,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAExH,OAAO,EAA4C,OAAO,EAAyB,MAAM,SAAS,CAAC;AACnG,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEpE,YAAY,EAAE,cAAc,EAAE,CAAC;AAoE/B;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,kBAAmB,SAAQ,QAAQ;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuB;IACnD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0C;IAChE,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,eAAe,CAAU;IACjC,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,iBAAiB,CAAqB;IAC9C,OAAO,CAAC,6BAA6B,CAAsB;IAC3D,OAAO,CAAC,qBAAqB,CAA4B;IACzD,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAU;IAEhC,0GAA0G;IAC1G,QAAQ,CAAC,YAAY,gBAAqB,MAAM,KAAK,IAAI,EAAI;IAE7D,YAAY,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,WAAW,EAAE,oBAAoB,EAAE,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAmBtK;IAED,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,UAAU;IAelB,gGAAgG;IAChG,OAAO,CAAC,UAAU;IAWlB,uGAAuG;IACvG,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,gBAAgB;IAexB,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAyBxC;IAED,IAAI,SAAS,IAAI,MAAM,CAAgC;IAEvD,qGAAqG;IACrG,IAAI,SAAS,CAAC,KAAK,EAAE,MAAM,EAM1B;IAED,IAAI,IAAI,IAAI,MAAM,CAA6B;IAE/C,4GAA4G;IAC5G,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,EAKrB;IAED,iHAAiH;IACjH,OAAO,CAAC,OAAO;CAKhB"}
1
+ {"version":3,"file":"PaginationNodeView.d.ts","sourceRoot":"","sources":["../../src/views/PaginationNodeView.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,cAAc,EAA4B,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAExH,OAAO,EAA4C,OAAO,EAAyB,MAAM,SAAS,CAAC;AACnG,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEpE,YAAY,EAAE,cAAc,EAAE,CAAC;AAuB/B;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG;IAAE,OAAO,EAAE;QAAE,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAA;KAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AA+CvG;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,kBAAmB,SAAQ,QAAQ;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuB;IACnD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0C;IAChE,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,MAAM,CAAC,CAAyD;IACxE,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,eAAe,CAAU;IACjC,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,iBAAiB,CAAqB;IAC9C,OAAO,CAAC,6BAA6B,CAAsB;IAC3D,OAAO,CAAC,qBAAqB,CAA4B;IACzD,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAU;IAEhC,0GAA0G;IAC1G,QAAQ,CAAC,YAAY,gBAAqB,MAAM,KAAK,IAAI,EAAI;IAE7D,YAAY,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,WAAW,EAAE,oBAAoB,EAAE,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAoBtK;IAED,OAAO,CAAC,oBAAoB;IAM5B,gGAAgG;IAChG,OAAO,KAAK,SAAS,GAEpB;IAED,0GAA0G;IAC1G,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,UAAU;IAmBlB,2FAA2F;IAC3F,OAAO,CAAC,UAAU;IAYlB,uGAAuG;IACvG,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,gBAAgB;IAmBxB;;;;OAIG;IACH,SAAS,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,EAAE,eAAe,CAAC;QAAC,IAAI,CAAC,EAAE,eAAe,CAAA;KAAE,GAAG,IAAI,CAK9E;IAED,mFAAmF;IACnF,OAAO,CAAC,eAAe;IAMvB,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CA2BxC;IAED,IAAI,SAAS,IAAI,MAAM,CAAgC;IAEvD,yFAAyF;IACzF,IAAI,SAAS,CAAC,KAAK,EAAE,MAAM,EAQ1B;IAED,IAAI,IAAI,IAAI,MAAM,CAA6B;IAE/C,4GAA4G;IAC5G,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,EAKrB;IAED,iHAAiH;IACjH,OAAO,CAAC,OAAO;CAKhB"}
@@ -81,12 +81,22 @@ class NumberItem extends Container {
81
81
  * item count/footprint is a runtime fact about this control, not scene topology — it only rebuilds this
82
82
  * view's own item children, never the owning `NodeView` or the wider scene tree. The active pill itself
83
83
  * is not structural: switching pages only recreates the (one) background view that needs to move.
84
+ *
85
+ * With `visiblePageCount` set, the indicator becomes a fixed strip of that many slots with the active
86
+ * page always in the middle one (4 pages, page 3 active → 2-3-4). A slot pointing outside the real
87
+ * page range keeps its footprint and renders nothing, so the active page never shifts sideways.
88
+ *
89
+ * The prev/next arrows are ordinary authored button nodes placed wherever the layout needs them; the
90
+ * widget only takes their views (`scene.ts`'s `wireNodeReferences`, the same address → view resolution
91
+ * Spine connector targets use) and steps the page on press. It never draws an arrow itself.
84
92
  */
85
93
  export class PaginationNodeView extends NodeView {
86
94
  list;
87
95
  interaction;
88
96
  fonts;
89
97
  items = [];
98
+ visibleSlots;
99
+ arrows;
90
100
  styleMode;
91
101
  activeTexture;
92
102
  inactiveTexture;
@@ -113,7 +123,8 @@ export class PaginationNodeView extends NodeView {
113
123
  this.numberActiveBackgroundTexture = node.numberActiveBackgroundAssetId === undefined ? undefined : this.textureFor(node.numberActiveBackgroundAssetId);
114
124
  this.numberActiveNineSlice = node.numberActiveNineSlice;
115
125
  this.pageCountState = Math.max(1, Math.round(node.pageCount));
116
- this.currentPage = clampPage(node.defaultPage, this.pageCountState);
126
+ this.visibleSlots = node.visiblePageCount;
127
+ this.currentPage = clampPage(node.defaultPage, this.pageCount);
117
128
  this.dotWidth = node.dotSize.width;
118
129
  this.dotHeight = node.dotSize.height;
119
130
  this.clickableState = node.clickable;
@@ -126,57 +137,97 @@ export class PaginationNodeView extends NodeView {
126
137
  ? new Sprite(texture)
127
138
  : new NineSliceSprite({ texture, leftWidth: this.numberActiveNineSlice.left, topHeight: this.numberActiveNineSlice.top, rightWidth: this.numberActiveNineSlice.right, bottomHeight: this.numberActiveNineSlice.bottom });
128
139
  }
129
- createItem(index) {
140
+ /** How many indicator slots are drawn: the fixed authored window, or one slot per real page. */
141
+ get slotCount() {
142
+ return this.visibleSlots === undefined ? this.pageCount : Math.max(1, Math.round(this.visibleSlots));
143
+ }
144
+ /** The page a slot points at. Windowed slots slide with the current page, which keeps its middle seat. */
145
+ pageForSlot(slot) {
146
+ return this.visibleSlots === undefined ? slot : this.currentPage - Math.floor(this.slotCount / 2) + slot;
147
+ }
148
+ createItem(slot) {
149
+ const page = this.pageForSlot(slot);
150
+ const exists = page >= 0 && page < this.pageCount;
151
+ const isActive = exists && page === this.currentPage;
130
152
  if (this.styleMode === "dots") {
131
- const dot = new Sprite(index === this.currentPage ? this.activeTexture : this.inactiveTexture);
153
+ const dot = new Sprite(isActive ? this.activeTexture : this.inactiveTexture);
132
154
  dot.setSize(this.dotWidth, this.dotHeight);
155
+ // An out-of-range slot renders nothing but keeps its size, so `List` still reserves its place.
156
+ dot.visible = exists;
133
157
  return dot;
134
158
  }
135
- const isActive = index === this.currentPage;
136
159
  const baseFill = this.numberStyle?.fill ?? DEFAULT_NUMBER_STYLE.fill;
137
160
  const fill = isActive ? this.numberActiveColor ?? baseFill : baseFill;
138
- const item = new NumberItem(new Text({ text: String(index + 1), style: numberTextStyle(this.numberStyle, fill, this.fonts) }));
161
+ const item = new NumberItem(new Text({ text: exists ? String(page + 1) : "", style: numberTextStyle(this.numberStyle, fill, this.fonts) }));
139
162
  item.resize(this.dotWidth, this.dotHeight);
140
163
  if (isActive && this.numberActiveBackgroundTexture !== undefined)
141
164
  item.setBackground(this.createBackgroundView(this.numberActiveBackgroundTexture));
142
165
  return item;
143
166
  }
144
- /** Recreates every item for the current `styleMode`/`pageCountState`/`dotWidth`/`dotHeight`. */
167
+ /** Recreates every item for the current `styleMode`/`slotCount`/`dotWidth`/`dotHeight`. */
145
168
  buildItems() {
146
169
  this.list.removeChildren();
147
170
  this.items = [];
148
- for (let index = 0; index < this.pageCountState; index++) {
149
- const item = this.createItem(index);
150
- this.wireItem(item, index);
171
+ for (let slot = 0; slot < this.slotCount; slot++) {
172
+ const item = this.createItem(slot);
173
+ this.wireItem(item, slot);
151
174
  this.items.push(item);
152
175
  this.list.addChild(item);
153
176
  }
177
+ this.applyArrowState();
154
178
  }
155
179
  /** Authoring canvas stays inert like every other control; a clickable item is only live at runtime. */
156
- wireItem(item, index) {
180
+ wireItem(item, slot) {
157
181
  if (this.interaction === "authoring" || !this.clickableState) {
158
182
  item.eventMode = "none";
159
183
  return;
160
184
  }
161
185
  item.eventMode = "static";
162
186
  item.cursor = "pointer";
163
- item.on("pointertap", () => this.setPage(index));
187
+ // Resolved on click, not captured: a windowed slot points at a different page after every move.
188
+ item.on("pointertap", () => {
189
+ const page = this.pageForSlot(slot);
190
+ if (page >= 0 && page < this.pageCount)
191
+ this.setPage(page);
192
+ });
164
193
  }
165
194
  applyCurrentPage() {
166
- if (this.styleMode === "dots") {
167
- this.items.forEach((item, index) => { if (item instanceof Sprite)
168
- item.texture = index === this.currentPage ? this.activeTexture : this.inactiveTexture; });
169
- return;
170
- }
171
195
  const baseFill = this.numberStyle?.fill ?? DEFAULT_NUMBER_STYLE.fill;
172
- this.items.forEach((item, index) => {
173
- if (!(item instanceof NumberItem))
196
+ this.items.forEach((item, slot) => {
197
+ const page = this.pageForSlot(slot);
198
+ const exists = page >= 0 && page < this.pageCount;
199
+ const isActive = exists && page === this.currentPage;
200
+ if (item instanceof NumberItem) {
201
+ item.numberText.text = exists ? String(page + 1) : "";
202
+ item.numberText.style = numberTextStyle(this.numberStyle, isActive ? this.numberActiveColor ?? baseFill : baseFill, this.fonts);
203
+ item.setBackground(isActive && this.numberActiveBackgroundTexture !== undefined ? this.createBackgroundView(this.numberActiveBackgroundTexture) : undefined);
174
204
  return;
175
- const isActive = index === this.currentPage;
176
- const fill = isActive ? this.numberActiveColor ?? baseFill : baseFill;
177
- item.numberText.style = numberTextStyle(this.numberStyle, fill, this.fonts);
178
- item.setBackground(isActive && this.numberActiveBackgroundTexture !== undefined ? this.createBackgroundView(this.numberActiveBackgroundTexture) : undefined);
205
+ }
206
+ item.texture = isActive ? this.activeTexture : this.inactiveTexture;
207
+ item.setSize(this.dotWidth, this.dotHeight);
208
+ item.visible = exists;
179
209
  });
210
+ this.applyArrowState();
211
+ }
212
+ /**
213
+ * Подключает авторские кнопки-стрелки: нажатие шагает на страницу назад/вперёд и эмитит
214
+ * `onPageChange`, как клик по самому пункту. Кнопки остаются обычными нодами сцены — виджет только
215
+ * подписывается на них и гасит ту, за которой страниц больше нет.
216
+ */
217
+ setArrows(arrows) {
218
+ this.arrows = arrows;
219
+ arrows.previous?.onPress.connect(() => this.setPage(this.currentPage - 1));
220
+ arrows.next?.onPress.connect(() => this.setPage(this.currentPage + 1));
221
+ this.applyArrowState();
222
+ }
223
+ /** На краях диапазона соответствующая стрелка недоступна — она никуда не ведёт. */
224
+ applyArrowState() {
225
+ if (this.arrows === undefined)
226
+ return;
227
+ if (this.arrows.previous !== undefined)
228
+ this.arrows.previous.enabled = this.currentPage > 0;
229
+ if (this.arrows.next !== undefined)
230
+ this.arrows.next.enabled = this.currentPage < this.pageCount - 1;
180
231
  }
181
232
  syncContent(node) {
182
233
  if (node.type !== "pagination")
@@ -185,7 +236,10 @@ export class PaginationNodeView extends NodeView {
185
236
  const inactive = node.inactiveAssetId === undefined ? Texture.WHITE : this.textureFor(node.inactiveAssetId) ?? Texture.WHITE;
186
237
  const pageCount = Math.max(1, Math.round(node.pageCount));
187
238
  this.clickableState = node.clickable;
188
- const structureChanged = node.style !== this.styleMode || pageCount !== this.pageCountState || node.dotSize.width !== this.dotWidth || node.dotSize.height !== this.dotHeight;
239
+ const previousSlotCount = this.slotCount;
240
+ this.visibleSlots = node.visiblePageCount;
241
+ this.pageCountState = pageCount;
242
+ const structureChanged = node.style !== this.styleMode || this.slotCount !== previousSlotCount || node.dotSize.width !== this.dotWidth || node.dotSize.height !== this.dotHeight;
189
243
  this.styleMode = node.style;
190
244
  this.activeTexture = active;
191
245
  this.inactiveTexture = inactive;
@@ -193,10 +247,9 @@ export class PaginationNodeView extends NodeView {
193
247
  this.numberActiveColor = node.numberActiveColor;
194
248
  this.numberActiveBackgroundTexture = node.numberActiveBackgroundAssetId === undefined ? undefined : this.textureFor(node.numberActiveBackgroundAssetId);
195
249
  this.numberActiveNineSlice = node.numberActiveNineSlice;
196
- this.pageCountState = pageCount;
197
250
  this.dotWidth = node.dotSize.width;
198
251
  this.dotHeight = node.dotSize.height;
199
- this.currentPage = clampPage(this.currentPage, pageCount);
252
+ this.currentPage = clampPage(this.currentPage, this.pageCount);
200
253
  if (this.list.type !== node.direction)
201
254
  this.list.type = node.direction;
202
255
  if (this.list.elementsMargin !== node.spacing)
@@ -207,14 +260,18 @@ export class PaginationNodeView extends NodeView {
207
260
  this.applyCurrentPage();
208
261
  }
209
262
  get pageCount() { return this.pageCountState; }
210
- /** Structural: rebuilds every item. Used both by document edits and `setPaginationViewPageCount`. */
263
+ /** Used by `setPaginationViewPageCount` once game code knows the real content length. */
211
264
  set pageCount(value) {
212
265
  const next = Math.max(1, Math.round(value));
213
266
  if (next === this.pageCountState)
214
267
  return;
268
+ const previousSlotCount = this.slotCount;
215
269
  this.pageCountState = next;
216
- this.currentPage = clampPage(this.currentPage, next);
217
- this.buildItems();
270
+ this.currentPage = clampPage(this.currentPage, this.pageCount);
271
+ if (this.slotCount === previousSlotCount)
272
+ this.applyCurrentPage();
273
+ else
274
+ this.buildItems();
218
275
  }
219
276
  get page() { return this.currentPage; }
220
277
  /** External sync from game code (or editor preview): highlights an item without emitting `onPageChange`. */