Files
OneCAD-Draft/src/scene/scene_markers.rs
T
Hakan SevenandClaude Opus 4.8 b30f9b9741 feat(scene): draw nonprint light, camera and geo-location glyphs
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]>
2026-07-19 22:15:40 +03:00

223 lines
7.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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);
}
}
}