Skip to content

Commit b82b4e6

Browse files
committed
Large perf increase in bitmapToMask
1 parent 9d7c1a0 commit b82b4e6

File tree

4 files changed

+126
-82
lines changed

4 files changed

+126
-82
lines changed

src/components/pg/button/button.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
width: fit-content;
44
}
55

6+
:host:has(button.block) {
7+
display: flex;
8+
width: unset;
9+
}
10+
611
[part=button] {
712
display: flex;
813
align-items: center;

src/components/pg/inputPixelEditor/inputPixelEditor.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,8 +1466,12 @@ export default class PgInputPixelEditor extends HTMLElement {
14661466
#getLayerPaths() {
14671467
return this.#data.map((layer, layerIndex) => {
14681468
const colors = this.getLayerColorIndexes(layerIndex);
1469-
return colors.map((color) => {
1470-
return [color, bitmaskToPath(layer, { scale: 1, include: [color] })];
1469+
const paths = bitmaskToPath(layer, {
1470+
scale: 1,
1471+
include: colors.map((color) => ([color])),
1472+
});
1473+
return colors.map((color, i) => {
1474+
return [color, paths[i]];
14711475
});
14721476
});
14731477
}
@@ -1489,7 +1493,7 @@ export default class PgInputPixelEditor extends HTMLElement {
14891493
color,
14901494
path: bitmaskToPath(this.#data[layerIndex], {
14911495
scale: 1,
1492-
include: [color]
1496+
include: [[color]]
14931497
}),
14941498
};
14951499
});

src/components/pg/inputPixelEditor/utils/bitmapToMask.ts

Lines changed: 113 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ interface Options {
1313
scale?: number;
1414
offsetX?: number;
1515
offsetY?: number;
16-
include?: number[];
16+
include?: number[][];
1717
}
1818

1919
export function toIndex(x: number, y: number, width: number) {
2020
return y * width + x;
2121
}
2222

