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
1 change: 1 addition & 0 deletions apps/editor/src/ui/editor/EditorCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
17 changes: 16 additions & 1 deletion apps/editor/src/ui/editor/layout/starLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,35 +47,49 @@ export function computeStarLayout(

// ── Count leaves for proportional angular allocation ─────────────────
const leafCount = new Map<string, number>()
const leafCountInProgress = new Set<string>() // 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)
const totalLeaves = startNodeIds.reduce((sum, id) => sum + (leafCount.get(id) ?? 1), 0)

// ── Recursive tangential span ────────────────────────────────────────
const tangentialSpan = new Map<string, number>()
const spanInProgress = new Set<string>() // 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)
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 10 additions & 1 deletion apps/editor/src/ui/editor/layout/treeLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,35 @@ export function computeTreeLayout(

// Recursive sub-tree width
const subtreeWidth = new Map<string, number>()
const widthInProgress = new Set<string>() // 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)

// 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)
Expand Down
18 changes: 16 additions & 2 deletions apps/editor/src/ui/editor/state/helpers/nodeGeometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string[]>()
const edgeBySourceTarget = new Map<string, CaseEditorEdge>()
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)
Expand All @@ -154,7 +165,10 @@ export function sortChildrenRecursive(
parentId: string,
childrenOf: Map<string, string[]>,
edgeBySourceTarget: Map<string, CaseEditorEdge>,
visited: Set<string> = 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) => {
Expand All @@ -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 ──────────────────────────────────────────
Expand Down
4 changes: 2 additions & 2 deletions apps/opencase/src/interfaces/http/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading