@@ -13,18 +13,18 @@ interface Options {
1313 scale ?: number ;
1414 offsetX ?: number ;
1515 offsetY ?: number ;
16- include ?: number [ ] ;
16+ include ?: number [ ] [ ] ;
1717}
1818
1919export 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 */
2929export 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}
0 commit comments