This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch feat/infra-3d-live-pipeline
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git

commit d808e25893b68a4ba0404bb88a27fc806559d012
Author: Wu Sheng <[email protected]>
AuthorDate: Fri May 29 22:59:34 2026 +0800

    feat(infra-3d): render live topology by default; tune camera default
    
    The 3D map now builds its structure from sequential per-layer OAP reads
    (services + service maps), assembled into a DemoTopology that
    buildSceneGraph consumes in place of the committed snapshot. The scene is
    re-keyed on a per-layer structure hash, so a 60s refresh that finds the
    same services/edges leaves the camera + metric/alarm visuals untouched
    and only a service appearing/disappearing rebuilds it. Until the first
    sequential load lands (or if it returns empty) the snapshot renders as a
    fallback; `?live=0` forces the snapshot for comparison.
    
    Also tune the default camera to a tighter, slightly lower 3/4 framing.
---
 apps/ui/src/features/infra-3d/Infra3DScene.vue | 14 ++++++++--
 apps/ui/src/features/infra-3d/Infra3DView.vue  | 38 ++++++++++++++++++++++----
 2 files changed, 43 insertions(+), 9 deletions(-)

diff --git a/apps/ui/src/features/infra-3d/Infra3DScene.vue 
b/apps/ui/src/features/infra-3d/Infra3DScene.vue
index e92a974..f4caa25 100644
--- a/apps/ui/src/features/infra-3d/Infra3DScene.vue
+++ b/apps/ui/src/features/infra-3d/Infra3DScene.vue
@@ -59,6 +59,7 @@ import {
 import {
   buildSceneGraph,
   loadDemoTopology,
+  type DemoTopology,
   type SceneCallEdge,
   type SceneCrossLayerEdge,
   type SceneHierarchyEdge,
@@ -116,6 +117,11 @@ interface Props {
    *  layer with a rule yielding ≥2 clusters is laid out cluster-by-
    *  cluster (k8s/mesh namespace grouping), matching the 2D map. */
   namingByLayer?: Record<string, ServiceNamingRule | null>;
+  /** Live-assembled topology to render instead of the committed snapshot.
+   *  Null/absent ⇒ the snapshot. The build below runs once at setup, so
+   *  the parent re-keys this component to rebuild when the live structure
+   *  changes. */
+  topology?: DemoTopology | null;
 }
 const props = defineProps<Props>();
 const emit = defineEmits<{
@@ -132,7 +138,7 @@ const emit = defineEmits<{
 // loaded before mounting this component (see Infra3DView.vue), so
 // `levelForLayer` returns deterministic values here, not the synchronous
 // fallback. `planeOrder` is the source of truth for vertical stacking.
-const topo = loadDemoTopology();
+const topo = props.topology ?? loadDemoTopology();
 const graph = buildSceneGraph(topo, levelForLayer);
 const placement = computePlacement(graph, props.planeOrder, props.groups, 
props.namingByLayer);
 
@@ -1262,8 +1268,10 @@ function defaultCameraPos(): [number, number, number] {
   const cz = (b.minZ + b.maxZ) / 2;
   const spanXZ = Math.max(b.maxX - b.minX, b.maxZ - b.minZ);
   const spanY = Math.max(8, b.maxY - b.minY);
-  const radius = spanXZ * 1.0 + 6;
-  return [cx + radius * 0.8, b.minY + spanY * 0.55 + radius * 0.55, cz + 
radius * 0.8];
+  // Tighter, slightly lower 3/4 framing — fills the viewport with the tier
+  // stack rather than a wide, high isometric.
+  const radius = spanXZ * 0.78 + 5;
+  return [cx + radius * 0.8, b.minY + spanY * 0.5 + radius * 0.5, cz + radius 
* 0.8];
 }
 function defaultTargetPos(): [number, number, number] {
   const b = placement.bounds;
diff --git a/apps/ui/src/features/infra-3d/Infra3DView.vue 
b/apps/ui/src/features/infra-3d/Infra3DView.vue
index 0b8e686..eeed071 100644
--- a/apps/ui/src/features/infra-3d/Infra3DView.vue
+++ b/apps/ui/src/features/infra-3d/Infra3DView.vue
@@ -153,17 +153,42 @@ interface PipelineCtx {
   topo: DemoTopology | null;
 }
 
-// Phase-1 seam for live data. Opt-in per visit with `?live=1`; with no
-// param (default) the scene renders from the committed snapshot exactly
-// as before, so the live wire can't break the map. `liveTopo` holds the
-// latest assembly so a later phase can swap the scene's data source.
-const liveTopologyEnabled = computed(() => route.query.live === '1');
+// Live data is the default. `?live=0` forces the committed snapshot (a
+// debug / comparison escape hatch). `liveTopo` holds the latest sequential
+// assembly; until the first run lands (or if it comes back empty) the
+// scene falls back to the snapshot so the map is never blank.
+const liveTopologyEnabled = computed(() => route.query.live !== '0');
 const liveTopo = shallowRef<DemoTopology | null>(null);
 const liveWindow = (): LiveWindow => ({
   startMs: Date.now() - 2 * 3600_000,
   endMs: Date.now(),
   step: 'HOUR',
 });
+
+// Topology the scene renders. The scene builds its graph once at setup, so
+// it is re-keyed on a STRUCTURE hash (per-layer service rosters + edge
+// counts): a 60s refresh that finds the same structure leaves the key
+// unchanged — no remount, so the camera and metric/alarm visuals persist —
+// while a service appearing / disappearing rebuilds the scene.
+const sceneTopology = computed<DemoTopology | null>(() => {
+  if (!liveTopologyEnabled.value) return null;
+  const t = liveTopo.value;
+  if (!t || Object.keys(t.servicesByLayer).length === 0) return null;
+  return t;
+});
+const sceneKey = computed(() => {
+  const naming = namingReady.value ? 'n' : '-';
+  const t = sceneTopology.value;
+  if (!t) return `${naming}:snapshot`;
+  const struct = t.layers
+    .map((L) => {
+      const ids = (t.servicesByLayer[L.key] ?? []).map((s) => s.id).sort();
+      const calls = t.topologies[L.key]?.calls.length ?? 0;
+      return `${L.key}:${calls}:${ids.join(',')}`;
+    })
+    .join('|');
+  return `${naming}:${struct}`;
+});
 const pipelineImpls: Record<PipelineStageId, StageImpl<PipelineCtx>> = {
   services: async (rep, ctx) => {
     rep.start();
@@ -792,9 +817,10 @@ const visibleServices = computed(() => {
       <div v-else-if="!ready" class="cfg-loading">Loading 3D map 
configuration…</div>
       <Infra3DScene
         v-else
-        :key="namingReady ? 'with-naming' : 'no-naming'"
+        :key="sceneKey"
         ref="sceneRef"
         :plane-order="planeOrder"
+        :topology="sceneTopology"
         :visible-layers="visibleLayers"
         :hovered-node-id="hoveredNodeId"
         :selected-node-id="selectedNodeId"

Reply via email to