Both canvases now share a canonical SysMLModel through React context.
Renames in the diagram inspector ripple to every chip in the narrative
referencing the same refId, and double-clicking a chip emits an update op
that re-renders the diagram block. Validation re-runs on every successful
apply. State is in-memory; M5.9 adds Postgres persistence.
apps/web/lib/sync (new)
- ops.ts: ModelOp alphabet (18 op kinds across block / property /
association / constraint / requirement / relation, add/update/remove for
each). tempId() helper + per-kind constructors.
- applyOps.ts: pure (model, ops) → { model, idMapping, errors, applied }
reducer. Atomic per batch. Cascades on remove-block (drops incident
associations + constraint applies-to + requirement satisfiers). tempId
resolution rewrites to canonical ids on duplicate-id collision.
- ModelStore.tsx: React provider exposing { model, apply, issues,
issuesByElement }. Validation memoized on every model change.
Editor refactor
- EditorShell wraps in ModelStoreProvider. Initial model derived from
fixture (+ optional ?break= corruptions). Validation now lives in the
store, not duplicated here.
- LeftRail consumes useModel(): Model section lists real blocks +
constraints (sorted by kind), Requirements section lists real
requirements with traced/untraced status from r.relations.
Diagram refactor (the tricky piece)
- React Flow now owns ephemeral state via useNodesState / useEdgesState.
Positions, drag-in-progress, selection are all RF-internal.
- Model → RF: a useEffect runs on model change, applies targeted setNodes
updates only for elements whose semantic data (label, kind, properties)
changed. Object identity preserved for unchanged nodes — fixes the
"re-render storm on drag" + RF measurement-cache loss.
- RF → Model: onNodesChange / onEdgesChange / onConnect / onDrop emit
ops via useApply(). Constraint-applies edges decompose into
updateConstraint ops. deleteKeyCode={[Backspace, Delete]}.
- onNodesChange now handles type:'remove' too (was missing — that's why
selecting a block + Del removed only the edges, leaving the block).
Chip refactor
- ChipView resolves displayed label from useModel() via refId lookup
(block / requirement / association / property). Double-click chip →
inline rename input → emit update-{block,requirement,association} op.
All other chips with the same refId update on the next render.
- Slash-menu inserted chips have refId=null and skip the rename
affordance (until M6 wires real model element resolution).
Removed obsolete components/diagram-canvas/fixtureToFlow.ts; replaced
with modelToFlow.ts. Bumped CSS for the chip-rename input.
62 lines
1.6 KiB
TypeScript
62 lines
1.6 KiB
TypeScript
// Custom edge that renders association / composition / constraint variants.
|
|
// - association: solid line with arrow
|
|
// - composition: solid line with diamond marker at the source side
|
|
// - constraint: dashed line, no arrow
|
|
|
|
"use client";
|
|
|
|
import { BaseEdge, EdgeLabelRenderer, getBezierPath, type EdgeProps } from "@xyflow/react";
|
|
import type { AssociationKind } from "../../../lib/sysml/model";
|
|
|
|
export interface SysmlEdgeData extends Record<string, unknown> {
|
|
label?: string;
|
|
kind: AssociationKind;
|
|
}
|
|
|
|
export function SysmlEdge(props: EdgeProps) {
|
|
const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data, markerEnd, markerStart } = props;
|
|
const d = (data ?? {}) as SysmlEdgeData;
|
|
const kind = d.kind ?? "association";
|
|
|
|
const [edgePath, labelX, labelY] = getBezierPath({
|
|
sourceX, sourceY,
|
|
sourcePosition,
|
|
targetX, targetY,
|
|
targetPosition,
|
|
curvature: 0.25,
|
|
});
|
|
|
|
const isConstraint = kind === "constraintApplies";
|
|
|
|
return (
|
|
<>
|
|
<BaseEdge
|
|
id={props.id}
|
|
path={edgePath}
|
|
style={{
|
|
stroke: "var(--edge)",
|
|
strokeWidth: 1,
|
|
strokeDasharray: isConstraint ? "4 4" : undefined,
|
|
opacity: 0.85,
|
|
}}
|
|
markerEnd={markerEnd}
|
|
markerStart={markerStart}
|
|
/>
|
|
|
|
{d.label && (
|
|
<EdgeLabelRenderer>
|
|
<div
|
|
className="sysml-edge-label"
|
|
style={{
|
|
position: "absolute",
|
|
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
|
}}
|
|
>
|
|
{d.label}
|
|
</div>
|
|
</EdgeLabelRenderer>
|
|
)}
|
|
</>
|
|
);
|
|
}
|