forked from 1b-agent/opencadstudio-upstream
Lights, camera views and the geographic-location marker had no on-screen representation. Add display glyphs for all three: - Lights render through a new EntityType::Light: a cone/flashlight for spot lights, a wire sphere for point lights, a sun for distant lights, sized in screen space (fixed-world fallback). - Camera views (perspective VIEW records) and the GEODATA "daisy" marker are synthesized into the model-space resident wire set from the document, since they are not entities. Co-Authored-By: Claude Opus 4.8 <[email protected]>
223 lines
7.5 KiB
Rust
223 lines
7.5 KiB
Rust
//! Synthesized nonprint helper glyphs that are *not* CAD entities.
|
||
//!
|
||
//! Some drawing decorations are derived from document objects/tables rather
|
||
//! than from an entity in model space, so they never ride the per-entity
|
||
//! resident tessellation. Today that is the geographic-location "daisy" marker
|
||
//! (from the `GEODATA` object). These are appended to the Model-space resident
|
||
//! wire set by [`Scene::append_scene_markers`].
|
||
//!
|
||
//! Coordinates are absolute f64 (drawings can sit at UTM-scale origins), so the
|
||
//! wires are built with [`WireModel::solid_f64`], which fills the double-single
|
||
//! residual buffer and keeps the glyph precise and jitter-free.
|
||
|
||
use std::f64::consts::TAU;
|
||
|
||
use acadrust::objects::ObjectType;
|
||
|
||
use crate::scene::model::wire_model::WireModel;
|
||
use crate::scene::Scene;
|
||
|
||
/// World-space radius of a synthesized marker glyph.
|
||
const MARKER_R: f64 = 3.0;
|
||
|
||
const NAN: [f64; 3] = [f64::NAN, f64::NAN, f64::NAN];
|
||
|
||
fn push_ring(out: &mut Vec<[f64; 3]>, c: [f64; 3], r: f64, n: usize) {
|
||
if !out.is_empty() {
|
||
out.push(NAN);
|
||
}
|
||
for i in 0..=n {
|
||
let a = i as f64 * TAU / n as f64;
|
||
out.push([c[0] + a.cos() * r, c[1] + a.sin() * r, c[2]]);
|
||
}
|
||
}
|
||
|
||
fn push_seg(out: &mut Vec<[f64; 3]>, a: [f64; 3], b: [f64; 3]) {
|
||
if !out.is_empty() {
|
||
out.push(NAN);
|
||
}
|
||
out.push(a);
|
||
out.push(b);
|
||
}
|
||
|
||
// ── vector helpers for oriented glyphs ─────────────────────────────────────
|
||
|
||
fn v_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||
}
|
||
fn v_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||
[
|
||
a[1] * b[2] - a[2] * b[1],
|
||
a[2] * b[0] - a[0] * b[2],
|
||
a[0] * b[1] - a[1] * b[0],
|
||
]
|
||
}
|
||
fn v_norm(a: [f64; 3]) -> [f64; 3] {
|
||
let l = (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt();
|
||
if l < 1e-12 {
|
||
[0.0, 0.0, 0.0]
|
||
} else {
|
||
[a[0] / l, a[1] / l, a[2] / l]
|
||
}
|
||
}
|
||
|
||
/// Two unit axes spanning the plane perpendicular to `a`.
|
||
fn perp_basis(a: [f64; 3]) -> ([f64; 3], [f64; 3]) {
|
||
let up = if a[2].abs() > 0.9 {
|
||
[1.0, 0.0, 0.0]
|
||
} else {
|
||
[0.0, 0.0, 1.0]
|
||
};
|
||
let u = v_norm(v_cross(up, a));
|
||
let v = v_cross(a, u);
|
||
(u, v)
|
||
}
|
||
|
||
/// Camera display glyph at `cam_pos`, aimed along `fwd` (unit, toward the
|
||
/// subject): an oriented body box with a lens cone poking out the front.
|
||
fn camera_glyph(cam_pos: [f64; 3], fwd: [f64; 3], r: f64) -> Vec<[f64; 3]> {
|
||
let (u, v) = perp_basis(fwd);
|
||
// Point in the camera frame: `along` runs on the aim axis (front is +).
|
||
let f = |along: f64, su: f64, sv: f64| -> [f64; 3] {
|
||
[
|
||
cam_pos[0] + fwd[0] * along + u[0] * su + v[0] * sv,
|
||
cam_pos[1] + fwd[1] * along + u[1] * su + v[1] * sv,
|
||
cam_pos[2] + fwd[2] * along + u[2] * su + v[2] * sv,
|
||
]
|
||
};
|
||
let mut out: Vec<[f64; 3]> = Vec::new();
|
||
// Body box: behind the lens plane (along −1.3r..−0.3r).
|
||
let (bw, bh) = (0.6 * r, 0.45 * r);
|
||
let (zb, zf) = (-1.3 * r, -0.3 * r);
|
||
let corner = |along: f64, sx: f64, sy: f64| f(along, sx * bw, sy * bh);
|
||
let back = [
|
||
corner(zb, -1.0, -1.0),
|
||
corner(zb, 1.0, -1.0),
|
||
corner(zb, 1.0, 1.0),
|
||
corner(zb, -1.0, 1.0),
|
||
];
|
||
let front = [
|
||
corner(zf, -1.0, -1.0),
|
||
corner(zf, 1.0, -1.0),
|
||
corner(zf, 1.0, 1.0),
|
||
corner(zf, -1.0, 1.0),
|
||
];
|
||
for k in 0..4 {
|
||
let n = (k + 1) % 4;
|
||
push_seg(&mut out, back[k], back[n]);
|
||
push_seg(&mut out, front[k], front[n]);
|
||
push_seg(&mut out, back[k], front[k]);
|
||
}
|
||
// Lens cone: apex at the body front, rim ring in front of the camera.
|
||
let apex = f(zf, 0.0, 0.0);
|
||
let rim_along = 0.5 * r;
|
||
let rim_r = 0.35 * r;
|
||
let n = 16;
|
||
if !out.is_empty() {
|
||
out.push(NAN);
|
||
}
|
||
for i in 0..=n {
|
||
let a = i as f64 * TAU / n as f64;
|
||
out.push(f(rim_along, a.cos() * rim_r, a.sin() * rim_r));
|
||
}
|
||
for k in 0..4 {
|
||
let a = k as f64 * TAU / 4.0;
|
||
push_seg(
|
||
&mut out,
|
||
apex,
|
||
f(rim_along, a.cos() * rim_r, a.sin() * rim_r),
|
||
);
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Build the geographic-location "daisy": concentric rings plus radial petals,
|
||
/// with a longer spoke marking the drawing's north direction.
|
||
fn geo_daisy(center: [f64; 3], north: [f64; 2], r: f64) -> Vec<[f64; 3]> {
|
||
let mut out: Vec<[f64; 3]> = Vec::new();
|
||
push_ring(&mut out, center, r, 24);
|
||
push_ring(&mut out, center, r * 0.45, 16);
|
||
// 12 radial petals from the inner ring to the rim.
|
||
for k in 0..12 {
|
||
let a = k as f64 * TAU / 12.0;
|
||
let (ca, sa) = (a.cos(), a.sin());
|
||
push_seg(
|
||
&mut out,
|
||
[
|
||
center[0] + ca * r * 0.45,
|
||
center[1] + sa * r * 0.45,
|
||
center[2],
|
||
],
|
||
[center[0] + ca * r, center[1] + sa * r, center[2]],
|
||
);
|
||
}
|
||
// North spoke — normalized north direction, defaulting to +Y.
|
||
let nlen = (north[0] * north[0] + north[1] * north[1]).sqrt();
|
||
let (nx, ny) = if nlen < 1e-9 {
|
||
(0.0, 1.0)
|
||
} else {
|
||
(north[0] / nlen, north[1] / nlen)
|
||
};
|
||
push_seg(
|
||
&mut out,
|
||
center,
|
||
[
|
||
center[0] + nx * r * 1.6,
|
||
center[1] + ny * r * 1.6,
|
||
center[2],
|
||
],
|
||
);
|
||
out
|
||
}
|
||
|
||
impl Scene {
|
||
/// Append synthesized nonprint markers to a freshly built Model-space
|
||
/// resident wire set. No-op for any block other than model space.
|
||
pub(super) fn append_scene_markers(&self, wires: &mut Vec<WireModel>, _bg: [f32; 4]) {
|
||
// Geographic-location daisy (one per GEODATA object; normally one).
|
||
for obj in self.document.objects.values() {
|
||
if let ObjectType::GeoData(g) = obj {
|
||
let c = [g.design_point.x, g.design_point.y, g.design_point.z];
|
||
let north = [g.north_direction.x, g.north_direction.y];
|
||
let pts = geo_daisy(c, north, MARKER_R);
|
||
// A muted geo-marker colour (nonprint helper); the reader keeps
|
||
// no display colour for GEODATA, so use a fixed grey-cyan.
|
||
let w = WireModel::solid_f64(
|
||
format!("geomarker-{}", u64::from(g.handle)),
|
||
pts,
|
||
[0.55, 0.75, 0.85, 1.0],
|
||
false,
|
||
);
|
||
wires.push(w);
|
||
}
|
||
}
|
||
|
||
// Camera glyphs — one per perspective VIEW (created by the CAMERA
|
||
// command; VIEWMODE perspective bit). The camera sits at target +
|
||
// direction and looks back toward the target.
|
||
for view in self.document.views.iter() {
|
||
if !view.perspective {
|
||
continue;
|
||
}
|
||
let target = [view.target.x, view.target.y, view.target.z];
|
||
let dir = [view.direction.x, view.direction.y, view.direction.z];
|
||
let cam_pos = [target[0] + dir[0], target[1] + dir[1], target[2] + dir[2]];
|
||
// Aim from the camera back toward the subject (= −direction).
|
||
let fwd = v_norm(v_sub(target, cam_pos));
|
||
let fwd = if fwd == [0.0, 0.0, 0.0] {
|
||
[0.0, 0.0, -1.0]
|
||
} else {
|
||
fwd
|
||
};
|
||
let pts = camera_glyph(cam_pos, fwd, MARKER_R);
|
||
let w = WireModel::solid_f64(
|
||
format!("camera-{}", u64::from(view.handle)),
|
||
pts,
|
||
[0.60, 0.70, 0.90, 1.0],
|
||
false,
|
||
);
|
||
wires.push(w);
|
||
}
|
||
}
|
||
}
|