import 'dart:convert';
import 'dart:html' as html;
import 'dart:math' as math;
import 'dart:ui_web' as ui_web;
import 'package:flutter/material.dart';
import 'package:manager_api_new/api.dart';
import 'package:manager_app/Components/message_notification.dart';
import 'package:manager_app/Components/resource_input_container.dart';
import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/app_context.dart';
import 'package:manager_app/constants.dart';
import 'package:manager_app/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
/// Configuration d'une section **maquette 3D** — item E7 du lot XR-4.
///
/// Deux choses : choisir le modèle dans la médiathèque, et **poser ses points
/// d'intérêt dessus**. Le second se fait dans le viewer 3D, affiché ici en iframe.
///
/// Pourquoi une iframe et pas un rendu Flutter. Le viewer existe déjà, en
/// TypeScript/three.js (`vr-app/viewer`, décision D3) : il charge un GLB, laisse
/// déplacer des objets à la souris, et sert aussi à la piste Scène 3D. Le refaire en
/// Flutter demanderait un moteur 3D et rejouerait le même travail. Le manager sait
/// déjà monter une iframe — `policy_screen`, `web_view` et `pdf_web_viewer` le font.
///
/// Le protocole n'est pas inventé ici : le viewer parle en **manifeste de
/// scène** (`bridge.ts`), le même objet que lit le casque (`SceneManifest.cs`). On lui
/// envoie donc un manifeste minimal — le modèle comme décor, les points comme
/// *hotspots* — et il renvoie les positions dès qu'on en déplace un. Un hotspot porte
/// un `geoPointId` précisément pour ce va-et-vient.
class Scene3DConfig extends StatefulWidget {
final Scene3DDTO initialValue;
final ValueChanged onChanged;
const Scene3DConfig({
Key? key,
required this.initialValue,
required this.onChanged,
}) : super(key: key);
@override
State createState() => _Scene3DConfigState();
}
class _Scene3DConfigState extends State {
/// Le repère du manifeste, vérifié littéralement des deux côtés. Un miroir d'axes
/// est invisible sur une maquette symétrique et se paie très cher découvert tard.
static const _coordinateSystem = 'gltf/y-up/right-handed/meters';
/// Le viewer est servi à côté du manager. En développement il tourne sur son
/// propre port (`npm run dev`), d'où la variable de compilation.
static const _viewerUrl =
String.fromEnvironment('VIEWER_URL', defaultValue: '/viewer/index.html');
late Scene3DDTO _dto;
late final String _viewType;
html.IFrameElement? _frame;
bool _viewerReady = false;
@override
void initState() {
super.initState();
_dto = widget.initialValue;
_viewType = 'model3d-viewer-${DateTime.now().microsecondsSinceEpoch}';
ui_web.platformViewRegistry.registerViewFactory(_viewType, (int _) {
final frame = html.IFrameElement()
..src = _viewerUrl
..style.border = 'none'
..style.width = '100%'
..style.height = '100%';
_frame = frame;
return frame;
});
html.window.onMessage.listen(_onViewerMessage);
}
void _onViewerMessage(html.MessageEvent event) {
final message = _asMap(event.data);
if (message == null) return;
switch (message['type']) {
case 'ready':
// Le viewer prévient quand il est prêt : envoyer avant qu'il ne le soit,
// c'est un manifeste perdu sans erreur.
_viewerReady = true;
_sendManifest();
break;
case 'changed':
_readHotspots(message['hotspots']);
break;
case 'error':
debugPrint('[Model3D] viewer : ${message['message']}');
break;
}
}
Map? _asMap(dynamic data) {
if (data is Map) return data.cast();
if (data is String) {
try {
final decoded = jsonDecode(data);
if (decoded is Map) return decoded.cast();
} catch (_) {}
}
return null;
}
/// Reporte sur les points ce que le gestionnaire vient de déplacer, **et
/// l'enregistre**.
///
/// L'écriture part tout de suite plutôt qu'au bouton « Enregistrer » de la
/// section : déplacer un point est un geste continu, et personne ne pense à
/// sauvegarder après avoir bougé une bille dans une maquette.
void _readHotspots(dynamic raw) {
if (raw is! List) return;
var changed = false;
for (final entry in raw) {
final hotspot = _asMap(entry);
final transform = hotspot == null ? null : _asMap(hotspot['transform']);
final position = transform?['position'];
if (hotspot == null || position is! List || position.length < 3) continue;
final geoPointId = (hotspot['geoPointId'] as num?)?.toInt();
final point = (_dto.points ?? []).firstWhere(
(p) => p.id == geoPointId,
orElse: () => GeoPointDTO(),
);
if (point.id == null) continue;
point.localTransform = Position3D(
x: (position[0] as num).toDouble(),
y: (position[1] as num).toDouble(),
z: (position[2] as num).toDouble(),
rotationY: _yawFromQuaternion(transform?['rotation']),
);
changed = true;
_savePoint(point);
}
if (changed) widget.onChanged(_dto);
}
Future _savePoint(GeoPointDTO point) async {
try {
await _api.sectionScene3DUpdatePoint(point);
} catch (e) {
debugPrint('[Model3D] position non enregistrée : $e');
if (mounted) {
showNotification(kError, kWhite,
AppLocalizations.of(context)!.model3DPointSaveError, context, null);
}
}
}
SectionScene3DApi get _api =>
(Provider.of(context, listen: false).getContext() as ManagerAppContext)
.clientAPI!
.sectionScene3DApi!;
Future _addPoint() async {
final l = AppLocalizations.of(context)!;
final sectionId = _dto.id;
if (sectionId == null) return;
try {
final created = await _api.sectionScene3DCreatePoint(
sectionId,
GeoPointDTO(
title: [TranslationDTO(language: 'FR', value: l.model3DNewPoint)],
description: [],
contents: [],
// À l'origine du modèle : c'est là que l'éditeur le montrera, à portée
// de souris, pour qu'on le pose au bon endroit.
localTransform: Position3D(),
),
);
if (created == null) return;
setState(() => (_dto.points ??= []).add(created));
widget.onChanged(_dto);
_sendManifest();
} catch (e) {
debugPrint('[Model3D] création de point : $e');
if (mounted) {
showNotification(kError, kWhite, l.model3DPointSaveError, context, null);
}
}
}
Future _deletePoint(GeoPointDTO point) async {
if (point.id == null) return;
try {
await _api.sectionScene3DDeletePoint(point.id!);
setState(() => _dto.points?.removeWhere((p) => p.id == point.id));
widget.onChanged(_dto);
_sendManifest();
} catch (e) {
debugPrint('[Model3D] suppression de point : $e');
}
}
/// Le manifeste ne stocke qu'un quaternion ; nous n'avons besoin que du lacet —
/// un panneau posé sur une maquette ne bascule pas.
double? _yawFromQuaternion(dynamic rotation) {
if (rotation is! List || rotation.length < 4) return null;
final x = (rotation[0] as num).toDouble();
final y = (rotation[1] as num).toDouble();
final z = (rotation[2] as num).toDouble();
final w = (rotation[3] as num).toDouble();
final yaw = math.atan2(2 * (w * y + x * z), 1 - 2 * (y * y + x * x));
return yaw * 180 / math.pi;
}
List _quaternionFromYaw(double? degrees) {
if (degrees == null || degrees == 0) return [0, 0, 0, 1];
final half = degrees * math.pi / 180 / 2;
return [0, math.sin(half), 0, math.cos(half)];
}
/// Construit le manifeste minimal que le viewer sait lire : le modèle est le
/// décor, les points sont les hotspots. Tout le reste est comblé par ses valeurs
/// par défaut — c'est une maquette, pas une scène immersive.
void _sendManifest() {
final frame = _frame;
final url = _dto.model3DSource;
if (frame?.contentWindow == null || !_viewerReady || url == null) return;
const assetId = 'model';
final manifest = {
'manifestVersion': 1,
'sceneId': _dto.id ?? '',
'instanceId': _dto.instanceId ?? '',
'configurationId': _dto.configurationId ?? '',
'version': 1,
'coordinateSystem': _coordinateSystem,
'world': {
'kind': 'mesh',
'assetId': assetId,
'transform': {
'position': [0, 0, 0],
'rotation': [0, 0, 0, 1],
'scale': [1, 1, 1],
},
},
'assets': [
{
'id': assetId,
'resourceId': _dto.model3DResourceId,
'url': url,
'mimeType': 'model/gltf-binary',
},
],
'hotspots': (_dto.points ?? []).map((point) {
final position = point.localTransform;
return {
'id': 'poi-${point.id}',
'geoPointId': point.id,
'transform': {
'position': [position?.x ?? 0, position?.y ?? 0, position?.z ?? 0],
'rotation': _quaternionFromYaw(position?.rotationY),
'scale': [1, 1, 1],
},
'title': _localized(point.title),
'description': _localized(point.description),
'contents': const [],
};
}).toList(),
};
frame!.contentWindow!.postMessage(
{'type': 'load', 'manifest': manifest, 'mode': 'edit'},
'*',
);
}
List