@pictogrammers/components 0.5.14 → 0.5.17
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/package.json +1 -1
- package/pg/autocompleteItem/__examples__/basic/basic.html +3 -0
- package/pg/autocompleteItem/autocompleteItem.css +55 -0
- package/pg/autocompleteItem/autocompleteItem.html +1 -0
- package/pg/autocompleteItem/autocompleteItem.ts +65 -0
- package/pg/button/button.css +5 -0
- package/pg/buttonMenu/buttonMenu.ts +1 -1
- package/pg/inputCombobox/__examples__/basic/basic.ts +33 -0
- package/pg/inputCombobox/inputCombobox.ts +158 -0
- package/pg/inputCombobox/inputCombobox.ts.tmp.44252.751480cb81cc +158 -0
- package/pg/inputCombobox/inputCombobox.ts.tmp.44252.dcea847c40a8 +156 -0
- package/pg/inputNumber/inputNumber.ts +31 -13
- package/pg/inputPill/inputPill.html +6 -0
- package/pg/inputPill/inputPill.html.tmp.26852.84ac63f4c46f +6 -0
- package/pg/inputPill/inputPill.ts +41 -0
- package/pg/inputPills/__examples__/basic/basic.html +9 -0
- package/pg/inputPills/__examples__/basic/basic.html.tmp.26852.4771e4cad5b7 +9 -0
- package/pg/inputPills/__examples__/basic/basic.ts.tmp.26852.d8fe41df18bd +0 -0
- package/pg/inputPixelEditor/README.md +104 -69
- package/pg/inputPixelEditor/README.md.tmp.42596.658de5f0a6f5 +0 -0
- package/pg/inputPixelEditor/__examples__/basic/basic.ts +6 -5
- package/pg/inputPixelEditor/inputPixelEditor.ts +95 -23
- package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.38516.96529dd82670 +1703 -0
- package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.42596.48ecc3ad9c72 +1724 -0
- package/pg/inputPixelEditor/utils/bitmapToMask.ts +113 -79
- package/pg/inputPixelEditor/utils/getLassoPixels.ts +51 -0
- package/pg/inputRange/inputRange.css +7 -0
- package/pg/inputSelect/__examples__/basic/basic.ts +2 -2
- package/pg/inputSelect/inputSelect.css +1 -1
- package/pg/inputSelect/inputSelect.ts +62 -5
- package/pg/inputText/README.md +6 -6
- package/pg/inputText/__examples__/basic/basic.ts +1 -1
- package/pg/inputText/inputText.css +2 -2
- package/pg/inputText copy/README.md +38 -0
- package/pg/inputText copy/inputText.css +41 -0
- package/pg/inputText copy/inputText.html +1 -0
- package/pg/inputText copy/inputText.spec.ts +59 -0
- package/pg/inputText copy/inputText.ts +67 -0
- package/pg/inputTextarea/README.md +39 -0
- package/pg/inputTextarea/__examples__/basic/basic.html +9 -0
- package/pg/inputTextarea/__examples__/basic/basic.ts +30 -0
- package/pg/inputTextarea/inputTextarea.ts +121 -0
- package/pg/inputTextarea copy/README.md +39 -0
- package/pg/inputTextarea copy/inputTextarea.css +41 -0
- package/pg/inputTextarea copy/inputTextarea.html +1 -0
- package/pg/inputTextarea copy/inputTextarea.spec.ts +59 -0
- package/pg/inputTextarea copy/inputTextarea.ts +79 -0
- package/pg/inputTextareaAutocomplete/README.md +70 -0
- package/pg/inputTextareaAutocomplete/__examples__/basic/basic.ts +67 -0
- package/pg/inputTextareaAutocomplete/inputTextareaAutocomplete.ts +267 -0
- package/pg/node/README.md +35 -0
- package/pg/node/__examples__/basic/basic.ts +34 -0
- package/pg/node/index.ts +3 -0
- package/pg/node/node.css +53 -0
- package/pg/node/node.html +5 -0
- package/pg/node/node.ts +270 -0
- package/pg/nodeEditorLink/nodeEditorLink.css +69 -0
- package/pg/nodeEditorLink/nodeEditorLink.css.tmp.1408.09e961fd0dcc +69 -0
- package/pg/nodeEditorLink/nodeEditorLink.html +9 -0
- package/pg/nodeEditorNumber/nodeEditorNumber.css +59 -0
- package/pg/nodeEditorNumber/nodeEditorNumber.ts +57 -0
- package/pg/nodeEditorRange/nodeEditorRange.css +23 -0
- package/pg/nodeEditorRange/nodeEditorRange.ts +69 -0
- package/pg/nodeEditorText/__examples__/basic/basic.html +9 -0
- package/pg/nodeEditorText/__examples__/basic/basic.ts +30 -0
- package/pg/nodeEditorText/nodeEditorText.css +17 -0
- package/pg/nodeEditorText/nodeEditorText.html +4 -0
- package/pg/nodeEditorText/nodeEditorText.ts +57 -0
- package/pg/nodeEditorText copy/README.md +35 -0
- package/pg/nodeEditorText copy/nodeEditorText.css +49 -0
- package/pg/nodeEditorText copy/nodeEditorText.html +4 -0
- package/pg/nodeEditorText copy/nodeEditorText.ts +50 -0
- package/pg/nodeEditorTextArray/nodeEditorTextArray.css +68 -0
- package/pg/nodeEditorTextArray/nodeEditorTextArray.html +9 -0
- package/pg/nodeEditorTextArray/nodeEditorTextArray.ts +162 -0
- package/pg/nodeEntry/nodeEntry.css +61 -0
- package/pg/nodeEntry/nodeEntry.ts +164 -0
- package/pg/nodeEntry/nodeEntry.ts.tmp.28724.dda67762bca5 +164 -0
- package/pg/nodeInputText/nodeInputText.css +75 -0
- package/pg/nodeInputText/nodeInputText.html +4 -0
- package/pg/nodeInputText/nodeInputText.ts +105 -0
- package/pg/nodeOutput/nodeOutput.css +32 -0
- package/pg/nodeResize/README.md +3 -0
- package/pg/nodeResize/THINKING.md +81 -0
- package/pg/nodeResize/THINKING.md.tmp.24812.99a70ba81062 +81 -0
- package/pg/nodeResize/basic/basic.ts +17 -0
- package/pg/nodeResize/dragUtil.ts +105 -0
- package/pg/nodeResize/dragUtil.ts.tmp.34372.787464c73309 +75 -0
- package/pg/nodeResize/nodeResize.css +187 -0
- package/pg/nodeResize/nodeResize.html +9 -0
- package/pg/nodeResize/nodeResize.ts +156 -0
- package/pg/nodes/README.md +177 -0
- package/pg/nodes/__examples__/basic/basic.html +33 -0
- package/pg/nodes/__examples__/basic/basic.ts +716 -0
- package/pg/nodes/__examples__/basic/exampleNodes.ts +129 -0
- package/pg/nodes/__examples__/basic/exampleScripts.ts +129 -0
- package/pg/nodes/__examples__/basic/keystore.ts +38 -0
- package/pg/nodes/executeInSandbox.ts +32 -0
- package/pg/nodes/index.ts +3 -0
- package/pg/nodes/nodes.css +69 -0
- package/pg/nodes/nodes.html +6 -0
- package/pg/nodes/nodes.ts +1240 -0
- package/pg/nodes copy/nodes.css +41 -0
- package/pg/nodes copy/nodes.html +1 -0
- package/pg/overlayContextMenu/__examples__/filter/filter.ts +87 -0
- package/pg/overlayContextMenu/overlayContextMenu.css +14 -0
- package/pg/overlayContextMenu/overlayContextMenu.html +4 -3
- package/pg/overlayContextMenu/overlayContextMenu.ts +6 -2
- package/pg/overlayContextMenu/overlayContextMenu.ts.tmp.37088.530ff1c0a321 +0 -0
- package/pg/overlayMenu/overlayMenu.css +3 -1
- package/pg/overlayMenu/overlayMenu.ts +11 -2
- package/pg/overlayMenu/overlayMenu.ts.tmp.44252.9bb22fd59ede +80 -0
- package/pg/overlaySelectMenu/overlaySelectMenu.css +2 -0
- package/pg/overlaySubMenu/overlaySubMenu.css +8 -6
- package/pg/poco/README.md +22 -0
- package/pg/poco/__examples__/basic/basic.html +11 -0
- package/pg/poco/__examples__/basic/basic.ts +95 -0
- package/pg/poco/__examples__/basic/examples.ts +258 -0
- package/pg/poco/__examples__/basic/myFont.ts +1300 -0
- package/pg/poco/index.ts +3 -0
- package/pg/poco/mockPoco.ts +640 -0
- package/pg/poco/poco.css +7 -0
- package/pg/poco/poco.html +1 -0
- package/pg/poco/poco.ts +75 -0
- package/pg/popover/README.md +47 -0
- package/pg/popover/__examples__/basic/basic.html +3 -0
- package/pg/popover/popover.html +3 -0
- package/pg/popover/popover.ts +44 -0
- package/pg/popoverAutocomplete/__examples__/basic/basic.html +4 -0
- package/pg/popoverAutocomplete/__examples__/basic/basic.ts +46 -0
- package/pg/popoverAutocomplete/popoverAutocomplete.css +25 -0
- package/pg/popoverAutocomplete/popoverAutocomplete.html +1 -0
- package/pg/popoverAutocomplete/popoverAutocomplete.ts +134 -0
- package/pg/popoverAutocompleteItem/popoverAutocompleteItem.css +69 -0
- package/pg/popoverAutocompleteItem/popoverAutocompleteItem.ts +90 -0
- package/pg/table/__examples__/basic/basic.ts +1 -0
- package/pg/tableCellFileLocal/tableCellFileLocal.html +3 -0
- package/pg/tableCellFileLocal/tableCellFileLocal.html.tmp.20484.68ee2e30710e +3 -0
- package/pg/tableCellFileLocal/tableCellFileLocal.ts +44 -0
- package/pg/tableCellSelect/tableCellSelect.html +3 -0
- package/pg/tableCellSelect/tableCellSelect.html.tmp.41808.99c1a6f14340 +3 -0
- package/pg/tableCellSelect/tableCellSelect.ts +51 -0
- package/pg/tree/__examples__/basic/basic.ts +12 -5
- package/pg/tree/tree.html +1 -1
- package/pg/tree/tree.ts +385 -227
- package/pg/tree/tree.ts.tmp.38376.07d376a04f41 +343 -0
- package/pg/tree/tree.ts.tmp.38376.2a04599ade9f +361 -0
- package/pg/tree/tree.ts.tmp.38376.6cb2c663d476 +0 -0
- package/pg/tree/tree.ts.tmp.38376.ac5af1aff2b0 +393 -0
- package/pg/tree/tree.ts.tmp.38376.ac9acfc4f95f +324 -0
- package/pg/treeItem/treeItem.ts +536 -558
- package/pg/treeItem/treeItem.ts.tmp.38376.145d3121d648 +530 -0
- package/pg/treeItem/treeItem.ts.tmp.38376.b5224d48e5c0 +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Component, Prop, Part } from '@pictogrammers/element';
|
|
2
|
+
|
|
3
|
+
import template from './nodeInputText.html';
|
|
4
|
+
import style from './nodeInputText.css';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'pg-node-input-text',
|
|
8
|
+
style,
|
|
9
|
+
template,
|
|
10
|
+
})
|
|
11
|
+
export default class PgNodeInputText extends HTMLElement {
|
|
12
|
+
|
|
13
|
+
@Prop() index: number;
|
|
14
|
+
@Prop() value: string = '';
|
|
15
|
+
@Prop() name: string = '';
|
|
16
|
+
@Prop() removable: boolean = false;
|
|
17
|
+
|
|
18
|
+
@Part() $input: HTMLInputElement;
|
|
19
|
+
@Part() $remove: HTMLButtonElement;
|
|
20
|
+
|
|
21
|
+
connectedCallback() {
|
|
22
|
+
this.$input.addEventListener('change', (e: any) => {
|
|
23
|
+
e.stopPropagation();
|
|
24
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
25
|
+
detail: {
|
|
26
|
+
index: this.index,
|
|
27
|
+
value: this.$input.value,
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
});
|
|
31
|
+
this.$input.addEventListener('input', (e: any) => {
|
|
32
|
+
e.stopPropagation();
|
|
33
|
+
this.dispatchEvent(new CustomEvent('input', {
|
|
34
|
+
detail: {
|
|
35
|
+
index: this.index,
|
|
36
|
+
value: this.$input.value,
|
|
37
|
+
}
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
40
|
+
this.$input.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
41
|
+
if (e.key === 'ArrowUp') {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
this.dispatchEvent(new CustomEvent('inputprevious', {
|
|
44
|
+
detail: {
|
|
45
|
+
index: this.index,
|
|
46
|
+
selectionIndex: this.$input.selectionDirection === 'forward'
|
|
47
|
+
? this.$input.selectionStart
|
|
48
|
+
: this.$input.selectionEnd,
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
} else if (e.key === 'ArrowDown') {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
this.dispatchEvent(new CustomEvent('inputnext', {
|
|
54
|
+
detail: {
|
|
55
|
+
index: this.index,
|
|
56
|
+
selectionIndex: this.$input.selectionDirection === 'forward'
|
|
57
|
+
? this.$input.selectionStart
|
|
58
|
+
: this.$input.selectionEnd,
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
} else if (e.key === 'Enter' && this.$input.value !== '') {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
this.dispatchEvent(new CustomEvent('enter', {
|
|
64
|
+
detail: {
|
|
65
|
+
index: this.index,
|
|
66
|
+
}
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
this.$remove.addEventListener('click', (e: any) => {
|
|
71
|
+
this.dispatchEvent(new CustomEvent('remove', {
|
|
72
|
+
detail: {
|
|
73
|
+
index: this.index,
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
render(changes: any) {
|
|
80
|
+
if (changes.value) {
|
|
81
|
+
this.$input.value = this.value;
|
|
82
|
+
}
|
|
83
|
+
if (changes.removable) {
|
|
84
|
+
this.$remove.classList.toggle('hide', !this.removable);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
focus() {
|
|
89
|
+
this.$input.focus();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get selectionStart(){
|
|
93
|
+
return this.$input.selectionStart || 0;
|
|
94
|
+
}
|
|
95
|
+
set selectionStart(value: number) {
|
|
96
|
+
this.$input.selectionStart = value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get selectionEnd(){
|
|
100
|
+
return this.$input.selectionEnd || 0;
|
|
101
|
+
}
|
|
102
|
+
set selectionEnd(value: number) {
|
|
103
|
+
this.$input.selectionEnd = value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
font-family: var(--pg-font-family, system-ui);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
label {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
[part=label] {
|
|
12
|
+
position: relative;
|
|
13
|
+
display: flex;
|
|
14
|
+
font-size: 0.75rem;
|
|
15
|
+
line-height: 1rem;
|
|
16
|
+
padding-right: 1.125rem;
|
|
17
|
+
color: rgba(255, 255, 255, 0.8);
|
|
18
|
+
justify-content: right;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
[part=label]::after {
|
|
22
|
+
position: absolute;
|
|
23
|
+
content: ' ';
|
|
24
|
+
width: 0.25rem;
|
|
25
|
+
height: 0.25rem;
|
|
26
|
+
transform: rotate(45deg);
|
|
27
|
+
border-top: 2px solid rgba(255, 255, 255, 0.5);
|
|
28
|
+
border-right: 2px solid rgba(255, 255, 255, 0.5);
|
|
29
|
+
border-top-right-radius: 0.125rem;
|
|
30
|
+
right: 0.5rem;
|
|
31
|
+
top: 0.4rem;
|
|
32
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Preview Sub-Grid Offset Bug
|
|
2
|
+
|
|
3
|
+
## The Problem
|
|
4
|
+
|
|
5
|
+
When `snap` fires it updates the node's x/y/width/height via `emit`. That moves the CSS
|
|
6
|
+
anchor. The preview CSS vars must then represent the **remaining sub-grid offset** so the
|
|
7
|
+
overlay tracks the cursor smoothly. If the formula is wrong, the overlay jumps by ~gridSize
|
|
8
|
+
pixels at each snap boundary.
|
|
9
|
+
|
|
10
|
+
## dragUtil snap algorithm (asymmetric — the root bug)
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
snapDx = (lastDx + half < 0)
|
|
14
|
+
? Math.ceil((lastDx + half) / gridSize) // negative direction: ceil
|
|
15
|
+
: Math.floor((lastDx + half) / gridSize) // positive direction: floor
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Because of ceil vs floor the first snap threshold is **asymmetric**:
|
|
19
|
+
- Positive drag: first snap at +8 px (half grid unit) ← works correctly (south)
|
|
20
|
+
- Negative drag: first snap at -24 px (1.5 grid units) ← broken (north)
|
|
21
|
+
|
|
22
|
+
Negative example step-by-step with gridSize=16, half=8:
|
|
23
|
+
dy=-8: (-8+8)/16 = 0/16 = 0, ceil(0) = 0 → no snap yet
|
|
24
|
+
dy=-16: (-16+8)/16 = -8/16 = -0.5, ceil(-0.5) = 0 → still no snap
|
|
25
|
+
dy=-24: (-24+8)/16 = -16/16 = -1, ceil(-1) = -1 → FIRST snap (at 1.5 grid units!)
|
|
26
|
+
|
|
27
|
+
**Fix: use Math.floor for ALL values** — this rounds toward −∞ symmetrically:
|
|
28
|
+
dy=+8: floor((8+8)/16) = floor(1) = 1 → snap ✓
|
|
29
|
+
dy=-8: floor((-8+8)/16) = floor(0) = 0 → no snap yet
|
|
30
|
+
dy=-9: floor((-9+8)/16) = floor(-0.0625)= -1 → snap (≈half grid unit) ✓
|
|
31
|
+
|
|
32
|
+
## Correct sub-grid formula
|
|
33
|
+
|
|
34
|
+
The correct sub-grid residual (pixels past the last snap) must mirror the same algorithm:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
subGrid(delta) = delta - (
|
|
38
|
+
delta + half < 0
|
|
39
|
+
? Math.ceil((delta + half) / gridSize) * gridSize
|
|
40
|
+
: Math.floor((delta + half) / gridSize) * gridSize
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Why the existing formulas fail
|
|
45
|
+
|
|
46
|
+
`delta % gridSize` (used by previewX/Y/Width):
|
|
47
|
+
- delta = -20 → -20 % 16 = -4 (JS truncated modulo)
|
|
48
|
+
- correct: no snap at -20 (threshold is -24), so sub-grid = -20
|
|
49
|
+
- WRONG by 16 px
|
|
50
|
+
|
|
51
|
+
`((delta + half) % gridSize) - half` (used by previewHeight):
|
|
52
|
+
- delta = 9 → ((17) % 16) - 8 = -7 ✓ (correct, snap fired at +8)
|
|
53
|
+
- delta = -20 → ((-12) % 16) - 8 = -12 - 8 = -20 ✓ (correct, no snap yet)
|
|
54
|
+
- delta = -9 → ((-1) % 16) - 8 = -1 - 8 = -9
|
|
55
|
+
- correct: snap fires at -24, so sub-grid = -9 ✓ happens to be right
|
|
56
|
+
|
|
57
|
+
## Why the west/north handles jump but south mostly does not
|
|
58
|
+
|
|
59
|
+
For south (hDir=1), dy is positive when the height grows (normal use). The previewHeight
|
|
60
|
+
formula is correct for positive dy. Negative dy (shrinking the south edge) rarely reaches
|
|
61
|
+
the problematic range.
|
|
62
|
+
|
|
63
|
+
For north (hDir=-1), the previewY/previewWidth/previewX use `% gridSize` which is
|
|
64
|
+
wrong in the range (-24, -16) for dx/dy values.
|
|
65
|
+
|
|
66
|
+
## Critical insight about width/x coupling
|
|
67
|
+
|
|
68
|
+
For a west handle (wDir=-1), dx is the raw pointer delta:
|
|
69
|
+
- x sub-grid = subGrid(dx)
|
|
70
|
+
- width sub-grid = -subGrid(dx) ← NOT subGrid(-dx)
|
|
71
|
+
|
|
72
|
+
`subGrid` is NOT an odd function (asymmetric snap means subGrid(-dx) ≠ -subGrid(dx)).
|
|
73
|
+
So calling previewWidth(-dx) with an internal formula on -dx is wrong.
|
|
74
|
+
|
|
75
|
+
## Fix
|
|
76
|
+
|
|
77
|
+
Compute `sgX = subGrid(dx)` and `sgY = subGrid(dy)` once per `move` event, then:
|
|
78
|
+
- delta-x = sgX (when wDir == -1)
|
|
79
|
+
- delta-width = wDir * sgX (when wDir != 0)
|
|
80
|
+
- delta-y = sgY (when hDir == -1)
|
|
81
|
+
- delta-height = hDir * sgY (when hDir != 0)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Preview Sub-Grid Offset Bug
|
|
2
|
+
|
|
3
|
+
## The Problem
|
|
4
|
+
|
|
5
|
+
When `snap` fires it updates the node's x/y/width/height via `emit`. That moves the CSS
|
|
6
|
+
anchor. The preview CSS vars must then represent the **remaining sub-grid offset** so the
|
|
7
|
+
overlay tracks the cursor smoothly. If the formula is wrong, the overlay jumps by ~gridSize
|
|
8
|
+
pixels at each snap boundary.
|
|
9
|
+
|
|
10
|
+
## dragUtil snap algorithm (asymmetric — the root bug)
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
snapDx = (lastDx + half < 0)
|
|
14
|
+
? Math.ceil((lastDx + half) / gridSize) // negative direction: ceil
|
|
15
|
+
: Math.floor((lastDx + half) / gridSize) // positive direction: floor
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Because of ceil vs floor the first snap threshold is **asymmetric**:
|
|
19
|
+
- Positive drag: first snap at +8 px (half grid unit) ← works correctly (south)
|
|
20
|
+
- Negative drag: first snap at -24 px (1.5 grid units) ← broken (north)
|
|
21
|
+
|
|
22
|
+
Negative example step-by-step with gridSize=16, half=8:
|
|
23
|
+
dy=-8: (-8+8)/16 = 0/16 = 0, ceil(0) = 0 → no snap yet
|
|
24
|
+
dy=-16: (-16+8)/16 = -8/16 = -0.5, ceil(-0.5) = 0 → still no snap
|
|
25
|
+
dy=-24: (-24+8)/16 = -16/16 = -1, ceil(-1) = -1 → FIRST snap (at 1.5 grid units!)
|
|
26
|
+
|
|
27
|
+
**Fix: use Math.floor for ALL values** — this rounds toward −∞ symmetrically:
|
|
28
|
+
dy=+8: floor((8+8)/16) = floor(1) = 1 → snap ✓
|
|
29
|
+
dy=-8: floor((-8+8)/16) = floor(0) = 0 → no snap yet
|
|
30
|
+
dy=-9: floor((-9+8)/16) = floor(-0.0625)= -1 → snap (≈half grid unit) ✓
|
|
31
|
+
|
|
32
|
+
## Correct sub-grid formula
|
|
33
|
+
|
|
34
|
+
The correct sub-grid residual (pixels past the last snap) must mirror the same algorithm:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
subGrid(delta) = delta - (
|
|
38
|
+
delta + half < 0
|
|
39
|
+
? Math.ceil((delta + half) / gridSize) * gridSize
|
|
40
|
+
: Math.floor((delta + half) / gridSize) * gridSize
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Why the existing formulas fail
|
|
45
|
+
|
|
46
|
+
`delta % gridSize` (used by previewX/Y/Width):
|
|
47
|
+
- delta = -20 → -20 % 16 = -4 (JS truncated modulo)
|
|
48
|
+
- correct: no snap at -20 (threshold is -24), so sub-grid = -20
|
|
49
|
+
- WRONG by 16 px
|
|
50
|
+
|
|
51
|
+
`((delta + half) % gridSize) - half` (used by previewHeight):
|
|
52
|
+
- delta = 9 → ((17) % 16) - 8 = -7 ✓ (correct, snap fired at +8)
|
|
53
|
+
- delta = -20 → ((-12) % 16) - 8 = -12 - 8 = -20 ✓ (correct, no snap yet)
|
|
54
|
+
- delta = -9 → ((-1) % 16) - 8 = -1 - 8 = -9
|
|
55
|
+
- correct: snap fires at -24, so sub-grid = -9 ✓ happens to be right
|
|
56
|
+
|
|
57
|
+
## Why the west/north handles jump but south mostly does not
|
|
58
|
+
|
|
59
|
+
For south (hDir=1), dy is positive when the height grows (normal use). The previewHeight
|
|
60
|
+
formula is correct for positive dy. Negative dy (shrinking the south edge) rarely reaches
|
|
61
|
+
the problematic range.
|
|
62
|
+
|
|
63
|
+
For north (hDir=-1), the previewY/previewWidth/previewX use `% gridSize` which is
|
|
64
|
+
wrong in the range (-24, -16) for dx/dy values.
|
|
65
|
+
|
|
66
|
+
## Critical insight about width/x coupling
|
|
67
|
+
|
|
68
|
+
For a west handle (wDir=-1), dx is the raw pointer delta:
|
|
69
|
+
- x sub-grid = subGrid(dx)
|
|
70
|
+
- width sub-grid = -subGrid(dx) ← NOT subGrid(-dx)
|
|
71
|
+
|
|
72
|
+
`subGrid` is NOT an odd function (asymmetric snap means subGrid(-dx) ≠ -subGrid(dx)).
|
|
73
|
+
So calling previewWidth(-dx) with an internal formula on -dx is wrong.
|
|
74
|
+
|
|
75
|
+
## Fix
|
|
76
|
+
|
|
77
|
+
Compute `sgX = subGrid(dx)` and `sgY = subGrid(dy)` once per `move` event, then:
|
|
78
|
+
- delta-x = sgX (when wDir == -1)
|
|
79
|
+
- delta-width = wDir * sgX (when wDir != 0)
|
|
80
|
+
- delta-y = sgY (when hDir == -1)
|
|
81
|
+
- delta-height = hDir * sgY (when hDir != 0)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Component, Part } from '@pictogrammers/element';
|
|
2
|
+
import PgNodeResize from '../nodeResize';
|
|
3
|
+
|
|
4
|
+
import template from './basic.html';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'x-pg-node-resize-basic',
|
|
8
|
+
template,
|
|
9
|
+
})
|
|
10
|
+
export default class XPgNodeResizeBasic extends HTMLElement {
|
|
11
|
+
|
|
12
|
+
@Part() $resize: PgNodeResize;
|
|
13
|
+
|
|
14
|
+
connectedCallback() {
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
interface DragConfig {
|
|
2
|
+
source: HTMLElement;
|
|
3
|
+
gridSize: number;
|
|
4
|
+
start?: (dx: number, dy: number) => void;
|
|
5
|
+
move?: (dx: number, dy: number) => void;
|
|
6
|
+
snap?: (dx: number, dy: number) => void;
|
|
7
|
+
end?: (dx: number, dy: number, complete: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function drag({ source, gridSize, start, move, snap, end }: DragConfig): () => void {
|
|
11
|
+
let startX = 0;
|
|
12
|
+
let startY = 0;
|
|
13
|
+
let lastDx = 0;
|
|
14
|
+
let lastDy = 0;
|
|
15
|
+
let lastSnapDx = 0;
|
|
16
|
+
let lastSnapDy = 0;
|
|
17
|
+
let activePointerId: number | null = null;
|
|
18
|
+
const halfGridSize = gridSize / 2;
|
|
19
|
+
|
|
20
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
startX = e.clientX;
|
|
23
|
+
startY = e.clientY;
|
|
24
|
+
lastDx = 0;
|
|
25
|
+
lastDy = 0;
|
|
26
|
+
lastSnapDx = 0;
|
|
27
|
+
lastSnapDy = 0;
|
|
28
|
+
activePointerId = e.pointerId;
|
|
29
|
+
source.setPointerCapture(e.pointerId);
|
|
30
|
+
source.addEventListener('pointermove', onPointerMove);
|
|
31
|
+
source.addEventListener('pointerup', onPointerUp);
|
|
32
|
+
source.addEventListener('pointercancel', onPointerCancel);
|
|
33
|
+
document.addEventListener('keydown', onKeyDown);
|
|
34
|
+
start?.(0, 0);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
38
|
+
lastDx = e.clientX - startX;
|
|
39
|
+
lastDy = e.clientY - startY;
|
|
40
|
+
move?.(lastDx, lastDy);
|
|
41
|
+
const snapDx = lastDx + halfGridSize < 0
|
|
42
|
+
? Math.ceil((lastDx + halfGridSize) / gridSize)
|
|
43
|
+
: Math.floor((lastDx + halfGridSize) / gridSize);
|
|
44
|
+
const snapDy = lastDy + halfGridSize < 0
|
|
45
|
+
? Math.ceil((lastDy + halfGridSize) / gridSize)
|
|
46
|
+
: Math.floor((lastDy + halfGridSize) / gridSize);
|
|
47
|
+
if (snapDx !== lastSnapDx || snapDy !== lastSnapDy) {
|
|
48
|
+
lastSnapDx = snapDx;
|
|
49
|
+
lastSnapDy = snapDy;
|
|
50
|
+
snap?.(snapDx, snapDy);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const onPointerUp = (e: PointerEvent) => {
|
|
55
|
+
const dx = e.clientX + halfGridSize - startX < 0
|
|
56
|
+
? Math.ceil((e.clientX + halfGridSize - startX) / gridSize)
|
|
57
|
+
: Math.floor((e.clientX + halfGridSize - startX) / gridSize);
|
|
58
|
+
const dy = e.clientY + halfGridSize - startY < 0
|
|
59
|
+
? Math.ceil((e.clientY + halfGridSize - startY) / gridSize)
|
|
60
|
+
: Math.floor((e.clientY + halfGridSize - startY) / gridSize);
|
|
61
|
+
stopDrag();
|
|
62
|
+
end?.(dx, dy, true);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const onPointerCancel = () => {
|
|
66
|
+
const dx = lastDx < 0
|
|
67
|
+
? Math.ceil((lastDx + halfGridSize) / gridSize)
|
|
68
|
+
: Math.floor((lastDx + halfGridSize) / gridSize);
|
|
69
|
+
const dy = lastDy < 0
|
|
70
|
+
? Math.ceil((lastDy + halfGridSize) / gridSize)
|
|
71
|
+
: Math.floor((lastDy + halfGridSize) / gridSize);
|
|
72
|
+
stopDrag();
|
|
73
|
+
end?.(dx, dy, false);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
77
|
+
if (e.key !== 'Escape') { return; }
|
|
78
|
+
const dx = lastDx < 0
|
|
79
|
+
? Math.ceil((lastDx + halfGridSize) / gridSize)
|
|
80
|
+
: Math.floor((lastDx + halfGridSize) / gridSize);
|
|
81
|
+
const dy = lastDy < 0
|
|
82
|
+
? Math.ceil((lastDy + halfGridSize) / gridSize)
|
|
83
|
+
: Math.floor((lastDy + halfGridSize) / gridSize);
|
|
84
|
+
if (activePointerId !== null) {
|
|
85
|
+
source.releasePointerCapture(activePointerId);
|
|
86
|
+
}
|
|
87
|
+
stopDrag();
|
|
88
|
+
end?.(dx, dy, false);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const stopDrag = () => {
|
|
92
|
+
activePointerId = null;
|
|
93
|
+
source.removeEventListener('pointermove', onPointerMove);
|
|
94
|
+
source.removeEventListener('pointerup', onPointerUp);
|
|
95
|
+
source.removeEventListener('pointercancel', onPointerCancel);
|
|
96
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
source.addEventListener('pointerdown', onPointerDown);
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
source.removeEventListener('pointerdown', onPointerDown);
|
|
103
|
+
stopDrag();
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
interface DragConfig {
|
|
2
|
+
source: HTMLElement;
|
|
3
|
+
start?: (dx: number, dy: number) => void;
|
|
4
|
+
move?: (dx: number, dy: number) => void;
|
|
5
|
+
end?: (dx: number, dy: number, complete: boolean) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function drag({ source, start, move, end }: DragConfig): () => void {
|
|
9
|
+
let startX = 0;
|
|
10
|
+
let startY = 0;
|
|
11
|
+
let lastDx = 0;
|
|
12
|
+
let lastDy = 0;
|
|
13
|
+
let activePointerId: number | null = null;
|
|
14
|
+
|
|
15
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
startX = e.clientX;
|
|
18
|
+
startY = e.clientY;
|
|
19
|
+
lastDx = 0;
|
|
20
|
+
lastDy = 0;
|
|
21
|
+
activePointerId = e.pointerId;
|
|
22
|
+
source.setPointerCapture(e.pointerId);
|
|
23
|
+
source.addEventListener('pointermove', onPointerMove);
|
|
24
|
+
source.addEventListener('pointerup', onPointerUp);
|
|
25
|
+
source.addEventListener('pointercancel', onPointerCancel);
|
|
26
|
+
document.addEventListener('keydown', onKeyDown);
|
|
27
|
+
start?.(0, 0);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
31
|
+
lastDx = e.clientX - startX;
|
|
32
|
+
lastDy = e.clientY - startY;
|
|
33
|
+
move?.(lastDx, lastDy);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const onPointerUp = (e: PointerEvent) => {
|
|
37
|
+
const dx = e.clientX - startX;
|
|
38
|
+
const dy = e.clientY - startY;
|
|
39
|
+
stopDrag();
|
|
40
|
+
end?.(dx, dy, true);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const onPointerCancel = () => {
|
|
44
|
+
const dx = lastDx;
|
|
45
|
+
const dy = lastDy;
|
|
46
|
+
stopDrag();
|
|
47
|
+
end?.(dx, dy, false);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
51
|
+
if (e.key !== 'Escape') { return; }
|
|
52
|
+
const dx = lastDx;
|
|
53
|
+
const dy = lastDy;
|
|
54
|
+
if (activePointerId !== null) {
|
|
55
|
+
source.releasePointerCapture(activePointerId);
|
|
56
|
+
}
|
|
57
|
+
stopDrag();
|
|
58
|
+
end?.(dx, dy, false);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const stopDrag = () => {
|
|
62
|
+
activePointerId = null;
|
|
63
|
+
source.removeEventListener('pointermove', onPointerMove);
|
|
64
|
+
source.removeEventListener('pointerup', onPointerUp);
|
|
65
|
+
source.removeEventListener('pointercancel', onPointerCancel);
|
|
66
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
source.addEventListener('pointerdown', onPointerDown);
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
source.removeEventListener('pointerdown', onPointerDown);
|
|
73
|
+
stopDrag();
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
position: absolute;
|
|
3
|
+
display: grid;
|
|
4
|
+
width: calc(anchor-size(width) + 0.5rem + var(--node-resize-delta-width, 0px));
|
|
5
|
+
height: calc(anchor-size(height) + 0.5rem + var(--node-resize-delta-height, 0px));
|
|
6
|
+
top: calc(anchor(top) - 0.25rem + var(--node-resize-delta-y, 0px));
|
|
7
|
+
left: calc(anchor(left) - 0.25rem + var(--node-resize-delta-x, 0px));
|
|
8
|
+
border: 0;
|
|
9
|
+
background: transparent;
|
|
10
|
+
--resize-radius: 0.25rem;
|
|
11
|
+
grid-template-columns: 0.75rem auto 0.75rem;
|
|
12
|
+
grid-template-rows: 0.75rem auto 0.75rem;
|
|
13
|
+
--node-resize-outline: transparent;
|
|
14
|
+
--node-resize-hover: #248BE5;
|
|
15
|
+
pointer-events: none;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:host(.preview) {
|
|
19
|
+
--node-resize-outline: rgba(36, 139, 229, 0.5);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
[part=header] {
|
|
23
|
+
height: 1rem;
|
|
24
|
+
pointer-events: all;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
[part=northWest] {
|
|
28
|
+
width: 0.75rem;
|
|
29
|
+
height: 0.75rem;
|
|
30
|
+
grid-column: 1;
|
|
31
|
+
grid-row: 1;
|
|
32
|
+
padding-top: 0.25rem;
|
|
33
|
+
padding-left: 0.25rem;
|
|
34
|
+
pointer-events: all;
|
|
35
|
+
}
|
|
36
|
+
[part=northWest]::before {
|
|
37
|
+
content: ' ';
|
|
38
|
+
display: flex;
|
|
39
|
+
border-top-left-radius: var(--resize-radius);
|
|
40
|
+
border-top: 2px solid var(--node-resize-outline);
|
|
41
|
+
border-left: 2px solid var(--node-resize-outline);
|
|
42
|
+
width: 0.375rem;
|
|
43
|
+
height: 0.375rem;
|
|
44
|
+
}
|
|
45
|
+
[part=northWest]:hover::before {
|
|
46
|
+
border-color: var(--node-resize-hover);
|
|
47
|
+
}
|
|
48
|
+
[part=north] {
|
|
49
|
+
height: 0.5rem;
|
|
50
|
+
grid-column: 2;
|
|
51
|
+
grid-row: 1;
|
|
52
|
+
padding-top: 0.25rem;
|
|
53
|
+
pointer-events: all;
|
|
54
|
+
}
|
|
55
|
+
[part=north]::before {
|
|
56
|
+
content: ' ';
|
|
57
|
+
display: flex;
|
|
58
|
+
border-top: 2px solid var(--node-resize-outline);
|
|
59
|
+
height: 0.375rem;
|
|
60
|
+
}
|
|
61
|
+
[part=north]:hover::before {
|
|
62
|
+
border-color: var(--node-resize-hover);
|
|
63
|
+
}
|
|
64
|
+
[part=northEast] {
|
|
65
|
+
width: 0.75rem;
|
|
66
|
+
height: 0.75rem;
|
|
67
|
+
grid-column: 3;
|
|
68
|
+
grid-row: 1;
|
|
69
|
+
padding-top: 0.25rem;
|
|
70
|
+
padding-right: 0.25rem;
|
|
71
|
+
pointer-events: all;
|
|
72
|
+
}
|
|
73
|
+
[part=northEast]::before {
|
|
74
|
+
content: ' ';
|
|
75
|
+
display: flex;
|
|
76
|
+
border-top-right-radius: var(--resize-radius);
|
|
77
|
+
border-top: 2px solid var(--node-resize-outline);
|
|
78
|
+
border-right: 2px solid var(--node-resize-outline);
|
|
79
|
+
width: 0.375rem;
|
|
80
|
+
height: 0.375rem;
|
|
81
|
+
}
|
|
82
|
+
[part=northEast]:hover::before {
|
|
83
|
+
border-color: var(--node-resize-hover);
|
|
84
|
+
}
|
|
85
|
+
[part=west] {
|
|
86
|
+
width: 0.5rem;
|
|
87
|
+
grid-column: 1;
|
|
88
|
+
grid-row: 2;
|
|
89
|
+
padding-left: 0.25rem;
|
|
90
|
+
pointer-events: all;
|
|
91
|
+
}
|
|
92
|
+
[part=west]::before {
|
|
93
|
+
content: ' ';
|
|
94
|
+
display: flex;
|
|
95
|
+
border-left: 2px solid var(--node-resize-outline);
|
|
96
|
+
width: 0.375rem;
|
|
97
|
+
height: 100%;
|
|
98
|
+
}
|
|
99
|
+
[part=west]:hover::before {
|
|
100
|
+
border-color: var(--node-resize-hover);
|
|
101
|
+
}
|
|
102
|
+
[part=east] {
|
|
103
|
+
width: 0.5rem;
|
|
104
|
+
grid-column: 3;
|
|
105
|
+
grid-row: 2;
|
|
106
|
+
padding-right: 0.25rem;
|
|
107
|
+
pointer-events: all;
|
|
108
|
+
}
|
|
109
|
+
[part=east]::before {
|
|
110
|
+
content: ' ';
|
|
111
|
+
display: flex;
|
|
112
|
+
border-right: 2px solid var(--node-resize-outline);
|
|
113
|
+
width: 0.375rem;
|
|
114
|
+
height: 100%;
|
|
115
|
+
}
|
|
116
|
+
[part=east]:hover::before {
|
|
117
|
+
border-color: var(--node-resize-hover);
|
|
118
|
+
}
|
|
119
|
+
[part=southWest] {
|
|
120
|
+
width: 0.5rem;
|
|
121
|
+
height: 0.5rem;
|
|
122
|
+
grid-column: 1;
|
|
123
|
+
grid-row: 3;
|
|
124
|
+
padding-left: 0.25rem;
|
|
125
|
+
padding-bottom: 0.25rem;
|
|
126
|
+
pointer-events: all;
|
|
127
|
+
}
|
|
128
|
+
[part=southWest]::before {
|
|
129
|
+
content: ' ';
|
|
130
|
+
display: flex;
|
|
131
|
+
border-bottom-left-radius: var(--resize-radius);
|
|
132
|
+
border-bottom: 2px solid var(--node-resize-outline);
|
|
133
|
+
border-left: 2px solid var(--node-resize-outline);
|
|
134
|
+
width: 0.375rem;
|
|
135
|
+
height: 0.375rem;
|
|
136
|
+
}
|
|
137
|
+
[part=southWest]:hover::before {
|
|
138
|
+
border-color: var(--node-resize-hover);
|
|
139
|
+
}
|
|
140
|
+
[part=south] {
|
|
141
|
+
height: 0.5rem;
|
|
142
|
+
grid-column: 2;
|
|
143
|
+
grid-row: 3;
|
|
144
|
+
pointer-events: all;
|
|
145
|
+
padding-bottom: 0.25rem;
|
|
146
|
+
}
|
|
147
|
+
[part=south]::before {
|
|
148
|
+
content: ' ';
|
|
149
|
+
display: flex;
|
|
150
|
+
border-bottom: 2px solid var(--node-resize-outline);
|
|
151
|
+
height: 0.375rem;
|
|
152
|
+
}
|
|
153
|
+
[part=south]:hover::before {
|
|
154
|
+
border-color: var(--node-resize-hover);
|
|
155
|
+
}
|
|
156
|
+
[part=southEast] {
|
|
157
|
+
width: 0.5rem;
|
|
158
|
+
height: 0.5rem;
|
|
159
|
+
grid-column: 3;
|
|
160
|
+
grid-row: 3;
|
|
161
|
+
padding-right: 0.25rem;
|
|
162
|
+
padding-bottom: 0.25rem;
|
|
163
|
+
pointer-events: all;
|
|
164
|
+
}
|
|
165
|
+
[part=southEast]::before {
|
|
166
|
+
content: ' ';
|
|
167
|
+
display: flex;
|
|
168
|
+
border-bottom-right-radius: var(--resize-radius);
|
|
169
|
+
border-bottom: 2px solid var(--node-resize-outline);
|
|
170
|
+
border-right: 2px solid var(--node-resize-outline);
|
|
171
|
+
width: 0.375rem;
|
|
172
|
+
height: 0.375rem;
|
|
173
|
+
}
|
|
174
|
+
[part=southEast]:hover::before {
|
|
175
|
+
border-color: var(--node-resize-hover);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
[part=northWest].stop::before,
|
|
179
|
+
[part=north].stop::before,
|
|
180
|
+
[part=northEast].stop::before,
|
|
181
|
+
[part=west].stop::before,
|
|
182
|
+
[part=east].stop::before,
|
|
183
|
+
[part=southWest].stop::before,
|
|
184
|
+
[part=south].stop::before,
|
|
185
|
+
[part=southEast].stop::before {
|
|
186
|
+
border-color: red;
|
|
187
|
+
}
|