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"
