Skip to content

Commit 5905421

Browse files
committed
fix: graph view error.
1 parent 9a97437 commit 5905421

4 files changed

Lines changed: 304 additions & 19 deletions

File tree

backend/app/routers/pages.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,19 +209,40 @@ async def page_graph(user=Depends(get_current_user)):
209209
readable = await list_readable_page_ids(db, user)
210210
id_clause, id_params = _build_id_clause(readable)
211211
pages = await db.execute_fetchall(
212-
f"SELECT id, slug, title FROM pages WHERE deleted_at IS NULL AND {id_clause}",
212+
f"SELECT id, slug, title, parent_id FROM pages WHERE deleted_at IS NULL AND {id_clause}",
213213
id_params,
214214
)
215-
nodes = [{"id": p["id"], "slug": p["slug"], "title": p["title"]} for p in pages]
216-
217-
# Only show links between pages the user can see.
218215
visible_ids = {p["id"] for p in pages}
216+
217+
# A page is a "star" if any visible page lists it as parent. Stars get
218+
# special rendering on the frontend (galaxy mode).
219+
star_ids = {
220+
p["parent_id"] for p in pages if p["parent_id"] is not None and p["parent_id"] in visible_ids
221+
}
222+
nodes = [
223+
{
224+
"id": p["id"],
225+
"slug": p["slug"],
226+
"title": p["title"],
227+
"parent_id": p["parent_id"] if p["parent_id"] in visible_ids else None,
228+
"is_star": p["id"] in star_ids,
229+
}
230+
for p in pages
231+
]
232+
219233
backlinks = await db.execute_fetchall("SELECT source_page_id, target_page_id FROM backlinks")
220234
links = [
221-
{"source": b["source_page_id"], "target": b["target_page_id"]}
235+
{"source": b["source_page_id"], "target": b["target_page_id"], "type": "wikilink"}
222236
for b in backlinks
223237
if b["source_page_id"] in visible_ids and b["target_page_id"] in visible_ids
224238
]
239+
# Hierarchy edges (parent → child) so the graph reflects nesting, not just
240+
# explicit wikilinks. Frontend renders these differently from wikilinks.
241+
for p in pages:
242+
if p["parent_id"] is not None and p["parent_id"] in visible_ids:
243+
links.append(
244+
{"source": p["parent_id"], "target": p["id"], "type": "hierarchy"}
245+
)
225246

226247
return {"nodes": nodes, "links": links}
227248

frontend/src/lib/galaxy.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as THREE from 'three'
2+
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
3+
4+
// Distant starfield rendered as a single THREE.Points cloud. One draw call,
5+
// no per-frame work; we place it in the scene once and let it sit far enough
6+
// out that the force-graph nodes never collide with it.
7+
export function buildStarfield({ count = 4000, radius = 4500 } = {}) {
8+
const positions = new Float32Array(count * 3)
9+
const colors = new Float32Array(count * 3)
10+
for (let i = 0; i < count; i++) {
11+
// Uniform sample on a sphere, then push out to a thick shell so the
12+
// background reads as a dome instead of a flat plane.
13+
const u = Math.random()
14+
const v = Math.random()
15+
const theta = 2 * Math.PI * u
16+
const phi = Math.acos(2 * v - 1)
17+
const r = radius * (0.7 + Math.random() * 0.3)
18+
positions[i * 3 + 0] = r * Math.sin(phi) * Math.cos(theta)
19+
positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta)
20+
positions[i * 3 + 2] = r * Math.cos(phi)
21+
// Most stars near-white, a few tinted blue/orange — much more believable
22+
// than uniform white.
23+
const tint = Math.random()
24+
if (tint < 0.1) {
25+
colors[i * 3 + 0] = 1.0
26+
colors[i * 3 + 1] = 0.75
27+
colors[i * 3 + 2] = 0.55
28+
} else if (tint < 0.2) {
29+
colors[i * 3 + 0] = 0.7
30+
colors[i * 3 + 1] = 0.85
31+
colors[i * 3 + 2] = 1.0
32+
} else {
33+
const b = 0.85 + Math.random() * 0.15
34+
colors[i * 3 + 0] = b
35+
colors[i * 3 + 1] = b
36+
colors[i * 3 + 2] = b
37+
}
38+
}
39+
const geom = new THREE.BufferGeometry()
40+
geom.setAttribute('position', new THREE.BufferAttribute(positions, 3))
41+
geom.setAttribute('color', new THREE.BufferAttribute(colors, 3))
42+
const mat = new THREE.PointsMaterial({
43+
size: 2.2,
44+
sizeAttenuation: true,
45+
vertexColors: true,
46+
transparent: true,
47+
opacity: 0.9,
48+
depthWrite: false,
49+
})
50+
const points = new THREE.Points(geom, mat)
51+
// Don't let frustum culling drop the field when the camera moves; this
52+
// shell is large enough that we always want it drawn.
53+
points.frustumCulled = false
54+
return points
55+
}
56+
57+
export function disposeStarfield(points) {
58+
if (!points) return
59+
if (points.geometry) points.geometry.dispose()
60+
if (points.material) points.material.dispose()
61+
}
62+
63+
export function makeBloomPass(width, height) {
64+
// strength, radius, threshold. Threshold > 0 keeps planet surfaces from
65+
// bleeding; only stars / glowing edges bloom.
66+
return new UnrealBloomPass(new THREE.Vector2(width, height), 1.1, 0.6, 0.55)
67+
}

