underpost 2.85.7 → 2.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.cd.yml +1 -1
- package/README.md +2 -2
- package/bin/build.js +8 -10
- package/cli.md +3 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +50 -50
- package/manifests/deployment/dd-test-development/proxy.yaml +4 -4
- package/package.json +1 -1
- package/src/api/file/file.service.js +29 -3
- package/src/cli/baremetal.js +1 -2
- package/src/cli/index.js +1 -0
- package/src/cli/repository.js +8 -1
- package/src/cli/run.js +97 -36
- package/src/client/components/core/AgGrid.js +42 -3
- package/src/client/components/core/CommonJs.js +4 -0
- package/src/client/components/core/Css.js +95 -48
- package/src/client/components/core/CssCore.js +0 -1
- package/src/client/components/core/LoadingAnimation.js +2 -2
- package/src/client/components/core/Logger.js +2 -9
- package/src/client/components/core/Modal.js +22 -14
- package/src/client/components/core/ObjectLayerEngine.js +300 -9
- package/src/client/components/core/ObjectLayerEngineModal.js +686 -148
- package/src/client/components/core/ObjectLayerEngineViewer.js +1061 -0
- package/src/client/components/core/Pagination.js +15 -5
- package/src/client/components/core/Router.js +5 -1
- package/src/client/components/core/Translate.js +4 -0
- package/src/client/components/core/Worker.js +8 -1
- package/src/client/services/default/default.management.js +86 -16
- package/src/db/mariadb/MariaDB.js +2 -2
- package/src/index.js +1 -1
- package/src/server/client-build.js +57 -2
- package/src/server/object-layer.js +44 -0
- package/src/server/start.js +12 -0
- package/src/ws/IoInterface.js +2 -3
- package/AUTHORS.md +0 -21
- package/src/server/network.js +0 -72
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { darkTheme, renderChessPattern } from './Css.js';
|
|
2
2
|
import { append, htmls } from './VanillaJs.js';
|
|
3
|
+
import { NotificationManager } from './NotificationManager.js';
|
|
3
4
|
|
|
4
5
|
class ObjectLayerEngineElement extends HTMLElement {
|
|
5
6
|
constructor() {
|
|
@@ -11,7 +12,13 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
11
12
|
--border: 1px solid #bbb;
|
|
12
13
|
--gap: 8px;
|
|
13
14
|
display: inline-block;
|
|
14
|
-
font-family:
|
|
15
|
+
font-family:
|
|
16
|
+
system-ui,
|
|
17
|
+
-apple-system,
|
|
18
|
+
'Segoe UI',
|
|
19
|
+
Roboto,
|
|
20
|
+
'Helvetica Neue',
|
|
21
|
+
Arial;
|
|
15
22
|
}
|
|
16
23
|
.wrap {
|
|
17
24
|
display: flex;
|
|
@@ -58,7 +65,26 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
58
65
|
|
|
59
66
|
<div class="wrap">
|
|
60
67
|
<div class="toolbar">
|
|
68
|
+
<button part="undo" title="Undo" disabled>Undo</button>
|
|
69
|
+
<button part="redo" title="Redo" disabled>Redo</button>
|
|
70
|
+
|
|
61
71
|
<input type="color" part="color" title="Brush color" value="#000000" />
|
|
72
|
+
<label
|
|
73
|
+
>hex
|
|
74
|
+
<input
|
|
75
|
+
type="text"
|
|
76
|
+
part="hex-input"
|
|
77
|
+
title="Hex color (e.g., #FF0000 or #FF0000FF)"
|
|
78
|
+
placeholder="#000000FF"
|
|
79
|
+
style="width:9ch"
|
|
80
|
+
/></label>
|
|
81
|
+
<label
|
|
82
|
+
>rgba
|
|
83
|
+
<input type="number" part="r-input" min="0" max="255" value="0" title="Red (0-255)" style="width:5ch" />
|
|
84
|
+
<input type="number" part="g-input" min="0" max="255" value="0" title="Green (0-255)" style="width:5ch" />
|
|
85
|
+
<input type="number" part="b-input" min="0" max="255" value="0" title="Blue (0-255)" style="width:5ch" />
|
|
86
|
+
<input type="number" part="a-input" min="0" max="255" value="255" title="Alpha (0-255)" style="width:5ch" />
|
|
87
|
+
</label>
|
|
62
88
|
<select part="tool">
|
|
63
89
|
<option value="pencil">pencil</option>
|
|
64
90
|
<option value="eraser">eraser</option>
|
|
@@ -112,6 +138,11 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
112
138
|
this._pixelCanvas = this.shadowRoot.querySelector('canvas[part="canvas"]');
|
|
113
139
|
this._gridCanvas = this.shadowRoot.querySelector('canvas[part="grid"]');
|
|
114
140
|
this._colorInput = this.shadowRoot.querySelector('input[part="color"]');
|
|
141
|
+
this._hexInput = this.shadowRoot.querySelector('input[part="hex-input"]');
|
|
142
|
+
this._rInput = this.shadowRoot.querySelector('input[part="r-input"]');
|
|
143
|
+
this._gInput = this.shadowRoot.querySelector('input[part="g-input"]');
|
|
144
|
+
this._bInput = this.shadowRoot.querySelector('input[part="b-input"]');
|
|
145
|
+
this._aInput = this.shadowRoot.querySelector('input[part="a-input"]');
|
|
115
146
|
this._toolSelect = this.shadowRoot.querySelector('select[part="tool"]');
|
|
116
147
|
this._brushSizeInput = this.shadowRoot.querySelector('input[part="brush-size"]');
|
|
117
148
|
this._pixelSizeInput = this.shadowRoot.querySelector('input[part="pixel-size"]');
|
|
@@ -131,6 +162,10 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
131
162
|
this._opacityRange = this.shadowRoot.querySelector('input[part="opacity"]');
|
|
132
163
|
this._opacityNumber = this.shadowRoot.querySelector('input[part="opacity-num"]');
|
|
133
164
|
|
|
165
|
+
// undo/redo buttons
|
|
166
|
+
this._undoBtn = this.shadowRoot.querySelector('button[part="undo"]');
|
|
167
|
+
this._redoBtn = this.shadowRoot.querySelector('button[part="redo"]');
|
|
168
|
+
|
|
134
169
|
// internal state
|
|
135
170
|
this._width = 16;
|
|
136
171
|
this._height = 16;
|
|
@@ -147,16 +182,25 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
147
182
|
this._tool = 'pencil';
|
|
148
183
|
this._showGrid = false;
|
|
149
184
|
|
|
185
|
+
// history (undo/redo)
|
|
186
|
+
this._undoStack = [];
|
|
187
|
+
this._redoStack = [];
|
|
188
|
+
this._maxHistory = 200;
|
|
189
|
+
this._transactionActive = false; // grouping for pointer drags
|
|
190
|
+
|
|
150
191
|
// binds
|
|
151
192
|
this._onPointerDown = this._onPointerDown.bind(this);
|
|
152
193
|
this._onPointerMove = this._onPointerMove.bind(this);
|
|
153
194
|
this._onPointerUp = this._onPointerUp.bind(this);
|
|
195
|
+
this._onKeyDown = this._onKeyDown.bind(this);
|
|
154
196
|
|
|
155
197
|
// transform methods bound (useful if passing as callbacks)
|
|
156
198
|
this.flipHorizontal = this.flipHorizontal.bind(this);
|
|
157
199
|
this.flipVertical = this.flipVertical.bind(this);
|
|
158
200
|
this.rotateCW = this.rotateCW.bind(this);
|
|
159
201
|
this.rotateCCW = this.rotateCCW.bind(this);
|
|
202
|
+
|
|
203
|
+
// ensure keyboard handlers bound for undo/redo
|
|
160
204
|
}
|
|
161
205
|
|
|
162
206
|
static get observedAttributes() {
|
|
@@ -185,6 +229,11 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
185
229
|
|
|
186
230
|
// initialize color & opacity UI
|
|
187
231
|
if (this._colorInput) this._colorInput.value = this._rgbaToHex(this._brushColor);
|
|
232
|
+
if (this._hexInput) this._hexInput.value = this._rgbaToHexWithAlpha(this._brushColor);
|
|
233
|
+
if (this._rInput) this._rInput.value = String(this._brushColor[0]);
|
|
234
|
+
if (this._gInput) this._gInput.value = String(this._brushColor[1]);
|
|
235
|
+
if (this._bInput) this._bInput.value = String(this._brushColor[2]);
|
|
236
|
+
if (this._aInput) this._aInput.value = String(this._brushColor[3]);
|
|
188
237
|
if (this._opacityRange) this._opacityRange.value = String(this._brushColor[3]);
|
|
189
238
|
if (this._opacityNumber) this._opacityNumber.value = String(this._brushColor[3]);
|
|
190
239
|
|
|
@@ -194,6 +243,28 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
194
243
|
// keep current alpha
|
|
195
244
|
this.setBrushColor([rgb[0], rgb[1], rgb[2], this._brushColor[3]]);
|
|
196
245
|
});
|
|
246
|
+
|
|
247
|
+
// hex text input with optional alpha
|
|
248
|
+
if (this._hexInput) {
|
|
249
|
+
this._hexInput.addEventListener('change', (e) => {
|
|
250
|
+
const rgba = this._hexToRgbaWithAlpha(e.target.value);
|
|
251
|
+
this.setBrushColor(rgba);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// individual RGBA inputs
|
|
256
|
+
const updateFromRGBAInputs = () => {
|
|
257
|
+
const r = Math.max(0, Math.min(255, parseInt(this._rInput.value, 10) || 0));
|
|
258
|
+
const g = Math.max(0, Math.min(255, parseInt(this._gInput.value, 10) || 0));
|
|
259
|
+
const b = Math.max(0, Math.min(255, parseInt(this._bInput.value, 10) || 0));
|
|
260
|
+
const a = Math.max(0, Math.min(255, parseInt(this._aInput.value, 10) || 0));
|
|
261
|
+
this.setBrushColor([r, g, b, a]);
|
|
262
|
+
};
|
|
263
|
+
if (this._rInput) this._rInput.addEventListener('change', updateFromRGBAInputs);
|
|
264
|
+
if (this._gInput) this._gInput.addEventListener('change', updateFromRGBAInputs);
|
|
265
|
+
if (this._bInput) this._bInput.addEventListener('change', updateFromRGBAInputs);
|
|
266
|
+
if (this._aInput) this._aInput.addEventListener('change', updateFromRGBAInputs);
|
|
267
|
+
|
|
197
268
|
this._toolSelect.addEventListener('change', (e) => this.setTool(e.target.value));
|
|
198
269
|
this._brushSizeInput.addEventListener('change', (e) => this.setBrushSize(parseInt(e.target.value, 10) || 1));
|
|
199
270
|
this._pixelSizeInput.addEventListener('change', (e) => {
|
|
@@ -232,13 +303,38 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
232
303
|
});
|
|
233
304
|
|
|
234
305
|
// transform buttons
|
|
235
|
-
if (this._flipHBtn)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
306
|
+
if (this._flipHBtn)
|
|
307
|
+
this._flipHBtn.addEventListener('click', () => {
|
|
308
|
+
this._beginTransaction();
|
|
309
|
+
this.flipHorizontal();
|
|
310
|
+
this._endTransaction();
|
|
311
|
+
});
|
|
312
|
+
if (this._flipVBtn)
|
|
313
|
+
this._flipVBtn.addEventListener('click', () => {
|
|
314
|
+
this._beginTransaction();
|
|
315
|
+
this.flipVertical();
|
|
316
|
+
this._endTransaction();
|
|
317
|
+
});
|
|
318
|
+
if (this._rotCWBtn)
|
|
319
|
+
this._rotCWBtn.addEventListener('click', () => {
|
|
320
|
+
this._beginTransaction();
|
|
321
|
+
this.rotateCW();
|
|
322
|
+
this._endTransaction();
|
|
323
|
+
});
|
|
324
|
+
if (this._rotCCWBtn)
|
|
325
|
+
this._rotCCWBtn.addEventListener('click', () => {
|
|
326
|
+
this._beginTransaction();
|
|
327
|
+
this.rotateCCW();
|
|
328
|
+
this._endTransaction();
|
|
329
|
+
});
|
|
239
330
|
|
|
240
331
|
// clear button (makes canvas fully transparent)
|
|
241
|
-
if (this._clearBtn)
|
|
332
|
+
if (this._clearBtn)
|
|
333
|
+
this._clearBtn.addEventListener('click', () => {
|
|
334
|
+
this._beginTransaction();
|
|
335
|
+
this.clear([0, 0, 0, 0]);
|
|
336
|
+
this._endTransaction();
|
|
337
|
+
});
|
|
242
338
|
|
|
243
339
|
// Export/Import
|
|
244
340
|
this._exportBtn.addEventListener('click', () => this.exportPNG());
|
|
@@ -258,19 +354,31 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
258
354
|
if (!file) return;
|
|
259
355
|
const text = await file.text();
|
|
260
356
|
try {
|
|
357
|
+
this._beginTransaction();
|
|
261
358
|
this.importMatrixJSON(text);
|
|
359
|
+
this._endTransaction();
|
|
262
360
|
} catch (err) {
|
|
263
361
|
console.error(err);
|
|
264
362
|
alert('Invalid JSON');
|
|
265
363
|
}
|
|
266
364
|
});
|
|
267
365
|
|
|
366
|
+
// undo/redo
|
|
367
|
+
if (this._undoBtn) this._undoBtn.addEventListener('click', () => this.undo());
|
|
368
|
+
if (this._redoBtn) this._redoBtn.addEventListener('click', () => this.redo());
|
|
369
|
+
|
|
268
370
|
// Pointer events
|
|
269
371
|
this._pixelCanvas.addEventListener('pointerdown', this._onPointerDown);
|
|
270
372
|
window.addEventListener('pointermove', this._onPointerMove);
|
|
271
373
|
window.addEventListener('pointerup', this._onPointerUp);
|
|
272
374
|
|
|
375
|
+
// keyboard for undo/redo
|
|
376
|
+
window.addEventListener('keydown', this._onKeyDown);
|
|
377
|
+
|
|
378
|
+
// initial render and clear history
|
|
273
379
|
this.render();
|
|
380
|
+
this._clearHistory();
|
|
381
|
+
this._updateToolbarButtons();
|
|
274
382
|
}
|
|
275
383
|
|
|
276
384
|
disconnectedCallback() {
|
|
@@ -285,6 +393,11 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
285
393
|
if (this._clearBtn) this._clearBtn.removeEventListener('click', () => this.clear([0, 0, 0, 0]));
|
|
286
394
|
if (this._opacityRange) this._opacityRange.removeEventListener('input', () => {});
|
|
287
395
|
if (this._opacityNumber) this._opacityNumber.removeEventListener('change', () => {});
|
|
396
|
+
|
|
397
|
+
if (this._undoBtn) this._undoBtn.removeEventListener('click', () => this.undo());
|
|
398
|
+
if (this._redoBtn) this._redoBtn.removeEventListener('click', () => this.redo());
|
|
399
|
+
|
|
400
|
+
window.removeEventListener('keydown', this._onKeyDown);
|
|
288
401
|
}
|
|
289
402
|
|
|
290
403
|
// ---------------- Matrix helpers ----------------
|
|
@@ -498,6 +611,11 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
498
611
|
const a = typeof rgba[3] === 'number' ? this._clampInt(rgba[3]) : this._brushColor[3];
|
|
499
612
|
this._brushColor = [r, g, b, a];
|
|
500
613
|
if (this._colorInput) this._colorInput.value = this._rgbaToHex(this._brushColor);
|
|
614
|
+
if (this._hexInput) this._hexInput.value = this._rgbaToHexWithAlpha(this._brushColor);
|
|
615
|
+
if (this._rInput) this._rInput.value = String(this._brushColor[0]);
|
|
616
|
+
if (this._gInput) this._gInput.value = String(this._brushColor[1]);
|
|
617
|
+
if (this._bInput) this._bInput.value = String(this._brushColor[2]);
|
|
618
|
+
if (this._aInput) this._aInput.value = String(this._brushColor[3]);
|
|
501
619
|
if (this._opacityRange) this._opacityRange.value = String(this._brushColor[3]);
|
|
502
620
|
if (this._opacityNumber) this._opacityNumber.value = String(this._brushColor[3]);
|
|
503
621
|
}
|
|
@@ -508,8 +626,10 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
508
626
|
this._brushColor[3] = v;
|
|
509
627
|
if (this._opacityRange) this._opacityRange.value = String(v);
|
|
510
628
|
if (this._opacityNumber) this._opacityNumber.value = String(v);
|
|
629
|
+
if (this._aInput) this._aInput.value = String(v);
|
|
511
630
|
// keep color input (hex) representing rgb only
|
|
512
631
|
if (this._colorInput) this._colorInput.value = this._rgbaToHex(this._brushColor);
|
|
632
|
+
if (this._hexInput) this._hexInput.value = this._rgbaToHexWithAlpha(this._brushColor);
|
|
513
633
|
}
|
|
514
634
|
getBrushAlpha() {
|
|
515
635
|
return this._brushColor[3];
|
|
@@ -533,9 +653,13 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
533
653
|
|
|
534
654
|
fillBucket(x, y, targetColor = null) {
|
|
535
655
|
if (!this._inBounds(x, y)) return;
|
|
656
|
+
this._beginTransaction();
|
|
536
657
|
const src = this.getPixel(x, y);
|
|
537
658
|
const newColor = targetColor ? targetColor.slice() : this._brushColor.slice();
|
|
538
|
-
if (this._colorsEqual(src, newColor))
|
|
659
|
+
if (this._colorsEqual(src, newColor)) {
|
|
660
|
+
this._endTransaction();
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
539
663
|
const stack = [[x, y]];
|
|
540
664
|
while (stack.length) {
|
|
541
665
|
const [cx, cy] = stack.pop();
|
|
@@ -547,6 +671,7 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
547
671
|
}
|
|
548
672
|
this.render();
|
|
549
673
|
this.dispatchEvent(new CustomEvent('fill', { detail: { x, y } }));
|
|
674
|
+
this._endTransaction();
|
|
550
675
|
}
|
|
551
676
|
|
|
552
677
|
_colorsEqual(a, b) {
|
|
@@ -573,6 +698,8 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
573
698
|
this._pixelCanvas.setPointerCapture(evt.pointerId);
|
|
574
699
|
} catch (e) {}
|
|
575
700
|
const [x, y] = this._toGridCoords(evt);
|
|
701
|
+
// start transaction for continuous stroke
|
|
702
|
+
this._beginTransaction();
|
|
576
703
|
this._applyToolAt(x, y, evt);
|
|
577
704
|
}
|
|
578
705
|
_onPointerMove(evt) {
|
|
@@ -585,6 +712,8 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
585
712
|
try {
|
|
586
713
|
this._pixelCanvas.releasePointerCapture(evt.pointerId);
|
|
587
714
|
} catch (e) {}
|
|
715
|
+
// finish transaction for the stroke
|
|
716
|
+
this._endTransaction();
|
|
588
717
|
}
|
|
589
718
|
|
|
590
719
|
_applyToolAt(x, y, evt, continuous = false) {
|
|
@@ -613,6 +742,7 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
613
742
|
importMatrixJSON(json) {
|
|
614
743
|
const data = typeof json === 'string' ? JSON.parse(json) : json;
|
|
615
744
|
if (!data || !Array.isArray(data.matrix)) throw new TypeError('Invalid matrix JSON');
|
|
745
|
+
// wrap import as transactional change
|
|
616
746
|
this.loadMatrix(data.matrix);
|
|
617
747
|
}
|
|
618
748
|
|
|
@@ -712,6 +842,117 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
712
842
|
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
713
843
|
}
|
|
714
844
|
|
|
845
|
+
// ---------------- Undo/Redo helpers ----------------
|
|
846
|
+
_snapshot() {
|
|
847
|
+
return {
|
|
848
|
+
width: this._width,
|
|
849
|
+
height: this._height,
|
|
850
|
+
matrix: this._matrix.map((r) => r.map((c) => c.slice())),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
_loadSnapshot(snap) {
|
|
855
|
+
if (!snap) return;
|
|
856
|
+
this._width = snap.width;
|
|
857
|
+
this._height = snap.height;
|
|
858
|
+
this._matrix = snap.matrix.map((r) => r.map((c) => c.slice()));
|
|
859
|
+
if (this._widthInput) this._widthInput.value = String(this._width);
|
|
860
|
+
if (this._heightInput) this._heightInput.value = String(this._height);
|
|
861
|
+
this.setAttribute('width', String(this._width));
|
|
862
|
+
this.setAttribute('height', String(this._height));
|
|
863
|
+
this._setupContextsAndSize();
|
|
864
|
+
this.render();
|
|
865
|
+
this.dispatchEvent(new CustomEvent('matrixload', { detail: { width: this._width, height: this._height } }));
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
_matricesEqual(a, b) {
|
|
869
|
+
if (!a || !b) return false;
|
|
870
|
+
if (a.width !== b.width || a.height !== b.height) return false;
|
|
871
|
+
const h = a.height;
|
|
872
|
+
for (let y = 0; y < h; y++) {
|
|
873
|
+
for (let x = 0; x < a.width; x++) {
|
|
874
|
+
const ac = a.matrix[y][x];
|
|
875
|
+
const bc = b.matrix[y][x];
|
|
876
|
+
for (let i = 0; i < 4; i++) if (ac[i] !== bc[i]) return false;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return true;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
_pushUndo(snap) {
|
|
883
|
+
this._undoStack.push(snap);
|
|
884
|
+
if (this._undoStack.length > this._maxHistory) this._undoStack.shift();
|
|
885
|
+
// clear redo
|
|
886
|
+
this._redoStack.length = 0;
|
|
887
|
+
this._updateToolbarButtons();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
_clearHistory() {
|
|
891
|
+
this._undoStack.length = 0;
|
|
892
|
+
this._redoStack.length = 0;
|
|
893
|
+
this._updateToolbarButtons();
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
_beginTransaction() {
|
|
897
|
+
if (this._transactionActive) return;
|
|
898
|
+
this._transactionActive = true;
|
|
899
|
+
const before = this._snapshot();
|
|
900
|
+
this._pushUndo(before);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
_endTransaction() {
|
|
904
|
+
if (!this._transactionActive) return;
|
|
905
|
+
this._transactionActive = false;
|
|
906
|
+
// if the last undo state is identical to current (no-op), remove it
|
|
907
|
+
const last = this._undoStack[this._undoStack.length - 1];
|
|
908
|
+
const now = this._snapshot();
|
|
909
|
+
if (this._matricesEqual(last, now)) this._undoStack.pop();
|
|
910
|
+
this._updateToolbarButtons();
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
undo() {
|
|
914
|
+
if (!this._undoStack.length) return;
|
|
915
|
+
const snap = this._undoStack.pop();
|
|
916
|
+
// push current to redo
|
|
917
|
+
this._redoStack.push(this._snapshot());
|
|
918
|
+
this._loadSnapshot(snap);
|
|
919
|
+
this._updateToolbarButtons();
|
|
920
|
+
this.dispatchEvent(new CustomEvent('undo'));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
redo() {
|
|
924
|
+
if (!this._redoStack.length) return;
|
|
925
|
+
const snap = this._redoStack.pop();
|
|
926
|
+
// push current to undo
|
|
927
|
+
this._undoStack.push(this._snapshot());
|
|
928
|
+
this._loadSnapshot(snap);
|
|
929
|
+
this._updateToolbarButtons();
|
|
930
|
+
this.dispatchEvent(new CustomEvent('redo'));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
_updateToolbarButtons() {
|
|
934
|
+
if (this._undoBtn) this._undoBtn.disabled = this._undoStack.length === 0;
|
|
935
|
+
if (this._redoBtn) this._redoBtn.disabled = this._redoStack.length === 0;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
_onKeyDown(e) {
|
|
939
|
+
const meta = e.ctrlKey || e.metaKey;
|
|
940
|
+
if (!meta) return;
|
|
941
|
+
// ctrl/cmd+z -> undo, ctrl/cmd+shift+z or ctrl+ y -> redo
|
|
942
|
+
if (e.key === 'z' || e.key === 'Z') {
|
|
943
|
+
if (e.shiftKey) {
|
|
944
|
+
e.preventDefault();
|
|
945
|
+
this.redo();
|
|
946
|
+
} else {
|
|
947
|
+
e.preventDefault();
|
|
948
|
+
this.undo();
|
|
949
|
+
}
|
|
950
|
+
} else if (e.key === 'y' || e.key === 'Y') {
|
|
951
|
+
e.preventDefault();
|
|
952
|
+
this.redo();
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
715
956
|
// ---------------- Helpers ----------------
|
|
716
957
|
_hexToRgba(hex) {
|
|
717
958
|
const h = (hex || '').replace('#', '');
|
|
@@ -730,6 +971,48 @@ class ObjectLayerEngineElement extends HTMLElement {
|
|
|
730
971
|
.slice(1)}`;
|
|
731
972
|
}
|
|
732
973
|
|
|
974
|
+
// convert RGBA to hex with alpha channel
|
|
975
|
+
_rgbaToHexWithAlpha(rgba) {
|
|
976
|
+
const [r, g, b, a] = rgba;
|
|
977
|
+
const rHex = this._clampInt(r).toString(16).padStart(2, '0');
|
|
978
|
+
const gHex = this._clampInt(g).toString(16).padStart(2, '0');
|
|
979
|
+
const bHex = this._clampInt(b).toString(16).padStart(2, '0');
|
|
980
|
+
const aHex = this._clampInt(a).toString(16).padStart(2, '0');
|
|
981
|
+
return `#${rHex}${gHex}${bHex}${aHex}`.toUpperCase();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// convert hex to RGBA with optional alpha channel support
|
|
985
|
+
_hexToRgbaWithAlpha(hex) {
|
|
986
|
+
const h = (hex || '').replace('#', '');
|
|
987
|
+
if (h.length === 3) {
|
|
988
|
+
// #RGB -> expand to RRGGBB
|
|
989
|
+
return [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16), 255];
|
|
990
|
+
}
|
|
991
|
+
if (h.length === 4) {
|
|
992
|
+
// #RGBA -> expand to RRGGBBAA
|
|
993
|
+
return [
|
|
994
|
+
parseInt(h[0] + h[0], 16),
|
|
995
|
+
parseInt(h[1] + h[1], 16),
|
|
996
|
+
parseInt(h[2] + h[2], 16),
|
|
997
|
+
parseInt(h[3] + h[3], 16),
|
|
998
|
+
];
|
|
999
|
+
}
|
|
1000
|
+
if (h.length === 6) {
|
|
1001
|
+
// #RRGGBB
|
|
1002
|
+
return [parseInt(h.substring(0, 2), 16), parseInt(h.substring(2, 4), 16), parseInt(h.substring(4, 6), 16), 255];
|
|
1003
|
+
}
|
|
1004
|
+
if (h.length === 8) {
|
|
1005
|
+
// #RRGGBBAA
|
|
1006
|
+
return [
|
|
1007
|
+
parseInt(h.substring(0, 2), 16),
|
|
1008
|
+
parseInt(h.substring(2, 4), 16),
|
|
1009
|
+
parseInt(h.substring(4, 6), 16),
|
|
1010
|
+
parseInt(h.substring(6, 8), 16),
|
|
1011
|
+
];
|
|
1012
|
+
}
|
|
1013
|
+
return [0, 0, 0, 255];
|
|
1014
|
+
}
|
|
1015
|
+
|
|
733
1016
|
// ---------------- Transform helpers (flip/rotate) ----------------
|
|
734
1017
|
flipHorizontal() {
|
|
735
1018
|
// reverse each row (mirror horizontally)
|
|
@@ -869,7 +1152,12 @@ class ObjectLayerPngLoader extends HTMLElement {
|
|
|
869
1152
|
<style>
|
|
870
1153
|
:host {
|
|
871
1154
|
display: block;
|
|
872
|
-
font-family:
|
|
1155
|
+
font-family:
|
|
1156
|
+
system-ui,
|
|
1157
|
+
-apple-system,
|
|
1158
|
+
'Segoe UI',
|
|
1159
|
+
Roboto,
|
|
1160
|
+
Arial;
|
|
873
1161
|
}
|
|
874
1162
|
.wrap {
|
|
875
1163
|
display: flex;
|
|
@@ -1045,7 +1333,10 @@ class ObjectLayerPngLoader extends HTMLElement {
|
|
|
1045
1333
|
}
|
|
1046
1334
|
|
|
1047
1335
|
_showError(msg) {
|
|
1048
|
-
|
|
1336
|
+
NotificationManager.Push({
|
|
1337
|
+
status: 'error',
|
|
1338
|
+
html: msg,
|
|
1339
|
+
});
|
|
1049
1340
|
}
|
|
1050
1341
|
_dispatchLoadedEvent(filename) {
|
|
1051
1342
|
this.dispatchEvent(new CustomEvent('pngloaded', { detail: { filename } }));
|