2323
/**
24-
* Convert a 2d array to a SVG path.
24+
* Convert a 2d array to SVG paths.
2525
* @param data
2626
* @param options
27-
* @returns path string
27+
* @returns array of path strings, one per include grouping
2828
*/
2929
export default function bitmaskToPath(data: number[] | number[][], options: Options = {}) {
3030

@@ -34,10 +34,10 @@ export default function bitmaskToPath(data: number[] | number[][], options: Opti
3434
scale = 1,
3535
offsetX = 0,
3636
offsetY = 0,
37-
include = [1];
37+
include: number[][] = [[1]];
3838

3939
if (options.width) {
40-
bitmask = data as number[]; // already flat
40+
bitmask = data as number[];
4141
width = options.width;
4242
height = bitmask.length / width;
4343
if (height % 1 !== 0) {
@@ -67,150 +67,184 @@ export default function bitmaskToPath(data: number[] | number[][], options: Opti
6767
include = options.include;
6868
}
6969

70-
// Naively copy into a new bitmask with a border of 1 to make sampling easier (no out of bounds checks)
70+
const groupCount = include.length;
7171
const newWidth = width + 2;
7272
const newHeight = height + 2;
73-
const bm = Array(newWidth * newHeight).fill(0);
7473

75-
// BM is just shifted over (1, 1) for the padding
7674
function BMXYToIndex(x: number, y: number) {
7775
return (y + 1) * newWidth + (x + 1);
7876
}
7977

78+
// Build value → group bitmask lookup
79+
const valueToGroupBits = new Map<number, number>();
80+
for (let g = 0; g < groupCount; ++g) {
81+
for (const val of include[g]) {
82+
valueToGroupBits.set(val, (valueToGroupBits.get(val) ?? 0) | (1 << g));
83+
}
84+
}
85+
86+
// Single pass: build padded cellMask where each cell stores a bitfield of group membership
87+
const cellMask = new Int32Array(newWidth * newHeight);
8088
for (let y = 0; y < height; ++y) {
8189
for (let x = 0; x < width; ++x) {
82-
bm[BMXYToIndex(x, y)] = include.includes(bitmask[toIndex(x, y, width)]) ? 1 : 0;
90+
const bits = valueToGroupBits.get(bitmask[toIndex(x, y, width)]) ?? 0;
91+
if (bits) {
92+
cellMask[BMXYToIndex(x, y)] = bits;
93+
}
8394
}
8495
}
8596

86-
// Edges data structure has [x, y, nextEdge, group]
97+
// Allocate per-group edge structures upfront
8798
const edgeXCount = width * (height + 1);
8899
const edgeYCount = (width + 1) * height;
89100
const edgeCount = edgeXCount + edgeYCount;
90101

91-
const edges = Array(edgeCount).fill(0).map(() => ({ x: 0, y: 0, next: undefined })) as Edge[];
92102
function EdgeXIndex(x: number, y: number) {
93103
return y * width + x;
94104
}
95105
function EdgeYIndex(x: number, y: number) {
96106
return edgeXCount + y * (width + 1) + x;
97107
}
98108

99-
const groups = new Set<Edge>();
109+
const allEdges: Edge[][] = [];
110+
const allContours: Set<Edge>[] = [];
111+
for (let g = 0; g < groupCount; ++g) {
112+
allEdges.push(Array(edgeCount).fill(0).map(() => ({ x: 0, y: 0, next: undefined })) as Edge[]);
113+
allContours.push(new Set<Edge>());
114+
}
100115

101-
function SetEdge(edge: Edge, x: number, y: number) {
116+
// Helper: check group membership via cellMask bit
117+
function isSet(x: number, y: number, bit: number) {
118+
return (cellMask[BMXYToIndex(x, y)] & bit) !== 0;
119+
}
120+
121+
function SetEdge(contours: Set<Edge>, edge: Edge, x: number, y: number) {
102122
edge.x = x;
103123
edge.y = y;
104-
groups.add(edge);
124+
contours.add(edge);
105125
}
106126

107-
function UnionGroup(edge: Edge) {
127+
function UnionGroup(contours: Set<Edge>, edge: Edge) {
108128
for (var itr = edge.next; itr !== undefined && itr !== edge; itr = itr.next) {
109-
groups.delete(itr);
129+
contours.delete(itr);
110130
}
111131
if (itr !== undefined) {
112-
groups.add(edge);
132+
contours.add(edge);
113133
}
114134
}
115135

136+
// Single pass edge detection for all groups
116137
for (let y = 0; y < height; ++y) {
117138
for (let x = 0; x < width; ++x) {
118-
if (bm[BMXYToIndex(x, y)] === 1) {
119-
const left = bm[BMXYToIndex(x - 1, y)];
120-
if (left == 0) {
139+
const myMask = cellMask[BMXYToIndex(x, y)];
140+
if (myMask === 0) continue;
141+
142+
for (let g = 0; g < groupCount; ++g) {
143+
const groupBit = 1 << g;
144+
if ((myMask & groupBit) === 0) continue;
145+
146+
const edges = allEdges[g];
147+
const contours = allContours[g];
148+
149+
if (!isSet(x - 1, y, groupBit)) {
121150
const edge = edges[EdgeYIndex(x, y)];
122-
SetEdge(edge, x, y + 1);
123-
if (bm[BMXYToIndex(x - 1, y - 1)]) {
151+
SetEdge(contours, edge, x, y + 1);
152+
if (isSet(x - 1, y - 1, groupBit)) {
124153
edge.next = edges[EdgeXIndex(x - 1, y)];
125-
} else if (bm[BMXYToIndex(x, y - 1)]) {
154+
} else if (isSet(x, y - 1, groupBit)) {
126155
edge.next = edges[EdgeYIndex(x, y - 1)];
127156
} else {
128157
edge.next = edges[EdgeXIndex(x, y)];
129158
}
130-
UnionGroup(edge);
159+
UnionGroup(contours, edge);
131160
}
132-
const right = bm[BMXYToIndex(x + 1, y)];
133-
if (right === 0) {
161+
if (!isSet(x + 1, y, groupBit)) {
134162
const edge = edges[EdgeYIndex(x + 1, y)];
135-
SetEdge(edge, x + 1, y);
136-
if (bm[BMXYToIndex(x + 1, y + 1)]) {
163+
SetEdge(contours, edge, x + 1, y);
164+
if (isSet(x + 1, y + 1, groupBit)) {
137165
edge.next = edges[EdgeXIndex(x + 1, y + 1)];
138-
} else if (bm[BMXYToIndex(x, y + 1)]) {
166+
} else if (isSet(x, y + 1, groupBit)) {
139167
edge.next = edges[EdgeYIndex(x + 1, y + 1)];
140168
} else {
141169
edge.next = edges[EdgeXIndex(x, y + 1)];
142170
}
143-
UnionGroup(edge);
171+
UnionGroup(contours, edge);
144172
}
145-
const top = bm[BMXYToIndex(x, y - 1)];
146-
if (top === 0) {
147-
const edge: Edge = edges[EdgeXIndex(x, y)];
148-
SetEdge(edge, x, y);
149-
if (bm[BMXYToIndex(x + 1, y - 1)]) {
173+
if (!isSet(x, y - 1, groupBit)) {
174+
const edge = edges[EdgeXIndex(x, y)];
175+
SetEdge(contours, edge, x, y);
176+
if (isSet(x + 1, y - 1, groupBit)) {
150177
edge.next = edges[EdgeYIndex(x + 1, y - 1)];
151-
} else if (bm[BMXYToIndex(x + 1, y)]) {
178+
} else if (isSet(x + 1, y, groupBit)) {
152179
edge.next = edges[EdgeXIndex(x + 1, y)];
153180
} else {
154181
edge.next = edges[EdgeYIndex(x + 1, y)];
155182
}
156-
UnionGroup(edge);
183+
UnionGroup(contours, edge);
157184
}
158-
const bottom = bm[BMXYToIndex(x, y + 1)];
159-
if (bottom === 0) {
185+
if (!isSet(x, y + 1, groupBit)) {
160186
const edge = edges[EdgeXIndex(x, y + 1)];
161-
SetEdge(edge, x + 1, y + 1);
162-
if (bm[BMXYToIndex(x - 1, y + 1)]) {
187+
SetEdge(contours, edge, x + 1, y + 1);
188+
if (isSet(x - 1, y + 1, groupBit)) {
163189
edge.next = edges[EdgeYIndex(x, y + 1)];
164-
} else if (bm[BMXYToIndex(x - 1, y)]) {
190+
} else if (isSet(x - 1, y, groupBit)) {
165191
edge.next = edges[EdgeXIndex(x - 1, y + 1)];
166192
} else {
167193
edge.next = edges[EdgeYIndex(x, y)];
168194
}
169-
UnionGroup(edge);
195+
UnionGroup(contours, edge);
170196
}
171197
}
172198
}
173199
}
174200

175-
for (const edge of groups) {
176-
let itr = edge;
177-
do {
178-
if (itr.next) {
179-
itr.next.type = itr.x == itr?.next?.x ? 'V' : 'H';
180-
itr = itr.next;
181-
}
182-
} while (itr !== edge);
183-
}
184-
185-
// Compress sequences of H and V
186-
for (let edge of groups) {
187-
let itr = edge;
188-
do {
189-
if (itr.type != itr.next?.type) {
190-
while (itr.next?.type == itr.next?.next?.type) {
191-
if (itr.next === edge) {
192-
groups.delete(edge);
193-
edge = itr.next.next as Edge;
194-
groups.add(edge); // Note this will cause it to iterate over this group again, meh.
201+
// Per-group post-processing: type assignment, compression, path building
202+
const paths: string[] = [];
203+
204+
for (let g = 0; g < groupCount; ++g) {
205+
const contours = allContours[g];
206+
207+
for (const edge of contours) {
208+
let itr = edge;
209+
do {
210+
if (itr.next) {
211+
itr.next.type = itr.x == itr?.next?.x ? 'V' : 'H';
212+
itr = itr.next;
213+
}
214+
} while (itr !== edge);
215+
}
216+
217+
for (let edge of contours) {
218+
let itr = edge;
219+
do {
220+
if (itr.type != itr.next?.type) {
221+
while (itr.next?.type == itr.next?.next?.type) {
222+
if (itr.next === edge) {
223+
contours.delete(edge);
224+
edge = itr.next.next as Edge;
225+
contours.add(edge);
226+
}
227+
itr.next = itr.next?.next;
195228
}
196-
itr.next = itr.next?.next;
229+
}
230+
itr = itr.next as Edge;
231+
} while (itr !== edge);
232+
}
233+
234+
let path = '';
235+
for (const edge of contours) {
236+
path += `M${edge.x * scale},${edge.y * scale}`;
237+
for (var itr = edge.next; itr != edge; itr = itr?.next) {
238+
if (itr?.type == 'H') {
239+
path += `H${(itr?.x * scale) + offsetX}`;
240+
} else if (itr?.type == 'V') {
241+
path += `V${(itr?.y * scale) + offsetY}`;
197242
}
198243
}
199-
itr = itr.next as Edge;
200-
} while (itr !== edge);
201-
}
202-
203-
let path = '';
204-
for (const edge of groups) {
205-
path += `M${edge.x * scale},${edge.y * scale}`;
206-
for (var itr = edge.next; itr != edge; itr = itr?.next) {
207-
if (itr?.type == 'H') {
208-
path += `H${(itr?.x * scale) + offsetX}`;
209-
} else if (itr?.type == 'V') {
210-
path += `V${(itr?.y * scale) + offsetY}`;
211-
}
244+
path += 'Z';
212245
}
213-
path += 'Z';
246+
paths.push(path);
214247
}
215-
return path;
248+
249+
return paths;
216250
}

src/components/pg/table/__examples__/basic/basic.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export default class XPgTableBasic extends HTMLElement {
9191
newCount = 0;
9292
handlePushData() {
9393
this.$table.data.push(createTableItem({
94+
selected: false,
9495
name: `new ${this.newCount++}`,
9596
age: this.newCount,
9697
favorite: {

0 commit comments

Comments
 (0)