Feat: Phase 4.3/4.4/4.5 — RasterImage, Wipeout, AttributeDefinition, AttributeEntity
- raster_image.rs: RasterImage renders as border rectangle + X diagonals
(pixel placeholder); Wipeout renders border or polygon clip boundary;
both support insertion_point grip, brightness/contrast/fade properties,
clipping toggle, and mirror transform (u/v vectors reflected)
- attribute.rs: AttributeDefinition renders default_value (or [TAG] if
empty) as font-tessellated text; AttributeEntity renders the live value;
both support insertion grip, position/height/rotation properties,
and transform operations
- mod.rs + traits.rs: wire up all four new entity types through the full
dispatch chain (to_truck, grips, geometry_properties, apply_geom_prop,
apply_grip, apply_transform)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:00:15 +03:00
|
|
|
use acadrust::entities::{RasterImage, Wipeout};
|
|
|
|
|
use glam::Vec3;
|
|
|
|
|
|
|
|
|
|
use crate::command::EntityTransform;
|
|
|
|
|
use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip};
|
|
|
|
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
|
|
|
|
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
|
|
|
|
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
|
|
|
|
|
|
|
|
|
// ── Shared geometry helpers ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Compute the four world-space corners of an image/wipeout from its
|
|
|
|
|
/// insertion_point, u_vector, v_vector and pixel size.
|
|
|
|
|
///
|
|
|
|
|
/// Returns (p0, p1, p2, p3) in counter-clockwise order:
|
|
|
|
|
/// p0 = origin
|
|
|
|
|
/// p1 = origin + U*W
|
|
|
|
|
/// p2 = origin + U*W + V*H
|
|
|
|
|
/// p3 = origin + V*H
|
|
|
|
|
fn image_corners(
|
|
|
|
|
origin: &acadrust::types::Vector3,
|
|
|
|
|
u: &acadrust::types::Vector3,
|
|
|
|
|
v: &acadrust::types::Vector3,
|
|
|
|
|
w: f64,
|
|
|
|
|
h: f64,
|
|
|
|
|
) -> [[f32; 3]; 4] {
|
|
|
|
|
let ox = origin.x as f32;
|
|
|
|
|
let oy = origin.y as f32;
|
|
|
|
|
let oz = origin.z as f32;
|
|
|
|
|
let ux = (u.x * w) as f32;
|
|
|
|
|
let uy = (u.y * w) as f32;
|
|
|
|
|
let uz = (u.z * w) as f32;
|
|
|
|
|
let vx = (v.x * h) as f32;
|
|
|
|
|
let vy = (v.y * h) as f32;
|
|
|
|
|
let vz = (v.z * h) as f32;
|
|
|
|
|
|
|
|
|
|
[
|
|
|
|
|
[ox, oy, oz],
|
|
|
|
|
[ox + ux, oy + uy, oz + uz],
|
|
|
|
|
[ox + ux + vx, oy + uy + vy, oz + uz + vz],
|
|
|
|
|
[ox + vx, oy + vy, oz + vz],
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Rectangle border + X diagonals — used as a placeholder for images.
|
|
|
|
|
fn image_wire(corners: [[f32; 3]; 4], with_x: bool) -> Vec<[f32; 3]> {
|
|
|
|
|
let [p0, p1, p2, p3] = corners;
|
|
|
|
|
let mut pts = vec![p0, p1, p2, p3, p0];
|
|
|
|
|
if with_x {
|
|
|
|
|
pts.push([f32::NAN; 3]);
|
|
|
|
|
pts.push(p0);
|
|
|
|
|
pts.push(p2);
|
|
|
|
|
pts.push([f32::NAN; 3]);
|
|
|
|
|
pts.push(p1);
|
|
|
|
|
pts.push(p3);
|
|
|
|
|
}
|
|
|
|
|
pts
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn reflect_vec3(
|
|
|
|
|
vx: &mut f64,
|
|
|
|
|
vy: &mut f64,
|
|
|
|
|
ax: f64,
|
|
|
|
|
ay: f64,
|
|
|
|
|
len2: f64,
|
|
|
|
|
) {
|
|
|
|
|
let dot = *vx * ax + *vy * ay;
|
|
|
|
|
*vx = 2.0 * dot * ax / len2 - *vx;
|
|
|
|
|
*vy = 2.0 * dot * ay / len2 - *vy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── RasterImage ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
impl TruckConvertible for RasterImage {
|
|
|
|
|
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
|
|
|
|
let corners = image_corners(
|
|
|
|
|
&self.insertion_point,
|
|
|
|
|
&self.u_vector,
|
|
|
|
|
&self.v_vector,
|
|
|
|
|
self.size.x,
|
|
|
|
|
self.size.y,
|
|
|
|
|
);
|
|
|
|
|
Some(TruckEntity {
|
|
|
|
|
object: TruckObject::Lines(image_wire(corners, true)),
|
|
|
|
|
snap_pts: vec![],
|
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
key_vertices: corners.to_vec(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Grippable for RasterImage {
|
|
|
|
|
fn grips(&self) -> Vec<GripDef> {
|
|
|
|
|
let corners = image_corners(
|
|
|
|
|
&self.insertion_point,
|
|
|
|
|
&self.u_vector,
|
|
|
|
|
&self.v_vector,
|
|
|
|
|
self.size.x,
|
|
|
|
|
self.size.y,
|
|
|
|
|
);
|
|
|
|
|
vec![
|
|
|
|
|
square_grip(0, Vec3::from(corners[0])),
|
|
|
|
|
diamond_grip(1, Vec3::from(corners[1])),
|
|
|
|
|
diamond_grip(2, Vec3::from(corners[2])),
|
|
|
|
|
diamond_grip(3, Vec3::from(corners[3])),
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
|
|
|
|
if grip_id == 0 {
|
|
|
|
|
match apply {
|
|
|
|
|
GripApply::Translate(d) => {
|
|
|
|
|
self.insertion_point.x += d.x as f64;
|
|
|
|
|
self.insertion_point.y += d.y as f64;
|
|
|
|
|
self.insertion_point.z += d.z as f64;
|
|
|
|
|
}
|
|
|
|
|
GripApply::Absolute(p) => {
|
|
|
|
|
self.insertion_point.x = p.x as f64;
|
|
|
|
|
self.insertion_point.y = p.y as f64;
|
|
|
|
|
self.insertion_point.z = p.z as f64;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Corner grips 1-3 are display-only (resizing changes u/v vectors,
|
|
|
|
|
// which requires careful normalization — deferred).
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PropertyEditable for RasterImage {
|
|
|
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
|
|
|
|
PropSection {
|
|
|
|
|
title: "Geometry".into(),
|
|
|
|
|
props: vec![
|
|
|
|
|
ro("File", "ri_file", self.file_path.clone()),
|
|
|
|
|
edit("Insert X", "ri_ox", self.insertion_point.x),
|
|
|
|
|
edit("Insert Y", "ri_oy", self.insertion_point.y),
|
|
|
|
|
edit("Insert Z", "ri_oz", self.insertion_point.z),
|
|
|
|
|
edit("Brightness", "ri_bright", self.brightness as f64),
|
|
|
|
|
edit("Contrast", "ri_contrast", self.contrast as f64),
|
|
|
|
|
edit("Fade", "ri_fade", self.fade as f64),
|
|
|
|
|
Property {
|
|
|
|
|
label: "Clipping".into(),
|
|
|
|
|
field: "ri_clip",
|
|
|
|
|
value: PropValue::BoolToggle {
|
|
|
|
|
field: "ri_clip",
|
|
|
|
|
value: self.clipping_enabled,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
|
|
|
|
match field {
|
|
|
|
|
"ri_clip" => {
|
|
|
|
|
self.clipping_enabled =
|
|
|
|
|
if value == "toggle" { !self.clipping_enabled } else { value == "true" };
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
let Ok(v) = value.trim().parse::<f64>() else { return };
|
|
|
|
|
match field {
|
|
|
|
|
"ri_ox" => self.insertion_point.x = v,
|
|
|
|
|
"ri_oy" => self.insertion_point.y = v,
|
|
|
|
|
"ri_oz" => self.insertion_point.z = v,
|
|
|
|
|
"ri_bright" => self.brightness = v.clamp(0.0, 100.0) as u8,
|
|
|
|
|
"ri_contrast" => self.contrast = v.clamp(0.0, 100.0) as u8,
|
|
|
|
|
"ri_fade" => self.fade = v.clamp(0.0, 100.0) as u8,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Transformable for RasterImage {
|
|
|
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
|
|
|
|
crate::scene::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
|
|
|
|
|
crate::scene::transform::reflect_xy_point(
|
|
|
|
|
&mut entity.insertion_point.x,
|
|
|
|
|
&mut entity.insertion_point.y,
|
|
|
|
|
p1,
|
|
|
|
|
p2,
|
|
|
|
|
);
|
|
|
|
|
let ax = (p2.x - p1.x) as f64;
|
|
|
|
|
let ay = (p2.y - p1.y) as f64;
|
|
|
|
|
let len2 = ax * ax + ay * ay;
|
|
|
|
|
if len2 > 1e-12 {
|
|
|
|
|
reflect_vec3(&mut entity.u_vector.x, &mut entity.u_vector.y, ax, ay, len2);
|
|
|
|
|
reflect_vec3(&mut entity.v_vector.x, &mut entity.v_vector.y, ax, ay, len2);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Wipeout ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
impl TruckConvertible for Wipeout {
|
|
|
|
|
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
|
|
|
|
let corners = image_corners(
|
|
|
|
|
&self.insertion_point,
|
|
|
|
|
&self.u_vector,
|
|
|
|
|
&self.v_vector,
|
|
|
|
|
self.size.x,
|
|
|
|
|
self.size.y,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// If clipping is enabled and there's a polygon boundary, show that.
|
|
|
|
|
let pts = if self.clipping_enabled
|
|
|
|
|
&& self.clip_boundary_vertices.len() >= 3
|
|
|
|
|
&& matches!(
|
|
|
|
|
self.clip_type,
|
|
|
|
|
acadrust::entities::WipeoutClipType::Polygonal
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
// Convert pixel-space boundary vertices to world space:
|
|
|
|
|
// world = insertion_point + u_vector * v.x * size.x + v_vector * v.y * size.y
|
|
|
|
|
let ox = self.insertion_point.x as f32;
|
|
|
|
|
let oy = self.insertion_point.y as f32;
|
|
|
|
|
let oz = self.insertion_point.z as f32;
|
|
|
|
|
let mut poly: Vec<[f32; 3]> = self
|
|
|
|
|
.clip_boundary_vertices
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|v| {
|
|
|
|
|
let wx = (self.u_vector.x * v.x * self.size.x
|
|
|
|
|
+ self.v_vector.x * v.y * self.size.y) as f32;
|
|
|
|
|
let wy = (self.u_vector.y * v.x * self.size.x
|
|
|
|
|
+ self.v_vector.y * v.y * self.size.y) as f32;
|
|
|
|
|
let wz = (self.u_vector.z * v.x * self.size.x
|
|
|
|
|
+ self.v_vector.z * v.y * self.size.y) as f32;
|
|
|
|
|
[ox + wx, oy + wy, oz + wz]
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
// Close the polygon.
|
|
|
|
|
if let Some(&first) = poly.first() {
|
|
|
|
|
poly.push(first);
|
|
|
|
|
}
|
|
|
|
|
poly
|
|
|
|
|
} else {
|
|
|
|
|
// Rectangular boundary — just the border, no diagonals (mask area).
|
|
|
|
|
image_wire(corners, false)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Some(TruckEntity {
|
|
|
|
|
object: TruckObject::Lines(pts),
|
|
|
|
|
snap_pts: vec![],
|
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
key_vertices: corners.to_vec(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Grippable for Wipeout {
|
|
|
|
|
fn grips(&self) -> Vec<GripDef> {
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
// If polygonal clipping is active, expose individual polygon vertices as grips.
|
|
|
|
|
let is_polygon = self.clipping_enabled
|
|
|
|
|
&& self.clip_boundary_vertices.len() >= 3
|
|
|
|
|
&& matches!(
|
|
|
|
|
self.clip_type,
|
|
|
|
|
acadrust::entities::WipeoutClipType::Polygonal
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if is_polygon {
|
|
|
|
|
let ox = self.insertion_point.x as f32;
|
|
|
|
|
let oy = self.insertion_point.y as f32;
|
|
|
|
|
let oz = self.insertion_point.z as f32;
|
|
|
|
|
self.clip_boundary_vertices
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.map(|(i, v)| {
|
|
|
|
|
let wx = (self.u_vector.x * v.x * self.size.x
|
|
|
|
|
+ self.v_vector.x * v.y * self.size.y) as f32;
|
|
|
|
|
let wy = (self.u_vector.y * v.x * self.size.x
|
|
|
|
|
+ self.v_vector.y * v.y * self.size.y) as f32;
|
|
|
|
|
let wz = (self.u_vector.z * v.x * self.size.x
|
|
|
|
|
+ self.v_vector.z * v.y * self.size.y) as f32;
|
|
|
|
|
if i == 0 {
|
|
|
|
|
square_grip(i, Vec3::new(ox + wx, oy + wy, oz + wz))
|
|
|
|
|
} else {
|
|
|
|
|
diamond_grip(i, Vec3::new(ox + wx, oy + wy, oz + wz))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
} else {
|
|
|
|
|
let corners = image_corners(
|
|
|
|
|
&self.insertion_point,
|
|
|
|
|
&self.u_vector,
|
|
|
|
|
&self.v_vector,
|
|
|
|
|
self.size.x,
|
|
|
|
|
self.size.y,
|
|
|
|
|
);
|
|
|
|
|
vec![
|
|
|
|
|
square_grip(0, Vec3::from(corners[0])),
|
|
|
|
|
diamond_grip(1, Vec3::from(corners[1])),
|
|
|
|
|
diamond_grip(2, Vec3::from(corners[2])),
|
|
|
|
|
diamond_grip(3, Vec3::from(corners[3])),
|
|
|
|
|
]
|
|
|
|
|
}
|
Feat: Phase 4.3/4.4/4.5 — RasterImage, Wipeout, AttributeDefinition, AttributeEntity
- raster_image.rs: RasterImage renders as border rectangle + X diagonals
(pixel placeholder); Wipeout renders border or polygon clip boundary;
both support insertion_point grip, brightness/contrast/fade properties,
clipping toggle, and mirror transform (u/v vectors reflected)
- attribute.rs: AttributeDefinition renders default_value (or [TAG] if
empty) as font-tessellated text; AttributeEntity renders the live value;
both support insertion grip, position/height/rotation properties,
and transform operations
- mod.rs + traits.rs: wire up all four new entity types through the full
dispatch chain (to_truck, grips, geometry_properties, apply_geom_prop,
apply_grip, apply_transform)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:00:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
let is_polygon = self.clipping_enabled
|
|
|
|
|
&& self.clip_boundary_vertices.len() >= 3
|
|
|
|
|
&& matches!(
|
|
|
|
|
self.clip_type,
|
|
|
|
|
acadrust::entities::WipeoutClipType::Polygonal
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if is_polygon {
|
|
|
|
|
// Move the clicked polygon vertex in world space → back-project to pixel space.
|
|
|
|
|
if let Some(v) = self.clip_boundary_vertices.get_mut(grip_id) {
|
|
|
|
|
// Compute current world position of this vertex.
|
|
|
|
|
let ox = self.insertion_point.x;
|
|
|
|
|
let oy = self.insertion_point.y;
|
|
|
|
|
let oz = self.insertion_point.z;
|
|
|
|
|
let cur_wx = ox + self.u_vector.x * v.x * self.size.x + self.v_vector.x * v.y * self.size.y;
|
|
|
|
|
let cur_wy = oy + self.u_vector.y * v.x * self.size.x + self.v_vector.y * v.y * self.size.y;
|
|
|
|
|
let cur_wz = oz + self.u_vector.z * v.x * self.size.x + self.v_vector.z * v.y * self.size.y;
|
|
|
|
|
let new_w = match apply {
|
|
|
|
|
GripApply::Translate(d) => {
|
|
|
|
|
[cur_wx + d.x as f64, cur_wy + d.y as f64, cur_wz + d.z as f64]
|
|
|
|
|
}
|
|
|
|
|
GripApply::Absolute(p) => [p.x as f64, p.y as f64, p.z as f64],
|
|
|
|
|
};
|
|
|
|
|
// Back-project: solve for pixel coords using u_vector and v_vector.
|
|
|
|
|
// In 2D (XY plane): new_w - insertion_point = u_vec * vx * sx + v_vec * vy * sy
|
|
|
|
|
let dx = new_w[0] - self.insertion_point.x;
|
|
|
|
|
let dy = new_w[1] - self.insertion_point.y;
|
|
|
|
|
let ux = self.u_vector.x * self.size.x;
|
|
|
|
|
let uy = self.u_vector.y * self.size.x;
|
|
|
|
|
let vx = self.v_vector.x * self.size.y;
|
|
|
|
|
let vy = self.v_vector.y * self.size.y;
|
|
|
|
|
let det = ux * vy - uy * vx;
|
|
|
|
|
if det.abs() > 1e-12 {
|
|
|
|
|
v.x = (dx * vy - dy * vx) / det;
|
|
|
|
|
v.y = (ux * dy - uy * dx) / det;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if grip_id == 0 {
|
Feat: Phase 4.3/4.4/4.5 — RasterImage, Wipeout, AttributeDefinition, AttributeEntity
- raster_image.rs: RasterImage renders as border rectangle + X diagonals
(pixel placeholder); Wipeout renders border or polygon clip boundary;
both support insertion_point grip, brightness/contrast/fade properties,
clipping toggle, and mirror transform (u/v vectors reflected)
- attribute.rs: AttributeDefinition renders default_value (or [TAG] if
empty) as font-tessellated text; AttributeEntity renders the live value;
both support insertion grip, position/height/rotation properties,
and transform operations
- mod.rs + traits.rs: wire up all four new entity types through the full
dispatch chain (to_truck, grips, geometry_properties, apply_geom_prop,
apply_grip, apply_transform)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:00:15 +03:00
|
|
|
match apply {
|
|
|
|
|
GripApply::Translate(d) => {
|
|
|
|
|
self.insertion_point.x += d.x as f64;
|
|
|
|
|
self.insertion_point.y += d.y as f64;
|
|
|
|
|
self.insertion_point.z += d.z as f64;
|
|
|
|
|
}
|
|
|
|
|
GripApply::Absolute(p) => {
|
|
|
|
|
self.insertion_point.x = p.x as f64;
|
|
|
|
|
self.insertion_point.y = p.y as f64;
|
|
|
|
|
self.insertion_point.z = p.z as f64;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PropertyEditable for Wipeout {
|
|
|
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
|
|
|
|
PropSection {
|
|
|
|
|
title: "Geometry".into(),
|
|
|
|
|
props: vec![
|
|
|
|
|
edit("Insert X", "wo_ox", self.insertion_point.x),
|
|
|
|
|
edit("Insert Y", "wo_oy", self.insertion_point.y),
|
|
|
|
|
edit("Insert Z", "wo_oz", self.insertion_point.z),
|
|
|
|
|
edit("Brightness", "wo_bright", self.brightness as f64),
|
|
|
|
|
edit("Contrast", "wo_contrast", self.contrast as f64),
|
|
|
|
|
edit("Fade", "wo_fade", self.fade as f64),
|
|
|
|
|
Property {
|
|
|
|
|
label: "Clipping".into(),
|
|
|
|
|
field: "wo_clip",
|
|
|
|
|
value: PropValue::BoolToggle {
|
|
|
|
|
field: "wo_clip",
|
|
|
|
|
value: self.clipping_enabled,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
|
|
|
|
match field {
|
|
|
|
|
"wo_clip" => {
|
|
|
|
|
self.clipping_enabled =
|
|
|
|
|
if value == "toggle" { !self.clipping_enabled } else { value == "true" };
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
let Ok(v) = value.trim().parse::<f64>() else { return };
|
|
|
|
|
match field {
|
|
|
|
|
"wo_ox" => self.insertion_point.x = v,
|
|
|
|
|
"wo_oy" => self.insertion_point.y = v,
|
|
|
|
|
"wo_oz" => self.insertion_point.z = v,
|
|
|
|
|
"wo_bright" => self.brightness = v.clamp(0.0, 100.0) as u8,
|
|
|
|
|
"wo_contrast" => self.contrast = v.clamp(0.0, 100.0) as u8,
|
|
|
|
|
"wo_fade" => self.fade = v.clamp(0.0, 100.0) as u8,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Transformable for Wipeout {
|
|
|
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
|
|
|
|
crate::scene::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
|
|
|
|
|
crate::scene::transform::reflect_xy_point(
|
|
|
|
|
&mut entity.insertion_point.x,
|
|
|
|
|
&mut entity.insertion_point.y,
|
|
|
|
|
p1,
|
|
|
|
|
p2,
|
|
|
|
|
);
|
|
|
|
|
let ax = (p2.x - p1.x) as f64;
|
|
|
|
|
let ay = (p2.y - p1.y) as f64;
|
|
|
|
|
let len2 = ax * ax + ay * ay;
|
|
|
|
|
if len2 > 1e-12 {
|
|
|
|
|
reflect_vec3(&mut entity.u_vector.x, &mut entity.u_vector.y, ax, ay, len2);
|
|
|
|
|
reflect_vec3(&mut entity.v_vector.x, &mut entity.v_vector.y, ax, ay, len2);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|