feat: project cover art with AI generation, branding rename, RBAC fix, computation graph
- Add DALL-E cover art generation for projects (Azure OpenAI + standard OpenAI)
- CoverArtSection component with generate/upload/remove/focus-point controls
- Client-side image compression (10MB input → WebP/JPEG, max 1920px)
- DALL-E settings in admin panel (deployment, endpoint, API key)
- MCP assistant tools for cover art (generate_project_cover, remove_project_cover)
- Rename "Planarchy" → "plANARCHY" across all UI-facing text (13 files)
- Fix hardcoded canEdit={true} on project detail page — now checks user role
- Computation graph visualization (2D/3D) for calculation rules
- OG image and OpenGraph metadata
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import * as THREE from "three";
|
||||
import type { PositionedNode } from "./graph-data";
|
||||
|
||||
// ─── Canvas-based node sprites ──────────────────────────────────────────────
|
||||
|
||||
const spriteCache = new Map<string, THREE.Sprite>();
|
||||
|
||||
/**
|
||||
* Creates a Three.js sprite for a graph node: colored circle with value label.
|
||||
*/
|
||||
export function createNodeSprite(node: PositionedNode): THREE.Sprite {
|
||||
const cacheKey = `${node.id}:${node.value}:${node.color}`;
|
||||
const cached = spriteCache.get(cacheKey);
|
||||
if (cached) return cached.clone();
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const size = 512;
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
const cx = size / 2;
|
||||
const cy = size / 2;
|
||||
const radius = size / 2 - 16;
|
||||
|
||||
// Outer glow ring
|
||||
const gradient = ctx.createRadialGradient(cx, cy, radius * 0.8, cx, cy, radius);
|
||||
gradient.addColorStop(0, node.color + "aa");
|
||||
gradient.addColorStop(1, node.color + "00");
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
|
||||
// Dark background circle for contrast
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius * 0.78, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "#0f172a";
|
||||
ctx.fill();
|
||||
|
||||
// Colored border ring
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius * 0.78, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = node.color;
|
||||
ctx.lineWidth = 8;
|
||||
ctx.stroke();
|
||||
|
||||
// Subtle inner fill
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius * 0.74, 0, Math.PI * 2);
|
||||
ctx.fillStyle = node.color + "20";
|
||||
ctx.fill();
|
||||
|
||||
// Label (top)
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.font = "bold 36px system-ui, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.shadowColor = "#000000";
|
||||
ctx.shadowBlur = 6;
|
||||
|
||||
const label = node.label.length > 16 ? node.label.slice(0, 14) + "..." : node.label;
|
||||
ctx.fillText(label, cx, cy - 28);
|
||||
|
||||
// Value (bottom) — brighter, larger
|
||||
ctx.fillStyle = node.color;
|
||||
ctx.font = "bold 44px system-ui, sans-serif";
|
||||
ctx.shadowBlur = 4;
|
||||
const valueStr = typeof node.value === "number" ? node.value.toFixed(1) : String(node.value);
|
||||
const displayValue = valueStr.length > 12 ? valueStr.slice(0, 10) + "..." : valueStr;
|
||||
ctx.fillText(displayValue, cx, cy + 24);
|
||||
|
||||
// Unit (small, below value)
|
||||
if (node.unit) {
|
||||
ctx.fillStyle = "#94a3b8";
|
||||
ctx.font = "24px system-ui, sans-serif";
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.fillText(node.unit, cx, cy + 60);
|
||||
}
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
const material = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
});
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(50, 50, 1);
|
||||
|
||||
spriteCache.set(cacheKey, sprite);
|
||||
return sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dimmed version of a node sprite (for non-highlighted nodes).
|
||||
*/
|
||||
export function createDimmedNodeSprite(node: PositionedNode): THREE.Sprite {
|
||||
const sprite = createNodeSprite({ ...node, color: "#4b5563" });
|
||||
sprite.material.opacity = 0.3;
|
||||
return sprite;
|
||||
}
|
||||
|
||||
// ─── Link particle rendering ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns a color for a link based on the source node's domain color.
|
||||
*/
|
||||
export function getLinkColor(
|
||||
link: { source: string | { color?: string }; weight: number },
|
||||
opacity = 0.4,
|
||||
): string {
|
||||
const sourceColor = typeof link.source === "object" && link.source.color
|
||||
? link.source.color
|
||||
: "#6b7280";
|
||||
// Convert hex to rgba
|
||||
const r = parseInt(sourceColor.slice(1, 3), 16);
|
||||
const g = parseInt(sourceColor.slice(3, 5), 16);
|
||||
const b = parseInt(sourceColor.slice(5, 7), 16);
|
||||
return `rgba(${r},${g},${b},${opacity})`;
|
||||
}
|
||||
Reference in New Issue
Block a user