Skip to content

Commit e5b8021

Browse files
committed
progress
1 parent 275aa85 commit e5b8021

5 files changed

Lines changed: 163 additions & 110 deletions

File tree

src/components/pg/node/node.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ export default class PgNode extends HTMLElement {
3232
forEach({
3333
container: this.$items,
3434
items: this.fields,
35-
type: (item) => PgNodeEditorText,
36-
create: ($item: PgNodeEditorText, item) => {
35+
type: (_item) => PgNodeEditorText,
36+
create: ($item: PgNodeEditorText, _item) => {
3737
this.height += $item.height;
3838
},
3939
});
@@ -67,16 +67,16 @@ export default class PgNode extends HTMLElement {
6767
}
6868
}
6969

70-
#handleSelect(e: any) {
70+
#handleSelect(_e: any) {
7171
this.dispatchEvent(new CustomEvent('select', {
7272
detail: {
7373
nodeId: `${this.node}`,
7474
}
7575
}));
7676
}
7777

78-
#resizeElement;
79-
#handlePointerOver(e: any) {
78+
#resizeElement: PgNodeResize | null = null;
79+
#handlePointerOver(_e: any) {
8080
if (this.#resizeElement) {
8181
this.#resizeElement.style.visibility = 'visible';
8282
return;
@@ -93,7 +93,6 @@ export default class PgNode extends HTMLElement {
9393
ele.minHeight = this.getMinHeight();
9494
ele.addEventListener('change', (e: any) => {
9595
const { x, y, width, height } = e.detail;
96-
console.log(x, y, width, height);
9796
this.x = x;
9897
this.y = y;
9998
this.width = width;
@@ -102,15 +101,18 @@ export default class PgNode extends HTMLElement {
102101
ele.y = y;
103102
ele.width = width;
104103
ele.height = height;
104+
this.dispatchEvent(new CustomEvent('change', { detail: { x, y, width, height } }));
105105
});
106106
this.shadowRoot?.appendChild(ele);
107107
this.#resizeElement = ele;
108108
this.$node.classList.toggle('resize', true);
109109
}
110110

111-
#handlePointerOut(e: any) {
111+
#handlePointerOut(_e: any) {
112112
this.$node.classList.toggle('resize', false);
113-
this.#resizeElement.style.visibility = 'hidden';
113+
if (this.#resizeElement) {
114+
this.#resizeElement.style.visibility = 'hidden';
115+
}
114116
}
115117

116118
select() {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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)
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 a grid unit)
20+
- Negative drag: first snap at -24 px (1.5 grid units)
21+
22+
## Correct sub-grid formula
23+
24+
The correct sub-grid residual (pixels past the last snap) must mirror the same algorithm:
25+
26+
```ts
27+
subGrid(delta) = delta - (
28+
delta + half < 0
29+
? Math.ceil((delta + half) / gridSize) * gridSize
30+
: Math.floor((delta + half) / gridSize) * gridSize
31+
)
32+
```
33+
34+
## Why the existing formulas fail
35+
36+
`delta % gridSize` (used by previewX/Y/Width):
37+
- delta = -20 → -20 % 16 = -4 (JS truncated modulo)
38+
- correct: no snap at -20 (threshold is -24), so sub-grid = -20
39+
- WRONG by 16 px
40+
41+
`((delta + half) % gridSize) - half` (used by previewHeight):
42+
- delta = 9 → ((17) % 16) - 8 = -7 ✓ (correct, snap fired at +8)
43+
- delta = -20 → ((-12) % 16) - 8 = -12 - 8 = -20 ✓ (correct, no snap yet)
44+
- delta = -9 → ((-1) % 16) - 8 = -1 - 8 = -9
45+
- correct: snap fires at -24, so sub-grid = -9 ✓ happens to be right
46+
47+
## Why the west/north handles jump but south mostly does not
48+
49+
For south (hDir=1), dy is positive when the height grows (normal use). The previewHeight
50+
formula is correct for positive dy. Negative dy (shrinking the south edge) rarely reaches
51+
the problematic range.
52+
53+
For north (hDir=-1), the previewY/previewWidth/previewX use `% gridSize` which is
54+
wrong in the range (-24, -16) for dx/dy values.
55+
56+
## Critical insight about width/x coupling
57+
58+
For a west handle (wDir=-1), dx is the raw pointer delta:
59+
- x sub-grid = subGrid(dx)
60+
- width sub-grid = -subGrid(dx) ← NOT subGrid(-dx)
61+
62+
`subGrid` is NOT an odd function (asymmetric snap means subGrid(-dx) ≠ -subGrid(dx)).
63+
So calling previewWidth(-dx) with an internal formula on -dx is wrong.
64+
65+
## Fix
66+
67+
Compute `sgX = subGrid(dx)` and `sgY = subGrid(dy)` once per `move` event, then:
68+
- delta-x = sgX (when wDir == -1)
69+
- delta-width = wDir * sgX (when wDir != 0)
70+
- delta-y = sgY (when hDir == -1)
71+
- delta-height = hDir * sgY (when hDir != 0)

