forked from 1b-agent/opencadstudio-upstream
The codec and kernel repositories and crates are renamed opencadcodec and opencadkernel. Inside OCS they are depended on under short keys, so code reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node graph engine). File-format and protocol spellings stay as they were: the ACIS product id `acadrust`, the ACADRUST_* switches and the plugin version keys `acadrust_version` / `acadrust_source`. A button left of MCP on the command line opens a translucent node graph over the viewport, with a palette of object, input, math, logic, list, text and geometry nodes added by click or drag. An object node owns drawing entities and its rows are the Properties panel's own rows for them: the single-object page is extracted from refresh_properties as entity_property_sections, values use the control protocol's property_json, and writes go through set_property_value with a property target override, so the panel, MCP and the graph share one read/write path. A list reaching an object row makes one entity per element. Each graph action is one undo step; the canvas pans and zooms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
6.5 KiB
Rust
186 lines
6.5 KiB
Rust
use super::super::Message;
|
|
use crate::scene::{VIEWCUBE_PX, VIEWCUBE_REGION_PX};
|
|
use iced::widget::{
|
|
button, container, mouse_area, stack, Space,
|
|
};
|
|
use iced::{Background, Border, Element, Theme};
|
|
|
|
// ── Render-mode picker ──────────────────────────────────────────────────────
|
|
|
|
/// Top-left viewport control bar: a single dark chip holding (optionally) the
|
|
/// horizontal/vertical split buttons, the render-mode picker, and the grid /
|
|
/// grid-snap toggles. `include_split` is off for paper-space viewports, which
|
|
/// have no model-tile splitting. Grid / snap reflect the active viewport's
|
|
/// state and emit `ToggleGrid` / `ToggleGridSnap`.
|
|
// ── ViewCube navigation controls (home / roll / nudge / UCS) ───────────────
|
|
|
|
/// Place `el` at pixel offset (x, y) inside a fill layer (top-left origin).
|
|
fn vc_place<'a>(x: f32, y: f32, el: Element<'a, Message>) -> Element<'a, Message> {
|
|
iced::widget::pin(el)
|
|
.position(iced::Point::new(x.max(0.0), y.max(0.0)))
|
|
.into()
|
|
}
|
|
|
|
/// Borderless square icon button used by the ViewCube nav controls.
|
|
fn vc_btn<'a>(content: Element<'a, Message>, size: f32, msg: Message) -> Element<'a, Message> {
|
|
button(
|
|
container(content)
|
|
.width(iced::Length::Fixed(size))
|
|
.height(iced::Length::Fixed(size))
|
|
.center_x(iced::Length::Fixed(size))
|
|
.center_y(iced::Length::Fixed(size)),
|
|
)
|
|
.padding(0)
|
|
.on_press(msg)
|
|
.style(|theme: &Theme, status| iced::widget::button::Style {
|
|
background: matches!(
|
|
status,
|
|
iced::widget::button::Status::Hovered | iced::widget::button::Status::Pressed
|
|
)
|
|
.then_some(Background::Color(
|
|
theme.palette().primary.weak.color
|
|
)),
|
|
border: Border {
|
|
radius: 3.0.into(),
|
|
..Default::default()
|
|
},
|
|
text_color: theme.palette().background.base.text,
|
|
..Default::default()
|
|
})
|
|
.into()
|
|
}
|
|
|
|
/// Overlay of home / roll / nudge controls sized to the whole nav region, so
|
|
/// the caller can position it exactly like the cube hit area.
|
|
pub(super) fn viewcube_nav_controls<'a>(
|
|
viewport: Option<codec::Handle>,
|
|
) -> Element<'a, Message> {
|
|
use crate::scene::NudgeDir;
|
|
use crate::ui::icons;
|
|
let r = VIEWCUBE_REGION_PX;
|
|
let c = r * 0.5;
|
|
let cube_half = VIEWCUBE_PX as f32 * 0.36; // VIEWCUBE_PX * VIEWCUBE_SCALE
|
|
let nr = cube_half + 6.0; // nudge triangle distance from centre
|
|
const BTN: f32 = 16.0;
|
|
const TRI: f32 = 9.0;
|
|
let ctr = |cx: f32, cy: f32, s: f32| (cx - s * 0.5, cy - s * 0.5);
|
|
|
|
// Home top-left, roll arrows top-right.
|
|
let (rax, ray) = (r - 2.0 * BTN - 4.0, 2.0);
|
|
let (rbx, rby) = (r - BTN - 2.0, 2.0);
|
|
// Nudge triangles pointing inward at the four cube faces.
|
|
let (tux, tuy) = ctr(c, c - nr, TRI);
|
|
let (tdx, tdy) = ctr(c, c + nr, TRI);
|
|
let (tlx, tly) = ctr(c - nr, c, TRI);
|
|
let (trx, try_) = ctr(c + nr, c, TRI);
|
|
|
|
// Bottom layer: the cube/cardinal hit area covering the whole region. The
|
|
// control buttons sit ABOVE it in the same stack, so a click on a button is
|
|
// caught by the button while a click on the cube (or empty space) falls
|
|
// through to this mouse_area → ViewportClick. Moves keep cursor_pos current.
|
|
let cube_hit = mouse_area(
|
|
Space::new()
|
|
.width(iced::Length::Fixed(r))
|
|
.height(iced::Length::Fixed(r)),
|
|
)
|
|
.on_move(move |point| Message::CursorMoved(point, viewport))
|
|
.on_press(Message::ViewportClick(viewport));
|
|
|
|
let controls = stack![
|
|
cube_hit,
|
|
vc_place(
|
|
3.0,
|
|
3.0,
|
|
vc_btn(icons::themed_home(13.0), BTN, Message::ViewCubeHome)
|
|
),
|
|
vc_place(
|
|
rax,
|
|
ray,
|
|
vc_btn(icons::themed_undo(12.0, true), BTN, Message::ViewCubeRoll(false))
|
|
),
|
|
vc_place(
|
|
rbx,
|
|
rby,
|
|
vc_btn(icons::themed_redo(12.0, true), BTN, Message::ViewCubeRoll(true))
|
|
),
|
|
vc_place(
|
|
tux,
|
|
tuy,
|
|
vc_btn(
|
|
icons::themed_arrow_down(8.0),
|
|
TRI,
|
|
Message::ViewCubeNudge(NudgeDir::Up)
|
|
)
|
|
),
|
|
vc_place(
|
|
tdx,
|
|
tdy,
|
|
vc_btn(
|
|
icons::themed_arrow_up(8.0),
|
|
TRI,
|
|
Message::ViewCubeNudge(NudgeDir::Down)
|
|
)
|
|
),
|
|
vc_place(
|
|
tlx,
|
|
tly,
|
|
vc_btn(
|
|
icons::themed_arrow_right(8.0),
|
|
TRI,
|
|
Message::ViewCubeNudge(NudgeDir::Left)
|
|
)
|
|
),
|
|
vc_place(
|
|
trx,
|
|
try_,
|
|
vc_btn(
|
|
icons::themed_arrow_left(8.0),
|
|
TRI,
|
|
Message::ViewCubeNudge(NudgeDir::Right)
|
|
)
|
|
),
|
|
];
|
|
|
|
container(controls)
|
|
.width(iced::Length::Fixed(r))
|
|
.height(iced::Length::Fixed(r))
|
|
.into()
|
|
}
|
|
|
|
/// Fixed width of the UCS picker, so it can be centred under the cube.
|
|
pub(super) const UCS_PICKER_W: f32 = 84.0;
|
|
|
|
/// The WCS / named-UCS selector shown under the cube.
|
|
pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec<String>) -> Element<'a, Message> {
|
|
let mut options = vec!["WCS".to_string()];
|
|
options.extend(names);
|
|
let selected = if current.is_empty() {
|
|
"WCS".to_string()
|
|
} else {
|
|
current
|
|
};
|
|
iced::widget::pick_list(Some(selected), options, |value| value.to_string())
|
|
.on_select(Message::SetViewcubeUcs)
|
|
.text_size(11)
|
|
.padding([2, 6])
|
|
// Fixed width so the caller can centre it under the cube centre with a
|
|
// simple half-width offset (content-sized width would drift off-centre).
|
|
.width(iced::Length::Fixed(UCS_PICKER_W))
|
|
.style(move |theme: &Theme, _| {
|
|
let palette = theme.palette();
|
|
iced::widget::pick_list::Style {
|
|
background: Background::Color(palette.background.weak.color),
|
|
border: Border {
|
|
radius: 3.0.into(),
|
|
color: palette.background.neutral.color,
|
|
width: 1.0,
|
|
..Default::default()
|
|
},
|
|
text_color: palette.background.base.text,
|
|
placeholder_color: palette.background.base.text.scale_alpha(0.68),
|
|
handle_color: palette.background.base.text,
|
|
}
|
|
})
|
|
.into()
|
|
}
|