Skip to content

Improve Connector API: Function-Based + Connectors as Components #2

@gerchowl

Description

@gerchowl

Improve Connector API: Function-Based + Connectors as Components

Problem Statement

1. Poor Developer Experience with Dictionary-Based API

Currently, connectors are defined using plain dictionaries, which provides no IDE support or discoverability:

component(
  name: "server",
  interfaces: (
    right: ((name: "out", label: [Out], show-indicator: true))
    //      ^ No autocomplete, no type hints, no parameter discovery
  )
)

Issues:

  • ❌ No autocomplete for available options
  • ❌ No inline documentation
  • ❌ Easy to make typos (show-indicater vs show-indicator)
  • ❌ No validation until runtime
  • ❌ Users must constantly reference docs to remember options

2. Architectural Inconsistency

Connectors create anchors but don't follow the component/anchor pattern consistently:

  • Connectors are not first-class components
  • Labels are attached but not independently addressable
  • No consistent anchor hierarchy (e.g., connector.center, connector.label.west)

Proposed Solution

Phase 1: Function-Based Connector Definitions (Quick Win)

Add a connector() helper function that returns the dictionary but provides IDE support:

#let connector(
  name,
  label: none,
  show-indicator: false,
  type: "connector",  // "input", "output", "error", "bidirectional", "connector"
  pos: 0.5,
  size: none,
  label-pos: none,
  label-connection-margin: none
) = {
  let def = (name: name, pos: pos, show-indicator: show-indicator, type: type)
  if label != none { def.insert("label", label) }
  if size != none { def.insert("size", size) }
  if label-pos != none { def.insert("label-pos", label-pos) }
  if label-connection-margin != none { 
    def.insert("label-connection-margin", label-connection-margin) 
  }
  def
}

// Usage - now with autocomplete! 🎉
component(
  name: "server",
  interfaces: (
    right: (connector(name: "out", label: [Out], show-indicator: true, type: "output"))
  )
)

Benefits:

  • ✅ Immediate IDE autocomplete support
  • ✅ Inline parameter documentation
  • ✅ Validation in function signature
  • ✅ Backward compatible (still returns dictionary)
  • ✅ Easy to implement (~20 lines of code)

Phase 2: Connectors as Components (Architectural Refactor)

Treat connectors as first-class components with proper anchor hierarchies:

component(
  name: "server",
  connectors: (
    connector(
      name: "out",
      side: "right", 
      pos: 0.5,
      label: component(
        label: [Out], 
        pos: "outside"  // or "inside"
      )
    )
  )
)

Anchor Hierarchy:

// Connect to the connector indicator itself
connect("server.out.center", "db.in.center")

// Connect to the label's anchor
connect("server.out.label.west", "other.component.east")

// Connect to label's outer edge (current behavior)
connect("server.out", "db.in")  // shorthand for "server.out.label.west"

Benefits:

  • ✅ Consistent component/anchor mental model
  • ✅ Labels are proper components with all anchor capabilities
  • ✅ Flexible connection targeting (connector vs label vs specific anchor)
  • ✅ Composable: connectors can have complex labels with their own styling
  • ✅ Reuses existing component infrastructure (DRY principle)

Implementation Approach:

  1. Refactor connector rendering to use component() internally for labels
  2. Create standardized anchor naming: <connector-name>.center, <connector-name>.label.<anchor>
  3. Update connect-simple() to handle new anchor paths
  4. Keep backward compatibility with current dictionary format

Phase 3: Leverage CeTZ Primitives More Directly

Review Current Abstractions:

Many functions in connector.typ, component.typ, and builders.typ wrap CeTZ functionality. We should:

  1. Audit wrapper functions - Identify which add value vs which just forward to CeTZ
  2. Simplify where possible - Use CeTZ anchors, groups, and positioning directly
  3. Remove redundancy - Already removed legacy functions (see connector.typ:167-186)
  4. Document CeTZ patterns - Help users understand when to use CeTZ directly

Potential Simplifications:

  • Use CeTZ's group() consistently instead of custom positioning
  • Leverage CeTZ's anchor() system more directly
  • Use CeTZ's copy-anchors() pattern uniformly
  • Reduce custom coordinate transformation logic

