Skip to content
Open
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
306 changes: 306 additions & 0 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

164 changes: 164 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# draw.io: Shapes, Connections, Waypoints and Edge Editing — Code Map

This README documents where in the draw.io (jgraph/drawio) codebase to find the logic that:

- Renders vertex shapes (squares, circles, custom shapes).
- Renders edges / connectors (lines, arrows, wire shapes).
- Computes anchor/connection points (perimeter math and connection constraints).
- Creates and stores breakpoints / waypoints during connection creation.
- Supports interactive editing: moving waypoints, moving the start/end terminals of edges.

Use this as a navigation / change guide when you want to inspect or modify behavior (visuals, snapping, attachment, editing) related to shapes and edges.

---

## High-level flow (how connection creation & editing works)

1. User starts creating a connection:
- `mxConnectionHandler` creates a preview shape and computes source/target perimeter points for the preview.
2. While previewing, intermediate points (waypoints) can be added; these are stored in the handler while creating the edge.
3. When the edge is inserted, the edge's `mxGeometry` is created with `relative = true` and the waypoints are stored in `geometry.points`.
4. After creation, `mxEdgeHandler` is used to edit existing edges:
- It creates draggable handles for waypoints and for the start/end terminals.
- Moving handles updates edge geometry and recomputes `state.absolutePoints`.
5. Rendering reads `state.absolutePoints` (or waypoints) and paints the polyline/connector using shape painters.

---

## Files of interest (what each implements + direct links)

- Shapes & waypoint visuals (many vertex shapes + edge-shape implementations)
- src/main/webapp/js/grapheditor/Shapes.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/js/grapheditor/Shapes.js
- Notes: registers shapes, implements `WaypointShape` (visual for breakpoints), `WireShape`, `LinkShape` and many vertex shape `paintVertexShape` / `redrawPath` functions.

- Base shape rendering used by vertex and edge shapes
- src/main/webapp/mxgraph/src/shape/mxShape.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/shape/mxShape.js
- Notes: decides whether to paint an edge (by checking `getWaypoints()` and `pts`) or a vertex; calls `paintEdgeShape` with the computed points.

- Cell renderer (shape creation + lifecycle)
- src/main/webapp/mxgraph/src/view/mxCellRenderer.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/view/mxCellRenderer.js
- Notes: creates shapes from states and manages DOM nodes.

- Perimeter math (anchor point calculation for shapes)
- src/main/webapp/mxgraph/src/view/mxPerimeter.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/view/mxPerimeter.js
- Notes: contains perimeter functions (Rectangle, Circle, Ellipse, Rhombus, Triangle, custom). Perimeter functions compute intersection on shape boundary given a `next` point (point along the edge).

- Graph view: calling perimeter functions and computing perimeter points
- src/main/webapp/mxgraph/src/view/mxGraphView.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/view/mxGraphView.js
- Notes: `mxGraphView.prototype.getPerimeterPoint` calls the perimeter function; also provides `getPerimeterBounds`.

- Connection constraint object (fixed anchor points)
- src/main/webapp/mxgraph/src/view/mxConnectionConstraint.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/view/mxConnectionConstraint.js
- Notes: `mxConnectionConstraint` stores a relative point and whether it should be projected to the perimeter.

- Graph-level helpers for connection points, style flags and connection constraint handling
- src/main/webapp/mxgraph/src/view/mxGraph.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/view/mxGraph.js
- Notes: `getConnectionPoint`, `getConnectionConstraint`, `setConnectionConstraint` and style flags for entry / exit perimeter (`STYLE_ENTRY_PERIMETER`, `STYLE_EXIT_PERIMETER`), `ENTRY_DX` / `ENTRY_DY`, `EXIT_DX` / `EXIT_DY`.

- Connection creation & preview (adding waypoints while creating an edge)
- src/main/webapp/mxgraph/src/handler/mxConnectionHandler.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/handler/mxConnectionHandler.js
- Notes:
- Creates preview `shape` (polyline) and updates `shape.points` as it moves.
- Has `this.waypoints` and pushes snapped `mxPoint` entries during connection creation: `this.waypoints.push(point)`.
- Uses `getSourcePerimeterPoint` / `getTargetPerimeterPoint` to compute terminal attach points for preview.

- Constraint UI (displaying fixed anchor points on hover)
- src/main/webapp/mxgraph/src/handler/mxConstraintHandler.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/handler/mxConstraintHandler.js
- Notes: shows small points/icons on vertices and snaps to them when creating connections.

- Edge editing (moving waypoints, moving start/end terminals)
- src/main/webapp/mxgraph/src/handler/mxEdgeHandler.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/handler/mxEdgeHandler.js
- Notes:
- Creates bend handles and virtual bends.
- Handles hit detection for bends and virtual bends.
- Handles `mouseDown`, `start`, `getPointForEvent`, snapping behavior, adding/removing points and moving handles.
- Uses `this.bends` and `this.virtualBends` arrays for current handles.

- Where edge waypoints are stored in the model
- Edge geometry: `mxGeometry.points` on the edge `mxCell` contains the waypoints (absolute or relative depending on geometry). Edge handlers and graph methods manage persisting these to the model.

---

## Key code spots to inspect / edit for common tasks

- Change how vertex shapes render (square/circle/custom):
- Edit the specific shape implementation in `Shapes.js` (e.g., `CubeShape`, `IsoRectangleShape`, `StateShape`, `Ellipse`/`Circle` implementations).
- If you need global behavior changes, inspect `mxShape` in `mxShape.js`.

- Change how anchor points are calculated (where edges attach on a vertex):
- Add/modify perimeter functions inside `mxPerimeter.js`. Add the new function to `mxPerimeter` and register via `mxStyleRegistry` if needed.
- Adjust how `mxGraphView.getPerimeterPoint` uses the perimeter (e.g., border handling, orthogonal behavior, or transform for rotation).

- Add or change fixed connection points (named connection points / explicit anchors):
- `mxConnectionConstraint` is the data object. See where constraints are read/written in `mxGraph.getConnectionConstraint` & `mxGraph.setConnectionConstraint`.
- Modify `mxConstraintHandler` to change UI for showing anchor points or snapping behavior.

- Change how waypoints are created when drawing a new edge:
- Edit `mxConnectionHandler`: it creates `this.waypoints` and pushes snapped points. You can modify snapping, quantization, or add alternate ways to create/remove waypoints here.

- Change how breakpoints (waypoints) are moved after creation:
- Edit `mxEdgeHandler`: the drag behavior (snap tolerance, handle shapes, movement constraints, whether terminals can be disconnected) is implemented here. `getPointForEvent` contains snap-to-terminal logic.

- Change how start/end terminals move around the object:
- This behavior uses `getConnectionPoint` + `getPerimeterPoint` to compute where the terminal should move to when dragged, and `mxEdgeHandler` orchestrates the drag. Look at `mxEdgeHandler.start` and the code changing the edge's terminal (reconnecting logic) and `mxConnectionHandler.updateEdgeState`.

---

## Example references (specific small code excerpts)
- Waypoint visual (paint): WaypointShape in Shapes.js — search for `WaypointShape.prototype.paintVertexShape` in `Shapes.js`.
Link: Shapes.js (see waypoint sections)
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/js/grapheditor/Shapes.js

- Where waypoints are pushed during connection creation:
- In `mxConnectionHandler` there is code that does:
```
if (this.waypoints == null) { this.waypoints = []; }
var point = new mxPoint(this.graph.snap(me.getGraphX() / scale) * scale,
this.graph.snap(me.getGraphY() / scale) * scale);
this.waypoints.push(point);
```
- Search for `this.waypoints.push` in `mxConnectionHandler.js`.
Link: mxConnectionHandler.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/handler/mxConnectionHandler.js

- Edge editing / bend handles: `mxEdgeHandler` manages detection and dragging of bend handles. Search for `getHandleForEvent`, `mouseDown`, `start`, and `getPointForEvent`.
Link: mxEdgeHandler.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/handler/mxEdgeHandler.js

- Perimeter intersection math is implemented in `mxPerimeter.js`. Look for `RectanglePerimeter`, `EllipsePerimeter`, `CenterPerimeter`, or other functions depending on the shape.
Link: mxPerimeter.js
https://github.com/jgraph/drawio/blob/34466eba2331b75cbf409b09240b01009cb4f600/src/main/webapp/mxgraph/src/view/mxPerimeter.js

---

## Practical next steps / suggestions

