forked from 1b-agent/opencadstudio-upstream
feat(automation): entity, block, publishing and REST operations from the fork
The operations docs/automation/API-SPEC.md (PR #1383) describes, brought over from huaninstratech/OpenCADStudio so the specification no longer runs ahead of the code: - entity CRUD: entities_create, entities_delete, entities_transform, entities_copy_to, and `where` filters on query - blocks and export: block_define, block_delete, a normalised wblock - publishing: layout_create, page_setup_set, plot (per_page included) - metadata: sysvar, xdata_set / xdata_get, file_identity - collections: group_create, selection_set_save / load - control plane: batch, events, view_focus, close - the REST transport: `--http <port>`, the /api/v1 routes and their OpenAPI 3 description - person-in-the-loop picks: user_select and getpoint, hosted by the GUI over the same REST routes, with on-screen pending signals Taken from the fork's commits b7a7ed80, 69396b42, 64f1cf6a, c3aafc8e, bb5a7bfa, 52317a5c, 50d293ba, b90e194f, 517c6397, 53a5c4c9, a49ac045, 37de5a46, a78b1822, 6d9aed2a, 33f522da, 5b5cafe2, 02a7e78d, 94df40dc, fd5bc14d, 485fac61 and 2c5fc994. Left out on purpose: 37de5a46 also removed the donation surface (the Patreon fetch, the donation prompt, the Start-page supporters rail, the DONATE command and its locale strings) and replaced the About dialog's call to action; that is the fork's own product decision and stays out. The fork's version bump and its unrelated IFC/BIM work are not part of this. Adapted while porting: the REST server and the GUI bridge are desktop-only and a template start is refused in the web build, so the wasm target still compiles; the two plot tests read the PDF through the shared stream helper, because exported streams are Flate-compressed; the benches set the new crosshair field; comments and docs describe each operation in its own terms rather than by another product's API names. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
531ba44faa
commit
279a87418e
37 files changed
+8795
-82
No files matched your search
+20
-5
@@ -51,12 +51,21 @@ fn cmd_input_id() -> iced::widget::Id {
|
||||
iced::widget::Id::new(CMD_INPUT_ID)
|
||||
}
|
||||
|
||||
fn mcp_status(enabled: bool, busy: bool) -> (&'static str, Color) {
|
||||
/// Footer pill state for the automation channel. `waiting` — a client
|
||||
/// `user_select` / `getpoint` is parked and the person at the screen must
|
||||
/// answer — outranks `busy`: the screen has to say "act now", not just
|
||||
/// "something is running".
|
||||
fn mcp_status(enabled: bool, busy: bool, waiting: bool) -> (&'static str, Color) {
|
||||
if !enabled {
|
||||
(
|
||||
"MCP control is off",
|
||||
Color::from_rgb(0.90, 0.35, 0.35),
|
||||
)
|
||||
} else if waiting {
|
||||
(
|
||||
"MCP is waiting for you to pick — Enter confirms, Esc cancels",
|
||||
Color::from_rgb(0.30, 0.55, 0.98),
|
||||
)
|
||||
} else if busy {
|
||||
(
|
||||
"MCP is handling a request",
|
||||
@@ -633,6 +642,7 @@ impl CommandLine {
|
||||
window_height: f32,
|
||||
control_enabled: bool,
|
||||
control_busy: bool,
|
||||
pick_pending: bool,
|
||||
) -> Element<'a, Message> {
|
||||
// Only the most recent entries pushed within COMMANDLINEFADETIME
|
||||
// show on the overlay (0 skips transient lines). The dropdown button
|
||||
@@ -844,7 +854,7 @@ impl CommandLine {
|
||||
.align_y(iced::alignment::Vertical::Center),
|
||||
]
|
||||
.width(Length::Fill);
|
||||
let (mcp_tooltip, mcp_color) = mcp_status(control_enabled, control_busy);
|
||||
let (mcp_tooltip, mcp_color) = mcp_status(control_enabled, control_busy, pick_pending);
|
||||
let mcp_btn = button(text("MCP").size(11))
|
||||
.on_press(Message::ControlToggle)
|
||||
.style(move |theme: &Theme, status| {
|
||||
@@ -1157,9 +1167,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mcp_status_distinguishes_off_ready_and_busy() {
|
||||
assert_eq!(mcp_status(false, false).0, "MCP control is off");
|
||||
assert_eq!(mcp_status(true, false).0, "MCP control is ready");
|
||||
assert_eq!(mcp_status(true, true).0, "MCP is handling a request");
|
||||
assert_eq!(mcp_status(false, false, false).0, "MCP control is off");
|
||||
assert_eq!(mcp_status(true, false, false).0, "MCP control is ready");
|
||||
assert_eq!(mcp_status(true, true, false).0, "MCP is handling a request");
|
||||
// A parked pick outranks the plain busy state: the person must act.
|
||||
assert_eq!(
|
||||
mcp_status(true, true, true).0,
|
||||
"MCP is waiting for you to pick — Enter confirms, Esc cancels"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+46
-24
@@ -223,6 +223,10 @@ pub struct CrosshairOptions {
|
||||
pub iso_plane: IsoPlane,
|
||||
pub snap_angle_deg: f32,
|
||||
pub point_mode: bool,
|
||||
/// A client pick (`user_select` / `getpoint`) is waiting for the person:
|
||||
/// the crosshair arms disappear and a blue pickbox remains — the
|
||||
/// screen-level "the client wants YOU to pick" signal.
|
||||
pub pick_pending: bool,
|
||||
}
|
||||
|
||||
/// Rendering style for the viewport grid.
|
||||
@@ -1769,8 +1773,16 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||
},
|
||||
);
|
||||
let color = Color { r, g, b, a };
|
||||
// Pending client pick: drop the arms, keep a blue pickbox —
|
||||
// the same blue as the MCP "waiting for you to pick" pill.
|
||||
let pick_pending = self.crosshair.pick_pending;
|
||||
let color = if pick_pending {
|
||||
Color::from_rgb(0.30, 0.55, 0.98)
|
||||
} else {
|
||||
color
|
||||
};
|
||||
let stroke = canvas::Stroke {
|
||||
width: 1.0,
|
||||
width: if pick_pending { 1.5 } else { 1.0 },
|
||||
style: canvas::Style::Solid(color),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -1786,33 +1798,43 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||
} else {
|
||||
[0.0, 90.0]
|
||||
};
|
||||
for angle in base_angles {
|
||||
let rad = (angle + self.crosshair.snap_angle_deg as f64).to_radians();
|
||||
let dir = Point::new(rad.cos() as f32, -rad.sin() as f32);
|
||||
let gap = if point_mode {
|
||||
9.0
|
||||
} else if sq > 0.0 {
|
||||
sq / dir.x.abs().max(dir.y.abs()).max(1e-6)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let arms = canvas::Path::new(|path| {
|
||||
path.move_to(Point::new(cp.x + dir.x * gap, cp.y + dir.y * gap));
|
||||
path.line_to(Point::new(cp.x + dir.x * arm, cp.y + dir.y * arm));
|
||||
path.move_to(Point::new(cp.x - dir.x * gap, cp.y - dir.y * gap));
|
||||
path.line_to(Point::new(cp.x - dir.x * arm, cp.y - dir.y * arm));
|
||||
});
|
||||
frame.stroke(&arms, stroke.clone());
|
||||
// The arms stay only for the normal (non-pending) cursor: a
|
||||
// waiting pick shows the box alone, regardless of the UCS
|
||||
// rotation, so the square is unmistakable.
|
||||
if !pick_pending {
|
||||
for angle in base_angles {
|
||||
let rad = (angle + self.crosshair.snap_angle_deg as f64).to_radians();
|
||||
let dir = Point::new(rad.cos() as f32, -rad.sin() as f32);
|
||||
let gap = if point_mode {
|
||||
9.0
|
||||
} else if sq > 0.0 {
|
||||
sq / dir.x.abs().max(dir.y.abs()).max(1e-6)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let arms = canvas::Path::new(|path| {
|
||||
path.move_to(Point::new(cp.x + dir.x * gap, cp.y + dir.y * gap));
|
||||
path.line_to(Point::new(cp.x + dir.x * arm, cp.y + dir.y * arm));
|
||||
path.move_to(Point::new(cp.x - dir.x * gap, cp.y - dir.y * gap));
|
||||
path.line_to(Point::new(cp.x - dir.x * arm, cp.y - dir.y * arm));
|
||||
});
|
||||
frame.stroke(&arms, stroke.clone());
|
||||
}
|
||||
}
|
||||
if point_mode {
|
||||
let dot = canvas::Path::circle(cp, 1.75);
|
||||
frame.fill(&dot, color);
|
||||
} else if sq > 0.0 {
|
||||
let square = canvas::Path::rectangle(
|
||||
Point::new(cp.x - sq, cp.y - sq),
|
||||
Size::new(sq * 2.0, sq * 2.0),
|
||||
);
|
||||
frame.stroke(&square, stroke);
|
||||
} else {
|
||||
// A pending pick keeps the square visible even when the
|
||||
// user's PICKBOX setting would hide it.
|
||||
let sq = if pick_pending { sq.max(8.0) } else { sq };
|
||||
if sq > 0.0 {
|
||||
let square = canvas::Path::rectangle(
|
||||
Point::new(cp.x - sq, cp.y - sq),
|
||||
Size::new(sq * 2.0, sq * 2.0),
|
||||
);
|
||||
frame.stroke(&square, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
// Locked-layer badge: a small padlock beside the crosshair when
|
||||
|
||||
Reference in new issue
Block a user