frontend/src/lib/planet.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,73 @@ function hslToColor(h, s, l) {
178178
return c
179179
}
180180

181+
const STAR_FRAGMENT = /* glsl */ `
182+
${SIMPLEX_GLSL}
183+
uniform float uTime;
184+
uniform vec3 uCore;
185+
uniform vec3 uFlare;
186+
varying vec3 vObjPos;
187+
varying vec3 vNormal;
188+
varying vec3 vViewDir;
189+
190+
void main() {
191+
vec3 unit = normalize(vObjPos);
192+
// Rolling plasma surface: two noise samples at different scales/speeds.
193+
float n1 = snoise(unit * 2.5 + vec3(uTime * 0.6));
194+
float n2 = snoise(unit * 5.0 - vec3(uTime * 0.9));
195+
float heat = 0.5 + 0.5 * (n1 * 0.6 + n2 * 0.4);
196+
vec3 col = mix(uCore, uFlare, smoothstep(0.3, 0.95, heat));
197+
// Strong fresnel halo so bloom can latch onto the rim.
198+
float rim = pow(1.0 - max(dot(normalize(vNormal), normalize(vViewDir)), 0.0), 2.0);
199+
col += uFlare * rim * 1.4;
200+
gl_FragColor = vec4(col, 1.0);
201+
}
202+
`
203+
204+
// A glowing star — used for pages that have children. Bright, emissive-feeling
205+
// material that interacts well with UnrealBloomPass. No light/shading: stars
206+
// emit, they don't receive.
207+
export function buildStarObject(node) {
208+
const params = planetParams(node)
209+
const group = new THREE.Group()
210+
// Stars read bigger than planets so they anchor the system visually.
211+
const baseR = 5.5 * params.radius
212+
213+
const geometry = new THREE.SphereGeometry(baseR, 48, 32)
214+
// Star colour: warm yellow-orange biased by the page's hue, so it still
215+
// varies per page but reads as a star, not a random planet.
216+
const coreHue = (params.hue * 0.2 + 0.08) % 1.0
217+
const flareHue = (coreHue + 0.04) % 1.0
218+
const material = new THREE.ShaderMaterial({
219+
vertexShader: VERTEX_SHADER,
220+
fragmentShader: STAR_FRAGMENT,
221+
uniforms: {
222+
uTime: { value: 0 },
223+
uCore: { value: hslToColor(coreHue, 0.85, 0.7) },
224+
uFlare: { value: hslToColor(flareHue, 1.0, 0.85) },
225+
},
226+
})
227+
const sphere = new THREE.Mesh(geometry, material)
228+
sphere.onBeforeRender = () => {
229+
material.uniforms.uTime.value = performance.now() * 0.001
230+
}
231+
group.add(sphere)
232+
233+
// Soft corona — additive sprite-like shell that fakes glow even without bloom,
234+
// and gives bloom something extra to latch onto when it's enabled.
235+
const coronaGeom = new THREE.SphereGeometry(baseR * 1.6, 32, 24)
236+
const coronaMat = new THREE.MeshBasicMaterial({
237+
color: hslToColor(flareHue, 1.0, 0.7),
238+
transparent: true,
239+
opacity: 0.18,
240+
blending: THREE.AdditiveBlending,
241+
depthWrite: false,
242+
})
243+
group.add(new THREE.Mesh(coronaGeom, coronaMat))
244+
245+
return group
246+
}
247+
181248
// Build a procedural planet mesh. Each node gets its own ShaderMaterial
182249
// instance (uniforms differ) but three.js dedups the program compile
183250
// because the shader source is identical across all planets.

0 commit comments

Comments
 (0)