underpost 2.8.82 → 2.8.85

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 (115) hide show
  1. package/.env.development +1 -0
  2. package/.env.production +1 -0
  3. package/.env.test +1 -0
  4. package/.github/workflows/{ghpkg.yml → ghpkg.ci.yml} +5 -5
  5. package/.github/workflows/{npmpkg.yml → npmpkg.ci.yml} +5 -5
  6. package/.github/workflows/{publish.yml → publish.ci.yml} +1 -1
  7. package/.github/workflows/{pwa-microservices-template.page.yml → pwa-microservices-template-page.cd.yml} +1 -1
  8. package/.github/workflows/{pwa-microservices-template.test.yml → pwa-microservices-template-test.ci.yml} +1 -1
  9. package/.vscode/extensions.json +1 -1
  10. package/.vscode/settings.json +0 -44
  11. package/README.md +62 -2
  12. package/bin/build.js +15 -5
  13. package/bin/deploy.js +42 -92
  14. package/bin/file.js +33 -9
  15. package/bin/vs.js +12 -4
  16. package/cli.md +90 -42
  17. package/conf.js +1 -1
  18. package/docker-compose.yml +1 -1
  19. package/manifests/deployment/dd-template-development/deployment.yaml +2 -2
  20. package/manifests/deployment/tensorflow/tf-gpu-test.yaml +65 -0
  21. package/manifests/maas/device-scan.sh +3 -3
  22. package/manifests/maas/gpu-diag.sh +19 -0
  23. package/manifests/maas/maas-setup.sh +10 -10
  24. package/manifests/maas/snap-clean.sh +26 -0
  25. package/package.json +4 -6
  26. package/src/api/user/user.router.js +24 -1
  27. package/src/api/user/user.service.js +1 -4
  28. package/src/cli/baremetal.js +105 -73
  29. package/src/cli/cloud-init.js +21 -12
  30. package/src/cli/cluster.js +227 -133
  31. package/src/cli/deploy.js +34 -0
  32. package/src/cli/index.js +28 -1
  33. package/src/cli/monitor.js +8 -12
  34. package/src/cli/repository.js +7 -4
  35. package/src/cli/run.js +367 -0
  36. package/src/cli/ssh.js +32 -0
  37. package/src/cli/test.js +1 -1
  38. package/src/client/Default.index.js +7 -3
  39. package/src/client/components/core/Account.js +1 -1
  40. package/src/client/components/core/Chat.js +1 -1
  41. package/src/client/components/core/CommonJs.js +24 -22
  42. package/src/client/components/core/Content.js +1 -5
  43. package/src/client/components/core/Css.js +258 -18
  44. package/src/client/components/core/CssCore.js +8 -8
  45. package/src/client/components/core/Docs.js +14 -61
  46. package/src/client/components/core/DropDown.js +137 -82
  47. package/src/client/components/core/EventsUI.js +92 -5
  48. package/src/client/components/core/LoadingAnimation.js +8 -15
  49. package/src/client/components/core/Modal.js +597 -136
  50. package/src/client/components/core/NotificationManager.js +2 -2
  51. package/src/client/components/core/ObjectLayerEngine.js +638 -0
  52. package/src/client/components/core/Panel.js +158 -34
  53. package/src/client/components/core/PanelForm.js +12 -3
  54. package/src/client/components/core/Recover.js +1 -1
  55. package/src/client/components/core/Router.js +77 -17
  56. package/src/client/components/core/SocketIo.js +3 -3
  57. package/src/client/components/core/Translate.js +6 -2
  58. package/src/client/components/core/VanillaJs.js +0 -3
  59. package/src/client/components/core/Worker.js +3 -1
  60. package/src/client/components/default/CssDefault.js +17 -3
  61. package/src/client/components/default/MenuDefault.js +264 -45
  62. package/src/client/components/default/RoutesDefault.js +6 -12
  63. package/src/client/public/default/android-chrome-144x144.png +0 -0
  64. package/src/client/public/default/android-chrome-192x192.png +0 -0
  65. package/src/client/public/default/android-chrome-256x256.png +0 -0
  66. package/src/client/public/default/android-chrome-36x36.png +0 -0
  67. package/src/client/public/default/android-chrome-48x48.png +0 -0
  68. package/src/client/public/default/android-chrome-72x72.png +0 -0
  69. package/src/client/public/default/android-chrome-96x96.png +0 -0
  70. package/src/client/public/default/apple-touch-icon-114x114-precomposed.png +0 -0
  71. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  72. package/src/client/public/default/apple-touch-icon-120x120-precomposed.png +0 -0
  73. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  74. package/src/client/public/default/apple-touch-icon-144x144-precomposed.png +0 -0
  75. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  76. package/src/client/public/default/apple-touch-icon-152x152-precomposed.png +0 -0
  77. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  78. package/src/client/public/default/apple-touch-icon-180x180-precomposed.png +0 -0
  79. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  80. package/src/client/public/default/apple-touch-icon-57x57-precomposed.png +0 -0
  81. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  82. package/src/client/public/default/apple-touch-icon-60x60-precomposed.png +0 -0
  83. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  84. package/src/client/public/default/apple-touch-icon-72x72-precomposed.png +0 -0
  85. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  86. package/src/client/public/default/apple-touch-icon-76x76-precomposed.png +0 -0
  87. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  88. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  89. package/src/client/public/default/apple-touch-icon.png +0 -0
  90. package/src/client/public/default/assets/background/dark.jpg +0 -0
  91. package/src/client/public/default/assets/background/dark.svg +557 -0
  92. package/src/client/public/default/assets/logo/base-icon.png +0 -0
  93. package/src/client/public/default/assets/logo/underpost.gif +0 -0
  94. package/src/client/public/default/assets/mailer/api-user-check.png +0 -0
  95. package/src/client/public/default/assets/mailer/api-user-invalid-token.png +0 -0
  96. package/src/client/public/default/assets/mailer/api-user-recover.png +0 -0
  97. package/src/client/public/default/favicon-16x16.png +0 -0
  98. package/src/client/public/default/favicon-32x32.png +0 -0
  99. package/src/client/public/default/favicon.ico +0 -0
  100. package/src/client/public/default/mstile-144x144.png +0 -0
  101. package/src/client/public/default/mstile-150x150.png +0 -0
  102. package/src/client/public/default/mstile-310x150.png +0 -0
  103. package/src/client/public/default/mstile-310x310.png +0 -0
  104. package/src/client/public/default/mstile-70x70.png +0 -0
  105. package/src/client/public/default/safari-pinned-tab.svg +24 -0
  106. package/src/client/ssr/body/DefaultSplashScreen.js +2 -2
  107. package/src/index.js +34 -17
  108. package/src/monitor.js +24 -0
  109. package/src/runtime/lampp/Dockerfile +30 -39
  110. package/src/runtime/lampp/Lampp.js +11 -2
  111. package/src/server/client-build-docs.js +205 -0
  112. package/src/server/client-build.js +16 -166
  113. package/src/server/conf.js +18 -8
  114. package/src/server/process.js +16 -19
  115. package/src/server/valkey.js +102 -41
