Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | 14x 14x 14x 14x 36x 34x 34x 34x 34x 14x 11x 8x 8x 7x 7x 7x 7x 7x 7x 7x 7x 7x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 7x 6x 2x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x | /**
* Géométrie du graphe 3D : boîte englobante des nœuds, pour centrer la cible
* d'orbite de la caméra (le graphe tourne autour de son centre réel, pas de
* l'origine du repère).
*/
/**
* Centre et plus grande dimension de la boîte englobante.
* Les positions sont celles mutées par la simulation (`x`, `y`, `z`).
*
* @param {Array<{x?:number,y?:number,z?:number}>} nodes
* @returns {{center:{x:number,y:number,z:number}, size:number}|null} `null` si aucune position valide
*/
export function measureNodes(nodes = []) {
let minX = Infinity; let minY = Infinity; let minZ = Infinity;
let maxX = -Infinity; let maxY = -Infinity; let maxZ = -Infinity;
let count = 0;
for (const node of nodes) {
if (!Number.isFinite(node?.x) || !Number.isFinite(node?.y) || !Number.isFinite(node?.z)) continue;
minX = Math.min(minX, node.x); maxX = Math.max(maxX, node.x);
minY = Math.min(minY, node.y); maxY = Math.max(maxY, node.y);
minZ = Math.min(minZ, node.z); maxZ = Math.max(maxZ, node.z);
count += 1;
}
if (!count) return null;
return {
center: {
x: (minX + maxX) / 2,
y: (minY + maxY) / 2,
z: (minZ + maxZ) / 2,
},
size: Math.max(maxX - minX, maxY - minY, maxZ - minZ),
};
}
/**
* Distance caméra nécessaire pour que **tous** les nœuds tiennent dans le champ.
*
* Remplace `zoomToFit` de la librairie : la boîte englobante 3D ne suffit pas, un
* nœud proche de la caméra se projette plus loin que sa position (perspective) et
* sort du cadre. On calcule donc, pour chaque nœud, la distance minimale exigée
* par son écart perpendiculaire à l'axe de vue, puis on prend le maximum.
*
* @param {Array<{x?:number,y?:number,z?:number}>} nodes positions (mutées par la simulation)
* @param {{x:number,y:number,z:number}} direction axe cible → caméra (normalisé ou non)
* @param {{ fovDeg?: number, aspect?: number, margin?: number }} [options]
* @returns {number|null} distance caméra → cible, `null` si aucune position
*/
export function fitDistanceForNodes(
nodes = [],
direction = { x: 0, y: 0, z: 1 },
{ fovDeg = 50, aspect = 1.6, margin = 1.08 } = {},
) {
const box = measureNodes(nodes);
if (!box) return null;
const vertical = (fovDeg * Math.PI) / 180;
const horizontal = 2 * Math.atan(Math.tan(vertical / 2) * (aspect || 1));
const tanHalf = Math.tan(Math.min(vertical, horizontal) / 2) || 1;
const length = Math.hypot(direction.x, direction.y, direction.z) || 1;
const ux = direction.x / length;
const uy = direction.y / length;
const uz = direction.z / length;
let distance = 0;
for (const node of nodes) {
Iif (!Number.isFinite(node?.x) || !Number.isFinite(node?.y) || !Number.isFinite(node?.z)) continue;
const dx = node.x - box.center.x;
const dy = node.y - box.center.y;
const dz = node.z - box.center.z;
const depth = dx * ux + dy * uy + dz * uz; // > 0 : du côté de la caméra
const rx = dx - depth * ux;
const ry = dy - depth * uy;
const rz = dz - depth * uz;
const perpendicular = Math.hypot(rx, ry, rz);
distance = Math.max(distance, perpendicular / tanHalf - depth);
}
return Math.max(distance, 1) * margin;
}
/**
* Cadrage sur un nœud : on garde l'orientation et la distance de la caméra
* (le zoom choisi par l'utilisateur) et on déplace la cible d'orbite sur le nœud.
*
* @param {{node:{x?:number,y?:number,z?:number}, cameraPosition:object, target:object,
* distance?:number, minDistance?:number, maxDistance?:number}} options
* @returns {{target:{x:number,y:number,z:number}, position:{x:number,y:number,z:number}}|null}
*/
export function focusCamera({
node,
cameraPosition,
target,
distance,
minDistance = 60,
maxDistance = 4000,
} = {}) {
if (!node || !Number.isFinite(node.x) || !Number.isFinite(node.y) || !Number.isFinite(node.z)) {
return null;
}
Iif (!cameraPosition || !target) return null;
let dx = cameraPosition.x - target.x;
let dy = cameraPosition.y - target.y;
let dz = cameraPosition.z - target.z;
const length = Math.hypot(dx, dy, dz);
Iif (length < 1e-6) {
dx = 0.5; dy = 0.3; dz = 1;
} else {
dx /= length; dy /= length; dz /= length;
}
const wanted = Number.isFinite(distance) && distance > 0 ? distance : length || 300;
const d = Math.min(maxDistance, Math.max(minDistance, wanted));
return {
target: { x: node.x, y: node.y, z: node.z },
position: { x: node.x + dx * d, y: node.y + dy * d, z: node.z + dz * d },
};
}
|