Guide de résolution de problèmes Penpot MCP
Objectif
Ce document compile les leçons apprises lors de la construction du Design System AstroVille sur Penpot via le serveur MCP. L’objectif est d’éviter de redécouvrir ces bizarreries de Penpot dans de futurs projets. Beaucoup de ces comportements ne sont pas documentés et n’apparaissent qu’au moment de l’export ou du groupement des éléments.
La leçon la plus importante : toujours encapsuler le contenu d’une section dans un Groupe, puis positionner tous les éléments à l’intérieur du Groupe en utilisant des coordonnées (x, y) simples relatives à l’origine du groupe. Cela réduit considérablement les erreurs de positionnement.
TL;DR — La règle « Groupe d’abord »
Pour chaque section de premier niveau d’un design Penpot (par exemple, un planche représentant « Couleurs », « Typographie », « Espacement ») :
- Créez la planche.
- Encapsulez tous ses enfants dans un Groupe (Penpot le crée automatiquement lorsque vous appelez
penpot.group([...enfants])). - Positionnez le Groupe lui-même en haut à gauche de votre zone de contenu, par ex.
penpotUtils.setParentXY(groupe, 64, 40). - À l’intérieur du Groupe, toutes les positions des enfants sont des coordonnées simples
(0, 0)relatives à l’origine du groupe. - Positionnez toujours les enfants d’abord en coordonnées relatives
(0, 0), PUIS groupez-les, et non l’inverse.
Ce modèle est ce qui rend le positionnement prévisible et l’export correct.
Problème 1 — Les enfants d’une carte obtiennent des parentX / parentY négatifs après le groupement
Symptôme
Après avoir appelé penpot.group([...enfants]) pour encapsuler le contenu d’une section, les enfants imbriqués à l’intérieur des cartes se retrouvent avec des valeurs parentX / parentY négatives. Par exemple, un Rectangle censé être à (0, 0) par rapport à sa carte se retrouve à (-64, -210) — dessiné en dehors des limites de la carte.
Visuellement, les fonds des cartes (les échantillons de couleur) semblent vides car le rectangle de remplissage réel est positionné à l’extérieur.
Cause racine
Lorsque vous groupez des formes qui étaient déjà disposées en positions absolues sur une planche parente, Penpot calcule la boîte englobante du groupe. Le groupe lui-même est positionné à (minX, minY) de ses membres. Ensuite, pour chaque enfant, son parentX / parentY est recalculé par rapport à la nouvelle origine du groupe.
Si la position absolue d’un enfant était, disons, (64, 218) et que le groupe se retrouve à (64, 218), le parentX / parentY de l’enfant devient (0, 0). Mais si la carte contenant l’enfant était à (64, 218) et que le groupe se retrouve à, disons, (64, 40), alors tout ce qui se trouve à l’intérieur de la carte obtient un parentY négatif car la carte est « plus haute » que l’origine du groupe.
Correctif
Positionnez toujours les enfants en coordonnées relatives à l’origine future du groupe (0, 0) AVANT d’appeler penpot.group(). Après le groupement, l’origine du groupe devient (0, 0) pour ses enfants.
Si vous avez déjà groupé et que les enfants sont mal alignés, la récupération est :
penpot.ungroup(groupe)— repositionnez à plat.- Soustrayez le décalage prévu du groupe de la position
y(oux) de chaque enfant. penpot.group(enfants)à nouveau.penpotUtils.setParentXY(groupe, parentX souhaité, parentY souhaité).
Concrètement (le modèle qui a fonctionné) :
// 1. Sauvegardez les positions absolues originales que vous souhaitez conserver.
const positionsOriginales = enfants.map(e => ({ e, x: e.x, y: e.y }));
// 2. Décalez chaque enfant de -decalageGroupe (pour qu'ils soient relatifs à l'origine du groupe).
for (const enfant of enfants) {
enfant.y -= DECALAGE_GROUPE_Y; // par ex. 40
}
// 3. Groupez-les.
const groupe = penpot.group(enfants);
// 4. Positionnez le groupe lui-même.
if (groupe) {
penpotUtils.setParentXY(groupe, groupe.parentX, DECALAGE_GROUPE_Y);
}
Problème 2 — Les formes de texte avec lineHeight fixe (valeur en pixels) s’affichent avec une hauteur énorme
Symptôme
Une forme de texte avec fontSize: 48 et lineHeight: 56 (une valeur en pixels) se retrouve avec bounds.height: 2689 (ou une valeur absurde similaire). Le texte se répartit verticalement — le texte réellement rendu apparaît bien en dessous de l’endroit où vous l’avez placé (souvent à y: 1313 en termes absolus), rendant le texte invisible.
Cause racine
lineHeight de Penpot accepte un multiplicateur numérique sous forme de chaîne comme "1.2", mais rejette certaines valeurs comme "56" ou toute valeur non numérique autre qu’un multiplicateur. Lorsqu’une valeur invalide est passée, Penpot utilise silencieusement une hauteur de ligne par défaut qui peut être énorme, provoquant un retour à la ligne. L’export SVG confirme le bogue — l’attribut y de l’élément <text> rapporte une valeur énorme comme 1386.44.
De plus, même lorsqu’une lineHeight en pixels apparemment valide est acceptée, le calcul des limites pour un texte auto-width peut produire des valeurs étranges comme width: 338 mais height: 2689, car le moteur de texte effectue un retour à la ligne à l’intérieur des limites que le texte pense avoir à sa création.
Correctif
Utilisez toujours lineHeight: "1.2" (un multiplicateur sous forme de chaîne) plutôt que des valeurs en pixels. La forme de texte aura alors des limites correctes et prévisibles :
const t = penpot.createText("ASTROGENESIS");
parent.appendChild(t);
t.resize(800, 80); // redimensionnez AVANT de définir fontSize
t.x = 64;
t.y = 100;
t.fontFamily = "Space Grotesk";
t.fontWeight = "700";
t.fontSize = 48;
t.lineHeight = "1.2"; // ← utilisez "1.2", pas "56"
t.fills = [{ fillColor: "#F8FAFC", fillOpacity: 1 }];
Cela fonctionne que le fontSize soit défini avant ou après l’ajout, tant que resize() est appelé AVANT fontSize et que lineHeight est une chaîne multiplicateur.
Problème 3 — Les changements de fontSize après l’ajout donnent des dimensions 1×1 si resize() n’est pas appelé d’abord
Symptôme
Un élément de texte créé avec penpot.createText(), ajouté à un parent, et ensuite doté de fontSize: 32 se retrouve avec width: 1, height: 1 initialement. Après un certain délai, les limites se rétablissent, mais le texte peut s’afficher à des positions inattendues.
Cause racine
Penpot mesure les dimensions du texte en utilisant les limites courantes. Si le texte est créé avec des dimensions 1×1 au moment où le moteur de police mesure, il le traite comme contraint à 1 pixel de large et effectue un retour à la ligne agressif.
Correctif
Appelez toujours resize(largeur, hauteur) AVANT de définir fontSize. Cela donne au texte une boîte englobante connue. L’assistant le plus simple :
function creerTexte(parent, chaine, opts) {
const t = penpot.createText(chaine);
parent.appendChild(t);
t.resize(opts.l || 400, opts.h || 60); // redimensionnez d'abord
t.x = opts.x;
t.y = opts.y;
t.fontFamily = opts.fontFamily || "Inter Tight";
t.fontWeight = opts.fontWeight || "400";
t.fontSize = opts.fontSize || 14;
t.lineHeight = opts.lineHeight || "1.2"; // multiplicateur, PAS des pixels
t.fills = [{ fillColor: opts.couleur || "#F8FAFC", fillOpacity: 1 }];
if (opts.transform === "majuscules") t.textTransform = "uppercase";
return t;
}
Problème 4 — L’espacement des lettres avec des unités em est rejeté
Symptôme
Définir text.letterSpacing = "0.08em" échoue avec Valeur non valide : 0.08em. Code : :letterSpacing.
Correctif
letterSpacing de Penpot n’accepte que des valeurs numériques (interprétées comme 1/100 em, ou similaire), pas des chaînes avec suffixe em. Utilisez "1" (ou 0, "2", etc.) au lieu de "0.08em". Pour un contrôle plus fin, utilisez un petit nombre comme "1" ou "2".
Problème 5 — penpot.generateMarkup() renvoie un contenu que l’export PNG ne rend pas
Symptôme
Vous créez des formes, le balisage SVG les montre correctement (<rect>, <text>, <stop> de gradient, etc.), mais export_shape retourne un PNG vierge ou dépourvu du contenu attendu. Il n’y a aucun message d’erreur.
Cause racine
Deux causes principales observées :
- Planches imbriquées avec
clipContent: trueet petites dimensions peuvent rogner la zone visible. Penpot exporte en utilisant la boîte englobante, mais le contenu intérieur dépasse. - Encapsulations de groupes avec contenu décalé (Problème 1 ci-dessus) font que l’export de Penpot utilise des limites de texte obsolètes. Le balisage SVG peut montrer les éléments, mais l’export ne respecte pas les décalages.
Correctif
- Faites confiance au balisage SVG (
penpot.generateMarkup) comme vérité première lorsque quelque chose ne s’affiche pas. - Après avoir corrigé le Problème 1 (enfants aux positions relatives correctes), les exports fonctionnent correctement.
- Vérifiez en exportant d’abord de petites planches individuelles pour isoler celle qui échoue.
Problème 6 — Les enfants d’une planche de section disparaissent après le groupement
Symptôme
Après le groupement, les échantillons / cartes / étiquettes de la section disparaissent de l’export visible, mais ils existent toujours dans la structure de données. Les conteneurs des cartes apparaissent (leurs bordures, remplissages) mais leur contenu (enfants Rectangle, Texte) est dessiné hors de la carte.
Cause racine
C’est la même chose que le Problème 1 — les enfants des cartes ont des parentX / parentY négatifs et sont dessinés en dehors des limites de leur carte.
Correctif
Après le groupement, parcourez les cartes et réinitialisez explicitement la position de chaque enfant imbriqué à son emplacement prévu par rapport à sa carte. Par exemple, dans une carte de couleur avec un échantillon Rectangle à (0, 0) et trois étiquettes de texte à (16, 132), (16, 154), (16, 174) :
const cartes = groupe.enfants.filter(e => e.nom && e.nom.startsWith("Carte Couleur"));
for (const carte of cartes) {
const ch = carte.enfants;
if (ch.length < 4) continue;
penpotUtils.setParentXY(ch[0], 0, 0); // Rectangle en haut à gauche de la carte
penpotUtils.setParentXY(ch[1], 16, 132); // étiquette
penpotUtils.setParentXY(ch[2], 16, 154); // nom du jeton
penpotUtils.setParentXY(ch[3], 16, 174); // valeur hexadécimale
}
Problème 7 — La racine de la page (page.root) a des dimensions fixes minuscules, donc les planches larges peuvent casser les exports
Symptôme
La planche racine de la page Penpot est toujours de taille 0.01 × 0.01 — ses dimensions ne peuvent pas être redimensionnées. Tout ce que vous y ajoutez vit en coordonnées absolues en dehors de la boîte englobante de la racine.
Cause racine
Penpot traite la racine de la page comme un espace réservé ; tout le contenu réel vit dans des planches nommées placées en positions absolues sur la page.
Correctif
C’est acceptable si vous gardez les choses dans des planches nommées avec leurs propres coordonnées absolues (comme vous le faites normalement). N’essayez pas d’utiliser page.root.resize(). Si vous avez besoin d’un conteneur maître, placez les planches enfants directement sur page.root plutôt que de les imbriquer dans une planche maîtresse.
Problème 8 — Ombres : color doit être { color: "#hex", opacity: 0..1 }, pas { r, g, b, a }
Symptôme
Définir une ombre comme :
forme.ombres = [{
color: { r: 139/255, g: 92/255, b: 246/255, a: 0.25 },
offsetX: 0, offsetY: 0, blur: 24, spread: 0,
style: "drop-shadow", visible: true
}];
Se rend avec du noir et une faible opacité au lieu de violet. La propriété .shadows de la forme se lit comme :
{ color: { color: "#000000", opacity: 0.2 }, ... }
Cause racine
Shadow.color est un objet Color de Penpot ayant la forme { color: "#hex", opacity: 0..1 } — pas un objet {r, g, b, a}. La forme erronée est silencieusement ignorée et Penpot revient à une ombre portée noire par défaut.
Correctif
Utilisez la forme d’objet Color avec une chaîne hexadécimale et un champ opacity séparé :
forme.ombres = [{
color: { color: "#8B5CF6", opacity: 0.25 },
offsetX: 0, offsetY: 0, blur: 24, spread: 0,
style: "drop-shadow", visible: true
}];
Ou utilisez penpot.shapesColors([forme]) pour obtenir une instance de Color et inspecter ses champs avant de l’appliquer.
Problème 9 — startX / startY / endX / endY du gradient sont normalisés, pas en pixels
Symptôme
Définir un gradient avec des coordonnées en pixels :
forme.remplissages = [{
fillColorGradient: {
type: "linear",
startX: 0, startY: 0, endX: 320, endY: 160,
stops: [...]
},
fillOpacity: 1
}];
Se relit avec endX: ~1.0, endY: ~1.0 — Penpot a silencieusement normalisé le vecteur. Le champ width conserve l’échelle d’origine, mais le vecteur de direction est maintenant dans l’espace [0, 1].
Cause racine
Penpot stocke la direction du gradient comme un vecteur normalisé entre 0 et 1. Les valeurs en pixels sont divisées par width. Si le vecteur résultant n’est pas strictement dans [0, 1] (par ex. endX: 320 / 320 = 1.0 exactement, ou endY: 160 / 320 = 0.5), Penpot semble l’écrêter et l’arrondir.
Dans AstroVille, l’utilisateur a fait glisser le second point de couleur dans l’interface utilisateur de Penpot pour le placer dans le coin inférieur droit du rectangle. Les valeurs obtenues étaient endX: ~1.0, endY: ~1.0. Le positionnement pixel-parfait du gradient n’est pas exposé comme une propriété modifiable — l’interface utilisateur est la source de vérité pour les positions des poignées de gradient.
Correctif
- Pour un gradient horizontal (de gauche à droite) :
endX: 1, endY: 0 - Pour un gradient diagonal (haut-gauche → bas-droite) :
endX: 1, endY: 1 - Pour un gradient vertical (haut → bas) :
endX: 0, endY: 1 - Pour des positions d’arrêts précises le long de la direction, utilisez
offset(0–1) par arrêt. - Si les positions exactes des poignées de gradient sont importantes, faites-les dans l’interface utilisateur de Penpot et relisez-les pour confirmer.
Modèle recommandé : Le modèle de page « Fondations » AstroVille
La structure qui a fonctionné pour la page « 00 — Design System » d’AstroVille :
page.root
├── "00 — Design System / En-tête" (planche, 0, 0, 1440×120)
│ └── Groupe à (64, 40)
│ ├── "Design System" texte à (0, 0) → rendu à (64, 40)
│ └── "AstroVille — Fondations" texte à (0, 52) → (64, 92)
│
├── "01 — Couleurs" (planche, 0, 200, 1440×1460)
│ └── Groupe à (64, 40)
│ ├── Textes Titre + Description à (0, 0), (0, 48)
│ ├── Étiquette "Arrière-plan" à (0, 100)
│ ├── 3 Cartes Couleur en ligne à (0, 178), (264, 178), (528, 178)
│ │ Chaque carte a ses propres enfants à (0, 0), (16, 132), (16, 154), (16, 174)
│ └── ... 3 autres groupes (Marque, Feedback, Texte) à (0, 498), (0, 818), (0, 1138)
│
└── ... autres sections avec la même structure
Décalages y des sections (relatifs à (0, 0) du groupe) :
| Type de section | Disposition |
|---|---|
| En-tête | Titre (0, 0), Sous-titre (0, 52) |
| Bloc de titre | Titre (0, 0), Description (0, 48) |
| En-tête de groupe | Étiquette (0, 100), Description (0, 130) |
| Ligne de cartes | 3 cartes à (0, 178), (264, 178), (528, 178) |
| Ligne suivante | 320 px en dessous de la précédente : (0, 498), (0, 818), (0, 1138) |
Assistant éprouvé pour créer des planches et des textes :
function creerTexte(parent, chaine, opts) {
const t = penpot.createText(chaine);
parent.appendChild(t);
t.resize(opts.l || 400, opts.h || 60);
t.x = opts.x;
t.y = opts.y;
t.fontFamily = opts.fontFamily || "Inter Tight";
t.fontWeight = opts.fontWeight || "400";
t.fontSize = opts.fontSize || 14;
t.lineHeight = opts.lineHeight || "1.2";
t.fills = [{ fillColor: opts.couleur || "#F8FAFC", fillOpacity: 1 }];
if (opts.transform === "majuscules") t.textTransform = "uppercase";
return t;
}
Résumé des causes racines et des correctifs
| Problème | Cause racine | Correctif |
|---|---|---|
Enfants de carte avec parentX/parentY négatifs | Penpot repositionne les enfants par rapport à l’origine du groupe | Décaler les enfants à (0, 0) relatif avant de grouper |
| Texte avec hauteur énorme | lineHeight en pixels rejeté ; valeur par défaut énorme utilisée | Utiliser un multiplicateur sous forme de chaîne comme "1.2" |
| Texte avec dimensions 1×1 | fontSize défini avant resize() | Appeler resize() avant fontSize |
letterSpacing rejeté | L’unité em n’est pas prise en charge | Utiliser une valeur numérique simple |
| Export PNG vide | Contenu rogné ou décalages obsolètes | Vérifier avec generateMarkup ; corriger les décalages |
| Enfants disparaissent après groupement | Même cause que le Problème 1 | Réinitialiser les positions relatives des enfants de carte |
| La racine de la page ne peut pas être redimensionnée | La racine de la page est un espace réservé fixe | Placer le contenu dans des planches nommées |
| Ombre rendue en noir | Format de couleur incorrect pour shadow.color | Utiliser { color: "#hex", opacity: n } |
| Positions de gradient normalisées | Penpot normalise les vecteurs de gradient en [0, 1] | Utiliser endX: 1, endY: 1 pour un diagonal complet |
Comment utiliser ce guide (pour les agents IA)
Lorsqu’un agent IA génère du code de design Penpot (via l’outil penpot), référez-vous à ce guide dans les cas suivants :
- Erreur de positionnement — Les éléments sont décalés ou en dehors des cartes. → Voir Problème 1.
- Texte manquant ou en hauteur — Le texte a des dimensions étranges ou invisibles. → Voir Problèmes 2–3.
- Export échoue — L’export PNG ne rend pas le contenu attendu. → Voir Problème 5.
- Ombres ou gradients incorrects — Les couleurs ou directions sont erronées. → Voir Problèmes 8–9.
- Règle générale — Toujours encapsuler le contenu dans un groupe, puis décaler les enfants en coordonnées relatives.
Pour les nouvelles sections, utilisez le Modèle de page AstroVille ci-dessus avec les décalages fournis.
Documenté à partir de l’expérience pratique de construction du Design System AstroVille sur Penpot via MCP.