Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 82 additions & 51 deletions .serena/project.yml
Original file line number Diff line number Diff line change
@@ -1,43 +1,6 @@
# the name by which the project can be referenced within Serena
# the name by which the project can be referenced within Serena/when chatting with the LLM.
project_name: "Arena3Dweb"


# list of languages for which language servers are started; choose from:
# al angular ansible bash clojure
# cpp cpp_ccls crystal csharp csharp_omnisharp
# dart elixir elm erlang fortran
# fsharp go groovy haskell haxe
# hlsl html java json julia
# kotlin lean4 lua luau markdown
# matlab msl nix ocaml pascal
# perl php php_phpactor powershell python
# python_jedi python_ty r rego ruby
# ruby_solargraph rust scala scss solidity
# svelte swift systemverilog terraform toml
# typescript typescript_vts vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
# R dropped: legacy code being deleted through the v3 migration, and its LSP
# (R `languageserver` package) isn't installed — its failure aborted the whole
# language-server manager. Active dev is TS (frontend) + Python (backend).
languages:
- typescript
- python

# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
Expand All @@ -59,23 +22,19 @@ ignore_all_files_in_gitignore: true

# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
ls_specific_settings: {}

# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
# Paths can be absolute or relative to the project root.
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
# symbols and references across package boundaries.
# Currently supported for: TypeScript.
# Example:
# additional_workspace_folders:
# - ../sibling-package
# - ../shared-lib
additional_workspace_folders: []

# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
# Example:
# ignored_paths:
# - "examples/**"
# - ".worktrees/**"
# - "**/bin/**"
# - "**/obj/**"
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []

Expand Down Expand Up @@ -135,3 +94,75 @@ read_only_memory_patterns: []
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []

# list of additional workspace folder paths for cross-package reference support.
# Paths can be absolute or relative to the project root.
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
# symbols and references across package boundaries, but these folders are not indexed by Serena,
# i.e. the respective symbols will not be found using Serena's symbol search tools.
# Example:
# additional_workspace_folders:
# - ../sibling-package
# - ../shared-lib
ls_additional_workspace_folders: []

# list of language servers to start when using the LSP backend; choose from:
# ada al angular ansible bash
# bsl clojure cpp cpp_ccls crystal
# csharp csharp_omnisharp cue dart elixir
# elm erlang fortran fsharp gdscript
# go groovy haskell haxe hlsl
# html java json julia kotlin
# latex lean4 lua luau markdown
# matlab msl nextflow nix ocaml
# pascal perl php php_phpactor php_phpantom
# powershell python python_basedpyright python_jedi python_pyrefly
# python_ty qml r rego ruby
# ruby_solargraph rust scala scss solidity
# svelte swift systemverilog terraform toml
# typescript typescript_vts vue yaml zig
# (This list may be outdated; generated with scripts/print_language_list.py;
# For the current list, see values of the LanguageServerId enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some language servers require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple language servers, the first language server that supports a given file will be used for that file.
# The first language server is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
language_servers:
- typescript
- python

# list of workspace folder paths (LSP backend only).
# These folders will be used to build up Serena's symbol index.
# Paths must be within the project root and should thus be relative to the project root.
# Furthermore, the paths should not be filtered by ignore settings.
# Default setting: The entire project root folder (".") is considered.
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
# ls_workspace_folders:
# - "./subproject1"
# - "./subproject2"
ls_workspace_folders:
- .

# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
# the command runs in the project root directory and is only executed if the project is trusted
# (see trusted_project_path_patterns in the global configuration).
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
# example: activation_command: "npx nx run-many -t build"
activation_command:

# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
# must be a positive number.
activation_command_timeout: 180.0
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@

