ntk 8.1.1 → 8.3.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,196 @@
1
+ /**
2
+ * Which RENDER picture format describes a drawable's pixels.
3
+ *
4
+ * A picture format is how the server is told to read and write a drawable:
5
+ * where the red, green, blue and alpha bits are and how many of each. Depth
6
+ * does not name one — a depth-16 visual can be 5:6:5 or 5:5:5 with a spare
7
+ * bit, a depth-24 one can be RGB or BGR, and both 8:8:8:8 and 10:10:10:2 are
8
+ * 32 bits wide. The **visual** is what fixes the layout, so that is what a
9
+ * format has to be picked from (issue #295).
10
+ *
11
+ * The mapping the server keeps is in `QueryPictFormats`' screens/depths/
12
+ * visuals section, which node-x11 decodes since 4.0.0
13
+ * ([node-x11#280](https://github.com/sidorares/node-x11/issues/280)) — that
14
+ * is the server's own answer, and it is what is used when the reply carries
15
+ * it.
16
+ *
17
+ * A visual the reply leaves out — one whose depth this server's RENDER does
18
+ * not describe — is reconstructed the way the server would have built it: a
19
+ * visual's `red_mask`/`green_mask`/`blue_mask` from the connection handshake,
20
+ * matched against the shift/mask pairs of the formats list. Both sides come
21
+ * from the same server, so this is a lookup rather than a guess — the only
22
+ * thing it cannot recover is a format for an *indexed* visual, which has no
23
+ * masks to match on and which only the reply can name.
24
+ */
25
+
26
+ /** `type` in a formats-list entry. */
27
+ export const PICT_TYPE = { Indexed: 0, Direct: 1 };
28
+
29
+ /**
30
+ * The fields of a `QueryPictFormats` entry, which node-x11 hands over as a
31
+ * positional array (`x11/lib/ext/render.js`).
32
+ */
33
+ const FIELD = {
34
+ id: 0,
35
+ type: 1,
36
+ depth: 2,
37
+ redShift: 3,
38
+ redMask: 4,
39
+ greenShift: 5,
40
+ greenMask: 6,
41
+ blueShift: 7,
42
+ blueMask: 8,
43
+ alphaShift: 9,
44
+ alphaMask: 10,
45
+ colormap: 11
46
+ };
47
+
48
+ /** A formats-list entry as an object, from node-x11's positional array. */
49
+ export function parsePictFormats(reply) {
50
+ const list = (Array.isArray(reply) ? reply : reply?.formats) ?? [];
51
+ return list.map((f) => {
52
+ if (!Array.isArray(f)) return f;
53
+ const out = {};
54
+ for (const name in FIELD) out[name] = f[FIELD[name]];
55
+ return out;
56
+ });
57
+ }
58
+
59
+ /** Where a channel's bits sit in a pixel, as one mask over the whole word. */
60
+ const channelMask = (shift, mask) => ((mask >>> 0) << shift) >>> 0;
61
+
62
+ /** How many bits a mask covers. */
63
+ function bitCount(mask) {
64
+ let n = 0;
65
+ for (let m = mask >>> 0; m; m >>>= 1) n += m & 1;
66
+ return n;
67
+ }
68
+
69
+ /**
70
+ * The format id for a visual, or `null` where the formats list holds none.
71
+ *
72
+ * The colour masks have to agree exactly; what is left over is the choice
73
+ * between, say, `a8r8g8b8` and `x8r8g8b8`, and the spare bits settle it. A
74
+ * whole byte the channels do not account for is the alpha channel every ARGB
75
+ * visual has; a bit or two — the spare bit of a 5:5:5 in 16, the two of a
76
+ * 10:10:10 in 32 — is padding no client ever wrote, and reading it as alpha
77
+ * would composite a garbage bit as transparency.
78
+ *
79
+ * Depth is matched first and relaxed only if that finds nothing, because the
80
+ * two lists do not always agree on it. A server names the format it created
81
+ * for a visual with that visual's depth, but the fixed formats every RENDER
82
+ * server also publishes carry the *pixel* width instead — `a2r10g10b10` is
83
+ * listed at depth 32, and a 10:10:10 visual is depth 30. Matching masks alone
84
+ * still identifies the layout, which is the part that decides how pixels are
85
+ * read.
86
+ *
87
+ * @param {object} visual a handshake visual (`{ red_mask, green_mask, blue_mask }`)
88
+ * @param {number} depth the depth its `depths` entry is under
89
+ * @param {Array<object>} formats parsed formats list
90
+ * @returns {number|null}
91
+ */
92
+ export function matchVisualFormat(visual, depth, formats) {
93
+ const red = (visual?.red_mask ?? 0) >>> 0;
94
+ const green = (visual?.green_mask ?? 0) >>> 0;
95
+ const blue = (visual?.blue_mask ?? 0) >>> 0;
96
+ // An indexed visual (PseudoColor, GrayScale and the static pair) carries no
97
+ // masks: its format is an Indexed one, tied to a colormap this list cannot
98
+ // be matched against. Nothing here can name it, and saying so is better
99
+ // than naming a Direct format whose channels it does not have.
100
+ if (!(red || green || blue)) return null;
101
+
102
+ const wantAlpha = depth - (bitCount(red) + bitCount(green) + bitCount(blue)) >= 8;
103
+ // near misses, in the order they would be settled for: same depth but the
104
+ // wrong side of the alpha choice, then the two the same way round again
105
+ // with the depth relaxed
106
+ let sameDepthAlt = null;
107
+ let otherDepth = null;
108
+ let otherDepthAlt = null;
109
+
110
+ for (const f of formats) {
111
+ if (f.type !== PICT_TYPE.Direct) continue;
112
+ if (channelMask(f.redShift, f.redMask) !== red) continue;
113
+ if (channelMask(f.greenShift, f.greenMask) !== green) continue;
114
+ if (channelMask(f.blueShift, f.blueMask) !== blue) continue;
115
+ const hasAlpha = (f.alphaMask ?? 0) !== 0;
116
+ if (f.depth === depth) {
117
+ if (hasAlpha === wantAlpha) return f.id;
118
+ sameDepthAlt ??= f.id;
119
+ } else if (hasAlpha === wantAlpha) {
120
+ otherDepth ??= f.id;
121
+ } else {
122
+ otherDepthAlt ??= f.id;
123
+ }
124
+ }
125
+ return sameDepthAlt ?? otherDepth ?? otherDepthAlt;
126
+ }
127
+
128
+ /**
129
+ * visual id -> format id, from the reply's screens section where the server
130
+ * sent one, and matched on colour masks where it did not.
131
+ *
132
+ * Visual ids are unique across screens, so one flat map answers for all of
133
+ * them. The server's own table is the whole answer where it is there — it
134
+ * names indexed visuals too, which masks cannot — and mask matching then
135
+ * fills in only the visuals it left out.
136
+ *
137
+ * @param {object} display node-x11's display object
138
+ * @param {Array<object>} formats parsed formats list
139
+ * @param {object} [reply] the `QueryPictFormats` reply, for its `screens`
140
+ * @returns {Map<number, number>}
141
+ */
142
+ export function visualFormats(display, formats, reply) {
143
+ const byVisual = new Map();
144
+ for (const screen of reply?.screens ?? []) {
145
+ for (const depth of screen?.depths ?? []) {
146
+ for (const { visual, format } of depth?.visuals ?? []) {
147
+ byVisual.set(visual >>> 0, format);
148
+ }
149
+ }
150
+ }
151
+ for (const screen of display?.screen ?? []) {
152
+ for (const depth in screen.depths ?? {}) {
153
+ for (const visual of Object.values(screen.depths[depth])) {
154
+ const vid = visual.vid >>> 0;
155
+ if (byVisual.has(vid)) continue;
156
+ const format = matchVisualFormat(visual, Number(depth), formats);
157
+ if (format != null) byVisual.set(vid, format);
158
+ }
159
+ }
160
+ }
161
+ return byVisual;
162
+ }
163
+
164
+ /**
165
+ * visual id -> the depth its pixels are, straight from the handshake.
166
+ *
167
+ * @param {object} display node-x11's display object
168
+ * @returns {Map<number, number>}
169
+ */
170
+ export function visualDepths(display) {
171
+ const byVisual = new Map();
172
+ for (const screen of display?.screen ?? []) {
173
+ for (const depth in screen.depths ?? {}) {
174
+ for (const visual of Object.values(screen.depths[depth])) {
175
+ byVisual.set(visual.vid >>> 0, Number(depth));
176
+ }
177
+ }
178
+ }
179
+ return byVisual;
180
+ }
181
+
182
+ /**
183
+ * The format a drawable of this depth is read through when its visual is not
184
+ * available — a pixmap, which has no visual at all, or a window whose visual
185
+ * has not been asked for yet.
186
+ *
187
+ * The standard formats node-x11 picks out of the list by their masks. Right
188
+ * for the layouts an X client meets most of the time (8:8:8 colour, 8-bit
189
+ * coverage) and wrong for everything else, which is why it is the fallback
190
+ * and not the answer.
191
+ */
192
+ export function formatForDepth(Render, depth) {
193
+ if (depth === 32) return Render.rgba32;
194
+ if (depth === 8) return Render.a8;
195
+ return Render.rgb24;
196
+ }
package/lib/pixmap.js CHANGED
@@ -1,15 +1,46 @@
1
1
  import { safeRelease } from './cleanup.js';