Benefits:

  • ✅ Smaller codebase, easier maintenance
  • ✅ Better performance (less indirection)
  • ✅ Users can leverage CeTZ knowledge directly
  • ✅ Clearer separation: Blueprint = high-level patterns, CeTZ = low-level control

Implementation Plan

Milestone 1: Quick DX Win (1-2 hours)

  • Add connector() helper function to src/builders.typ or src/connector.typ
  • Export from src/lib.typ
  • Add JSDoc-style documentation
  • Update one example to demonstrate usage
  • Add to documentation

Milestone 2: Connectors as Components (1-2 days)

  • Design new anchor naming convention
  • Refactor connector rendering to use component() for labels
  • Update connection system to handle new anchor paths
  • Add backward compatibility layer
  • Update all tests
  • Migration guide for users

Milestone 3: CeTZ Simplification (Ongoing)

  • Audit all wrapper functions
  • Document which abstractions to keep and why
  • Create deprecation plan for redundant wrappers
  • Add "when to use CeTZ directly" guide
  • Refactor to use CeTZ primitives where appropriate

Breaking Changes

Phase 1: None (backward compatible)

The connector() function is purely additive.

Phase 2: Potential Breaking Changes

  • Anchor naming changes: component.connectorcomponent.connector.center
  • May need deprecation period with warnings
  • Migration script could be provided

Phase 3: TBD based on audit

  • Document breaking changes in CHANGELOG
  • Provide migration guide
  • Consider major version bump

Examples

Current (Dictionary)

#blueprint({
  component(
    name: "server",
    label: [Server],
    border-shape: "rect",
    border-stroke: 1pt + black,
    interfaces: (
      right: (
        (name: "http", pos: 0.3, label: [HTTP], type: "output", show-indicator: true),
        (name: "ws", pos: 0.7, label: [WS], type: "output", show-indicator: true)
      )
    )
  )
})

Phase 1: Function-Based (Backward Compatible)

#import "@preview/blueprint:0.x.0": blueprint, component, connector

#blueprint({
  component(
    name: "server",
    label: [Server],
    border-shape: "rect",
    border-stroke: 1pt + black,
    interfaces: (
      right: (
        connector(name: "http", pos: 0.3, label: [HTTP], type: "output", show-indicator: true),
        connector(name: "ws", pos: 0.7, label: [WS], type: "output", show-indicator: true)
      )
    )
  )
})

Phase 2: Connectors as Components (New Architecture)

#import "@preview/blueprint:0.x.0": blueprint, component, connector

#blueprint({
  component(
    name: "server",
    label: [Server],
    border-shape: "rect",
    border-stroke: 1pt + black,
    connectors: (
      connector(
        name: "http",
        side: "right",
        pos: 0.3,
        type: "output",
        label: component(
          label: [HTTP],
          border-shape: "rect",
          border-fill: green.lighten(90%),
          pos: "outside"
        )
      ),
      connector(
        name: "ws",
        side: "right",
        pos: 0.7,
        type: "output",
        show-indicator: true,
        label: [WS]  // Simple label (auto-wrapped in component)
      )
    )
  )
  
  component(
    name: "client",
    pos: relative-to("server", (3, 0)),
    label: [Client]
  )
  
  // New flexible anchor addressing
  connect("server.http.label.east", "client.west")  // Connect from label
  connect("server.ws.center", "client.west")        // Connect from indicator
  connect("server.http", "client.west")             // Shorthand (smart routing)
})

Related Issues

  • Connector edge detection improvements
  • Label positioning and collision avoidance
  • Standardized anchor naming conventions across all elements

Questions for Discussion

  1. Should Phase 1 be released before starting Phase 2?
  2. How long of a deprecation period for breaking changes?
  3. Should we provide a migration CLI tool or just documentation?
  4. Are there other CeTZ patterns we're reinventing unnecessarily?
  5. Should connector labels support all component features (nested connectors, etc.)?

References

  • Current connector implementation: src/connector.typ
  • Current interface handling: src/component.typ:430-600
  • Existing deprecation example: src/connector.typ:167-186
  • CeTZ documentation: https://github.com/cetz-package/cetz

Priority: Medium-High (DX improvement)
Complexity: Phase 1 = Low, Phase 2 = Medium, Phase 3 = Medium-High
Impact: High (affects all users defining connectors)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions