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]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-19 22:15:40 +03:00
1 parent 007d03422e
commit b30f9b9741
10 files changed
+561 -7

No files matched your search

+317
View File
@@ -0,0 +1,317 @@
//! Light entity display glyph (point / spot / distant).
//!
//! Lights are nonprint helper entities: the DWG reader decodes the source
//! position and aim point (see `acadrust`'s `Light`); here we synthesize a
//! wireframe glyph so the light is visible and selectable in the editor, the
//! same way AutoCAD shows a light symbol.
//!
//! The glyph is sized in **screen space** (a roughly constant pixel size across
//! zoom) via [`relative_truck`], falling back to a small fixed world size when
//! no world-per-pixel factor is available (e.g. snapshot tessellation).
use acadrust::entities::Light;
use acadrust::EntityType;
use crate::command::EntityTransform;
use crate::entities::common::{center_grip, edit_prop as edit, ro_prop, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection};
use crate::scene::model::wire_model::SnapHint;
/// On-screen glyph radius in pixels for the screen-relative path.
const GLYPH_PX: f64 = 16.0;
/// Fixed world-space glyph radius used when no zoom factor is available.
const FIXED_WORLD: f64 = 2.0;
// ── vector helpers (f64, absolute world space) ─────────────────────────────
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
fn 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 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]
}
}
fn pos(l: &Light) -> [f64; 3] {
[l.position.x, l.position.y, l.position.z]
}
fn tgt(l: &Light) -> [f64; 3] {
[l.target.x, l.target.y, l.target.z]
}
/// Unit aim direction (position → target), defaulting to straight down when
/// the target coincides with the source (point lights carry no meaningful aim).
fn aim(l: &Light) -> [f64; 3] {
let d = norm(sub(tgt(l), pos(l)));
if d == [0.0, 0.0, 0.0] {
[0.0, 0.0, -1.0]
} else {
d
}
}
/// 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 = norm(cross(up, a));
let v = cross(a, u);
(u, v)
}
const NAN: [f64; 3] = [f64::NAN, f64::NAN, f64::NAN];
/// Append a disconnected segment, NaN-separated from prior geometry.
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);
}
/// Append a closed ring of radius `r` in the (u, v) plane about `c`.
fn push_ring(out: &mut Vec<[f64; 3]>, c: [f64; 3], u: [f64; 3], v: [f64; 3], r: f64, n: usize) {
if !out.is_empty() {
out.push(NAN);
}
for i in 0..=n {
let a = i as f64 * std::f64::consts::TAU / n as f64;
let (ca, sa) = (a.cos() * r, a.sin() * r);
out.push([
c[0] + u[0] * ca + v[0] * sa,
c[1] + u[1] * ca + v[1] * sa,
c[2] + u[2] * ca + v[2] * sa,
]);
}
}
/// Build the light glyph wire (absolute world coordinates) at radius `r`.
fn light_wire(l: &Light, r: f64) -> Vec<[f64; 3]> {
let p = pos(l);
let mut out: Vec<[f64; 3]> = Vec::new();
match l.light_type {
// ── Spot (cone / flashlight) ──────────────────────────────────────
3 => {
let a = aim(l);
let (u, v) = perp_basis(a);
let len = r * 2.4;
let base_r = r;
let c = add(p, scale(a, len));
// Emitter ring at the source.
push_ring(&mut out, p, u, v, r * 0.35, 12);
// Cone mouth + rays from the source to the rim.
push_ring(&mut out, c, u, v, base_r, 16);
for k in 0..4 {
let ang = k as f64 * std::f64::consts::FRAC_PI_2;
let rim = add(
c,
add(scale(u, ang.cos() * base_r), scale(v, ang.sin() * base_r)),
);
push_seg(&mut out, p, rim);
}
}
// ── Distant (sun with parallel rays) ─────────────────────────────
1 => {
let (u, v) = ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
push_ring(&mut out, p, u, v, r * 0.6, 16);
// A few parallel rays streaming off toward −X.
let dir = [-1.0, 0.0, 0.0];
for k in -1..=1 {
let off = scale(v, k as f64 * r * 0.5);
let a = add(add(p, off), scale(u, r * 0.6));
let b = add(a, scale(dir, r * 1.4));
push_seg(&mut out, a, b);
}
}
// ── Point (radiant sphere) ───────────────────────────────────────
_ => {
push_ring(&mut out, p, [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], r, 20);
push_ring(&mut out, p, [1.0, 0.0, 0.0], [0.0, 0.0, 1.0], r, 20);
push_ring(&mut out, p, [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], r, 20);
// Short radial ticks to read as a light source.
for k in 0..4 {
let ang = k as f64 * std::f64::consts::FRAC_PI_2 + std::f64::consts::FRAC_PI_4;
let d = [ang.cos(), ang.sin(), 0.0];
push_seg(&mut out, add(p, scale(d, r)), add(p, scale(d, r * 1.5)));
}
}
}
out
}
fn build(l: &Light, r: f64) -> TruckEntity {
let p = pos(l);
let snap = glam::DVec3::new(p[0], p[1], p[2]);
let mut snap_pts = vec![(snap, SnapHint::Node)];
let mut key = vec![p];
if l.is_spot() {
let t = tgt(l);
snap_pts.push((glam::DVec3::new(t[0], t[1], t[2]), SnapHint::Node));
key.push(t);
}
TruckEntity {
pick_tris: Vec::new(),
object: TruckObject::Lines(light_wire(l, r)),
snap_pts,
tangent_geoms: vec![],
key_vertices: key,
fill_tris: vec![],
}
}
/// Viewport-aware glyph: constant on-screen size from the world-per-pixel
/// factor. Returns `None` for a non-light entity or when no factor is
/// available (the caller then falls back to the fixed-world [`to_truck`]).
pub fn relative_truck(
entity: &EntityType,
_document: &acadrust::CadDocument,
wpp: Option<f32>,
) -> Option<TruckEntity> {
let EntityType::Light(l) = entity else {
return None;
};
let w = wpp.filter(|w| *w > 0.0)? as f64;
Some(build(l, w * GLYPH_PX))
}
impl TruckConvertible for Light {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
Some(build(self, FIXED_WORLD))
}
}
impl Grippable for Light {
fn grips(&self) -> Vec<GripDef> {
let p = pos(self);
let mut g = vec![square_grip(0, glam::DVec3::new(p[0], p[1], p[2]))];
if self.is_spot() {
let t = tgt(self);
g.push(center_grip(1, glam::DVec3::new(t[0], t[1], t[2])));
}
g
}
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
match (grip_id, apply) {
// Grip 0 — light source: moves the whole light (aim preserved).
(0, GripApply::Translate(d)) => {
self.position.x += d.x as f64;
self.position.y += d.y as f64;
self.position.z += d.z as f64;
self.target.x += d.x as f64;
self.target.y += d.y as f64;
self.target.z += d.z as f64;
}
(0, GripApply::Absolute(p)) => {
let dx = p.x as f64 - self.position.x;
let dy = p.y as f64 - self.position.y;
let dz = p.z as f64 - self.position.z;
self.position.x += dx;
self.position.y += dy;
self.position.z += dz;
self.target.x += dx;
self.target.y += dy;
self.target.z += dz;
}
// Grip 1 — spot target: re-aims the light.
(1, GripApply::Translate(d)) => {
self.target.x += d.x as f64;
self.target.y += d.y as f64;
self.target.z += d.z as f64;
}
(1, GripApply::Absolute(p)) => {
self.target.x = p.x as f64;
self.target.y = p.y as f64;
self.target.z = p.z as f64;
}
_ => {}
}
}
}
impl PropertyEditable for Light {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
let kind = match self.light_type {
1 => "Distant",
3 => "Spot",
_ => "Point",
};
let mut props = vec![
ro_prop("Name", "li_name", self.name.clone()),
ro_prop("Type", "li_type", kind),
edit("Position X", "li_px", self.position.x),
edit("Position Y", "li_py", self.position.y),
edit("Position Z", "li_pz", self.position.z),
];
if self.light_type != 2 {
props.push(edit("Target X", "li_tx", self.target.x));
props.push(edit("Target Y", "li_ty", self.target.y));
props.push(edit("Target Z", "li_tz", self.target.z));
}
vec![PropSection {
title: "Geometry".into(),
props,
}]
}
fn apply_geom_prop(&mut self, field: &str, value: &str) {
let Ok(v) = value.trim().parse::<f64>() else {
return;
};
match field {
"li_px" => self.position.x = v,
"li_py" => self.position.y = v,
"li_pz" => self.position.z = v,
"li_tx" => self.target.x = v,
"li_ty" => self.target.y = v,
"li_tz" => self.target.z = v,
_ => {}
}
}
}
impl Transformable for Light {
fn apply_transform(&mut self, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(
self,
t,
|entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(
&mut entity.position.x,
&mut entity.position.y,
p1,
p2,
);
crate::scene::view::transform::reflect_xy_point(
&mut entity.target.x,
&mut entity.target.y,
p1,
p2,
);
},
);
}
}
+1
View File
@@ -9,6 +9,7 @@ pub mod hatch;
pub mod helix;
pub mod insert;
pub mod leader;
pub mod light;
pub mod line;
pub mod lwpolyline;
pub mod mesh;
+2
View File
@@ -54,6 +54,7 @@ pub fn ui_name(e: &EntityType) -> &'static str {
EntityType::AttributeDefinition(_) => "Attribute Definition",
EntityType::AttributeEntity(_) => "Attribute",
EntityType::Ole2Frame(_) => "OLE Frame",
EntityType::Light(_) => "Light",
EntityType::Seqend(_) => "Seqend",
EntityType::Unknown(_) => "Unknown",
}
@@ -101,6 +102,7 @@ pub fn dxf_name(e: &EntityType) -> &'static str {
EntityType::Body(_) => "BODY",
EntityType::Surface(_) => "SURFACE",
EntityType::Mesh(_) => "MESH",
EntityType::Light(_) => "LIGHT",
_ => "ENTITY",
}
}
+7 -6
View File
@@ -142,6 +142,7 @@ pub fn entity_type_name(et: &EntityType) -> &'static str {
EntityType::Seqend(_) => "Seqend",
EntityType::Ole2Frame(_) => "Ole2Frame",
EntityType::PolygonMesh(_) => "PolygonMesh",
EntityType::Light(_) => "Light",
EntityType::Unknown(_) => "Unknown",
}
}
@@ -272,7 +273,7 @@ impl EntityTypeOps for EntityType {
Wipeout, AttributeDefinition, AttributeEntity, MLine,
Tolerance, Solid, Face3D, PolygonMesh, PolyfaceMesh, Mesh,
Table, Text, MText, Leader, MultiLeader, Underlay, Shape,
Ole2Frame, Helix,
Ole2Frame, Helix, Light,
],
_ => None,
)
@@ -288,7 +289,7 @@ impl EntityTypeOps for EntityType {
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
Underlay, Shape, Ole2Frame, Helix,
Underlay, Shape, Ole2Frame, Helix, Light,
],
_ => vec![],
)
@@ -304,7 +305,7 @@ impl EntityTypeOps for EntityType {
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
PolyfaceMesh, Mesh, Table, Hatch, Point, Spline, Text, MText,
Viewport, Insert, Dimension, Leader, MultiLeader, Underlay,
Shape, Ole2Frame, Helix,
Shape, Ole2Frame, Helix, Light,
],
_ => vec![],
)
@@ -320,7 +321,7 @@ impl EntityTypeOps for EntityType {
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
PolyfaceMesh, Mesh, Table, Hatch, Point, Spline, Text, MText,
Viewport, Insert, Dimension, Leader, MultiLeader, Underlay,
Shape, Ole2Frame, Helix,
Shape, Ole2Frame, Helix, Light,
],
_ => {},
)
@@ -336,7 +337,7 @@ impl EntityTypeOps for EntityType {
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
Underlay, Shape, Ole2Frame, Helix,
Underlay, Shape, Ole2Frame, Helix, Light,
],
_ => {},
)
@@ -425,7 +426,7 @@ impl EntityTypeOps for EntityType {
Tolerance, Solid, Face3D, PolygonMesh, PolyfaceMesh, Mesh,
Table, MText, Point, Spline, Text, Viewport, Dimension,
Leader, MultiLeader, Underlay, Shape, Ole2Frame,
Solid3D, Region, Body, Surface, Helix,
Solid3D, Region, Body, Surface, Helix, Light,
],
_ => {},
)