2
2
  import Drawable from './drawable.js';
3
+ import { extensionEventNames } from './events_map.js';
3
4
 
4
5
  // GC fallback: free the server-side pixmap when the wrapper is collected
5
6
  // without an explicit destroy()
6
- const registry = new FinalizationRegistry(({ X, id }) => {
7
+ const registry = new FinalizationRegistry(({ X, id, releaseId }) => {
7
8
  safeRelease(X, () => {
8
9
  X.FreePixmap(id);
9
- X.ReleaseID(id);
10
+ if (releaseId) X.ReleaseID(id);
10
11
  });
11
12
  });
12
13
 
14
+ /**
15
+ * A GetGeometry reply in ntk's spelling (window.js has the window-shaped
16
+ * twin). For a pixmap, `x`, `y` and `borderWidth` are always 0 — X reports
17
+ * them anyway, and returning the same shape as `wnd.getGeometry()` keeps the
18
+ * two interchangeable.
19
+ */
20
+ function unpackGeometry(res) {
21
+ return {
22
+ x: res.xPos,
23
+ y: res.yPos,
24
+ width: res.width,
25
+ height: res.height,
26
+ depth: res.depth,
27
+ borderWidth: res.borderWidth,
28
+ root: res.windowid
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Whether this connection's allocator handed out `id`. Owning an adopted
34
+ * pixmap does not always mean the id was ours: XCompositeNameWindowPixmap
35
+ * names a pixmap into an id the adopter allocated itself, but a pixmap made
36
+ * by another client carries an id from that client's range. Only an id of
37
+ * ours may go back into the pool on destroy — releasing a foreign one would
38
+ * eventually make AllocID hand out an id the server refuses as not ours.
39
+ */
40
+ function ownAllocation(display, id) {
41
+ return ((id & ~display.resource_mask) >>> 0) === (display.resource_base >>> 0);
42
+ }
43
+
13
44
  export default class Pixmap extends Drawable {
14
45
  constructor(app, args = {}) {
15
46
  super();
@@ -18,29 +49,157 @@ export default class Pixmap extends Drawable {
18
49
  this.X = X;
19
50
  this.display = app.display;
20
51
 
21
- const parentId = args.parent ? args.parent.id : app.display.screen[0].root;
22
- this.depth = args.depth || 24;
52
+ // A pixmap has no visual of its own — X gives it a depth and nothing
53
+ // else. What its pixels mean is decided by whoever put them there, so a
54
+ // pixmap holding a window's content (a backing store, a compositor's
55
+ // NameWindowPixmap) is told which visual that was: it is what names the
56
+ // picture format the pixels can be read through (issue #295). Left 0,
57
+ // the format is picked from the depth as before.
58
+ this.visualId = args.visual ?? 0;
59
+
60
+ // `pixmap.ready` (see the getter): resolved as soon as this wrapper
61
+ // knows its geometry — at the end of the constructor for a pixmap ntk
62
+ // created or one adopted with its geometry declared, when GetGeometry
63
+ // replies for one adopted by bare id
64
+ this._readyPromiseResolve = null;
65
+ this._readyPromise = new Promise((resolve) => {
66
+ this._readyPromiseResolve = resolve;
67
+ });
68
+ this._adoptError = null;
23
69
 
24
70
  if (!args.id) {
71
+ const parentId = args.parent ? args.parent.id : app.display.screen[0].root;
72
+ this.depth = args.depth || 24;
25
73
  this.id = X.AllocID();
26
74
  X.CreatePixmap(this.id, parentId, this.depth, args.width, args.height);
27
75
  this.width = args.width;
28
76
  this.height = args.height;
29
77
  this._owned = true;
30
- registry.register(this, { X, id: this.id }, this);
78
+ this._releaseId = true;
79
+ registry.register(this, { X, id: this.id, releaseId: true }, this);
80
+ this._readyPromiseResolve(this);
31
81
  } else {
32
82
  this.id = args.id;
33
- this._owned = false;
83
+ // Nothing is defaulted for an adopted pixmap: what the caller declared
84
+ // is recorded, and what they did not is asked of the server rather
85
+ // than guessed — a depth invented here would pick the picture format
86
+ // everything drawing on it reads the pixels through (issue #291).
87
+ this.width = args.width;
88
+ this.height = args.height;
89
+ this.depth = args.depth;
90
+ this._owned = !!args.own;
91
+ this._releaseId = this._owned && ownAllocation(this.display, this.id);
92
+ if (this._owned) {
93
+ registry.register(this, { X, id: this.id, releaseId: this._releaseId }, this);
94
+ }
95
+ if (this.width !== undefined && this.height !== undefined && this.depth !== undefined) {
96
+ this._readyPromiseResolve(this);
97
+ } else {
98
+ X.GetGeometry(this.id, (err, res) => {
99
+ // A pixmap freed between the id reaching us and this request
100
+ // reaching the server answers BadDrawable, and there is nothing to
101
+ // record. `ready` still resolves — a wait that never ends is worse
102
+ // than one that ends with `width` undefined — and `Pixmap.adopt`
103
+ // is the form that turns this into a rejection.
104
+ if (err) this._adoptError = err;
105
+ else this._applyGeometry(unpackGeometry(res));
106
+ this._readyPromiseResolve(this);
107
+ });
108
+ }
109
+ }
110
+
111
+ // Extension events name their drawable, and a DAMAGE object can watch a
112
+ // pixmap, so routing (see App#_routeExtensionEvents) looks the target up
113
+ // in `X.event_consumers` — where windows already live. A pixmap enrols
114
+ // only when someone listens, because the entry is a strong reference:
115
+ // enrolling every pixmap would pin them all past the GC fallback above.
116
+ // A listening pixmap therefore stays until destroy(), which is also what
117
+ // removes it from the table.
118
+ this.on('newListener', (name) => {
119
+ if (extensionEventNames.has(name) && X.event_consumers) {
120
+ X.event_consumers[this.id] = this;
121
+ }
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Adopt an existing pixmap id: ask the server for its geometry and depth,
127
+ * and take responsibility for freeing it. This is the compositor's case —
128
+ * XCompositeNameWindowPixmap hands back a pixmap the adopting client must
129
+ * FreePixmap, and must re-adopt on every resize of the named window — but
130
+ * any handover of a pixmap id works the same way.
131
+ *
132
+ * Rejects if the pixmap does not exist (already freed — for a compositor,
133
+ * re-name the window and adopt the fresh id). Pass `own: false` to observe
134
+ * a pixmap that stays another client's to free, `visual` to name the
135
+ * visual its pixels are laid out in (see the constructor), and any of
136
+ * `width`/`height`/`depth` already known — with all three declared no
137
+ * round trip is made.
138
+ */
139
+ static async adopt(app, id, { own = true, ...args } = {}) {
140
+ const pixmap = new Pixmap(app, { ...args, id, own });
141
+ await pixmap.ready;
142
+ if (pixmap._adoptError) {
143
+ // there is nothing server-side to own, so the GC fallback must not
144
+ // send a FreePixmap of its own to fail the same way
145
+ pixmap._owned = false;
146
+ registry.unregister(pixmap);
147
+ throw new Error(
148
+ `Pixmap.adopt: pixmap 0x${id.toString(16)} does not exist — it was freed ` +
149
+ 'between its id being obtained and this request. A compositor sees this ' +
150
+ 'when the named window was resized or destroyed: name it again and adopt ' +
151
+ 'the fresh id.',
152
+ { cause: pixmap._adoptError }
153
+ );
34
154
  }
155
+ return pixmap;
156
+ }
157
+
158
+ /**
159
+ * Resolves with this pixmap once its geometry and depth are known —
160
+ * immediately for a pixmap ntk created or one adopted with `width`,
161
+ * `height` and `depth` all declared, when the GetGeometry sent by the
162
+ * constructor replies for one adopted by bare id. Never rejects: a pixmap
163
+ * that was already gone resolves with `width` still `undefined`.
164
+ */
165
+ get ready() {
166
+ return this._readyPromise;
167
+ }
168
+
169
+ /**
170
+ * Ask the server about this pixmap now — `{ x, y, width, height, depth,
171
+ * borderWidth, root }`, the same shape `wnd.getGeometry()` resolves with
172
+ * (`x`, `y` and `borderWidth` are 0 for a pixmap). The reply is written
173
+ * back to `width`/`height`/`depth`, and resolves `ready` if it was still
174
+ * pending.
175
+ */
176
+ getGeometry() {
177
+ return new Promise((resolve, reject) => {
178
+ this.X.GetGeometry(this.id, (err, res) => {
179
+ if (err) return reject(err);
180
+ const geometry = unpackGeometry(res);
181
+ this._applyGeometry(geometry);
182
+ resolve(geometry);
183
+ });
184
+ });
185
+ }
186
+
187
+ /** Record a GetGeometry reply, and let anything waiting on it go. */
188
+ _applyGeometry({ width, height, depth }) {
189
+ this.width = width;
190
+ this.height = height;
191
+ this.depth = depth;
192
+ this._readyPromiseResolve(this);
35
193
  }
36
194
 
37
195
  destroy() {
196
+ if (this.X.event_consumers?.[this.id] === this) delete this.X.event_consumers[this.id];
38
197
  if (!this._owned) return;
39
198
  this._owned = false;
40
199
  registry.unregister(this);
41
200
  safeRelease(this.X, () => {
42
201
  this.X.FreePixmap(this.id);
43
- this.X.ReleaseID(this.id);
202
+ if (this._releaseId) this.X.ReleaseID(this.id);
44
203
  });
45
204
  }
46
205
 
package/lib/region.js ADDED
@@ -0,0 +1,148 @@
1
+ import { safeRelease } from './cleanup.js';
2
+
3
+ export const REGION_DOCS = 'https://github.com/sidorares/ntk/blob/master/docs/context-2d.md#region-clips';
4
+
5
+ const registry = new FinalizationRegistry(({ fixes, X, id }) => {
6
+ safeRelease(X, () => {
7
+ fixes.DestroyRegion(id);
8
+ X.ReleaseID(id);
9
+ });
10
+ });
11
+
12
+ /**
13
+ * One rectangle in the form XFIXES wants, from either spelling.
14
+ *
15
+ * ntk's own boxes are `{ x, y, w, h }` — that is what `_clipRect()` and the
16
+ * damage rectangles a window reports look like — and the protocol's are
17
+ * `{ x, y, width, height }`. Taking both means a region can be built out of
18
+ * boxes ntk handed the caller without a translation step whose only job is
19
+ * to rename two fields.
20
+ */
21
+ function toRect(r) {
22
+ const width = r.width ?? r.w;
23
+ const height = r.height ?? r.h;
24
+ if (!Number.isFinite(r.x) || !Number.isFinite(r.y) || !Number.isFinite(width) || !Number.isFinite(height)) {
25
+ throw new TypeError(`ntk: region rectangle needs finite x/y/width/height, got ${JSON.stringify(r)}`);
26
+ }
27
+ return { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(width), height: Math.round(height) };
28
+ }
29
+
30
+ const toRects = (rects) => (rects || []).map(toRect);
31
+
32
+ /**
33
+ * The id of an XFIXES region, given a `Region`, a raw id, or anything else
34
+ * carrying one (`{ id }`). Regions are ids on the wire, so a caller who made
35
+ * one through node-x11 directly can still hand it to `ctx.clipRegion()`.
36
+ */
37
+ export function regionId(value) {
38
+ const id = typeof value === 'number' ? value : value?.id;
39
+ if (!Number.isInteger(id) || id <= 0) {
40
+ throw new TypeError(
41
+ `ntk: expected an XFIXES region (app.createRegion(...)) or its id, got ${JSON.stringify(value)}`
42
+ );
43
+ }
44
+ return id;
45
+ }
46
+
47
+ /**
48
+ * A server-side XFIXES region: a set of rectangles the X server keeps and
49
+ * combines for you, with no round trip per operation.
50
+ *
51
+ * Regions are how X describes a non-rectangular area — damage from an
52
+ * expose, a window's SHAPE, the area a compositor has left to paint after
53
+ * subtracting the windows in front. `ctx.clipRegion(region)` clips a 2d
54
+ * context to one (docs/context-2d.md).
55
+ *
56
+ * Build one with `await app.createRegion(rects)`; the await is the XFIXES
57
+ * extension being loaded on first use, not a round trip per region.
58
+ * Everything after that is asynchronous in the X sense — requests go out and
59
+ * nothing waits — except `fetch()`, which is a reply.
60
+ */
61
+ export default class Region {
62
+ /**
63
+ * Not called directly: `app.createRegion(rects)` loads XFIXES first and is
64
+ * the supported way in.
65
+ *
66
+ * @param {App} app
67
+ * @param {object} fixes the XFIXES extension, as `app.fixes()` resolves it
68
+ * @param {Array<object>} [rects] `{ x, y, width, height }` or `{ x, y, w, h }`
69
+ */
70
+ constructor(app, fixes, rects = []) {
71
+ this.app = app;
72
+ this.X = app.X;
73
+ this.fixes = fixes;
74
+ this.id = this.X.AllocID();
75
+ this.fixes.CreateRegion(this.id, toRects(rects));
76
+ this._owned = true;
77
+ registry.register(this, { fixes, X: this.X, id: this.id }, this);
78
+ }
79
+
80
+ /** Replace the contents with this rectangle list. */
81
+ set(rects) {
82
+ this.fixes.SetRegion(this.id, toRects(rects));
83
+ return this;
84
+ }
85
+
86
+ /** Replace the contents with another region's. */
87
+ copyFrom(other) {
88
+ this.fixes.CopyRegion(regionId(other), this.id);
89
+ return this;
90
+ }
91
+
92
+ /** Move by (dx, dy). */
93
+ translate(dx, dy) {
94
+ this.fixes.TranslateRegion(this.id, Math.round(dx), Math.round(dy));
95
+ return this;
96
+ }
97
+
98
+ /** Keep only what is also in `other`. In place. */
99
+ intersect(other) {
100
+ this.fixes.IntersectRegion(this.id, regionId(other), this.id);
101
+ return this;
102
+ }
103
+
104
+ /** Add everything in `other`. In place. */
105
+ union(other) {
106
+ this.fixes.UnionRegion(this.id, regionId(other), this.id);
107
+ return this;
108
+ }
109
+
110
+ /**
111
+ * Remove everything in `other`. In place — and the operation a compositor
112
+ * runs once per window, painting front to back and taking each window's
113
+ * shape out of what is left for the ones behind it.
114
+ */
115
+ subtract(other) {
116
+ this.fixes.SubtractRegion(this.id, regionId(other), this.id);
117
+ return this;
118
+ }
119
+
120
+ /**
121
+ * Read the region back: `{ extents, rectangles }`, each rectangle
122
+ * `{ x, y, width, height }`. The one round trip in the class — regions are
123
+ * meant to be combined server-side, so reach for this to inspect or to
124
+ * test, not inside a paint loop.
125
+ *
126
+ * @returns {Promise<{extents: object, rectangles: Array<object>}>}
127
+ */
128
+ fetch() {
129
+ return new Promise((resolve, reject) => {
130
+ this.fixes.FetchRegion(this.id, (err, region) => (err ? reject(err) : resolve(region)));
131
+ });
132
+ }
133
+
134
+ /** Free the region server-side. Idempotent. */
135
+ destroy() {
136
+ if (!this._owned) return;
137
+ this._owned = false;
138
+ registry.unregister(this);
139
+ safeRelease(this.X, () => {
140
+ this.fixes.DestroyRegion(this.id);
141
+ this.X.ReleaseID(this.id);
142
+ });
143
+ }
144
+
145
+ [Symbol.dispose]() {
146
+ this.destroy();
147
+ }
148
+ }