Upload files to "frontend/templates/partials"
This commit is contained in:
parent
c9685c4ce2
commit
c4b3c41718
4 changed files with 331 additions and 21 deletions
|
|
@ -6,13 +6,17 @@
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost"
|
class="btn btn-ghost"
|
||||||
hx-get="/viewer/list"
|
hx-get="/viewer/list"
|
||||||
hx-target="#viewer-panel"
|
hx-target="#cloud-list-body"
|
||||||
hx-trigger="click"
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#cloud-list-spinner"
|
||||||
>
|
>
|
||||||
🔄 Actualiser
|
🔄 Actualiser
|
||||||
</button>
|
</button>
|
||||||
|
<span id="cloud-list-spinner" class="loading loading-spinner loading-sm htmx-indicator"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ CORRECTION BUG 1 : cible séparée du bouton -->
|
||||||
|
<div id="cloud-list-body">
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert-error">
|
<div class="alert alert-error">
|
||||||
<span>{{ error }}</span>
|
<span>{{ error }}</span>
|
||||||
|
|
@ -37,17 +41,29 @@
|
||||||
<tbody id="cloud-table-body">
|
<tbody id="cloud-table-body">
|
||||||
{% for pc in pointclouds %}
|
{% for pc in pointclouds %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ pc.id }}</td>
|
<td class="font-mono text-xs">{{ pc.id }}</td>
|
||||||
<td>{{ pc.size_mb }} MB</td>
|
<td>{{ pc.size_mb }} MB</td>
|
||||||
<td>{{ pc.file_count }}</td>
|
<td>{{ pc.file_count }}</td>
|
||||||
<td>{{ pc.created|datetimeformat }}</td>
|
<td>{{ pc.created|datetimeformat }}</td>
|
||||||
<td>
|
<td class="flex gap-1 flex-wrap">
|
||||||
<a href="/admin/debug/{{ pc.id }}" class="btn btn-sm btn-ghost">🔍</a>
|
<a href="/admin/debug/{{ pc.id }}" class="btn btn-sm btn-ghost">🔍</a>
|
||||||
<a href="/viewer/{{ pc.id }}" class="btn btn-sm" target="_blank">👁️</a>
|
<a href="/viewer/{{ pc.id }}" class="btn btn-sm" target="_blank">👁️</a>
|
||||||
|
<!-- ✅ NOUVEAU : bouton Crop qui charge la section crop dans #crop-panel -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-warning"
|
||||||
|
hx-get="/crop?pc_id={{ pc.id }}"
|
||||||
|
hx-target="#crop-panel"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
onclick="document.getElementById('crop-panel').scrollIntoView({behavior:'smooth'})"
|
||||||
|
>✂️</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-error"
|
class="btn btn-sm btn-error"
|
||||||
onclick="if(confirm('Supprimer ce nuage ?')) window.location.href='/admin/delete/{{ pc.id }}'"
|
hx-delete="/admin/delete/{{ pc.id }}"
|
||||||
|
hx-target="#cloud-list-body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-confirm="Supprimer le nuage {{ pc.id }} ?"
|
||||||
>🗑️</button>
|
>🗑️</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -57,4 +73,9 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ NOUVEAU : section crop injectée dynamiquement ici -->
|
||||||
|
<div id="crop-panel" class="mt-4"></div>
|
||||||
61
frontend/templates/partials/cloud_list_body.html
Normal file
61
frontend/templates/partials/cloud_list_body.html
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<span>{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% elif not pointclouds %}
|
||||||
|
<p class="text-base-content/40 text-sm text-center py-8">
|
||||||
|
Aucun nuage disponible sur le serveur.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<p class="text-xs text-base-content/40 mb-3">{{ pointclouds|length }} nuage(s)</p>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Taille</th>
|
||||||
|
<th>Fichiers</th>
|
||||||
|
<th>Créé le</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for pc in pointclouds %}
|
||||||
|
<tr>
|
||||||
|
<td class="font-mono text-xs max-w-[120px] truncate" title="{{ pc.id }}">{{ pc.id }}</td>
|
||||||
|
<td>{{ pc.size_mb }} MB</td>
|
||||||
|
<td>{{ pc.file_count }}</td>
|
||||||
|
<td>{{ pc.created|datetimeformat }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex gap-1 flex-wrap">
|
||||||
|
<a href="/admin/debug/{{ pc.id }}" class="btn btn-xs btn-ghost" title="Debug">🔍</a>
|
||||||
|
<a href="/viewer/{{ pc.id }}" class="btn btn-xs" target="_blank" title="Visualiser">👁️</a>
|
||||||
|
<!-- ✅ Bouton crop : charge crop_section.html dans #crop-panel (hors de ce div) -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-xs btn-warning"
|
||||||
|
title="Cropper ce nuage"
|
||||||
|
hx-get="/crop?pc_id={{ pc.id }}"
|
||||||
|
hx-target="#crop-panel"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-on:htmx:after-swap="document.getElementById('crop-panel').scrollIntoView({behavior:'smooth'})"
|
||||||
|
>✂️ Crop</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-xs btn-error"
|
||||||
|
title="Supprimer"
|
||||||
|
hx-delete="/admin/delete/{{ pc.id }}"
|
||||||
|
hx-target="#cloud-list-body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-confirm="Supprimer le nuage {{ pc.id }} ?"
|
||||||
|
>🗑️</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
157
frontend/templates/partials/crop_section.html
Normal file
157
frontend/templates/partials/crop_section.html
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
<!-- crop_section.html — injecté dans #crop-panel (hors de viewer-panel) -->
|
||||||
|
<div class="card bg-base-100 shadow border border-warning/30" x-data="cropForm()">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<!-- En-tête -->
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h2 class="card-title text-base">
|
||||||
|
✂️ Crop du nuage
|
||||||
|
<span class="badge badge-warning font-mono text-xs">{{ pc_id }}</span>
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-sm"
|
||||||
|
onclick="document.getElementById('crop-panel').innerHTML = ''"
|
||||||
|
>✕ Fermer</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm text-base-content/60 mb-4">
|
||||||
|
Renseignez les coordonnées de la boîte 3D à découper. Les valeurs sont dans
|
||||||
|
le système de coordonnées du nuage de points (en mètres ou en unités du fichier source).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Schéma visuel de la box -->
|
||||||
|
<div class="bg-base-200 rounded-lg p-4 mb-4 flex items-center justify-center gap-8">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-xs text-base-content/50 mb-1">Vue de dessus (XY)</div>
|
||||||
|
<svg width="110" height="75" viewBox="0 0 110 75">
|
||||||
|
<ellipse cx="55" cy="37" rx="50" ry="30" fill="none" stroke="currentColor" stroke-opacity="0.15" stroke-width="1" stroke-dasharray="3 2"/>
|
||||||
|
<rect x="18" y="14" width="74" height="46" rx="2" fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="4 2"/>
|
||||||
|
<text x="100" y="40" font-size="9" fill="currentColor" opacity="0.4">X</text>
|
||||||
|
<text x="52" y="9" font-size="9" fill="currentColor" opacity="0.4">Y</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-xs text-base-content/50 mb-1">Vue de côté (XZ)</div>
|
||||||
|
<svg width="110" height="75" viewBox="0 0 110 75">
|
||||||
|
<ellipse cx="55" cy="37" rx="50" ry="25" fill="none" stroke="currentColor" stroke-opacity="0.15" stroke-width="1" stroke-dasharray="3 2"/>
|
||||||
|
<rect x="18" y="16" width="74" height="42" rx="2" fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="4 2"/>
|
||||||
|
<text x="100" y="40" font-size="9" fill="currentColor" opacity="0.4">X</text>
|
||||||
|
<text x="52" y="9" font-size="9" fill="currentColor" opacity="0.4">Z</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulaire -->
|
||||||
|
<form
|
||||||
|
hx-post="/admin/crop/{{ pc_id }}"
|
||||||
|
hx-target="#crop-result"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#crop-spinner"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
|
||||||
|
|
||||||
|
<!-- X -->
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-semibold text-base-content/50 uppercase tracking-wider mb-2">Axe X</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<label class="label py-0 pb-1"><span class="label-text text-xs">Min X</span></label>
|
||||||
|
<input type="number" step="any" name="minX" x-model="box.minX"
|
||||||
|
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 100.0" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label py-0 pb-1"><span class="label-text text-xs">Max X</span></label>
|
||||||
|
<input type="number" step="any" name="maxX" x-model="box.maxX"
|
||||||
|
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 500.0" required/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Y -->
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-semibold text-base-content/50 uppercase tracking-wider mb-2">Axe Y</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<label class="label py-0 pb-1"><span class="label-text text-xs">Min Y</span></label>
|
||||||
|
<input type="number" step="any" name="minY" x-model="box.minY"
|
||||||
|
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 200.0" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label py-0 pb-1"><span class="label-text text-xs">Max Y</span></label>
|
||||||
|
<input type="number" step="any" name="maxY" x-model="box.maxY"
|
||||||
|
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 800.0" required/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Z -->
|
||||||
|
<div>
|
||||||
|
<div class="text-xs font-semibold text-base-content/50 uppercase tracking-wider mb-2">Axe Z (altitude)</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<label class="label py-0 pb-1"><span class="label-text text-xs">Min Z</span></label>
|
||||||
|
<input type="number" step="any" name="minZ" x-model="box.minZ"
|
||||||
|
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 10.0" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label py-0 pb-1"><span class="label-text text-xs">Max Z</span></label>
|
||||||
|
<input type="number" step="any" name="maxZ" x-model="box.maxZ"
|
||||||
|
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 50.0" required/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Résumé dimensions en temps réel -->
|
||||||
|
<div class="bg-base-200 rounded p-3 mb-4 text-xs font-mono" x-show="isValid()">
|
||||||
|
<span class="text-base-content/60 mr-2">Dimensions :</span>
|
||||||
|
<span class="text-warning">ΔX=<span x-text="delta('X')"></span></span>
|
||||||
|
<span class="mx-2 text-base-content/30">|</span>
|
||||||
|
<span class="text-warning">ΔY=<span x-text="delta('Y')"></span></span>
|
||||||
|
<span class="mx-2 text-base-content/30">|</span>
|
||||||
|
<span class="text-warning">ΔZ=<span x-text="delta('Z')"></span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-warning"
|
||||||
|
:disabled="!isValid()"
|
||||||
|
>
|
||||||
|
✂️ Lancer le crop
|
||||||
|
</button>
|
||||||
|
<div id="crop-spinner" class="loading loading-spinner loading-sm htmx-indicator text-warning"></div>
|
||||||
|
<span class="text-xs text-base-content/50">Traitement PDAL — peut prendre plusieurs minutes</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="crop-result" class="mt-4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function cropForm() {
|
||||||
|
return {
|
||||||
|
box: { minX: '', minY: '', minZ: '', maxX: '', maxY: '', maxZ: '' },
|
||||||
|
isValid() {
|
||||||
|
const keys = ['minX', 'minY', 'minZ', 'maxX', 'maxY', 'maxZ'];
|
||||||
|
if (keys.some(k => this.box[k] === '' || isNaN(parseFloat(this.box[k])))) return false;
|
||||||
|
return (
|
||||||
|
parseFloat(this.box.minX) < parseFloat(this.box.maxX) &&
|
||||||
|
parseFloat(this.box.minY) < parseFloat(this.box.maxY) &&
|
||||||
|
parseFloat(this.box.minZ) < parseFloat(this.box.maxZ)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
delta(axis) {
|
||||||
|
const min = parseFloat(this.box['min' + axis]);
|
||||||
|
const max = parseFloat(this.box['max' + axis]);
|
||||||
|
if (isNaN(min) || isNaN(max) || min >= max) return '—';
|
||||||
|
return (max - min).toFixed(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
71
frontend/templates/partials/upload_form.html
Normal file
71
frontend/templates/partials/upload_form.html
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<!-- upload_form.html — partial HTMX uniquement (sans navbar ni layout) -->
|
||||||
|
<!-- Injecté dans #main-content lors du clic sur le tab Upload -->
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
|
<!-- Colonne gauche : upload -->
|
||||||
|
<div class="lg:col-span-1 flex flex-col gap-4">
|
||||||
|
<!-- Configuration Backend -->
|
||||||
|
<div id="backend-config-panel" hx-get="/admin/backend-config" hx-target="this" hx-swap="innerHTML" hx-trigger="load">
|
||||||
|
{% include "partials/backend_config.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-base mb-2">📤 Upload</h2>
|
||||||
|
<p class="text-sm text-base-content/60 mb-3">
|
||||||
|
Formats acceptés : LAS, LAZ, PLY, XYZ, PTS
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
hx-post="/upload"
|
||||||
|
hx-target="#upload-result"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-encoding="multipart/form-data"
|
||||||
|
hx-indicator="#upload-spinner"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="file"
|
||||||
|
accept=".las,.laz,.ply,.xyz,.pts"
|
||||||
|
class="file-input file-input-bordered w-full mb-4"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn"
|
||||||
|
>
|
||||||
|
📤 Uploader & convertir
|
||||||
|
</button>
|
||||||
|
<div id="upload-spinner" class="loading loading-spinner loading-sm htmx-indicator"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="upload-result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonne droite : viewer -->
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<div
|
||||||
|
id="viewer-container"
|
||||||
|
class="card bg-base-100 shadow min-h-[600px] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<p class="text-base-content/40 text-sm">
|
||||||
|
Uploadez un fichier pour lancer la visualisation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="viewer-panel"
|
||||||
|
class="card bg-base-100 shadow mt-4"
|
||||||
|
hx-get="/viewer/list"
|
||||||
|
hx-target="#cloud-list-body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-trigger="load"
|
||||||
|
>
|
||||||
|
{% include "partials/cloud_list.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue