From 1a19a841e6ce21e5c6fb7450c64f0f33fd5f922b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 29 Apr 2026 20:58:44 +0300 Subject: [PATCH] fix: correct polyline arc direction, text precision, Face3D edge color, buffer chunking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: polyline arc center sign was inverted (- → +) causing arcs to render on the wrong side in LwPolyline and Polyline2D - fix: store glyph strokes in local space with f64 origin so world_offset subtraction uses f64 precision; prevents blocky text at large UTM coordinates - fix: Face3D fill darkened to 45% so edge wires are visually distinct from fill - fix: wire batch vertex buffer chunked to 256 MB GPU limit (was panic on large files) - fix: gpu_face3d_edges changed from Option to Vec to support chunks Co-Authored-By: Claude Sonnet 4.6 --- src/entities/attribute.rs | 12 +++--- src/entities/lwpolyline.rs | 8 ++-- src/entities/mtext.rs | 34 ++++++++-------- src/entities/polyline.rs | 4 +- src/entities/text.rs | 21 +++++----- src/entities/tolerance.rs | 17 ++++---- src/scene/acad_to_truck.rs | 10 ++++- src/scene/pipeline/face3d_gpu.rs | 5 ++- src/scene/pipeline/mod.rs | 66 ++++++++++++++++---------------- src/scene/pipeline/wire_gpu.rs | 36 ++++++++++------- src/scene/tessellate.rs | 36 +++++++++-------- 11 files changed, 136 insertions(+), 113 deletions(-) diff --git a/src/entities/attribute.rs b/src/entities/attribute.rs index 1aafe481f..612893910 100644 --- a/src/entities/attribute.rs +++ b/src/entities/attribute.rs @@ -5,7 +5,7 @@ use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::text_support::resolve_text_style; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; +use crate::scene::acad_to_truck::{TextStroke, TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection}; use crate::scene::wire_model::SnapHint; use crate::scene::{cxf, transform}; @@ -26,8 +26,9 @@ impl TruckConvertible for AttributeDefinition { self.default_value.clone() }; let wf = (self.width_factor as f32).max(0.01); + let origin = [self.insertion_point.x, self.insertion_point.y]; let strokes = cxf::tessellate_text_ex( - [self.insertion_point.x as f32, self.insertion_point.y as f32], + [0.0, 0.0], self.height as f32, self.rotation as f32, wf * resolved.width_factor.max(0.01), @@ -36,7 +37,7 @@ impl TruckConvertible for AttributeDefinition { &display, ); Some(TruckEntity { - object: TruckObject::Text(strokes), + object: TruckObject::Text(vec![TextStroke { strokes, origin }]), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], @@ -128,8 +129,9 @@ impl TruckConvertible for AttributeEntity { ); let resolved = resolve_text_style(&self.text_style, document); let wf = (self.width_factor as f32).max(0.01); + let origin = [self.insertion_point.x, self.insertion_point.y]; let strokes = cxf::tessellate_text_ex( - [self.insertion_point.x as f32, self.insertion_point.y as f32], + [0.0, 0.0], self.height as f32, self.rotation as f32, wf * resolved.width_factor.max(0.01), @@ -138,7 +140,7 @@ impl TruckConvertible for AttributeEntity { &self.value, ); Some(TruckEntity { - object: TruckObject::Text(strokes), + object: TruckObject::Text(vec![TextStroke { strokes, origin }]), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index 47d0170b4..c3512eb4d 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -27,8 +27,8 @@ fn arc_midpoint(p0: [f64; 2], p1: [f64; 2], bulge: f64) -> [f64; 2] { let py = dx / d; let sign = if bulge > 0.0 { 1.0_f64 } else { -1.0_f64 }; let h = r - (r * r - d * d / 4.0).max(0.0).sqrt(); - let cx = mx - sign * px * (r - h); - let cy = my - sign * py * (r - h); + let cx = mx + sign * px * (r - h); + let cy = my + sign * py * (r - h); let a0 = (p0[1] - cy).atan2(p0[0] - cx); let a1 = (p1[1] - cy).atan2(p1[0] - cx); let (sa, mut ea) = if bulge > 0.0 { (a0, a1) } else { (a1, a0) }; @@ -112,8 +112,8 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { let py = dx / len; let sagitta_sign = if bulge > 0.0 { 1.0_f64 } else { -1.0_f64 }; let h = r - (r * r - d * d / 4.0).max(0.0).sqrt(); - let cx = mx - sagitta_sign * px * (r - h); - let cy = my - sagitta_sign * py * (r - h); + let cx = mx + sagitta_sign * px * (r - h); + let cy = my + sagitta_sign * py * (r - h); let mid_a = { let a0 = (p0.y - cy).atan2(p0.x - cx); let a1 = (p1.y - cy).atan2(p1.x - cx); diff --git a/src/entities/mtext.rs b/src/entities/mtext.rs index dd2b1fffc..f80c751ff 100644 --- a/src/entities/mtext.rs +++ b/src/entities/mtext.rs @@ -7,7 +7,7 @@ use crate::entities::text_support::{ measure_mtext_chars, resolve_text_style, split_mtext_lines, strip_mtext_codes, word_wrap, }; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; +use crate::scene::acad_to_truck::{TextStroke, TruckEntity, TruckObject}; use crate::scene::cxf; use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::wire_model::SnapHint; @@ -122,26 +122,22 @@ fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity { let vertical_text = matches!(t.drawing_direction, DrawingDirection::TopToBottom); let rot = t.rotation as f32; let (cos_r, sin_r) = (rot.cos(), rot.sin()); - let insertion = Vec3::new( - t.insertion_point.x as f32, - t.insertion_point.y as f32, - t.insertion_point.z as f32, - ); - let mut all_strokes = Vec::new(); + let ins_x = t.insertion_point.x; + let ins_y = t.insertion_point.y; + let insertion = Vec3::new(ins_x as f32, ins_y as f32, t.insertion_point.z as f32); + let mut all_strokes: Vec = Vec::new(); for (i, line) in lines.iter().enumerate() { let li = i as f32; - let (ox, oy) = if vertical_text { + // Compute line offset as f32 (small relative values), apply to f64 insertion point. + let (small_x, small_y) = if vertical_text { let col_offset = li * t.height as f32 * 1.2; ( - t.insertion_point.x as f32 + col_offset * cos_r + v_offset * (-sin_r), - t.insertion_point.y as f32 + col_offset * sin_r + v_offset * cos_r, + col_offset * cos_r + v_offset * (-sin_r), + col_offset * sin_r + v_offset * cos_r, ) } else { let line_y = -(li * line_h) + v_offset; - ( - t.insertion_point.x as f32 + line_y * (-sin_r), - t.insertion_point.y as f32 + line_y * cos_r, - ) + (line_y * (-sin_r), line_y * cos_r) }; let line_w = if h_anchor > 0.0 { let scale = t.height as f32 / 9.0 * style_width_factor; @@ -150,10 +146,12 @@ fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity { 0.0 }; let h_shift = -line_w * h_anchor; - let origin_x = ox + h_shift * cos_r; - let origin_y = oy + h_shift * sin_r; + let origin: [f64; 2] = [ + ins_x + (small_x + h_shift * cos_r) as f64, + ins_y + (small_y + h_shift * sin_r) as f64, + ]; let strokes = cxf::tessellate_text_ex( - [origin_x, origin_y], + [0.0, 0.0], t.height as f32, rot, style_width_factor, @@ -161,7 +159,7 @@ fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity { &font_name, line, ); - all_strokes.extend(strokes); + all_strokes.push(TextStroke { strokes, origin }); } TruckEntity { object: TruckObject::Text(all_strokes), diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index 9bcfd38d6..ef3df1eff 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -169,8 +169,8 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { let py = dx / len; let sagitta_sign = if bulge > 0.0 { 1.0_f64 } else { -1.0_f64 }; let h = r - (r * r - d * d / 4.0).max(0.0).sqrt(); - let cx = mx - sagitta_sign * px * (r - h); - let cy = my - sagitta_sign * py * (r - h); + let cx = mx + sagitta_sign * px * (r - h); + let cy = my + sagitta_sign * py * (r - h); let mid_a = { let a0 = (p0.y - cy).atan2(p0.x - cx); let a1 = (p1.y - cy).atan2(p1.x - cx); diff --git a/src/entities/text.rs b/src/entities/text.rs index 74c7ad1e1..918c7fb0f 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -5,7 +5,7 @@ use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, parse_f64, square_grip}; use crate::entities::text_support::{resolve_dxf_special_chars, resolve_text_style, text_local_bounds}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; +use crate::scene::acad_to_truck::{TextStroke, TruckEntity, TruckObject}; use crate::scene::cxf; use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::wire_model::SnapHint; @@ -101,14 +101,17 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { } else { (0.0, 0.0) }; - let (cos_r, sin_r) = (rotation.cos(), rotation.sin()); - let origin = [ - anchor[0] - (anchor_local_x * cos_r - anchor_local_y * sin_r), - anchor[1] - (anchor_local_x * sin_r + anchor_local_y * cos_r), + let (cos_r, sin_r) = (rotation.cos() as f64, rotation.sin() as f64); + // Keep origin as f64 — large coordinates (UTM etc.) must not be cast to + // f32 here; world_offset subtraction happens later in tessellate.rs. + let anchor_f64 = [anchor[0] as f64, anchor[1] as f64]; + let origin: [f64; 2] = [ + anchor_f64[0] - (anchor_local_x as f64 * cos_r - anchor_local_y as f64 * sin_r), + anchor_f64[1] - (anchor_local_x as f64 * sin_r + anchor_local_y as f64 * cos_r), ]; - // Pass raw value — tessellate_text_ex resolves %%x codes and emits decoration strokes. - let strokes_2d = cxf::tessellate_text_ex( - origin, + // Strokes are in glyph-local space (origin = [0,0]). + let strokes = cxf::tessellate_text_ex( + [0.0, 0.0], t.height as f32, rotation, width_factor, @@ -117,7 +120,7 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { &t.value, ); TruckEntity { - object: TruckObject::Text(strokes_2d), + object: TruckObject::Text(vec![TextStroke { strokes, origin }]), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], diff --git a/src/entities/tolerance.rs b/src/entities/tolerance.rs index b0f5645cb..cc8c445ef 100644 --- a/src/entities/tolerance.rs +++ b/src/entities/tolerance.rs @@ -4,7 +4,7 @@ use glam::Vec3; use crate::command::EntityTransform; use crate::entities::common::{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::acad_to_truck::{TextStroke, TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection}; use crate::scene::wire_model::SnapHint; use crate::scene::{cxf, transform}; @@ -138,14 +138,13 @@ fn tessellate_tolerance(tol: &Tolerance) -> Vec> { let total_w: f32 = col_widths.iter().sum(); let total_h = cell_h * rows.len() as f32; - // ── Transform helpers (world 2-D, ignoring Z for Text path) ────────── - let ox = tol.insertion_point.x as f32; - let oy = tol.insertion_point.y as f32; + // ── Transform helpers (local space — translation applied in tessellate.rs) ── let angle = (tol.direction.y as f32).atan2(tol.direction.x as f32); let (sa, ca) = angle.sin_cos(); + // Rotate only; origin is kept as f64 and applied later with full precision. let rot = |x: f32, y: f32| -> [f32; 2] { - [ox + x * ca - y * sa, oy + x * sa + y * ca] + [x * ca - y * sa, x * sa + y * ca] }; let mut out: Vec> = Vec::new(); @@ -228,12 +227,12 @@ impl TruckConvertible for Tolerance { self.insertion_point.z as f32, ); - // Build the feature-control frame: box outline + cell text, all - // represented as 2-D CXF polylines (TruckObject::Text). - let polylines = tessellate_tolerance(self); + // Build the feature-control frame in local space; origin stored as f64. + let strokes = tessellate_tolerance(self); + let origin = [self.insertion_point.x, self.insertion_point.y]; Some(TruckEntity { - object: TruckObject::Text(polylines), + object: TruckObject::Text(vec![TextStroke { strokes, origin }]), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], diff --git a/src/scene/acad_to_truck.rs b/src/scene/acad_to_truck.rs index 9081ceb0f..b7f05e6e3 100644 --- a/src/scene/acad_to_truck.rs +++ b/src/scene/acad_to_truck.rs @@ -7,12 +7,20 @@ use truck_modeling::{Edge, Solid, Vertex, Wire}; use crate::entities::traits::EntityTypeOps; use crate::scene::wire_model::{SnapHint, TangentGeom}; +/// One group of glyph strokes with its world-space origin stored in f64. +/// Strokes are in glyph-local space (origin = [0,0]) so that the large +/// world offset can be subtracted with f64 precision in tessellate.rs. +pub struct TextStroke { + pub strokes: Vec>, + pub origin: [f64; 2], +} + #[allow(dead_code)] pub enum TruckObject { Point(Vertex), Curve(Edge), Contour(Wire), - Text(Vec>), + Text(Vec), /// Pre-computed NaN-separated 3-D point list (leader lines, arrowheads, etc.). Lines(Vec<[f32; 3]>), Volume(Solid), diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index c39bd0cda..2695fdd9d 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -65,9 +65,10 @@ impl Face3DGpu { if wire.key_vertices.len() < 4 { continue; } - let color = wire.color; + let [r, g, b, a] = wire.color; + let fill_color = [r * 0.45, g * 0.45, b * 0.45, a]; let p = &wire.key_vertices; - let v = |i: usize| Face3DVertex { position: p[i], color }; + let v = |i: usize| Face3DVertex { position: p[i], color: fill_color }; // Triangle 1: p0, p1, p2 vertices.push(v(0)); diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index ed49d4bec..3533731ec 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -61,7 +61,7 @@ pub struct Pipeline { gpu_meshes: Vec, /// Batched 3DFACE fill (all faces in one buffer) and edges (merged wire). gpu_face3d_fill: Option, - gpu_face3d_edges: Option, + gpu_face3d_edges: Vec, pub viewcube: ViewCubePipeline, /// Last geometry epoch for which GPU buffers were uploaded. /// Initialized to u64::MAX so the first frame always uploads. @@ -590,7 +590,7 @@ impl Pipeline { gpu_images: vec![], gpu_meshes: vec![], gpu_face3d_fill: None, - gpu_face3d_edges: None, + gpu_face3d_edges: vec![], viewcube, cached_epoch: u64::MAX, } @@ -613,7 +613,7 @@ impl Pipeline { pub fn upload_face3d(&mut self, device: &wgpu::Device, face3d_wires: &[WireModel]) { if face3d_wires.is_empty() { self.gpu_face3d_fill = None; - self.gpu_face3d_edges = None; + self.gpu_face3d_edges = vec![]; return; } self.gpu_face3d_fill = Some(Face3DGpu::from_wires(device, face3d_wires)); @@ -813,36 +813,38 @@ impl Pipeline { } } - // ── Pass 5b: 3DFACE edges (batched) ────────────────────────────── - if let Some(ref edges) = self.gpu_face3d_edges { - if edges.vertex_count >= 6 { - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("face3d_edges.render_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: msaa, - depth_slice: None, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &self.depth_view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, + // ── Pass 5b: 3DFACE edges (batched, possibly multiple chunks) ──── + if !self.gpu_face3d_edges.is_empty() { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("face3d_edges.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: msaa, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, }), - timestamp_writes: None, - occlusion_query_set: None, - }); - pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0); - pass.set_pipeline(&self.wire_pipeline); - pass.set_bind_group(0, &self.uniform_bind_group, &[]); - pass.set_vertex_buffer(0, edges.vertex_buffer.slice(..)); - pass.draw(0..edges.vertex_count, 0..1); + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0); + pass.set_pipeline(&self.wire_pipeline); + pass.set_bind_group(0, &self.uniform_bind_group, &[]); + for edges in &self.gpu_face3d_edges { + if edges.vertex_count >= 6 { + pass.set_vertex_buffer(0, edges.vertex_buffer.slice(..)); + pass.draw(0..edges.vertex_count, 0..1); + } } } diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 1c117b7db..9dc59bb11 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -125,13 +125,13 @@ impl WireGpu { Self::build(device, wire, [r, g, b, a * alpha]) } - /// Merge multiple WireModels into a single GPU buffer (1 draw call for all). + /// Merge multiple WireModels into GPU buffers chunked to fit the 256 MB GPU limit. /// Each wire keeps its own color and pattern — they're stored per-vertex. - /// Returns None if the combined vertex list is empty. - pub fn from_batch(device: &wgpu::Device, wires: &[WireModel]) -> Option { + /// Returns an empty Vec if the combined vertex list is empty. + pub fn from_batch(device: &wgpu::Device, wires: &[WireModel]) -> Vec { let total_segs: usize = wires.iter().map(|w| w.points.len().saturating_sub(1)).sum(); if total_segs == 0 { - return None; + return vec![]; } let mut vertices: Vec = Vec::with_capacity(total_segs * 6); @@ -186,19 +186,27 @@ impl WireGpu { } if vertices.is_empty() { - return None; + return vec![]; } - let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("wire.batch.vbuf"), - contents: bytemuck::cast_slice(&vertices), - usage: wgpu::BufferUsages::VERTEX, - }); + // GPU max buffer size is 256 MB; chunk to stay within the limit. + const MAX_VERTS: usize = 268_435_456 / std::mem::size_of::(); - Some(Self { - vertex_buffer, - vertex_count: vertices.len() as u32, - }) + vertices + .chunks(MAX_VERTS) + .enumerate() + .map(|(i, chunk)| { + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("wire.batch.vbuf.{i}")), + contents: bytemuck::cast_slice(chunk), + usage: wgpu::BufferUsages::VERTEX, + }); + Self { + vertex_buffer, + vertex_count: chunk.len() as u32, + } + }) + .collect() } fn build(device: &wgpu::Device, wire: &WireModel, color: [f32; 4]) -> Self { diff --git a/src/scene/tessellate.rs b/src/scene/tessellate.rs index 1dedb5d4f..95ca05607 100644 --- a/src/scene/tessellate.rs +++ b/src/scene/tessellate.rs @@ -64,27 +64,29 @@ pub fn tessellate( if let Some(te) = convert(entity, document) { match te.object { // ── Text / MText: pre-tessellated glyph strokes ─────────────── - TruckObject::Text(strokes_2d) => { - // Glyph strokes come from acad_to_truck in world-space f32. - // Subtract world_offset (f32 subtraction — acceptable precision - // for text rendering; the anchor was already cast to f32 there). + TruckObject::Text(stroke_groups) => { + // Each TextStroke keeps its strokes in glyph-local space and + // its world origin as f64. Subtract world_offset in f64 before + // casting to f32 so large UTM coordinates don't crush precision. let [ox, oy, oz] = world_offset; let elev = entity_z(entity) - oz as f32; - // Pack all strokes into one flat point list, separated by - // NaN sentinels so wire_gpu.rs skips disconnected segments. let mut points: Vec<[f32; 3]> = Vec::new(); - for (i, stroke) in strokes_2d.iter().enumerate() { - if stroke.len() < 2 { - continue; - } - if i > 0 && !points.is_empty() { - // NaN sentinel — wire_gpu skips any segment where - // either endpoint contains NaN. - points.push([f32::NAN, f32::NAN, f32::NAN]); - } - for &[x, y] in stroke { - points.push([x - ox as f32, y - oy as f32, elev]); + let mut first = true; + for group in &stroke_groups { + let lx = (group.origin[0] - ox) as f32; + let ly = (group.origin[1] - oy) as f32; + for stroke in &group.strokes { + if stroke.len() < 2 { + continue; + } + if !first && !points.is_empty() { + points.push([f32::NAN, f32::NAN, f32::NAN]); + } + first = false; + for &[x, y] in stroke { + points.push([x + lx, y + ly, elev]); + } } }