///|
/// One final visible connector and its untouched original route. Identities refer
/// to source connections, message indices or hierarchy/grammar occurrences.
pub struct ResolvedConnector {
id : String
source : String
target : String
route : ConnectorPath
path : ConnectorPath
mask : String?
} derive(Eq, Debug)
///|
pub struct ConnectorGeometry {
paths : ReadOnlyArray[ResolvedConnector]
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
} derive(Eq, Debug)
///|
/// A protected outline or label rectangle. `owner` can identify an endpoint
/// whose attachment is intentional. Use an empty owner for unrelated content.
pub(all) struct ConnectorObstacle {
owner : String
x : Double
y : Double
width : Double
height : Double
} derive(Eq, Debug)
///|
/// Inspect a rendered artifact. This accepts only this renderer's generated SVG,
/// not general XML. Original routes and final geometry are immutable values.
pub fn inspect_connectors(
canonical_svg : String,
) -> ReadOnlyArray[ResolvedConnector] {
let paths = []
for line in canonical_svg.split("\n") {
let tag = line.trim_start().to_owned()
if !tag.has_prefix(" ConnectorGeometry {
let result = self.render(diagram)
{
paths: result.output.map(inspect_connectors).unwrap_or([]),
diagnostics: result.diagnostics,
}
}
///|
fn cintersects(
a : ConnectorPoint,
b : ConnectorPoint,
box : ConnectorObstacle,
) -> Bool {
let dx = b.x - a.x
let dy = b.y - a.y
let mut lo = 0.0
let mut hi = 1.0
for
(p, q) in [
(-dx, a.x - box.x),
(dx, box.x + box.width - a.x),
(-dy, a.y - box.y),
(dy, box.y + box.height - a.y),
] {
if p.abs() < 0.0000001 {
if q <= 0.001 {
return false
}
} else {
let t = q / p
if p < 0.0 {
lo = lo.max(t)
} else {
hi = hi.min(t)
}
if lo >= hi {
return false
}
}
}
hi > 0.001 && lo < 0.999
}
///|
/// Check the actual flattened curves, including every bridge. Underpass masks
/// exempt no shape or label. Intentional endpoint attachment is owner-scoped.
/// Work exhaustion is an error and never a successful incomplete check.
pub fn check_connectors(
paths : ArrayView[ResolvedConnector],
obstacles : ArrayView[ConnectorObstacle],
width~ : Double,
height~ : Double,
limits~ : ConnectorLimits = ConnectorLimits::new(),
) -> ReadOnlyArray[@diagrams.Diagnostic] {
let diagnostics = []
let work : ConnectorWork = {
options: ConnectorOptions::new(limits~),
diagnostics,
candidates: 0,
subdivisions: 0,
segments: 0,
crossings: 0,
failed: false,
}
for connector in paths {
let flat = cflatten(connector.path, work)
for segment in flat {
if !work.charge(0) {
return ReadOnlyArray::from_array(diagnostics)
}
if [segment.a, segment.b]
.iter()
.any(p => p.x < 0.0 || p.y < 0.0 || p.x > width || p.y > height) {
diagnostics.push(
@diagrams.Diagnostic::error(
"connector/routing-conflict",
"Connector outside viewport: " + connector.id,
),
)
break
}
for obstacle in obstacles {
if !work.charge(0) {
return ReadOnlyArray::from_array(diagnostics)
}
if obstacle.owner != "" &&
(
obstacle.owner == connector.source ||
obstacle.owner == connector.target
) {
continue
}
if cintersects(segment.a, segment.b, obstacle) {
diagnostics.push(
@diagrams.Diagnostic::error(
"connector/routing-conflict",
"Connector intersects protected content: " +
connector.id +
" / " +
obstacle.owner,
),
)
break
}
}
}
}
ReadOnlyArray::from_array(diagnostics)
}
///|
pub extend ResolvedConnector with Eq::{equal, not_equal}
///|
pub extend ResolvedConnector with @debug.Debug::{to_repr}
///|
pub extend ConnectorGeometry with Eq::{equal, not_equal}
///|
pub extend ConnectorGeometry with @debug.Debug::{to_repr}
///|
pub extend ConnectorObstacle with Eq::{equal, not_equal}
///|
pub extend ConnectorObstacle with @debug.Debug::{to_repr}
///|
/// Construct immutable absolute geometry, refusing invalid numeric coordinates.
pub fn ConnectorPath::new(
commands : ArrayView[ConnectorCommand],
) -> ConnectorPath? {
if commands.is_empty() || !(commands[0] is Move(_)) {
return None
}
fn finite(p : ConnectorPoint) -> Bool {
p.x == p.x &&
p.y == p.y &&
p.x.abs() <= 1000000000.0 &&
p.y.abs() <= 1000000000.0
}
for command in commands {
let valid = match command {
Move(p) | Line(p) => finite(p)
Quadratic(c, p) => finite(c) && finite(p)
Cubic(a, b, p) => finite(a) && finite(b) && finite(p)
}
if !valid {
return None
}
}
Some({ commands: ReadOnlyArray::from_array(commands), })
}