add RoutingPlugin interface that plugins can implement for cross-graph edge routing
This commit is contained in:
parent
14db47457c
commit
0bf10f1634
7 changed files with 119 additions and 26 deletions
|
|
@ -368,6 +368,46 @@ func LayoutResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RouterResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin) func(engine string) (d2graph.RouteEdges, error) {
|
||||||
|
cached := make(map[string]d2graph.RouteEdges)
|
||||||
|
return func(engine string) (d2graph.RouteEdges, error) {
|
||||||
|
if c, ok := cached[engine]; ok {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin, err := d2plugin.FindPlugin(ctx, plugins, engine)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, exec.ErrNotFound) {
|
||||||
|
return nil, layoutNotFound(ctx, plugins, engine)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginInfo, err := plugin.Info(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hasRouter := false
|
||||||
|
for _, feat := range pluginInfo.Features {
|
||||||
|
if feat == d2plugin.ROUTES_EDGES {
|
||||||
|
hasRouter = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasRouter {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
routingPlugin, ok := plugin.(d2plugin.RoutingPlugin)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("plugin has routing feature but does not implement RoutingPlugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
routeEdges := d2graph.RouteEdges(routingPlugin.RouteEdges)
|
||||||
|
cached[engine] = routeEdges
|
||||||
|
return routeEdges, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath, boardPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
|
func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath, boardPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
input, err := ms.ReadPath(inputPath)
|
input, err := ms.ReadPath(inputPath)
|
||||||
|
|
@ -386,6 +426,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
|
||||||
InputPath: inputPath,
|
InputPath: inputPath,
|
||||||
LayoutResolver: LayoutResolver(ctx, ms, plugins),
|
LayoutResolver: LayoutResolver(ctx, ms, plugins),
|
||||||
Layout: layout,
|
Layout: layout,
|
||||||
|
RouterResolver: RouterResolver(ctx, ms, plugins),
|
||||||
FS: fs,
|
FS: fs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ func run(t *testing.T, tc testCase) {
|
||||||
assert.JSON(t, nil, err)
|
assert.JSON(t, nil, err)
|
||||||
|
|
||||||
graphInfo := d2layouts.NestedGraphInfo(g.Root)
|
graphInfo := d2layouts.NestedGraphInfo(g.Root)
|
||||||
err = d2layouts.LayoutNested(ctx, g, graphInfo, d2dagrelayout.DefaultLayout)
|
err = d2layouts.LayoutNested(ctx, g, graphInfo, d2dagrelayout.DefaultLayout, d2layouts.DefaultRouter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ func (g *Graph) RootBoard() *Graph {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LayoutGraph func(context.Context, *Graph) error
|
type LayoutGraph func(context.Context, *Graph) error
|
||||||
|
type RouteEdges func(context.Context, *Graph, []*Edge) error
|
||||||
|
|
||||||
// TODO consider having different Scalar types
|
// TODO consider having different Scalar types
|
||||||
// Right now we'll hold any types in Value and just convert, e.g. floats
|
// Right now we'll hold any types in Value and just convert, e.g. floats
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ func SaveOrder(g *d2graph.Graph) (restoreOrder func()) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, coreLayout d2graph.LayoutGraph) error {
|
func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, coreLayout d2graph.LayoutGraph, edgeRouter d2graph.RouteEdges) error {
|
||||||
g.Root.Box = &geo.Box{}
|
g.Root.Box = &geo.Box{}
|
||||||
|
|
||||||
// Before we can layout these nodes, we need to handle all nested diagrams first.
|
// Before we can layout these nodes, we need to handle all nested diagrams first.
|
||||||
|
|
@ -118,7 +118,7 @@ func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, co
|
||||||
|
|
||||||
// Then we layout curr as a nested graph and re-inject it
|
// Then we layout curr as a nested graph and re-inject it
|
||||||
id := curr.AbsID()
|
id := curr.AbsID()
|
||||||
err := LayoutNested(ctx, nestedGraph, GraphInfo{}, coreLayout)
|
err := LayoutNested(ctx, nestedGraph, GraphInfo{}, coreLayout, edgeRouter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +209,7 @@ func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, co
|
||||||
curr.NearKey = nil
|
curr.NearKey = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := LayoutNested(ctx, nestedGraph, nestedInfo, coreLayout)
|
err := LayoutNested(ctx, nestedGraph, nestedInfo, coreLayout, edgeRouter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -291,36 +291,61 @@ func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, co
|
||||||
PositionNested(obj, nestedGraph)
|
PositionNested(obj, nestedGraph)
|
||||||
}
|
}
|
||||||
|
|
||||||
// update map with injected objects
|
if len(extractedEdges) > 0 {
|
||||||
for _, o := range g.Objects {
|
// update map with injected objects
|
||||||
idToObj[o.AbsID()] = o
|
for _, o := range g.Objects {
|
||||||
|
idToObj[o.AbsID()] = o
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore cross-graph edges and route them
|
||||||
|
g.Edges = append(g.Edges, extractedEdges...)
|
||||||
|
for _, e := range extractedEdges {
|
||||||
|
// update object references
|
||||||
|
src, exists := idToObj[e.Src.AbsID()]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("could not find object %#v after layout", e.Src.AbsID())
|
||||||
|
}
|
||||||
|
e.Src = src
|
||||||
|
dst, exists := idToObj[e.Dst.AbsID()]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("could not find object %#v after layout", e.Dst.AbsID())
|
||||||
|
}
|
||||||
|
e.Dst = dst
|
||||||
|
}
|
||||||
|
|
||||||
|
err = edgeRouter(ctx, g, extractedEdges)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// need to update pointers if plugin performs edge routing
|
||||||
|
for _, e := range extractedEdges {
|
||||||
|
src, exists := idToObj[e.Src.AbsID()]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("could not find object %#v after routing", e.Src.AbsID())
|
||||||
|
}
|
||||||
|
e.Src = src
|
||||||
|
dst, exists := idToObj[e.Dst.AbsID()]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("could not find object %#v after routing", e.Dst.AbsID())
|
||||||
|
}
|
||||||
|
e.Dst = dst
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore cross-graph edges and route them
|
log.Debug(ctx, "done", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
|
||||||
g.Edges = append(g.Edges, extractedEdges...)
|
return err
|
||||||
for _, e := range extractedEdges {
|
}
|
||||||
// update object references
|
|
||||||
src, exists := idToObj[e.Src.AbsID()]
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("could not find object %#v after layout", e.Src.AbsID())
|
|
||||||
}
|
|
||||||
e.Src = src
|
|
||||||
dst, exists := idToObj[e.Dst.AbsID()]
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("could not find object %#v after layout", e.Dst.AbsID())
|
|
||||||
}
|
|
||||||
e.Dst = dst
|
|
||||||
|
|
||||||
// simple straight line edge routing when going across graphs
|
func DefaultRouter(ctx context.Context, graph *d2graph.Graph, edges []*d2graph.Edge) error {
|
||||||
|
for _, e := range edges {
|
||||||
|
// TODO replace simple straight line edge routing
|
||||||
e.Route = []*geo.Point{e.Src.Center(), e.Dst.Center()}
|
e.Route = []*geo.Point{e.Src.Center(), e.Dst.Center()}
|
||||||
e.TraceToShape(e.Route, 0, 1)
|
e.TraceToShape(e.Route, 0, 1)
|
||||||
if e.Label.Value != "" {
|
if e.Label.Value != "" {
|
||||||
e.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
|
e.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
log.Debug(ctx, "done", slog.F("rootlevel", g.RootLevel), slog.F("shapes", g.PrintString()))
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NestedGraphInfo(obj *d2graph.Object) (gi GraphInfo) {
|
func NestedGraphInfo(obj *d2graph.Object) (gi GraphInfo) {
|
||||||
|
|
|
||||||
20
d2lib/d2.go
20
d2lib/d2.go
|
|
@ -25,6 +25,7 @@ type CompileOptions struct {
|
||||||
FS fs.FS
|
FS fs.FS
|
||||||
MeasuredTexts []*d2target.MText
|
MeasuredTexts []*d2target.MText
|
||||||
Ruler *textmeasure.Ruler
|
Ruler *textmeasure.Ruler
|
||||||
|
RouterResolver func(engine string) (d2graph.RouteEdges, error)
|
||||||
LayoutResolver func(engine string) (d2graph.LayoutGraph, error)
|
LayoutResolver func(engine string) (d2graph.LayoutGraph, error)
|
||||||
|
|
||||||
Layout *string
|
Layout *string
|
||||||
|
|
@ -81,9 +82,13 @@ func compile(ctx context.Context, g *d2graph.Graph, compileOpts *CompileOptions,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
edgeRouter, err := getEdgeRouter(compileOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
graphInfo := d2layouts.NestedGraphInfo(g.Root)
|
graphInfo := d2layouts.NestedGraphInfo(g.Root)
|
||||||
err = d2layouts.LayoutNested(ctx, g, graphInfo, coreLayout)
|
err = d2layouts.LayoutNested(ctx, g, graphInfo, coreLayout, edgeRouter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -131,6 +136,19 @@ func getLayout(opts *CompileOptions) (d2graph.LayoutGraph, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getEdgeRouter(opts *CompileOptions) (d2graph.RouteEdges, error) {
|
||||||
|
if opts.Layout != nil && opts.RouterResolver != nil {
|
||||||
|
router, err := opts.RouterResolver(*opts.Layout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if router != nil {
|
||||||
|
return router, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d2layouts.DefaultRouter, nil
|
||||||
|
}
|
||||||
|
|
||||||
// applyConfigs applies the configs read from D2 and applies it to passed in opts
|
// applyConfigs applies the configs read from D2 and applies it to passed in opts
|
||||||
// It will only write to opt fields that are nil, as passed-in opts have precedence
|
// It will only write to opt fields that are nil, as passed-in opts have precedence
|
||||||
func applyConfigs(config *d2target.Config, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) {
|
func applyConfigs(config *d2target.Config, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,11 @@ type Plugin interface {
|
||||||
PostProcess(context.Context, []byte) ([]byte, error)
|
PostProcess(context.Context, []byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RoutingPlugin interface {
|
||||||
|
// RouteEdges runs the plugin's edge routing algorithm for the given edges in the input graph
|
||||||
|
RouteEdges(context.Context, *d2graph.Graph, []*d2graph.Edge) error
|
||||||
|
}
|
||||||
|
|
||||||
// PluginInfo is the current info information of a plugin.
|
// PluginInfo is the current info information of a plugin.
|
||||||
// note: The two fields Type and Path are not set by the plugin
|
// note: The two fields Type and Path are not set by the plugin
|
||||||
// itself but only in ListPlugins.
|
// itself but only in ListPlugins.
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ const TOP_LEFT PluginFeature = "top_left"
|
||||||
// When this is true, containers can have connections to descendants
|
// When this is true, containers can have connections to descendants
|
||||||
const DESCENDANT_EDGES PluginFeature = "descendant_edges"
|
const DESCENDANT_EDGES PluginFeature = "descendant_edges"
|
||||||
|
|
||||||
|
// When this is true, the plugin also implements RoutingPlugin interface to route edges
|
||||||
|
const ROUTES_EDGES PluginFeature = "routes_edges"
|
||||||
|
|
||||||
func FeatureSupportCheck(info *PluginInfo, g *d2graph.Graph) error {
|
func FeatureSupportCheck(info *PluginInfo, g *d2graph.Graph) error {
|
||||||
// Older version of plugin. Skip checking.
|
// Older version of plugin. Skip checking.
|
||||||
if info.Features == nil {
|
if info.Features == nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue