-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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-indicatervsshow-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:
- Refactor connector rendering to use
component()internally for labels - Create standardized anchor naming:
<connector-name>.center,<connector-name>.label.<anchor> - Update
connect-simple()to handle new anchor paths - 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:
- Audit wrapper functions - Identify which add value vs which just forward to CeTZ
- Simplify where possible - Use CeTZ anchors, groups, and positioning directly
- Remove redundancy - Already removed legacy functions (see
connector.typ:167-186) - 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 tosrc/builders.typorsrc/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.connector→component.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
- Should Phase 1 be released before starting Phase 2?
- How long of a deprecation period for breaking changes?
- Should we provide a migration CLI tool or just documentation?
- Are there other CeTZ patterns we're reinventing unnecessarily?
- 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)