Aller au contenu principal

Vos propres composants

Le second type de plugin n'ouvre aucun panneau. Il s'attache à un objet comme composant et dessine son interface dans l'inspecteur de l'éditeur, à côté de Transform et de Material.

C'est la bonne forme quand votre outil parle d'un objet en particulier : des notes sur un objet, un contrôle sur mesure, un générateur qui appartient à la chose qu'il génère.

En déclarer un

Déclarez-le dans components ; panels peut rester vide :

{
"id": "entity-notes",
"entry": "main.tsx",
"panels": [],
"components": [{ "id": "notes", "title": "Notes", "entry": "main.tsx" }]
}

En écrire un

import {
Button,
Column,
Text,
defineProps,
useEntity,
useSignals,
useSyncedState,
} from '@was/extension';
import { TransformComponent } from '@was/engine';

const useProps = defineProps({ step: { type: 'number', default: 0.5 } });

export default function Notes() {
useSignals(); // suivre les données de scène lues au rendu
const props = useProps(); // les valeurs du formulaire de l'inspecteur
const entity = useEntity(); // l'objet auquel ceci est attaché
const [note, setNote] = useSyncedState('note', '');

return (
<Column gap={2}>
<Text muted>{note}</Text>
<Button
onClick={() =>
setNote(`y=${entity.getComponent(TransformComponent)?.get('position').y}`)
}
>
Remember
</Button>
</Column>
);
}

Quatre hooks font le travail :

HookVous donne
useEntity()l'objet auquel votre composant est attaché
useProps()les valeurs du formulaire déclaré avec defineProps
useSignals()un nouveau rendu quand les données de scène lues au rendu changent
useSyncedState()un état stocké sur l'objet et synchronisé comme tout composant

defineProps utilise les mêmes types de champs que les propriétés de script — nombre, chaîne, booléen, couleur, liste, objet, ressource — si bien que votre composant obtient gratuitement un vrai formulaire d'inspecteur.

Comment cela s'exécute

C'est du vrai React, pas une imitation simplifiée

Toutes les extensions de composant de tous les plugins tournent dans un unique worker partagé, avec React 19 dedans. Les hooks, useEffect, le contexte et Suspense fonctionnent exactement comme prévu.

Ce que produit le worker n'est pas du DOM mais une description de celui-ci, que l'éditeur dessine avec ses propres composants. C'est ce qui fait qu'une extension a l'air native plutôt que d'une page embarquée.

Deux conséquences de cette frontière :

  • Le jeu d'éléments est une liste fixe. Vous construisez à partir des composants fournis, pas de HTML arbitraire.
  • Les gestionnaires ne franchissent pas la frontière. On dit seulement à l'éditeur qu'un gestionnaire existe ; un clic revient sous forme de message et appelle votre fonction dans le worker. Vous écrivez onClick normalement — cela ne compte que si vous comptiez faire circuler un événement DOM.

Comment on l'ajoute

Votre extension apparaît dans + New component, dans un groupe Extensions — elle n'est pas traitée comme plus spéciale qu'un composant intégré.

Elle apparaît aussi dans la barre d'outils sous +, regroupée sous un titre Plugins pour qu'on voie d'où vient un bouton inconnu. Les deux points d'entrée se comportent différemment, et c'est voulu :

Ajouté depuisRésultat
+ New components'attache à l'objet déjà sélectionné
La barre d'outilscrée un nouvel objet avec votre composant et le sélectionne

Un dossier connecté localement apparaît aussi dans cette liste s'il a un panneau — sinon le seul moyen de l'atteindre serait d'ouvrir le panneau Plugins à chaque fois. Un dossier ne contenant qu'une surcouche n'apparaît pas : il n'y aurait rien à presser.

Confiance

Une extension de composant s'exécute sans être ouverte

Elle se monte dès que quelqu'un ouvre un projet qui la contient, sans le moindre clic. C'est ce qui la place derrière le consentement renforcé décrit dans confiance et vérification.


Suite : Raccourcis clavier