- To change visual appearance of waypoints:
- Edit `WaypointShape` in `Shapes.js` (change size, stroke, fill).
- To add a new anchor behavior (e.g., fixed named ports on a vertex):
- Add a new perimeter function or use `mxConnectionConstraint` with `perimeter=false` and compute the offset in `mxGraph.getConnectionPoint`.
- To customize snapping when moving bend handles:
- Edit `mxEdgeHandler.getPointForEvent` (it currently uses `getSnapToTerminalTolerance` and `snapToPoint`).
- To change how the start/end attachment moves around rotated shapes:
- Review `mxConnectionHandler.getSourcePerimeterPoint` and `getTargetPerimeterPoint` (they rotate/perimeter-project points).
- To persist custom data for attachments (e.g., named ports):
- Use edge style entries or store attributes in the cell (edge or vertex) and adjust `mxGraph.getConnectionPoint` and `mxConnectionHandler` to consult them.

---

## If you want, I can:
- Produce specific code snippets/patches for one of the tasks above (e.g., add a new perimeter function, change waypoint visuals, tweak snap tolerance).
- Create a short PR that implements a minimal, well-scoped change (please tell me which repo/branch to target).
- Walk through the exact lines to change for a chosen behavior and explain the implications (model vs view vs handler changes).

Tell me which specific change you want implemented and I will prepare a minimal concrete patch (or step-by-step edits) for that work.
139 changes: 124 additions & 15 deletions src/components/EditorCanvas/Canvas.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { useTranslation } from "react-i18next";
import { useEventListener } from "usehooks-ts";
import { areFieldsCompatible } from "../../utils/utils";
import { useLocation, useNavigate } from "react-router-dom";
import { getAllTablePerimeterPoints, findClosestPerimeterPoint, getFieldPerimeterPoints, calculateOrthogonalPath } from "../../utils/perimeterPoints";

export default function Canvas() {
const { t } = useTranslation();
Expand Down Expand Up @@ -111,6 +112,7 @@ export default function Canvas() {
useUndoRedo();

const { selectedElement, setSelectedElement } = useSelect();

const [dragging, setDragging] = useState({
element: ObjectType.NONE,
id: -1,
Expand All @@ -133,6 +135,8 @@ export default function Canvas() {
startY: 0,
endX: 0,
endY: 0,
startPoint: null, // Perimeter point info: {x, y, side, fieldIndex}
previewEndPoint: null, // Preview point for mouse hover
});

// Estado para conexiones de jerarquía
Expand Down Expand Up @@ -2250,11 +2254,40 @@ export default function Canvas() {
}

if (linking) {
setLinkingLine({
const updates = {
...linkingLine,
endX: pointer.spaces.diagram.x,
endY: pointer.spaces.diagram.y,
});
};

// If hovering over a table, calculate preview endpoint on perimeter
if (hoveredTable.tableId >= 0 && hoveredTable.fieldId >= 0) {
const targetTable = tables.find(t => t.id === hoveredTable.tableId);
if (targetTable) {
const hasColorStrip = settings.notation === Notation.DEFAULT;
const allPerimeterPoints = getAllTablePerimeterPoints(targetTable, hasColorStrip);

// Find closest perimeter point to mouse position
const closestPoint = findClosestPerimeterPoint(
allPerimeterPoints,
pointer.spaces.diagram.x,
pointer.spaces.diagram.y,
100 // threshold
);

if (closestPoint) {
updates.previewEndPoint = closestPoint;
updates.endTableId = hoveredTable.tableId;
} else {
updates.previewEndPoint = null;
}
}
} else {
updates.previewEndPoint = null;
updates.endTableId = -1;
}

setLinkingLine(updates);
} else if (resizing.element === ObjectType.TABLE && resizing.id >= 0) {
const table = tables.find((t) => t.id === resizing.id);
const newWidth = Math.max(-(table.x - pointer.spaces.diagram.x), 180);
Expand Down Expand Up @@ -2802,16 +2835,39 @@ export default function Canvas() {
}
setPanning((old) => ({ ...old, isPanning: false }));
setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });

// Calculate startPoint with perimeter information
const parentTable = tables.find((t) => t.id === fieldTableid);
let startPoint = null;

if (parentTable) {
const fieldIndex = parentTable.fields.findIndex(f => f.id === field.id);
const hasColorStrip = settings.notation === Notation.DEFAULT;

// Get perimeter points for this field
const fieldPerimeterPoints = getFieldPerimeterPoints(
parentTable,
fieldIndex,
parentTable.fields.length,
hasColorStrip
);

// Use the right side by default (common starting point)
startPoint = fieldPerimeterPoints.right;
}

setLinkingLine({
...linkingLine,
startTableId: fieldTableid,
startFieldId: field.id,
startX: pointer.spaces.diagram.x,
startY: pointer.spaces.diagram.y,
endX: pointer.spaces.diagram.x,
endY: pointer.spaces.diagram.y,
startX: startPoint ? startPoint.x : pointer.spaces.diagram.x,
startY: startPoint ? startPoint.y : pointer.spaces.diagram.y,
endX: startPoint ? startPoint.x : pointer.spaces.diagram.x,
endY: startPoint ? startPoint.y : pointer.spaces.diagram.y,
endTableId: -1,
endFieldId: -1,
startPoint: startPoint,
previewEndPoint: null,
});
setLinking(true);
};
Expand Down Expand Up @@ -2906,6 +2962,26 @@ export default function Canvas() {
(f) => f.id === linkingLine.startFieldId,
);
const relationshipName = `${parentTable.name}_${actualStartFieldId ? actualStartFieldId.name : "table"}`;

// Calculate perimeter points for start and end
const hasColorStrip = settings.notation === Notation.DEFAULT;
const startFieldIndex = parentTable.fields.findIndex(f => f.id === linkingLine.startFieldId);
const startPerimeterPoints = getFieldPerimeterPoints(
parentTable,
startFieldIndex,
parentTable.fields.length,
hasColorStrip
);

// Get all perimeter points for end table to find closest one
const endTablePerimeterPoints = getAllTablePerimeterPoints(childTable, hasColorStrip);
const closestEndPoint = findClosestPerimeterPoint(
endTablePerimeterPoints,
linkingLine.endX,
linkingLine.endY,
50 // threshold
);

// Use the updated childTable fields to create the new relationship
const newRelationship = {
startTableId: linkingLine.startTableId,
Expand All @@ -2918,6 +2994,10 @@ export default function Canvas() {
updateConstraint: Constraint.NONE,
deleteConstraint: Constraint.NONE,
name: relationshipName,
// Store perimeter connection points
startPoint: startPerimeterPoints.right, // Default to right side of start field
endPoint: closestEndPoint || endTablePerimeterPoints[0], // Use closest or first point
waypoints: [], // Initialize empty waypoints for orthogonal routing
};

delete newRelationship.startX;
Expand Down Expand Up @@ -3165,14 +3245,16 @@ export default function Canvas() {
}
return true;
})
.map((e, i) => (
<Relationship
key={e.id || i}
data={e}
onContextMenu={handleRelationshipContextMenu}
onConnectSubtypePoint={handleSubtypePointClick}
/>
))}
.map((e, i) => {
return (
<Relationship
key={e.id || i}
data={e}
onContextMenu={handleRelationshipContextMenu}
onConnectSubtypePoint={handleSubtypePointClick}
/>
);
})}
{tables.map((table) => {
const isMoving =
dragging.element === ObjectType.TABLE &&
Expand All @@ -3187,6 +3269,7 @@ export default function Canvas() {
setHoveredTable={setHoveredTable}
handleGripField={handleGripField}
setLinkingLine={setLinkingLine}
isLinking={linking}
onPointerDown={(e) =>
handlePointerDownOnElement(e, table.id, ObjectType.TABLE)
}
Expand All @@ -3209,7 +3292,33 @@ export default function Canvas() {
/>
)
}
{linking && (
{linking && linkingLine.startPoint && (
<path
d={(() => {
// If we have both start and preview end points, use orthogonal routing
if (linkingLine.previewEndPoint) {
const startTable = tables.find(t => t.id === linkingLine.startTableId);
const endTable = tables.find(t => t.id === linkingLine.endTableId);

if (startTable && endTable) {
return calculateOrthogonalPath(
linkingLine.startPoint,
linkingLine.previewEndPoint,
[]
);
}
}
// Fallback to simple line
return `M ${linkingLine.startX} ${linkingLine.startY} L ${linkingLine.endX} ${linkingLine.endY}`;
})()}
stroke="#3b82f6"
strokeDasharray="8,8"
strokeWidth="2"
fill="none"
className="pointer-events-none touch-none"
/>
)}
{linking && !linkingLine.startPoint && (
<path
d={`M ${linkingLine.startX} ${linkingLine.startY} L ${linkingLine.endX} ${linkingLine.endY}`}
stroke="red"
Expand Down
Loading