diff --git a/.env.example b/.env.example index 6eb25ea..7e791bc 100644 --- a/.env.example +++ b/.env.example @@ -21,8 +21,18 @@ COMBINE_OUTPUT_LOCATION=/data/vkg_full.ttl COMBINE_FORCE=true # AnzoGraph / SPARQL endpoint settings +# Use `local` for the Docker Compose AnzoGraph container, or `external` for the +# remote endpoint below using a runtime-generated bearer token. +SPARQL_SOURCE_MODE=local SPARQL_HOST=http://anzograph:8080 # SPARQL_ENDPOINT=http://anzograph:8080/sparql +EXTERNAL_SPARQL_ENDPOINT=https://anzograph.k8s.inf.ufrgs.br/sparql +KEYCLOAK_TOKEN_ENDPOINT=https://keycloak.k8s.inf.ufrgs.br/realms/INF/protocol/openid-connect/token +KEYCLOAK_CLIENT_ID=anzograph +KEYCLOAK_USERNAME= +KEYCLOAK_PASSWORD= +KEYCLOAK_SCOPE=openid +ACCESS_TOKEN= SPARQL_USER=admin SPARQL_PASS=Passw0rd1 @@ -32,7 +42,6 @@ SPARQL_DATA_FILE=file:///opt/shared-files/o3po.ttl # Currently not used. # Startup behavior for AnzoGraph mode SPARQL_LOAD_ON_START=false -SPARQL_CLEAR_ON_START=false SPARQL_READY_TIMEOUT_S=10 # Dev UX @@ -40,18 +49,69 @@ CORS_ORIGINS=http://localhost:5173 VITE_BACKEND_URL=http://backend:8000 # Frontend right-pane cosmos.gl layout +# Colors accept CSS strings or RGBA tuples like [53,214,255,255]. +# Ranges accept comma-separated values like 50,150. VITE_COSMOS_ENABLE_SIMULATION=true VITE_COSMOS_DEBUG_LAYOUT=false +VITE_COSMOS_BACKGROUND_COLOR="#05070a" VITE_COSMOS_SPACE_SIZE=4096 +VITE_COSMOS_POINT_DEFAULT_COLOR="#b3b3b3" +VITE_COSMOS_POINT_GREYOUT_COLOR= +VITE_COSMOS_POINT_GREYOUT_OPACITY= +VITE_COSMOS_POINT_DEFAULT_SIZE=4 +VITE_COSMOS_POINT_OPACITY=1 +VITE_COSMOS_POINT_SIZE_SCALE=1 +VITE_COSMOS_HOVERED_POINT_CURSOR=pointer +VITE_COSMOS_HOVERED_LINK_CURSOR=pointer +VITE_COSMOS_RENDER_HOVERED_POINT_RING=true +VITE_COSMOS_HOVERED_POINT_RING_COLOR="#35d6ff" +VITE_COSMOS_FOCUSED_POINT_RING_COLOR=white +VITE_COSMOS_RENDER_LINKS=true +VITE_COSMOS_LINK_DEFAULT_COLOR="#666666" +VITE_COSMOS_LINK_OPACITY=1 +VITE_COSMOS_LINK_GREYOUT_OPACITY=0.1 +VITE_COSMOS_LINK_DEFAULT_WIDTH=1 +VITE_COSMOS_HOVERED_LINK_COLOR="#ffd166" +VITE_COSMOS_HOVERED_LINK_WIDTH_INCREASE=2.5 +VITE_COSMOS_LINK_WIDTH_SCALE=1 +VITE_COSMOS_SCALE_LINKS_ON_ZOOM=false VITE_COSMOS_CURVED_LINKS=true +VITE_COSMOS_CURVED_LINK_SEGMENTS=19 +VITE_COSMOS_CURVED_LINK_WEIGHT=0.8 +VITE_COSMOS_CURVED_LINK_CONTROL_POINT_DISTANCE=0.5 +VITE_COSMOS_LINK_DEFAULT_ARROWS=false +VITE_COSMOS_LINK_ARROWS_SIZE_SCALE=1 +VITE_COSMOS_LINK_VISIBILITY_DISTANCE_RANGE=50,150 +VITE_COSMOS_LINK_VISIBILITY_MIN_TRANSPARENCY=0.25 +VITE_COSMOS_USE_CLASSIC_QUADTREE=false VITE_COSMOS_FIT_VIEW_PADDING=0.12 +VITE_COSMOS_FIT_VIEW_ON_INIT=false +VITE_COSMOS_FIT_VIEW_DELAY=250 +VITE_COSMOS_FIT_VIEW_DURATION=250 VITE_COSMOS_SIMULATION_DECAY=5000 VITE_COSMOS_SIMULATION_GRAVITY=0 VITE_COSMOS_SIMULATION_CENTER=0.05 VITE_COSMOS_SIMULATION_REPULSION=0.5 +VITE_COSMOS_SIMULATION_REPULSION_THETA=1.15 +VITE_COSMOS_SIMULATION_REPULSION_QUADTREE_LEVELS=12 VITE_COSMOS_SIMULATION_LINK_SPRING=1 VITE_COSMOS_SIMULATION_LINK_DISTANCE=10 +VITE_COSMOS_SIMULATION_LINK_DISTANCE_RANDOM_VARIATION_RANGE=1,1.2 +VITE_COSMOS_SIMULATION_REPULSION_FROM_MOUSE=2 +VITE_COSMOS_ENABLE_RIGHT_CLICK_REPULSION=false VITE_COSMOS_SIMULATION_FRICTION=0.1 +VITE_COSMOS_SIMULATION_CLUSTER=0.1 +VITE_COSMOS_SHOW_FPS_MONITOR=false +VITE_COSMOS_PIXEL_RATIO=2 +VITE_COSMOS_SCALE_POINTS_ON_ZOOM=false +VITE_COSMOS_INITIAL_ZOOM_LEVEL= +VITE_COSMOS_ENABLE_ZOOM=true +VITE_COSMOS_ENABLE_SIMULATION_DURING_ZOOM=false +VITE_COSMOS_ENABLE_DRAG=true +VITE_COSMOS_RANDOM_SEED= +VITE_COSMOS_POINT_SAMPLING_DISTANCE=150 +VITE_COSMOS_RESCALE_POSITIONS=false +VITE_COSMOS_ATTRIBUTION= # Debugging LOG_SNAPSHOT_TIMINGS=false diff --git a/frontend/src/TripleGraphView.tsx b/frontend/src/TripleGraphView.tsx index 1c5d61e..c2b03a1 100644 --- a/frontend/src/TripleGraphView.tsx +++ b/frontend/src/TripleGraphView.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useMemo, useRef, useState } from "react"; import { Graph, type GraphConfig } from "@cosmos.gl/graph"; -import { cosmosRuntimeConfig } from "./cosmos_config"; +import { cosmosBackgroundCss, cosmosRuntimeConfig } from "./cosmos_config"; import { computeLayoutMetrics, type GraphLayoutMetrics, @@ -194,28 +194,68 @@ export const TripleGraphView = memo(function TripleGraphView({ model }: TripleGr }; const config: GraphConfig = { - backgroundColor: "#05070a", + backgroundColor: cosmosRuntimeConfig.backgroundColor, spaceSize: cosmosRuntimeConfig.spaceSize, enableSimulation: cosmosRuntimeConfig.enableSimulation, - enableDrag: true, - enableZoom: true, - fitViewOnInit: false, + pointDefaultColor: cosmosRuntimeConfig.pointDefaultColor, + pointGreyoutColor: cosmosRuntimeConfig.pointGreyoutColor, + pointGreyoutOpacity: cosmosRuntimeConfig.pointGreyoutOpacity, + pointDefaultSize: cosmosRuntimeConfig.pointDefaultSize, + pointOpacity: cosmosRuntimeConfig.pointOpacity, + pointSizeScale: cosmosRuntimeConfig.pointSizeScale, + hoveredPointCursor: cosmosRuntimeConfig.hoveredPointCursor, + hoveredLinkCursor: cosmosRuntimeConfig.hoveredLinkCursor, + renderHoveredPointRing: cosmosRuntimeConfig.renderHoveredPointRing, + hoveredPointRingColor: cosmosRuntimeConfig.hoveredPointRingColor, + focusedPointRingColor: cosmosRuntimeConfig.focusedPointRingColor, + renderLinks: cosmosRuntimeConfig.renderLinks, + linkDefaultColor: cosmosRuntimeConfig.linkDefaultColor, + linkOpacity: cosmosRuntimeConfig.linkOpacity, + linkGreyoutOpacity: cosmosRuntimeConfig.linkGreyoutOpacity, + linkDefaultWidth: cosmosRuntimeConfig.linkDefaultWidth, + hoveredLinkColor: cosmosRuntimeConfig.hoveredLinkColor, + hoveredLinkWidthIncrease: cosmosRuntimeConfig.hoveredLinkWidthIncrease, + linkWidthScale: cosmosRuntimeConfig.linkWidthScale, + scaleLinksOnZoom: cosmosRuntimeConfig.scaleLinksOnZoom, + enableDrag: cosmosRuntimeConfig.enableDrag, + enableZoom: cosmosRuntimeConfig.enableZoom, + enableSimulationDuringZoom: cosmosRuntimeConfig.enableSimulationDuringZoom, + fitViewOnInit: cosmosRuntimeConfig.fitViewOnInit, + fitViewDelay: cosmosRuntimeConfig.fitViewDelay, fitViewPadding: cosmosRuntimeConfig.fitViewPadding, - rescalePositions: false, + fitViewDuration: cosmosRuntimeConfig.fitViewDuration, + initialZoomLevel: cosmosRuntimeConfig.initialZoomLevel, + pointSamplingDistance: cosmosRuntimeConfig.pointSamplingDistance, + rescalePositions: cosmosRuntimeConfig.rescalePositions, curvedLinks: cosmosRuntimeConfig.curvedLinks, + curvedLinkSegments: cosmosRuntimeConfig.curvedLinkSegments, + curvedLinkWeight: cosmosRuntimeConfig.curvedLinkWeight, + curvedLinkControlPointDistance: cosmosRuntimeConfig.curvedLinkControlPointDistance, + linkDefaultArrows: cosmosRuntimeConfig.linkDefaultArrows, + linkArrowsSizeScale: cosmosRuntimeConfig.linkArrowsSizeScale, + linkVisibilityDistanceRange: cosmosRuntimeConfig.linkVisibilityDistanceRange, + linkVisibilityMinTransparency: cosmosRuntimeConfig.linkVisibilityMinTransparency, + useClassicQuadtree: cosmosRuntimeConfig.useClassicQuadtree, simulationDecay: cosmosRuntimeConfig.simulationDecay, simulationGravity: cosmosRuntimeConfig.simulationGravity, simulationCenter: cosmosRuntimeConfig.simulationCenter, simulationRepulsion: cosmosRuntimeConfig.simulationRepulsion, + simulationRepulsionTheta: cosmosRuntimeConfig.simulationRepulsionTheta, + simulationRepulsionQuadtreeLevels: + cosmosRuntimeConfig.simulationRepulsionQuadtreeLevels, simulationLinkSpring: cosmosRuntimeConfig.simulationLinkSpring, simulationLinkDistance: cosmosRuntimeConfig.simulationLinkDistance, + simulationLinkDistRandomVariationRange: + cosmosRuntimeConfig.simulationLinkDistRandomVariationRange, + simulationRepulsionFromMouse: cosmosRuntimeConfig.simulationRepulsionFromMouse, + enableRightClickRepulsion: cosmosRuntimeConfig.enableRightClickRepulsion, simulationFriction: cosmosRuntimeConfig.simulationFriction, - renderHoveredPointRing: true, - hoveredPointRingColor: "#35d6ff", - hoveredPointCursor: "pointer", - hoveredLinkCursor: "pointer", - hoveredLinkColor: "#ffd166", - hoveredLinkWidthIncrease: 2.5, + simulationCluster: cosmosRuntimeConfig.simulationCluster, + randomSeed: cosmosRuntimeConfig.randomSeed, + showFPSMonitor: cosmosRuntimeConfig.showFPSMonitor, + pixelRatio: cosmosRuntimeConfig.pixelRatio, + scalePointsOnZoom: cosmosRuntimeConfig.scalePointsOnZoom, + attribution: cosmosRuntimeConfig.attribution, onSimulationStart: () => { reportLayout("simulation-start", "running", 1); }, @@ -320,7 +360,14 @@ export const TripleGraphView = memo(function TripleGraphView({ model }: TripleGr }, [activeDetail]); return ( -
+
{ - graph.fitViewByPointPositions(Array.from(model.pointPositions), 0, cosmosRuntimeConfig.fitViewPadding); + if (typeof cosmosRuntimeConfig.initialZoomLevel === "number") { + graph.setZoomLevel( + cosmosRuntimeConfig.initialZoomLevel, + cosmosRuntimeConfig.fitViewDuration, + ); + } else { + graph.fitViewByPointPositions( + Array.from(model.pointPositions), + cosmosRuntimeConfig.fitViewDuration, + cosmosRuntimeConfig.fitViewPadding, + ); + } if (cosmosRuntimeConfig.enableSimulation) { graph.start(1); } diff --git a/frontend/src/cosmos_config.ts b/frontend/src/cosmos_config.ts index 65df336..835718c 100644 --- a/frontend/src/cosmos_config.ts +++ b/frontend/src/cosmos_config.ts @@ -1,3 +1,5 @@ +type CosmosColor = string | [number, number, number, number]; + function parseBoolean(value: string | undefined, fallback: boolean): boolean { if (value === undefined) return fallback; const normalized = value.trim().toLowerCase(); @@ -12,17 +14,167 @@ function parseNumber(value: string | undefined, fallback: number): number { return Number.isFinite(parsed) ? parsed : fallback; } +function parseOptionalNumber(value: string | undefined): number | undefined { + if (value === undefined) return undefined; + const trimmed = value.trim(); + if (trimmed.length === 0) return undefined; + const parsed = Number(trimmed); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function parseOptionalString(value: string | undefined): string | undefined { + if (value === undefined) return undefined; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} + +function parseNumberList(value: string | undefined, fallback: number[]): number[] { + return parseOptionalNumberList(value) ?? fallback; +} + +function parseOptionalNumberList(value: string | undefined): number[] | undefined { + const raw = parseOptionalString(value); + if (!raw) return undefined; + const normalized = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw; + const parts = normalized + .split(",") + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0); + if (parts.length === 0) return undefined; + const parsed = parts.map((entry) => Number(entry)); + return parsed.every((entry) => Number.isFinite(entry)) ? parsed : undefined; +} + +function parseColor(value: string | undefined, fallback: CosmosColor): CosmosColor { + return parseOptionalColor(value) ?? fallback; +} + +function parseOptionalColor(value: string | undefined): CosmosColor | undefined { + const raw = parseOptionalString(value); + if (!raw) return undefined; + const rgba = parseOptionalNumberList(raw); + if (rgba && rgba.length === 4) { + return [rgba[0], rgba[1], rgba[2], rgba[3]]; + } + return raw; +} + +function parseOptionalSeed(value: string | undefined): number | string | undefined { + const raw = parseOptionalString(value); + if (!raw) return undefined; + const numeric = Number(raw); + return Number.isFinite(numeric) ? numeric : raw; +} + +function toCssColor(color: CosmosColor): string { + if (typeof color === "string") return color; + return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] / 255})`; +} + export const cosmosRuntimeConfig = { enableSimulation: parseBoolean(import.meta.env.VITE_COSMOS_ENABLE_SIMULATION, true), debugLayout: parseBoolean(import.meta.env.VITE_COSMOS_DEBUG_LAYOUT, false), + backgroundColor: parseColor(import.meta.env.VITE_COSMOS_BACKGROUND_COLOR, "#05070a"), spaceSize: parseNumber(import.meta.env.VITE_COSMOS_SPACE_SIZE, 4096), + pointDefaultColor: parseOptionalColor(import.meta.env.VITE_COSMOS_POINT_DEFAULT_COLOR), + pointGreyoutColor: parseOptionalColor(import.meta.env.VITE_COSMOS_POINT_GREYOUT_COLOR), + pointGreyoutOpacity: parseOptionalNumber(import.meta.env.VITE_COSMOS_POINT_GREYOUT_OPACITY), + pointDefaultSize: parseNumber(import.meta.env.VITE_COSMOS_POINT_DEFAULT_SIZE, 4), + pointOpacity: parseNumber(import.meta.env.VITE_COSMOS_POINT_OPACITY, 1), + pointSizeScale: parseNumber(import.meta.env.VITE_COSMOS_POINT_SIZE_SCALE, 1), + hoveredPointCursor: parseOptionalString(import.meta.env.VITE_COSMOS_HOVERED_POINT_CURSOR) ?? "pointer", + hoveredLinkCursor: parseOptionalString(import.meta.env.VITE_COSMOS_HOVERED_LINK_CURSOR) ?? "pointer", + renderHoveredPointRing: parseBoolean( + import.meta.env.VITE_COSMOS_RENDER_HOVERED_POINT_RING, + true, + ), + hoveredPointRingColor: parseColor( + import.meta.env.VITE_COSMOS_HOVERED_POINT_RING_COLOR, + "#35d6ff", + ), + focusedPointRingColor: parseColor( + import.meta.env.VITE_COSMOS_FOCUSED_POINT_RING_COLOR, + "white", + ), + renderLinks: parseBoolean(import.meta.env.VITE_COSMOS_RENDER_LINKS, true), + linkDefaultColor: parseOptionalColor(import.meta.env.VITE_COSMOS_LINK_DEFAULT_COLOR), + linkOpacity: parseNumber(import.meta.env.VITE_COSMOS_LINK_OPACITY, 1), + linkGreyoutOpacity: parseNumber(import.meta.env.VITE_COSMOS_LINK_GREYOUT_OPACITY, 0.1), + linkDefaultWidth: parseNumber(import.meta.env.VITE_COSMOS_LINK_DEFAULT_WIDTH, 1), + hoveredLinkColor: parseColor(import.meta.env.VITE_COSMOS_HOVERED_LINK_COLOR, "#ffd166"), + hoveredLinkWidthIncrease: parseNumber( + import.meta.env.VITE_COSMOS_HOVERED_LINK_WIDTH_INCREASE, + 2.5, + ), + linkWidthScale: parseNumber(import.meta.env.VITE_COSMOS_LINK_WIDTH_SCALE, 1), + scaleLinksOnZoom: parseBoolean(import.meta.env.VITE_COSMOS_SCALE_LINKS_ON_ZOOM, false), curvedLinks: parseBoolean(import.meta.env.VITE_COSMOS_CURVED_LINKS, true), - fitViewPadding: parseNumber(import.meta.env.VITE_COSMOS_FIT_VIEW_PADDING, 0.12), + curvedLinkSegments: parseNumber(import.meta.env.VITE_COSMOS_CURVED_LINK_SEGMENTS, 19), + curvedLinkWeight: parseNumber(import.meta.env.VITE_COSMOS_CURVED_LINK_WEIGHT, 0.8), + curvedLinkControlPointDistance: parseNumber( + import.meta.env.VITE_COSMOS_CURVED_LINK_CONTROL_POINT_DISTANCE, + 0.5, + ), + linkDefaultArrows: parseBoolean(import.meta.env.VITE_COSMOS_LINK_DEFAULT_ARROWS, false), + linkArrowsSizeScale: parseNumber(import.meta.env.VITE_COSMOS_LINK_ARROWS_SIZE_SCALE, 1), + linkVisibilityDistanceRange: parseNumberList( + import.meta.env.VITE_COSMOS_LINK_VISIBILITY_DISTANCE_RANGE, + [50, 150], + ), + linkVisibilityMinTransparency: parseNumber( + import.meta.env.VITE_COSMOS_LINK_VISIBILITY_MIN_TRANSPARENCY, + 0.25, + ), + useClassicQuadtree: parseBoolean(import.meta.env.VITE_COSMOS_USE_CLASSIC_QUADTREE, false), simulationDecay: parseNumber(import.meta.env.VITE_COSMOS_SIMULATION_DECAY, 5000), simulationGravity: parseNumber(import.meta.env.VITE_COSMOS_SIMULATION_GRAVITY, 0), simulationCenter: parseNumber(import.meta.env.VITE_COSMOS_SIMULATION_CENTER, 0.05), simulationRepulsion: parseNumber(import.meta.env.VITE_COSMOS_SIMULATION_REPULSION, 0.5), + simulationRepulsionTheta: parseNumber( + import.meta.env.VITE_COSMOS_SIMULATION_REPULSION_THETA, + 1.15, + ), + simulationRepulsionQuadtreeLevels: parseNumber( + import.meta.env.VITE_COSMOS_SIMULATION_REPULSION_QUADTREE_LEVELS, + 12, + ), simulationLinkSpring: parseNumber(import.meta.env.VITE_COSMOS_SIMULATION_LINK_SPRING, 1), simulationLinkDistance: parseNumber(import.meta.env.VITE_COSMOS_SIMULATION_LINK_DISTANCE, 10), + simulationLinkDistRandomVariationRange: parseNumberList( + import.meta.env.VITE_COSMOS_SIMULATION_LINK_DISTANCE_RANDOM_VARIATION_RANGE, + [1, 1.2], + ), + simulationRepulsionFromMouse: parseNumber( + import.meta.env.VITE_COSMOS_SIMULATION_REPULSION_FROM_MOUSE, + 2, + ), + enableRightClickRepulsion: parseBoolean( + import.meta.env.VITE_COSMOS_ENABLE_RIGHT_CLICK_REPULSION, + false, + ), simulationFriction: parseNumber(import.meta.env.VITE_COSMOS_SIMULATION_FRICTION, 0.1), -} as const; + simulationCluster: parseNumber(import.meta.env.VITE_COSMOS_SIMULATION_CLUSTER, 0.1), + showFPSMonitor: parseBoolean(import.meta.env.VITE_COSMOS_SHOW_FPS_MONITOR, false), + pixelRatio: parseNumber(import.meta.env.VITE_COSMOS_PIXEL_RATIO, 2), + scalePointsOnZoom: parseBoolean(import.meta.env.VITE_COSMOS_SCALE_POINTS_ON_ZOOM, false), + initialZoomLevel: parseOptionalNumber(import.meta.env.VITE_COSMOS_INITIAL_ZOOM_LEVEL), + enableZoom: parseBoolean(import.meta.env.VITE_COSMOS_ENABLE_ZOOM, true), + enableSimulationDuringZoom: parseBoolean( + import.meta.env.VITE_COSMOS_ENABLE_SIMULATION_DURING_ZOOM, + false, + ), + enableDrag: parseBoolean(import.meta.env.VITE_COSMOS_ENABLE_DRAG, true), + fitViewOnInit: parseBoolean(import.meta.env.VITE_COSMOS_FIT_VIEW_ON_INIT, false), + fitViewDelay: parseNumber(import.meta.env.VITE_COSMOS_FIT_VIEW_DELAY, 250), + fitViewPadding: parseNumber(import.meta.env.VITE_COSMOS_FIT_VIEW_PADDING, 0.12), + fitViewDuration: parseNumber(import.meta.env.VITE_COSMOS_FIT_VIEW_DURATION, 250), + randomSeed: parseOptionalSeed(import.meta.env.VITE_COSMOS_RANDOM_SEED), + pointSamplingDistance: parseNumber( + import.meta.env.VITE_COSMOS_POINT_SAMPLING_DISTANCE, + 150, + ), + rescalePositions: parseBoolean(import.meta.env.VITE_COSMOS_RESCALE_POSITIONS, false), + attribution: parseOptionalString(import.meta.env.VITE_COSMOS_ATTRIBUTION), +}; + +export const cosmosBackgroundCss = toCssColor(cosmosRuntimeConfig.backgroundColor); diff --git a/frontend/src/renderer.ts b/frontend/src/renderer.ts index b0d4465..877f6b4 100644 --- a/frontend/src/renderer.ts +++ b/frontend/src/renderer.ts @@ -213,12 +213,23 @@ export class Renderer { const count = xs.length; const edgeCount = edges.length / 2; this.nodeCount = count; + console.log("[renderer.init] start", { + nodes: count, + edges: edgeCount, + route_line_vertices: routeLineVertices ? routeLineVertices.length / 2 : 0, + }); // Build quadtree (spatially sorts the array) + const spatialStart = performance.now(); const { sorted, leaves, order } = buildSpatialIndex(xs, ys); this.leaves = leaves; this.sorted = sorted; this.sortedToOriginal = order; + console.log("[renderer.init] spatial index built", { + nodes: count, + leaves: leaves.length, + spatial_ms: Math.round(performance.now() - spatialStart), + }); // Pre-allocate arrays for render loop (zero-allocation rendering) this.visibleLeafIndices = new Uint32Array(leaves.length); @@ -226,12 +237,18 @@ export class Renderer { this.countsArray = new Int32Array(leaves.length); // Upload sorted particles to GPU as STATIC VBO (never changes) + const uploadNodesStart = performance.now(); gl.bindVertexArray(this.vao); gl.bindBuffer(gl.ARRAY_BUFFER, this.nodeVbo); gl.bufferData(gl.ARRAY_BUFFER, sorted, gl.STATIC_DRAW); gl.bindVertexArray(null); + console.log("[renderer.init] node buffer uploaded", { + upload_ms: Math.round(performance.now() - uploadNodesStart), + sorted_bytes: sorted.byteLength, + }); // Build vertex ID → original input index mapping + const mapsStart = performance.now(); const vertexIdToOriginal = new Map(); for (let i = 0; i < count; i++) { vertexIdToOriginal.set(vertexIds[i], i); @@ -250,6 +267,10 @@ export class Renderer { vertexIdToSortedIndex.set(vertexIds[i], originalToSorted[i]); } this.vertexIdToSortedIndex = vertexIdToSortedIndex; + console.log("[renderer.init] index maps built", { + maps_ms: Math.round(performance.now() - mapsStart), + vertex_id_map_size: vertexIdToSortedIndex.size, + }); this.useRawLineSegments = routeLineVertices !== null && routeLineVertices.length > 0; this.rawLineVertexCount = this.useRawLineSegments && routeLineVertices ? routeLineVertices.length / 2 : 0; @@ -257,14 +278,20 @@ export class Renderer { this.edgeCount = edgeCount; this.leafEdgeStarts = new Uint32Array(0); this.leafEdgeCounts = new Uint32Array(0); + const uploadRoutesStart = performance.now(); gl.bindVertexArray(this.lineVao); gl.bindBuffer(gl.ARRAY_BUFFER, this.lineVbo); gl.bufferData(gl.ARRAY_BUFFER, routeLineVertices, gl.STATIC_DRAW); gl.bindVertexArray(null); + console.log("[renderer.init] raw line segments uploaded", { + upload_ms: Math.round(performance.now() - uploadRoutesStart), + total_ms: Math.round(performance.now() - t0), + }); return performance.now() - t0; } // Remap edges from vertex IDs to sorted indices + const remapEdgesStart = performance.now(); const lineIndices = new Uint32Array(edgeCount * 2); let validEdges = 0; for (let i = 0; i < edgeCount; i++) { @@ -278,9 +305,15 @@ export class Renderer { validEdges++; } this.edgeCount = validEdges; + console.log("[renderer.init] edges remapped", { + remap_ms: Math.round(performance.now() - remapEdgesStart), + valid_edges: validEdges, + line_indices_bytes: lineIndices.byteLength, + }); // Build per-leaf edge index for efficient visible-only edge drawing // Find which leaf each sorted index belongs to + const edgeIndexStart = performance.now(); const nodeToLeaf = new Uint32Array(count); for (let li = 0; li < leaves.length; li++) { const lf = leaves[li]; @@ -314,11 +347,22 @@ export class Renderer { this.leafEdgeStarts = leafEdgeOffsets; this.leafEdgeCounts = leafEdgeCounts; + console.log("[renderer.init] leaf edge index built", { + leaf_index_ms: Math.round(performance.now() - edgeIndexStart), + leaves: leaves.length, + sorted_edge_indices_bytes: sortedEdgeIndices.byteLength, + }); // Upload sorted edges to GPU + const uploadEdgesStart = performance.now(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.linesIbo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, sortedEdgeIndices, gl.STATIC_DRAW); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); + console.log("[renderer.init] edge buffer uploaded", { + upload_ms: Math.round(performance.now() - uploadEdgesStart), + total_ms: Math.round(performance.now() - t0), + valid_edges: validEdges, + }); return performance.now() - t0; } diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 99f4f74..5af6984 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -4,16 +4,65 @@ interface ImportMetaEnv { readonly VITE_BACKEND_URL?: string; readonly VITE_COSMOS_ENABLE_SIMULATION?: string; readonly VITE_COSMOS_DEBUG_LAYOUT?: string; + readonly VITE_COSMOS_BACKGROUND_COLOR?: string; readonly VITE_COSMOS_SPACE_SIZE?: string; + readonly VITE_COSMOS_POINT_DEFAULT_COLOR?: string; + readonly VITE_COSMOS_POINT_GREYOUT_COLOR?: string; + readonly VITE_COSMOS_POINT_GREYOUT_OPACITY?: string; + readonly VITE_COSMOS_POINT_DEFAULT_SIZE?: string; + readonly VITE_COSMOS_POINT_OPACITY?: string; + readonly VITE_COSMOS_POINT_SIZE_SCALE?: string; + readonly VITE_COSMOS_HOVERED_POINT_CURSOR?: string; + readonly VITE_COSMOS_HOVERED_LINK_CURSOR?: string; + readonly VITE_COSMOS_RENDER_HOVERED_POINT_RING?: string; + readonly VITE_COSMOS_HOVERED_POINT_RING_COLOR?: string; + readonly VITE_COSMOS_FOCUSED_POINT_RING_COLOR?: string; + readonly VITE_COSMOS_RENDER_LINKS?: string; + readonly VITE_COSMOS_LINK_DEFAULT_COLOR?: string; + readonly VITE_COSMOS_LINK_OPACITY?: string; + readonly VITE_COSMOS_LINK_GREYOUT_OPACITY?: string; + readonly VITE_COSMOS_LINK_DEFAULT_WIDTH?: string; + readonly VITE_COSMOS_HOVERED_LINK_COLOR?: string; + readonly VITE_COSMOS_HOVERED_LINK_WIDTH_INCREASE?: string; + readonly VITE_COSMOS_LINK_WIDTH_SCALE?: string; + readonly VITE_COSMOS_SCALE_LINKS_ON_ZOOM?: string; readonly VITE_COSMOS_CURVED_LINKS?: string; + readonly VITE_COSMOS_CURVED_LINK_SEGMENTS?: string; + readonly VITE_COSMOS_CURVED_LINK_WEIGHT?: string; + readonly VITE_COSMOS_CURVED_LINK_CONTROL_POINT_DISTANCE?: string; + readonly VITE_COSMOS_LINK_DEFAULT_ARROWS?: string; + readonly VITE_COSMOS_LINK_ARROWS_SIZE_SCALE?: string; + readonly VITE_COSMOS_LINK_VISIBILITY_DISTANCE_RANGE?: string; + readonly VITE_COSMOS_LINK_VISIBILITY_MIN_TRANSPARENCY?: string; + readonly VITE_COSMOS_USE_CLASSIC_QUADTREE?: string; readonly VITE_COSMOS_FIT_VIEW_PADDING?: string; readonly VITE_COSMOS_SIMULATION_DECAY?: string; readonly VITE_COSMOS_SIMULATION_GRAVITY?: string; readonly VITE_COSMOS_SIMULATION_CENTER?: string; readonly VITE_COSMOS_SIMULATION_REPULSION?: string; + readonly VITE_COSMOS_SIMULATION_REPULSION_THETA?: string; + readonly VITE_COSMOS_SIMULATION_REPULSION_QUADTREE_LEVELS?: string; readonly VITE_COSMOS_SIMULATION_LINK_SPRING?: string; readonly VITE_COSMOS_SIMULATION_LINK_DISTANCE?: string; + readonly VITE_COSMOS_SIMULATION_LINK_DISTANCE_RANDOM_VARIATION_RANGE?: string; + readonly VITE_COSMOS_SIMULATION_REPULSION_FROM_MOUSE?: string; + readonly VITE_COSMOS_ENABLE_RIGHT_CLICK_REPULSION?: string; readonly VITE_COSMOS_SIMULATION_FRICTION?: string; + readonly VITE_COSMOS_SIMULATION_CLUSTER?: string; + readonly VITE_COSMOS_SHOW_FPS_MONITOR?: string; + readonly VITE_COSMOS_PIXEL_RATIO?: string; + readonly VITE_COSMOS_SCALE_POINTS_ON_ZOOM?: string; + readonly VITE_COSMOS_INITIAL_ZOOM_LEVEL?: string; + readonly VITE_COSMOS_ENABLE_ZOOM?: string; + readonly VITE_COSMOS_ENABLE_SIMULATION_DURING_ZOOM?: string; + readonly VITE_COSMOS_ENABLE_DRAG?: string; + readonly VITE_COSMOS_FIT_VIEW_ON_INIT?: string; + readonly VITE_COSMOS_FIT_VIEW_DELAY?: string; + readonly VITE_COSMOS_FIT_VIEW_DURATION?: string; + readonly VITE_COSMOS_RANDOM_SEED?: string; + readonly VITE_COSMOS_POINT_SAMPLING_DISTANCE?: string; + readonly VITE_COSMOS_RESCALE_POSITIONS?: string; + readonly VITE_COSMOS_ATTRIBUTION?: string; } interface ImportMeta {