src/components/pg/nodeResize/nodeResize.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,6 @@
177177
[part=east].stop::before,
178178
[part=southWest].stop::before,
179179
[part=south].stop::before,
180-
[part=southWest].stop::before {
180+
[part=southEast].stop::before {
181181
border-color: red;
182182
}
Lines changed: 76 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, Prop, Part, forEach } from '@pictogrammers/element';
1+
import { Component, Prop, Part } from '@pictogrammers/element';
22

33
import template from './nodeResize.html';
44
import style from './nodeResize.css';
@@ -29,121 +29,96 @@ export default class PgNodeResize extends HTMLElement {
2929
@Part() $southEast: HTMLDivElement;
3030

3131
connectedCallback() {
32-
let startX = 0,
33-
startY = 0,
34-
startWidth = 12,
35-
startHeight = 3;
32+
let startX = 0, startY = 0, startWidth = 0, startHeight = 0;
33+
3634
const start = () => {
3735
startX = this.x;
3836
startY = this.y;
3937
startWidth = this.width;
4038
startHeight = this.height;
4139
this.classList.toggle('preview', true);
4240
};
43-
drag({
44-
source: this.$northWest,
45-
gridSize: this.gridSize,
46-
start,
47-
move: (dx, dy) => {
48-
this.previewX(dx);
49-
this.previewY(dy);
50-
this.previewWidth(dx * -1);
51-
this.previewHeight(dy * -1);
52-
},
53-
snap: (dx, dy) => {
54-
this.emit(
55-
startX + dx,
56-
startY + dy,
57-
startWidth + dx * -1,
58-
startHeight + dy * -1
59-
);
60-
},
61-
end: (dx, dy, complete) => {
62-
if (complete) {
63-
this.emit(
64-
startX + dx,
65-
startY + dy,
66-
startWidth + dx * -1,
67-
startHeight + dy * -1
68-
);
69-
} else {
70-
this.emit(startX, startY, startWidth, startHeight);
71-
}
72-
this.classList.toggle('preview', false);
73-
},
74-
});
75-
drag({
76-
source: this.$south,
77-
gridSize: this.gridSize,
78-
start,
79-
move: (dx, dy) => {
80-
console.log('dy', dy);
81-
if (startHeight === this.minHeight) {
82-
this.$south.classList.toggle('stop', dy < 0);
83-
}
84-
this.previewHeight(dy);
85-
},
86-
snap: (dx, dy) => {
87-
if (startHeight + dy > this.minHeight) {
88-
this.emit(startX, startY, startWidth, startHeight + dy);
89-
} else {
90-
this.emit(startX, startY, startWidth, this.minHeight);
91-
92-
}
93-
},
94-
end: (dx, dy, complete) => {
95-
if (complete) {
96-
this.emit(
97-
startX,
98-
startY,
99-
startWidth,
100-
startHeight + dy
101-
);
102-
} else {
103-
this.emit(startX, startY, startWidth, startHeight);
104-
}
105-
this.$south.classList.toggle('stop', false);
106-
this.classList.toggle('preview', false);
107-
this.previewHeight(0);
108-
},
109-
});
110-
}
11141

112-
render(changes: any) {
42+
// wDir: -1 = west edge (x + width change), 0 = no width, 1 = east edge (width only)
43+
// hDir: -1 = north edge (y + height change), 0 = no height, 1 = south edge (height only)
44+
const addHandle = (source: HTMLElement, wDir: -1 | 0 | 1, hDir: -1 | 0 | 1) => {
45+
drag({
46+
source,
47+
gridSize: this.gridSize,
48+
start,
49+
move: (dx, dy) => {
50+
// Sub-grid residual must mirror dragUtil's asymmetric floor/ceil snap algorithm.
51+
// For positive deltas: floor; for negative: ceil. This avoids a ~gridSize jump
52+
// each time snap fires and the anchor moves.
53+
const sgX = this.#subGrid(dx);
54+
const sgY = this.#subGrid(dy);
55+
this.style.setProperty('--node-resize-delta-x', `${wDir === -1 ? sgX : 0}px`);
56+
this.style.setProperty('--node-resize-delta-width', `${wDir !== 0 ? wDir * sgX : 0}px`);
57+
this.style.setProperty('--node-resize-delta-y', `${hDir === -1 ? sgY : 0}px`);
58+
this.style.setProperty('--node-resize-delta-height', `${hDir !== 0 ? hDir * sgY : 0}px`);
59+
const atMinW = wDir !== 0 && startWidth + wDir * dx < this.minWidth;
60+
const atMinH = hDir !== 0 && startHeight + hDir * dy < this.minHeight;
61+
source.classList.toggle('stop', atMinW || atMinH);
62+
},
63+
snap: (dx, dy) => {
64+
this.emit(...this.#compute(startX, startY, startWidth, startHeight, dx, dy, wDir, hDir));
65+
},
66+
end: (dx, dy, complete) => {
67+
if (complete) {
68+
this.emit(...this.#compute(startX, startY, startWidth, startHeight, dx, dy, wDir, hDir));
69+
} else {
70+
this.emit(startX, startY, startWidth, startHeight);
71+
}
72+
source.classList.toggle('stop', false);
73+
this.classList.toggle('preview', false);
74+
this.style.setProperty('--node-resize-delta-x', '0px');
75+
this.style.setProperty('--node-resize-delta-y', '0px');
76+
this.style.setProperty('--node-resize-delta-width', '0px');
77+
this.style.setProperty('--node-resize-delta-height', '0px');
78+
},
79+
});
80+
};
11381

82+
addHandle(this.$northWest, -1, -1);
83+
addHandle(this.$north, 0, -1);
84+
addHandle(this.$northEast, 1, -1);
85+
addHandle(this.$west, -1, 0);
86+
addHandle(this.$east, 1, 0);
87+
addHandle(this.$southWest, -1, 1);
88+
addHandle(this.$south, 0, 1);
89+
addHandle(this.$southEast, 1, 1);
11490
}
11591

116-
/**
117-
* Emits delta size changes or 0 for no change.
118-
* @param x Delta
119-
* @param y Delta
120-
* @param width Delta
121-
* @param height Delta
122-
*/
123-
emit(x: number, y: number, width: number, height: number) {
124-
this.dispatchEvent(new CustomEvent('change', {
125-
detail: {
126-
x,
127-
y,
128-
width,
129-
height,
130-
}
131-
}));
132-
}
133-
134-
previewX(x: number) {
135-
this.style.setProperty('--node-resize-delta-x', `${x % this.gridSize}px`);
136-
}
92+
render(_changes: any) {}
13793

138-
previewY(y: number) {
139-
this.style.setProperty('--node-resize-delta-y', `${y % this.gridSize}px`);
94+
// Mirrors dragUtil's snap: floor for positive deltas, ceil for negative.
95+
// Returns pixel residual past the last snap boundary so CSS preview tracks smoothly.
96+
#subGrid(delta: number): number {
97+
const { gridSize } = this;
98+
const half = gridSize / 2;
99+
const snapped = delta + half < 0
100+
? Math.ceil((delta + half) / gridSize) * gridSize
101+
: Math.floor((delta + half) / gridSize) * gridSize;
102+
return delta - snapped;
140103
}
141104

142-
previewWidth(width: number) {
143-
this.style.setProperty('--node-resize-delta-width', `${width % this.gridSize}px`);
105+
#compute(
106+
startX: number, startY: number,
107+
startWidth: number, startHeight: number,
108+
dx: number, dy: number,
109+
wDir: -1 | 0 | 1, hDir: -1 | 0 | 1
110+
): [number, number, number, number] {
111+
const newWidth = wDir !== 0 ? Math.max(startWidth + wDir * dx, this.minWidth) : startWidth;
112+
const newHeight = hDir !== 0 ? Math.max(startHeight + hDir * dy, this.minHeight) : startHeight;
113+
// West/north edges: shift x/y to keep the opposite edge stationary
114+
const newX = wDir === -1 ? startX + (startWidth - newWidth) : startX;
115+
const newY = hDir === -1 ? startY + (startHeight - newHeight) : startY;
116+
return [newX, newY, newWidth, newHeight];
144117
}
145118

146-
previewHeight(height: number) {
147-
this.style.setProperty('--node-resize-delta-height', `${((height + (this.gridSize / 2)) % this.gridSize) - (this.gridSize / 2)}px`);
119+
emit(x: number, y: number, width: number, height: number) {
120+
this.dispatchEvent(new CustomEvent('change', {
121+
detail: { x, y, width, height },
122+
}));
148123
}
149124
}

src/components/pg/nodes/nodes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export default class PgNodes extends HTMLElement {
172172
connector.setOutputPin(`${item.node}`, 'nodes', 16);
173173
$item.addEventListener('registernode', this.#registerNode.bind(this));
174174
$item.addEventListener('select', this.#handleSelect.bind(this));
175+
$item.addEventListener('change', this.#handleChange.bind(this));
175176
},
176177
});
177178
}
@@ -213,6 +214,10 @@ export default class PgNodes extends HTMLElement {
213214
console.log('select', nodeId);
214215
}
215216

217+
#handleChange(e: any) {
218+
this.#updatePins(String((e.target as any).node));
219+
}
220+
216221
#deleteNode(nodeId: string) {
217222
const index = this.items.findIndex(x => String(x.node) === nodeId);
218223
if (index > 0) {

0 commit comments

Comments
 (0)