- Edge weight can now be shown as **edge thickness**, not only opacity. The Edge Actions panel replaces the "Edge Opacity By Weight" checkbox with a "Show Edge Weight As" radio — Nothing / Opacity / Width / Both — plus intra- and inter-layer width sliders for whichever property weight isn't driving. Sessions carry the choice as the independent `edgeOpacityByWeight` and `edgeWidthByWeight` booleans; files written before this default to opacity, so they render unchanged. Thickness needed `Line2` (instanced quads) because WebGL renders every line primitive at exactly 1px regardless of `linewidth`.

### Changed

- Channel curvature sliders reach much further — intra-layer 10–60 (was 10–20), inter-layer 1–30 (was 1–10) — so the channels of a multi-channel edge can be pulled well apart instead of running nearly parallel. Defaults are unchanged, so existing sessions render exactly as before.

### Fixed

- Edges no longer glow. Thick edges cover a large share of the screen, and bloom on all of them was blinding against dark backgrounds. Bloom is now selective (three's off-screen bloom-composer pattern) and reads from node spheres alone, so nodes still glow, edges and layer planes stay crisp, and the extra pass costs a few spheres rather than a second full scene.
- Curved channel edges rendered as dotted lines with beads at the curve points once thickness landed. `LineMaterial`'s `worldUnits` mode assumes a perspective camera — its fragment shader traces a view ray from the camera origin and discards anything farther than half a width from the segment, which under this app's orthographic camera discards along the whole segment. Widths are now screen-space, sized against a shared resolution uniform kept on the frustum size (so the numbers still mean world units), retargeted on resize and for the PNG export.
- Node colors rendered washed out/dark in the 3D scene compared to the 2D navigator. The bloom composer was blitting linear color straight to the sRGB canvas (missing `OutputPass`), and the ambient light was left at the pre-r155 intensity that physical lighting divides by PI.

## [3.0.0] - 2026-07-27
Expand Down
19 changes: 15 additions & 4 deletions frontend/e2e/edge-width.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { expect, test } from '@playwright/test'
// background, mid-grey layer planes, and bright near-grey edges. Only the
// bright band counts — the planes' own pixel count *falls* as thicker edges
// cover them, so any metric including them cancels the effect out. Nodes are
// the saturated pixels and are excluded by the hue test. Retries because the
// render loop is FPS-limited and the headless GPU context can drop and
// restore, leaving most frames' buffers empty.
// the saturated pixels and are excluded by the hue test. Requires bloom off
// (loadExample): glow lifts the whole plane into the bright band, which swamps
// the edges' own pixels. Retries because the render loop is FPS-limited and
// the headless GPU context can drop and restore, leaving most frames' buffers
// empty.
async function edgeCoverage(page: import('@playwright/test').Page) {
for (let i = 0; i < 60; i++) {
const sample = await page.evaluate(() => {
Expand All @@ -34,7 +36,8 @@ async function edgeCoverage(page: import('@playwright/test').Page) {
const b = data[p + 2]
const hi = Math.max(r, gr, b)
const lo = Math.min(r, gr, b)
if (hi - lo >= 30) node++ // saturated: node spheres
if (hi - lo >= 30)
node++ // saturated: node spheres
else if (hi >= 128) edge++ // bright near-grey: edges
}
resolve({ edge, node })
Expand All @@ -55,6 +58,14 @@ async function loadExample(page: import('@playwright/test').Page) {
await page.getByRole('tab', { name: 'File' }).click()
await page.getByRole('button', { name: 'Load Example' }).click()
await expect(page.locator('#file_status')).toContainText('Loaded network')
// Bloom off so pixel counts measure geometry, not glow. Driven on the input
// itself rather than through its tab: uncheck() waits for visibility, and on
// a slow runner the Scene Actions pane isn't shown by the time we get here.
await page.locator('#toggleBloom').evaluate((el) => {
const box = el as HTMLInputElement
box.checked = false
box.dispatchEvent(new Event('change'))
})
await page.getByRole('tab', { name: 'Edge Actions' }).click()
}

Expand Down
9 changes: 6 additions & 3 deletions frontend/e2e/network-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ async function selectOption(
value: string
) {
await page.locator(selector).evaluate((el, v) => {
;(el as HTMLSelectElement).value = v as string
;(el as HTMLSelectElement).value = v
el.dispatchEvent(new Event('change', { bubbles: true }))
}, value)
}

type NodeSnapshot = { pos: [number, number, number]; cluster: string }

function readNodes() {
return async (): Promise<NodeSnapshot[]> => {
// Runs inside page.evaluate, which accepts a sync function just as happily.
return (): NodeSnapshot[] => {
const ctx = (
window as unknown as { __arena: { ctx: { nodeObjects: unknown[] } } }
).__arena.ctx
Expand Down Expand Up @@ -113,7 +114,9 @@ test('load example → layout → clustered layout → export', async ({ page })
// --- Export session ------------------------------------------------------
await page.getByRole('tab', { name: 'File' }).dispatchEvent('click')
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Save Session' }).dispatchEvent('click')
await page
.getByRole('button', { name: 'Save Session' })
.dispatchEvent('click')
const download = await downloadPromise
expect(download.suggestedFilename()).toMatch(/\.json$/)
})
11 changes: 11 additions & 0 deletions frontend/e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
// The specs live outside the app's tsconfig (which covers src/ only) and run
// in Node, not the browser bundle — hence node types and their own project.
// typescript-eslint resolves each file against its nearest tsconfig.json, so
// this file is what makes typed linting work in e2e/.
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["node"]
},
"include": ["."]
}
18 changes: 18 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"lint": "eslint src",
"format": "prettier --write src",
"lint": "eslint src e2e",
"format": "prettier --write src e2e",
"test": "vitest",
"test:e2e": "playwright test"
},
Expand All @@ -18,6 +18,7 @@
"devDependencies": {
"@playwright/test": "^1.61.1",
"@types/bootstrap": "^5.2.11",
"@types/node": "^26.2.0",
"@types/three": "^0.185.0",
"eslint": "^10.6.0",
"prettier": "^3.9.4",
Expand Down
4 changes: 4 additions & 0 deletions frontend/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export default defineConfig({
testDir: './e2e',
fullyParallel: false,
retries: process.env.CI ? 2 : 0,
// These drive a WebGL scene, and CI has no GPU — every test renders through
// software GL and lands in the 15-30s range, so the 30s default was passing
// on retries alone.
timeout: 90_000,
reporter: [['list']],
use: {
baseURL: 'http://localhost:5173',
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/actions/screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Port of v2 www/js/object_actions/screen.js. Shiny sync calls dropped.

import * as THREE from 'three'
import { ctx } from '../three'
import { ctx, edgeResolution } from '../three'
import {
onBackgroundColor,
renderFrame,
Expand Down Expand Up @@ -40,6 +40,9 @@ export function setWindowBounds(): void {
ctx.yBoundMax = window.innerHeight / 2
ctx.zBoundMin = -window.innerHeight / 2.5
ctx.zBoundMax = window.innerHeight / 2.5
// Every edge LineMaterial shares this uniform; keep it on the frustum size
// so screen-space linewidths keep reading as world units.
edgeResolution.set(window.innerWidth, window.innerHeight)
}

export function setCamera(): void {
Expand Down Expand Up @@ -127,7 +130,12 @@ export function exportSceneImage(): boolean {
})
renderer.setSize(Math.round(w * scale), Math.round(h * scale), false)
renderer.setClearColor(ctx.renderer.getClearColor(new THREE.Color()), 1)
// Edge linewidths are screen-space against the shared frustum-sized
// resolution; point it at the export frustum for this render so widths
// scale with the PNG, then hand it back to the live view.
edgeResolution.set(w, h)
renderer.render(ctx.scene.THREE_Object, camera)
edgeResolution.set(2 * ctx.xBoundMax, 2 * ctx.yBoundMax)

// Composite onto a 2D canvas so the DOM-overlay labels can be drawn on top.
const out = document.createElement('canvas')
Expand Down
Loading
Loading