@@ -19,6 +19,14 @@ const ICON_COMPLEX_VECTOR_TYPES: ReadonlySet<NodeType> = new Set([
1919 "BOOLEAN_OPERATION" ,
2020] ) ;
2121
22+ // Types that are considered icons regardless of size if they are top-level
23+ const ICON_TYPES_IGNORE_SIZE : ReadonlySet < NodeType > = new Set ( [
24+ "VECTOR" ,
25+ "BOOLEAN_OPERATION" ,
26+ "POLYGON" ,
27+ "STAR" ,
28+ ] ) ;
29+
2230const ICON_CONTAINER_TYPES : ReadonlySet < NodeType > = new Set ( [
2331 "FRAME" ,
2432 "GROUP" ,
@@ -58,32 +66,20 @@ const DISALLOWED_CHILD_TYPES: ReadonlySet<NodeType> = new Set([
5866// ========================================================================
5967
6068/**
61- * Checks if a node's dimensions fall within a typical size range for icons.
69+ * Checks if a node's dimensions fall within a typical *maximum* size for icons.
70+ * Simplified to only check max size.
6271 */
6372function isTypicalIconSize (
6473 node : SceneNode ,
65- minSize = 8 ,
6674 maxSize = 64 , // Standard max size
67- aspectRatioTolerance = 0.5 , // Allow slightly non-square icons
6875) : boolean {
6976 if (
7077 ! ( "width" in node && "height" in node && node . width > 0 && node . height > 0 )
7178 ) {
7279 return false ; // Needs dimensions
7380 }
74- if (
75- node . width < minSize ||
76- node . height < minSize ||
77- node . width > maxSize ||
78- node . height > maxSize
79- ) {
80- return false ; // Outside size limits
81- }
82- const aspectRatio = node . width / node . height ;
83- return (
84- aspectRatio >= 1 - aspectRatioTolerance &&
85- aspectRatio <= 1 + aspectRatioTolerance // Check aspect ratio
86- ) ;
81+ // Only check if dimensions exceed the maximum allowed size
82+ return node . width <= maxSize && node . height <= maxSize ;
8783}
8884
8985/**
@@ -146,6 +142,9 @@ function checkChildrenRecursively(children: ReadonlyArray<SceneNode>): {
146142/**
147143 * Analyzes a Figma SceneNode using simplified structural rules to determine if it's likely an icon.
148144 * v5.1: Added rule to always consider nodes with SVG export settings as icons.
145+ * v5.2: Always consider VECTOR nodes as icons, regardless of size.
146+ * v5.3: Always consider VECTOR, BOOLEAN_OPERATION, POLYGON, STAR as icons regardless of size. Simplified size check to max dimension only.
147+ * v5.4: Check for disallowed types *before* checking SVG export settings.
149148 *
150149 * @param node The Figma SceneNode to evaluate.
151150 * @param logDetails Set to true to print debug information to the console.
@@ -156,49 +155,58 @@ export function isLikelyIcon(node: SceneNode, logDetails = false): boolean {
156155 let result = false ;
157156 let reason = "" ;
158157
159- // --- 1. Check for SVG Export Settings ---
160- if ( hasSvgExportSettings ( node ) ) {
158+ // --- 1. Initial Filtering (Disallowed Types First) ---
159+ if ( DISALLOWED_ICON_TYPES . has ( node . type ) ) {
160+ reason = `Disallowed Type: ${ node . type } ` ;
161+ result = false ;
162+ }
163+ // --- 2. Check for SVG Export Settings (Only if not disallowed) ---
164+ else if ( hasSvgExportSettings ( node ) ) {
161165 reason = "Has SVG export settings" ;
162166 result = true ;
163167 }
164- // --- 2. Initial Filtering ---
165- else if ( node . visible === false ) {
166- reason = "Invisible" ;
167- result = false ;
168- } else if ( DISALLOWED_ICON_TYPES . has ( node . type ) ) {
169- reason = `Disallowed Type: ${ node . type } ` ;
170- result = false ;
171- } else if (
168+ // --- 3. Dimension Check ---
169+ else if (
172170 ! ( "width" in node && "height" in node && node . width > 0 && node . height > 0 )
173171 ) {
174- reason = "No dimensions" ;
175- result = false ;
172+ // Exception: Allow specific types even without dimensions initially.
173+ if ( ICON_TYPES_IGNORE_SIZE . has ( node . type ) ) {
174+ reason = `Direct ${ node . type } type (no dimensions check needed)` ;
175+ result = true ;
176+ } else {
177+ reason = "No dimensions" ;
178+ result = false ;
179+ }
176180 } else {
177- // --- 3. Direct Vector/Boolean/Primitive ---
178- if (
179- ICON_COMPLEX_VECTOR_TYPES . has ( node . type ) ||
180- ICON_PRIMITIVE_TYPES . has ( node . type )
181- ) {
181+ // --- 4. Direct Vector/Boolean/Primitive ---
182+ // Special case: VECTOR, BOOLEAN_OPERATION, POLYGON, STAR are always icons
183+ if ( ICON_TYPES_IGNORE_SIZE . has ( node . type ) ) {
184+ reason = `Direct ${ node . type } type (size ignored)` ;
185+ result = true ;
186+ }
187+ // Check other primitives (ELLIPSE, RECTANGLE, LINE) with size constraint
188+ else if ( ICON_PRIMITIVE_TYPES . has ( node . type ) ) {
182189 if ( isTypicalIconSize ( node ) ) {
183190 reason = `Direct ${ node . type } with typical size` ;
184191 result = true ;
185192 } else {
186- reason = `Direct ${ node . type } but wrong size (${ Math . round ( node . width ) } x${ Math . round ( node . height ) } )` ;
193+ reason = `Direct ${ node . type } but too large (${ Math . round ( node . width ) } x${ Math . round ( node . height ) } )` ;
187194 result = false ;
188195 }
189196 }
190- // --- 4 . Container Logic ---
197+ // --- 5 . Container Logic ---
191198 else if ( ICON_CONTAINER_TYPES . has ( node . type ) && "children" in node ) {
199+ // Container size check still uses the simplified isTypicalIconSize
192200 if ( ! isTypicalIconSize ( node ) ) {
193- reason = `Container but wrong size (${ Math . round ( node . width ) } x${ Math . round ( node . height ) } )` ;
201+ reason = `Container but too large (${ Math . round ( node . width ) } x${ Math . round ( node . height ) } )` ;
194202 result = false ;
195203 } else {
196204 const visibleChildren = node . children . filter (
197205 ( child ) => child . visible !== false ,
198206 ) ;
199207
200208 if ( visibleChildren . length === 0 ) {
201- // Check for styling on empty containers
209+ // Check for styling on empty containers (size already checked)
202210 const hasVisibleFill =
203211 "fills" in node &&
204212 Array . isArray ( node . fills ) &&
@@ -215,21 +223,25 @@ export function isLikelyIcon(node: SceneNode, logDetails = false): boolean {
215223 node . strokes . some ( ( s ) => s . visible !== false ) ;
216224
217225 if ( hasVisibleFill || hasVisibleStroke ) {
218- reason = "Empty container with visible fill/stroke" ;
226+ reason =
227+ "Empty container with visible fill/stroke and typical size" ;
219228 result = true ;
220229 } else {
221230 reason = "Empty container with no visible style" ;
222- result = false ;
231+ result = false ; // Size is okay, but no content or style
223232 }
224233 } else {
225- // Check content of non-empty containers
234+ // Check content of non-empty containers (size already checked)
226235 const checkResult = checkChildrenRecursively ( visibleChildren ) ;
227236
228237 if ( checkResult . hasDisallowedChild ) {
229238 reason =
230239 "Container has disallowed child type (Text, Frame, Component, Instance, etc.)" ;
231240 result = false ;
232241 } else if ( ! checkResult . hasValidContent ) {
242+ // Allow containers if they *only* contain other groups,
243+ // as long as those groups eventually contain valid content.
244+ // The checkResult.hasValidContent handles this.
233245 reason = "Container has no vector or primitive content" ;
234246 result = false ;
235247 } else {
@@ -239,7 +251,7 @@ export function isLikelyIcon(node: SceneNode, logDetails = false): boolean {
239251 }
240252 }
241253 }
242- // --- 5 . Default ---
254+ // --- 6 . Default ---
243255 else {
244256 reason =
245257 "Not a recognized icon structure (Vector, Primitive, or valid Container)" ;
0 commit comments