8 Commits
Author SHA1 Message Date
angeandClaude Opus 5.5 0027f74a56 Tests : code de retour 1 en cas d'échec (convention a2m, lancés avant chaque release)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:47:34 +02:00
angeandClaude Opus 5.5 c88a7f9a2a CLAUDE.md : documentation en deux langues (README et CHANGELOG fr/en)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:18:08 +02:00
ange 8a92c2b784 Version 1.4.2 2026-10-20 18:16:57 +02:00
angeandClaude Opus 5.5 d351379a23 README en deux langues : README.md (en), README.fr.md (fr)
Le README français devient README.fr.md (historique conservé) ; README.md est sa
traduction anglaise. Les fiches du site lisent les deux (contrat apc-spec §4.2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:16:21 +02:00
ange ed13bf6493 Version 1.4.1 2026-10-20 17:46:38 +02:00
ange a1bc74d294 Adoption par a2m : .a2m/project.toml 2026-10-20 17:17:55 +02:00
angeandClaude Opus 5.5 bc01a01e09 Préparation pour la chaîne d'outils a2m
- Manifest : Blender 5.1 minimum, maintainer Ange-Marie MAURIN, website
  (fiche a2msystemes.org), .a2m/ exclu du paquet
- CHANGELOG.md (en) et CHANGELOG.fr.md (fr) : historique 1.0.0 → 1.4.0 repris
  du CLAUDE.md, mise au format a2m dans la section non publiée
- tools/Build-Package.ps1 et tools/New-GiteaRelease.ps1 retirés : remplacés
  par a2m build et a2m release
- README, CLAUDE.md, .gitignore : commandes a2m, historique dans le CHANGELOG,
  note « dépôt d'extensions » rattachée à APC

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:14:32 +02:00
angeandClaude Opus 5.5 ec3ed21b1e CLAUDE.md : note sur le dépôt d'extensions Blender d'a2msystemes.org (2026-10-17)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:08:30 +02:00
11 changed files with 418 additions and 387 deletions

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
schema = 2
id = "slice_wrap"
type = "blender-addon"
source = "repo"
tags = ["blender-addons", "python", "modelisation-3d"]
license = "GPL-3.0-or-later"
status = "active"
[visibility]
level = "public"
[distribution]
featured = false
search = true
[title]
fr = "Slice Wrap"
en = "Slice Wrap"
[summary]
fr = "Nettoyage de mesh par slice, et lissage de surface"
en = "Closed envelope of holey meshes, then wrap, freeze and despike"
+1 -1
View File
@@ -2,7 +2,7 @@
__pycache__/
*.py[cod]
# Paquets d'extension (construits par tools/Build-Package.ps1)
# Paquets d'extension (construits par a2m build)
dist/
*.zip
+54
View File
@@ -0,0 +1,54 @@
# Journal des modifications
Format : [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) ; versions : [SemVer](https://semver.org/lang/fr/).
La section « Non publié » est reprise par `a2m release` sous le numéro de la nouvelle version
(version anglaise : [CHANGELOG.md](CHANGELOG.md)).
## [Non publié]
## [1.4.2] - 2026-10-20
### Modifié
- Documentation en deux langues : `README.md` en anglais, `README.fr.md` en français.
## [1.4.1] - 2026-10-20
### Modifié
- Version minimale de Blender : 5.1.
- Publication par la chaîne d'outils a2m, pour le dépôt d'extensions d'a2msystemes.org (mises à jour proposées
directement dans Blender) ; fiche du projet : https://a2msystemes.org/projets/slice_wrap.
## [1.4.0] - 2026-10-17
### Modifié
- Réglage Target supprimé : l'objet sélectionné est utilisé.
- Confirmation avant les calculs longs, barre de progression (barre d'état et panneau) ; Blender n'est plus bloqué,
Échap annule sans rien laisser.
## [1.3.0]
### Ajouté
- Suffix (`_slicewrapped`), numérotation automatique (plus aucun objet écrasé), collection optionnelle.
## [1.2.0]
### Ajouté
- Analyze (carte de détail, tags, direction des plis), Show Detail / Shapes, Protect Detail.
## [1.1.0]
### Ajouté
- Bouton unique « Slice Wrap » et Despike After ; Steps en sous-panneau.
- Despike : Distance, Strength, Iterations, filtres par groupes de sommets ; réparation des ratés au Freeze.
## [1.0.0]
### Ajouté
- Envelope, Regenerate, Wrap Setup, Freeze (Reshape), Despike.
+54
View File
@@ -0,0 +1,54 @@
# Changelog
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions: [SemVer](https://semver.org/).
The "Unreleased" section is taken by `a2m release` under the number of the new version
(French version: [CHANGELOG.fr.md](CHANGELOG.fr.md)).
## [Unreleased]
## [1.4.2] - 2026-10-20
### Changed
- Documentation in two languages: `README.md` in English, `README.fr.md` in French.
## [1.4.1] - 2026-10-20
### Changed
- Minimum Blender version: 5.1.
- Published by the a2m toolchain, for the a2msystemes.org extension repository (updates offered directly in
Blender); project page: https://a2msystemes.org/en/projects/slice_wrap.
## [1.4.0] - 2026-10-17
### Changed
- Target setting removed: the selected object is used.
- Confirmation before long computations, progress bar (status bar and panel); Blender is no longer blocked, Esc
cancels without leaving anything behind.
## [1.3.0]
### Added
- Suffix (`_slicewrapped`), automatic numbering (no object is ever overwritten), optional collection.
## [1.2.0]
### Added
- Analyze (detail map, tags, fold direction), Show Detail / Shapes, Protect Detail.
## [1.1.0]
### Added
- Single "Slice Wrap" button and Despike After; Steps as a sub-panel.
- Despike: Distance, Strength, Iterations, vertex group filters; missed rays repaired at Freeze.
## [1.0.0]
### Added
- Envelope, Regenerate, Wrap Setup, Freeze (Reshape), Despike.
+27 -18
View File
@@ -1,7 +1,9 @@
# Slice Wrap — add-on Blender : surface propre à partir d'un maillage troué, wrap, nettoyage et analyse
L'utilisateur parle **français** : répondre et documenter en français. Libellés d'interface Blender en anglais
(convention Blender), descriptions / docstrings / README en français.
(convention Blender), descriptions / docstrings en français. Documentation en deux langues, à tenir à jour
ensemble : `README.fr.md` en français, `README.md` en anglais (sa traduction) ; de même `CHANGELOG.fr.md` /
`CHANGELOG.md` (les fiches du site et les notes de version les lisent dans les deux langues).
## But
Transformer un maillage **troué, à doubles parois ou bruité** (scan, génération IA type Pixal3D / Trellis — ex.
@@ -79,35 +81,42 @@ flow ici : la retopologie guidée est un projet séparé, **Slice Retopo**
`voxelize`, `blocky_mesh`), nettoyage (`taubin`, `keep_largest_island`, `despike`, `group_weights`,
`fix_missed_rays`), analyse (`analyze_shape`, `write_analysis`), `unique_object_name`, `build_envelope`.
- `tests/run_tests.py` : suite (sphère trouée bruitée ; `--head` = tête réelle + rendus). Exclu du paquet.
- `blender_manifest.toml` : extension 4.2+ ; `[build]` exclut `__pycache__/`, zips, `.claude/`, `.git/`, `CLAUDE.md`,
`tests/`, `tools/`, `dist/`.
- `tools/Build-Package.ps1` (paquet), `tools/New-GiteaRelease.ps1` (release Gitea). Exclus du paquet.
- `blender_manifest.toml` : extension **5.1+** (décidé le 2026-10-20) ; `[build]` exclut `__pycache__/`, zips,
`.a2m/`, `.claude/`, `.git/`, `.gitignore`, `CLAUDE.md`, `tests/`, `dist/`.
- `.a2m/project.toml` : données du projet pour la chaîne d'outils a2m et le site (écrit par `a2m adopt`).
- `CHANGELOG.md` (en) / `CHANGELOG.fr.md` (fr) : historique ; la section « Unreleased » / « Non publié » donne les
notes de la prochaine release.
## Commandes
- Tests : `"C:\Program Files\Blender Foundation\Blender 5.2\blender.exe" -b -P tests/run_tests.py -- <dossier> [--head]`
→ `RESULTAT : TOUT OK` (~1 min, 29 contrôles dont annulations) ; avec `--head`, regarder `slice_wrap_head_face.png` / `_cote.png`.
- Paquet : `.\tools\Build-Package.ps1 [-RunTests]` → `dist\slice_wrap-<version>.zip` (validate + build + contrôle
du contenu). **Monter `version`** à chaque livraison. L'utilisateur installe lui-même le zip.
- Release : `.\tools\New-GiteaRelease.ps1 [-DryRun]` — Gitea `https://a2msystemes-git.duckdns.org`, dépôt
`ange/slice_wrap_addon` (déduit de `origin`), jeton `GITEA_TOKEN`. Exige un dépôt propre et poussé ; crée le tag
`v<version>`, la release (notes = entrée de l'Historique ci-dessous) et joint le zip. Publication = action
externe : ne la lancer qu'à la demande de l'utilisateur.
- `.gitignore` : `__pycache__/`, `dist/`, `*.zip`, rendus de test. Le manifeste exclut aussi `tools/`, `dist/`, `.git/`.
**À lancer avant chaque release** (la chaîne a2m ne lance pas encore les tests de l'addon).
- Construction et publication : chaîne d'outils **a2m** (repo `ange/apc-toolchain`, remplace les anciens
`tools/Build-Package.ps1` et `tools/New-GiteaRelease.ps1`, retirés le 2026-10-20) :
`a2m validate --load` (manifest validé et addon chargé dans Blender 5.1 et 5.2, profil temporaire),
`a2m build` → `dist\slice_wrap-<version>.zip`, `a2m release [patch|minor|major]` (version du manifest, CHANGELOG,
commit, tag `v<version>`, release Gitea avec le zip et `publication.json`), `a2m release --dev` (pré-release de
test, réservée aux membres). Publication = action externe : ne la lancer qu'à la demande de l'utilisateur.
- `.gitignore` : `__pycache__/`, `dist/`, `*.zip`, rendus de test.
## Historique
- 1.0.0 Envelope / Regenerate / Wrap Setup / Freeze (Reshape) / Despike.
- 1.1.0 Bouton unique « Slice Wrap » + Despike After ; Steps en sous-panneau ; Despike : Distance, Strength,
Iterations, filtres par groupes de sommets ; réparation des ratés au Freeze.
- 1.2.0 Analyze (carte de détail, tags, direction des plis), Show Detail / Shapes, Protect Detail.
- 1.3.0 Suffix (`_slicewrapped`), numérotation automatique (plus d'écrasement), collection optionnelle.
- 1.4.0 Réglage Target supprimé (objet sélectionné uniquement) ; confirmation avant les calculs longs, barre de
progression (barre d'état + panneau), Blender non bloqué, Échap annule sans rien laisser.
Voir [CHANGELOG.fr.md](CHANGELOG.fr.md) (une entrée par version ; les changements à venir dans « Non publié »).
## Mesures de référence (Blender 5.2.1, tête Pixal3D brute 11,8 M faces)
Enveloppe 70 k faces en 11 s ; chaîne complète ~1,5–2 min ; ~93 k ratés réparés au Freeze ; Despike filtré
« Oreilles » 683 corrections vs 4 749 global ; Analyze 9 s (Flat 86 %, Ridge 5 %, Bulge 3 %, Fold 3 %, Hollow 2 %,
Saddle 1 % — hélice des oreilles et lèvres en Ridge, paupières et commissures en Fold).
## Dépôt d'extensions Blender sur a2msystemes.org (idée du 2026-10-17, prise en charge par APC)
But : les addons de l'utilisateur (Slice Wrap, Slice Retopo, suivants) installables et mis à jour via
*Preferences > Get Extensions > Repositories > Add Remote Repository*, au lieu d'*Install from Disk*.
Cette note est à l'origine du projet **APC** (*A2M Publication Contract*) : le dépôt n'est plus à écrire ici.
- **Publication** : `a2m release` (chaîne d'outils `ange/apc-toolchain`) joint à chaque release Gitea le zip et un
`publication.json` (contrat `ange/apc-spec`) ; le script de déploiement du site (`ange/site-a2msystemes-org`)
en fabrique `blender-addons/index.json` et les pages du projet. Adresse du dépôt et pages : voir le site.
- **À rappeler à l'utilisateur** : les addons déjà installés par *Install from Disk* (dépôt `user_default`) ne
reçoivent pas les mises à jour → les désinstaller puis réinstaller une fois depuis le nouveau dépôt.
## Pistes
- Option avancée repliée pour Absolute ; préréglages par type d'objet (tête, main, accessoire).
- Accélérer le remplissage (propagation par lignes / scanline au lieu d'une dilatation voxel par voxel).
+138
View File
@@ -0,0 +1,138 @@
# Slice Wrap — add-on Blender (5.1 → 5.x)
*[English version](README.md)*
Transforme un maillage **troué, à doubles parois ou bruité** (scan, génération IA type Pixal3D / Trellis)
en une **surface propre, fermée et détaillée**, prête à sculpter. Aucune dépendance : numpy seulement
(fourni avec Blender).
Panneau : **Vue 3D > barre latérale (N) > onglet « Slice Wrap »**.
## Pourquoi pas le Voxel Remesh de Blender ?
Le Voxel Remesh (et *Mesh to Volume* en Geometry Nodes) reconstruit un volume… mais **fuit** dès que la
coque a un trou : l'intérieur n'est pas rempli et on obtient une double peau, du moiré, des ratés.
Slice Wrap remplit le volume **tranche par tranche selon X, Y et Z** : un trou ne fait fuir que les quelques
tranches qu'il traverse, les autres directions le compensent.
## Utilisation
Tout s'applique à **l'objet sélectionné** (actif) : il n'y a pas de cible à choisir. La projection se fait toujours
sur la source de l'enveloppe. Sans maillage sélectionné, les boutons sont grisés.
1. Sélectionnez le maillage source (troué) en mode Objet.
2. Réglez au besoin la section **Envelope** et **Wrap**, puis cliquez **Slice Wrap** (un clic) :
Envelope → Wrap Setup → Freeze, et **Despike** en plus si **Despike After** est coché.
3. Le résultat `<source><suffixe>` (par défaut `<source>_slicewrapped`) est prêt à sculpter (base légère + niveaux
Multires). La source est cachée.
### Calculs longs : confirmation, progression, annulation
Slice Wrap, Envelope, Regenerate et Despike affichent d'abord un message : le calcul peut être long, Blender
ignorera les clics pendant ce temps (il n'est pas planté), Échap annule. Case « Show this message next time »
(aussi dans *Preferences > Add-ons > Slice Wrap > Confirm Long Operations*).
Pendant le calcul (ainsi que Freeze et Analyze) : **barre de progression** avec l'étape en cours et le temps écoulé,
dans la barre d'état en bas de la fenêtre et en haut du panneau. **Échap** annule : rien n'est créé ni modifié.
Limite : quelques opérations internes de Blender (subdivision Multires, Multires Reshape, Voxel Remesh) sont d'un
seul tenant. Sur un maillage lourd, l'affichage reste figé quelques secondes sur cette étape, puis la progression
reprend.
### Nom et rangement du résultat
- **Suffix** (défaut `_slicewrapped`) : nom du maillage créé = nom de la source + suffixe.
- **Numérotation automatique** : si le nom existe déjà, `.001`, `.002`… comme Blender — **aucun objet existant
n'est jamais écrasé** (même avec un suffixe vide, la source reste intacte).
- **Collection** (case + nom, défaut `SliceWrapCollection`) : le résultat y est rangé ; si la collection existe,
elle est simplement réutilisée (son contenu n'est pas touché), sinon elle est créée et liée à la scène.
Case décochée : le résultat va dans les collections de la source.
- **Regenerate** est le seul bouton qui recalcule un objet existant : l'enveloppe active, sur place.
### Sous-panneau « Steps » (replié) — chaque étape séparément
| # | Bouton | Ce qu'il fait |
|---|--------|---------------|
| 1 | **Envelope** | Sur l'objet actif (la source, jamais modifiée) : voxels → fermeture des trous → remplissage par tranches → surface fermée (Voxel Remesh sur volume déjà plein, sans fuite) → plus grande pièce → lissage de Taubin. Crée `<source><suffixe>` (numéroté si le nom existe). |
| | ↻ **Regenerate** | Recalcule l'enveloppe active depuis sa source avec les réglages actuels (pseudo-modificateur). Refusé si l'enveloppe est déjà wrappée. |
| 2 | **Wrap Setup** | Gonfle l'enveloppe le long des normales (pour qu'elle soit entièrement *au-dessus* de la peau), ajoute **Multires** (N niveaux) puis **Shrinkwrap** : *Project*, le long des normales, **vers l'intérieur seulement**, avec une distance limite. |
| 3 | **Freeze** | Fige la projection **dans les niveaux du Multires** (Multires Reshape) et retire le Shrinkwrap. Avec **Repair Missed Rays**, les sommets dont la projection a raté — rien touché (aiguilles) ou trop loin (creux sur une paroi interne) — sont détectés exactement (comparaison avant / après) et reconstruits depuis leurs voisins. |
Astuce : pour couper le socle / les épaules avant la projection, faites **Envelope**, coupez (Bisect), puis
**Wrap Setup** et **Freeze**.
### Sous-panneau « Despike » — outil manuel
Filtre anti-pointes résiduel : un sommet n'est corrigé que s'il est **isolé**, nettement plus loin de la moyenne
de ses voisins que ceux-ci ne le sont des leurs ; le relief normal (oreilles, paupières) n'est pas lissé.
- **Distance** : écart minimal pour être une pointe (0 = automatique : longueur moyenne d'arête).
- **Strength** (0–1) : part du chemin vers la moyenne des voisins.
- **Iterations** : nombre de passes. **Isolation** : la pointe doit dévier N fois plus que ses voisins.
- **Vertex Group Filters** : liste (+ / − / vider) des groupes de sommets à traiter. Le **poids** du groupe module
la force (fondu possible en bordure). **Liste vide = toute la surface.** Si la liste contient des groupes
dont aucun n'existe, le Despike refuse de s'exécuter (pour ne jamais traiter toute la surface par erreur).
Les groupes de la base sont propagés aux niveaux du Multires.
- **Protect Detail** : atténue le Despike en proportion du détail (analyse de forme recalculée à la volée), pour
ne jamais aplatir paupières, narines ou oreilles.
### Sous-panneau « Analyze » — optionnel, ne modifie jamais la géométrie
Analyse la forme de l'objet actif (Multires compris) : pour chaque sommet, la répartition des normales sur un
voisinage (tenseur de covariance) et la courbure moyenne signée. Résultat écrit sur le maillage de base :
| Donnée | Type | Contenu |
|--------|------|---------|
| `sw_detail` | attribut float 0–1 | densité de détail (la surface « tourne » beaucoup) — carte de densité pour une retopo |
| `sw_curvature` | attribut float −1…1 | > 0 convexe (bosse), < 0 concave (creux) |
| `sw_anisotropy` | attribut float 0–1 | ~1 : la surface tourne dans **une** direction (pli, crête, doigt) ; ~0 : dans toutes |
| `sw_shape` | attribut entier | tag : 0 Flat, 1 Bulge (dôme), 2 Hollow (cuvette), 3 Ridge (crête), 4 Fold (pli), 5 Saddle (selle) |
| `sw_flow` | attribut vecteur | direction du pli / de la crête = direction que doit suivre une boucle d'arêtes |
| `SW_Detail_Color`, `SW_Shape_Color` | couleurs | affichage (boutons **Detail** / **Shapes**, ✕ pour revenir) |
| `SW_Detail`, `SW_Bulge`, `SW_Hollow`, `SW_Ridge`, `SW_Fold`, `SW_Saddle` | groupes de sommets | filtres Despike, masques, ou carte de densité d'un remailleur |
Couleurs des tags : gris Flat · rouge Bulge · bleu Hollow · jaune Ridge · cyan Fold · violet Saddle.
Les couleurs portent le suffixe `_Color` car groupes de sommets et attributs partagent le même espace de noms.
## Réglages
- **Voxel : Relative / Absolute** — la finesse de l'enveloppe.
- *Relative* (défaut, **Voxel %** = 0,6 % de la plus grande dimension) : indépendant de l'échelle — le même
réglage donne le même résultat sur une tête de 23 cm, une tête normalisée à 1 m ou une main importée à 3,7 m.
Plus petit = plus fidèle mais plus lourd ; trop fin sur une source bruitée → la projection « voit » le grain.
- *Absolute* (**Voxel Size** en unités) : même finesse physique sur plusieurs objets de tailles différentes
(ex. tête et mains d'un même personnage, pour qu'elles se raccordent avec une densité identique), ou pour
viser une densité précise. La valeur équivalente du mode Relative est affichée sous le champ.
- **Close Holes** (3) : bouche les trous jusqu'à ~2 × cette valeur en voxels.
- **Slice Fill X/Y/Z** : axes de remplissage par tranches (les trois par défaut).
- **Remove Filaments** (1), **Smooth** (20 itérations de Taubin).
- **Inflate** (0 = auto : 2 voxels) et **Limit** (0 = auto : 4 × gonflement) pour le Shrinkwrap.
- **Multires Levels** (2) : densité de la projection (×16 faces).
- **Repair Missed Rays** (activé) et **Despike After** (désactivé) pour la chaîne en un clic.
- Analyze : **Neighborhood** (8 anneaux d'arêtes), **Flat Below** (0,15), **Directional Above** (0,55).
## Conseils
- Couper la source **et** l'enveloppe là où elles ne servent pas (socle, épaules) améliore la projection.
- Les **surplombs** (pavillon de l'oreille) restent le point faible d'une projection le long des normales :
quelques pointes peuvent subsister → Despike, ou pinceau Smooth.
- Après Freeze, l'objet garde une **base légère** + niveaux Multires : sculptez, ou bakez les détails.
## Installation
- **Depuis le dépôt d'extensions d'a2msystemes.org** (à venir) : les mises à jour seront proposées directement dans
Blender. Voir https://a2msystemes.org/projets/slice_wrap.
- **Paquet** : `slice_wrap-<version>.zip` (page *Releases* du dépôt Gitea, ou `dist\` après construction) →
*Edit > Preferences > Get Extensions > ⌄ > Install from Disk…*
- **Construire et publier** : chaîne d'outils `a2m` (repo `ange/apc-toolchain`) — `a2m validate --load`,
`a2m build` (→ `dist\slice_wrap-<version>.zip`), `a2m release [patch|minor|major]` (version, CHANGELOG, tag,
release Gitea avec le zip et `publication.json`), `a2m release --dev` (pré-release de test).
Historique des versions : [CHANGELOG.fr.md](CHANGELOG.fr.md).
- **Développement** : copier / lier ce dossier dans
`%APPDATA%\Blender Foundation\Blender\<version>\extensions\user_default\slice_wrap`.
## Fichiers
- `__init__.py` : réglages, opérateurs, panneaux (principal, Steps, Despike, Analyze), liste des groupes.
- `core.py` : algorithmes (voxelisation, morphologie, remplissage par tranches, Taubin, réparation des ratés, despike,
analyse de forme).
- `blender_manifest.toml` : manifeste d'extension.
## Tests (Blender 5.2.1)
Tête Pixal3D brute de 11,8 M faces → enveloppe 70 k faces (11 s) → Wrap Multires 2 → Freeze avec réparation
(~93 k ratés réparés) — chaîne complète en ~1,5–2 min. Despike filtré sur un groupe « Oreilles » : 683 sommets
corrigés contre 4 749 sur toute la surface. Analyze (1,1 M sommets évalués) en 9 s : Flat 86 %, Ridge 5 %,
Bulge 3 %, Fold 3 %, Hollow 2 %, Saddle 1 % — hélice des oreilles et lèvres en Ridge, paupières et commissures en Fold.
## Pistes (non implémentées)
- **Marqueurs de flow** dessinés par l'utilisateur (courbes / Grease Pencil sur la surface) avec des tags
(« pli », « gonflement », « boucle œil »…), combinés aux tags automatiques d'Analyze.
- **Stratégies par tag** pour une retopologie guidée : boucles parallèles à `sw_flow` sur un Fold / Ridge, anneaux
concentriques autour d'un Hollow (œil, bouche), pôle au sommet d'un Bulge, anneaux perpendiculaires sur un
cylindre (doigt), densité pilotée par `sw_detail`.
+114 -113
View File
@@ -1,136 +1,137 @@
# Slice Wrap — add-on Blender (4.2 → 5.x)
# Slice Wrap — Blender add-on (5.1 → 5.x)
Transforme un maillage **troué, à doubles parois ou bruité** (scan, génération IA type Pixal3D / Trellis)
en une **surface propre, fermée et détaillée**, prête à sculpter. Aucune dépendance : numpy seulement
(fourni avec Blender).
*[Version française](README.fr.md)*
Panneau : **Vue 3D > barre latérale (N) > onglet « Slice Wrap »**.
Turns a **holey, double-walled or noisy** mesh (scan, AI generation such as Pixal3D / Trellis) into a
**clean, closed and detailed surface**, ready to sculpt. No dependencies: numpy only (shipped with Blender).
## Pourquoi pas le Voxel Remesh de Blender ?
Le Voxel Remesh (et *Mesh to Volume* en Geometry Nodes) reconstruit un volume… mais **fuit** dès que la
coque a un trou : l'intérieur n'est pas rempli et on obtient une double peau, du moiré, des ratés.
Slice Wrap remplit le volume **tranche par tranche selon X, Y et Z** : un trou ne fait fuir que les quelques
tranches qu'il traverse, les autres directions le compensent.
Panel: **3D Viewport > Sidebar (N) > "Slice Wrap" tab**.
## Utilisation
Tout s'applique à **l'objet sélectionné** (actif) : il n'y a pas de cible à choisir. La projection se fait toujours
sur la source de l'enveloppe. Sans maillage sélectionné, les boutons sont grisés.
## Why not Blender's Voxel Remesh?
Voxel Remesh (and *Mesh to Volume* in Geometry Nodes) rebuilds a volume… but **leaks** as soon as the shell has a
hole: the inside is not filled and you get a double skin, moiré and misses.
Slice Wrap fills the volume **slice by slice along X, Y and Z**: a hole only leaks into the few slices it crosses,
and the other directions make up for it.
1. Sélectionnez le maillage source (troué) en mode Objet.
2. Réglez au besoin la section **Envelope** et **Wrap**, puis cliquez **Slice Wrap** (un clic) :
Envelope → Wrap Setup → Freeze, et **Despike** en plus si **Despike After** est coché.
3. Le résultat `<source><suffixe>` (par défaut `<source>_slicewrapped`) est prêt à sculpter (base légère + niveaux
Multires). La source est cachée.
## Usage
Everything applies to the **selected (active) object**: there is no target to pick. The projection always goes
onto the source of the envelope. Without a selected mesh, the buttons are greyed out.
### Calculs longs : confirmation, progression, annulation
Slice Wrap, Envelope, Regenerate et Despike affichent d'abord un message : le calcul peut être long, Blender
ignorera les clics pendant ce temps (il n'est pas planté), Échap annule. Case « Show this message next time »
(aussi dans *Preferences > Add-ons > Slice Wrap > Confirm Long Operations*).
Pendant le calcul (ainsi que Freeze et Analyze) : **barre de progression** avec l'étape en cours et le temps écoulé,
dans la barre d'état en bas de la fenêtre et en haut du panneau. **Échap** annule : rien n'est créé ni modifié.
Limite : quelques opérations internes de Blender (subdivision Multires, Multires Reshape, Voxel Remesh) sont d'un
seul tenant. Sur un maillage lourd, l'affichage reste figé quelques secondes sur cette étape, puis la progression
reprend.
1. Select the (holey) source mesh in Object Mode.
2. Adjust the **Envelope** and **Wrap** sections if needed, then click **Slice Wrap** (one click):
Envelope → Wrap Setup → Freeze, plus **Despike** if **Despike After** is checked.
3. The result `<source><suffix>` (by default `<source>_slicewrapped`) is ready to sculpt (light base + Multires
levels). The source is hidden.
### Nom et rangement du résultat
- **Suffix** (défaut `_slicewrapped`) : nom du maillage créé = nom de la source + suffixe.
- **Numérotation automatique** : si le nom existe déjà, `.001`, `.002`… comme Blender — **aucun objet existant
n'est jamais écrasé** (même avec un suffixe vide, la source reste intacte).
- **Collection** (case + nom, défaut `SliceWrapCollection`) : le résultat y est rangé ; si la collection existe,
elle est simplement réutilisée (son contenu n'est pas touché), sinon elle est créée et liée à la scène.
Case décochée : le résultat va dans les collections de la source.
- **Regenerate** est le seul bouton qui recalcule un objet existant : l'enveloppe active, sur place.
### Long computations: confirmation, progress, cancel
Slice Wrap, Envelope, Regenerate and Despike first show a message: the computation can take a while, Blender will
ignore clicks meanwhile (it has not crashed), Esc cancels. "Show this message next time" checkbox (also in
*Preferences > Add-ons > Slice Wrap > Confirm Long Operations*).
During the computation (as well as Freeze and Analyze): a **progress bar** with the current step and the elapsed
time, in the status bar at the bottom of the window and at the top of the panel. **Esc** cancels: nothing is
created or modified.
Limitation: a few internal Blender operations (Multires subdivision, Multires Reshape, Voxel Remesh) run in one
go. On a heavy mesh, the display stays frozen on that step for a few seconds, then progress resumes.
### Sous-panneau « Steps » (replié) — chaque étape séparément
| # | Bouton | Ce qu'il fait |
|---|--------|---------------|
| 1 | **Envelope** | Sur l'objet actif (la source, jamais modifiée) : voxels → fermeture des trous → remplissage par tranches → surface fermée (Voxel Remesh sur volume déjà plein, sans fuite) → plus grande pièce → lissage de Taubin. Crée `<source><suffixe>` (numéroté si le nom existe). |
| | ↻ **Regenerate** | Recalcule l'enveloppe active depuis sa source avec les réglages actuels (pseudo-modificateur). Refusé si l'enveloppe est déjà wrappée. |
| 2 | **Wrap Setup** | Gonfle l'enveloppe le long des normales (pour qu'elle soit entièrement *au-dessus* de la peau), ajoute **Multires** (N niveaux) puis **Shrinkwrap** : *Project*, le long des normales, **vers l'intérieur seulement**, avec une distance limite. |
| 3 | **Freeze** | Fige la projection **dans les niveaux du Multires** (Multires Reshape) et retire le Shrinkwrap. Avec **Repair Missed Rays**, les sommets dont la projection a raté — rien touché (aiguilles) ou trop loin (creux sur une paroi interne) — sont détectés exactement (comparaison avant / après) et reconstruits depuis leurs voisins. |
### Naming and placing the result
- **Suffix** (default `_slicewrapped`): name of the created mesh = source name + suffix.
- **Automatic numbering**: if the name already exists, `.001`, `.002`… like Blender — **no existing object is ever
overwritten** (even with an empty suffix, the source stays intact).
- **Collection** (checkbox + name, default `SliceWrapCollection`): the result is placed in it; if the collection
exists it is simply reused (its content is not touched), otherwise it is created and linked to the scene.
Unchecked: the result goes into the collections of the source.
- **Regenerate** is the only button that recomputes an existing object: the active envelope, in place.
Astuce : pour couper le socle / les épaules avant la projection, faites **Envelope**, coupez (Bisect), puis
**Wrap Setup** et **Freeze**.
### "Steps" sub-panel (collapsed) — each step separately
| # | Button | What it does |
|---|--------|--------------|
| 1 | **Envelope** | On the active object (the source, never modified): voxels → hole closing → slice filling → closed surface (Voxel Remesh on an already full volume, no leak) → largest piece → Taubin smoothing. Creates `<source><suffix>` (numbered if the name exists). |
| | ↻ **Regenerate** | Recomputes the active envelope from its source with the current settings (pseudo-modifier). Refused if the envelope is already wrapped. |
| 2 | **Wrap Setup** | Inflates the envelope along the normals (so that it lies entirely *above* the skin), adds **Multires** (N levels) then **Shrinkwrap**: *Project*, along the normals, **inwards only**, with a limit distance. |
| 3 | **Freeze** | Freezes the projection **into the Multires levels** (Multires Reshape) and removes the Shrinkwrap. With **Repair Missed Rays**, vertices whose projection missed — nothing hit (needles) or too far (hollow on an inner wall) — are detected exactly (before / after comparison) and rebuilt from their neighbours. |
### Sous-panneau « Despike » — outil manuel
Filtre anti-pointes résiduel : un sommet n'est corrigé que s'il est **isolé**, nettement plus loin de la moyenne
de ses voisins que ceux-ci ne le sont des leurs ; le relief normal (oreilles, paupières) n'est pas lissé.
- **Distance** : écart minimal pour être une pointe (0 = automatique : longueur moyenne d'arête).
- **Strength** (0–1) : part du chemin vers la moyenne des voisins.
- **Iterations** : nombre de passes. **Isolation** : la pointe doit dévier N fois plus que ses voisins.
- **Vertex Group Filters** : liste (+ / − / vider) des groupes de sommets à traiter. Le **poids** du groupe module
la force (fondu possible en bordure). **Liste vide = toute la surface.** Si la liste contient des groupes
dont aucun n'existe, le Despike refuse de s'exécuter (pour ne jamais traiter toute la surface par erreur).
Les groupes de la base sont propagés aux niveaux du Multires.
- **Protect Detail** : atténue le Despike en proportion du détail (analyse de forme recalculée à la volée), pour
ne jamais aplatir paupières, narines ou oreilles.
Tip: to cut the base / shoulders before projecting, run **Envelope**, cut (Bisect), then **Wrap Setup** and
**Freeze**.
### Sous-panneau « Analyze » — optionnel, ne modifie jamais la géométrie
Analyse la forme de l'objet actif (Multires compris) : pour chaque sommet, la répartition des normales sur un
voisinage (tenseur de covariance) et la courbure moyenne signée. Résultat écrit sur le maillage de base :
### "Despike" sub-panel — manual tool
Residual anti-spike filter: a vertex is only corrected if it is **isolated**, clearly further from the average of
its neighbours than they are from theirs; normal relief (ears, eyelids) is not smoothed.
- **Distance**: minimum deviation to count as a spike (0 = automatic: average edge length).
- **Strength** (0–1): fraction of the way towards the neighbours' average.
- **Iterations**: number of passes. **Isolation**: the spike must deviate N times more than its neighbours.
- **Vertex Group Filters**: list (+ / − / clear) of the vertex groups to process. The group **weight** modulates
the strength (fade-out possible at the border). **Empty list = the whole surface.** If the list contains groups
none of which exist, Despike refuses to run (so as never to process the whole surface by mistake).
The groups of the base are propagated to the Multires levels.
- **Protect Detail**: attenuates Despike in proportion to the detail (shape analysis computed on the fly), so as
never to flatten eyelids, nostrils or ears.
| Donnée | Type | Contenu |
|--------|------|---------|
| `sw_detail` | attribut float 0–1 | densité de détail (la surface « tourne » beaucoup) — carte de densité pour une retopo |
| `sw_curvature` | attribut float −1…1 | > 0 convexe (bosse), < 0 concave (creux) |
| `sw_anisotropy` | attribut float 0–1 | ~1 : la surface tourne dans **une** direction (pli, crête, doigt) ; ~0 : dans toutes |
| `sw_shape` | attribut entier | tag : 0 Flat, 1 Bulge (dôme), 2 Hollow (cuvette), 3 Ridge (crête), 4 Fold (pli), 5 Saddle (selle) |
| `sw_flow` | attribut vecteur | direction du pli / de la crête = direction que doit suivre une boucle d'arêtes |
| `SW_Detail_Color`, `SW_Shape_Color` | couleurs | affichage (boutons **Detail** / **Shapes**, ✕ pour revenir) |
| `SW_Detail`, `SW_Bulge`, `SW_Hollow`, `SW_Ridge`, `SW_Fold`, `SW_Saddle` | groupes de sommets | filtres Despike, masques, ou carte de densité d'un remailleur |
### "Analyze" sub-panel — optional, never modifies the geometry
Analyses the shape of the active object (Multires included): for each vertex, the spread of the normals over a
neighbourhood (covariance tensor) and the signed mean curvature. Result written on the base mesh:
Couleurs des tags : gris Flat · rouge Bulge · bleu Hollow · jaune Ridge · cyan Fold · violet Saddle.
Les couleurs portent le suffixe `_Color` car groupes de sommets et attributs partagent le même espace de noms.
| Data | Type | Content |
|------|------|---------|
| `sw_detail` | float attribute 0–1 | detail density (the surface "turns" a lot) — density map for a retopology |
| `sw_curvature` | float attribute −1…1 | > 0 convex (bump), < 0 concave (hollow) |
| `sw_anisotropy` | float attribute 0–1 | ~1: the surface turns in **one** direction (fold, ridge, finger); ~0: in all directions |
| `sw_shape` | integer attribute | tag: 0 Flat, 1 Bulge (dome), 2 Hollow (bowl), 3 Ridge, 4 Fold, 5 Saddle |
| `sw_flow` | vector attribute | direction of the fold / ridge = direction an edge loop should follow |
| `SW_Detail_Color`, `SW_Shape_Color` | colours | display (**Detail** / **Shapes** buttons, ✕ to go back) |
| `SW_Detail`, `SW_Bulge`, `SW_Hollow`, `SW_Ridge`, `SW_Fold`, `SW_Saddle` | vertex groups | Despike filters, masks, or density map for a remesher |
## Réglages
- **Voxel : Relative / Absolute** — la finesse de l'enveloppe.
- *Relative* (défaut, **Voxel %** = 0,6 % de la plus grande dimension) : indépendant de l'échelle — le même
réglage donne le même résultat sur une tête de 23 cm, une tête normalisée à 1 m ou une main importée à 3,7 m.
Plus petit = plus fidèle mais plus lourd ; trop fin sur une source bruitée → la projection « voit » le grain.
- *Absolute* (**Voxel Size** en unités) : même finesse physique sur plusieurs objets de tailles différentes
(ex. tête et mains d'un même personnage, pour qu'elles se raccordent avec une densité identique), ou pour
viser une densité précise. La valeur équivalente du mode Relative est affichée sous le champ.
- **Close Holes** (3) : bouche les trous jusqu'à ~2 × cette valeur en voxels.
- **Slice Fill X/Y/Z** : axes de remplissage par tranches (les trois par défaut).
- **Remove Filaments** (1), **Smooth** (20 itérations de Taubin).
- **Inflate** (0 = auto : 2 voxels) et **Limit** (0 = auto : 4 × gonflement) pour le Shrinkwrap.
- **Multires Levels** (2) : densité de la projection (×16 faces).
- **Repair Missed Rays** (activé) et **Despike After** (désactivé) pour la chaîne en un clic.
- Analyze : **Neighborhood** (8 anneaux d'arêtes), **Flat Below** (0,15), **Directional Above** (0,55).
Tag colours: grey Flat · red Bulge · blue Hollow · yellow Ridge · cyan Fold · purple Saddle.
Colours carry the `_Color` suffix because vertex groups and attributes share the same namespace.
## Conseils
- Couper la source **et** l'enveloppe là où elles ne servent pas (socle, épaules) améliore la projection.
- Les **surplombs** (pavillon de l'oreille) restent le point faible d'une projection le long des normales :
quelques pointes peuvent subsister → Despike, ou pinceau Smooth.
- Après Freeze, l'objet garde une **base légère** + niveaux Multires : sculptez, ou bakez les détails.
## Settings
- **Voxel: Relative / Absolute** — the resolution of the envelope.
- *Relative* (default, **Voxel %** = 0.6 % of the largest dimension): scale-independent — the same setting gives
the same result on a 23 cm head, a head normalised to 1 m or a hand imported at 3.7 m.
Smaller = more faithful but heavier; too fine on a noisy source → the projection "sees" the grain.
- *Absolute* (**Voxel Size** in units): same physical resolution on several objects of different sizes
(e.g. head and hands of the same character, so that they join with an identical density), or to target a
precise density. The equivalent value of the Relative mode is shown under the field.
- **Close Holes** (3): plugs holes up to ~2 × this value in voxels.
- **Slice Fill X/Y/Z**: slice filling axes (all three by default).
- **Remove Filaments** (1), **Smooth** (20 Taubin iterations).
- **Inflate** (0 = auto: 2 voxels) and **Limit** (0 = auto: 4 × inflate) for the Shrinkwrap.
- **Multires Levels** (2): density of the projection (×16 faces).
- **Repair Missed Rays** (on) and **Despike After** (off) for the one-click chain.
- Analyze: **Neighborhood** (8 edge rings), **Flat Below** (0.15), **Directional Above** (0.55).
## Tips
- Cutting the source **and** the envelope where they are not needed (base, shoulders) improves the projection.
- **Overhangs** (outer ear) remain the weak point of a projection along the normals: a few spikes may remain →
Despike, or the Smooth brush.
- After Freeze, the object keeps a **light base** + Multires levels: sculpt, or bake the details.
## Installation
- **Paquet** : `slice_wrap-1.4.0.zip` (page *Releases* du dépôt Gitea, ou `dist\` après construction) →
- **From the a2msystemes.org extension repository** (coming soon): updates will be offered directly in Blender.
See https://a2msystemes.org/en/projects/slice_wrap.
- **Package**: `slice_wrap-<version>.zip` (*Releases* page of the Gitea repository, or `dist\` after building) →
*Edit > Preferences > Get Extensions > ⌄ > Install from Disk…*
- **Construire le paquet** : `.\tools\Build-Package.ps1` (→ `dist\slice_wrap-<version>.zip` ; `-RunTests` lance
d'abord les tests ; Blender trouvé automatiquement, ou `-Blender <chemin>` / variable `BLENDER_EXE`).
- **Publier une release Gitea** : monter `version` dans `blender_manifest.toml`, commiter et pousser, puis
`.\tools\New-GiteaRelease.ps1` (tests, paquet, tag `v<version>`, release avec le zip en pièce jointe).
Jeton : variable `GITEA_TOKEN` (sinon demandé). `-DryRun` montre ce qui serait fait sans rien publier ;
`-Prerelease`, `-Draft`, `-Notes "…"` (par défaut : l'entrée de la version dans l'historique).
- **Développement** : copier / lier ce dossier dans
- **Build and publish**: `a2m` toolchain (repository `ange/apc-toolchain`) — `a2m validate --load`, `a2m build`
(→ `dist\slice_wrap-<version>.zip`), `a2m release [patch|minor|major]` (version, CHANGELOG, tag, Gitea release
with the zip and `publication.json`), `a2m release --dev` (test pre-release).
Version history: [CHANGELOG.md](CHANGELOG.md).
- **Development**: copy / link this folder into
`%APPDATA%\Blender Foundation\Blender\<version>\extensions\user_default\slice_wrap`.
## Fichiers
- `__init__.py` : réglages, opérateurs, panneaux (principal, Steps, Despike, Analyze), liste des groupes.
- `core.py` : algorithmes (voxelisation, morphologie, remplissage par tranches, Taubin, réparation des ratés, despike,
analyse de forme).
- `blender_manifest.toml` : manifeste d'extension.
## Files
- `__init__.py`: settings, operators, panels (main, Steps, Despike, Analyze), group list.
- `core.py`: algorithms (voxelisation, morphology, slice filling, Taubin, missed-ray repair, despike, shape
analysis).
- `blender_manifest.toml`: extension manifest.
## Tests (Blender 5.2.1)
Tête Pixal3D brute de 11,8 M faces → enveloppe 70 k faces (11 s) → Wrap Multires 2 → Freeze avec réparation
(~93 k ratés réparés) — chaîne complète en ~1,5–2 min. Despike filtré sur un groupe « Oreilles » : 683 sommets
corrigés contre 4 749 sur toute la surface. Analyze (1,1 M sommets évalués) en 9 s : Flat 86 %, Ridge 5 %,
Bulge 3 %, Fold 3 %, Hollow 2 %, Saddle 1 % — hélice des oreilles et lèvres en Ridge, paupières et commissures en Fold.
Raw Pixal3D head of 11.8 M faces → 70 k-face envelope (11 s) → Wrap Multires 2 → Freeze with repair (~93 k misses
repaired) — full chain in ~1.5–2 min. Despike filtered on an "Ears" group: 683 vertices corrected versus 4,749 on
the whole surface. Analyze (1.1 M evaluated vertices) in 9 s: Flat 86 %, Ridge 5 %, Bulge 3 %, Fold 3 %, Hollow 2 %,
Saddle 1 % — ear helix and lips as Ridge, eyelids and mouth corners as Fold.
## Pistes (non implémentées)
- **Marqueurs de flow** dessinés par l'utilisateur (courbes / Grease Pencil sur la surface) avec des tags
(« pli », « gonflement », « boucle œil »…), combinés aux tags automatiques d'Analyze.
- **Stratégies par tag** pour une retopologie guidée : boucles parallèles à `sw_flow` sur un Fold / Ridge, anneaux
concentriques autour d'un Hollow (œil, bouche), pôle au sommet d'un Bulge, anneaux perpendiculaires sur un
cylindre (doigt), densité pilotée par `sw_detail`.
## Ideas (not implemented)
- **Flow markers** drawn by the user (curves / Grease Pencil on the surface) with tags ("fold", "bulge",
"eye loop"…), combined with the automatic Analyze tags.
- **Per-tag strategies** for a guided retopology: loops parallel to `sw_flow` on a Fold / Ridge, concentric rings
around a Hollow (eye, mouth), a pole at the top of a Bulge, perpendicular rings on a cylinder (finger), density
driven by `sw_detail`.
+5 -4
View File
@@ -1,15 +1,16 @@
schema_version = "1.0.0"
id = "slice_wrap"
version = "1.4.0"
version = "1.4.2"
name = "Slice Wrap"
tagline = "Closed envelope of holey meshes, then wrap, freeze and despike"
maintainer = "a-mma"
maintainer = "Ange-Marie MAURIN"
type = "add-on"
website = "https://a2msystemes.org/projets/slice_wrap"
tags = ["Mesh", "Modeling", "Sculpt"]
blender_version_min = "4.2.0"
blender_version_min = "5.1.0"
license = [
"SPDX:GPL-3.0-or-later",
@@ -19,11 +20,11 @@ license = [
paths_exclude_pattern = [
"__pycache__/",
"/*.zip",
"/.a2m/",
"/.claude/",
"/.git/",
"/.gitignore",
"/CLAUDE.md",
"/tests/",
"/tools/",
"/dist/",
]
+3 -1
View File
@@ -4,7 +4,8 @@
- rapide (~40 s) : sphère trouée et bruitée → nom / numérotation / collection / Regenerate, Envelope fermée,
Wrap + Freeze (réparation), Despike (global, filtré, refus groupe absent, Protect Detail), Analyze.
- --head : chaîne complète sur la tête brute Pixal3D (11,8 M faces, ~2 min) + rendus de contrôle.
Termine par « RESULTAT : TOUT OK » ou la liste des échecs.
Termine par « RESULTAT : TOUT OK » ou la liste des échecs ; code de retour 0 si tout est bon, 1 sinon
(convention de la chaîne a2m : `tests/run_tests.py`, lancé avant chaque release).
"""
import importlib
import math
@@ -236,3 +237,4 @@ if "--head" in ARGS and os.path.isfile(HEAD):
mod.unregister()
print("RESULTAT :", "TOUT OK" if not fails else "ECHECS " + ", ".join(fails))
sys.exit(1 if fails else 0) # code de retour lu par « a2m validate --tests » et « a2m release »
-78
View File
@@ -1,78 +0,0 @@
<#
.SYNOPSIS
Construit le paquet d'extension Blender de Slice Wrap dans dist\.
.DESCRIPTION
Valide blender_manifest.toml, construit slice_wrap-<version>.zip avec « blender --command extension build »
(exclusions : section [build] du manifeste), puis vérifie que le zip ne contient ni tests, ni .claude, ni
CLAUDE.md, ni .git. Renvoie le chemin du zip.
.EXAMPLE
.\tools\Build-Package.ps1
.EXAMPLE
.\tools\Build-Package.ps1 -RunTests -Blender "C:\Program Files\Blender Foundation\Blender 5.2\blender.exe"
#>
param(
# Exécutable Blender (défaut : variable BLENDER_EXE, sinon le Blender 5.x le plus récent installé)
[string]$Blender = $env:BLENDER_EXE,
# Dossier de sortie
[string]$OutDir = (Join-Path (Split-Path $PSScriptRoot -Parent) 'dist'),
# Lance d'abord la suite de tests en arrière-plan (exige « RESULTAT : TOUT OK »)
[switch]$RunTests
)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
function Find-Blender {
$base = 'C:\Program Files\Blender Foundation'
$exe = Get-ChildItem $base -Directory -Filter 'Blender *' -ErrorAction SilentlyContinue |
Sort-Object { [version]($_.Name -replace '^Blender\s+', '') } -Descending |
ForEach-Object { Join-Path $_.FullName 'blender.exe' } |
Where-Object { Test-Path $_ } |
Select-Object -First 1
if (-not $exe) { throw "Blender introuvable : passez -Blender <chemin\blender.exe> ou définissez BLENDER_EXE" }
return $exe
}
if (-not $Blender) { $Blender = Find-Blender }
if (-not (Test-Path $Blender)) { throw "Blender introuvable : $Blender" }
$manifest = Get-Content (Join-Path $root 'blender_manifest.toml') -Raw
if ($manifest -notmatch '(?m)^\s*version\s*=\s*"([^"]+)"') { throw "version introuvable dans blender_manifest.toml" }
$version = $Matches[1]
if ($manifest -notmatch '(?m)^\s*id\s*=\s*"([^"]+)"') { throw "id introuvable dans blender_manifest.toml" }
$id = $Matches[1]
Write-Host "Slice Wrap $version — $Blender" -ForegroundColor Cyan
if ($RunTests) {
$testOut = Join-Path ([IO.Path]::GetTempPath()) 'slice_wrap_tests'
New-Item -ItemType Directory -Force $testOut | Out-Null
Write-Host "Tests (~1 min)…"
$log = & $Blender -b --factory-startup -P (Join-Path $root 'tests\run_tests.py') -- $testOut 2>&1
$log | Where-Object { $_ -match '^(OK|FAIL|RESULTAT)' } | ForEach-Object { Write-Host $_ }
if (-not ($log -match 'RESULTAT : TOUT OK')) { throw "Les tests ont échoué : paquet non construit" }
}
Write-Host "Validation du manifeste…"
$out = & $Blender --command extension validate $root 2>&1
if ($LASTEXITCODE -ne 0) { $out | Write-Host; throw "Manifeste invalide" }
New-Item -ItemType Directory -Force $OutDir | Out-Null
$zip = Join-Path $OutDir "$id-$version.zip"
if (Test-Path $zip) { Remove-Item $zip -Force }
Write-Host "Construction…"
$out = & $Blender --command extension build --source-dir $root --output-dir $OutDir 2>&1
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $zip)) { $out | Write-Host; throw "Échec de la construction" }
# Contrôle du contenu : rien de privé ni de développement dans le paquet
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [IO.Compression.ZipFile]::OpenRead($zip)
try { $entries = $archive.Entries | ForEach-Object FullName } finally { $archive.Dispose() }
$bad = $entries | Where-Object { $_ -match '^(tests|tools|dist|\.claude|\.git)/|^CLAUDE\.md$|__pycache__|\.zip$' }
if ($bad) { throw "Fichiers indésirables dans le paquet : $($bad -join ', ')" }
Write-Host "Paquet : $zip ($([math]::Round((Get-Item $zip).Length / 1KB)) Ko)" -ForegroundColor Green
$entries | ForEach-Object { Write-Host " $_" }
return $zip
-172
View File
@@ -1,172 +0,0 @@
<#
.SYNOPSIS
Publie une release de Slice Wrap sur Gitea : tag v<version>, release et paquet zip en pièce jointe.
.DESCRIPTION
1. Lit la version dans blender_manifest.toml.
2. Vérifie que le dépôt est propre (tout est commité) et que HEAD est poussé sur origin.
3. Construit le paquet (tools\Build-Package.ps1, tests compris sauf -SkipTests).
4. Crée le tag annoté v<version> sur HEAD et le pousse (s'il existe déjà, il doit pointer sur HEAD).
5. Crée la release via l'API Gitea (ou réutilise celle du tag) et y joint slice_wrap-<version>.zip
(une pièce jointe du même nom est remplacée).
Notes de version : -Notes, sinon l'entrée de la version dans la section « Historique » de CLAUDE.md.
Jeton (Gitea > Paramètres > Applications, portée « repository : lecture et écriture »), cherché dans l'ordre :
1. variable d'environnement GITEA_TOKEN ;
2. %APPDATA%\Gitea\token.xml, chiffré par Windows (DPAPI) : seul votre compte, sur ce PC, peut le lire.
Pour le créer : Read-Host "Jeton Gitea" -AsSecureString | Export-Clixml "$env:APPDATA\Gitea\token.xml"
3. sinon il est demandé (saisie masquée), avec proposition de l'enregistrer dans ce fichier.
.EXAMPLE
.\tools\New-GiteaRelease.ps1 -DryRun # montre ce qui serait fait, ne publie rien
.EXAMPLE
$env:GITEA_TOKEN = '...'; .\tools\New-GiteaRelease.ps1
.EXAMPLE
.\tools\New-GiteaRelease.ps1 -Prerelease -Notes "Version de test"
#>
param(
[string]$Server = 'https://a2msystemes-git.duckdns.org',
# Propriétaire et dépôt : déduits de l'URL du remote « origin » si absents
[string]$Owner,
[string]$Repo,
[string]$Notes,
[switch]$Prerelease,
[switch]$Draft,
[switch]$SkipTests,
# Publier même si le dépôt contient des modifications non commitées
[switch]$AllowDirty,
[switch]$DryRun,
[string]$Blender = $env:BLENDER_EXE
)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
function Invoke-Git {
$out = & git -C $root @args 2>&1
if ($LASTEXITCODE -ne 0) { throw "git $($args -join ' ') : $out" }
return $out
}
# ------------------------------------------------------------------ version, dépôt, notes
$manifest = Get-Content (Join-Path $root 'blender_manifest.toml') -Raw
if ($manifest -notmatch '(?m)^\s*version\s*=\s*"([^"]+)"') { throw "version introuvable dans blender_manifest.toml" }
$version = $Matches[1]
$tag = "v$version"
if (-not $Owner -or -not $Repo) {
$url = (& git -C $root remote get-url origin 2>$null)
if (-not $url) { throw "Aucun remote « origin » : ajoutez-le (git remote add origin $Server/<owner>/<repo>.git) ou passez -Owner / -Repo" }
if ($url -notmatch '[:/]([^/:]+)/([^/]+?)(\.git)?/?$') { throw "URL du remote non reconnue : $url" }
if (-not $Owner) { $Owner = $Matches[1] }
if (-not $Repo) { $Repo = $Matches[2] }
}
if (-not $Notes) {
# Entrée « - <version> … » de la section Historique de CLAUDE.md (lignes de continuation indentées comprises)
$claude = Join-Path $root 'CLAUDE.md'
if (Test-Path $claude) {
$lines = Get-Content $claude -Encoding utf8
$i = [array]::FindIndex($lines, [Predicate[string]] { param($l) $l -match "^- $([regex]::Escape($version))\b" })
if ($i -ge 0) {
$entry = @($lines[$i] -replace "^- $([regex]::Escape($version))\s*", '')
for ($j = $i + 1; $j -lt $lines.Count -and $lines[$j] -match '^\s{2,}\S'; $j++) { $entry += $lines[$j].Trim() }
$Notes = $entry -join ' '
}
}
if (-not $Notes) { $Notes = "Slice Wrap $version" }
}
$body = @"
$Notes
**Installation** : Blender 4.2+ > *Edit > Preferences > Get Extensions > ⌄ > Install from Disk…* > ``slice_wrap-$version.zip``
"@
# ------------------------------------------------------------------ état du dépôt
& git -C $root rev-parse --verify --quiet HEAD *> $null
if ($LASTEXITCODE -ne 0) { throw "Le dépôt n'a encore aucun commit : commitez et poussez avant de publier une release." }
$dirty = Invoke-Git status --porcelain
if ($dirty -and -not $AllowDirty) { throw "Modifications non commitées :`n$($dirty -join "`n")`nCommitez-les (ou -AllowDirty)." }
$head = (Invoke-Git rev-parse HEAD).Trim()
$branch = (Invoke-Git rev-parse --abbrev-ref HEAD).Trim()
& git -C $root fetch origin --tags --quiet 2>$null
$remoteHead = (& git -C $root rev-parse "origin/$branch" 2>$null)
if ($remoteHead -ne $head -and -not $DryRun) { throw "HEAD ($($head.Substring(0,8))) n'est pas poussé sur origin/$branch : faites git push d'abord." }
$tagCommit = (& git -C $root rev-list -n 1 $tag 2>$null)
if ($tagCommit -and $tagCommit.Trim() -ne $head) { throw "Le tag $tag existe déjà sur un autre commit ($($tagCommit.Substring(0,8))) : montez la version dans blender_manifest.toml." }
Write-Host "Release $tag de $Owner/$Repo sur $Server (commit $($head.Substring(0,8)), branche $branch)" -ForegroundColor Cyan
Write-Host "Notes : $Notes"
# ------------------------------------------------------------------ paquet
$buildArgs = @{}
if ($Blender) { $buildArgs.Blender = $Blender }
if (-not $SkipTests) { $buildArgs.RunTests = $true }
$zip = & (Join-Path $PSScriptRoot 'Build-Package.ps1') @buildArgs | Select-Object -Last 1
$zipName = Split-Path $zip -Leaf
if ($DryRun) {
Write-Host "`n[DryRun] Rien n'est publié. Seraient faits :" -ForegroundColor Yellow
if (-not $tagCommit) { Write-Host " git tag -a $tag $($head.Substring(0,8)) ; git push origin $tag" }
Write-Host " POST $Server/api/v1/repos/$Owner/$Repo/releases (tag $tag$(if ($Prerelease) {', préversion'})$(if ($Draft) {', brouillon'}))"
Write-Host " pièce jointe $zipName"
return
}
# ------------------------------------------------------------------ jeton
# Ordre : variable GITEA_TOKEN, puis fichier chiffré par Windows (DPAPI), sinon saisie masquée
$tokenFile = Join-Path $env:APPDATA 'Gitea\token.xml'
$token = $env:GITEA_TOKEN
if (-not $token -and (Test-Path $tokenFile)) {
$token = [Net.NetworkCredential]::new('', (Import-Clixml $tokenFile)).Password
}
if (-not $token) {
$sec = Read-Host "Jeton Gitea (portée repository, lecture/écriture)" -AsSecureString
$token = [Net.NetworkCredential]::new('', $sec).Password
if ($token -and (Read-Host "L'enregistrer chiffré dans $tokenFile ? (o/N)") -match '^[oOyY]') {
New-Item -ItemType Directory -Force (Split-Path $tokenFile) | Out-Null
$sec | Export-Clixml $tokenFile
Write-Host "Jeton enregistré (lisible seulement par votre compte Windows sur ce PC)"
}
}
if (-not $token) { throw "Jeton Gitea manquant" }
$headers = @{ Authorization = "token $token"; Accept = 'application/json' }
$api = "$Server/api/v1/repos/$Owner/$Repo"
# ------------------------------------------------------------------ tag
if (-not $tagCommit) {
Invoke-Git tag -a $tag -m "Slice Wrap $version" | Out-Null
Invoke-Git push origin $tag | Out-Null
Write-Host "Tag $tag créé et poussé"
}
# ------------------------------------------------------------------ release
$release = $null
try {
$release = Invoke-RestMethod -Method Get -Uri "$api/releases/tags/$tag" -Headers $headers
Write-Host "Release $tag existante (id $($release.id)) : réutilisée"
} catch {
if ($_.Exception.Response.StatusCode.value__ -ne 404) { throw }
}
if (-not $release) {
$payload = @{
tag_name = $tag
name = "Slice Wrap $version"
body = $body
draft = [bool]$Draft
prerelease = [bool]$Prerelease
} | ConvertTo-Json
$release = Invoke-RestMethod -Method Post -Uri "$api/releases" -Headers $headers `
-ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($payload))
Write-Host "Release créée (id $($release.id))"
}
# ------------------------------------------------------------------ pièce jointe (remplacée si elle existe)
foreach ($a in @($release.assets) | Where-Object { $_ -and $_.name -eq $zipName }) {
Invoke-RestMethod -Method Delete -Uri "$api/releases/$($release.id)/assets/$($a.id)" -Headers $headers | Out-Null
Write-Host "Ancienne pièce jointe $zipName supprimée"
}
Invoke-RestMethod -Method Post -Uri "$api/releases/$($release.id)/assets?name=$zipName" -Headers $headers `
-Form @{ attachment = Get-Item $zip } | Out-Null
Write-Host "Publié : $($release.html_url)" -ForegroundColor Green