@@ -15,10 +15,10 @@ const NotificationManager = {
15
15
  right: 5px !important;
16
16
  width: 300px !important;
17
17
  bottom: ${5 + (options?.heightBottomBar ? options.heightBottomBar : 0)}px !important;
18
- z-index: 5 !important;
18
+ z-index: 11 !important;
19
19
  }
20
20
  .notification-board-title {
21
- padding: 5px !important;
21
+ padding: 11px !important;
22
22
  }
23
23
  .notification-manager-date {
24
24
  font-size: 20px !important;
@@ -0,0 +1,638 @@
1
+ import { darkTheme, renderChessPattern } from './Css.js';
2
+
3
+ const templateHTML = html`
4
+ <style>
5
+ :host {
6
+ --border: 1px solid #bbb;
7
+ --gap: 8px;
8
+ display: inline-block;
9
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
10
+ }
11
+ .wrap {
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: var(--gap);
15
+ align-items: flex-start;
16
+ }
17
+ .canvas-frame {
18
+ border: var(--border);
19
+ display: inline-block;
20
+ line-height: 0;
21
+ position: relative;
22
+ background: transparent;
23
+ }
24
+ canvas.canvas-layer {
25
+ display: block;
26
+ image-rendering: pixelated;
27
+ touch-action: none;
28
+ cursor: crosshair;
29
+ }
30
+ canvas.grid-layer {
31
+ position: absolute;
32
+ left: 0;
33
+ top: 0;
34
+ pointer-events: none;
35
+ }
36
+ </style>
37
+
38
+ <div class="wrap">
39
+ <div class="toolbar">
40
+ <input type="color" part="color" title="Brush color" value="#000000" />
41
+ <select part="tool">
42
+ <option value="pencil">pencil</option>
43
+ <option value="eraser">eraser</option>
44
+ <option value="fill">fill</option>
45
+ <option value="eyedropper">eyedropper</option>
46
+ </select>
47
+
48
+ <label>brush <input type="number" part="brush-size" min="1" value="1" /></label>
49
+ <label>pixel-size <input type="number" part="pixel-size" min="1" value="16" /></label>
50
+
51
+ <label class="switch"> <input type="checkbox" part="toggle-grid" /> grid </label>
52
+
53
+ <button part="export">Export PNG</button>
54
+ <button part="export-json">Export JSON</button>
55
+ <button part="import-json">Import JSON</button>
56
+ </div>
57
+ <div class="canvas-frame" style="${renderChessPattern()}">
58
+ <canvas part="canvas" class="canvas-layer"></canvas>
59
+ <canvas part="grid" class="grid-layer"></canvas>
60
+ </div>
61
+ </div>
62
+ `;
63
+
64
+ class ObjectLayerEngineElement extends HTMLElement {
65
+ constructor() {
66
+ super();
67
+ this.attachShadow({ mode: 'open' });
68
+ this.shadowRoot.innerHTML = templateHTML;
69
+
70
+ // DOM
71
+ this._pixelCanvas = this.shadowRoot.querySelector('canvas[part="canvas"]');
72
+ this._gridCanvas = this.shadowRoot.querySelector('canvas[part="grid"]');
73
+ this._colorInput = this.shadowRoot.querySelector('input[part="color"]');
74
+ this._toolSelect = this.shadowRoot.querySelector('select[part="tool"]');
75
+ this._brushSizeInput = this.shadowRoot.querySelector('input[part="brush-size"]');
76
+ this._pixelSizeInput = this.shadowRoot.querySelector('input[part="pixel-size"]');
77
+ this._exportBtn = this.shadowRoot.querySelector('button[part="export"]');
78
+ this._exportJsonBtn = this.shadowRoot.querySelector('button[part="export-json"]');
79
+ this._importJsonBtn = this.shadowRoot.querySelector('button[part="import-json"]');
80
+ this._toggleGrid = this.shadowRoot.querySelector('input[part="toggle-grid"]');
81
+
82
+ // internal state
83
+ this._width = 16;
84
+ this._height = 16;
85
+ this._pixelSize = 16;
86
+ this._brushSize = 1;
87
+ this._matrix = this._createEmptyMatrix(this._width, this._height);
88
+
89
+ this._pixelCtx = null;
90
+ this._gridCtx = null;
91
+
92
+ this._isPointerDown = false;
93
+ this._tool = 'pencil';
94
+ this._brushColor = [0, 0, 0, 255];
95
+ this._showGrid = false;
96
+
97
+ // binds
98
+ this._onPointerDown = this._onPointerDown.bind(this);
99
+ this._onPointerMove = this._onPointerMove.bind(this);
100
+ this._onPointerUp = this._onPointerUp.bind(this);
101
+ }
102
+
103
+ static get observedAttributes() {
104
+ return ['width', 'height', 'pixel-size'];
105
+ }
106
+ attributeChangedCallback(name, oldV, newV) {
107
+ if (oldV === newV) return;
108
+ if (name === 'width') this.width = parseInt(newV, 10) || this._width;
109
+ if (name === 'height') this.height = parseInt(newV, 10) || this._height;
110
+ if (name === 'pixel-size') this.pixelSize = parseInt(newV, 10) || this._pixelSize;
111
+ }
112
+
113
+ connectedCallback() {
114
+ if (this.hasAttribute('width')) this._width = Math.max(1, parseInt(this.getAttribute('width'), 10));
115
+ if (this.hasAttribute('height')) this._height = Math.max(1, parseInt(this.getAttribute('height'), 10));
116
+ if (this.hasAttribute('pixel-size')) this._pixelSize = Math.max(1, parseInt(this.getAttribute('pixel-size'), 10));
117
+
118
+ this._setupContextsAndSize();
119
+
120
+ // UI events
121
+ this._colorInput.addEventListener('input', (e) => {
122
+ const rgba = this._hexToRgba(e.target.value);
123
+ this.setBrushColor([rgba[0], rgba[1], rgba[2], 255]);
124
+ });
125
+ this._toolSelect.addEventListener('change', (e) => this.setTool(e.target.value));
126
+ this._brushSizeInput.addEventListener('change', (e) => this.setBrushSize(parseInt(e.target.value, 10) || 1));
127
+ this._pixelSizeInput.addEventListener('change', (e) => {
128
+ this.pixelSize = Math.max(1, parseInt(e.target.value, 10) || 1);
129
+ });
130
+ this._toggleGrid.addEventListener('change', (e) => {
131
+ this._showGrid = !!e.target.checked;
132
+ this._renderGrid();
133
+ });
134
+
135
+ // Export/Import
136
+ this._exportBtn.addEventListener('click', () => this.exportPNG());
137
+ this._exportJsonBtn.addEventListener('click', () => {
138
+ const json = this.exportMatrixJSON();
139
+ const blob = new Blob([json], { type: 'application/json' });
140
+ const url = URL.createObjectURL(blob);
141
+ const a = document.createElement('a');
142
+ a.href = url;
143
+ a.download = 'object-layer.json';
144
+ a.click();
145
+ URL.revokeObjectURL(url);
146
+ });
147
+
148
+ this._importJsonBtn.addEventListener('click', async () => {
149
+ const file = await this._pickFile();
150
+ if (!file) return;
151
+ const text = await file.text();
152
+ try {
153
+ this.importMatrixJSON(text);
154
+ } catch (err) {
155
+ console.error(err);
156
+ alert('Invalid JSON');
157
+ }
158
+ });
159
+
160
+ // Pointer events
161
+ this._pixelCanvas.addEventListener('pointerdown', this._onPointerDown);
162
+ window.addEventListener('pointermove', this._onPointerMove);
163
+ window.addEventListener('pointerup', this._onPointerUp);
164
+
165
+ this.render();
166
+ }
167
+
168
+ disconnectedCallback() {
169
+ this._pixelCanvas.removeEventListener('pointerdown', this._onPointerDown);
170
+ window.removeEventListener('pointermove', this._onPointerMove);
171
+ window.removeEventListener('pointerup', this._onPointerUp);
172
+ }
173
+
174
+ // ---------------- Matrix helpers ----------------
175
+ _createEmptyMatrix(w, h) {
176
+ const mat = new Array(h);
177
+ for (let y = 0; y < h; y++) {
178
+ mat[y] = new Array(w);
179
+ for (let x = 0; x < w; x++) mat[y][x] = [0, 0, 0, 0];
180
+ }
181
+ return mat;
182
+ }
183
+
184
+ createMatrix(width, height, fill = [0, 0, 0, 0]) {
185
+ const w = Math.max(1, Math.floor(width));
186
+ const h = Math.max(1, Math.floor(height));
187
+ const mat = this._createEmptyMatrix(w, h);
188
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) mat[y][x] = fill.slice();
189
+ return mat;
190
+ }
191
+
192
+ loadMatrix(matrix) {
193
+ if (!Array.isArray(matrix) || matrix.length === 0) throw new TypeError('matrix must be non-empty 2D array');
194
+ const h = matrix.length;
195
+ const w = matrix[0].length;
196
+ for (let y = 0; y < h; y++) {
197
+ if (!Array.isArray(matrix[y]) || matrix[y].length !== w) throw new TypeError('matrix must be rectangular');
198
+ for (let x = 0; x < w; x++) {
199
+ const v = matrix[y][x];
200
+ if (!Array.isArray(v) || v.length !== 4) throw new TypeError('each cell must be [r,g,b,a]');
201
+ matrix[y][x] = v.map((n) => this._clampInt(n));
202
+ }
203
+ }
204
+ this._width = w;
205
+ this._height = h;
206
+ this._matrix = matrix.map((r) => r.map((c) => c.slice()));
207
+ this._setupContextsAndSize();
208
+ this.render();
209
+ this.dispatchEvent(new CustomEvent('matrixload', { detail: { width: w, height: h } }));
210
+ }
211
+
212
+ clear(fill = [0, 0, 0, 0]) {
213
+ for (let y = 0; y < this._height; y++) for (let x = 0; x < this._width; x++) this._matrix[y][x] = fill.slice();
214
+ this.render();
215
+ this.dispatchEvent(new CustomEvent('clear'));
216
+ }
217
+
218
+ resize(w, h, { preserve = true } = {}) {
219
+ const nw = Math.max(1, Math.floor(w));
220
+ const nh = Math.max(1, Math.floor(h));
221
+ const newMat = this._createEmptyMatrix(nw, nh);
222
+ if (preserve) {
223
+ const minW = Math.min(nw, this._width);
224
+ const minH = Math.min(nh, this._height);
225
+ for (let y = 0; y < minH; y++) for (let x = 0; x < minW; x++) newMat[y][x] = this._matrix[y][x].slice();
226
+ }
227
+ this._width = nw;
228
+ this._height = nh;
229
+ this._matrix = newMat;
230
+ this._setupContextsAndSize();
231
+ this.render();
232
+ this.dispatchEvent(new CustomEvent('resize', { detail: { width: nw, height: nh } }));
233
+ }
234
+
235
+ setPixel(x, y, rgba, renderNow = true) {
236
+ if (!this._inBounds(x, y)) return false;
237
+ this._matrix[y][x] = rgba.map((n) => this._clampInt(n));
238
+ if (renderNow) this.render();
239
+ this.dispatchEvent(new CustomEvent('pixelchange', { detail: { x, y, rgba: this._matrix[y][x].slice() } }));
240
+ return true;
241
+ }
242
+
243
+ getPixel(x, y) {
244
+ return this._inBounds(x, y) ? this._matrix[y][x].slice() : null;
245
+ }
246
+ _inBounds(x, y) {
247
+ return x >= 0 && y >= 0 && x < this._width && y < this._height;
248
+ }
249
+ _clampInt(v) {
250
+ const n = Number(v) || 0;
251
+ return Math.min(255, Math.max(0, Math.floor(n)));
252
+ }
253
+
254
+ // ---------------- Canvas sizing and contexts ----------------
255
+ _setupContextsAndSize() {
256
+ // logical canvas (one logical pixel per image pixel). CSS scales by pixelSize.
257
+ this._pixelCanvas.width = this._width;
258
+ this._pixelCanvas.height = this._height;
259
+ this._pixelCanvas.style.width = `${this._width * this._pixelSize}px`;
260
+ this._pixelCanvas.style.height = `${this._height * this._pixelSize}px`;
261
+
262
+ // grid overlay uses CSS pixel coordinates
263
+ this._gridCanvas.width = this._width * this._pixelSize;
264
+ this._gridCanvas.height = this._height * this._pixelSize;
265
+ this._gridCanvas.style.width = this._pixelCanvas.style.width;
266
+ this._gridCanvas.style.height = this._pixelCanvas.style.height;
267
+
268
+ this._pixelCtx = this._pixelCanvas.getContext('2d');
269
+ this._gridCtx = this._gridCanvas.getContext('2d');
270
+ try {
271
+ this._pixelCtx.imageSmoothingEnabled = false;
272
+ this._gridCtx.imageSmoothingEnabled = false;
273
+ } catch (e) {}
274
+ this._renderGrid();
275
+ }
276
+
277
+ render() {
278
+ // sanity: ensure matrix shape matches
279
+ if (
280
+ !Array.isArray(this._matrix) ||
281
+ this._matrix.length !== this._height ||
282
+ !Array.isArray(this._matrix[0]) ||
283
+ this._matrix[0].length !== this._width
284
+ ) {
285
+ this._matrix = this._createEmptyMatrix(this._width, this._height);
286
+ }
287
+
288
+ // detect transparency (fast bailout)
289
+ let hasTransparent = false;
290
+ for (let y = 0; y < this._height && !hasTransparent; y++) {
291
+ for (let x = 0; x < this._width; x++) {
292
+ const a = this._matrix[y] && this._matrix[y][x] ? this._matrix[y][x][3] : 0;
293
+ if (a !== 255) {
294
+ hasTransparent = true;
295
+ break;
296
+ }
297
+ }
298
+ }
299
+
300
+ // clear and optionally draw checkerboard (visual only)
301
+ this._pixelCtx.clearRect(0, 0, this._pixelCanvas.width, this._pixelCanvas.height);
302
+ if (hasTransparent) this._drawCheckerboard();
303
+
304
+ // write image data
305
+ const img = this._pixelCtx.createImageData(this._width, this._height);
306
+ const data = img.data;
307
+ let p = 0;
308
+ for (let y = 0; y < this._height; y++) {
309
+ for (let x = 0; x < this._width; x++) {
310
+ const cell = this._matrix[y] && this._matrix[y][x] ? this._matrix[y][x] : [0, 0, 0, 0];
311
+ data[p++] = this._clampInt(cell[0]);
312
+ data[p++] = this._clampInt(cell[1]);
313
+ data[p++] = this._clampInt(cell[2]);
314
+ data[p++] = this._clampInt(cell[3]);
315
+ }
316
+ }
317
+ this._pixelCtx.putImageData(img, 0, 0);
318
+
319
+ if (this._showGrid) this._renderGrid();
320
+ }
321
+
322
+ _drawCheckerboard() {
323
+ const ctx = this._pixelCtx;
324
+ const w = this._width;
325
+ const h = this._height;
326
+ const light = '#e9e9e9',
327
+ dark = '#cfcfcf';
328
+ // draw one logical pixel per matrix cell
329
+ for (let y = 0; y < h; y++) {
330
+ for (let x = 0; x < w; x++) {
331
+ ctx.fillStyle = ((x + y) & 1) === 0 ? light : dark;
332
+ ctx.fillRect(x, y, 1, 1);
333
+ }
334
+ }
335
+ }
336
+
337
+ _renderGrid() {
338
+ const ctx = this._gridCtx;
339
+ if (!ctx) return;
340
+ const w = this._gridCanvas.width;
341
+ const h = this._gridCanvas.height;
342
+ ctx.clearRect(0, 0, w, h);
343
+ if (!this._showGrid) return;
344
+ const ps = this._pixelSize;
345
+ ctx.save();
346
+ ctx.strokeStyle = darkTheme ? '#e1e1e1' : '#272727';
347
+ ctx.lineWidth = 2;
348
+ ctx.beginPath();
349
+ for (let x = 0; x <= this._width; x++) {
350
+ const xx = x * ps + 0.5;
351
+ ctx.moveTo(xx, 0);
352
+ ctx.lineTo(xx, h);
353
+ }
354
+ for (let y = 0; y <= this._height; y++) {
355
+ const yy = y * ps + 0.5;
356
+ ctx.moveTo(0, yy);
357
+ ctx.lineTo(w, yy);
358
+ }
359
+ ctx.stroke();
360
+ ctx.restore();
361
+ }
362
+
363
+ // ---------------- Tools & painting ----------------
364
+ setTool(name) {
365
+ this._tool = name;
366
+ if (this._toolSelect) this._toolSelect.value = name;
367
+ }
368
+ setBrushColor(rgba) {
369
+ this._brushColor = rgba.map((n) => this._clampInt(n));
370
+ if (this._colorInput) this._colorInput.value = this._rgbaToHex(this._brushColor);
371
+ }
372
+ setBrushSize(n) {
373
+ this._brushSize = Math.max(1, Math.floor(n));
374
+ if (this._brushSizeInput) this._brushSizeInput.value = this._brushSize;
375
+ }
376
+
377
+ _applyBrush(x, y, color, renderAfter = false) {
378
+ const half = Math.floor(this._brushSize / 2);
379
+ for (let oy = -half; oy <= half; oy++)
380
+ for (let ox = -half; ox <= half; ox++) {
381
+ const tx = x + ox,
382
+ ty = y + oy;
383
+ if (this._inBounds(tx, ty)) this._matrix[ty][tx] = color.slice();
384
+ }
385
+ if (renderAfter) this.render();
386
+ }
387
+
388
+ fillBucket(x, y, targetColor = null) {
389
+ if (!this._inBounds(x, y)) return;
390
+ const src = this.getPixel(x, y);
391
+ const newColor = targetColor ? targetColor.slice() : this._brushColor.slice();
392
+ if (this._colorsEqual(src, newColor)) return;
393
+ const stack = [[x, y]];
394
+ while (stack.length) {
395
+ const [cx, cy] = stack.pop();
396
+ if (!this._inBounds(cx, cy)) continue;
397
+ const cur = this.getPixel(cx, cy);
398
+ if (!this._colorsEqual(cur, src)) continue;
399
+ this._matrix[cy][cx] = newColor.slice();
400
+ stack.push([cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]);
401
+ }
402
+ this.render();
403
+ this.dispatchEvent(new CustomEvent('fill', { detail: { x, y } }));
404
+ }
405
+
406
+ _colorsEqual(a, b) {
407
+ if (!a || !b) return false;
408
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
409
+ }
410
+
411
+ // ---------------- Pointer handling ----------------
412
+ _toGridCoords(evt) {
413
+ const rect = this._pixelCanvas.getBoundingClientRect();
414
+ const cssX = evt.clientX - rect.left;
415
+ const cssY = evt.clientY - rect.top;
416
+ const scaleX = this._pixelCanvas.width / rect.width;
417
+ const scaleY = this._pixelCanvas.height / rect.height;
418
+ const x = Math.floor(cssX * scaleX);
419
+ const y = Math.floor(cssY * scaleY);
420
+ return [x, y];
421
+ }
422
+
423
+ _onPointerDown(evt) {
424
+ evt.preventDefault();
425
+ this._isPointerDown = true;
426
+ try {
427
+ this._pixelCanvas.setPointerCapture(evt.pointerId);
428
+ } catch (e) {}
429
+ const [x, y] = this._toGridCoords(evt);
430
+ this._applyToolAt(x, y, evt);
431
+ }
432
+ _onPointerMove(evt) {
433
+ if (!this._isPointerDown) return;
434
+ const [x, y] = this._toGridCoords(evt);
435
+ this._applyToolAt(x, y, evt, true);
436
+ }
437
+ _onPointerUp(evt) {
438
+ this._isPointerDown = false;
439
+ try {
440
+ this._pixelCanvas.releasePointerCapture(evt.pointerId);
441
+ } catch (e) {}
442
+ }
443
+
444
+ _applyToolAt(x, y, evt, continuous = false) {
445
+ if (!this._inBounds(x, y)) return;
446
+ switch (this._tool) {
447
+ case 'pencil':
448
+ this._applyBrush(x, y, this._brushColor, true);
449
+ break;
450
+ case 'eraser':
451
+ this._applyBrush(x, y, [0, 0, 0, 0], true);
452
+ break;
453
+ case 'fill':
454
+ if (!continuous) this.fillBucket(x, y);
455
+ break;
456
+ case 'eyedropper':
457
+ const picked = this.getPixel(x, y);
458
+ if (picked) this.setBrushColor(picked);
459
+ break;
460
+ }
461
+ }
462
+
463
+ // ---------------- Import / Export ----------------
464
+ exportMatrixJSON() {
465
+ return JSON.stringify({ width: this._width, height: this._height, matrix: this._matrix });
466
+ }
467
+ importMatrixJSON(json) {
468
+ const data = typeof json === 'string' ? JSON.parse(json) : json;
469
+ if (!data || !Array.isArray(data.matrix)) throw new TypeError('Invalid matrix JSON');
470
+ this.loadMatrix(data.matrix);
471
+ }
472
+
473
+ async _pickFile() {
474
+ return new Promise((resolve) => {
475
+ const input = document.createElement('input');
476
+ input.type = 'file';
477
+ input.accept = 'application/json';
478
+ input.addEventListener('change', () => {
479
+ resolve(input.files && input.files[0] ? input.files[0] : null);
480
+ });
481
+ input.click();
482
+ });
483
+ }
484
+
485
+ // Create a PNG data URL at the requested scale (scale = number of CSS pixels per logical pixel)
486
+ toDataURL(scale = this._pixelSize) {
487
+ const w = this._width,
488
+ h = this._height;
489
+ const outW = Math.max(1, Math.floor(w * scale));
490
+ const outH = Math.max(1, Math.floor(h * scale));
491
+
492
+ // create logical image at native resolution
493
+ const src = document.createElement('canvas');
494
+ src.width = w;
495
+ src.height = h;
496
+ const sctx = src.getContext('2d');
497
+ const img = sctx.createImageData(w, h);
498
+ const data = img.data;
499
+ let p = 0;
500
+ for (let y = 0; y < h; y++)
501
+ for (let x = 0; x < w; x++) {
502
+ const c = this._matrix[y][x] || [0, 0, 0, 0];
503
+ data[p++] = this._clampInt(c[0]);
504
+ data[p++] = this._clampInt(c[1]);
505
+ data[p++] = this._clampInt(c[2]);
506
+ data[p++] = this._clampInt(c[3]);
507
+ }
508
+ sctx.putImageData(img, 0, 0);
509
+
510
+ // scale into output canvas (nearest-neighbor)
511
+ const out = document.createElement('canvas');
512
+ out.width = outW;
513
+ out.height = outH;
514
+ const octx = out.getContext('2d');
515
+ try {
516
+ octx.imageSmoothingEnabled = false;
517
+ } catch (e) {}
518
+ octx.drawImage(src, 0, 0, outW, outH);
519
+ return out.toDataURL('image/png');
520
+ }
521
+
522
+ // Async blob version (recommended for large images)
523
+ toBlob(scale = this._pixelSize) {
524
+ return new Promise((resolve) => {
525
+ const w = this._width,
526
+ h = this._height;
527
+ const outW = Math.max(1, Math.floor(w * scale));
528
+ const outH = Math.max(1, Math.floor(h * scale));
529
+ const src = document.createElement('canvas');
530
+ src.width = w;
531
+ src.height = h;
532
+ const sctx = src.getContext('2d');
533
+ const img = sctx.createImageData(w, h);
534
+ const data = img.data;
535
+ let p = 0;
536
+ for (let y = 0; y < h; y++)
537
+ for (let x = 0; x < w; x++) {
538
+ const c = this._matrix[y][x] || [0, 0, 0, 0];
539
+ data[p++] = this._clampInt(c[0]);
540
+ data[p++] = this._clampInt(c[1]);
541
+ data[p++] = this._clampInt(c[2]);
542
+ data[p++] = this._clampInt(c[3]);
543
+ }
544
+ sctx.putImageData(img, 0, 0);
545
+ const out = document.createElement('canvas');
546
+ out.width = outW;
547
+ out.height = outH;
548
+ const octx = out.getContext('2d');
549
+ try {
550
+ octx.imageSmoothingEnabled = false;
551
+ } catch (e) {}
552
+ octx.drawImage(src, 0, 0, outW, outH);
553
+ out.toBlob((b) => resolve(b), 'image/png');
554
+ });
555
+ }
556
+
557
+ // Trigger download of PNG (uses blob to avoid huge data URLs on big exports)
558
+ async exportPNG(filename = 'object-layer.png', scale = this._pixelSize) {
559
+ const blob = await this.toBlob(scale);
560
+ const url = URL.createObjectURL(blob);
561
+ const a = document.createElement('a');
562
+ a.href = url;
563
+ a.download = filename;
564
+ a.click();
565
+ // revoke after a tick to ensure download started
566
+ setTimeout(() => URL.revokeObjectURL(url), 5000);
567
+ }
568
+
569
+ // ---------------- Helpers ----------------
570
+ _hexToRgba(hex) {
571
+ const h = (hex || '').replace('#', '');
572
+ if (h.length === 3) {
573
+ return [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16), 255];
574
+ }
575
+ if (h.length === 6) {
576
+ return [parseInt(h.substring(0, 2), 16), parseInt(h.substring(2, 4), 16), parseInt(h.substring(4, 6), 16), 255];
577
+ }
578
+ return [0, 0, 0, 255];
579
+ }
580
+ _rgbaToHex(rgba) {
581
+ const [r, g, b] = rgba;
582
+ return `#${((1 << 24) + (this._clampInt(r) << 16) + (this._clampInt(g) << 8) + this._clampInt(b))
583
+ .toString(16)
584
+ .slice(1)}`;
585
+ }
586
+
587
+ // ---------------- Properties ----------------
588
+ get width() {
589
+ return this._width;
590
+ }
591
+ set width(v) {
592
+ this._width = Math.max(1, Math.floor(v));
593
+ this.setAttribute('width', String(this._width));
594
+ this._setupContextsAndSize();
595
+ this.render();
596
+ }
597
+ get height() {
598
+ return this._height;
599
+ }
600
+ set height(v) {
601
+ this._height = Math.max(1, Math.floor(v));
602
+ this.setAttribute('height', String(this._height));
603
+ this._setupContextsAndSize();
604
+ this.render();
605
+ }
606
+ get pixelSize() {
607
+ return this._pixelSize;
608
+ }
609
+ set pixelSize(v) {
610
+ this._pixelSize = Math.max(1, Math.floor(v));
611
+ this.setAttribute('pixel-size', String(this._pixelSize));
612
+ this._setupContextsAndSize();
613
+ this.render();
614
+ }
615
+ get brushSize() {
616
+ return this._brushSize;
617
+ }
618
+ set brushSize(v) {
619
+ this.setBrushSize(v);
620
+ }
621
+ get matrix() {
622
+ return this._matrix.map((row) => row.map((c) => c.slice()));
623
+ }
624
+
625
+ exportJSON() {
626
+ return this.exportMatrixJSON();
627
+ }
628
+ importJSON(json) {
629
+ return this.importMatrixJSON(json);
630
+ }
631
+ }
632
+
633
+ customElements.define('object-layer-engine', ObjectLayerEngineElement);
634
+
635
+ /*
636
+ Example usage:
637
+ <object-layer-engine id="ole" width="20" height="12" pixel-size="20"></object-layer-engine>
638
+ */