From 90459f0ba4d656e9bd4431af27e479b88f9c06e2 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 15 Jan 2026 16:28:07 +0100 Subject: [PATCH] Add PenpotUtils.analyzeDescendants as a powerful utility function for validation #26 --- mcp-server/data/prompts.yml | 16 ++++++++++++ penpot-plugin/src/PenpotUtils.ts | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/mcp-server/data/prompts.yml b/mcp-server/data/prompts.yml index 40e0472..4a808a6 100644 --- a/mcp-server/data/prompts.yml +++ b/mcp-server/data/prompts.yml @@ -152,6 +152,10 @@ initial_instructions: | Returns true if child is fully within parent's visual bounds * setParentXY(shape: Shape, parentX: number, parentY: number): void Sets shape position relative to its parent (since parentX/parentY are read-only) + * analyzeDescendants(root: Shape, evaluator: (descendant: Shape) => T | null | undefined, maxDepth?: number): Array<{ shape: Shape, result: T }> + General-purpose utility for analyzing/validating descendants + Calls evaluator on each descendant; collects non-null/undefined results + Powerful pattern: evaluator can return corrector functions or diagnostic data General pointers for working with Penpot designs: * Prefer `penpotUtils` helper functions — avoid reimplementing shape searching. @@ -173,6 +177,18 @@ initial_instructions: | const structure = penpotUtils.shapeStructure(penpot.selection[0]); * Find shapes in current selection/board: const shapes = penpotUtils.findShapes(predicate, penpot.selection[0] || penpot.root); + * Validate/analyze descendants (returns corrector functions): + const fixes = penpotUtils.analyzeDescendants(board, (shape) => { + const xMod = shape.parentX % 4; + if (xMod !== 0) { + return () => penpotUtils.setParentXY(shape, Math.round(shape.parentX / 4) * 4, shape.parentY); + } + }); + fixes.forEach(f => f.result()); // Apply all fixes + * Find containment violations: + const violations = penpotUtils.analyzeDescendants(board, (shape) => { + return !penpotUtils.isContainedIn(shape, board) ? 'outside-bounds' : null; + }); # Asset Libraries diff --git a/penpot-plugin/src/PenpotUtils.ts b/penpot-plugin/src/PenpotUtils.ts index 55285a5..2ae115d 100644 --- a/penpot-plugin/src/PenpotUtils.ts +++ b/penpot-plugin/src/PenpotUtils.ts @@ -198,6 +198,48 @@ export class PenpotUtils { shape.y = shape.parent.y + parentY; } + /** + * Analyzes all descendants of a shape by applying an evaluator function to each. + * Only descendants for which the evaluator returns a non-null/non-undefined value are included in the result. + * This is a general-purpose utility for validation, analysis, or collecting corrector functions. + * + * @param root - The root shape whose descendants to analyze + * @param evaluator - Function called for each descendant; return null/undefined to skip + * @param maxDepth - Optional maximum depth to traverse (undefined for unlimited) + * @returns Array of objects containing the shape and the evaluator's result + */ + public static analyzeDescendants( + root: Shape, + evaluator: (descendant: Shape) => T | null | undefined, + maxDepth: number | undefined = undefined + ): Array<{ shape: Shape; result: NonNullable }> { + const results: Array<{ shape: Shape; result: NonNullable }> = []; + + const traverse = (shape: Shape, currentDepth: number): void => { + const result = evaluator(shape); + if (result !== null && result !== undefined) { + results.push({ shape, result: result as NonNullable }); + } + + if (maxDepth === undefined || currentDepth < maxDepth) { + if ("children" in shape && shape.children) { + for (const child of shape.children) { + traverse(child, currentDepth + 1); + } + } + } + }; + + // Start traversal with root's children (not root itself) + if ("children" in root && root.children) { + for (const child of root.children) { + traverse(child, 1); + } + } + + return results; + } + /** * Decodes a base64 string to a Uint8Array. * This is required because the Penpot plugin environment does not provide the atob function.