Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0027f74a56 | ||
|
|
c88a7f9a2a | ||
|
|
8a92c2b784 | ||
|
|
d351379a23 | ||
|
|
ed13bf6493 | ||
|
|
a1bc74d294 | ||
|
|
bc01a01e09 | ||
|
|
ec3ed21b1e |
No files matched your search
@@ -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
@@ -2,7 +2,7 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
||||||
# Paquets d'extension (construits par tools/Build-Package.ps1)
|
# Paquets d'extension (construits par a2m build)
|
||||||
dist/
|
dist/
|
||||||
*.zip
|
*.zip
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
# Slice Wrap — add-on Blender : surface propre à partir d'un maillage troué, wrap, nettoyage et analyse
|
# 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
|
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
|
## But
|
||||||
Transformer un maillage **troué, à doubles parois ou bruité** (scan, génération IA type Pixal3D / Trellis — ex.
|
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`,
|
`voxelize`, `blocky_mesh`), nettoyage (`taubin`, `keep_largest_island`, `despike`, `group_weights`,
|
||||||
`fix_missed_rays`), analyse (`analyze_shape`, `write_analysis`), `unique_object_name`, `build_envelope`.
|
`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.
|
- `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`,
|
- `blender_manifest.toml` : extension **5.1+** (décidé le 2026-10-20) ; `[build]` exclut `__pycache__/`, zips,
|
||||||
`tests/`, `tools/`, `dist/`.
|
`.a2m/`, `.claude/`, `.git/`, `.gitignore`, `CLAUDE.md`, `tests/`, `dist/`.
|
||||||
- `tools/Build-Package.ps1` (paquet), `tools/New-GiteaRelease.ps1` (release Gitea). Exclus du paquet.
|
- `.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
|
## Commandes
|
||||||
- Tests : `"C:\Program Files\Blender Foundation\Blender 5.2\blender.exe" -b -P tests/run_tests.py -- <dossier> [--head]`
|
- 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`.
|
→ `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
|
**À lancer avant chaque release** (la chaîne a2m ne lance pas encore les tests de l'addon).
|
||||||
du contenu). **Monter `version`** à chaque livraison. L'utilisateur installe lui-même le zip.
|
- Construction et publication : chaîne d'outils **a2m** (repo `ange/apc-toolchain`, remplace les anciens
|
||||||
- Release : `.\tools\New-GiteaRelease.ps1 [-DryRun]` — Gitea `https://a2msystemes-git.duckdns.org`, dépôt
|
`tools/Build-Package.ps1` et `tools/New-GiteaRelease.ps1`, retirés le 2026-10-20) :
|
||||||
`ange/slice_wrap_addon` (déduit de `origin`), jeton `GITEA_TOKEN`. Exige un dépôt propre et poussé ; crée le tag
|
`a2m validate --load` (manifest validé et addon chargé dans Blender 5.1 et 5.2, profil temporaire),
|
||||||
`v<version>`, la release (notes = entrée de l'Historique ci-dessous) et joint le zip. Publication = action
|
`a2m build` → `dist\slice_wrap-<version>.zip`, `a2m release [patch|minor|major]` (version du manifest, CHANGELOG,
|
||||||
externe : ne la lancer qu'à la demande de l'utilisateur.
|
commit, tag `v<version>`, release Gitea avec le zip et `publication.json`), `a2m release --dev` (pré-release de
|
||||||
- `.gitignore` : `__pycache__/`, `dist/`, `*.zip`, rendus de test. Le manifeste exclut aussi `tools/`, `dist/`, `.git/`.
|
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
|
## Historique
|
||||||
- 1.0.0 Envelope / Regenerate / Wrap Setup / Freeze (Reshape) / Despike.
|
Voir [CHANGELOG.fr.md](CHANGELOG.fr.md) (une entrée par version ; les changements à venir dans « Non publié »).
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Mesures de référence (Blender 5.2.1, tête Pixal3D brute 11,8 M faces)
|
## 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é
|
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 %,
|
« 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).
|
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
|
## Pistes
|
||||||
- Option avancée repliée pour Absolute ; préréglages par type d'objet (tête, main, accessoire).
|
- 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).
|
- Accélérer le remplissage (propagation par lignes / scanline au lieu d'une dilatation voxel par voxel).
|
||||||
|
|||||||
+138
@@ -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`.
|
||||||
@@ -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)
|
*[Version française](README.fr.md)*
|
||||||
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 »**.
|
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 ?
|
Panel: **3D Viewport > Sidebar (N) > "Slice Wrap" tab**.
|
||||||
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
|
## Why not Blender's Voxel Remesh?
|
||||||
Tout s'applique à **l'objet sélectionné** (actif) : il n'y a pas de cible à choisir. La projection se fait toujours
|
Voxel Remesh (and *Mesh to Volume* in Geometry Nodes) rebuilds a volume… but **leaks** as soon as the shell has a
|
||||||
sur la source de l'enveloppe. Sans maillage sélectionné, les boutons sont grisés.
|
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.
|
## Usage
|
||||||
2. Réglez au besoin la section **Envelope** et **Wrap**, puis cliquez **Slice Wrap** (un clic) :
|
Everything applies to the **selected (active) object**: there is no target to pick. The projection always goes
|
||||||
Envelope → Wrap Setup → Freeze, et **Despike** en plus si **Despike After** est coché.
|
onto the source of the envelope. Without a selected mesh, the buttons are greyed out.
|
||||||
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
|
1. Select the (holey) source mesh in Object Mode.
|
||||||
Slice Wrap, Envelope, Regenerate et Despike affichent d'abord un message : le calcul peut être long, Blender
|
2. Adjust the **Envelope** and **Wrap** sections if needed, then click **Slice Wrap** (one click):
|
||||||
ignorera les clics pendant ce temps (il n'est pas planté), Échap annule. Case « Show this message next time »
|
Envelope → Wrap Setup → Freeze, plus **Despike** if **Despike After** is checked.
|
||||||
(aussi dans *Preferences > Add-ons > Slice Wrap > Confirm Long Operations*).
|
3. The result `<source><suffix>` (by default `<source>_slicewrapped`) is ready to sculpt (light base + Multires
|
||||||
Pendant le calcul (ainsi que Freeze et Analyze) : **barre de progression** avec l'étape en cours et le temps écoulé,
|
levels). The source is hidden.
|
||||||
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
|
### Long computations: confirmation, progress, cancel
|
||||||
- **Suffix** (défaut `_slicewrapped`) : nom du maillage créé = nom de la source + suffixe.
|
Slice Wrap, Envelope, Regenerate and Despike first show a message: the computation can take a while, Blender will
|
||||||
- **Numérotation automatique** : si le nom existe déjà, `.001`, `.002`… comme Blender — **aucun objet existant
|
ignore clicks meanwhile (it has not crashed), Esc cancels. "Show this message next time" checkbox (also in
|
||||||
n'est jamais écrasé** (même avec un suffixe vide, la source reste intacte).
|
*Preferences > Add-ons > Slice Wrap > Confirm Long Operations*).
|
||||||
- **Collection** (case + nom, défaut `SliceWrapCollection`) : le résultat y est rangé ; si la collection existe,
|
During the computation (as well as Freeze and Analyze): a **progress bar** with the current step and the elapsed
|
||||||
elle est simplement réutilisée (son contenu n'est pas touché), sinon elle est créée et liée à la scène.
|
time, in the status bar at the bottom of the window and at the top of the panel. **Esc** cancels: nothing is
|
||||||
Case décochée : le résultat va dans les collections de la source.
|
created or modified.
|
||||||
- **Regenerate** est le seul bouton qui recalcule un objet existant : l'enveloppe active, sur place.
|
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
|
### Naming and placing the result
|
||||||
| # | Bouton | Ce qu'il fait |
|
- **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
|
||||||
| 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). |
|
overwritten** (even with an empty suffix, the source stays intact).
|
||||||
| | ↻ **Regenerate** | Recalcule l'enveloppe active depuis sa source avec les réglages actuels (pseudo-modificateur). Refusé si l'enveloppe est déjà wrappée. |
|
- **Collection** (checkbox + name, default `SliceWrapCollection`): the result is placed in it; if the collection
|
||||||
| 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. |
|
exists it is simply reused (its content is not touched), otherwise it is created and linked to the scene.
|
||||||
| 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. |
|
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
|
### "Steps" sub-panel (collapsed) — each step separately
|
||||||
**Wrap Setup** et **Freeze**.
|
| # | 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
|
Tip: to cut the base / shoulders before projecting, run **Envelope**, cut (Bisect), then **Wrap Setup** and
|
||||||
Filtre anti-pointes résiduel : un sommet n'est corrigé que s'il est **isolé**, nettement plus loin de la moyenne
|
**Freeze**.
|
||||||
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
|
### "Despike" sub-panel — manual tool
|
||||||
Analyse la forme de l'objet actif (Multires compris) : pour chaque sommet, la répartition des normales sur un
|
Residual anti-spike filter: a vertex is only corrected if it is **isolated**, clearly further from the average of
|
||||||
voisinage (tenseur de covariance) et la courbure moyenne signée. Résultat écrit sur le maillage de base :
|
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 |
|
### "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
|
||||||
| `sw_detail` | attribut float 0–1 | densité de détail (la surface « tourne » beaucoup) — carte de densité pour une retopo |
|
neighbourhood (covariance tensor) and the signed mean curvature. Result written on the base mesh:
|
||||||
| `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.
|
| Data | Type | Content |
|
||||||
Les couleurs portent le suffixe `_Color` car groupes de sommets et attributs partagent le même espace de noms.
|
|------|------|---------|
|
||||||
|
| `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
|
Tag colours: grey Flat · red Bulge · blue Hollow · yellow Ridge · cyan Fold · purple Saddle.
|
||||||
- **Voxel : Relative / Absolute** — la finesse de l'enveloppe.
|
Colours carry the `_Color` suffix because vertex groups and attributes share the same namespace.
|
||||||
- *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
|
## Settings
|
||||||
- Couper la source **et** l'enveloppe là où elles ne servent pas (socle, épaules) améliore la projection.
|
- **Voxel: Relative / Absolute** — the resolution of the envelope.
|
||||||
- Les **surplombs** (pavillon de l'oreille) restent le point faible d'une projection le long des normales :
|
- *Relative* (default, **Voxel %** = 0.6 % of the largest dimension): scale-independent — the same setting gives
|
||||||
quelques pointes peuvent subsister → Despike, ou pinceau Smooth.
|
the same result on a 23 cm head, a head normalised to 1 m or a hand imported at 3.7 m.
|
||||||
- Après Freeze, l'objet garde une **base légère** + niveaux Multires : sculptez, ou bakez les détails.
|
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
|
## 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…*
|
*Edit > Preferences > Get Extensions > ⌄ > Install from Disk…*
|
||||||
- **Construire le paquet** : `.\tools\Build-Package.ps1` (→ `dist\slice_wrap-<version>.zip` ; `-RunTests` lance
|
- **Build and publish**: `a2m` toolchain (repository `ange/apc-toolchain`) — `a2m validate --load`, `a2m build`
|
||||||
d'abord les tests ; Blender trouvé automatiquement, ou `-Blender <chemin>` / variable `BLENDER_EXE`).
|
(→ `dist\slice_wrap-<version>.zip`), `a2m release [patch|minor|major]` (version, CHANGELOG, tag, Gitea release
|
||||||
- **Publier une release Gitea** : monter `version` dans `blender_manifest.toml`, commiter et pousser, puis
|
with the zip and `publication.json`), `a2m release --dev` (test pre-release).
|
||||||
`.\tools\New-GiteaRelease.ps1` (tests, paquet, tag `v<version>`, release avec le zip en pièce jointe).
|
Version history: [CHANGELOG.md](CHANGELOG.md).
|
||||||
Jeton : variable `GITEA_TOKEN` (sinon demandé). `-DryRun` montre ce qui serait fait sans rien publier ;
|
- **Development**: copy / link this folder into
|
||||||
`-Prerelease`, `-Draft`, `-Notes "…"` (par défaut : l'entrée de la version dans l'historique).
|
|
||||||
- **Développement** : copier / lier ce dossier dans
|
|
||||||
`%APPDATA%\Blender Foundation\Blender\<version>\extensions\user_default\slice_wrap`.
|
`%APPDATA%\Blender Foundation\Blender\<version>\extensions\user_default\slice_wrap`.
|
||||||
|
|
||||||
## Fichiers
|
## Files
|
||||||
- `__init__.py` : réglages, opérateurs, panneaux (principal, Steps, Despike, Analyze), liste des groupes.
|
- `__init__.py`: settings, operators, panels (main, Steps, Despike, Analyze), group list.
|
||||||
- `core.py` : algorithmes (voxelisation, morphologie, remplissage par tranches, Taubin, réparation des ratés, despike,
|
- `core.py`: algorithms (voxelisation, morphology, slice filling, Taubin, missed-ray repair, despike, shape
|
||||||
analyse de forme).
|
analysis).
|
||||||
- `blender_manifest.toml` : manifeste d'extension.
|
- `blender_manifest.toml`: extension manifest.
|
||||||
|
|
||||||
## Tests (Blender 5.2.1)
|
## 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
|
Raw Pixal3D head of 11.8 M faces → 70 k-face envelope (11 s) → Wrap Multires 2 → Freeze with repair (~93 k misses
|
||||||
(~93 k ratés réparés) — chaîne complète en ~1,5–2 min. Despike filtré sur un groupe « Oreilles » : 683 sommets
|
repaired) — full chain in ~1.5–2 min. Despike filtered on an "Ears" group: 683 vertices corrected versus 4,749 on
|
||||||
corrigés contre 4 749 sur toute la surface. Analyze (1,1 M sommets évalués) en 9 s : Flat 86 %, Ridge 5 %,
|
the whole surface. Analyze (1.1 M evaluated vertices) in 9 s: Flat 86 %, Ridge 5 %, Bulge 3 %, Fold 3 %, Hollow 2 %,
|
||||||
Bulge 3 %, Fold 3 %, Hollow 2 %, Saddle 1 % — hélice des oreilles et lèvres en Ridge, paupières et commissures en Fold.
|
Saddle 1 % — ear helix and lips as Ridge, eyelids and mouth corners as Fold.
|
||||||
|
|
||||||
## Pistes (non implémentées)
|
## Ideas (not implemented)
|
||||||
- **Marqueurs de flow** dessinés par l'utilisateur (courbes / Grease Pencil sur la surface) avec des tags
|
- **Flow markers** drawn by the user (curves / Grease Pencil on the surface) with tags ("fold", "bulge",
|
||||||
(« pli », « gonflement », « boucle œil »…), combinés aux tags automatiques d'Analyze.
|
"eye loop"…), combined with the automatic Analyze tags.
|
||||||
- **Stratégies par tag** pour une retopologie guidée : boucles parallèles à `sw_flow` sur un Fold / Ridge, anneaux
|
- **Per-tag strategies** for a guided retopology: loops parallel to `sw_flow` on a Fold / Ridge, concentric rings
|
||||||
concentriques autour d'un Hollow (œil, bouche), pôle au sommet d'un Bulge, anneaux perpendiculaires sur un
|
around a Hollow (eye, mouth), a pole at the top of a Bulge, perpendicular rings on a cylinder (finger), density
|
||||||
cylindre (doigt), densité pilotée par `sw_detail`.
|
driven by `sw_detail`.
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "slice_wrap"
|
id = "slice_wrap"
|
||||||
version = "1.4.0"
|
version = "1.4.2"
|
||||||
name = "Slice Wrap"
|
name = "Slice Wrap"
|
||||||
tagline = "Closed envelope of holey meshes, then wrap, freeze and despike"
|
tagline = "Closed envelope of holey meshes, then wrap, freeze and despike"
|
||||||
maintainer = "a-mma"
|
maintainer = "Ange-Marie MAURIN"
|
||||||
type = "add-on"
|
type = "add-on"
|
||||||
|
website = "https://a2msystemes.org/projets/slice_wrap"
|
||||||
|
|
||||||
tags = ["Mesh", "Modeling", "Sculpt"]
|
tags = ["Mesh", "Modeling", "Sculpt"]
|
||||||
|
|
||||||
blender_version_min = "4.2.0"
|
blender_version_min = "5.1.0"
|
||||||
|
|
||||||
license = [
|
license = [
|
||||||
"SPDX:GPL-3.0-or-later",
|
"SPDX:GPL-3.0-or-later",
|
||||||
@@ -19,11 +20,11 @@ license = [
|
|||||||
paths_exclude_pattern = [
|
paths_exclude_pattern = [
|
||||||
"__pycache__/",
|
"__pycache__/",
|
||||||
"/*.zip",
|
"/*.zip",
|
||||||
|
"/.a2m/",
|
||||||
"/.claude/",
|
"/.claude/",
|
||||||
"/.git/",
|
"/.git/",
|
||||||
"/.gitignore",
|
"/.gitignore",
|
||||||
"/CLAUDE.md",
|
"/CLAUDE.md",
|
||||||
"/tests/",
|
"/tests/",
|
||||||
"/tools/",
|
|
||||||
"/dist/",
|
"/dist/",
|
||||||
]
|
]
|
||||||
+3
-1
@@ -4,7 +4,8 @@
|
|||||||
- rapide (~40 s) : sphère trouée et bruitée → nom / numérotation / collection / Regenerate, Envelope fermée,
|
- 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.
|
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.
|
- --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 importlib
|
||||||
import math
|
import math
|
||||||
@@ -236,3 +237,4 @@ if "--head" in ARGS and os.path.isfile(HEAD):
|
|||||||
|
|
||||||
mod.unregister()
|
mod.unregister()
|
||||||
print("RESULTAT :", "TOUT OK" if not fails else "ECHECS " + ", ".join(fails))
|
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 »
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in new issue
Block a user