Chantier F - Typage Python - ruff S101 déplacé en per-file-ignores (pyproject.toml) - pyrightconfig.json : mode standard confirmé Chantier G - Assertions & bare-except - 16 blocs except documentés dans config.py - --cov-branch activé, seuil coverage 60→68% Chantier H - Cache settings - Stratégie documentée dans settings.py - _resolve_index_dir() logguée dans config.py Chantier I - Split gros fichiers JS - client.js 1509L → client.js 406L + clientAuth.js + clientOps.js + fetchUtils.js - toolbar.js 927L → toolbar.js 458L + toolbarActions.js + toolbarControls.js - FloatingViewer.js → FloatingViewer.impl.js + floatingViewer*(8 modules) - Viewer_impl.js → ViewerRuntime.js + viewerShell.js + viewerThemeStyles.js + viewerOverlayDismiss.js - model3dRenderer.js → model3dRenderer.impl.js + model3dCore.js + model3dLoaders.js + model3dSupport.js - dialogs.js → dialogs.js + DialogTemplates.js Chantier J - Tests frontend (Phases 1-2) - use_drag_drop.vitest.mjs : 5 tests - use_grid_state.vitest.mjs : 18 tests - security_client_headers, security_deep_merge, security_runtime_state, runtime_store, dialog_templates - Total: 80 fichiers test / 350 tests vitest Chantier K - Pinia migration - runtimeState.js entièrement migré vers Pinia - getOptionalRuntimeStore.js + runtimeEnrichmentState.js ajoutés Chantier L - Durcissement sécurité - tests/security/test_rate_limit.py : 8 tests rate-limit (state machine complète) - clientAuth.js : docstring sessionStorage security boundary - three.js 0.183.2 audité : 0 CVE (Snyk) Chantier M - Simplification config - deps.py, tool_detect.py, utils.py nettoyés docs: PLAN_REFACTO_V2_LONG_TERME.md ajouté et clôturé .gitignore: coverage/ ajouté
196 lines
6.5 KiB
JavaScript
196 lines
6.5 KiB
JavaScript
export function visitModel3DMeshes(object, visit) {
|
|
if (!object || typeof visit !== "function") return;
|
|
try {
|
|
object.traverse?.((child) => {
|
|
if (child?.isMesh || child?.isPoints || child?.isLine) visit(child);
|
|
});
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
|
|
export function hasModel3DMeshMaterials(object) {
|
|
let found = false;
|
|
visitModel3DMeshes(object, (child) => {
|
|
if (!found && child?.isMesh) found = true;
|
|
});
|
|
return found;
|
|
}
|
|
|
|
export function findModel3DHasSkeleton(object) {
|
|
let found = false;
|
|
try {
|
|
object.traverse?.((child) => {
|
|
if (!found && child?.isSkinnedMesh) {
|
|
const boneCount = child.skeleton?.bones?.length ?? 0;
|
|
if (boneCount > 0) found = true;
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
export function storeModel3DOriginalMaterials(object) {
|
|
const store = new Map();
|
|
visitModel3DMeshes(object, (child) => {
|
|
store.set(child.uuid, child.material);
|
|
});
|
|
return store;
|
|
}
|
|
|
|
export function applyModel3DMaterialMode(
|
|
THREE,
|
|
object,
|
|
mode,
|
|
originalMaterials,
|
|
tempMaterials,
|
|
) {
|
|
visitModel3DMeshes(object, (child) => {
|
|
try {
|
|
if (mode === "original") {
|
|
const orig = originalMaterials?.get(child.uuid);
|
|
if (orig !== undefined) child.material = orig;
|
|
} else if (mode === "normal" && child.isMesh) {
|
|
const m = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide });
|
|
tempMaterials?.add(m);
|
|
child.material = m;
|
|
} else if (mode === "depth" && child.isMesh) {
|
|
const m = new THREE.MeshDepthMaterial();
|
|
tempMaterials?.add(m);
|
|
child.material = m;
|
|
} else if (mode === "wireframe" && child.isMesh) {
|
|
const m = new THREE.MeshBasicMaterial({ color: 0x90caf9, wireframe: true });
|
|
tempMaterials?.add(m);
|
|
child.material = m;
|
|
} else if (mode === "pointcloud") {
|
|
const hasColors = !!child.geometry?.getAttribute?.("color");
|
|
const m = new THREE.PointsMaterial({
|
|
size: 0.012,
|
|
sizeAttenuation: true,
|
|
vertexColors: hasColors,
|
|
color: hasColors ? 0xffffff : 0x90caf9,
|
|
});
|
|
tempMaterials?.add(m);
|
|
child.material = m;
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function disposeModel3DTempMaterials(tempMaterials) {
|
|
if (!tempMaterials) return;
|
|
for (const mat of tempMaterials) {
|
|
try {
|
|
mat?.dispose?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
tempMaterials.clear();
|
|
}
|
|
|
|
export function getModel3DAnimations(loaded, loaderType) {
|
|
try {
|
|
if (loaderType === "gltf" || loaderType === "fbx") {
|
|
return Array.isArray(loaded?.animations) ? loaded.animations : [];
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
export function computeModel3DObjectFrame(THREE, object) {
|
|
const box = new THREE.Box3().setFromObject(object);
|
|
if (box.isEmpty()) return null;
|
|
const size = box.getSize(new THREE.Vector3());
|
|
const center = box.getCenter(new THREE.Vector3());
|
|
const maxDim = Math.max(size.x, size.y, size.z, 0.01);
|
|
return { box, size, center, maxDim, radius: Math.max(maxDim * 0.72, 0.5) };
|
|
}
|
|
|
|
export function fitModel3DPerspectiveCamera(THREE, camera, controls, frame) {
|
|
if (!frame || !camera || !controls) return;
|
|
const fov = camera.fov * (Math.PI / 180);
|
|
const distance = (frame.maxDim / (2 * Math.tan(fov / 2))) * 1.55;
|
|
const dir = new THREE.Vector3(1, 0.8, 1).normalize();
|
|
camera.position.copy(frame.center).addScaledVector(dir, distance);
|
|
camera.near = Math.max(0.01, distance / 250);
|
|
camera.far = Math.max(1000, distance * 60);
|
|
camera.updateProjectionMatrix();
|
|
controls.target.copy(frame.center);
|
|
controls.minDistance = Math.max(0.01, distance / 25);
|
|
controls.maxDistance = Math.max(10, distance * 12);
|
|
controls.minZoom = 0.25;
|
|
controls.maxZoom = 12;
|
|
controls.update();
|
|
}
|
|
|
|
export function fitModel3DOrthographicCamera(camera, controls, frame, aspect) {
|
|
if (!frame || !camera || !controls) return null;
|
|
const safeAspect = Math.max(0.0001, Number(aspect) || 1);
|
|
const frustumHeight = Math.max(frame.maxDim * 1.9, 2);
|
|
const frustumWidth = frustumHeight * safeAspect;
|
|
camera.left = -frustumWidth / 2;
|
|
camera.right = frustumWidth / 2;
|
|
camera.top = frustumHeight / 2;
|
|
camera.bottom = -frustumHeight / 2;
|
|
camera.near = -Math.max(1000, frame.maxDim * 40);
|
|
camera.far = Math.max(1000, frame.maxDim * 40);
|
|
camera.zoom = 1;
|
|
camera.position
|
|
.copy(frame.center)
|
|
.add({ x: frame.radius * 2.1, y: frame.radius * 1.6, z: frame.radius * 2.1 });
|
|
camera.updateProjectionMatrix();
|
|
controls.target.copy(frame.center);
|
|
controls.minZoom = 0.25;
|
|
controls.maxZoom = 12;
|
|
controls.minDistance = 0;
|
|
controls.maxDistance = Math.max(10, frame.maxDim * 12);
|
|
controls.update();
|
|
return frustumHeight;
|
|
}
|
|
|
|
export function buildRenderableModel3DObject(THREE, loaderType, loaded) {
|
|
if (!loaded) return null;
|
|
if (loaderType === "gltf") return loaded.scene || null;
|
|
if (loaderType === "obj" || loaderType === "fbx") return loaded;
|
|
if (loaderType === "stl" || loaderType === "ply") {
|
|
const geometry = loaded;
|
|
if (geometry?.computeVertexNormals) {
|
|
try {
|
|
geometry.computeVertexNormals();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
const hasNormals = !!geometry?.getAttribute?.("normal");
|
|
const hasColors = !!geometry?.getAttribute?.("color");
|
|
if (loaderType === "ply" && !hasNormals) {
|
|
return new THREE.Points(
|
|
geometry,
|
|
new THREE.PointsMaterial({
|
|
size: 0.02,
|
|
sizeAttenuation: true,
|
|
vertexColors: hasColors,
|
|
color: hasColors ? 0xffffff : 0x90caf9,
|
|
}),
|
|
);
|
|
}
|
|
return new THREE.Mesh(
|
|
geometry,
|
|
new THREE.MeshStandardMaterial({
|
|
color: hasColors ? 0xffffff : 0xd8dee9,
|
|
metalness: 0.08,
|
|
roughness: 0.72,
|
|
vertexColors: hasColors,
|
|
}),
|
|
);
|
|
}
|
|
return null;
|
|
}
|