Skip to content

Commit d0e8a0c

Browse files
refactor(library): harden routing integration
1 parent 69e497f commit d0e8a0c

38 files changed

Lines changed: 302 additions & 515 deletions

.changeset/orthogonal-edge-routing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@tumaet/apollon": minor
33
---
44

5-
Edges now choose sides, ports, and orthogonal routes together to avoid unnecessary corners, crossings, overlaps, and off-centre attachments. Connections spread into balanced grid-aligned gaps, shared pinned junctions keep stable trunks, and required-interface sockets remain concentric and visually separated from neighbouring markers. Endpoint and bend customization stays live without jumping on release, while edge-dense diagrams route in the background to keep dragging responsive. Results remain deterministic across reloads and collaborators, and existing v3 and older v4 diagrams continue to open and upgrade automatically.
5+
Create cleaner diagrams with automatic edges that balance connection points, reduce collisions with nodes and nearby edges, and stay responsive while you drag, without losing pinned endpoints, hand-placed bends, aligned component interfaces, or compatibility with older diagrams; embedding apps can also normalize diagrams in server-side code through the new `@tumaet/apollon/model` entry.

.github/workflows/pr-health-checks.yml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,18 +393,30 @@ jobs:
393393
run: pnpm --filter @tumaet/apollon run test
394394

395395
- name: Verify ESM exports-map resolution
396-
# The library is ESM-only. `import.meta.resolve` (sync, no side
397-
# effects) validates every subpath in the exports map — actually
398-
# evaluating the module would require jsdom.
396+
# The library is ESM-only. `import.meta.resolve` validates every
397+
# subpath in the exports map. The model-only entry is intentionally
398+
# DOM-free; the follow-up check evaluates its complete runtime graph.
399399
working-directory: standalone/server
400400
run: |
401401
node --input-type=module -e '
402402
const root = import.meta.resolve("@tumaet/apollon");
403403
const internals = import.meta.resolve("@tumaet/apollon/internals");
404404
const exportEntry = import.meta.resolve("@tumaet/apollon/export");
405+
const model = import.meta.resolve("@tumaet/apollon/model");
405406
const css = import.meta.resolve("@tumaet/apollon/style.css");
406-
console.log({ root, internals, exportEntry, css });
407+
const schema = import.meta.resolve("@tumaet/apollon/schema");
408+
const packageJson = import.meta.resolve("@tumaet/apollon/package.json");
409+
console.log({
410+
root,
411+
internals,
412+
exportEntry,
413+
model,
414+
css,
415+
schema,
416+
packageJson,
417+
});
407418
'
419+
node ../../scripts/check-model-entry.mjs
408420
409421
- name: Lint published package shape (publint)
410422
working-directory: library

