forked from 1b-agent/opencadstudio-upstream
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:
1 parent
007d03422e
commit
b30f9b9741
10 files changed
+561
-7
No files matched your search
Generated
+1
-1
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
_ => {},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user