From 3299c8b28b0ccc2a340b351aa38d416006b02009 Mon Sep 17 00:00:00 2001 From: Alan Lail Date: Thu, 17 Sep 2026 11:53:17 -0400 Subject: [PATCH] Changes to speed up authoring experience --- apps/editor/src/ui/editor/EditorCanvas.tsx | 1 + apps/editor/src/ui/editor/layout/starLayout.ts | 17 ++++++++++++++++- apps/editor/src/ui/editor/layout/treeLayout.ts | 11 ++++++++++- .../ui/editor/state/helpers/nodeGeometry.ts | 18 ++++++++++++++++-- apps/opencase/src/interfaces/http/server.ts | 4 ++-- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/apps/editor/src/ui/editor/EditorCanvas.tsx b/apps/editor/src/ui/editor/EditorCanvas.tsx index 7ba3ed9..fc55f91 100644 --- a/apps/editor/src/ui/editor/EditorCanvas.tsx +++ b/apps/editor/src/ui/editor/EditorCanvas.tsx @@ -1331,6 +1331,7 @@ export default function EditorCanvas({ onBack, onSaveToServer, isPublishedToOpen onReconnectEnd={onReconnectEnd} connectOnClick={true} connectionMode={ConnectionMode.Loose} + onlyRenderVisibleElements defaultEdgeOptions={{ interactionWidth: 20, style: { strokeWidth: 1.5, stroke: '#94a3b8' }, diff --git a/apps/editor/src/ui/editor/layout/starLayout.ts b/apps/editor/src/ui/editor/layout/starLayout.ts index a99523a..19f68e7 100644 --- a/apps/editor/src/ui/editor/layout/starLayout.ts +++ b/apps/editor/src/ui/editor/layout/starLayout.ts @@ -47,14 +47,20 @@ export function computeStarLayout( // ── Count leaves for proportional angular allocation ───────────────── const leafCount = new Map() + const leafCountInProgress = new Set() // cycle guard: malformed/cyclic hierarchical data const countLeaves = (id: string): number => { + if (leafCount.has(id)) return leafCount.get(id)! + if (leafCountInProgress.has(id)) return 1 // re-entrant call means a cycle — treat as a leaf and bail + leafCountInProgress.add(id) const kids = childrenOf.get(id) ?? [] if (!kids.length) { leafCount.set(id, 1) + leafCountInProgress.delete(id) return 1 } const count = kids.reduce((sum, kid) => sum + countLeaves(kid), 0) leafCount.set(id, count) + leafCountInProgress.delete(id) return count } for (const id of startNodeIds) countLeaves(id) @@ -62,20 +68,28 @@ export function computeStarLayout( // ── Recursive tangential span ──────────────────────────────────────── const tangentialSpan = new Map() + const spanInProgress = new Set() // cycle guard: malformed/cyclic hierarchical data const calcSpan = (id: string): number => { if (tangentialSpan.has(id)) return tangentialSpan.get(id)! + if (spanInProgress.has(id)) return DEFAULT_NODE_WIDTH // re-entrant call means a cycle — bail instead of recursing forever + spanInProgress.add(id) const n = nodeById.get(id) - if (!n) return DEFAULT_NODE_WIDTH + if (!n) { + spanInProgress.delete(id) + return DEFAULT_NODE_WIDTH + } const { w } = getNodeSize(n) const kids = childrenOf.get(id) ?? [] if (!kids.length) { tangentialSpan.set(id, w) + spanInProgress.delete(id) return w } const kidsSpan = kids.reduce((sum, kid) => sum + calcSpan(kid), 0) + Math.max(0, kids.length - 1) * STAR_SIBLING_GAP const span = Math.max(w, kidsSpan) tangentialSpan.set(id, span) + spanInProgress.delete(id) return span } for (const id of startNodeIds) calcSpan(id) @@ -139,6 +153,7 @@ export function computeStarLayout( let kidAngleOffset = sectorStart for (const kid of kids) { + if (positions[kid]) continue // already positioned — guards against cycles / multi-parent DAGs const kidNode = nodeById.get(kid) if (!kidNode) continue const kidSize = getNodeSize(kidNode) diff --git a/apps/editor/src/ui/editor/layout/treeLayout.ts b/apps/editor/src/ui/editor/layout/treeLayout.ts index 56a94a6..a7f5e1e 100644 --- a/apps/editor/src/ui/editor/layout/treeLayout.ts +++ b/apps/editor/src/ui/editor/layout/treeLayout.ts @@ -35,19 +35,27 @@ export function computeTreeLayout( // Recursive sub-tree width const subtreeWidth = new Map() + const widthInProgress = new Set() // cycle guard: malformed/cyclic hierarchical data const calcWidth = (id: string): number => { if (subtreeWidth.has(id)) return subtreeWidth.get(id)! + if (widthInProgress.has(id)) return 0 // re-entrant call means a cycle — bail instead of recursing forever + widthInProgress.add(id) const n = nodeById.get(id) - if (!n) return 0 + if (!n) { + widthInProgress.delete(id) + return 0 + } const { w } = getNodeSize(n) const kids = childrenOf.get(id) ?? [] if (!kids.length) { subtreeWidth.set(id, w) + widthInProgress.delete(id) return w } const total = kids.map(calcWidth).reduce((a, b) => a + b, 0) + TREE_GAP_X * Math.max(0, kids.length - 1) const sw = Math.max(w, total) subtreeWidth.set(id, sw) + widthInProgress.delete(id) return sw } calcWidth(frameworkNode.id) @@ -55,6 +63,7 @@ export function computeTreeLayout( // Recursive positioning const positions: LayoutResult['positions'] = {} const layoutNode = (id: string, centerX: number, y: number) => { + if (positions[id]) return // already positioned — guards against cycles / multi-parent DAGs const n = nodeById.get(id) if (!n) return const { w, h } = getNodeSize(n) diff --git a/apps/editor/src/ui/editor/state/helpers/nodeGeometry.ts b/apps/editor/src/ui/editor/state/helpers/nodeGeometry.ts index 2e64511..c12290e 100644 --- a/apps/editor/src/ui/editor/state/helpers/nodeGeometry.ts +++ b/apps/editor/src/ui/editor/state/helpers/nodeGeometry.ts @@ -136,11 +136,22 @@ export function findNonOverlappingPosition( // ── Graph adjacency ──────────────────────────────────────────────────── -/** Build parent → children map and edge-lookup map from edges. */ +/** + * Build parent → children map and edge-lookup map from edges. + * + * Only hierarchical edges (isChildOf/isPartOf/framework-root, i.e. + * `data.isHierarchical`) are considered. Non-hierarchical associations + * (isRelatedTo, precedes, exactMatchOf, etc.) are NOT parent/child + * relationships — including them here can introduce cycles (e.g. a + * reciprocal isRelatedTo pair) into what tree/star layout assume is a DAG + * rooted at the framework node, which previously caused unbounded + * recursion ("Maximum Call Stack Size Exceeded") in calcWidth/layoutNode. + */ export function buildAdjacency(edges: CaseEditorEdge[]) { const childrenOf = new Map() const edgeBySourceTarget = new Map() for (const e of edges) { + if (!e.data?.isHierarchical) continue const kids = childrenOf.get(e.source) ?? [] kids.push(e.target) childrenOf.set(e.source, kids) @@ -154,7 +165,10 @@ export function sortChildrenRecursive( parentId: string, childrenOf: Map, edgeBySourceTarget: Map, + visited: Set = new Set(), ) { + if (visited.has(parentId)) return // cycle guard: malformed/cyclic hierarchical data + visited.add(parentId) const kids = childrenOf.get(parentId) if (!kids) return kids.sort((a, b) => { @@ -164,7 +178,7 @@ export function sortChildrenRecursive( const seqB = eB?.data?.cfAssociation?.sequenceNumber ?? eB?.data?.sequenceNumber ?? Infinity return seqA - seqB }) - for (const kid of kids) sortChildrenRecursive(kid, childrenOf, edgeBySourceTarget) + for (const kid of kids) sortChildrenRecursive(kid, childrenOf, edgeBySourceTarget, visited) } // ── Shared layout result type ────────────────────────────────────────── diff --git a/apps/opencase/src/interfaces/http/server.ts b/apps/opencase/src/interfaces/http/server.ts index 8e0628f..fbac892 100644 --- a/apps/opencase/src/interfaces/http/server.ts +++ b/apps/opencase/src/interfaces/http/server.ts @@ -18,8 +18,8 @@ export function createServer (container: Container): express.Express { allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'] })) - app.use(express.json({ limit: '10mb' })) - app.use(express.urlencoded({ extended: true, limit: '10mb' })) + app.use(express.json({ limit: '50mb' })) + app.use(express.urlencoded({ extended: true, limit: '50mb' })) // Service Discovery endpoints (no auth required - used for service discovery) app.get(