docs/library/api/model-contract.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ import { importDiagram } from "@tumaet/apollon"
9797
editor.model = importDiagram(maybeV2OrV3Json) // → guaranteed v4
9898
```
9999

100+
Server code that only needs to normalize model JSON can use the DOM-free entry:
101+
102+
```ts
103+
import { importDiagram } from "@tumaet/apollon/model"
104+
105+
export const normalizeDiagram = (data: unknown) => importDiagram(data)
106+
```
107+
100108
The v2 / v3 detectors and converters live behind `@tumaet/apollon/internals`
101109
and are **not** part of the stability guarantee — only `importDiagram` is.
102110

docs/library/embedding/react.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,16 @@ export default function Page() {
244244

245245
Remix and Nuxt have equivalent client-only loading.
246246

247-
`@tumaet/apollon` is a client module in its entirety (`"use client"`), so its
248-
pure helpers (`importDiagram`, `createApollonTheme`, …) are client-only too —
249-
import them from client components, not Server Components.
247+
`@tumaet/apollon` is a client module in its entirety (`"use client"`), so
248+
import the editor and theme helpers from client components, not Server
249+
Components. Model migration is the exception: server code can import the
250+
DOM-free entry directly.
251+
252+
```ts
253+
import { importDiagram } from "@tumaet/apollon/model"
254+
255+
export const normalizeDiagram = (data: unknown) => importDiagram(data)
256+
```
250257

251258
Non-React hosts that want imperative control mount `ApollonEditor` directly —
252259
see [Vanilla JS / CDN](/library/embedding/vanilla).

docs/library/upgrading.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,14 @@ to React 19 before upgrading Apollon — on React 18 the install fails with an
6666

6767
## `"use client"` / RSC
6868

69-
The main entry is now a client module (`"use client"`) in its entirety, so its
70-
pure helpers (`importDiagram`, `createApollonTheme`, …) are client-only too.
71-
React Server Components must treat `@tumaet/apollon` as client-only: import it
72-
from client components, not Server Components. See
73-
[SSR](/library/embedding/react#ssr-nextjs-remix-nuxt-sveltekit).
69+
The main entry is now a client module (`"use client"`) in its entirety. React
70+
Server Components must therefore treat `@tumaet/apollon` as client-only and
71+
import it from client components, not Server Components.
72+
73+
Model migration is also available from the DOM-free
74+
`@tumaet/apollon/model` entry. Server code that only needs to normalize a
75+
diagram can import `importDiagram` from that subpath without loading the
76+
editor. See [SSR](/library/embedding/react#ssr-nextjs-remix-nuxt-sveltekit).
7477

7578
## Removed deep imports (CDN caveat)
7679

library/lib/components/toolbars/NodeToolbar.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,12 @@ export const NodeToolbar: FC<Props> = ({ elementId, showEdit = true }) => {
3030
align="end"
3131
offset={10}
3232
className="apollon-element-toolbar-host"
33-
style={{ pointerEvents: "none" }}
3433
>
3534
<ButtonGroup
3635
aria-label={t.selectionActions}
3736
orientation="vertical"
3837
className="apollon-element-toolbar nodrag nopan"
3938
onPointerDownCapture={(event) => event.stopPropagation()}
40-
onMouseDownCapture={(event) => event.stopPropagation()}
41-
onTouchStartCapture={(event) => event.stopPropagation()}
4239
>
4340
<IconButton
4441
ariaLabel={t.deleteElement}

library/lib/edges/GenericEdge.tsx

Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,6 @@ export const EdgeEndpointMarkers = ({
278278
onEndpointPointerDown,
279279
straight = false,
280280
bendHandles,
281-
sourcePinned = false,
282-
targetPinned = false,
283281
}: {
284282
sourcePoint: IPoint
285283
targetPoint: IPoint
@@ -297,11 +295,6 @@ export const EdgeEndpointMarkers = ({
297295
// The edge's bend handles, so a reconnect target can cap its outward reach at
298296
// this end's terminal bend handle instead of painting over it.
299297
bendHandles?: BendHandle[]
300-
// Whether each end is USER-PINNED (a custom anchor) rather than auto-anchored.
301-
// Kept as a prop/class for the reset affordance and tests; the grip itself looks the
302-
// same either way (a clean white handle), so nothing clutters the endpoint.
303-
sourcePinned?: boolean
304-
targetPinned?: boolean
305298
}) => {
306299
const screenScale = useHandleScreenScale()
307300

@@ -385,9 +378,7 @@ export const EdgeEndpointMarkers = ({
385378
{showEndpointGrips && (
386379
<>
387380
<rect
388-
className={`edge-circle edge-endpoint-grip edge-endpoint-grip--source${
389-
sourcePinned ? " edge-endpoint-grip--pinned" : ""
390-
}`}
381+
className="edge-circle edge-endpoint-grip edge-endpoint-grip--source"
391382
x={sourceGrip.x}
392383
y={sourceGrip.y}
393384
width={sourceGrip.width}
@@ -399,9 +390,7 @@ export const EdgeEndpointMarkers = ({
399390
pointerEvents="none"
400391
/>
401392
<rect
402-
className={`edge-circle edge-endpoint-grip edge-endpoint-grip--target${
403-
targetPinned ? " edge-endpoint-grip--pinned" : ""
404-
}`}
393+
className="edge-circle edge-endpoint-grip edge-endpoint-grip--target"
405394
x={targetGrip.x}
406395
y={targetGrip.y}
407396
width={targetGrip.width}
@@ -568,19 +557,6 @@ export const StepEdgeBody = ({
568557
) => void
569558
children?: ReactNode
570559
}) => {
571-
// Which ends the user has PINNED (a custom anchor) vs left to the router. Two
572-
// boolean selectors (not one object) so equality stays primitive and the edge
573-
// only re-renders when a pin actually flips. Drives the filled "anchored" grip.
574-
const sourcePinned = useDiagramStore((state) =>
575-
isFreeformEdgeAnchor(
576-
state.edges.find((edge) => edge.id === id)?.data?.sourceAnchor
577-
)
578-
)
579-
const targetPinned = useDiagramStore((state) =>
580-
isFreeformEdgeAnchor(
581-
state.edges.find((edge) => edge.id === id)?.data?.targetAnchor
582-
)
583-
)
584560
return (
585561
<g className="edge-container">
586562
<BaseEdge
@@ -656,8 +632,6 @@ export const StepEdgeBody = ({
656632
canEditEndpoint={canEditEndpoint}
657633
onEndpointPointerDown={handleEndpointPointerDown}
658634
bendHandles={bendHandles}
659-
sourcePinned={sourcePinned}
660-
targetPinned={targetPinned}
661635
/>
662636

663637
{children}
@@ -667,6 +641,7 @@ export const StepEdgeBody = ({
667641

668642
export const CommonEdgeElements = ({
669643
id,
644+
data,
670645
pathMiddlePosition,
671646
toolbarPosition,
672647
isDiagramModifiable,
@@ -676,6 +651,7 @@ export const CommonEdgeElements = ({
676651
type,
677652
}: {
678653
id: string
654+
data: BaseEdgeProps["data"]
679655
pathMiddlePosition: IPoint
680656
toolbarPosition?: IPoint
681657
isDiagramModifiable: boolean
@@ -691,15 +667,12 @@ export const CommonEdgeElements = ({
691667
const [anchorEl, anchorRef] = usePopoverAnchor<HTMLDivElement>()
692668

693669
const setEdges = useDiagramStore((state) => state.setEdges)
694-
const hasManualRoute = useDiagramStore((state) => {
695-
const data = state.edges.find((edge) => edge.id === id)?.data
696-
const points = data?.points
697-
const hasManualPoints = Array.isArray(points) && points.length > 0
698-
const hasPinnedAnchor =
699-
isFreeformEdgeAnchor(data?.sourceAnchor) ||
700-
isFreeformEdgeAnchor(data?.targetAnchor)
701-
return hasManualPoints || hasPinnedAnchor
702-
})
670+
const points = data?.points
671+
const hasManualPoints = Array.isArray(points) && points.length > 0
672+
const hasPinnedAnchor =
673+
isFreeformEdgeAnchor(data?.sourceAnchor) ||
674+
isFreeformEdgeAnchor(data?.targetAnchor)
675+
const hasManualRoute = hasManualPoints || hasPinnedAnchor
703676

704677
const handleResetRouting = useCallback(() => {
705678
setEdges((edges) =>

library/lib/edges/edgeTypes/ActivityDiagramEdge.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export const ActivityDiagramEdge = ({
120120

121121
<CommonEdgeElements
122122
id={id}
123+
data={data}
123124
pathMiddlePosition={edgeData.pathMiddlePosition}
124125
toolbarPosition={edgeData.toolbarPosition}
125126
isDiagramModifiable={isDiagramModifiable}

library/lib/edges/edgeTypes/BPMNDiagramEdge.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export const BPMNDiagramEdge = ({
128128

129129
<CommonEdgeElements
130130
id={id}
131+
data={data}
131132
pathMiddlePosition={edgeData.pathMiddlePosition}
132133
toolbarPosition={edgeData.toolbarPosition}
133134
isDiagramModifiable={isDiagramModifiable}

library/lib/edges/edgeTypes/ClassDiagramEdge.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export const ClassDiagramEdge = ({
132132

133133
<CommonEdgeElements
134134
id={id}
135+
data={data}
135136
pathMiddlePosition={edgeData.pathMiddlePosition}
136137
toolbarPosition={edgeData.toolbarPosition}
137138
isDiagramModifiable={isDiagramModifiable}

0 commit comments

Comments
 (0)