Capture full-scene native editing and probe label candidate feasibility

This commit is contained in:
2026-10-01 18:43:00 -07:00
parent 971b9721c3
commit 05b1b1d804
80 changed files with 28209 additions and 1 deletions
@@ -330,6 +330,278 @@ fn snapshot(project: &Project, diagram: Id, point: Position, zoom: f32) -> Resul
)
}
// Bounded reconstruction of routing.rs Rect predicates, not a production API.
// The clearance calculation additionally requires exactly orthogonal segments.
#[derive(Clone, Copy)]
struct LabelProbeRect {
left: f32,
top: f32,
right: f32,
bottom: f32,
}
impl LabelProbeRect {
fn new(position: Position, size: oa_core::Size) -> Self {
Self {
left: position.x,
top: position.y,
right: position.x + size.width,
bottom: position.y + size.height,
}
}
fn inflate(self, amount: f32) -> Self {
Self {
left: self.left - amount,
top: self.top - amount,
right: self.right + amount,
bottom: self.bottom + amount,
}
}
fn overlaps(self, other: Self) -> bool {
self.left < other.right
&& self.right > other.left
&& self.top < other.bottom
&& self.bottom > other.top
}
fn crosses(self, a: Position, b: Position) -> bool {
// Keep the source's strict bounds and epsilon, including point segments.
const EPSILON: f32 = 0.001;
if a == b {
return a.x > self.left + EPSILON
&& a.x < self.right - EPSILON
&& a.y > self.top + EPSILON
&& a.y < self.bottom - EPSILON;
}
if (a.x - b.x).abs() < EPSILON {
a.x > self.left + EPSILON
&& a.x < self.right - EPSILON
&& a.y.min(b.y) < self.bottom - EPSILON
&& a.y.max(b.y) > self.top + EPSILON
} else if (a.y - b.y).abs() < EPSILON {
a.y > self.top + EPSILON
&& a.y < self.bottom - EPSILON
&& a.x.min(b.x) < self.right - EPSILON
&& a.x.max(b.x) > self.left + EPSILON
} else {
true
}
}
fn clearance(self, a: Position, b: Position) -> f32 {
let dx = (a.x.min(b.x) - self.right)
.max(self.left - a.x.max(b.x))
.max(0.);
let dy = (a.y.min(b.y) - self.bottom)
.max(self.top - a.y.max(b.y))
.max(0.);
dx.hypot(dy)
}
}
fn label_candidates(project: &Project, diagram_id: Id, edge_id: Id) -> Result<Value> {
let original = project.clone();
let diagram = project.diagrams.get(&diagram_id).ok_or("Unknown diagram")?;
let view = projection::project_view(project, diagram.id);
if view.nodes.iter().any(|node| node.visual_parent.is_some()) {
return Err("Label candidate probe supports only flat projected nodes".into());
}
let routed = routing::route_diagram(project, diagram, &view);
let target = routed.edges.get(&edge_id).ok_or("Unknown routed edge")?;
if target.custom_suspended
|| diagram
.routes
.iter()
.any(|route| route.edge == edge_id && route.label.is_some())
{
return Err("Label candidate probe requires an automatic, unsuspended label".into());
}
if target.label.text.is_empty() || target.points.len() < 2 {
return Err("Label candidate probe requires nonempty text and a routed path".into());
}
for edge in routed.edges.values() {
if project
.relations
.get(&edge.id)
.is_some_and(|relation| !routing::association_end_geometry(relation, edge).is_empty())
{
return Err("Label candidate probe does not support association-end labels".into());
}
if edge.id < edge_id && !edge.label.text.is_empty() {
return Err("Label candidate probe does not support earlier nonempty labels".into());
}
if edge
.points
.iter()
.any(|p| !p.x.is_finite() || !p.y.is_finite())
|| edge
.points
.windows(2)
.any(|p| p[0].x != p[1].x && p[0].y != p[1].y)
{
return Err("Label candidate probe requires finite, exactly orthogonal paths".into());
}
}
// All projected nodes are flat, so edge_obstacles has no traversable ancestors.
let obstacles: Vec<_> = view
.nodes
.iter()
.map(|node| LabelProbeRect::new(node.position, node.size).inflate(12.))
.collect();
// Match record_segments' exact undirected deduplication. Semantic ownership
// is reported separately below and must not be lost by this score dedup.
let mut strokes: Vec<(Position, Position)> = Vec::new();
for edge in routed.edges.values() {
for pair in edge.points.windows(2) {
let (a, b) = (pair[0], pair[1]);
if a != b
&& !strokes
.iter()
.any(|&(x, y)| (x == a && y == b) || (x == b && y == a))
{
strokes.push((a, b));
}
}
}
let size = oa_core::Size {
width: target.label.text.chars().count() as f32 * 7. + 16.,
height: 24.,
};
if size != target.label.bounds.size {
return Err(
"Production label dimensions differ from this experiment's reconstruction".into(),
);
}
let length = |a: Position, b: Position| (b.x - a.x).hypot(b.y - a.y);
let total: f32 = target.points.windows(2).map(|p| length(p[0], p[1])).sum();
let mut alternatives = vec![0.5_f32];
if total > 0. {
let mut traversed = 0.;
for pair in target.points.windows(2) {
let len = length(pair[0], pair[1]);
alternatives.push((traversed + len / 2.) / total);
traversed += len;
}
alternatives.extend((1..20).map(|i| i as f32 / 20.));
}
alternatives.sort_by(|a, b| {
(a - 0.5)
.abs()
.total_cmp(&(b - 0.5).abs())
.then_with(|| a.total_cmp(b))
});
alternatives.dedup();
alternatives.truncate(32);
let offsets = [
Position { x: 0., y: -18. },
Position { x: 0., y: 18. },
Position { x: 0., y: -46. },
Position { x: 0., y: 46. },
Position { x: 0., y: -74. },
Position { x: 0., y: 74. },
Position {
x: size.width / 2. + 18.,
y: 0.,
},
Position {
x: -size.width / 2. - 18.,
y: 0.,
},
];
let mut rows = Vec::new();
let mut scores = Vec::new();
for offset in offsets {
for &fraction in &alternatives {
let anchor = routing::point_along_polyline(&target.points, fraction)
.ok_or("Missing candidate anchor")?;
let center = Position {
x: anchor.x + offset.x,
y: anchor.y + offset.y,
};
let position = Position {
x: center.x - size.width / 2.,
y: center.y - size.height / 2.,
};
let bounds = LabelProbeRect::new(position, size);
let inflated = bounds.inflate(3.);
let score = (
obstacles
.iter()
.filter(|obstacle| obstacle.overlaps(bounds))
.count(),
0_usize,
strokes
.iter()
.filter(|&&(a, b)| inflated.crosses(a, b))
.count(),
);
let mut own_clearance: Option<f32> = None;
let mut foreign_clearance: Option<f32> = None;
let mut own_intersections = Vec::new();
let mut foreign_intersections = Vec::new();
for edge in routed.edges.values() {
let mut indices = Vec::new();
for (index, pair) in edge.points.windows(2).enumerate() {
if pair[0] == pair[1] {
continue;
}
let clearance = bounds.clearance(pair[0], pair[1]);
let nearest = if edge.id == edge_id {
&mut own_clearance
} else {
&mut foreign_clearance
};
*nearest = Some(nearest.map_or(clearance, |old| old.min(clearance)));
if inflated.crosses(pair[0], pair[1]) {
indices.push(index);
}
}
if !indices.is_empty() {
let entry = json!({"edge":edge.id,
"relation_alias":project.relations.get(&edge.id).map(|r| &r.alias),
"segments":indices});
if edge.id == edge_id {
own_intersections.push(entry);
} else {
foreign_intersections.push(entry);
}
}
}
scores.push(score);
rows.push(
json!({"fraction":fraction,"offset":offset,"anchor":anchor,"center":center,
"bounds":{"position":position,"size":size},"collision_tuple":score,
"own_intersections":own_intersections,"foreign_intersections":foreign_intersections,
"own_clearance":own_clearance,"foreign_clearance":foreign_clearance}),
);
}
}
let best = scores.iter().min().copied().ok_or("No label candidates")?;
let best_indices: Vec<_> = scores
.iter()
.enumerate()
.filter_map(|(index, &score)| (score == best).then_some(index))
.collect();
if *project != original {
return Err("Project changed during label candidate experiment".into());
}
Ok(
json!({"experiment":"issue38 exhaustive f32 label candidate feasibility; not usability acceptance",
"scope":"Unbudgeted exhaustive superset of existing candidates. No 8192-comparison budget, early pruning, early exit, or production chooser claim.",
"restrictions":"Flat projected nodes, automatic unsuspended nonempty target label, no association-end labels or earlier nonempty labels, finite orthogonal paths.",
"geometry_policy":"Source-exact f32 candidate construction and collision predicates: nodes inflated 12; label inflated 3 for stroke intersections; exact undirected stroke deduplication. Clearances use the uninflated label box. Semantic intersection lists retain every distinct edge owner; counts are not the deduplicated collision score.",
"build_provenance":"Require external source/build identity; this field does not identify the executable",
"diagram":diagram.id,"edge":edge_id,"text":target.label.text,
"project":project,"project_conserved":true,"diagnostics":validation::validate(project),
"fraction_count":alternatives.len(),"offset_count":offsets.len(),"candidate_count":rows.len(),
"distinct_stroke_count":strokes.len(),
"zero_node_zero_stroke_count":scores.iter().filter(|score| score.0 == 0 && score.2 == 0).count(),
"best_collision_tuple":best,"best_candidate_indices":best_indices,"candidates":rows}),
)
}
fn write_new(path: &Path, bytes: &[u8]) -> Result<()> {
let mut file = OpenOptions::new().write(true).create_new(true).open(path)?;
file.write_all(bytes)?;
@@ -374,8 +646,19 @@ fn main() -> Result<()> {
}
return Ok(());
}
if args.len() == 6 && args[0] == "label-candidates" {
let input = fs::read(&args[1])?;
let project = admit(&input, &fs::read(&args[2])?)?;
let mut result = label_candidates(&project, args[3].parse()?, args[4].parse()?)?;
result["original_input_utf8"] = json!(std::str::from_utf8(&input)?);
if fs::read(&args[1])? != input {
return Err("Input changed during label candidate experiment".into());
}
result["input_bytes_conserved"] = json!(true);
return write_new(Path::new(&args[5]), &serde_json::to_vec_pretty(&result)?);
}
if args.len() != 8 || args[0] != "probe" {
return Err("Usage: generate NEW_DIRECTORY | probe INPUT_JSON_OR_SINGLE_SOURCE EXPECTED_DIAGNOSTICS DIAGRAM_UUID WORLD_X WORLD_Y ZOOM NEW_OUTPUT_JSON".into());
return Err("Usage: generate NEW_DIRECTORY | probe INPUT_JSON_OR_SINGLE_SOURCE EXPECTED_DIAGNOSTICS DIAGRAM_UUID WORLD_X WORLD_Y ZOOM NEW_OUTPUT_JSON | label-candidates INPUT EXPECTED_DIAGNOSTICS DIAGRAM_UUID EDGE_UUID NEW_OUTPUT_JSON".into());
}
let input = fs::read(&args[1])?;
let project = admit(&input, &fs::read(&args[2])?)?;
@@ -399,6 +682,56 @@ fn main() -> Result<()> {
mod tests {
use super::*;
#[test]
fn conversation_existing_candidates_cannot_avoid_foreign_allocation() {
let project = representative().unwrap();
let original = project.clone();
let target: Id = "63009b5f-539e-5914-b630-ec6b7fefb4d5".parse().unwrap();
let foreign = "725f2e1b-aa2c-5615-9b1f-28da178168fa";
let result = label_candidates(&project, EIGHT.parse().unwrap(), target).unwrap();
assert_eq!(project, original);
assert_eq!(result["project"], serde_json::to_value(&original).unwrap());
assert_eq!(result["fraction_count"], 23);
assert_eq!(result["candidate_count"], 184);
assert_eq!(result["distinct_stroke_count"], 37);
assert_eq!(result["zero_node_zero_stroke_count"], 0);
assert_eq!(result["best_collision_tuple"], json!([0, 0, 1]));
// Independently declared from the saved baseline geometry and a JS
// double-precision reconstruction, not generated by this Rust scorer.
// Allow f32 interpolation differences without changing the finding.
let expected = [
(0.5, 274.5),
(0.45, 294.8),
(0.55, 254.2),
(0.6, 233.9),
(0.65, 213.6),
(0.6761083743842364, 203.),
];
let near = |value: &Value, expected: f64| {
assert!(
(value.as_f64().unwrap() - expected).abs() < 0.0002,
"{value} differs from declared {expected}"
);
};
let best = result["best_candidate_indices"].as_array().unwrap();
assert_eq!(best.len(), expected.len());
for (index, (fraction, y)) in best.iter().zip(expected) {
let row = &result["candidates"][index.as_u64().unwrap() as usize];
near(&row["fraction"], fraction);
near(&row["offset"]["x"], 134.5);
near(&row["offset"]["y"], 0.);
near(&row["center"]["x"], 1296.5);
near(&row["center"]["y"], y);
near(&row["own_clearance"], 18.);
near(&row["foreign_clearance"], 0.);
assert_eq!(row["own_intersections"], json!([]));
let intersections = row["foreign_intersections"].as_array().unwrap();
assert_eq!(intersections.len(), 1);
assert_eq!(intersections[0]["edge"], foreign);
assert_eq!(intersections[0]["relation_alias"], "sa_repair_allocation");
assert_eq!(intersections[0]["segments"], json!([2]));
}
}
#[test]
fn representative_preserves_every_original_record_and_view() {
let baseline = admit(RELEASE, RELEASE_WARNINGS).unwrap();
let mut generated = representative().unwrap();