From b30f9b9741dfe84bdd90ccfa5b15d7028a6d3c9c Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 19 Jul 2026 22:15:40 +0300 Subject: [PATCH] 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 --- Cargo.lock | 2 +- src/app/helpers.rs | 1 + src/entities/light.rs | 317 ++++++++++++++++++++++++++++++++ src/entities/mod.rs | 1 + src/entities/names.rs | 2 + src/entities/traits.rs | 13 +- src/scene/camera_ops.rs | 2 + src/scene/convert/tessellate.rs | 1 + src/scene/mod.rs | 7 + src/scene/scene_markers.rs | 222 ++++++++++++++++++++++ 10 files changed, 561 insertions(+), 7 deletions(-) create mode 100644 src/entities/light.rs create mode 100644 src/scene/scene_markers.rs diff --git a/Cargo.lock b/Cargo.lock index dd753cfaa..88ff809b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,7 +73,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.0" -source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#389b31ac5865e72f403d16d81eec958228065af0" +source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#a925f1bf2e929233c79f376ee6aeb9aa4511d262" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/src/app/helpers.rs b/src/app/helpers.rs index 7cca42db9..d34ad4c91 100644 --- a/src/app/helpers.rs +++ b/src/app/helpers.rs @@ -345,6 +345,7 @@ pub(super) fn entity_type_key(entity: &acadrust::EntityType) -> String { AttributeDefinition(_) => "attdef", AttributeEntity(_) => "attrib", Ole2Frame(_) => "ole2frame", + Light(_) => "light", Seqend(_) => "seqend", Unknown(_) => "unknown", } diff --git a/src/entities/light.rs b/src/entities/light.rs new file mode 100644 index 000000000..75df95545 --- /dev/null +++ b/src/entities/light.rs @@ -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, +) -> Option { + 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 { + Some(build(self, FIXED_WORLD)) + } +} + +impl Grippable for Light { + fn grips(&self) -> Vec { + 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 { + 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::() 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, + ); + }, + ); + } +} diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 6df477506..8e9acb215 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -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; diff --git a/src/entities/names.rs b/src/entities/names.rs index da50336f9..0b910eb8f 100644 --- a/src/entities/names.rs +++ b/src/entities/names.rs @@ -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", } } diff --git a/src/entities/traits.rs b/src/entities/traits.rs index 1997e4ad5..1649e5b32 100644 --- a/src/entities/traits.rs +++ b/src/entities/traits.rs @@ -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, ], _ => {}, ) diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 6ee97ded9..116dfc8a5 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -86,6 +86,8 @@ impl Scene { front_clip: 0.0, back_clip: 0.0, twist_angle: 0.0, + // OCS saves an orthographic model view, not a perspective camera. + perspective: false, } } diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 86cbca151..74c54a0b3 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -421,6 +421,7 @@ pub fn tessellate( // Relative-PDSIZE points size their glyph from the current zoom so they // stay a roughly constant on-screen size; otherwise the header-driven path. let te = crate::entities::point::relative_truck(entity, document, world_per_pixel) + .or_else(|| crate::entities::light::relative_truck(entity, document, world_per_pixel)) .or_else(|| convert(entity, document)); if let Some(te) = te { match te.object { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 780de775c..bce1bde9a 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -18,6 +18,7 @@ pub mod view; // blocks and/or free functions). Pure text-move from the original mod.rs. mod entity; mod group_layer; +mod scene_markers; mod camera_ops; mod layout; mod modify; @@ -2915,6 +2916,12 @@ impl Scene { let t_tess = iced::time::Instant::now(); let mut wires = self.wires_for_block_culled(block, None, None, frozen_layers, anno_scale_override); + // Synthesized nonprint markers (geo-location daisy) live in model space + // only and are derived from document objects, not entities — append them + // to the freshly built resident set (incremental patches preserve them). + if block == self.model_space_block_handle() { + self.append_scene_markers(&mut wires, bg); + } self.apply_refedit_fade(&mut wires, bg); self.last_tess_ms.set(t_tess.elapsed().as_secs_f32() * 1000.0); self.last_tess_wires.set(wires.len()); diff --git a/src/scene/scene_markers.rs b/src/scene/scene_markers.rs new file mode 100644 index 000000000..93397fa96 --- /dev/null +++ b/src/scene/scene_markers.rs @@ -0,0 +1,222 @@ +//! 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, _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); + } + } +}