From 5b9fffdfba7a2f6b4b0c8e7cafde442e0fe67e66 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 13 Aug 2026 00:28:50 +0300 Subject: [PATCH 01/38] fix(hatch): clip fill with kernel mesh Resolve boundary topology before rendering so corner-aligned pattern rows cannot flip shader parity. Chunk indexed geometry to device buffer limits and retain the texture compatibility transport. Refs #489 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/scene/model/hatch_model.rs | 41 ++- src/scene/pipeline/device_capabilities.rs | 5 +- src/scene/pipeline/hatch_gpu/mod.rs | 62 +++-- src/scene/pipeline/hatch_gpu/storage.rs | 305 ++++++++++------------ src/scene/pipeline/hatch_gpu/texture.rs | 89 ++----- src/shaders/hatch.wgsl | 103 +------- src/shaders/hatch_texture.wgsl | 66 +---- 9 files changed, 259 insertions(+), 416 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 736d70c4f..cd87836de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -891,7 +891,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1b84d6c#1b84d6c926d8053690a9975ccad6814800790008" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=8d05593#8d05593221cf6158076c38e98fef54ab767b81d9" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 38101daaa..42a3ec4c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4736846", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1b84d6c", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "8d05593", features = ["acis", "offset"] } acadifc = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "47b5603", optional = true } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs index 641708f22..492ce02cd 100644 --- a/src/scene/model/hatch_model.rs +++ b/src/scene/model/hatch_model.rs @@ -1,8 +1,5 @@ -// HatchModel — CPU-side hatch fill data; rendered entirely on the GPU. -// -// The boundary is a closed polygon in world XY coordinates. -// The GPU fragment shader performs point-in-polygon and hatch-line tests so -// no line geometry is ever tessellated on the CPU. +// CPU-side hatch fill data. The kernel triangulates the boundary and the GPU +// evaluates the pattern inside that mesh. use std::sync::Arc; @@ -207,6 +204,40 @@ pub struct HatchModel { } impl HatchModel { + /// Indexed local-space fill mesh with even-odd loop containment. + pub(crate) fn fill_mesh(&self) -> (Vec<[f32; 2]>, Vec) { + let mut rings = Vec::new(); + let mut ring = Vec::new(); + for &[x, y] in self.boundary.iter() { + if x.is_finite() && y.is_finite() { + ring.push([x as f64, y as f64]); + } else if ring.len() >= 3 { + rings.push(std::mem::take(&mut ring)); + } else { + ring.clear(); + } + } + if ring.len() >= 3 { + rings.push(ring); + } + + let (points, triangles) = cadkernel::geom2d::triangulate_rings(&rings); + let vertices = points + .into_iter() + .map(|[x, y]| [x as f32, y as f32]) + .collect(); + let mut indices = Vec::with_capacity(triangles.len() * 3); + for triangle in triangles { + for index in triangle { + let Ok(index) = u32::try_from(index) else { + return (Vec::new(), Vec::new()); + }; + indices.push(index); + } + } + (vertices, indices) + } + /// CPU-side rasteriser for `HatchPattern::Pattern` — produces the line /// segments inside the boundary so non-GPU consumers (PDF export, /// `paper_canvas`, print preview) can draw the actual pattern instead diff --git a/src/scene/pipeline/device_capabilities.rs b/src/scene/pipeline/device_capabilities.rs index 7650bfba0..c87cc6113 100644 --- a/src/scene/pipeline/device_capabilities.rs +++ b/src/scene/pipeline/device_capabilities.rs @@ -16,9 +16,8 @@ impl DeviceCapabilities { /// Indexed wires read one per-wire constants storage buffer. const WIRE_STORAGE_BINDINGS: u32 = 1; - /// Batched hatch uses five storage bindings in one shader stage: - /// instances, boundaries, families, dashes, and visibility. - const HATCH_STORAGE_BINDINGS: u32 = 5; + /// Batched hatch stores instances, families, dashes, and visibility. + const HATCH_STORAGE_BINDINGS: u32 = 4; pub fn detect(device: &wgpu::Device) -> Self { Self::from_limits(&device.limits()) diff --git a/src/scene/pipeline/hatch_gpu/mod.rs b/src/scene/pipeline/hatch_gpu/mod.rs index 86503453d..1585d043e 100644 --- a/src/scene/pipeline/hatch_gpu/mod.rs +++ b/src/scene/pipeline/hatch_gpu/mod.rs @@ -34,8 +34,8 @@ impl HatchBackendKind { enum HatchBackend { Storage { - resident: Option, - preview: Option, + resident: Vec, + preview: Vec, }, Texture { resident: Vec, @@ -137,8 +137,8 @@ impl HatchGpu { }); let backend = match backend_kind { HatchBackendKind::Storage => HatchBackend::Storage { - resident: None, - preview: None, + resident: Vec::new(), + preview: Vec::new(), }, HatchBackendKind::Texture => HatchBackend::Texture { resident: Vec::new(), @@ -223,28 +223,29 @@ impl HatchGpu { queue: &wgpu::Queue, mut is_visible: impl FnMut([f32; 4]) -> bool, ) -> usize { - let HatchBackend::Storage { - resident: Some(batch), - .. - } = &mut self.backend + let HatchBackend::Storage { resident, .. } = &mut self.backend else { return 0; }; - for index in 0..batch.unique_source_count { - batch.source_visibility[index] = u32::from(is_visible(batch.source_aabbs[index])); + let mut updated = 0; + for batch in resident { + for index in 0..batch.unique_source_count { + batch.source_visibility[index] = u32::from(is_visible(batch.source_aabbs[index])); + } + for index in batch.unique_source_count..batch.source_visibility.len() { + batch.source_visibility[index] = 1; + } + for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() { + batch.placements[index].visible = if index == 0 && batch.unique_source_count > 0 { + 1 + } else { + u32::from(is_visible(aabb)) + }; + } + batch.upload_visibility(queue); + updated += batch.instance_aabbs.len() + batch.unique_source_count; } - for index in batch.unique_source_count..batch.source_visibility.len() { - batch.source_visibility[index] = 1; - } - for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() { - batch.placements[index].visible = if index == 0 && batch.unique_source_count > 0 { - 1 - } else { - u32::from(is_visible(aabb)) - }; - } - batch.upload_visibility(queue); - batch.instance_aabbs.len() + batch.unique_source_count + updated } pub fn draw<'pass>( @@ -258,12 +259,13 @@ impl HatchGpu { pass.set_stencil_reference(stencil_reference); match &self.backend { HatchBackend::Storage { resident, preview } => { - for batch in [resident.as_ref(), preview.as_ref()].into_iter().flatten() { + for batch in resident.iter().chain(preview) { pass.set_bind_group(1, &batch.bind_group, &[]); pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..)); pass.set_vertex_buffer(1, batch.placement_buffer.slice(..)); - for (vertices, instances) in &batch.draws { - pass.draw(vertices.clone(), instances.clone()); + pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + for (indices, instances) in &batch.draws { + pass.draw_indexed(indices.clone(), 0, instances.clone()); } } } @@ -272,7 +274,11 @@ impl HatchGpu { pass.set_bind_group(1, &hatch.bind_group, &[]); pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..)); pass.set_vertex_buffer(1, hatch.placement_buffer.slice(..)); - pass.draw(0..6, 0..hatch.instance_count); + pass.set_index_buffer( + hatch.index_buffer.slice(..), + wgpu::IndexFormat::Uint32, + ); + pass.draw_indexed(0..hatch.index_count, 0, 0..hatch.instance_count); } } } @@ -294,11 +300,11 @@ mod tests { #[test] fn selects_backend_from_device_limits() { assert_eq!( - HatchBackendKind::select(capabilities(5), false), + HatchBackendKind::select(capabilities(4), false), HatchBackendKind::Storage ); assert_eq!( - HatchBackendKind::select(capabilities(4), false), + HatchBackendKind::select(capabilities(3), false), HatchBackendKind::Texture ); } diff --git a/src/scene/pipeline/hatch_gpu/storage.rs b/src/scene/pipeline/hatch_gpu/storage.rs index 01e9ede65..4ab94131e 100644 --- a/src/scene/pipeline/hatch_gpu/storage.rs +++ b/src/scene/pipeline/hatch_gpu/storage.rs @@ -1,48 +1,5 @@ -// Hatch rendering — the single, canonical GPU hatch renderer. One draw -// call for all hatches, per-instance data fetched from storage buffers in -// the vertex shader. There is NO per-family cap here: every pattern line -// family is uploaded (`family_count` per instance) and the fragment shader -// loops over all of them, so complex baked patterns (ROOFING, GRAVEL, -// CAROLINA-LEDGESTONE with dozens–hundreds of families) draw in full, -// matching the PDF export. Unavailable on devices that lack vertex-stage -// storage buffers. The old per-hatch renderer with the -// 16-family cap now lives in `wipeout_gpu.rs` and serves only wipeouts. -// -// Data layout — three storage buffers fed from `HatchModel`s: -// -// InstanceBuffer (binding 0) : HatchInstance[] (128 B each) -// color, color2, mode, gradient, -// pattern angle/scale, world_origin, -// boundary_offset, boundary_count, -// family_offset, family_count, -// dash_offset, dash_count, aabb, -// visibility flag (CPU writes; GPU -// skip) -// BoundaryBuffer (binding 1) : vec4[] (all boundary -// verts concatenated; NaN markers -// preserved as separators just like -// the per-hatch path) -// FamilyBuffer (binding 2) : LineFamilyGpu[] (all line -// families concatenated) -// DashBuffer (binding 3) : f32[] (all dash -// lengths concatenated) -// -// The vertex buffer holds each hatch's tessellated boundary triangles as -// per-vertex local-space positions (`local_xy`) plus a per-vertex -// `instance_index` (u32 attribute) — instance_index lets us avoid relying -// on `@builtin(instance_index)` for portability. An instance whose -// boundary failed to tessellate instead gets an AABB quad here, with -// `poly_test == 1` on its `HatchInstance` so the fragment shader still -// runs the `in_polygon` test to clip the quad to the real shape; on the -// tessellated fast path (`poly_test == 0`) the triangles already bound -// the fill and the fragment shader skips that test. When the visibility -// flag is 0, the vertex shader returns a degenerate position and the -// fragment shader runs zero times for that instance. -// -// Two storage usages — vertex shader reads InstanceBuffer + Boundary -// for the AABB / boundary range; fragment shader reads -// InstanceBuffer + Boundary + Family + Dash. Both stages share group -// 1 with `read_only` access. +// Batched hatch rendering. The kernel triangulates each boundary once; the +// fragment shader only evaluates the fill pattern inside that mesh. use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily}; use iced::wgpu; @@ -70,23 +27,16 @@ pub struct HatchInstance { pub grad_range: f32, // 84 pub mode: u32, // 88 (0=pattern, 1=solid, 2=gradient) pub visible: u32, // 92 (CPU sets to 0 to skip) - pub boundary_offset: u32, // 96 (first boundary vert index) - pub boundary_count: u32, // 100 - pub family_offset: u32, // 104 - pub family_count: u32, // 108 + pub family_offset: u32, // 96 + pub family_count: u32, // 100 /// Signed draw-order depth (-1,1); 0.0 = neutral. Applied as a clip-z /// bias in the vertex shader so this fill orders against other types. - pub draw_depth: f32, // 112 - /// 1 = run the per-fragment `in_polygon` boundary test (fallback path for - /// an instance whose boundary failed to tessellate); 0 = the rasterized - /// triangles already bound the fill, skip the test. - pub poly_test: u32, // 116 + pub draw_depth: f32, // 104 /// Gradient shape (`GradientKind::shader_kind`), bit 4 = inverted stops. - pub grad_kind: u32, // 120 - pub _pad2: u32, // 124 + pub grad_kind: u32, // 108 } -const _: () = assert!(std::mem::size_of::() == 128); +const _: () = assert!(std::mem::size_of::() == 112); /// Split a hatch's f64 world-origin anchor into double-single (high, low) f32 /// pairs so the GPU keeps sub-unit precision at UTM-scale coordinates. @@ -120,10 +70,7 @@ pub struct LineFamilyGpu { const _: () = assert!(std::mem::size_of::() == 48); -/// Per-vertex data — tessellated boundary triangles in local (world_origin- -/// relative) space, each carrying its instance index (avoids relying on -/// `@builtin(instance_index)` across backends). Instances whose boundary -/// failed to tessellate emit an AABB quad here instead (see `build`). +/// Kernel mesh vertex in local space with its source instance. #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub(super) struct HatchVertex { @@ -184,6 +131,7 @@ impl HatchPlacement { /// hatch render pass entirely). pub(super) struct StorageHatchBatch { pub vertex_buffer: wgpu::Buffer, + pub index_buffer: wgpu::Buffer, pub placement_buffer: wgpu::Buffer, pub draws: Vec<(std::ops::Range, std::ops::Range)>, // The four storage buffers below are referenced via `bind_group` — @@ -191,7 +139,6 @@ pub(super) struct StorageHatchBatch { // only direct consumer. Keep them as fields to keep ownership in // one place; `#[allow(dead_code)]` silences the read-never warning. #[allow(dead_code)] pub instance_buffer: wgpu::Buffer, - #[allow(dead_code)] pub boundary_buffer: wgpu::Buffer, #[allow(dead_code)] pub family_buffer: wgpu::Buffer, #[allow(dead_code)] pub dash_buffer: wgpu::Buffer, /// Per-instance visibility flag (1=draw, 0=skip). Stored in its @@ -205,8 +152,7 @@ pub(super) struct StorageHatchBatch { #[allow(dead_code)] pub instance_count: u32, /// CPU mirror — `update_visibility` re-uploads this whole slice /// when any flag changes. ~4 B per hatch, so 40 KB / 10 k hatches - /// per pan tick. Far cheaper than touching the 128 B-per-instance - /// data. + /// per pan tick. Far cheaper than touching the instance data. pub placements: Vec, pub source_visibility: Vec, pub source_aabbs: Vec<[f32; 4]>, @@ -218,24 +164,76 @@ pub(super) struct StorageHatchBatch { pub instance_aabbs: Vec<[f32; 4]>, } +fn hatch_buffer_cost(hatch: &HatchModel) -> [u64; 7] { + let boundary = hatch.boundary.len() as u64; + let (families, dashes) = match &hatch.pattern { + HatchPattern::Pattern(families) => ( + families.len() as u64, + families.iter().map(|family| family.dashes.len() as u64).sum(), + ), + _ => (0, 0), + }; + [ + boundary.saturating_mul(std::mem::size_of::() as u64), + boundary.saturating_mul(24), + std::mem::size_of::() as u64, + families.saturating_mul(std::mem::size_of::() as u64), + dashes.saturating_mul(std::mem::size_of::() as u64), + std::mem::size_of::() as u64, + std::mem::size_of::() as u64, + ] +} + impl StorageHatchBatch { - /// One-time build from the full hatch list. Re-uploaded only when - /// `geometry_epoch` advances (mirrors the existing per-hatch - /// upload trigger). Per-frame visibility flips go through - /// [`upload_visibility`]. + /// Builds whole-hatch chunks within the device's buffer limits. pub(super) fn build( device: &wgpu::Device, bgl: &wgpu::BindGroupLayout, hatches: &[HatchModel], - ) -> Option { + ) -> Vec { if hatches.is_empty() { - return None; + return Vec::new(); } + let limits = device.limits(); + let chunk_limit = limits + .max_buffer_size + .min(limits.max_storage_buffer_binding_size as u64); + let mut ranges = Vec::new(); + let mut start = 0; + let mut used = [0u64; 7]; + for (index, hatch) in hatches.iter().enumerate() { + let cost = hatch_buffer_cost(hatch); + let overflow = used + .iter() + .zip(cost) + .any(|(used, cost)| used.saturating_add(cost) > chunk_limit); + if overflow && index > start { + ranges.push(start..index); + start = index; + used = [0; 7]; + } + for (used, cost) in used.iter_mut().zip(cost) { + *used = used.saturating_add(cost); + } + } + ranges.push(start..hatches.len()); + ranges + .into_iter() + .filter_map(|range| Self::build_chunk(device, bgl, &hatches[range])) + .collect() + } + + fn build_chunk( + device: &wgpu::Device, + bgl: &wgpu::BindGroupLayout, + hatches: &[HatchModel], + ) -> Option { + let mut instances: Vec = Vec::with_capacity(hatches.len()); - let mut boundary: Vec<[f32; 4]> = Vec::new(); let mut families: Vec = Vec::new(); let mut dashes: Vec = Vec::new(); + let mut meshes = Vec::with_capacity(hatches.len()); let mut slots = rustc_hash::FxHashMap::default(); let mut groups: Vec> = Vec::new(); for (index, hatch) in hatches.iter().enumerate() { @@ -255,22 +253,9 @@ impl StorageHatchBatch { for group in &groups { let h = group[0]; - let boundary_offset = boundary.len() as u32; - for &[x, y] in h.boundary.iter() { - // NaN sub-loop separators become the finite sentinel on the - // GPU: Intel drivers fold the shader NaN self-compare to - // `true`, turning separators into vertices and bleeding the - // fill outside its boundary (#386, #416). - if x.is_finite() && y.is_finite() { - boundary.push([x, y, 0.0, 0.0]); - } else { - let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP; - boundary.push([s, s, 0.0, 0.0]); - } - } - let boundary_count = boundary.len() as u32 - boundary_offset; - - + let mesh = h.fill_mesh(); + let has_mesh = !mesh.0.is_empty() && !mesh.1.is_empty(); + meshes.push(mesh); let family_offset = families.len() as u32; let mut family_count = 0u32; @@ -301,11 +286,8 @@ impl StorageHatchBatch { dashes.push(d); } let n_dashes = (dashes.len() as u32 - dash_offset).min(u32::MAX); - // QCAD PAT local-frame convention (mirrors - // the storage batch convention: `dy` is - // the perpendicular spacing, `dx` is the along-line - // phase shift — both in family-local coords. The - // shader applies cos_off/sin_off to rotate them. + // PAT local frame: perpendicular spacing and + // along-line phase. let perp_step = fam.dy; let along_step = fam.dx; // Screen-space derivative drives 1-px line width @@ -366,39 +348,19 @@ impl StorageHatchBatch { grad_range, mode, visible: 0, - boundary_offset, - boundary_count, family_offset, family_count, draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 }, - poly_test: 1, grad_kind, - _pad2: 0, }); continue; } - // Pad the AABB so the quad covers any pattern halo + the - // family origin. Mirrors the per-hatch shader's quad sizing - // logic — `diag * 0.8 + max_spacing * 2 * scale`. - let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt(); - // `perp_step.abs()` per family — uses the same QCAD local- - // frame convention as `LineFamilyGpu.perp_step` above so - // the quad padding matches what the shader will sample. - let max_spacing = match &h.pattern { - HatchPattern::Pattern(fs) => fs - .iter() - .map(|f| f.dy.abs()) - .fold(0.0f32, f32::max), - _ => 5.0, - }; - let pad = (diag * 0.8 + max_spacing * 2.0 * h.scale).max(1.0); - let (wo_hi, wo_lo) = split_origin_ds(h.world_origin); instances.push(HatchInstance { color: h.color, color2, - aabb: [min_x - pad, min_y - pad, max_x + pad, max_y + pad], + aabb: [min_x, min_y, max_x, max_y], world_origin: wo_hi, world_origin_low: wo_lo, angle_offset: h.angle_offset, @@ -408,26 +370,14 @@ impl StorageHatchBatch { grad_min, grad_range, mode, - visible: 1, - boundary_offset, - boundary_count, + visible: u32::from(has_mesh), family_offset, family_count, draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 }, - // Always resolve inside/outside per fragment on the GPU (the - // boundary is uploaded below). Dropping CPU pre-triangulation - // keeps the vertex buffer at 6 verts/hatch regardless of - // boundary complexity, so a hatch-dense drawing can't overflow - // the device buffer limit. Each instance draws its AABB quad. - poly_test: 1, grad_kind, - _pad2: 0, }); } // Empty fallbacks — storage buffers can't be zero-sized. - if boundary.is_empty() { - boundary.push([0.0; 4]); - } if families.is_empty() { families.push(LineFamilyGpu::default_filler()); } @@ -435,21 +385,33 @@ impl StorageHatchBatch { dashes.push(0.0); } - // Vertex buffer — one AABB quad (two triangles, BL,BR,TL, BR,TR,TL) per - // instance; the GPU shader clips each fragment to the boundary via the - // in_polygon test (poly_test == 1). 6 verts/hatch, independent of - // boundary complexity, so the buffer can never overflow the device - // limit no matter how dense the drawing. - let mut verts: Vec = Vec::with_capacity(instances.len() * 6); - for (i, inst) in instances.iter().enumerate() { - let [xmin, ymin, xmax, ymax] = inst.aabb; - let quad = [ - [xmin, ymin], [xmax, ymin], [xmin, ymax], - [xmax, ymin], [xmax, ymax], [xmin, ymax], - ]; - for c in quad { - verts.push(HatchVertex { local_xy: c, instance_index: i as u32 }); + let mut verts = Vec::new(); + let mut indices = Vec::new(); + let mut mesh_ranges = Vec::with_capacity(meshes.len()); + for (instance_index, (points, mesh_indices)) in meshes.into_iter().enumerate() { + let Ok(base) = u32::try_from(verts.len()) else { + return None; + }; + let Ok(start) = u32::try_from(indices.len()) else { + return None; + }; + verts.extend(points.into_iter().map(|local_xy| HatchVertex { + local_xy, + instance_index: instance_index as u32, + })); + for index in mesh_indices { + let Some(index) = base.checked_add(index) else { + return None; + }; + indices.push(index); } + let Ok(end) = u32::try_from(indices.len()) else { + return None; + }; + mesh_ranges.push(start..end); + } + if indices.is_empty() { + return None; } let mut placements = Vec::new(); let mut draws = Vec::with_capacity(groups.len()); @@ -461,7 +423,10 @@ impl StorageHatchBatch { draw_depth: 0.0, visible: 1, }); - draws.push((0..unique_source_count as u32 * 6, 0..1)); + let end = mesh_ranges[unique_source_count - 1].end; + if end > 0 { + draws.push((0..end, 0..1)); + } let mut union = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY]; for inst in instances.iter().take(unique_source_count) { union[0] = union[0].min(inst.aabb[0] + inst.world_origin[0]); @@ -500,10 +465,32 @@ impl StorageHatchBatch { inst.aabb[3] + inst.world_origin[1] + high[1], ]); } - draws.push(( - source_index as u32 * 6..source_index as u32 * 6 + 6, - placement_start..placements.len() as u32, - )); + if !mesh_ranges[source_index].is_empty() { + draws.push(( + mesh_ranges[source_index].clone(), + placement_start..placements.len() as u32, + )); + } + } + + let visibility: Vec = instances.iter().map(|instance| instance.visible).collect(); + let limits = device.limits(); + let buffer_fits = |count: usize, stride: usize| { + (count as u64).saturating_mul(stride as u64) <= limits.max_buffer_size + }; + let storage_fits = |count: usize, stride: usize| { + (count as u64).saturating_mul(stride as u64) + <= limits.max_storage_buffer_binding_size as u64 + }; + if !buffer_fits(verts.len(), std::mem::size_of::()) + || !buffer_fits(indices.len(), std::mem::size_of::()) + || !buffer_fits(placements.len(), std::mem::size_of::()) + || !storage_fits(instances.len(), std::mem::size_of::()) + || !storage_fits(families.len(), std::mem::size_of::()) + || !storage_fits(dashes.len(), std::mem::size_of::()) + || !storage_fits(visibility.len(), std::mem::size_of::()) + { + return None; } let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -511,6 +498,11 @@ impl StorageHatchBatch { contents: bytemuck::cast_slice(&verts), usage: wgpu::BufferUsages::VERTEX, }); + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("hatch.index"), + contents: bytemuck::cast_slice(&indices), + usage: wgpu::BufferUsages::INDEX, + }); let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.placements"), contents: bytemuck::cast_slice(&placements), @@ -521,11 +513,6 @@ impl StorageHatchBatch { contents: bytemuck::cast_slice(&instances), usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, }); - let boundary_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("hatch.boundary"), - contents: bytemuck::cast_slice(&boundary), - usage: wgpu::BufferUsages::STORAGE, - }); let family_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.families"), contents: bytemuck::cast_slice(&families), @@ -537,7 +524,6 @@ impl StorageHatchBatch { usage: wgpu::BufferUsages::STORAGE, }); - let visibility: Vec = instances.iter().map(|i| i.visible).collect(); let visibility_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.visibility"), contents: bytemuck::cast_slice(&visibility), @@ -554,18 +540,14 @@ impl StorageHatchBatch { }, wgpu::BindGroupEntry { binding: 1, - resource: boundary_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 2, resource: family_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { - binding: 3, + binding: 2, resource: dash_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { - binding: 4, + binding: 3, resource: visibility_buffer.as_entire_binding(), }, ], @@ -582,10 +564,10 @@ impl StorageHatchBatch { Some(Self { vertex_buffer, + index_buffer, placement_buffer, draws, instance_buffer, - boundary_buffer, family_buffer, dash_buffer, visibility_buffer, @@ -614,10 +596,7 @@ impl StorageHatchBatch { ); } - /// Group-1 bind group layout — shared by the pipeline so it can be - /// constructed once at startup. All four bindings are read-only - /// storage and visible to both VS (AABB+visibility lookup) and FS - /// (boundary / family / dash sampling). + /// Shared layout for instance, family, dash, and visibility storage. #[allow(dead_code)] pub(super) fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { let entry = |binding: u32| wgpu::BindGroupLayoutEntry { @@ -632,7 +611,7 @@ impl StorageHatchBatch { }; device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("hatch.bgl"), - entries: &[entry(0), entry(1), entry(2), entry(3), entry(4)], + entries: &[entry(0), entry(1), entry(2), entry(3)], }) } } diff --git a/src/scene/pipeline/hatch_gpu/texture.rs b/src/scene/pipeline/hatch_gpu/texture.rs index d2037a922..83fe314b8 100644 --- a/src/scene/pipeline/hatch_gpu/texture.rs +++ b/src/scene/pipeline/hatch_gpu/texture.rs @@ -1,14 +1,5 @@ -// Storage-free hatch renderer — texture-backed, UNCAPPED. -// -// Devices without storage buffers use this per-hatch renderer. It reuses the -// storage-free hatch algorithm (see wipeout.wgsl / hatch_texture.wgsl) but packs the -// variable-length boundary / family / dash arrays into ONE RGBA32F data texture -// read via textureLoad — removing the MAX_FAMILIES / MAX_HATCH_BOUNDARY_VERTS / -// MAX_DASHES caps of the uniform (WipeoutGpu) path. Every hatch type — solid, -// gradient, and arbitrarily complex line patterns — renders in compat mode. -// -// Storage-capable devices use the sibling storage backend; wipeout masks use -// wipeout_gpu.rs. +// Texture-backed hatch renderer for devices without storage buffers. Boundary +// geometry comes from the same kernel mesh as the storage backend. use crate::scene::model::hatch_model::{HatchModel, HatchPattern}; use iced::wgpu; @@ -71,7 +62,7 @@ struct TextureHatchUniform { color: [f32; 4], // 0 color2: [f32; 4], // 16 mode: u32, // 32 - vcount: u32, // 36 + _reserved: u32, // 36 angle_offset: f32, // 40 scale: f32, // 44 grad_cos: f32, // 48 @@ -90,7 +81,9 @@ struct TextureHatchUniform { pub(super) struct TextureHatch { pub(super) vertex_buffer: wgpu::Buffer, + pub(super) index_buffer: wgpu::Buffer, pub(super) placement_buffer: wgpu::Buffer, + pub(super) index_count: u32, pub(super) instance_count: u32, pub(super) bind_group: wgpu::BindGroup, /// Reserved for per-frame AABB LOD (mirrors `WipeoutGpu`); not yet wired @@ -125,7 +118,7 @@ impl TextureHatch { } groups .into_iter() - .map(|group| Self::new(device, queue, &group, bgl1)) + .filter_map(|group| Self::new(device, queue, &group, bgl1)) .collect() } @@ -164,7 +157,7 @@ impl TextureHatch { queue: &wgpu::Queue, models: &[&HatchModel], bgl1: &wgpu::BindGroupLayout, - ) -> Self { + ) -> Option { let model = models[0]; // ── Decode pattern mode (mirrors WipeoutGpu::new) ───────────────── // The gradient shape (kind + invert bit) rides in the mode's high @@ -197,41 +190,26 @@ impl TextureHatch { max_y = max_y.max(y); } - let max_spacing = match &model.pattern { - HatchPattern::Pattern(families) => { - families.iter().map(|f| f.dy.abs()).fold(0.0f32, f32::max) - } - _ => 5.0, - }; - let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt(); - let pad = (diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0); - - // Anchor pattern phase at `world_origin` with the boundary stored raw, - // matching the storage backend — NOT WipeoutGpu, - // whose f64 origin grid-snap is dead code (wipeouts are always solid) - // and would phase-shift every line pattern relative to desktop. No drift. let origin = model.world_origin; let drift = [0.0f32, 0.0f32]; - let (x0, x1, y0, y1) = ( - min_x + drift[0] - pad, - max_x + drift[0] + pad, - min_y + drift[1] - pad, - max_y + drift[1] + pad, - ); - - let quad = [ - HatchVertex { pos: [x0, y0, 0.0], _pad: 0.0 }, - HatchVertex { pos: [x1, y0, 0.0], _pad: 0.0 }, - HatchVertex { pos: [x1, y1, 0.0], _pad: 0.0 }, - HatchVertex { pos: [x0, y0, 0.0], _pad: 0.0 }, - HatchVertex { pos: [x1, y1, 0.0], _pad: 0.0 }, - HatchVertex { pos: [x0, y1, 0.0], _pad: 0.0 }, - ]; + let (mesh_points, indices) = model.fill_mesh(); + if mesh_points.is_empty() || indices.is_empty() { + return None; + } + let vertices: Vec = mesh_points + .into_iter() + .map(|[x, y]| HatchVertex { pos: [x, y, 0.0], _pad: 0.0 }) + .collect(); let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("hatch.texture.vbuf"), - contents: bytemuck::cast_slice(&quad), + contents: bytemuck::cast_slice(&vertices), usage: wgpu::BufferUsages::VERTEX, }); + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("hatch.texture.ibuf"), + contents: bytemuck::cast_slice(&indices), + usage: wgpu::BufferUsages::INDEX, + }); let base = model .render_instance .map_or([0.0; 3], |instance| instance.translation); @@ -290,21 +268,8 @@ impl TextureHatch { (0.0, 1.0) }; - // ── Pack the data texture: boundary | families | dashes ─────────── + // ── Pack the data texture: families | dashes ───────────────────── let mut texels: Vec<[f32; 4]> = Vec::new(); - // Boundary section (texels 0..vcount). NaN separators become the - // finite GPU sentinel — the shader NaN self-compare is folded to - // `true` on some drivers (#386, #416). - for &[x, y] in model.boundary.iter() { - if x.is_finite() && y.is_finite() { - texels.push([x + drift[0], y + drift[1], 0.0, 0.0]); - } else { - let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP; - texels.push([s, s, 0.0, 0.0]); - } - } - let vcount = texels.len() as u32; - // Family section (3 texels each) + a flat dash pool. let fam_off = texels.len() as u32; let mut n_families = 0u32; @@ -320,7 +285,7 @@ impl TextureHatch { 0.0 }; let angle_r = fam.angle_deg.to_radians(); - // QCAD PAT convention: perp_step = dy, along_step = dx. + // PAT local frame: perpendicular spacing and along-line phase. texels.push([angle_r.cos(), angle_r.sin(), fam.x0, fam.y0]); texels.push([fam.dx, fam.dy, fam.dy, fam.dx]); // Counts as exact f32 (small integers → no denormal/bitcast risk). @@ -385,7 +350,7 @@ impl TextureHatch { color: model.color, color2, mode, - vcount, + _reserved: 0, angle_offset: model.angle_offset, // Clamp like the desktop renderer so scale==0 can't make perp_step 0 // → round(perp/0)=NaN → an invisible hatch. @@ -433,14 +398,16 @@ impl TextureHatch { [min_x, min_y, max_x, max_y] }; - Self { + Some(Self { vertex_buffer, + index_buffer, placement_buffer, + index_count: indices.len() as u32, instance_count: placements.len() as u32, bind_group, world_aabb, _uniform_buf, _data_tex: data_tex, - } + }) } } diff --git a/src/shaders/hatch.wgsl b/src/shaders/hatch.wgsl index cef1f51dc..f288faee5 100644 --- a/src/shaders/hatch.wgsl +++ b/src/shaders/hatch.wgsl @@ -1,23 +1,10 @@ -// Phase 4-B — batched hatch shader. All hatches in one draw call; -// per-instance data fetched from storage buffers indexed by the -// `instance_index` vertex attribute (passed from the per-vertex -// (local_xy, instance_index) stream of tessellated boundary-triangle -// positions so we don't depend on @builtin(instance_index) edge cases -// across backends). +// Batched hatch shader. Kernel mesh vertices carry their source instance. // // Layout — matches `hatch_gpu/storage.rs`: -// group 1 binding 0 InstanceBuffer HatchInstance[] (128 B / inst) -// group 1 binding 1 BoundaryBuffer vec4[] (xy in .xy) -// group 1 binding 2 FamilyBuffer LineFamilyGpu[] (48 B / fam) -// group 1 binding 3 DashBuffer f32[] -// -// The vertex shader emits tessellated boundary triangles in local space -// (an instance whose boundary failed to tessellate falls back to an -// AABB quad with `poly_test == 1`, so the fragment shader still runs -// `in_polygon` to clip it to the real shape). A `visible == 0` instance -// gets an out-of-NDC clip position so the fragment shader never runs -// for it — that's the GPU-side cull (Phase 4-B equivalent of -// `compute_hatch_lod` writing `hatch_skip_flags`). +// group 1 binding 0 InstanceBuffer HatchInstance[] +// group 1 binding 1 FamilyBuffer LineFamilyGpu[] +// group 1 binding 2 DashBuffer f32[] +// group 1 binding 3 Visibility u32[] // ── Group 0: shared frame uniforms (matches hatch.wgsl) ────────────────── @@ -53,14 +40,10 @@ struct HatchInstance { grad_range: f32, mode: u32, // 0=pattern, 1=solid, 2=gradient visible: u32, // 0 = skip (CPU writes via compute_hatch_lod) - boundary_offset: u32, - boundary_count: u32, family_offset: u32, family_count: u32, draw_depth: f32, // signed (-1,1) draw-order bias; 0 = neutral - poly_test: u32, // 1 = run in_polygon (fallback), 0 = skip grad_kind: u32, // shape (0=linear,1=cyl,2=sph,3=hemi,4=curved), bit4=invert - _pad2: u32, } // Draw-order depth bias (see wire.wgsl). Higher draw_depth → smaller z → @@ -83,14 +66,13 @@ struct LineFamily { } @group(1) @binding(0) var instances: array; -@group(1) @binding(1) var boundary: array>; -@group(1) @binding(2) var families: array; -@group(1) @binding(3) var dashes: array; +@group(1) @binding(1) var families: array; +@group(1) @binding(2) var dashes: array; // Per-instance visibility (Phase 4-B sub-pixel + frustum skip). // CPU writes `1` to draw / `0` to skip every frame; vertex shader // emits an out-of-NDC clip position for 0-instances so the GPU // rasterizer culls the primitive before any fragment runs. -@group(1) @binding(4) var visibility: array; +@group(1) @binding(3) var visibility: array; // ── Vertex shader ──────────────────────────────────────────────────────── @@ -144,58 +126,6 @@ struct VOut { return o; } -// ── Point-in-polygon (ray casting) over a sub-range of BoundaryBuffer ──── - -fn valid_vertex(p: vec2) -> bool { - // Sub-loop separators are a huge finite sentinel (1e30, see - // GPU_BOUNDARY_SEP), not NaN: `x == x` NaN detection gets folded to - // `true` by some drivers (Intel Mesa fast math), which made separators - // read as real vertices and bleed fills past their boundary (#386, #416). - return abs(p.x) < 1.0e29 && abs(p.y) < 1.0e29; -} - -fn edge_crosses(p: vec2, a: vec2, c: vec2) -> bool { - if (a.y > p.y) != (c.y > p.y) { - let x_int = (c.x - a.x) * (p.y - a.y) / (c.y - a.y) + a.x; - return p.x < x_int; - } - return false; -} - -fn in_polygon(p: vec2, offset: u32, count: u32) -> bool { - var inside = false; - var prev = vec2(0.0, 0.0); - var first = vec2(0.0, 0.0); - var have_prev = false; - for (var i = 0u; i < count; i++) { - let vi = boundary[offset + i].xy; - if !valid_vertex(vi) { - // Close the sub-loop that just ended (last → first edge). An - // unclosed boundary — e.g. a SOLID's 4 corners, which are not - // repeated — otherwise miscounts crossings and the fill bleeds - // outside the shape. (#140) - if have_prev && edge_crosses(p, prev, first) { - inside = !inside; - } - have_prev = false; - continue; - } - if have_prev { - if edge_crosses(p, prev, vi) { - inside = !inside; - } - } else { - first = vi; - } - prev = vi; - have_prev = true; - } - if have_prev && edge_crosses(p, prev, first) { - inside = !inside; - } - return inside; -} - // ── Per-family hatch test (same math as hatch.wgsl, dashes from // global DashBuffer instead of per-hatch FamilyBatch) ──────────────────── @@ -282,15 +212,7 @@ fn check_family( let inst = instances[v.instance_index]; - // 1. Boundary test — only on the fallback path (poly_test==1). On the - // tessellated fast path the triangles already bound the fill. - if inst.poly_test == 1u { - if !in_polygon(v.xz, inst.boundary_offset, inst.boundary_count) { - discard; - } - } - - // 2. Mode dispatch. + // Mode dispatch. if inst.mode == 1u { return inst.color; } else if inst.mode == 2u { @@ -319,12 +241,11 @@ fn check_family( t = 1.0 - t; } // Radial stops run OUTSIDE-IN: colour 1 at the rim, colour 2 at the - // centre (AutoCAD's SPHERICAL/HEMISPHERICAL convention; the INV bit - // above swaps them back). + // centre; the INV bit above swaps them back. return mix(inst.color2, inst.color, t); } - // 3. Pattern LOD. Keep every family visible until all family spacings + // Pattern LOD. Keep every family visible until all family spacings // project below 2 px, then substitute one solid fill. A single dense // family must neither hide itself nor turn a complex hatch solid. var all_families_subpixel = inst.family_count > 0u && u.world_per_pixel > 0.0; @@ -343,7 +264,7 @@ fn check_family( return inst.color; } - // 4. Pattern evaluation. + // Pattern evaluation. let cos_off = cos(inst.angle_offset); let sin_off = sin(inst.angle_offset); for (var i = 0u; i < inst.family_count; i++) { diff --git a/src/shaders/hatch_texture.wgsl b/src/shaders/hatch_texture.wgsl index 3a7775382..c67df0b8d 100644 --- a/src/shaders/hatch_texture.wgsl +++ b/src/shaders/hatch_texture.wgsl @@ -1,15 +1,7 @@ -// Hatch texture shader — storage-free, UNCAPPED variant of wipeout.wgsl. -// -// Devices without vertex/fragment storage buffers cannot use the batched -// storage-buffer renderer (hatch.wgsl). This shader keeps the exact -// wipeout.wgsl hatch algorithm (in_polygon + per-family line/dash/dot + solid + -// gradient) but reads the variable-length boundary / family / dash arrays from a -// single RGBA32F data texture via textureLoad instead of fixed-size uniforms — -// removing the MAX_FAMILIES (16) / MAX_HATCH_BOUNDARY_VERTS (1024) / MAX_DASHES -// (128) caps of the uniform path. +// Texture-backed hatch shader. The kernel mesh bounds the fill; this shader +// evaluates only the pattern, solid, or gradient colour. // // Data texture layout (row-major, width = h.tex_width, one vec4 per texel): -// [ 0 .. vcount ) boundary verts, .xy = local XY, NaN = break // [ fam_off .. fam_off+3*n_fam ) families, 3 texels each (see load_family) // [ dash_off .. ) dash values, 4 per texel (RGBA) @@ -36,7 +28,7 @@ struct HatchUniforms { color: vec4, // 0 color2: vec4, // 16 mode: u32, // 32: 0=pattern, 1=solid, 2=gradient - vcount: u32, // 36: boundary vertex count + _reserved: u32, // 36 angle_offset: f32, // 40 scale: f32, // 44 grad_cos: f32, // 48 @@ -119,56 +111,6 @@ struct VOut { return o; } -// ── Point-in-polygon (ray casting) ──────────────────────────────────────── - -fn valid_vertex(p: vec2) -> bool { - // Sub-loop separators are a huge finite sentinel (1e30, see - // GPU_BOUNDARY_SEP), not NaN: `x == x` NaN detection gets folded to - // `true` by some drivers (Intel Mesa fast math), which made separators - // read as real vertices and bleed fills past their boundary (#386, #416). - return abs(p.x) < 1.0e29 && abs(p.y) < 1.0e29; -} - -fn edge_crosses(p: vec2, a: vec2, c: vec2) -> bool { - if (a.y > p.y) != (c.y > p.y) { - let x_int = (c.x - a.x) * (p.y - a.y) / (c.y - a.y) + a.x; - return p.x < x_int; - } - return false; -} - -fn in_polygon(p: vec2) -> bool { - var inside = false; - let n = h.vcount; - var prev = vec2(0.0, 0.0); - var first = vec2(0.0, 0.0); - var have_prev = false; - for (var i = 0u; i < n; i++) { - let vi = texel(i).xy; - if !valid_vertex(vi) { - // NaN sentinel closes the current sub-loop (last → first edge). (#140) - if have_prev && edge_crosses(p, prev, first) { - inside = !inside; - } - have_prev = false; - continue; - } - if have_prev { - if edge_crosses(p, prev, vi) { - inside = !inside; - } - } else { - first = vi; - } - prev = vi; - have_prev = true; - } - if have_prev && edge_crosses(p, prev, first) { - inside = !inside; - } - return inside; -} - // ── Per-family hatch test (identical math to wipeout.wgsl) ───────────────── // `ddx_xz`/`ddy_xz` are screen-space derivatives of `xz`, taken once in @@ -244,8 +186,6 @@ fn check_family( let ddx_xz = dpdx(v.xz); let ddy_xz = dpdy(v.xz); - if !in_polygon(v.xz) { discard; } - let base_mode = h.mode & 0xFFu; let gk = (h.mode >> 8u) & 15u; let ginv = ((h.mode >> 8u) & 16u) != 0u; From e21d4954a3d0fbd6e762f0afe9c188fc278b5171 Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Wed, 12 Aug 2026 23:41:22 -0300 Subject: [PATCH 02/38] fix: respect drawing units in annotation scales --- src/app/update/file.rs | 11 +++++++---- src/scene/mod.rs | 29 +++++++++++++++++++++-------- src/scene/view/render.rs | 2 +- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 7f89dc2cd..aceda9873 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -1144,13 +1144,16 @@ pub(super) fn on_open_file(&mut self) -> Task { crate::scene::text::ttf_glyph::clear_fallback_cache(); } // Current model-space annotation scale comes from the drawing's - // CANNOSCALEVALUE (paper/drawing factor); the multiplier we use - // for text/dim sizing is its inverse (1:50 -> 0.02 -> 50.0). + // CANNOSCALEVALUE (paper/drawing factor). Convert its inverse into + // drawing units as well: metric annotation sizes are paper millimetres + // and imperial annotation sizes are paper inches. let cannoscale_value = self.tabs[i].scene.document.header.annotation_scale_value; + let unit_factor = self.tabs[i].scene.annotation_scale_unit_factor(); + self.tabs[i].scene.annotation_scale = if cannoscale_value > 1e-9 { - (1.0 / cannoscale_value) as f32 + ((1.0 / cannoscale_value) / unit_factor) as f32 } else { - 1.0 + (1.0 / unit_factor) as f32 }; // Open-time breakdown so regressions are visible immediately. diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 67134058d..85245b5e2 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -872,10 +872,12 @@ pub fn prepare_open_geometry( scene.local_center = caches.local_center; scene.bg_color = model_bg; let cannoscale_value = scene.document.header.annotation_scale_value; + let unit_factor = scene.annotation_scale_unit_factor(); + scene.annotation_scale = if cannoscale_value > 1e-9 { - (1.0 / cannoscale_value) as f32 + ((1.0 / cannoscale_value) / unit_factor) as f32 } else { - 1.0 + (1.0 / unit_factor) as f32 }; scene.current_layout = "Model".to_string(); let camera = scene.camera.borrow().clone(); @@ -3553,7 +3555,7 @@ impl Scene { /// The built-in scale factors are defined assuming model and paper use /// the same base unit. A drawing in metres therefore needs an extra /// factor of 1000 when its paper side is measured in millimetres. - fn annotation_scale_unit_factor(&self) -> f64 { + pub(crate) fn annotation_scale_unit_factor(&self) -> f64 { let paper_unit = if self.prefers_imperial_scales() == Some(true) { 1 // Inches } else { @@ -3836,14 +3838,25 @@ impl Scene { pub fn set_annotation_scale_named(&mut self, name: &str) -> Option { let handle = self.scale_handle_ensuring(name)?; - let ObjectType::Scale(scale) = self.document.objects.get(&handle)? else { - return None; + let unit_factor = self.annotation_scale_unit_factor(); + + let (scale_name, scale_factor, multiplier) = { + let ObjectType::Scale(scale) = self.document.objects.get(&handle)? else { + return None; + }; + + ( + scale.name.clone(), + scale.factor(), + scale.inverse_factor() / unit_factor, + ) }; - let multiplier = scale.inverse_factor(); + self.annotation_scale = multiplier as f32; - self.document.header.current_annotation_scale = scale.name.clone(); - self.document.header.annotation_scale_value = scale.factor(); + self.document.header.current_annotation_scale = scale_name; + self.document.header.annotation_scale_value = scale_factor; self.invalidate_annotation_dependencies(); + Some(handle) } diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 20d20f725..d2ab7b66f 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -3112,7 +3112,7 @@ impl Scene { } let context_scale = match self.document.objects.get(&scale) { Some(acadrust::objects::ObjectType::Scale(value)) => { - value.inverse_factor() as f32 + (value.inverse_factor() / self.annotation_scale_unit_factor()) as f32 } _ => annotation_scale, }; From 2da520de80af23816d5ffab2f456118e384192dd Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Thu, 13 Aug 2026 09:46:56 -0300 Subject: [PATCH 03/38] fix: keep dimension text grips aligned during stretch --- src/app/properties.rs | 43 ++++++++ src/entities/dimension.rs | 227 +++++++++++++++++++++++++++++++++++++- 2 files changed, 268 insertions(+), 2 deletions(-) diff --git a/src/app/properties.rs b/src/app/properties.rs index 15c1e93bb..b233c5614 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1618,6 +1618,49 @@ impl OpenCADStudio { annotation_scale_handle, ); let mut entity_grips = dispatch::grips(contextual.as_ref()); + // Dimension::grips() cannot see the document, so an automatic dimension + // text grip cannot resolve its real DIMSTYLE/annotation-scaled position + // there. Correct it here, where both the document and displayed annotation + // scale are available. + if let acadrust::EntityType::Dimension(dim) = contextual.as_ref() { + if matches!( + dim, + acadrust::entities::Dimension::Linear(_) + | acadrust::entities::Dimension::Aligned(_) + ) && !dim.base().text_user_positioned + { + let anno_scale = annotation_scale_handle + .and_then(|handle| { + match self.tabs[i].scene.document.objects.get(&handle) { + Some(acadrust::objects::ObjectType::Scale(scale)) => Some( + scale.inverse_factor() + / self.tabs[i].scene.annotation_scale_unit_factor(), + ), + _ => None, + } + }) + .unwrap_or(self.tabs[i].scene.annotation_scale as f64); + + if let Some(position) = + crate::entities::dimension::dimension_text_grip_position( + dim, + &self.tabs[i].scene.document, + anno_scale, + ) + { + // The text grip is the final native grip of Linear/Aligned dims. + // While the text is still automatic, make this a point/stretch grip + // rather than a midpoint-translate grip. That makes the first drag use + // the displayed automatic position as its absolute starting point instead + // of translating the stale DWG text_middle_point. + if let Some(text_grip) = entity_grips.last_mut() { + text_grip.world = + glam::DVec3::new(position.x, position.y, position.z); + text_grip.is_midpoint = false; + } + } + } + } entity_grips.extend(crate::scene::model::solid_history::primitive_grips( &self.tabs[i].scene.document, handle, diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 998b8327c..204942df6 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -853,6 +853,184 @@ fn apply_to_v3(target: &mut acadrust::types::Vector3, apply: &GripApply) { } } + + +#[derive(Clone, Copy)] +struct DimTextRelativePosition { + along_fraction: f64, + perpendicular_offset: f64, + z_offset: f64, +} + +fn capture_dim_text_relative_position(dim: &Dimension) -> Option { + let base = dim.base(); + + // Auto-positioned text is recomputed by the renderer from DIMSTYLE. + // Only an explicitly moved text point needs to follow a grip deformation. + if !base.text_user_positioned { + return None; + } + + let text = base.text_middle_point; + if text.x * text.x + text.y * text.y + text.z * text.z <= 1e-16 { + return None; + } + + let (first, second, defpt, ax, ay) = match dim { + Dimension::Linear(d) => ( + d.first_point, + d.second_point, + d.definition_point, + d.rotation.cos(), + d.rotation.sin(), + ), + Dimension::Aligned(d) => { + let dx = d.second_point.x - d.first_point.x; + let dy = d.second_point.y - d.first_point.y; + let len = (dx * dx + dy * dy).sqrt(); + if len <= 1e-12 { + return None; + } + + ( + d.first_point, + d.second_point, + d.definition_point, + dx / len, + dy / len, + ) + } + _ => return None, + }; + + let px = -ay; + let py = ax; + + let t1 = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay; + let t2 = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay; + let text_t = (text.x - defpt.x) * ax + (text.y - defpt.y) * ay; + + let span = t2 - t1; + let along_fraction = if span.abs() > 1e-12 { + (text_t - t1) / span + } else { + 0.5 + }; + + let along = t1 + span * along_fraction; + let line_x = defpt.x + ax * along; + let line_y = defpt.y + ay * along; + + let perpendicular_offset = + (text.x - line_x) * px + (text.y - line_y) * py; + + Some(DimTextRelativePosition { + along_fraction, + perpendicular_offset, + z_offset: text.z - defpt.z, + }) +} + +fn restore_dim_text_relative_position( + dim: &mut Dimension, + saved: DimTextRelativePosition, +) { + let (first, second, defpt, ax, ay) = match dim { + Dimension::Linear(d) => ( + d.first_point, + d.second_point, + d.definition_point, + d.rotation.cos(), + d.rotation.sin(), + ), + Dimension::Aligned(d) => { + let dx = d.second_point.x - d.first_point.x; + let dy = d.second_point.y - d.first_point.y; + let len = (dx * dx + dy * dy).sqrt(); + if len <= 1e-12 { + return; + } + + ( + d.first_point, + d.second_point, + d.definition_point, + dx / len, + dy / len, + ) + } + _ => return, + }; + + let px = -ay; + let py = ax; + + let t1 = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay; + let t2 = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay; + + let along = t1 + (t2 - t1) * saved.along_fraction; + + let base = dim.base_mut(); + base.text_middle_point = Vector3::new( + defpt.x + ax * along + px * saved.perpendicular_offset, + defpt.y + ay * along + py * saved.perpendicular_offset, + defpt.z + saved.z_offset, + ); +} + +fn dimension_line_grip_position(dim: &Dimension) -> Option { + let (first, second, defpt, ax, ay) = match dim { + Dimension::Linear(d) => ( + d.first_point, + d.second_point, + d.definition_point, + d.rotation.cos(), + d.rotation.sin(), + ), + Dimension::Aligned(d) => { + let dx = d.second_point.x - d.first_point.x; + let dy = d.second_point.y - d.first_point.y; + let len = (dx * dx + dy * dy).sqrt(); + + if len <= 1e-12 { + return None; + } + + ( + d.first_point, + d.second_point, + d.definition_point, + dx / len, + dy / len, + ) + } + _ => return None, + }; + + let px = -ay; + let py = ax; + + // Project both extension origins onto the current dimension line. + let off1 = + (defpt.x - first.x) * px + (defpt.y - first.y) * py; + let off2 = + (defpt.x - second.x) * px + (defpt.y - second.y) * py; + + let p1 = DVec3::new( + first.x + px * off1, + first.y + py * off1, + defpt.z, + ); + + let p2 = DVec3::new( + second.x + px * off2, + second.y + py * off2, + defpt.z, + ); + + Some((p1 + p2) * 0.5) +} + impl Grippable for Dimension { fn grips(&self) -> Vec { // Auto-placed dimensions carry a zero text_middle_point sentinel; put @@ -870,13 +1048,21 @@ impl Grippable for Dimension { Dimension::Linear(d) => vec![ square_grip(0, dv3(&d.first_point)), center_grip(1, dv3(&d.second_point)), - center_grip(2, dv3(&d.definition_point)), + center_grip( + 2, + dimension_line_grip_position(self) + .unwrap_or_else(|| dv3(&d.definition_point)), + ), center_grip(3, text), ], Dimension::Aligned(d) => vec![ square_grip(0, dv3(&d.first_point)), center_grip(1, dv3(&d.second_point)), - center_grip(2, dv3(&d.definition_point)), + center_grip( + 2, + dimension_line_grip_position(self) + .unwrap_or_else(|| dv3(&d.definition_point)), + ), center_grip(3, text), ], Dimension::Radius(d) => vec![ @@ -935,6 +1121,8 @@ impl Grippable for Dimension { } } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { // Last grip always moves the text. let text_grip = match self { @@ -953,6 +1141,12 @@ impl Grippable for Dimension { return; } + // A manually positioned dimension text point is stored in the DWG as an + // absolute coordinate. Capture its relation to the old dimension geometry + // before moving a definition grip so that it can be reconstructed against + // the new geometry afterwards. + let relative_text = capture_dim_text_relative_position(self); + match self { Dimension::Linear(d) => match grip_id { 0 => apply_to_v3(&mut d.first_point, &apply), @@ -1013,6 +1207,11 @@ impl Grippable for Dimension { _ => {} }, } + + if let Some(saved) = relative_text { + restore_dim_text_relative_position(self, saved); + } + self.base_mut().actual_measurement = self.measurement(); } @@ -3800,6 +3999,30 @@ pub(crate) fn baked_dimension_text_entity( Some(ent) } +pub(crate) fn dimension_text_grip_position( + dim: &Dimension, + document: &CadDocument, + anno_scale: f64, +) -> Option { + // Once the user has moved the text, its saved point is authoritative. + let base = dim.base(); + if base.text_user_positioned { + let p = base.text_middle_point; + if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 { + return Some(p); + } + } + + // For automatic text, use the same text-building path used by the + // dimension picture so the grip follows the actual DIMSTYLE placement, + // including annotation scaling and fit behaviour. + match baked_dimension_text_entity(dim, document, anno_scale)? { + EntityType::Text(text) => Some(text.insertion_point), + EntityType::MText(text) => Some(text.insertion_point), + _ => None, + } +} + #[cfg(test)] mod dimtad_tests { use super::text_on_dim_line; From 7a8da58d5537668e96871631c3a65ed2d2b39112 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 13 Aug 2026 17:16:29 +0300 Subject: [PATCH 04/38] feat(hatch): add multi-region selection Refs #532 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app/command_driver.rs | 10 +- src/app/commands/draw.rs | 10 +- src/command.rs | 6 + src/modules/draw/draw/hatch.rs | 227 +++++++++++++++++++++++++++++---- src/scene/boundary.rs | 76 ++++++----- src/scene/entity.rs | 130 +++++++++++-------- src/scene/mod.rs | 1 + src/scene/model/hatch_model.rs | 2 + src/scene/preview.rs | 5 + src/ui/properties.rs | 1 + 12 files changed, 352 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd87836de..9841c6ed7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -891,7 +891,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=8d05593#8d05593221cf6158076c38e98fef54ab767b81d9" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1fe426d#1fe426d1922dfc79685b8f6d34ce66ab15d97814" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 42a3ec4c7..c3272527c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4736846", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "8d05593", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1fe426d", features = ["acis", "offset"] } acadifc = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "47b5603", optional = true } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 4491f33dd..04db3aeec 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -14,7 +14,15 @@ impl OpenCADStudio { true } - fn refresh_area_preview(&mut self, i: usize) { + pub(super) fn refresh_area_preview(&mut self, i: usize) { + let hatches = self.tabs[i] + .active_cmd + .as_ref() + .and_then(|command| command.hatch_preview_models()); + if let Some(hatches) = hatches { + self.tabs[i].scene.set_command_preview_hatches(hatches); + return; + } let regions = self.tabs[i] .active_cmd .as_ref() diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 20a3d3934..4fd5a3e26 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -619,9 +619,17 @@ impl OpenCADStudio { "HATCH" => { use crate::modules::draw::draw::hatch::HatchCommand; let outlines = self.tabs[i].scene.hatch_boundary_outlines(); - let new_cmd = HatchCommand::new(outlines); + let boundary_sources = self.tabs[i].scene.hatch_boundary_sources(); + let selected = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .map(|(handle, _)| handle) + .collect(); + let new_cmd = HatchCommand::new(outlines, boundary_sources, selected); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + self.refresh_area_preview(i); } "HATCHEDIT" => { diff --git a/src/command.rs b/src/command.rs index 4ebe6d8b2..5c135ff91 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1844,6 +1844,12 @@ pub trait CadCommand: Send { None } + fn hatch_preview_models( + &self, + ) -> Option> { + None + } + /// Returns `true` when the current step picks a corner of a selection /// *window* by point (e.g. STRETCH's crossing window). Such a pick must be a /// free point: applying the Ortho/Polar lock would pin the opposite corner to diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index 265829137..e8cf5ce96 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -13,6 +13,8 @@ use crate::command::{CadCommand, CmdResult}; use crate::modules::IconKind; use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily}; use crate::scene::model::wire_model::WireModel; +use acadrust::Handle; +use cadkernel::geom2d::{bounded_faces, Line, Tolerance}; use glam::DVec3; use crate::t; @@ -48,6 +50,13 @@ enum Mode { Manual, } +#[derive(Clone, Copy, PartialEq, Eq)] +enum HatchMode { + PickInside, + SelectObjects, + Manual, +} + // ── CPU point-in-polygon (ray casting) ──────────────────────────────────── fn point_in_polygon(p: [f64; 2], poly: &[[f64; 2]]) -> bool { @@ -195,23 +204,91 @@ fn rte_boundary(pts: impl Iterator) -> (Vec<[f32; 2]>, [f64; pub struct HatchCommand { outlines: Vec>, - mode: Mode, + boundary_sources: rustc_hash::FxHashMap>, + point_regions: Vec>>, + object_regions: Vec>>, + selected_objects: Vec, + mode: HatchMode, manual_pts: Vec, missed: bool, } impl HatchCommand { - pub fn new(outlines: Vec>) -> Self { - Self { + pub fn new( + outlines: Vec>, + boundary_sources: rustc_hash::FxHashMap>, + selected_objects: Vec, + ) -> Self { + let has_selection = !selected_objects.is_empty(); + let mut command = Self { outlines, - mode: Mode::PickInside, + boundary_sources, + point_regions: Vec::new(), + object_regions: Vec::new(), + selected_objects: Vec::new(), + mode: if has_selection { + HatchMode::SelectObjects + } else { + HatchMode::PickInside + }, manual_pts: vec![], missed: false, + }; + command.set_object_selection(selected_objects); + command + } + + fn set_object_selection(&mut self, handles: Vec) { + let mut segments = Vec::new(); + for handle in &handles { + if let Some(source) = self.boundary_sources.get(handle) { + segments.extend(source.iter().copied()); + } } + self.object_regions = bounded_faces(&segments, Tolerance::new(1.0e-6)) + .into_iter() + .map(|ring| vec![ring]) + .collect(); + self.missed = !handles.is_empty() && self.object_regions.is_empty(); + self.selected_objects = handles; + } + + fn add_point_region(&mut self, rings: Vec>) { + let duplicate = rings.first().is_some_and(|outer| { + self.point_regions + .iter() + .any(|region| region.first() == Some(outer)) + }); + if !duplicate { + self.point_regions.push(rings); + } + } + + fn region_count(&self) -> usize { + self.point_regions.len() + self.object_regions.len() + } + + fn combined_rings(&self) -> Vec> { + let mut rings = Vec::new(); + for ring in self + .point_regions + .iter() + .chain(self.object_regions.iter()) + .flat_map(|region| region.iter()) + { + if !rings.iter().any(|existing| existing == ring) { + rings.push(ring.clone()); + } + } + rings } fn make_hatch(&self, rings: Vec>) -> HatchModel { let (rel, origin, wcs) = pack_rings(&rings); + let exterior = cadkernel::geom2d::ring_nesting_depths(&rings) + .into_iter() + .map(|depth| depth % 2 == 0) + .collect(); // Default: ANSI31 from catalog; fallback to a single 45° family. let pat_name = "ANSI31"; let families = crate::scene::model::hatch_patterns::find(pat_name) @@ -246,6 +323,7 @@ impl HatchCommand { scale: 1.0, world_origin: origin, boundary_wcs: Some(std::sync::Arc::new(wcs)), + boundary_exterior: Some(std::sync::Arc::new(exterior)), draw_depth: 0.0, } } @@ -258,15 +336,34 @@ impl CadCommand for HatchCommand { fn prompt(&self) -> String { match &self.mode { - Mode::PickInside => { + HatchMode::PickInside => { let miss = if self.missed { t!(" ⚠ No closed boundary found.").into_owned() } else { String::new() }; - t!("HATCH Pick internal point:%{miss}", miss = miss).into_owned() + t!( + "HATCH Pick internal point (%{count} regions selected, Enter to apply):%{miss}", + count = self.region_count(), + miss = miss + ) + .into_owned() } - Mode::Manual => { + HatchMode::SelectObjects => { + let miss = if self.missed { + t!(" ⚠ Selection has no closed boundary.").into_owned() + } else { + String::new() + }; + t!( + "HATCH Select boundary objects (%{objects} objects, %{count} regions; Enter to apply):%{miss}", + objects = self.selected_objects.len(), + count = self.region_count(), + miss = miss + ) + .into_owned() + } + HatchMode::Manual => { if self.manual_pts.is_empty() { t!("HATCH Boundary point 1:").into_owned() } else { @@ -280,8 +377,27 @@ impl CadCommand for HatchCommand { fn options(&self) -> Vec { use crate::command::CmdOption; match &self.mode { - Mode::PickInside => vec![CmdOption::new(t!("Draw manually").as_ref(), "S")], - Mode::Manual => { + HatchMode::PickInside => { + let mut options = vec![ + CmdOption::new(t!("Select objects").as_ref(), "O"), + CmdOption::new(t!("Draw manually").as_ref(), "S"), + ]; + if self.region_count() > 0 { + options.push(CmdOption::enter(t!("Accept").as_ref())); + } + options + } + HatchMode::SelectObjects => { + let mut options = vec![ + CmdOption::new(t!("Pick internal points").as_ref(), "I"), + CmdOption::new(t!("Draw manually").as_ref(), "S"), + ]; + if self.region_count() > 0 { + options.push(CmdOption::enter(t!("Accept").as_ref())); + } + options + } + HatchMode::Manual => { // Enter accepts the boundary once at least 3 points are picked. if self.manual_pts.len() >= 3 { vec![CmdOption::enter(t!("Accept").as_ref())] @@ -294,12 +410,13 @@ impl CadCommand for HatchCommand { fn on_point(&mut self, pt: DVec3) -> CmdResult { match &self.mode { - Mode::PickInside => { + HatchMode::PickInside => { let xy = [pt.x, pt.y]; match resolve_hatch_rings(&self.outlines, xy) { Some(rings) => { self.missed = false; - return CmdResult::CommitHatch(self.make_hatch(rings)); + self.add_point_region(rings); + CmdResult::NeedPoint } None => { self.missed = true; @@ -307,7 +424,8 @@ impl CadCommand for HatchCommand { } } } - Mode::Manual => { + HatchMode::SelectObjects => CmdResult::NeedPoint, + HatchMode::Manual => { // Keep the typed/snapped point exact (issue #311). self.manual_pts.push(pt); CmdResult::NeedPoint @@ -316,16 +434,55 @@ impl CadCommand for HatchCommand { } fn on_enter(&mut self) -> CmdResult { - match &self.mode { - Mode::PickInside => CmdResult::Cancel, - Mode::Manual => { - if self.manual_pts.len() < 3 { - return CmdResult::Cancel; - } - let wcs = self.manual_pts.iter().map(|p| [p.x, p.y]).collect(); - CmdResult::CommitHatch(self.make_hatch(vec![wcs])) - } + if matches!(self.mode, HatchMode::Manual) && self.manual_pts.len() >= 3 { + let ring = self.manual_pts.iter().map(|p| [p.x, p.y]).collect(); + self.add_point_region(vec![ring]); } + let rings = self.combined_rings(); + if rings.is_empty() { + CmdResult::Cancel + } else { + CmdResult::CommitHatch(self.make_hatch(rings)) + } + } + + fn is_selection_gathering(&self) -> bool { + matches!(self.mode, HatchMode::SelectObjects) + } + + fn selection_forces_add(&self) -> bool { + matches!(self.mode, HatchMode::SelectObjects) + } + + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + if matches!(self.mode, HatchMode::SelectObjects) { + self.set_object_selection(handles); + CmdResult::NeedPoint + } else { + CmdResult::Cancel + } + } + + fn on_undo_step(&mut self) -> Option { + if matches!(self.mode, HatchMode::PickInside) && self.point_regions.pop().is_some() { + Some(CmdResult::NeedPoint) + } else { + None + } + } + + fn hatch_preview_models(&self) -> Option> { + let mut rings = self.combined_rings(); + if matches!(self.mode, HatchMode::Manual) && self.manual_pts.len() >= 3 { + rings.push(self.manual_pts.iter().map(|point| [point.x, point.y]).collect()); + } + Some(if rings.is_empty() { + Vec::new() + } else { + let mut preview = self.make_hatch(rings); + preview.color = [0.15, 0.55, 1.0, 0.75]; + vec![preview] + }) } fn on_escape(&mut self) -> CmdResult { @@ -333,20 +490,32 @@ impl CadCommand for HatchCommand { } fn wants_text_input(&self) -> bool { - matches!(self.mode, Mode::PickInside) + !matches!(self.mode, HatchMode::Manual) } fn on_text_input(&mut self, text: &str) -> Option { - if text.trim().eq_ignore_ascii_case("s") { - self.mode = Mode::Manual; - self.missed = false; - return Some(CmdResult::NeedPoint); + match text.trim().to_ascii_uppercase().as_str() { + "O" | "OBJECT" | "OBJECTS" => { + self.mode = HatchMode::SelectObjects; + self.missed = false; + Some(CmdResult::NeedPoint) + } + "I" | "INTERNAL" => { + self.mode = HatchMode::PickInside; + self.missed = false; + Some(CmdResult::NeedPoint) + } + "S" => { + self.mode = HatchMode::Manual; + self.missed = false; + Some(CmdResult::NeedPoint) + } + _ => None, } - None } fn on_mouse_move(&mut self, pt: DVec3) -> Option { let pt = pt.as_vec3(); - if let Mode::Manual = &self.mode { + if let HatchMode::Manual = &self.mode { if self.manual_pts.is_empty() { return None; } @@ -416,6 +585,7 @@ impl GradientCommand { scale: 1.0, world_origin: origin, boundary_wcs: Some(std::sync::Arc::new(wcs)), + boundary_exterior: None, draw_depth: 0.0, } } @@ -627,6 +797,7 @@ impl CadCommand for BoundaryCommand { scale: 1.0, world_origin: origin, boundary_wcs: Some(std::sync::Arc::new(wcs)), + boundary_exterior: None, draw_depth: 0.0, }; CmdResult::CommitHatch(model) diff --git a/src/scene/boundary.rs b/src/scene/boundary.rs index d6b4d4bc9..3c1a5438b 100644 --- a/src/scene/boundary.rs +++ b/src/scene/boundary.rs @@ -10,6 +10,31 @@ use cadkernel::geom2d::{bounded_faces, Line, Tolerance}; /// enough not to weld genuinely separate corners together. const WELD_TOLERANCE: f64 = 1.0e-6; +fn wire_segments(wire: &WireModel) -> Vec { + let mut segments = Vec::new(); + let mut previous: Option<[f64; 2]> = None; + + for (index, high) in wire.points.iter().copied().enumerate() { + if !high[0].is_finite() || !high[1].is_finite() { + previous = None; + continue; + } + let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]); + let current = [ + high[0] as f64 + low[0] as f64, + high[1] as f64 + low[1] as f64, + ]; + if let Some(start) = previous { + let (dx, dy) = (current[0] - start[0], current[1] - start[1]); + if dx.hypot(dy) > WELD_TOLERANCE { + segments.push(Line { start, end: current }); + } + } + previous = Some(current); + } + segments +} + impl Scene { /// Build closed planar regions from the visible wire geometry. /// @@ -30,41 +55,26 @@ impl Scene { let mut segments = Vec::::new(); for wire in self.entity_wires().iter() { - let mut previous: Option<[f64; 2]> = None; - - for (index, high) in wire.points.iter().copied().enumerate() { - // NaNs delimit independent segments inside some WireModels, - // notably polylines stored as A-B | B-C | C-D. - if !high[0].is_finite() || !high[1].is_finite() { - previous = None; - continue; - } - - // The wire carries its coordinates as a double-single pair, so - // both halves are needed to recover the f64 the tessellation - // produced. Reading only the high half would put every vertex - // of a survey-coordinate drawing on a grid coarser than the - // weld tolerance. - let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]); - let current = [ - high[0] as f64 + low[0] as f64, - high[1] as f64 + low[1] as f64, - ]; - - if let Some(start) = previous { - let (dx, dy) = (current[0] - start[0], current[1] - start[1]); - if dx.hypot(dy) > WELD_TOLERANCE { - segments.push(Line { - start, - end: current, - }); - } - } - - previous = Some(current); - } + segments.extend(wire_segments(wire)); } bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE)) } + + /// Tessellated boundary segments grouped by their selectable entity. + pub fn hatch_boundary_sources( + &self, + ) -> rustc_hash::FxHashMap> { + let mut sources = rustc_hash::FxHashMap::default(); + for wire in self.entity_wires().iter() { + let Some(handle) = Self::handle_from_wire_name(&wire.name) else { + continue; + }; + sources + .entry(handle) + .or_insert_with(Vec::new) + .extend(wire_segments(wire)); + } + sources + } } diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 130bd0f80..7bc02908c 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1362,6 +1362,7 @@ impl Scene { render_instance, boundary: Arc::new(boundary), boundary_wcs: None, + boundary_exterior: None, pattern: model::hatch_model::HatchPattern::Solid, name: "WIPEOUT_FILL".into(), color, @@ -1466,6 +1467,7 @@ impl Scene { } let mut boundary: Vec<[f64; 2]> = Vec::new(); + let mut boundary_exterior = Vec::new(); for path in &dxf.paths { // Skip TEXTBOX boundary paths (flag bit 3). These are text @@ -1499,6 +1501,7 @@ impl Scene { boundary.truncate(before_path); continue; } + boundary_exterior.push(path.flags.is_external() || path.flags.is_outermost()); if boundary.len() >= path_start + 3 { let first = boundary[path_start]; let last = *boundary.last().unwrap(); @@ -1511,6 +1514,11 @@ impl Scene { if boundary.is_empty() { return None; } + if !boundary_exterior.iter().any(|role| *role) { + if let Some(first) = boundary_exterior.first_mut() { + *first = true; + } + } // The batched hatch renderer keeps boundaries in a GPU storage // buffer (no fixed length), so a hatch with many island loops must // retain *every* loop or even-odd island detection breaks. The old @@ -1705,6 +1713,7 @@ impl Scene { render_instance: None, boundary: std::sync::Arc::new(boundary_f32), boundary_wcs: None, + boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)), pattern, name, // A gradient starts from its first stop; other fills use the @@ -2019,6 +2028,7 @@ impl Scene { render_instance: None, boundary: std::sync::Arc::new(boundary), boundary_wcs: None, + boundary_exterior: None, pattern: model::hatch_model::HatchPattern::Solid, name: "SOLID".into(), color, @@ -2037,64 +2047,74 @@ impl Scene { model.pattern, crate::scene::model::hatch_model::HatchPattern::Solid ); - // Prefer the command-supplied exact f64 boundary so a typed vertex is - // persisted without f32 quantization (issue #311). Falling back to the - // render-side `boundary`, the points arrive in local render space - // (world_offset already subtracted), so add `world_origin` back — - // otherwise the boundary wire lands `world_offset` away from the fill. - // Build one DXF boundary path per ring. The command layer separates - // the outer boundary from its holes with NaN sentinels in - // `boundary_wcs`; split on those so nested hatches (e.g. a small - // rectangle inside a big one) persist with real holes instead of a - // single self-intersecting polyline. Only the FIRST ring carries the - // external / outermost flags — every later ring is a hole and must not - // be flagged external, or DXF/DWG consumers treat the inner loop as - // another outer island rather than a hole. - if let Some(wcs) = &model.boundary_wcs { - let mut ring: Vec = Vec::new(); - let mut first = true; - let mut push_ring = |r: &mut Vec, is_outer: bool| { - if !r.is_empty() { - let edge = PolylineEdge::new(std::mem::take(r), true); - let mut path = if is_outer { - let mut p = BoundaryPath::external(); - p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits( - p.flags.bits() - | acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(), - ); - p - } else { - BoundaryPath::new() - }; - path.add_edge(BoundaryEdge::Polyline(edge)); - dxf.paths.push(path); - } - }; - for &[x, y] in wcs.iter() { - if x.is_finite() && y.is_finite() { - ring.push(Vector2::new(x, y)); - } else { - let is_outer = first; - first = false; - push_ring(&mut ring, is_outer); - } - } - push_ring(&mut ring, first); - } - if dxf.paths.is_empty() { - let wx = model.world_origin[0]; - let wy = model.world_origin[1]; - let verts: Vec = model + // Prefer exact command geometry; otherwise reconstruct every ring from + // the render offsets without dropping its separators. + // Build one DXF path per NaN-separated ring and retain each outer/hole role. + let reconstructed_wcs: Vec<[f64; 2]> = if model.boundary_wcs.is_none() { + let [wx, wy] = model.world_origin; + model .boundary .iter() - .filter(|v| v[0].is_finite() && v[1].is_finite()) - .map(|&[x, y]| Vector2::new(x as f64 + wx, y as f64 + wy)) - .collect(); - let edge = PolylineEdge::new(verts, true); - let mut path = BoundaryPath::external(); - path.add_edge(BoundaryEdge::Polyline(edge)); - dxf.paths.push(path); + .map(|&[x, y]| { + if x.is_finite() && y.is_finite() { + [x as f64 + wx, y as f64 + wy] + } else { + [f64::NAN, f64::NAN] + } + }) + .collect() + } else { + Vec::new() + }; + let wcs = model + .boundary_wcs + .as_deref() + .map(|points| points.as_slice()) + .unwrap_or(reconstructed_wcs.as_slice()); + let mut ring: Vec = Vec::new(); + let mut first = true; + let mut ring_index = 0usize; + let mut push_ring = |r: &mut Vec, is_outer: bool| { + if !r.is_empty() { + let edge = PolylineEdge::new(std::mem::take(r), true); + let mut path = if is_outer { + let mut p = BoundaryPath::external(); + p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits( + p.flags.bits() + | acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(), + ); + p + } else { + BoundaryPath::new() + }; + path.add_edge(BoundaryEdge::Polyline(edge)); + dxf.paths.push(path); + } + }; + for &[x, y] in wcs { + if x.is_finite() && y.is_finite() { + ring.push(Vector2::new(x, y)); + } else { + let is_outer = model + .boundary_exterior + .as_deref() + .and_then(|roles| roles.get(ring_index)) + .copied() + .unwrap_or(first); + if !ring.is_empty() { + ring_index += 1; + } + first = false; + push_ring(&mut ring, is_outer); + } } + let is_outer = model + .boundary_exterior + .as_deref() + .and_then(|roles| roles.get(ring_index)) + .copied() + .unwrap_or(first); + push_ring(&mut ring, is_outer); if let Some(entry) = crate::scene::model::hatch_patterns::find(&model.name) { dxf.pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry); } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 85245b5e2..205794af1 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3181,6 +3181,7 @@ impl Scene { world_origin: [0.0, 0.0], boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]), boundary_wcs: None, + boundary_exterior: None, pattern: crate::scene::model::hatch_model::HatchPattern::Solid, name: "SOLID".to_string(), color: self.paper_bg_color, diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs index 492ce02cd..4572d2acc 100644 --- a/src/scene/model/hatch_model.rs +++ b/src/scene/model/hatch_model.rs @@ -180,6 +180,8 @@ pub struct HatchModel { /// rebuilt from a DXF entity — `add_hatch` then reconstructs the persisted /// vertices from `boundary` + `world_origin` instead. pub boundary_wcs: Option>>, + /// Per-ring DXF role, aligned with the NaN-separated boundary paths. + pub boundary_exterior: Option>>, /// Fill pattern. pub pattern: HatchPattern, /// Catalog name for this pattern (e.g. "ANSI31", "SOLID", "LINEAR"). diff --git a/src/scene/preview.rs b/src/scene/preview.rs index a6b42d9ea..08b63d926 100644 --- a/src/scene/preview.rs +++ b/src/scene/preview.rs @@ -29,6 +29,10 @@ impl Scene { self.preview_hatches = std::sync::Arc::new(models); } + pub fn set_command_preview_hatches(&mut self, models: Vec) { + self.preview_hatches = std::sync::Arc::new(models); + } + pub fn set_area_preview_regions(&mut self, regions: &[AreaPreviewRegion]) { let mut models = Vec::new(); for region in regions { @@ -154,6 +158,7 @@ impl Scene { world_origin: origin, boundary: std::sync::Arc::new(boundary), boundary_wcs: None, + boundary_exterior: None, pattern: HatchPattern::Solid, name: "AREA_PREVIEW".into(), color: [0.0; 4], diff --git a/src/ui/properties.rs b/src/ui/properties.rs index fda11b9cc..55fd780af 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -130,6 +130,7 @@ impl canvas::Program for HatchPatternPreview { [pad, bounds.height - pad], ]), boundary_wcs: None, + boundary_exterior: None, pattern: self.pattern.clone(), name: String::new(), color: [1.0; 4], From 2088372672e938f5535641b8905184e70da7b5bc Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 13 Aug 2026 17:25:46 +0300 Subject: [PATCH 05/38] fix(hatch): preserve preview pattern origin Refs #532 --- src/scene/entity.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 7bc02908c..454784885 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -2115,15 +2115,31 @@ impl Scene { .copied() .unwrap_or(first); push_ring(&mut ring, is_outer); - if let Some(entry) = crate::scene::model::hatch_patterns::find(&model.name) { - dxf.pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry); - } - dxf.pattern_angle = model.angle_offset as f64; - dxf.pattern_scale = if model.scale.abs() > 1e-6 { + let pattern_scale = if model.scale.abs() > 1e-6 { model.scale as f64 } else { 1.0 }; + if let Some(entry) = crate::scene::model::hatch_patterns::find(&model.name) { + dxf.pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry); + if let Some(base) = dxf.pattern.lines.first().map(|line| line.base_point) { + let angle = model.angle_offset as f64; + let (sin, cos) = angle.sin_cos(); + let target_x = model.world_origin[0] + + (base.x * cos - base.y * sin) * pattern_scale; + let target_y = model.world_origin[1] + + (base.x * sin + base.y * cos) * pattern_scale; + crate::entities::hatch::scale_pattern_geometry(&mut dxf.pattern, pattern_scale); + crate::entities::hatch::rotate_pattern_geometry(&mut dxf.pattern, angle); + crate::entities::hatch::translate_pattern_geometry( + &mut dxf.pattern, + target_x - base.x, + target_y - base.y, + ); + } + } + dxf.pattern_angle = model.angle_offset as f64; + dxf.pattern_scale = pattern_scale; // A gradient fill must be encoded on the DXF entity itself: the render // model is rebuilt from the entity below (`add_entity` → // `hatch_model_from_dxf`), so a gradient kept only on the command's From 2d1ee0d9367abed9d9db6d64c53b59e95d3617dd Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Thu, 13 Aug 2026 11:58:54 -0300 Subject: [PATCH 06/38] fix: improve classic leader behavior and annotation linkage --- src/app/command_driver.rs | 64 +++++++++++-- src/entities/leader.rs | 56 ++++++++++- src/modules/annotate/leader_cmd.rs | 148 +++++++++++++++++++---------- src/modules/draw/mod.rs | 12 +-- src/scene/annotative.rs | 10 +- src/scene/modify.rs | 144 +++++++++++++++++++++++++++- src/scene/selection.rs | 57 ++++++++++- 7 files changed, 418 insertions(+), 73 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 4491f33dd..8cc2e7441 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1176,10 +1176,27 @@ impl OpenCADStudio { // Link the leader to its annotation so the pair edits as a unit // (double-click on the leader resolves to the text entity). if let (Some(lh), Some(ah)) = (leader_handle, edit_handle) { - if let Some(acadrust::EntityType::Leader(l)) = + let linked = if let Some(acadrust::EntityType::Leader(l)) = self.tabs[i].scene.document.get_entity_mut(lh) { l.annotation_handle = ah; + true + } else { + false + }; + + if linked { + // The LEADER may already have received its annotation context while + // annotation_handle was still NULL. Refresh it now that the MTEXT link + // is known so the context represents the finished leader. + self.tabs[i] + .scene + .sync_displayed_annotation_context(lh); + + self.tabs[i].scene.bump_entities(&[( + lh, + crate::scene::ChangeKind::Modified, + )]); } } self.tabs[i].dirty = true; @@ -3367,6 +3384,45 @@ impl OpenCADStudio { _ => glam::DVec3::ZERO, }; self.merge_clipboard_ext_objects(i, &by_index, annotation_delta); + // Source handles stored in the clipboard map one-to-one to the freshly + // pasted handles. Use that map to reconnect LEADER -> copied annotation. + let mut handle_map = rustc_hash::FxHashMap::default(); + + for (source, &copied) in self.clipboard.iter().zip(by_index.iter()) { + if !copied.is_null() { + handle_map.insert(source.common().handle, copied); + } + } + + let leader_links: Vec<(Handle, Handle)> = self + .clipboard + .iter() + .filter_map(|source| { + let acadrust::EntityType::Leader(leader) = source else { + return None; + }; + + let copied_leader = handle_map.get(&source.common().handle).copied()?; + let copied_annotation = handle_map + .get(&leader.annotation_handle) + .copied() + .unwrap_or(Handle::NULL); + + Some((copied_leader, copied_annotation)) + }) + .collect(); + + for (leader_handle, annotation_handle) in leader_links { + if let Some(acadrust::EntityType::Leader(leader)) = + self.tabs[i].scene.document.get_entity_mut(leader_handle) + { + leader.annotation_handle = annotation_handle; + } + + let _ = self.tabs[i] + .scene + .sync_displayed_annotation_context(leader_handle); + } // Recreate any group whose whole membership was copied, so a pasted // group stays grouped — cross-drawing too, since the groups were // snapshotted into the clipboard at copy time. `by_index` is aligned @@ -3374,12 +3430,6 @@ impl OpenCADStudio { // its clipboard clone to its new handle. Same shared `recreate_groups` // the in-drawing COPY path uses. (#440) if !self.clipboard_deps.groups.is_empty() { - let mut handle_map = rustc_hash::FxHashMap::default(); - for (src, &new) in self.clipboard.iter().zip(by_index.iter()) { - if !new.is_null() { - handle_map.insert(src.common().handle, new); - } - } let groups = self.clipboard_deps.groups.clone(); self.tabs[i].scene.recreate_groups(groups, &handle_map); } diff --git a/src/entities/leader.rs b/src/entities/leader.rs index 7d989e53c..f42f8abc1 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -159,7 +159,59 @@ fn grips(leader: &Leader) -> Vec { fn apply_grip(leader: &mut Leader, grip_id: usize, apply: GripApply) { let n = leader.vertices.len(); + if grip_id < n { + if n >= 3 && leader.creation_type == LeaderCreationType::WithText { + // Grip del codo: mueve el codo libremente, pero arrastra también + // el extremo del renglón manteniendo la distancia relativa. + if grip_id == n - 2 { + let old_elbow = leader.vertices[n - 2]; + let old_end = leader.vertices[n - 1]; + + let delta = match apply { + GripApply::Absolute(p) => acadrust::types::Vector3::new( + p.x as f64 - old_elbow.x, + p.y as f64 - old_elbow.y, + p.z as f64 - old_elbow.z, + ), + GripApply::Translate(d) => acadrust::types::Vector3::new( + d.x as f64, + d.y as f64, + d.z as f64, + ), + }; + + leader.vertices[n - 2].x = old_elbow.x + delta.x; + leader.vertices[n - 2].y = old_elbow.y + delta.y; + leader.vertices[n - 2].z = old_elbow.z + delta.z; + + leader.vertices[n - 1].x = old_end.x + delta.x; + leader.vertices[n - 1].y = old_end.y + delta.y; + leader.vertices[n - 1].z = old_end.z + delta.z; + + return; + } + + // Grip del extremo horizontal: sólo debe estirar en X; + // Y/Z quedan pegados al codo para que siga horizontal. + if grip_id == n - 1 { + let elbow = leader.vertices[n - 2]; + + match apply { + GripApply::Absolute(p) => { + leader.vertices[n - 1].x = p.x as f64; + } + GripApply::Translate(d) => { + leader.vertices[n - 1].x += d.x as f64; + } + } + + leader.vertices[n - 1].y = elbow.y; + leader.vertices[n - 1].z = elbow.z; + return; + } + } + if let Some(v) = leader.vertices.get_mut(grip_id) { match apply { GripApply::Absolute(p) => { @@ -176,7 +228,9 @@ fn apply_grip(leader: &mut Leader, grip_id: usize, apply: GripApply) { } } else if let GripApply::Translate(d) = apply { leader.translate(acadrust::types::Vector3::new( - d.x as f64, d.y as f64, d.z as f64, + d.x as f64, + d.y as f64, + d.z as f64, )); } } diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index 1f67116f2..48b6d4ad6 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -53,6 +53,7 @@ impl LeaderCommand { } else { defaults.scale }; + Self { verts: Vec::new(), plane: WorkingPlane::default(), @@ -65,6 +66,78 @@ impl LeaderCommand { annotative: defaults.annotative, } } + + fn finish(&self) -> CmdResult { + if self.verts.len() < 2 { + return CmdResult::Cancel; + } + + let local: Vec = self + .verts + .iter() + .map(|point| self.plane.to_local(*point)) + .collect(); + + let displayed_height = self.text_height * self.display_scale; + + let displayed_landing = if self.arrow_size > 1.0e-9 { + self.arrow_size * self.display_scale + } else { + displayed_height * 1.5 + }; + + // The user supplies only the arrow point and elbow. The horizontal + // landing is stored as a real third LEADER vertex so it can have its own grip. + let mut leader_points = local.clone(); + + let first = local[0]; + let elbow = local[1]; + let sign = if elbow.x >= first.x { 1.0 } else { -1.0 }; + + let landing_end = DVec3::new( + elbow.x + sign * displayed_landing, + elbow.y, + elbow.z, + ); + + leader_points.push(landing_end); + + let leader = build_leader( + &leader_points, + Mat4::IDENTITY, + &self.dimension_style, + self.text_height, + self.gap, + self.arrow_size, + ); + + // The MTEXT starts at the real end of the landing. + let (anchor, attach) = + annotation_anchor(&leader_points, 0.0, Mat4::IDENTITY); + + // Store the MTEXT at its native model-space size for the current + // annotation scale. Its annotation contexts then scale it relatively + // when another representation becomes active. + let mtext_height = displayed_height; + + let mtext = build_mtext( + "", + anchor, + mtext_height, + attach, + Mat4::IDENTITY, + &self.text_style, + self.annotative, + ); + + CmdResult::CommitManyAndEditText { + entities: vec![ + self.plane.place_entity(EntityType::Leader(leader)), + self.plane.place_entity(EntityType::MText(mtext)), + ], + edit_index: 1, + } + } } impl CadCommand for LeaderCommand { @@ -80,61 +153,22 @@ impl CadCommand for LeaderCommand { if self.verts.is_empty() { t!("LEADER Specify arrowhead point:").into_owned() } else { - t!( - "LEADER Specify next point [%{count} pts — Enter to place text]:", - count = self.verts.len() - ) - .into_owned() + t!("LEADER Specify landing point:").into_owned() } } fn on_point(&mut self, pt: DVec3) -> CmdResult { self.verts.push(pt); - CmdResult::NeedPoint + + if self.verts.len() >= 2 { + self.finish() + } else { + CmdResult::NeedPoint + } } fn on_enter(&mut self) -> CmdResult { - if self.verts.len() < 2 { - return CmdResult::Cancel; - } - // Place the leader plus an empty MText annotation, link them, then open - // the in-place MText editor so the user types the annotation text. - let local: Vec = self - .verts - .iter() - .map(|point| self.plane.to_local(*point)) - .collect(); - let leader = build_leader( - &local, - Mat4::IDENTITY, - &self.dimension_style, - self.text_height, - self.gap, - self.arrow_size, - ); - let displayed_height = self.text_height * self.display_scale; - let (anchor, attach) = annotation_anchor(&local, displayed_height, Mat4::IDENTITY); - let mtext_height = if self.annotative { - self.text_height - } else { - displayed_height - }; - let mtext = build_mtext( - "", - anchor, - mtext_height, - attach, - Mat4::IDENTITY, - &self.text_style, - self.annotative, - ); - CmdResult::CommitManyAndEditText { - entities: vec![ - self.plane.place_entity(EntityType::Leader(leader)), - self.plane.place_entity(EntityType::MText(mtext)), - ], - edit_index: 1, - } + self.finish() } fn on_escape(&mut self) -> CmdResult { @@ -189,7 +223,10 @@ fn build_leader( ) -> Leader { let mut l = Leader::from_vertices(verts.iter().map(|p| dv3(*p)).collect()); l.creation_type = LeaderCreationType::WithText; - l.hookline_enabled = true; + + // New leaders store the landing as their final real vertex, so the renderer + // must not append a second synthetic hookline. + l.hookline_enabled = false; l.dimension_style = dimension_style.to_string(); l.text_height = text_height; l.dimension_gap = gap; @@ -206,21 +243,28 @@ fn build_leader( /// left-pointing landing, to the left of a right-pointing one). fn annotation_anchor( verts: &[DVec3], - text_height: f64, + landing_length: f64, ucs: Mat4, ) -> (DVec3, AttachmentPoint) { let last = *verts.last().unwrap(); let prev = verts[verts.len() - 2]; - // Side + landing run along the UCS X axis (identity = world). - let ux = ucs.transform_vector3(Vec3::X).normalize_or(Vec3::X).as_dvec3(); + + let ux = ucs + .transform_vector3(Vec3::X) + .normalize_or(Vec3::X) + .as_dvec3(); + let to_right = (last - prev).dot(ux) >= 0.0; let sign = if to_right { 1.0_f64 } else { -1.0_f64 }; - let anchor = last + ux * (sign * text_height * 1.5); + + let anchor = last + ux * (sign * landing_length); + let attach = if to_right { AttachmentPoint::MiddleLeft } else { AttachmentPoint::MiddleRight }; + (anchor, attach) } diff --git a/src/modules/draw/mod.rs b/src/modules/draw/mod.rs index 1f265e43c..9b860ea12 100644 --- a/src/modules/draw/mod.rs +++ b/src/modules/draw/mod.rs @@ -160,18 +160,18 @@ impl CadModule for DrawModule { label: "Leader", icon: leader_cmd::ICON, items: vec![ - ( - mleader_cmd::tool().id, - mleader_cmd::tool().label, - mleader_cmd::tool().icon, - ), ( leader_cmd::tool().id, leader_cmd::tool().label, leader_cmd::tool().icon, ), + ( + mleader_cmd::tool().id, + mleader_cmd::tool().label, + mleader_cmd::tool().icon, + ), ], - default: "MLEADER", + default: "LEADER", }, ], }, diff --git a/src/scene/annotative.rs b/src/scene/annotative.rs index 2badd3a72..bef6d168d 100644 --- a/src/scene/annotative.rs +++ b/src/scene/annotative.rs @@ -480,7 +480,15 @@ pub fn annotative_offscale_for( }), } } - +pub(crate) fn annotation_scale_handles_for_entity( + doc: &CadDocument, + entity_handle: Handle, +) -> Vec { + object_scale_memberships(doc, entity_handle) + .into_iter() + .map(|(_, scale_handle)| scale_handle) + .collect() +} pub fn scale_handle_by_name(doc: &CadDocument, name: &str) -> Option { doc.objects.iter().find_map(|(handle, object)| match object { ObjectType::Scale(scale) diff --git a/src/scene/modify.rs b/src/scene/modify.rs index ae1676514..c215ef127 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -551,11 +551,43 @@ impl Scene { } pub fn copy_entities(&mut self, handles: &[Handle], t: &EntityTransform) -> Vec { + let copy_handles = self.handles_expanded_for_leader_annotations(handles); + + // LEADER + attached MTEXT are a logical pair. Their entity clones must not + // retain the source extension dictionary, otherwise both copies share the + // same annotation-context objects. + let leader_pair_handles: Vec = copy_handles + .iter() + .flat_map(|&handle| { + let annotation = match self.document.get_entity(handle) { + Some(EntityType::Leader(leader)) if !leader.annotation_handle.is_null() => { + Some(leader.annotation_handle) + } + _ => None, + }; + + std::iter::once(handle).chain(annotation) + }) + .collect(); + // Objects on a locked layer can be selected but not copied. - let clones: Vec<(Handle, EntityType)> = handles + let clones: Vec<(Handle, EntityType, Vec)> = copy_handles .iter() .filter(|&&h| !self.is_layer_locked(h)) - .filter_map(|&h| self.document.get_entity(h).cloned().map(|e| (h, e))) + .filter_map(|&h| { + let entity = self.document.get_entity(h)?.clone(); + + let annotation_scales = if leader_pair_handles.contains(&h) { + crate::scene::annotative::annotation_scale_handles_for_entity( + &self.document, + h, + ) + } else { + Vec::new() + }; + + Some((h, entity, annotation_scales)) + }) .collect(); // MIRRTEXT also governs the copy path (default MIRROR keeps the source // and adds a mirrored copy): keep the copied text right-reading when the @@ -567,7 +599,7 @@ impl Scene { let mut new_handles = Vec::with_capacity(clones.len()); let mut handle_map = rustc_hash::FxHashMap::default(); let mut refresh_solid_handles = Vec::new(); - for (src_handle, mut entity) in clones { + for (src_handle, mut entity, annotation_scales) in clones { let text_orient = if preserve_text_orientation { capture_text_orient(&entity) } else { @@ -598,9 +630,31 @@ impl Scene { } } Self::reset_clone_subhandles(&mut self.document, &mut entity); + + // An annotative LEADER/MTEXT pair must receive a fresh extension dictionary. + // Keeping this handle would make the copy share the source's context tree. + if !annotation_scales.is_empty() { + entity.common_mut().xdictionary_handle = None; + } + entity.common_mut().handle = Handle::NULL; let h = self.document.add_entity(entity).unwrap_or(Handle::NULL); if !h.is_null() { + if !annotation_scales.is_empty() { + for scale_handle in annotation_scales { + crate::scene::annotative::create_annotation_context( + &mut self.document, + h, + scale_handle, + ); + } + + // Annotation contexts add dictionary/object records outside the entity + // delta itself, so keep undo on the safe full-snapshot path. + if self.is_recording_undo() { + self.poison_undo_recording(); + } + } // Delta-undo: a copy's before-image is "nothing" (undo erases it). if self.is_recording_undo() { self.record_undo_before(h, None); @@ -640,7 +694,35 @@ impl Scene { handle_map.insert(src_handle, h); } } + // A copied LEADER must reference the copied annotation, never the + // source annotation. Both entities now exist, so remap the stored handle. + let leader_links: Vec<(Handle, Handle)> = handle_map + .iter() + .filter_map(|(&source_handle, &copied_handle)| { + let EntityType::Leader(source_leader) = + self.document.get_entity(source_handle)? + else { + return None; + }; + let copied_annotation = handle_map + .get(&source_leader.annotation_handle) + .copied() + .unwrap_or(Handle::NULL); + + Some((copied_handle, copied_annotation)) + }) + .collect(); + + for (leader_handle, annotation_handle) in leader_links { + if let Some(EntityType::Leader(leader)) = + self.document.get_entity_mut(leader_handle) + { + leader.annotation_handle = annotation_handle; + } + + let _ = self.sync_displayed_annotation_context(leader_handle); + } // Complete group copies record their new Group objects and dictionary // entry as targeted object deltas inside copy_complete_groups. self.copy_complete_groups(&handle_map); @@ -956,10 +1038,66 @@ impl Scene { .get_entity(handle) .and_then(crate::entities::solid3d::point_of_reference) .map(|p| [p.x, p.y, p.z]); + // A LEADER's final vertex is the end of its horizontal landing. + // Remember its old position and linked MTEXT so the annotation can follow + // when that grip stretches the landing. + let leader_landing_before = self.document.get_entity(handle).and_then(|entity| { + let EntityType::Leader(leader) = entity else { + return None; + }; + let n = leader.vertices.len(); + if n < 3 || (grip_id != n - 1 && grip_id != n - 2) || leader.annotation_handle.is_null() { + return None; + } + + let point = leader.vertices.last()?; + + Some(( + leader.annotation_handle, + glam::DVec3::new(point.x, point.y, point.z), + )) + }); if let Some(entity) = self.document.get_entity_mut(handle) { view::dispatch::apply_grip(entity, grip_id, apply); } + if let Some((annotation_handle, old_landing)) = leader_landing_before { + let new_landing = self.document.get_entity(handle).and_then(|entity| { + let EntityType::Leader(leader) = entity else { + return None; + }; + + let point = leader.vertices.last()?; + Some(glam::DVec3::new(point.x, point.y, point.z)) + }); + + if let Some(new_landing) = new_landing { + let delta = new_landing - old_landing; + + if delta.length_squared() > 1.0e-20 { + if self.is_recording_undo() { + if let Some(before) = self.document.get_entity_arc(annotation_handle) { + self.record_undo_before(annotation_handle, Some(before)); + } + } + + if let Some(annotation) = self.document.get_entity_mut(annotation_handle) { + view::dispatch::apply_transform( + annotation, + &crate::command::EntityTransform::Translate(delta), + ); + } + + if self.sync_displayed_annotation_context(annotation_handle) { + self.poison_undo_recording(); + } + self.bump_entities(&[( + annotation_handle, + crate::scene::ChangeKind::Modified, + )]); + } + } + } if self.sync_displayed_annotation_context(handle) { self.poison_undo_recording(); } diff --git a/src/scene/selection.rs b/src/scene/selection.rs index 80ac65af0..b073a1956 100644 --- a/src/scene/selection.rs +++ b/src/scene/selection.rs @@ -3,12 +3,46 @@ use super::*; impl Scene { // ── Selection ───────────────────────────────────────────────────────── + /// Treat a classic LEADER and its attached annotation as one logical object. + /// Clicking/copying/deleting either side expands to the complete pair. + pub(crate) fn handles_expanded_for_leader_annotations( + &self, + handles: &[Handle], + ) -> Vec { + let mut expanded = handles.to_vec(); + for &handle in handles { + // LEADER -> annotation. + if let Some(EntityType::Leader(leader)) = self.document.get_entity(handle) { + if !leader.annotation_handle.is_null() { + expanded.push(leader.annotation_handle); + } + } + + // Annotation -> LEADER. + expanded.extend(self.document.entities().filter_map(|entity| match entity { + EntityType::Leader(leader) + if !leader.annotation_handle.is_null() + && leader.annotation_handle == handle => + { + Some(entity.common().handle) + } + _ => None, + })); + } + + expanded.sort_unstable_by_key(|handle| handle.value()); + expanded.dedup(); + expanded + } pub fn select_entity(&mut self, handle: Handle, exclusive: bool) { + let handles = self.handles_expanded_for_leader_annotations(&[handle]); + if exclusive { self.selected.clear(); } - self.selected.insert(handle); + + self.selected.extend(handles); self.bump_selection(); } @@ -21,6 +55,12 @@ impl Scene { /// only when its contents actually changed. History/file/command paths must /// use this instead of assigning `selected` directly. pub(crate) fn replace_selection(&mut self, selected: HashSet) { + let handles: Vec = selected.iter().copied().collect(); + let selected: HashSet = self + .handles_expanded_for_leader_annotations(&handles) + .into_iter() + .collect(); + if self.selected != selected { self.selected = selected; self.bump_selection(); @@ -29,7 +69,14 @@ impl Scene { /// Remove a single entity from the selection (Shift+click subtractive pick). pub fn deselect_entity(&mut self, handle: Handle) { - if self.selected.remove(&handle) { + let handles = self.handles_expanded_for_leader_annotations(&[handle]); + let mut changed = false; + + for handle in handles { + changed |= self.selected.remove(&handle); + } + + if changed { self.bump_selection(); } } @@ -608,10 +655,14 @@ impl Scene { // ── Erase ───────────────────────────────────────────────────────────── pub fn erase_entities(&mut self, handles: &[Handle]) { + + let erase_handles = self.handles_expanded_for_leader_annotations(handles); + let mut handle_set: HashSet = HashSet::default(); let mut erased: Vec<(Handle, ChangeKind)> = Vec::new(); let mut highlight_changed = false; - for &h in handles { + + for &h in &erase_handles { // Objects on a locked layer can't be erased. if self.is_layer_locked(h) { continue; From 3d2848021f1ec4f4a25bec4752572be9d22f2685 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 13 Aug 2026 21:47:13 +0300 Subject: [PATCH 07/38] feat(readme): combine growth metrics --- .github/workflows/pages.yml | 6 +- README.md | 4 +- scripts/generate-star-history.py | 144 +++++++++++++++++++++++++++---- 3 files changed, 132 insertions(+), 22 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index b2402643b..4a5f681f4 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -177,10 +177,8 @@ jobs: print(f"supporters: {len(supporters)}") PY - # Publish an OpenCADStudio-branded star-history chart for the README. - # The timestamp media type returns when each current stargazer starred - # the repository; the script follows every API page and emits both themes. - - name: Generate star history charts + # Publish one themed growth chart for stars and release downloads. + - name: Generate project growth charts env: GITHUB_TOKEN: ${{ secrets.OCS_GITHUB_TOKEN }} run: python3 scripts/generate-star-history.py --output-dir dist diff --git a/README.md b/README.md index ae71fb7c2..3c850767d 100644 --- a/README.md +++ b/README.md @@ -156,13 +156,13 @@ cd OpenCADStudio cargo build --release --bin OpenCADStudio ./target/release/OpenCADStudio ``` -## Star History +## Stars & Release Downloads - OpenCADStudio GitHub star history + OpenCADStudio GitHub stars and release downloads diff --git a/scripts/generate-star-history.py b/scripts/generate-star-history.py index e4f523dee..2840a5a02 100644 --- a/scripts/generate-star-history.py +++ b/scripts/generate-star-history.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Generate OpenCADStudio-branded GitHub star-history SVG charts.""" +"""Generate OpenCADStudio GitHub growth SVG charts.""" from __future__ import annotations @@ -16,11 +16,15 @@ from xml.sax.saxutils import escape API_VERSION = "2026-03-10" WIDTH = 960 -HEIGHT = 380 +HEIGHT = 410 PLOT_LEFT = 72 -PLOT_RIGHT = 928 -PLOT_TOP = 82 -PLOT_BOTTOM = 326 +PLOT_RIGHT = 876 +PLOT_TOP = 108 +PLOT_BOTTOM = 350 + + +# Isolated historical spike replaced by its neighboring-release mean. +DOWNLOAD_OVERRIDES = {"v0.4.1": 61} THEMES = { @@ -33,6 +37,8 @@ THEMES = { "line": "#0284c7", "area": "#38bdf8", "point": "#0369a1", + "download_line": "#ea580c", + "download_point": "#c2410c", }, "dark": { "background": "#0b1220", @@ -43,6 +49,8 @@ THEMES = { "line": "#38bdf8", "area": "#0ea5e9", "point": "#7dd3fc", + "download_line": "#fb923c", + "download_point": "#fdba74", }, } @@ -89,8 +97,53 @@ def fetch_star_dates(repository: str, token: str | None) -> list[datetime]: return dates -def nice_axis(stars: int) -> tuple[int, int]: - rough_step = max(stars / 5, 1) +def fetch_release_downloads( + repository: str, token: str | None +) -> list[tuple[str, datetime, int]]: + url = f"https://api.github.com/repos/{repository}/releases?per_page=100" + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "OpenCADStudio-growth-history", + "X-GitHub-Api-Version": API_VERSION, + } + if token: + headers["Authorization"] = f"Bearer {token}" + + releases: list[tuple[str, datetime, int]] = [] + for _ in range(100): + request = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(request, timeout=30) as response: + payload = json.load(response) + link = response.headers.get("Link") + for release in payload: + published_at = release.get("published_at") + if release.get("draft") or not published_at: + continue + tag = str(release.get("tag_name", "")) + downloads = sum( + int(asset.get("download_count", 0)) for asset in release.get("assets", []) + ) + releases.append( + ( + tag, + datetime.fromisoformat(published_at.replace("Z", "+00:00")), + DOWNLOAD_OVERRIDES.get(tag, downloads), + ) + ) + url = next_link(link) + if not url: + break + else: + raise RuntimeError("release pagination exceeded 100 pages") + + releases.sort(key=lambda release: release[1]) + if len(releases) < 2: + raise RuntimeError("GitHub returned fewer than two published releases") + return releases[:-1] + + +def nice_axis(value: int) -> tuple[int, int]: + rough_step = max(value / 5, 1) magnitude = 10 ** math.floor(math.log10(rough_step)) step = magnitude for multiplier in (1, 2, 5, 10): @@ -98,21 +151,27 @@ def nice_axis(stars: int) -> tuple[int, int]: if candidate >= rough_step: step = candidate break - top = math.ceil(stars / step) * step - if top <= stars: + top = math.ceil(value / step) * step + if top <= value: top += step return int(top), int(step) -def render_svg(repository: str, dates: list[datetime], theme: str) -> str: +def render_svg( + repository: str, + dates: list[datetime], + releases: list[tuple[str, datetime, int]], + theme: str, +) -> str: colors = THEMES[theme] now = datetime.now(timezone.utc) - start = dates[0] - timedelta(days=2) - end = max(now, dates[-1] + timedelta(days=2)) + start = min(dates[0], releases[0][1]) - timedelta(days=2) + end = max(now, dates[-1], releases[-1][1]) + timedelta(days=2) duration = max((end - start).total_seconds(), 1) plot_width = PLOT_RIGHT - PLOT_LEFT plot_height = PLOT_BOTTOM - PLOT_TOP y_top, y_step = nice_axis(len(dates)) + download_top, download_step = nice_axis(max(downloads for _, _, downloads in releases)) def x_position(moment: datetime) -> float: ratio = (moment - start).total_seconds() / duration @@ -121,11 +180,21 @@ def render_svg(repository: str, dates: list[datetime], theme: str) -> str: def y_position(stars: int) -> float: return PLOT_BOTTOM - (stars / y_top) * plot_height + def download_y_position(downloads: int) -> float: + return PLOT_BOTTOM - (downloads / download_top) * plot_height + line_parts = [f"M {PLOT_LEFT:.1f} {PLOT_BOTTOM:.1f}"] for count, moment in enumerate(dates, 1): line_parts.append(f"H {x_position(moment):.1f} V {y_position(count):.1f}") line_path = " ".join(line_parts) area_path = f"{line_path} V {PLOT_BOTTOM:.1f} H {PLOT_LEFT:.1f} Z" + download_path = " ".join( + ( + "M" if index == 0 else "L" + ) + + f" {x_position(published):.1f} {download_y_position(downloads):.1f}" + for index, (_, published, downloads) in enumerate(releases) + ) grid = [] for value in range(0, y_top + 1, y_step): @@ -139,6 +208,18 @@ def render_svg(repository: str, dates: list[datetime], theme: str) -> str: f'fill="{colors["muted"]}" font-size="12">{value}' ) + download_axis = [] + for value in range(0, download_top + 1, download_step): + y = download_y_position(value) + download_axis.append( + f'' + ) + download_axis.append( + f'{value}' + ) + x_labels = [] for index in range(5): moment = start + (end - start) * (index / 4) @@ -156,11 +237,24 @@ def render_svg(repository: str, dates: list[datetime], theme: str) -> str: f'fill="{colors["muted"]}" font-size="12">{escape(label)}' ) - title = f"{repository.split('/')[-1]} Star History" - subtitle = f"{len(dates)} GitHub stars · Updated {now:%d %b %Y}" + title = f"{repository.split('/')[-1]} Growth" + total_downloads = sum(downloads for _, _, downloads in releases) + subtitle = ( + f"{len(dates)} GitHub stars · {total_downloads:,} cleaned asset downloads across " + f"{len(releases)} releases · Latest excluded · Updated {now:%d %b %Y}" + ) last_x = x_position(dates[-1]) last_y = y_position(len(dates)) label_y = max(PLOT_TOP + 16, last_y - 14) + last_release, last_published, last_downloads = releases[-1] + last_release_x = x_position(last_published) + last_release_y = download_y_position(last_downloads) + + download_points = "".join( + f'' + for _, published, downloads in releases + ) return f''' {escape(title)} @@ -175,12 +269,24 @@ def render_svg(repository: str, dates: list[datetime], theme: str) -> str: {escape(title)} {escape(subtitle)} + + GitHub stars + + + Release downloads + Stars + Downloads / release {''.join(grid)} + {''.join(download_axis)} {''.join(x_labels)} + + {download_points} {len(dates)} stars + + {escape(last_release)} · {last_downloads} ''' @@ -197,11 +303,17 @@ def main() -> None: token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") dates = fetch_star_dates(args.repository, token) + releases = fetch_release_downloads(args.repository, token) args.output_dir.mkdir(parents=True, exist_ok=True) for theme in THEMES: output = args.output_dir / f"star-history-{theme}.svg" - output.write_text(render_svg(args.repository, dates, theme), encoding="utf-8") - print(f"star history: {len(dates)} stars") + output.write_text( + render_svg(args.repository, dates, releases, theme), encoding="utf-8" + ) + print( + f"growth history: {len(dates)} stars, " + f"{sum(downloads for _, _, downloads in releases)} downloads" + ) if __name__ == "__main__": From 1d2f0a09c150f06e8a3bf3302f28a8e59e9bb06f Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 13 Aug 2026 23:47:50 +0300 Subject: [PATCH 08/38] feat(hatch): add associative boundaries --- src/app/command_driver.rs | 82 +++++++++- src/app/commands/draw.rs | 12 +- src/command.rs | 14 ++ src/modules/draw/draw/hatch.rs | 118 ++++++++++++--- src/scene/boundary.rs | 265 ++++++++++++++++++++++++++++++++- src/scene/entity.rs | 99 +++++++++--- src/scene/mod.rs | 32 +++- src/scene/model/hatch_model.rs | 2 + src/scene/preview.rs | 1 + src/ui/properties.rs | 1 + 10 files changed, 577 insertions(+), 49 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 8bbf0e03b..8b6b2764e 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -793,6 +793,24 @@ impl OpenCADStudio { self.commit_undo_delta(i, pd); } } + CmdResult::CommitEntitiesAndExit(entities) => { + let label = self.history_label_from_active_cmd(i, "ENTITY"); + let delta_safe = entities + .iter() + .all(|entity| self.delta_add_safe(i, entity)); + let pending = self.begin_undo(i, label, entities.len(), delta_safe); + for entity in entities { + self.commit_entity(entity); + } + self.tabs[i].dirty = true; + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.restore_pre_cmd_tangent(); + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + } CmdResult::MviewCreate { viewport, preserve_view, @@ -1271,7 +1289,67 @@ impl OpenCADStudio { let label = self.history_label_from_active_cmd(i, "HATCH"); let pending = self.begin_undo(i, label, 1, true); let layer = self.tabs[i].active_layer.clone(); - let new_handle = self.tabs[i].scene.add_hatch(hatch, Some(&layer)); + let new_handle = self.tabs[i].scene.add_hatch(hatch, Some(&layer), None); + if !new_handle.is_null() { + self.tabs[i].scene.select_entity(new_handle, true); + } + self.tabs[i].dirty = true; + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.restore_pre_cmd_tangent(); + self.refresh_properties(); + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + } + CmdResult::CommitStyledHatch { + hatch, + color, + transparency, + } => { + let label = self.history_label_from_active_cmd(i, "HATCH"); + let pending = self.begin_undo(i, label, 1, true); + let layer = self.tabs[i].active_layer.clone(); + let new_handle = self.tabs[i].scene.add_hatch( + hatch, + Some(&layer), + Some((color, transparency)), + ); + if !new_handle.is_null() { + self.tabs[i].scene.select_entity(new_handle, true); + } + self.tabs[i].dirty = true; + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.restore_pre_cmd_tangent(); + self.refresh_properties(); + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + } + CmdResult::CommitHatchWithBoundaries { + mut hatch, + boundaries, + entity_style, + } => { + let label = self.history_label_from_active_cmd(i, "HATCH"); + let pending = self.begin_undo(i, label, boundaries.len() + 1, true); + let mut sources = Vec::with_capacity(boundaries.len()); + for boundary in boundaries { + let handles = self + .commit_entity_handle(boundary) + .into_iter() + .collect::>(); + sources.push(handles); + } + hatch.boundary_sources = Some(std::sync::Arc::new(sources)); + let layer = self.tabs[i].active_layer.clone(); + let new_handle = + self.tabs[i] + .scene + .add_hatch(hatch, Some(&layer), entity_style); if !new_handle.is_null() { self.tabs[i].scene.select_entity(new_handle, true); } @@ -3097,7 +3175,7 @@ impl OpenCADStudio { // Remove old hatch (entity + GPU model) self.tabs[i].scene.erase_entities(&[handle]); // Re-add with updated model - self.tabs[i].scene.add_hatch(model, Some(&layer)); + self.tabs[i].scene.add_hatch(model, Some(&layer), None); self.tabs[i].dirty = true; self.command_line.push_output(crate::t!("HATCHEDIT: hatch updated.").as_ref()); } else { diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 4fd5a3e26..f5dc9e6c7 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -625,8 +625,16 @@ impl OpenCADStudio { .selected_entities() .into_iter() .map(|(handle, _)| handle) - .collect(); - let new_cmd = HatchCommand::new(outlines, boundary_sources, selected); + .collect::>(); + let inherited = selected + .iter() + .find_map(|handle| { + let model = self.tabs[i].scene.hatches.get(handle)?.clone(); + let common = self.tabs[i].scene.document.get_entity(*handle)?.common(); + Some((model, common.color.clone(), common.transparency)) + }); + let new_cmd = + HatchCommand::new(outlines, boundary_sources, selected, inherited); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); self.refresh_area_preview(i); diff --git a/src/command.rs b/src/command.rs index 5c135ff91..d2473dc47 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1151,6 +1151,8 @@ pub enum CmdResult { CommitEntity(EntityType), /// Commit several acadrust entities in one undo step; keep the command active. CommitEntities(Vec), + /// Commit several entities in one undo step and end the command. + CommitEntitiesAndExit(Vec), /// Commit an acadrust entity to the document and end the command. CommitAndExit(EntityType), /// Commit a Model-tab 3D solid: the acadrust entity (for selection / @@ -1183,6 +1185,18 @@ pub enum CmdResult { CopySelected(Vec, EntityTransform), /// Commit a hatch fill (stored in Scene::hatches, not the DXF document). CommitHatch(HatchModel), + /// Commit a hatch with the selected hatch's entity colour and transparency. + CommitStyledHatch { + hatch: HatchModel, + color: acadrust::types::Color, + transparency: acadrust::types::Transparency, + }, + /// Commit a hatch and retain each boundary ring as an entity. + CommitHatchWithBoundaries { + hatch: HatchModel, + boundaries: Vec, + entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>, + }, /// Copy selected entities with multiple transforms (e.g. rectangular array); end command. BatchCopy(Vec, Vec), /// Erase `handle` and replace with new entities; command stays active. diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index e8cf5ce96..614fee11b 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -211,6 +211,12 @@ pub struct HatchCommand { mode: HatchMode, manual_pts: Vec, missed: bool, + retain_boundaries: bool, + inherited: Option<( + HatchModel, + acadrust::types::Color, + acadrust::types::Transparency, + )>, } impl HatchCommand { @@ -218,7 +224,16 @@ impl HatchCommand { outlines: Vec>, boundary_sources: rustc_hash::FxHashMap>, selected_objects: Vec, + inherited: Option<( + HatchModel, + acadrust::types::Color, + acadrust::types::Transparency, + )>, ) -> Self { + let selected_objects: Vec<_> = selected_objects + .into_iter() + .filter(|handle| boundary_sources.contains_key(handle)) + .collect(); let has_selection = !selected_objects.is_empty(); let mut command = Self { outlines, @@ -233,6 +248,8 @@ impl HatchCommand { }, manual_pts: vec![], missed: false, + retain_boundaries: false, + inherited, }; command.set_object_selection(selected_objects); command @@ -289,6 +306,51 @@ impl HatchCommand { .into_iter() .map(|depth| depth % 2 == 0) .collect(); + let boundary_sources = rings + .iter() + .map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources)) + .collect(); + if let Some((source, _, _)) = &self.inherited { + let mut pattern = source.pattern.clone(); + if let HatchPattern::Pattern(families) = &mut pattern { + let scale = if source.scale.abs() > 1.0e-6 { + source.scale + } else { + 1.0 + }; + let (sin, cos) = source.angle_offset.sin_cos(); + for family in families { + let base_x = source.world_origin[0] + + (family.x0 as f64 * cos as f64 + - family.y0 as f64 * sin as f64) + * scale as f64; + let base_y = source.world_origin[1] + + (family.x0 as f64 * sin as f64 + + family.y0 as f64 * cos as f64) + * scale as f64; + let dx = base_x - origin[0]; + let dy = base_y - origin[1]; + family.x0 = ((dx * cos as f64 + dy * sin as f64) / scale as f64) as f32; + family.y0 = ((-dx * sin as f64 + dy * cos as f64) / scale as f64) as f32; + } + } + return HatchModel { + render_instance: None, + boundary: std::sync::Arc::new(rel), + pattern, + name: source.name.clone(), + color: source.color, + aci: source.aci, + line_weight_px: source.line_weight_px, + angle_offset: source.angle_offset, + scale: source.scale, + world_origin: origin, + boundary_wcs: Some(std::sync::Arc::new(wcs)), + boundary_exterior: Some(std::sync::Arc::new(exterior)), + boundary_sources: Some(std::sync::Arc::new(boundary_sources)), + draw_depth: source.draw_depth, + }; + } // Default: ANSI31 from catalog; fallback to a single 45° family. let pat_name = "ANSI31"; let families = crate::scene::model::hatch_patterns::find(pat_name) @@ -324,6 +386,7 @@ impl HatchCommand { world_origin: origin, boundary_wcs: Some(std::sync::Arc::new(wcs)), boundary_exterior: Some(std::sync::Arc::new(exterior)), + boundary_sources: Some(std::sync::Arc::new(boundary_sources)), draw_depth: 0.0, } } @@ -381,6 +444,14 @@ impl CadCommand for HatchCommand { let mut options = vec![ CmdOption::new(t!("Select objects").as_ref(), "O"), CmdOption::new(t!("Draw manually").as_ref(), "S"), + CmdOption::new( + if self.retain_boundaries { + "Keep boundaries: on" + } else { + "Keep boundaries: off" + }, + "B", + ), ]; if self.region_count() > 0 { options.push(CmdOption::enter(t!("Accept").as_ref())); @@ -391,6 +462,14 @@ impl CadCommand for HatchCommand { let mut options = vec![ CmdOption::new(t!("Pick internal points").as_ref(), "I"), CmdOption::new(t!("Draw manually").as_ref(), "S"), + CmdOption::new( + if self.retain_boundaries { + "Keep boundaries: on" + } else { + "Keep boundaries: off" + }, + "B", + ), ]; if self.region_count() > 0 { options.push(CmdOption::enter(t!("Accept").as_ref())); @@ -441,6 +520,21 @@ impl CadCommand for HatchCommand { let rings = self.combined_rings(); if rings.is_empty() { CmdResult::Cancel + } else if self.retain_boundaries { + CmdResult::CommitHatchWithBoundaries { + hatch: self.make_hatch(rings.clone()), + boundaries: crate::scene::boundary_entities(&rings), + entity_style: self + .inherited + .as_ref() + .map(|(_, color, transparency)| (color.clone(), *transparency)), + } + } else if let Some((_, color, transparency)) = &self.inherited { + CmdResult::CommitStyledHatch { + hatch: self.make_hatch(rings), + color: color.clone(), + transparency: *transparency, + } } else { CmdResult::CommitHatch(self.make_hatch(rings)) } @@ -510,6 +604,10 @@ impl CadCommand for HatchCommand { self.missed = false; Some(CmdResult::NeedPoint) } + "B" | "BOUNDARY" | "BOUNDARIES" => { + self.retain_boundaries = !self.retain_boundaries; + Some(CmdResult::NeedPoint) + } _ => None, } } @@ -586,6 +684,7 @@ impl GradientCommand { world_origin: origin, boundary_wcs: Some(std::sync::Arc::new(wcs)), boundary_exterior: None, + boundary_sources: None, draw_depth: 0.0, } } @@ -783,24 +882,7 @@ impl CadCommand for BoundaryCommand { match resolve_hatch_rings(&self.outlines, xy) { Some(rings) => { self.missed = false; - // Store as a Hatch entity (solid fill) so it is selectable. - let (rel, origin, wcs) = pack_rings(&rings); - let model = HatchModel { - render_instance: None, - boundary: std::sync::Arc::new(rel), - pattern: HatchPattern::Solid, - name: "SOLID".into(), - color: [0.45, 0.45, 0.45, 0.60], - aci: 0, - line_weight_px: 1.0, - angle_offset: 0.0, - scale: 1.0, - world_origin: origin, - boundary_wcs: Some(std::sync::Arc::new(wcs)), - boundary_exterior: None, - draw_depth: 0.0, - }; - CmdResult::CommitHatch(model) + CmdResult::CommitEntitiesAndExit(crate::scene::boundary_entities(&rings)) } None => { self.missed = true; diff --git a/src/scene/boundary.rs b/src/scene/boundary.rs index 3c1a5438b..1e0c12ce0 100644 --- a/src/scene/boundary.rs +++ b/src/scene/boundary.rs @@ -1,6 +1,9 @@ use super::*; -use cadkernel::geom2d::{bounded_faces, Line, Tolerance}; +use cadkernel::geom2d::{ + bounded_faces, contains, distance_to, segment_crossing, triangulate, Curve, Line, + SegmentCrossing, Tolerance, +}; /// How far apart two points may be and still be taken for the same one. /// @@ -35,7 +38,267 @@ fn wire_segments(wire: &WireModel) -> Vec { segments } +fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> { + let mut ring = Vec::new(); + let mut index = 0usize; + for &[x, y] in model.boundary.iter() { + if x.is_finite() && y.is_finite() { + if index == wanted { + ring.push([ + model.world_origin[0] + x as f64, + model.world_origin[1] + y as f64, + ]); + } + } else if index == wanted { + break; + } else { + index += 1; + } + } + if ring.len() < 3 { + return None; + } + let (points, triangles) = triangulate(&ring, &[]); + if let Some(triangle) = triangles.first() { + let [a, b, c] = triangle.map(|vertex| points[vertex]); + return Some([ + (a[0] + b[0] + c[0]) / 3.0, + (a[1] + b[1] + c[1]) / 3.0, + ]); + } + Some([ + ring.iter().map(|point| point[0]).sum::() / ring.len() as f64, + ring.iter().map(|point| point[1]).sum::() / ring.len() as f64, + ]) +} + +fn face_curves(face: &[[f64; 2]]) -> Vec { + face.iter() + .copied() + .zip(face.iter().copied().cycle().skip(1)) + .take(face.len()) + .map(|(start, end)| Curve::Line(Line { start, end })) + .collect() +} + +fn matching_face(faces: &[Vec<[f64; 2]>], seed: Option<[f64; 2]>) -> Option<&Vec<[f64; 2]>> { + if faces.len() == 1 { + return faces.first(); + } + let seed = seed?; + let tolerance = Tolerance::new(WELD_TOLERANCE); + if let Some(face) = faces.iter().find(|face| { + let curves = face_curves(face); + contains(&curves, seed, tolerance) + }) { + return Some(face); + } + faces.iter().min_by(|a, b| { + let nearest = |face: &Vec<[f64; 2]>| { + face_curves(face) + .iter() + .map(|curve| distance_to(curve, seed)) + .fold(f64::INFINITY, f64::min) + }; + nearest(a).total_cmp(&nearest(b)) + }) +} + +pub(crate) fn ring_source_handles( + ring: &[[f64; 2]], + sources: &rustc_hash::FxHashMap>, +) -> Vec { + let mut handles = rustc_hash::FxHashSet::default(); + for (&start, &end) in ring + .iter() + .zip(ring.iter().cycle().skip(1)) + .take(ring.len()) + { + let edge = Line { start, end }; + let edge_length = (end[0] - start[0]).hypot(end[1] - start[1]); + for (&handle, lines) in sources { + if lines.iter().any(|line| { + matches!( + segment_crossing(edge, *line, Tolerance::new(WELD_TOLERANCE)), + SegmentCrossing::Overlap { a, .. } + if (a[1] - a[0]).abs() * edge_length > WELD_TOLERANCE + ) + }) { + handles.insert(handle); + } + } + } + let mut handles: Vec<_> = handles.into_iter().collect(); + handles.sort_by_key(|handle| handle.value()); + handles +} + +pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec { + rings + .iter() + .filter_map(|ring| { + let mut points = Vec::with_capacity(ring.len()); + for &point in ring { + if point.iter().all(|value| value.is_finite()) + && points.last() != Some(&point) + { + points.push(point); + } + } + if points.len() > 1 && points.first() == points.last() { + points.pop(); + } + if points.len() < 3 { + return None; + } + let mut polyline = acadrust::entities::LwPolyline::new(); + polyline.is_closed = true; + polyline.vertices = points + .into_iter() + .map(|[x, y]| { + acadrust::entities::LwVertex::new(acadrust::types::Vector2::new(x, y)) + }) + .collect(); + Some(acadrust::EntityType::LwPolyline(polyline)) + }) + .collect() +} + impl Scene { + fn associative_boundary_segments(&self, handles: &[Handle]) -> Vec { + self.wire_models_for(handles) + .iter() + .flat_map(wire_segments) + .collect() + } + + fn associative_hatch_dependents( + &self, + changed: &rustc_hash::FxHashSet, + ) -> Vec { + if self.associative_hatch_source_cache.borrow().is_none() { + let mut index: HashMap> = HashMap::default(); + for entity in self.document.entities() { + let EntityType::Hatch(hatch) = entity else { + continue; + }; + if !hatch.is_associative { + continue; + } + for source in hatch + .paths + .iter() + .flat_map(|path| path.boundary_handles.iter().copied()) + { + let dependents = index.entry(source).or_default(); + if !dependents.contains(&hatch.common.handle) { + dependents.push(hatch.common.handle); + } + } + } + *self.associative_hatch_source_cache.borrow_mut() = Some(index); + } + let cache = self.associative_hatch_source_cache.borrow(); + let index = cache.as_ref().expect("associative hatch index"); + let mut handles = rustc_hash::FxHashSet::default(); + for source in changed { + if let Some(dependents) = index.get(source) { + handles.extend(dependents.iter().copied()); + } + } + handles.into_iter().collect() + } + + pub(crate) fn refresh_associative_hatches( + &mut self, + changes: &[(Handle, ChangeKind)], + ) -> Vec<(Handle, ChangeKind)> { + if changes.is_empty() { + return Vec::new(); + } + let changed: rustc_hash::FxHashSet<_> = + changes.iter().map(|(handle, _)| *handle).collect(); + let candidates: Vec<_> = self + .associative_hatch_dependents(&changed) + .into_iter() + .filter_map(|handle| { + let EntityType::Hatch(hatch) = self.document.get_entity(handle)? else { + return None; + }; + Some({ + let seeds = self + .hatches + .get(&hatch.common.handle) + .map(|model| { + (0..hatch.paths.len()) + .map(|index| ring_seed(model, index)) + .collect::>() + }) + .unwrap_or_default(); + (handle, hatch.clone(), seeds) + }) + }) + .collect(); + + let mut refreshed = Vec::new(); + for (handle, mut hatch, seeds) in candidates { + let normal = hatch.normal; + if normal.x.abs() > 1.0e-8 || normal.y.abs() > 1.0e-8 { + continue; + } + let mut modified = false; + let mut association_changed = false; + for (index, path) in hatch.paths.iter_mut().enumerate() { + if !path + .boundary_handles + .iter() + .any(|source| changed.contains(source)) + { + continue; + } + let old_count = path.boundary_handles.len(); + path.boundary_handles + .retain(|source| self.document.get_entity(*source).is_some()); + association_changed |= path.boundary_handles.len() != old_count; + modified |= association_changed; + let segments = self.associative_boundary_segments(&path.boundary_handles); + let faces = bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE)); + let Some(face) = matching_face(&faces, seeds.get(index).copied().flatten()) else { + continue; + }; + path.edges = vec![acadrust::entities::hatch::BoundaryEdge::Polyline( + acadrust::entities::hatch::PolylineEdge::new( + face.iter() + .map(|point| acadrust::types::Vector2::new(point[0], point[1])) + .collect(), + true, + ), + )]; + modified = true; + } + hatch.is_associative = hatch + .paths + .iter() + .any(|path| !path.boundary_handles.is_empty()); + if !modified { + continue; + } + if association_changed { + self.associative_hatch_source_cache.borrow_mut().take(); + } + if self.is_recording_undo() { + let before = self.document.get_entity_arc(handle); + self.record_undo_before(handle, before); + } + if let Some(slot) = self.document.get_entity_mut(handle) { + *slot = EntityType::Hatch(hatch); + self.refresh_fill_model(handle); + refreshed.push((handle, ChangeKind::Modified)); + } + } + refreshed + } + /// Build closed planar regions from the visible wire geometry. /// /// Unlike `closed_outlines()`, the source entities do not need to be closed diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 454784885..b19b57807 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1363,6 +1363,7 @@ impl Scene { boundary: Arc::new(boundary), boundary_wcs: None, boundary_exterior: None, + boundary_sources: None, pattern: model::hatch_model::HatchPattern::Solid, name: "WIPEOUT_FILL".into(), color, @@ -1714,6 +1715,13 @@ impl Scene { boundary: std::sync::Arc::new(boundary_f32), boundary_wcs: None, boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)), + boundary_sources: Some(std::sync::Arc::new( + dxf.paths + .iter() + .filter(|path| path.flags.bits() & 8 == 0) + .map(|path| path.boundary_handles.clone()) + .collect(), + )), pattern, name, // A gradient starts from its first stop; other fills use the @@ -2029,6 +2037,7 @@ impl Scene { boundary: std::sync::Arc::new(boundary), boundary_wcs: None, boundary_exterior: None, + boundary_sources: None, pattern: model::hatch_model::HatchPattern::Solid, name: "SOLID".into(), color, @@ -2041,7 +2050,12 @@ impl Scene { } } - pub fn add_hatch(&mut self, model: HatchModel, layer: Option<&str>) -> Handle { + pub fn add_hatch( + &mut self, + model: HatchModel, + layer: Option<&str>, + entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>, + ) -> Handle { let mut dxf = DxfHatch::new(); dxf.is_solid = matches!( model.pattern, @@ -2074,7 +2088,7 @@ impl Scene { let mut ring: Vec = Vec::new(); let mut first = true; let mut ring_index = 0usize; - let mut push_ring = |r: &mut Vec, is_outer: bool| { + let mut push_ring = |r: &mut Vec, is_outer: bool, index: usize| { if !r.is_empty() { let edge = PolylineEdge::new(std::mem::take(r), true); let mut path = if is_outer { @@ -2088,6 +2102,17 @@ impl Scene { BoundaryPath::new() }; path.add_edge(BoundaryEdge::Polyline(edge)); + if let Some(handles) = model + .boundary_sources + .as_deref() + .and_then(|sources| sources.get(index)) + { + for &handle in handles { + if handle.is_valid() { + path.add_boundary_handle(handle); + } + } + } dxf.paths.push(path); } }; @@ -2101,11 +2126,11 @@ impl Scene { .and_then(|roles| roles.get(ring_index)) .copied() .unwrap_or(first); + first = false; if !ring.is_empty() { + push_ring(&mut ring, is_outer, ring_index); ring_index += 1; } - first = false; - push_ring(&mut ring, is_outer); } } let is_outer = model @@ -2114,29 +2139,56 @@ impl Scene { .and_then(|roles| roles.get(ring_index)) .copied() .unwrap_or(first); - push_ring(&mut ring, is_outer); + push_ring(&mut ring, is_outer, ring_index); + dxf.is_associative = dxf + .paths + .iter() + .any(|path| !path.boundary_handles.is_empty()); let pattern_scale = if model.scale.abs() > 1e-6 { model.scale as f64 } else { 1.0 }; - if let Some(entry) = crate::scene::model::hatch_patterns::find(&model.name) { - dxf.pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry); - if let Some(base) = dxf.pattern.lines.first().map(|line| line.base_point) { - let angle = model.angle_offset as f64; - let (sin, cos) = angle.sin_cos(); - let target_x = model.world_origin[0] - + (base.x * cos - base.y * sin) * pattern_scale; - let target_y = model.world_origin[1] - + (base.x * sin + base.y * cos) * pattern_scale; - crate::entities::hatch::scale_pattern_geometry(&mut dxf.pattern, pattern_scale); - crate::entities::hatch::rotate_pattern_geometry(&mut dxf.pattern, angle); - crate::entities::hatch::translate_pattern_geometry( - &mut dxf.pattern, - target_x - base.x, - target_y - base.y, - ); + if let crate::scene::model::hatch_model::HatchPattern::Pattern(families) = &model.pattern { + let mut pattern = acadrust::entities::HatchPattern::new(&model.name); + let rotation = model.angle_offset as f64; + let (rotation_sin, rotation_cos) = rotation.sin_cos(); + for family in families { + let family_angle = (family.angle_deg as f64).to_radians(); + let angle = family_angle + rotation; + let (family_sin, family_cos) = family_angle.sin_cos(); + let local_offset_x = family.dx as f64 * family_cos + - family.dy as f64 * family_sin; + let local_offset_y = family.dx as f64 * family_sin + + family.dy as f64 * family_cos; + let base_x = family.x0 as f64 * pattern_scale; + let base_y = family.y0 as f64 * pattern_scale; + pattern.lines.push(acadrust::entities::HatchPatternLine { + angle, + base_point: Vector2::new( + model.world_origin[0] + + base_x * rotation_cos + - base_y * rotation_sin, + model.world_origin[1] + + base_x * rotation_sin + + base_y * rotation_cos, + ), + offset: Vector2::new( + (local_offset_x * rotation_cos + - local_offset_y * rotation_sin) + * pattern_scale, + (local_offset_x * rotation_sin + + local_offset_y * rotation_cos) + * pattern_scale, + ), + dash_lengths: family + .dashes + .iter() + .map(|dash| *dash as f64 * pattern_scale) + .collect(), + }); } + dxf.pattern = pattern; } dxf.pattern_angle = model.angle_offset as f64; dxf.pattern_scale = pattern_scale; @@ -2195,6 +2247,10 @@ impl Scene { if let Some(layer) = layer { entity.as_entity_mut().set_layer(layer.to_string()); } + if let Some((color, transparency)) = entity_style { + entity.common_mut().color = color; + entity.common_mut().transparency = transparency; + } self.add_entity(entity) } @@ -2207,6 +2263,7 @@ impl Scene { self.preview_text = vec![]; self.current_layout = "Model".to_string(); self.hatches = HashMap::default(); + self.associative_hatch_source_cache.borrow_mut().take(); self.images = HashMap::default(); self.meshes = HashMap::default(); self.block_meshes = HashMap::default(); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 205794af1..406ca0738 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -33,6 +33,8 @@ mod project; mod scene_markers; mod selection; +pub(crate) use boundary::{boundary_entities, ring_source_handles}; + // Parallel tessellation free functions live in `convert::tess` (alongside the // other tessellation code); re-exported here so this root and sibling topic // modules (each does `use super::*`) keep referencing them unqualified. @@ -1677,6 +1679,9 @@ pub struct Scene { /// `geometry_epoch`: a layer colour toggle can reuse the index, invalidate /// only its dependants, and avoid a whole-document scan on every toggle. dependency_index_cache: RefCell>, + /// Boundary source → associative hatch handles. Source edits reuse this + /// index instead of scanning the document during every drag step. + associative_hatch_source_cache: RefCell>>>, /// Tessellated block definitions in block-local coords, keyed by render /// background and block epoch. Model and Paper adapt black/white colours /// differently; retaining both variants prevents a full block rebuild on @@ -1893,6 +1898,7 @@ impl Scene { model_extents_cache: RefCell::new(None), entity_block_map_cache: RefCell::new(None), dependency_index_cache: RefCell::new(None), + associative_hatch_source_cache: RefCell::new(None), block_defn_cache: RefCell::new(HashMap::default()), entity_index_cache: RefCell::new(None), last_render_aspect: std::cell::Cell::new(16.0 / 9.0), @@ -2368,6 +2374,21 @@ impl Scene { } pub fn bump_entities(&mut self, changes: &[(Handle, ChangeKind)]) { + if changes.iter().any(|(handle, kind)| { + matches!(kind, ChangeKind::Removed) + || self + .document + .get_entity(*handle) + .is_some_and(|entity| matches!(entity, EntityType::Hatch(_))) + }) { + self.associative_hatch_source_cache.borrow_mut().take(); + } + let mut changes = changes.to_vec(); + for change in self.refresh_associative_hatches(&changes) { + if !changes.iter().any(|(handle, _)| *handle == change.0) { + changes.push(change); + } + } if !changes.is_empty() { // A Modified delta can change layer/style/block references as well // as coordinates. Rebuild the reverse dependency map lazily on its @@ -2386,14 +2407,14 @@ impl Scene { .is_some_and(|entity| matches!(entity, EntityType::Light(_))) }); if cached_light_changed || live_light_changed { - for (handle, _) in changes { + for &(handle, _) in &changes { let exists = self .document - .get_entity(*handle) + .get_entity(handle) .is_some_and(|entity| matches!(entity, EntityType::Light(_))); crate::entities::object_data::update_light_entity( &mut self.object_data_cache, - *handle, + handle, exists, ); } @@ -2404,7 +2425,7 @@ impl Scene { { let mut tm = self.tess_memo.borrow_mut(); let mut rm = self.resident_tess_memo.borrow_mut(); - for &(h, k) in changes { + for &(h, k) in &changes { // A changed or removed entity must re-tessellate; a fresh Add has // no memo entry yet, so there is nothing to drop. if matches!(k, ChangeKind::Modified | ChangeKind::Removed) { @@ -2413,7 +2434,7 @@ impl Scene { } } } - self.push_geometry_delta(epoch, changes.to_vec(), false); + self.push_geometry_delta(epoch, changes, false); } /// True when any changed handle belongs to an ordinary block definition @@ -3182,6 +3203,7 @@ impl Scene { boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]), boundary_wcs: None, boundary_exterior: None, + boundary_sources: None, pattern: crate::scene::model::hatch_model::HatchPattern::Solid, name: "SOLID".to_string(), color: self.paper_bg_color, diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs index 4572d2acc..0afc3e667 100644 --- a/src/scene/model/hatch_model.rs +++ b/src/scene/model/hatch_model.rs @@ -182,6 +182,8 @@ pub struct HatchModel { pub boundary_wcs: Option>>, /// Per-ring DXF role, aligned with the NaN-separated boundary paths. pub boundary_exterior: Option>>, + /// Source entity handles for each boundary ring. + pub boundary_sources: Option>>>, /// Fill pattern. pub pattern: HatchPattern, /// Catalog name for this pattern (e.g. "ANSI31", "SOLID", "LINEAR"). diff --git a/src/scene/preview.rs b/src/scene/preview.rs index 08b63d926..805363b0a 100644 --- a/src/scene/preview.rs +++ b/src/scene/preview.rs @@ -159,6 +159,7 @@ impl Scene { boundary: std::sync::Arc::new(boundary), boundary_wcs: None, boundary_exterior: None, + boundary_sources: None, pattern: HatchPattern::Solid, name: "AREA_PREVIEW".into(), color: [0.0; 4], diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 55fd780af..77bc5f99d 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -131,6 +131,7 @@ impl canvas::Program for HatchPatternPreview { ]), boundary_wcs: None, boundary_exterior: None, + boundary_sources: None, pattern: self.pattern.clone(), name: String::new(), color: [1.0; 4], From 4aa317f4139dbad7aec2dd802b47d43780480e61 Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Thu, 13 Aug 2026 17:53:24 -0300 Subject: [PATCH 09/38] fix: restore text editing on double click --- src/app/update/viewport.rs | 103 ++++++++++++++++++++++++------------- 1 file changed, 66 insertions(+), 37 deletions(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index e0649bfc5..aeb505213 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -4206,46 +4206,75 @@ impl OpenCADStudio { height: vh, }; - // 1) Try direct wire hit — works when the border is clicked. - let wire_hit: Option = { - let (view_rot, eye) = { - let c = self.tabs[i].scene.camera.borrow(); - (c.view_proj_rte(bounds), c.eye()) - }; - let all_wires = self.tabs[i].scene.hit_test_wires(); - let click_world = self.cursor_model_point(i, &edit_cam, p, bounds); - let click_candidates = self.tabs[i].scene.interaction_pick_candidates_near( - all_wires, - click_world, - view_rot, - eye, - bounds, - crate::ui::overlay::pick_box_aperture_px(self.pick_box) * 2.0, - ); - scene::pick::hit_test::click_hit( - p, - &click_candidates, - view_rot, - eye, - bounds, - self.tabs[i].scene.document.header.lineweight_display, - crate::ui::overlay::pick_box_aperture_px(self.pick_box), - ) - .and_then(|s| Scene::handle_from_wire_name(s)) - .and_then(|h| { - if let Some(AcadEntityType::Viewport(vp)) = - self.tabs[i].scene.document.get_entity(h) - { - if Scene::is_content_viewport(vp) { - Some(h) - } else { - None - } + let (view_rot, eye) = { + let c = self.tabs[i].scene.camera.borrow(); + (c.view_proj_rte(bounds), c.eye()) + }; + + let all_wires = self.tabs[i].scene.hit_test_wires(); + let click_world = self.cursor_model_point(i, &edit_cam, p, bounds); + let click_candidates = self.tabs[i].scene.interaction_pick_candidates_near( + all_wires, + click_world, + view_rot, + eye, + bounds, + crate::ui::overlay::pick_box_aperture_px(self.pick_box) * 2.0, + ); + + let raw_wire_hit = scene::pick::hit_test::click_hit( + p, + &click_candidates, + view_rot, + eye, + bounds, + self.tabs[i].scene.document.header.lineweight_display, + crate::ui::overlay::pick_box_aperture_px(self.pick_box), + ) + .and_then(|s| Scene::handle_from_wire_name(s)); + + // In paper space, text editing takes precedence over entering MSPACE. + // This mirrors the model-space double-click behaviour. + if let Some(handle) = raw_wire_hit { + let is_editable_text = self.tabs[i] + .scene + .document + .get_entity(handle) + .is_some_and(|entity| { + crate::app::text_inline::read_text_field(entity).is_some() + || matches!(entity, AcadEntityType::Leader(_)) + }); + + if is_editable_text { + if let Some(layer) = self.tabs[i].scene.locked_layer_name(handle) { + self.command_line.push_info( + crate::tf!( + "Object is on locked layer \"{layer}\" — unlock the layer to edit it." + ) + .as_ref(), + ); + return Task::none(); + } + + return self.begin_text_edit(handle); + } + } + + // If the double-click was not on editable text, keep the existing + // paper-space behaviour and try to enter the clicked viewport. + let wire_hit: Option = raw_wire_hit.and_then(|h| { + if let Some(AcadEntityType::Viewport(vp)) = + self.tabs[i].scene.document.get_entity(h) + { + if Scene::is_content_viewport(vp) { + Some(h) } else { None } - }) - }; + } else { + None + } + }); // 2) Decide which viewport (if any) the click enters, // keyed on the *visible* on-screen rectangle. Screen-space From e58bef39dc74cc03b800b21f511c459ec7ec5ec9 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 13 Aug 2026 23:57:47 +0300 Subject: [PATCH 10/38] fix(web): use browser-safe render timing --- src/scene/pick/hit_test.rs | 2 +- src/scene/pipeline/mesh_gpu.rs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/scene/pick/hit_test.rs b/src/scene/pick/hit_test.rs index 6f0125a86..42f1c9561 100644 --- a/src/scene/pick/hit_test.rs +++ b/src/scene/pick/hit_test.rs @@ -692,7 +692,7 @@ fn mesh_click_result<'a>( eye: glam::DVec3, bounds: Rectangle, ) -> Option<(f64, Handle, glam::DVec3)> { - let profile = crate::perf::enabled().then(std::time::Instant::now); + let profile = crate::perf::enabled().then(iced::time::Instant::now); let mut set_count = 0usize; let mut bound_hits = 0usize; let mut exact_triangles = 0usize; diff --git a/src/scene/pipeline/mesh_gpu.rs b/src/scene/pipeline/mesh_gpu.rs index 91c519363..b23d6b94f 100644 --- a/src/scene/pipeline/mesh_gpu.rs +++ b/src/scene/pipeline/mesh_gpu.rs @@ -1148,7 +1148,7 @@ fn build_instanced_chunk( parts: &[MeshBatchPart<'_>], profile_enabled: bool, ) -> Option<(MeshBatchChunk, u64, InstancedBuildProfile)> { - let started = profile_enabled.then(std::time::Instant::now); + let started = profile_enabled.then(iced::time::Instant::now); let first = parts.first()?; let source = first.set.instance_source.as_ref()?; let mesh = first.mesh; @@ -1209,7 +1209,7 @@ fn build_instanced_chunk( } else { Vec::new() }; - let vertices_at = profile_enabled.then(std::time::Instant::now); + let vertices_at = profile_enabled.then(iced::time::Instant::now); let edge_color = first .set .visual_style @@ -1250,7 +1250,7 @@ fn build_instanced_chunk( ]); } } - let edges_at = profile_enabled.then(std::time::Instant::now); + let edges_at = profile_enabled.then(iced::time::Instant::now); let transparent = material_is_transparent(material, color); let face_indices = first.include_faces.then_some(first.indices.as_ref()); let opaque_indices = if transparent { @@ -1298,7 +1298,7 @@ fn build_instanced_chunk( } } } - let instances_at = profile_enabled.then(std::time::Instant::now); + let instances_at = profile_enabled.then(iced::time::Instant::now); let triangles = if first.include_faces { (first.indices.len() / 3) as u64 * instances.len() as u64 } else { @@ -1332,7 +1332,7 @@ fn build_instanced_chunk( .entry(edge_key) .or_insert_with(|| chunk.edge_vertex_buffer.clone()); } - let uploaded_at = profile_enabled.then(std::time::Instant::now); + let uploaded_at = profile_enabled.then(iced::time::Instant::now); Some(( chunk, triangles, @@ -1559,7 +1559,7 @@ pub fn build_mesh_batch_filtered( sets: &[MeshLodSet], handles: Option<&rustc_hash::FxHashSet>, ) -> (Vec, u64) { - let perf_started = crate::perf::enabled().then(std::time::Instant::now); + let perf_started = crate::perf::enabled().then(iced::time::Instant::now); // Derive the caps from the real device limit and vertex size. The previous // fixed 6 M-vertex cap assumed 40 B/vertex, but `position_low` (RTE) grew // MeshVertex to 52 B, so 6 M × 52 B = 312 MB blew past the 256 MB cap. @@ -1741,7 +1741,7 @@ pub fn build_mesh_batch_filtered( }); } } - let prepared_at = std::time::Instant::now(); + let prepared_at = iced::time::Instant::now(); let spatial_bounds = sets.iter().fold( [ f32::INFINITY, @@ -1828,7 +1828,7 @@ pub fn build_mesh_batch_filtered( mesh_spatial_key(part.set, spatial_bounds), ) }); - let grouped_at = std::time::Instant::now(); + let grouped_at = iced::time::Instant::now(); let ordered = direct_parts; let mut active_key: Option = None; let mut active_material: Option<&crate::scene::model::material_model::MeshMaterial> = None; @@ -2224,7 +2224,7 @@ pub fn build_mesh_batch_filtered( None, )); } - let direct_at = std::time::Instant::now(); + let direct_at = iced::time::Instant::now(); let mut instanced_profile = InstancedBuildProfile::default(); let mut instanced_buffers = InstancedBufferCache::default(); for parts in instance_groups.values() { @@ -2249,7 +2249,7 @@ pub fn build_mesh_batch_filtered( } } if let Some(started) = perf_started { - let done = std::time::Instant::now(); + let done = iced::time::Instant::now(); crate::perf_record!( "[perf] mesh-build-detail prepare={:.1} group={:.1} direct={:.1} instanced={:.1}", prepared_at.duration_since(started).as_secs_f64() * 1000.0, From 5a6ba9dd94053fbd63ae485e3654d3194a88c75d Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 14 Aug 2026 00:58:53 +0300 Subject: [PATCH 11/38] fix(web): use custom domain root --- .github/workflows/pages.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 4a5f681f4..83c9ae7d6 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -50,10 +50,8 @@ jobs: with: tool: wasm-bindgen@${{ steps.wasm-bindgen-version.outputs.version }} - # `--public-url` matches the project page sub-path: - # https://.github.io/OpenCADStudio/. - name: Build web bundle - run: trunk build --release --public-url /OpenCADStudio/ + run: trunk build --release --public-url / # GitHub Pages runs Jekyll, which ignores files; opt out so the # wasm-bindgen output is served verbatim. From 844ae227ecee4351bca6f98a5daa016a8a8557eb Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 14 Aug 2026 01:09:47 +0300 Subject: [PATCH 12/38] Change OCS Web link to www.opencadstudio.com Updated the OCS Web URL for browser access. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c850767d..60f91bb3d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ OCS is a CAD application for 2D drafting and 3D modeling, built with Rust. Reads and writes DWG and DXF files natively. Also has a web version now! -## OCS Web try it in the browser: https://hakanseven12.github.io/OpenCADStudio/ +## OCS Web try it in the browser: www.opencadstudio.com resim resim2 From 26fd9a3c02f4ac39b5434f19e61855e443c2f228 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 14 Aug 2026 11:06:12 +0300 Subject: [PATCH 13/38] feat(i18n): add seven UI locales --- locales/ar-SA/opencadstudio.ftl | 7 + locales/cs-CZ/opencadstudio.ftl | 3509 +++++++++++++++++++++++++++++ locales/de-DE/opencadstudio.ftl | 7 + locales/en-US/opencadstudio.ftl | 9 +- locales/es-ES/opencadstudio.ftl | 7 + locales/fi-FI/opencadstudio.ftl | 3509 +++++++++++++++++++++++++++++ locales/fr-FR/opencadstudio.ftl | 7 + locales/hi-IN/opencadstudio.ftl | 7 + locales/hu-HU/opencadstudio.ftl | 3509 +++++++++++++++++++++++++++++ locales/it-IT/opencadstudio.ftl | 3509 +++++++++++++++++++++++++++++ locales/ja-JP/opencadstudio.ftl | 7 + locales/ko-KR/opencadstudio.ftl | 3509 +++++++++++++++++++++++++++++ locales/nl-NL/opencadstudio.ftl | 7 + locales/pl-PL/opencadstudio.ftl | 3509 +++++++++++++++++++++++++++++ locales/pt-BR/opencadstudio.ftl | 7 + locales/ru-RU/opencadstudio.ftl | 7 + locales/tr-TR/opencadstudio.ftl | 7 + locales/zh-CN/opencadstudio.ftl | 7 + locales/zh-TW/opencadstudio.ftl | 3505 ++++++++++++++++++++++++++++ src/i18n.rs | 45 +- src/scene/text/web_font.rs | 63 +- web/fonts/generate.sh | 7 +- web/fonts/traditional_chinese.ttf | Bin 0 -> 10304628 bytes 23 files changed, 24729 insertions(+), 31 deletions(-) create mode 100644 locales/cs-CZ/opencadstudio.ftl create mode 100644 locales/fi-FI/opencadstudio.ftl create mode 100644 locales/hu-HU/opencadstudio.ftl create mode 100644 locales/it-IT/opencadstudio.ftl create mode 100644 locales/ko-KR/opencadstudio.ftl create mode 100644 locales/pl-PL/opencadstudio.ftl create mode 100644 locales/zh-TW/opencadstudio.ftl create mode 100644 web/fonts/traditional_chinese.ttf diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 711bff973..333dc5324 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -12,6 +12,13 @@ language = .portuguese = Português (Brasil) .arabic = العربية .japanese = 日本語 + .korean = 한국어 + .czech = Čeština + .italian = Italiano + .finnish = Suomi + .hungarian = Magyar + .polish = Polski + .chinese-traditional = 繁體中文 action = .close = إغلاق diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl new file mode 100644 index 000000000..b6182fd64 --- /dev/null +++ b/locales/cs-CZ/opencadstudio.ftl @@ -0,0 +1,3509 @@ +language = + .system = Jazyk systému + .english = English + .turkish = Türkçe + .dutch = Nederlands + .french = Français + .german = Deutsch + .hindi = हिन्दी + .russian = Русский + .chinese-simplified = 简体中文 + .spanish = Español + .portuguese = Português (Brasil) + .arabic = العربية + .japanese = 日本語 + .korean = 한국어 + .czech = Čeština + .italian = Italiano + .finnish = Suomi + .hungarian = Magyar + .polish = Polski + .chinese-traditional = 繁體中文 + +action = + .close = Zavřít + .options = Možnosti + .plugins = Zásuvné moduly + +ribbon-tab = + .draw = Kreslení + .annotate = Poznámky + .insert = Vložit + .model = Model + .layout = Rozvržení + .manage = Správa + .view = Zobrazení + +options = + .language-section = Jazyk + .language-label = Jazyk rozhraní: + .open-save-section = Otevírání a ukládání + .default-save-format-label = Výchozí formát ukládání: + .default-save-format-help = Použije se při prvním uložení nového výkresu. Stávající výkresy si zachovají typ a verzi souboru. + .theme-section = Motiv + .theme-label = Motiv aplikace: + .theme-help = Změnou základní barvy přejdete na vlastní motiv. Odstíny všech součástí motivu se vytvářejí z těchto šesti barev. + .color-background = Pozadí + .color-text = Text + .color-primary = Primární + .color-success = Úspěch + .color-warning = Varování + .color-danger = Nebezpečí + .browser-storage = Úložiště prohlížeče + .dark = Tmavý + .default-application = Výchozí aplikace + .iced-theme = Motiv Iced: + .theme = Motiv + +command-line = + .ready = Open CAD Studio je připraveno. + .hint = Zadejte příkaz nebo použijte pás karet. Soubor OBJ otevřete na kartě Vložit. + .label = Příkaz: + .literal-spaces = Doslovné mezery: mezerník vloží mezeru do řádku místo spuštění příkazu. Volba zůstane aktivní, dokud ji nevypnete. + .invalid = Neplatné + .unable-to-save-file-is-in-use = Nelze uložit: soubor se používá. + +start = + .new-drawing = Nový výkres + .open-file = Otevřít soubor… + .donate = Přispět + .send-feedback = Odeslat zpětnou vazbu + .sponsors = Sponzoři + .tutorials = Návody + .loading-videos = Načítání videí… + .videos-online = Videa se načítají z internetu. + .open-playlist = Otevřít seznam videí na YouTube + .discussions = Diskuze + .pinned = Připnuto + .loading-discussions = Načítání diskuzí… + .discussions-online = Diskuze se načítají z GitHubu. + .open-discussions = Otevřít diskuze na GitHubu + .supporters = Podporovatelé + .support-on-patreon = Podpořit na Patreonu + .recent-files = Nedávné soubory + .videos = Videa + .welcome = Vítejte + .recent-documents = Nedávné dokumenty + .no-recent-files = Otevřené soubory se zobrazí zde. + .browser-storage = Úložiště prohlížeče + .keep-recent-files = Uchovávat nedávné soubory + +modal = + .about = O aplikaci + .keyboard-shortcuts = Klávesové zkratky + .command-aliases = Aliasy příkazů + .find-replace = Najít a nahradit + .plugin-manager = Správce zásuvných modulů + .update-available = Je dostupná aktualizace + .layer-manager = Správce hladin + .layer-state-manager = Správce stavů hladin + .edit-layer-state = Upravit stav hladiny + .plot = Vykreslit + .layout-manager = Správce rozvržení + .scale-manager = Správce měřítek + .annotation-object-scale = Měřítko objektu poznámky + .plot-style-editor = Editor stylu vykreslování + .text-style-manager = Správce stylů textu + .multiline-style-manager = Správce stylů multičar + .table-style-manager = Správce stylů tabulek + .multileader-style-manager = Správce stylů multiodkazů + .dimension-style-manager = Správce kótovacích stylů + .default-application = Výchozí aplikace + .save-warning = Upozornění při ukládání + .unable-save = Výkres nelze uložit + .drawing-changed = Výkres byl na disku změněn + .delete-layer = Odstranit hladinu + .unsaved-changes = Neuložené změny + .unsaved-save-prompt = Chcete změny uložit? + .point-style = Styl bodu + .attribute-editor = Editor atributů + .save-drawing-as = Uložit výkres jako + .recovery-report = Zpráva o obnovení výkresu + .recovery-prompt = Obnovení výkresu + .file-name-could-not-be-saved = Soubor „__ocs_arg_file_name__“ se nepodařilo uložit. + .file-name-was-changed-by-another-application = Soubor „__ocs_arg_file_name__“ byl změněn jinou aplikací. + .count-selected-layers = Počet vybraných hladin: __ocs_arg_count__ + .subject-still-has-count-obj-deleting-will-also = + __ocs_arg_subject__ stále __ocs_arg_has__ __ocs_arg_count__ __ocs_arg_obj__. + Odstraněním se z výkresu odstraní také __ocs_arg_those__. Pokračovat? + .delete-objects = Odstranit objekty + .details-error = Podrobnosti: __ocs_arg_error__ + .file-name = Název souboru: + .format = Formát: + .layer-name = Hladina „__ocs_arg_name__“ + .make-open-cad-studio-your-default-cad-app = Nastavit Open CAD Studio jako výchozí CAD aplikaci? + .not-now = Teď ne + .open-dwg-and-dxf-drawings-in-open-cad-studio-b = Ve výchozím nastavení otevírat výkresy .dwg a .dxf v Open CAD Studio. Později to můžete změnit v nastavení systému. + .path-path = Cesta: __ocs_arg_path__ + .reload-from-disk = Znovu načíst z disku + .save-as = Uložit jako… + .saving-now-could-destroy-those-external-change = Uložením by mohly být ztraceny externí změny. Znovu načtěte kopii z disku, uložte místní práci jinam nebo výslovně potvrďte přepsání. + .the-file-is-open-or-being-used-by-another-appl = Soubor je otevřený nebo ho používá jiná aplikace. Zavřete ho v dané aplikaci a zkuste to znovu, případně výkres uložte pod jiným názvem. + .the-selected-layer-state-is-no-longer-availabl = Vybraný stav hladiny již není dostupný. + .this-drawing-contains-count-aec-civil-objects = + Tento výkres obsahuje __ocs_arg_count__ objektů AEC/Civil, které „__ocs_arg_target__“ nedokáže uložit, a proto nebudou uloženy. + Chcete-li je zachovat, uložte výkres ve zdrojové verzi (__ocs_arg_src_version__). + .yes-set-as-default = Ano, nastavit jako výchozí + .has = obsahuje + .hold = obsahuje + .objects = objektů + .that-object = daný objekt + .those-objects = dané objekty + +clipboard = + .no-supported-content = Schránka neobsahuje podporovaný text ani objekty výkresu. + .unavailable = Systémová schránka není dostupná. + .occupied = Systémová schránka se právě používá. Zkuste to znovu. + .conversion-failed = Text ze schránky se nepodařilo přečíst. + .copybase-specify-base-point = COPYBASE Určete základní bod: + .copy-to-clipboard = Kopírovat do schránky + .cut = Vyjmout + .cut-to-clipboard = Vyjmout do schránky + .pasteclip-pick-insertion-point = PASTECLIP Vyberte bod vložení: + .paste-as-block = Vložit jako blok + .paste-from-clipboard = Vložit ze schránky + .paste-to-original-coordinates = Vložit na původní souřadnice + +viewport = + .split-vertical = Rozdělit výřez svisle + .split-horizontal = Rozdělit výřez vodorovně + .move = Přesunout výřez + .close = Zavřít výřez + +recovery = + .opened-with-repairs = Výkres byl otevřen se zprávou o obnovení + .open-failed = Výkres se nepodařilo otevřít + .repaired-description = Při otevírání výkresu byly zjištěny problémy. Projděte zprávu a pokud byla data opravena, uložte výkres jako nový soubor. + .failed-description = Při obnovení se nepodařilo získat použitelná data výkresu. Projděte zprávu a zkuste zálohu nebo automaticky uloženou kopii. + .entities-checked = Zkontrolované entity + .issues-found = Nalezené problémy + .entities-removed = Odstraněné entity + .references-checked = Zkontrolované reference + .referenced-entities-removed = Odstraněné odkazované entity + .references-unavailable = Nedostupné reference + .log-path = Protokol obnovení + .log-write-failed = Protokol obnovení se nepodařilo zapsat + .log-download-ready = Zpráva o obnovení je připravena ke stažení. + .save-copy = Uložit obnovenou kopii + .show-log = Zobrazit protokol obnovení + .save-new-file-required = Opravený výkres musí být uložen do nového souboru. + .prompt-heading = Běžné otevření se nezdařilo + .prompt-description = Výkres nebyl změněn. Režim obnovení se může pokusit zachránit použitelná data, poškozené záznamy však mohou být přeskočeny. Chcete pokračovat? + .attempt = Zkusit obnovení + .decline = Zrušit +area = + .option-objects =Objekty + .option-back = Zpět + .option-add = Přidat oblast + .option-subtract = Odečíst oblast + .prompt-first = AREA Určete první rohový bod nebo [Objekty/Přidat oblast/Odečíst oblast] : + .prompt-add = AREA ADD Určete první rohový bod nebo [Objekty/oblast odečtení] : + .prompt-subtract = AREA SUBTRACT Určete první rohový bod nebo [Objekty/Přidat oblast] : + .prompt-next = AREA Zadejte další bod (vybráno { $count }, výpočet Enter): + .prompt-objects = AREA Vyberte objekty ({ $count } vybráno, Enter k výpočtu): + .objects-not-measurable = AREA: žádný z vybraných objektů nemá měřitelnou plochu. + .result = Plocha = { $area }, obvod = { $perimeter } + .result-area-only = Oblast = { $area } + .running-result = + Plocha = { $area }, obvod = { $perimeter } + Celková plocha = { $total_area }, Celkový obvod = { $total_perimeter } + .running-result-area-only = + Oblast = { $area } + Celková plocha = { $total_area } + +command-move = + .base = + MOVE Zadejte základní bod [{ $count -> + [one] { $count } objekt + *[other] { $count } objektů + }]: + .target = MOVE Zadejte cíl [základ { $x },{ $y }]: + +command-copy = + .array-count = COPY Enter počet položek do pole: + .base = + COPY Zadejte základní bod [{ $count -> + [one] { $count } objekt + *[other] { $count } objektů + }]: + .array-target = COPY Zadejte druhý bod pro pole položek { $count } [základní { $x },{ $y }]: + .target = + COPY Zadejte cíl [{ $count -> + [one] zatím { $count } kopie + *[other] zatím { $count } kopií + } | Pole | Enter=hotovo | základna { $x },{ $y }]: + +common = + .none-use-shortcuts-set-key-command =(žádné — použijte: SHORTCUTS SET ) + .alignment-indented-label-title-case = Zarovnání: + .background-fill-enabled = Výplň pozadí povolena + .borders-type-weight-color-spacing-hidden = Ohraničení (typ / hmotnost / barva / mezery / skryté) + .data-type = Typ dat: + .fill-color-indented-label-sentence-case = Barva výplně: + .format-string = Formátovat řetězec: + .off-color-ltype = Vypnuto Barva Ltype + .text-color-indented-label-sentence-case = Barva textu: + .text-height-indented-label-sentence-case = Výška textu: + .unit-type = Typ jednotky: + .name-already-in-use = Název "__ocs_fmt_0__" se již používá + .rrggbb-or-blank = #RRGGBB nebo prázdné + .default-parenthesized-lowercase = (výchozí) + .none-parenthesized-lowercase = (žádný) + .value-1st-arrowhead-dimblk1 = 1. hrot šipky (DIMBLK1) + .value-2d-wireframe = 2D drátěný model + .value-2nd-arrowhead-dimblk2 = 2. hrot šipky (DIMBLK2) + .value-3d-wireframe = 3D drátěný model + .value-3dalign-aligned-the-solid-to-the-destination-p = 3DALIGN: zarovná těleso k cílovým bodům. + .value-3dalign-each-point-triple-must-be-non-coincide = 3DALIGN: každá trojice bodů musí být neshodná a nekolineární. + .value-3dalign-select-exactly-one-solid-created-this = 3DALIGN: vyberte přesně jedno těleso vytvořené v této relaci. + .value-3dmirror-added-a-mirror-across-the-plane = 3DMIRROR: přidáno zrcadlo přes rovinu __ocs_fmt_0__. + .value-3dmirror-could-not-determine-the-solid-s-exten = 3DMIRROR: nemohl určit rozsah tělesa. + .value-3dmirror-select-exactly-one-solid-created-this = 3DMIRROR: vyberte přesně jedno těleso vytvořené v této relaci. + .value-3drotate-could-not-determine-the-solid-s-exten = 3DROTATE: nelze určit rozsah tělesa. + .value-3drotate-rotated-angle-deg-about-the-axis = 3DROTATE: otočeno o __ocs_fmt_0__° kolem osy __ocs_fmt_1__. + .value-3drotate-select-exactly-one-solid-created-this = 3DROTATE: vyberte přesně jedno těleso vytvořené v této relaci. + .align-applied = ALIGN: použito. + .above = Nahoře + .accept = Přijmout + .add = Přidat + .add-leader = Přidat vůdce + .align-space = Zarovnat prostor: + .aligned = Zarovnáno + .alignment-title-case = Zarovnání + .all = všechny + .alternate = Střídat + .alternate-units = Alternativní jednotky + .angle = Úhel + .angular-decimals-dimadec = Úhlová desetinná místa (DIMADEC) + .angular-unit-dimaunit = Úhlová jednotka (DIMAUNIT) + .angular-zero-supp-dimazin = Podpora úhlové nuly (DIMAZIN) + .annotation = Anotace + .annotative = Anotace + .apparent-intersection = Zdánlivá křižovatka + .apply = Použít + .apply-cell = Použít buňku + .apply-margins = Použít okraje + .arc = Oblouk + .arch =Oblouk + .architectural = Architektonické + .area = Oblast + .arrow-size = Velikost šipky + .arrowhead = Hrot šípu + .arrowhead-size = Velikost hrotu šipky + .as-displayed = Jak je zobrazeno + .attachment-direction = Směr přílohy: + .attribute = Atribut + .attributes = Atributy + .auto = Auto + .bedit-block-tab-name-is-not-open = BEDIT: karta bloku "__ocs_fmt_0__" není otevřená. + .break-entity-type-not-supported = BREAK: Typ entity není podporován. + .break-fragment-s = BREAK: Fragment(y) __ocs_fmt_0__. + .back = Zpět + .background-color = Barva pozadí + .backward = Pozadu + .baseline = Základní linie + .below = Níže + .block = Blokovat + .block-name-created = Blok "__ocs_fmt_0__" byl vytvořen. + .block-orig-new-name = Blok "__ocs_fmt_0__" → "__ocs_fmt_1__" + .block-content = Blokovat obsah + .block-color = Barva bloku + .block-connection = Blokovat připojení + .block-rotation = Rotace bloku + .blocks = Bloky + .blue = Modrá + .boolean-failed-the-solids-may-not-overlap = Logická hodnota selhala – tělesa se nemusí překrývat. + .boolean-select-exactly-two-solids-created-this = Boolean: vyberte přesně dvě tělesa vytvořená v této relaci. + .bottom = Spodní + .bottom-center-title-case = Spodní střed + .bottom-left-title-case = Vlevo dole + .bottom-right-title-case = Vpravo dole + .bottom-attachment = Spodní příloha: + .bottom-center-sentence-case = Spodní střed + .bottom-left-sentence-case = Vlevo dole + .bottom-right-sentence-case = Vpravo dole + .bottomcenter = Spodní střed + .bottomleft = Vlevo dole + .bottomright = Vpravo dole + .box = Krabice + .break = Přestávka + .brightness = Jas + .by-layer = Podle vrstvy + .byblock = ByBlock + .bylayer = Podle vrstvy + .convtosurface-converted-n-solid-s-to-surface-s = CONVTOSURFACE: převede tělesa __ocs_fmt_0__ na povrchy. + .convtosurface-select-a-solid-created-this-sess = CONVTOSURFACE: vyberte těleso vytvořené v této relaci. + .coords-mode-label = COORDS = __ocs_fmt_0__ (__ocs_fmt_1__) + .copy-specify-destination-count-copies-so-far-a = COPY Zadejte cíl [zatím kopie __ocs_arg_count__ | Pole | Enter=hotovo | základ __ocs_arg_bx__,__ocs_arg_by__]: + .cancel = Zrušit + .cancel-active-command = Zrušit aktivní příkaz + .cannot-delete-a-style-that-is-current-or-in-us = Styl, který je aktuální nebo používaný, nelze odstranit. + .cannot-delete-the-model-layout =Nelze odstranit rozvržení modelu. + .cannot-delete-the-standard-style = Standardní styl nelze odstranit. + .cannot-rename-the-model-layout = Rozvržení modelu nelze přejmenovat. + .cannot-rename-the-standard-style = Standardní styl nelze přejmenovat. + .catchment-tagged-successfully = Povodí úspěšně označeno. + .center = Střed + .center-mark-dimcen = Středová značka (DIMCEN) + .centered = Na střed + .centimeters = Centimetry + .chamfer = Zkosení + .changelog = Seznam změn + .changing-a-base-colour-switches-to-custom-iced = Změna základní barvy se přepne na Vlastní. Iced vytváří každý odstín složky z těchto šesti barev. + .circle = Kruh + .class-version = Verze třídy + .clear = Jasný + .clear-all = Vymazat vše + .clip = Klip + .clipboard = Schránka + .clipboard-is-empty = Schránka je prázdná. + .close = Zavřít + .closed-filled = Zavřeno naplněné + .color = Barva + .command-cancelled-because-the-active-drawing-s = Příkaz byl zrušen, protože se změnil aktivní kreslicí prostor. + .command-history-navigation = Navigace historie příkazů + .command-label-title-case = příkaz: + .conceptual = Koncepční + .cone = Kužel + .content = Obsah + .content-type = Typ obsahu: + .continue = Pokračovat + .contrast = Kontrast + .coordinates = Souřadnice + .copied-path = Zkopírovaná cesta: __ocs_fmt_0__ + .copy = Kopírovat + .could-not-open-file-location-error = Nelze otevřít umístění souboru: __ocs_fmt_0__ + .could-not-rename-block-orig = Nelze přejmenovat blok "__ocs_fmt_0__" + .could-not-set-default-app-err = Nelze nastavit výchozí aplikaci: __ocs_fmt_0__ + .crossing = Přejezd + .current-dim-style-set-to = Aktuální styl ztlumení nastaven na „__ocs_fmt_0__“. + .current-multiline-style = Aktuální víceřádkový styl: __ocs_fmt_0__ + .current-table-style-name = Aktuální styl tabulky: __ocs_fmt_0__ + .current-text-style = Aktuální styl textu: __ocs_fmt_0__ + .custom = Vlastní + .cyan = azurová + .cylinder = Válec + .dataextraction-rows-rows-fname = DATAEXTRACTION __ocs_fmt_0__ řádky → "__ocs_fmt_1__" + .dataextraction-write-failed-e = DATAEXTRACTION: zápis se nezdařil: __ocs_fmt_0__ + .ddedit-entity-type-not-supported = DDEDIT: Typ entity není podporován. + .ddedit-text-updated = DDEDIT: text aktualizován. + .dimbreak-not-yet-implemented-nothing-changed = DIMBREAK: dosud neimplementováno — nic se nezměnilo. + .dimjogline-not-yet-implemented-nothing-changed = DIMJOGLINE: dosud neimplementováno — nic se nezměnilo. + .dimspace-spacing-adjusted =DIMSPACE Rozteč upravena. + .divide-entity-type-not-supported-or-n-2 = DIVIDE: Typ entity není podporován nebo N < 2. + .divide-count-point-s-placed = DIVIDE: Umístění bodu(ů) __ocs_fmt_0__. + .draworder-select-entities-first = DRAWORDER: nejprve vyberte entity. + .danger = Nebezpečí + .data = Data + .data-row = Řádek dat: + .decimal = Desetinné + .decimal-sep-ascii-dimdsep = Desetinný odděl ASCII (DIMDSEP) + .decimal-separator = Desetinný oddělovač + .decimals-dimaltd = Desetinná čísla (DIMALTD) + .decimals-dimdec = Desetinná čísla (DIMDEC) + .default-title-case = Výchozí + .default-text = Výchozí text: + .deg-min-sec = Deg/min/sec + .delete = Smazat + .delete-selected-entities = Smazat vybrané entity + .description-title-case = Popis + .description-label-title-case = Popis: + .diameter = Průměr + .dim-style = Tlumený styl + .dimension = Dimenze + .dimensions = Rozměry + .discard = Zahodit + .discussions = Diskuse + .discussions-load-from-github = Načítání diskuzí ze GitHub. + .display = Displej + .distance = Vzdálenost + .donate = Darovat + .done = Hotovo + .double = Dvojité + .draft = Koncept + .draw = Kreslit + .drawing = Kreslení + .drawing-units = Výkresové jednotky + .drop-0-ft-keep-0-in = Klesejte 0 stop, udržujte 0 + .dynamic = Dynamický + .extrude-could-not-build-profile-select-a-close = EXTRUDE: Nelze vytvořit profil. Vyberte uzavřenou 2D entitu (Kruh, LwPolyline atd.). + .extrude-entity-not-found = EXTRUDE: entita nenalezena. + .extrude-solid-created = EXTRUDE: těleso vytvořeno. + .edge = Okraj + .editing-selected = Střih: __ocs_arg_selected__ + .elements = Prvky: + .ellipse = Elipsa + .enable-alternate-units-dimalt = Povolit alternativní jednotky (DIMALT) + .end-angle = Koncový úhel + .endpoint = Koncový bod + .engineering = Strojírenství + .enter = Enter + .expected-coordinates-x-y-or-a-number-got-text = Očekávané souřadnice (x,y) nebo číslo: "__ocs_fmt_0__" + .extension = Rozšíření + .extension-dimdle = Rozšíření (DIMDLE) + .extension-dimexe = Rozšíření (DIMEXE) + .extents = Rozsahy + .find-replace-replaced-1-occurrence-of-search = FIND/REPLACE: nahrazen 1 výskytem „__ocs_fmt_0__“. + .find-replace-replaced-replaced-occurrence-s-of = FIND/REPLACE: nahrazen __ocs_fmt_0__ výskyt(y) "__ocs_fmt_1__". + .flatshot-created-n-projected-edge-s-at-z-0 =FLATSHOT: vytvořen __ocs_fmt_0__ promítnutý okraj(y) na Z=0. + .flatshot-select-a-solid-created-this-session = FLATSHOT: vyberte těleso vytvořené v této relaci. + .fade = Vyblednout + .failed-to-create-layout-e = Vytvoření rozvržení se nezdařilo: __ocs_fmt_0__ + .feet = Nohy + .fence = Plot + .file-locations-are-unavailable-in-the-web-appl = Umístění souborů není ve webové aplikaci dostupné. + .fill-color-sentence-case = Barva výplně + .fill-color-aci-dimtfillclr = Barva výplně ACI (DIMTFILLCLR) + .fillet = Filet + .finalize-command-repeat-last = Dokončit příkaz / Opakovat jako poslední + .finalizing = Dokončování… + .find-and-replace = Najít a nahradit + .first-seg-angle = První seg. úhel: + .fit = Fit + .fit-dimatfit = Přizpůsobit (DIMATFIT) + .fit-legacy-dimfit = Přizpůsobit (starší DIMFIT) + .fixed-height = Pevná výška: + .fixed-length-dimfxl = Pevná délka (DIMFXL) + .fixed-length-ext-lines-dimfxlon = Ext linky pevné délky (DIMFXLON) + .flat-shaded = Ploché stínované + .flat-shaded-edges = Ploché stínované + okraje + .flow-direction = Směr toku: + .font-load-failed-script-e = Načtení písma se nezdařilo (__ocs_fmt_0__): __ocs_fmt_1__ + .force-text-inside-dimtix = Vynutit text uvnitř (DIMTIX) + .format = Formát + .format-dimlunit = Formát (DIMLUNIT) + .fraction-format-dimfrac = Formát zlomku (DIMFRAC) + .fractional = Zlomkové + .front = Přední + .full = Plný + .full-file-paths-are-unavailable-in-the-web-app = Úplné cesty k souborům nejsou ve webové aplikaci k dispozici. + .general = Generál + .generate-tolerances-dimtol = Vygenerovat tolerance (DIMTOL) + .geometry = Geometrie + .gouraud-shaded = Gouraud Shaded + .gouraud-shaded-edges = Gouraud stínované + hrany + .green = Zelená + .group = Skupina + .group-created = Byla vytvořena skupina „__ocs_fmt_0__“. + .groups = Skupiny + .h-margin = Okraj H: + .hatchedit-hatch-entity-not-found = HATCHEDIT: entita šrafování nebyla nalezena. + .hatchedit-hatch-updated = HATCHEDIT: poklop aktualizován. + .hatchedit-not-a-hatch-entity = HATCHEDIT: není šrafovací entita. + .handle = Rukojeť + .hatch = Poklop + .header-row = Řádek záhlaví: + .header-row-suppressed = Řádek záhlaví byl potlačen + .height = Výška + .height-dimtxt = Výška (DIMTXT) + .helix = Spirála + .hidden = Skrytá + .hidden-line = Skrytá čára + .high = Vysoká + .horizontal = Horizontální + .horizontal-inside-dimtih =Horizontální vnitřní (DIMTIH) + .horizontal-just-dimjust = Pouze horizontální (DIMJUST) + .horizontal-outside-dimtoh = Horizontální vnější (DIMTOH) + .hyperlink = Hypertextový odkaz + .image-short-pw-ph-px = IMAGE "__ocs_fmt_0__": __ocs_fmt_1__×__ocs_fmt_2__ px + .image-e = IMAGE: __ocs_fmt_0__ + .importobj-imported-file-stem-as-mesh = IMPORTOBJ: importováno „__ocs_fmt_0__“ jako síť. + .importobj-target-drawing-was-closed = IMPORTOBJ: cílové kreslení bylo uzavřeno. + .importobj-error = IMPORTOBJ: __ocs_fmt_0__ + .interfere-created-an-interference-solid-from-t = INTERFERE: vytvořil interferenční těleso z překrytí. + .interfere-select-exactly-two-solids-created-th = INTERFERE: vyberte přesně dvě tělesa vytvořená v této relaci. + .interfere-the-selected-solids-do-not-overlap = INTERFERE: vybraná tělesa se nepřekrývají. + .import = Importovat + .inches = palce + .index = Index + .insert = Vložit + .insert-block = Vložit blok + .insertion = Vkládání + .installed = Instalováno + .intersection = Křižovatka + .invert-selection-object-s-selected = Invertovat výběr: Vybrány objekty __ocs_fmt_0__. + .invisible = Neviditelný + .jis = JIS + .join-count-in-object-s-joined-into-count-out = JOIN: Objekty __ocs_fmt_0__ spojené do __ocs_fmt_1__. + .jog-angle-dimjogang = Úhel posuvu ° (DIMJOGANG) + .join = Připojte se + .keep-0-ft-drop-0-in = Udržujte 0 stop, spusťte 0 + .keep-recent-files = Uchovávejte poslední soubory + .kilometers = Kilometry + .layerstate-deleted-name = LAYERSTATE: odstraněno "__ocs_fmt_0__". + .layerstate-restored-name-restored-layer-s = LAYERSTATE: obnoveno "__ocs_fmt_0__" (vrstvy __ocs_fmt_1__). + .layerstate-saved-name-in-the-drawing = LAYERSTATE: uloženo "__ocs_fmt_0__" ve výkresu. + .layerstate-updated-name = LAYERSTATE: aktualizováno "__ocs_fmt_0__". + .layout-e = LAYOUT: __ocs_fmt_0__ + .lengthen-applied = LENGTHEN: použito. + .lengthen-entity-type-not-supported = LENGTHEN: Typ entity není podporován. + .loft-could-not-loft-profiles-ensure-sections-h = LOFT: Profily nelze loftovat. Ujistěte se, že sekce mají stejný počet hran a jsou kompatibilní. + .loft-solid-created-from-profiles = LOFT: těleso vytvořené z profilů __ocs_fmt_0__. + .landing-distance-sentence-case = Vzdálenost přistání + .landing-distance-label-sentence-case = Vzdálenost přistání: + .landing-gap-sentence-case = Přistávací mezera + .landing-gap-label-sentence-case = Přistávací mezera: + .landscape = Krajina + .language-title-case = Jazyk + .language-label-title-case = jazyk: + .layer = Vrstva + .layer-matched-to-layer = Vrstva odpovídá „__ocs_fmt_0__“. + .layer-state-name-already-exists =Stav vrstvy "__ocs_fmt_0__" již existuje. + .layer-state-name-was-not-found = Stav vrstvy "__ocs_fmt_0__" nebyl nalezen. + .layer-state-name-cannot-be-empty = Název stavu vrstvy nemůže být prázdný. + .layers = Vrstvy + .layout = Rozložení + .layout-name-silindi = Rozložení „__ocs_fmt_0__“ bylo odstraněno + .layout-new-name-created-use-mview-to-add-a-vie = Bylo vytvořeno rozvržení „__ocs_fmt_0__“ – pomocí MVIEW přidejte výřez + .layout-orig-new-name = Rozložení "__ocs_fmt_0__" → "__ocs_fmt_1__" + .layout-name-created = Bylo vytvořeno rozvržení '__ocs_fmt_0__'. + .layout-name-deleted = Rozvržení „__ocs_fmt_0__“ bylo smazáno. + .layout-manager = Správce rozložení + .layout-name-cannot-be-empty = Název rozvržení nemůže být prázdný. + .layout-renamed-old-name-new-name = Rozložení přejmenováno: „__ocs_fmt_0__“ → „__ocs_fmt_1__“ + .leader = vůdce + .leader-format = Formát vůdce + .leader-structure = Struktura vůdce + .leaders = Vedoucí + .leading-trailing = Vedení a konec + .left = Vlevo + .left-attachment = Levá příloha: + .left-to-right-dimtxtdirection = Zleva doprava (DIMTXTDIRECTION) + .length = Délka + .light = Světlo + .limits-generation-dimlim = Generování limitů (DIMLIM) + .line = Linka + .linear = Lineární + .lines = Čáry + .linetype-title-case = Typ čáry + .linetype-scale = Měřítko typu čáry + .lineweight-title-case = Tloušťka čáry + .list = Seznam + .loaded = Nabito + .loading-discussions = Načítání diskuzí… + .loading-videos = Načítání videí… + .load = Načíst… + .low = Nízká + .measure-entity-type-not-supported-or-distance = MEASURE: Typ entity není podporován nebo je vzdálenost příliš velká. + .measure-count-point-s-placed = MEASURE: Umístění bodu(ů) __ocs_fmt_0__. + .mleaderalign-leaders-aligned = MLEADERALIGN Vůdce zarovnány. + .mleadercollect-leaders-collected = MLEADERCOLLECT Lídři shromážděni. + .model-uppercase = MODEL + .ms-is-only-available-in-paper-space-layouts = MS je k dispozici pouze v rozložení papírového prostoru. + .mspace = MSPACE + .mtext = Mtext + .mview-the-clipping-boundary-has-no-usable-area = MVIEW: hranice oříznutí nemá žádnou použitelnou plochu. + .magenta = purpurová + .manage = Spravovat + .meters = metrů + .middle = Střední + .middle-center-title-case = Střed střed + .middle-left-title-case = Uprostřed vlevo + .middle-right-title-case = Uprostřed vpravo + .middle-center-sentence-case = Střed uprostřed + .middle-left-sentence-case = Uprostřed vlevo + .middle-right-sentence-case = Uprostřed vpravo + .middleleft = Uprostřed vlevo + .middleright =Uprostřed vpravo + .midpoint = Střed + .miles = Miles + .millimeters = milimetry + .model-title-case = Model + .modify = Upravit + .move = Pohybujte se + .multiple = Vícenásobné + .multiplier-dimaltf = Násobitel (DIMALTF) + .name-title-case = Jméno + .name-label-title-case = jméno: + .nearest = Nejbližší + .new = Nové + .no = ne + .no-drawing-open-use-new-or-open-first = Žádný výkres není otevřený – nejprve použijte Nový nebo Otevřít. + .no-groups-found-for-selected-objects = Pro vybrané objekty nebyly nalezeny žádné skupiny. + .no-plugin-handled-command = Nebyl zpracován žádný plugin: __ocs_fmt_0__ + .no-viewport-found-in-this-layout = V tomto rozložení nebyl nalezen žádný výřez. + .node = Uzel + .none-title-case = žádný + .normal = Normální + .nothing-found-at-that-point = V tu chvíli nebylo nic nalezeno. + .nothing-to-redo = Není co předělávat. + .nothing-to-undo = Není co vrátit. + .ok = OK + .object-title-case = Objekt + .object-is-on-locked-layer-layer-unlock-the-layer-to-edit-it = Objekt je na zamčené vrstvě "__ocs_fmt_0__" — odemkněte vrstvu, abyste ji mohli upravit. + .object-is-on-locked-layer-layer-unlock-the-layer-to-select-or-edit-it = Objekt je na zamčené vrstvě "__ocs_fmt_0__" — odemkněte vrstvu, abyste ji mohli vybrat nebo upravit. + .oblique = Šikmé (°): + .oblique-angle = Šikmý úhel + .off = Vypnuto + .offset = Offset + .on = Zapnuto + .open = Otevřít + .open-dwg-dxf-not-yet-implemented = Otevřít DWG/DXF: dosud neimplementováno. + .open-cancelled = Otevření zrušeno: "__ocs_fmt_0__" + .open-failed-e = Otevření se nezdařilo: __ocs_fmt_0__ + .open-or-create-a-drawing-to-add-a-layout = Otevřete nebo vytvořte výkres a přidejte rozvržení. + .open-or-create-a-drawing-to-manage-layer-state = Otevřete nebo vytvořte výkres pro správu stavů hladin. + .open-or-create-a-drawing-to-manage-layouts = Otevřete nebo vytvořte výkres pro správu rozvržení. + .open-or-create-a-drawing-to-switch-layouts = Otevřete nebo vytvořte výkres a přepněte rozvržení. + .opened-file-location = Umístění otevřeného souboru: __ocs_fmt_0__ + .opening-name-size-label = Otevírá se „__ocs_fmt_0__“ (__ocs_fmt_1__)… + .options-title-case = Možnosti + .origin = Původ + .ortho = Ortho + .output = Výstup + .outside = Venku + .outside-limits = Mimo limity. + .overall-scale = Celkové měřítko + .overwrite = Přepsat + .pedit-applied = PEDIT: použito. + .pedit-cannot-convert-this-entity = PEDIT: Tuto entitu nelze převést. + .pedit-operation-not-applicable-to-this-entity = PEDIT: operace se na tuto entitu nevztahuje. + .perf = PERF + .polysolid-built-a-wall-solid-from-used-segment =POLYSOLID: postavil pevnou stěnu ze segmentů __ocs_fmt_0__ (šířka __ocs_fmt_1__, výška __ocs_fmt_2__). + .polysolid-select-a-polyline-first = POLYSOLID: nejprve vyberte křivku. + .polysolid-the-polyline-has-no-usable-segments = POLYSOLID: křivka nemá žádné použitelné segmenty. + .pspace = PSPACE + .pyramid-could-not-build-all-faces = PYRAMID: Nelze sestavit všechny plochy. + .pyramid-created-a-n-sided-pyramid-radius-radiu = PYRAMID: vytvořil jehlan se stěnou __ocs_fmt_0__ (poloměr __ocs_fmt_1__, výška __ocs_fmt_2__). + .pyramid-radius-and-height-must-be-positive = PYRAMID: poloměr a výška musí být kladné. + .pyramid-tessellation-failed = PYRAMID: teselace se nezdařila. + .pyramid-tessellation-produced-no-geometry = PYRAMID: teselace neprodukovala žádnou geometrii. + .page-setup = Nastavení stránky + .paper = Papír + .parallel = Paralelní + .parsing-entities = Analýza entit… + .paste = Vložit + .perpendicular = Kolmý + .photometric = Fotometrické + .pick-a-point = Vyberte bod + .pick-an-object = Vyberte objekt + .pinned = Připnuto + .place-text-manually-dimupt = Umístit text ručně (DIMUPT) + .plot = Spiknutí + .plot-style = Styl zápletky + .plot-style-loaded-color-entries = Načten styl vykreslování '__ocs_fmt_0__' (barevné položky __ocs_fmt_1__). + .plot-style-table-cleared = Tabulka stylu vykreslování byla vymazána. + .plot-style-table-saved-to = Tabulka stylu vykreslování byla uložena do "__ocs_fmt_0__". + .plot-window-x0-2-y0-2-to-x1-2-y1-2 = Okno grafu: __ocs_fmt_0__,__ocs_fmt_1__ až __ocs_fmt_2__,__ocs_fmt_3__ + .plot-window-x1-2-y1-2-to-x2-2-y2-2 = Okno grafu: __ocs_fmt_0__,__ocs_fmt_1__ až __ocs_fmt_2__,__ocs_fmt_3__ + .plugin-id-failed-to-load-e = Plugin '__ocs_fmt_0__' se nepodařilo načíst: __ocs_fmt_1__ + .plugin-manager = Správce pluginů + .point = Bod + .point-cloud = Mrak bodů + .polyline = Polyline + .portrait = Portrét + .position = Pozice + .position-x = Pozice X + .position-y = Pozice Y + .position-z = Pozice Z + .precision = Přesnost + .preset = Přednastaveno + .previous = Předchozí + .primary = Primární + .primary-units = Primární jednotky + .print = Tisk + .print-failed-e = Tisk se nezdařil: __ocs_fmt_0__ + .prompt = Výzva + .properties = Vlastnosti + .properties-matched-to-object-s = Vlastnosti odpovídající objektu(ům) __ocs_fmt_0__. + .qselect-object-s-selected = QSELECT: Vybrány objekty __ocs_fmt_0__. + .quadrant = Kvadrant + .quality = Kvalita + .revolve-could-not-revolve-profile =REVOLVE: Profil nelze otočit. + .revolve-entity-not-found = REVOLVE: entita nenalezena. + .revolve-solid-created-0 = REVOLVE: vytvořeno pevné těleso (__ocs_fmt_0__°). + .radians = radiány + .radius = Poloměr + .ray = Paprsek + .read-only-session-read-only-saving-is-disabled = Relace pouze pro čtení (--read-only): ukládání je zakázáno. + .realistic = Realistické + .recent-documents = Nedávné dokumenty + .recent-files = Nedávné soubory + .recent-file-no-longer-exists = Poslední soubor již neexistuje: __ocs_fmt_0__ + .red = Červená + .redo-last-label = Znovu: __ocs_fmt_0__ + .reference = Odkaz + .reference-point = Referenční bod + .remove-leader = Odebrat vůdce + .rename = Přejmenovat + .renamed-old-new = Přejmenováno na „__ocs_fmt_0__“ → „__ocs_fmt_1__“. + .replace = Vyměňte + .replacement-text = Náhradní text + .report = Zpráva + .requires-0-or-1-or-multiple-or-single = Vyžaduje 0 OR 1 OR MULTIPLE OR SINGLE. + .requires-single-or-multiple = Vyžaduje Single nebo Multiple. + .restore = Obnovit + .retry = Zkuste to znovu + .reverse = Reverzní + .right = Správně + .right-attachment = Pravá příloha: + .rotate = Otočit + .rotation = Rotace + .round-off-dimaltrnd = Zaokrouhlení (DIMALTRND) + .round-off-dimrnd = Zaokrouhlení (DIMRND) + .section-could-not-build-the-cutting-plane = SECTION: Nelze sestavit rovinu řezu. + .section-could-not-determine-the-solid-s-extent = SECTION: Nelze určit rozsah tělesa. + .section-created-section-line-s-at-value = SECTION: vytvořena čára(y) řezu __ocs_fmt_0__ na __ocs_fmt_1__=__ocs_fmt_2__. + .section-select-exactly-one-solid-created-this = SECTION: vyberte přesně jedno těleso vytvořené v této relaci. + .section-solid-modelling-is-unavailable-in-this = SECTION: Solid modeling není v tomto sestavení k dispozici. + .section-the-plane-does-not-cross-the-solid = SECTION: rovina neprotíná těleso. + .slice-failed-the-plane-may-not-cross-the-solid = SLICE selhalo – letadlo nesmí protnout těleso. + .slice-could-not-determine-the-solid-s-extent = SLICE: Nelze určit rozsah tělesa. + .slice-cut-at-ax-value-kept-the-half = SLICE: řez na __ocs_fmt_0__=__ocs_fmt_1__, zachování poloviny __ocs_fmt_2__. + .slice-select-exactly-one-solid-created-this-se = SLICE: vyberte přesně jedno těleso vytvořené v této relaci. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: rovina neprotíná těleso na držené straně. + .splinefit-fit-a-spline-through-n-points = SPLINEFIT: proložení spline přes body __ocs_fmt_0__. + .splinefit-need-at-least-3-points =SPLINEFIT: potřebujete alespoň 3 body. + .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: vyberte křivku pro proložení spline. + .stepout-exported-to = STEPOUT: exportováno do "__ocs_fmt_0__" + .stepout-no-3d-mesh-data-in-this-drawing = STEPOUT: v tomto výkresu nejsou žádná data 3D sítě. + .stepout-error = STEPOUT: __ocs_fmt_0__ + .stlout-exported-to = STLOUT: exportováno do "__ocs_fmt_0__" + .stlout-no-3d-mesh-data-in-this-drawing = STLOUT: v tomto výkresu nejsou žádná data 3D sítě. + .stlout-error = STLOUT: __ocs_fmt_0__ + .stretch-nothing-crosses-the-window = STRETCH: Oknem nic nepřekročí. + .stretch-count-entity-ies-stretched = STRETCH: Entita(y) __ocs_fmt_0__ roztažená. + .sweep-could-not-sweep-profile-along-path-use-a = SWEEP: Nelze posunout profil podél cesty. Použijte uzavřený 2D profil a cestu čáry nebo křivky. + .sweep-solid-created = SWEEP: těleso vytvořeno. + .save = Uložit + .save-as = Uložit jako + .save-drawing-as = Uložit výkres jako + .save-already-running-for-this-drawing = Uložit již běžící pro tento výkres. + .save-anyway = Přesto uložit + .save-error-e = Chyba ukládání: __ocs_fmt_0__ + .save-in-source-version = Uložit ve zdrojové verzi + .save-the-drawing-before-copying-its-file-path = Před zkopírováním cesty k souboru výkres uložte. + .save-the-drawing-before-opening-its-file-locat = Před otevřením umístění souboru výkres uložte. + .saved-download-but-recent-copy-could-not-be-st = Stahování bylo uloženo, ale poslední kopii nelze uložit: __ocs_fmt_0__ + .scale = Měřítko + .scene-cleared = Scéna vymazána. + .scene-cleared-standard-linetypes-loaded = Scéna vymazána. Načteny standardní typy čar. + .scientific = Vědecký + .search-layers = Prohledat vrstvy… + .second-seg-angle = Druhý seg. úhel: + .select-all = Vyberte Vše + .select-similar-added = Vybrat Podobné: Přidáno __ocs_fmt_0__. + .select-a-tangent-object = Vyberte tečný objekt. + .select-one-object-first-then-run-objectscale = Nejprve vyberte jeden objekt a poté spusťte OBJECTSCALE. + .send-feedback = Odeslat zpětnou vazbu + .sent-to-printer-printer = Odesláno do tiskárny: __ocs_fmt_0__ + .set-current = Nastavit aktuální + .shade-plot = Stínový pozemek + .shadow-flags = Stínové vlajky + .single = Svobodný + .size = Velikost + .snap-override-label-next-pick-only = Přepsání uchopení: __ocs_fmt_0__ (pouze další výběr). + .solid-created = Pevná látka vytvořena. + .source-object-not-found = Zdrojový objekt nebyl nalezen. + .spacing = Mezery + .spacing-dimdli = Rozteč (DIMDLI) + .sphere = Koule + .spline = Spline + .sponsors = sponzoři + .standard = Standardní + .start =Začněte + .start-angle = Počáteční úhel + .status = Stav + .stretch = Protáhnout + .style-name-created = Styl '__ocs_fmt_0__' byl vytvořen. + .style-name-deleted = Styl „__ocs_fmt_0__“ byl smazán. + .style-new-already-exists = Styl '__ocs_fmt_0__' již existuje. + .success = Úspěch + .suffix-dimapost = Přípona (DIMAPOST) + .suffix-dimpost = Přípona (DIMPOST) + .support-on-patreon = Podpora na Patreon + .supporters = Příznivci + .suppress-0-ft-0-in = Potlačit 0 stop & 0 palců + .suppress-leading = Potlačit vedení + .suppress-leading-trailing = Potlačit vedení a sestup + .suppress-leading-zeros = Potlačit úvodní nuly + .suppress-none = Žádné potlačit + .suppress-outside-arrows-dimsoxd = Potlačit vnější šipky (DIMSOXD) + .suppress-trailing = Potlačit koncové + .suppress-trailing-zeros = Potlačit koncové nuly + .switched-to-layout-name = Přepnuto na rozvržení „__ocs_fmt_0__“. + .textedit-selected-entity-is-not-text = TEXTEDIT: vybraná entita není text. + .texteditmode-set-to-display-val = TEXTEDITMODE nastaveno na __ocs_fmt_0__ + .table = Tabulka + .tag = Štítek + .tangent = Tangenta + .text = Text + .text-alignment = Zarovnání textu: + .text-always-left = Text vždy vlevo + .text-angle = Úhel textu: + .text-color-sentence-case = Barva textu + .text-color-aci = Barva textu (ACI): + .text-color-aci-dimclrt = Barva textu ACI (DIMCLRT) + .text-fill-mode-dimtfill = Režim textové výplně (DIMTFILL) + .text-frame = Textový rámeček + .text-gap-dimgap = Mezera textu (DIMGAP) + .text-height-sentence-case = Výška textu + .text-height-label-sentence-case = Výška textu: + .text-movement = Pohyb textu + .text-movement-dimtmove = Pohyb textu (DIMTMOVE) + .text-out-first = Nejprve odešlete SMS + .text-style = Styl textu + .text-to-find = Text k nalezení + .the-selected-object-does-not-support-annotatio = Vybraný objekt nepodporuje měřítka poznámek. + .tick-size-dimtsz = Velikost klíštěte (DIMTSZ) + .title = Název + .title-row = Titulní řádek: + .title-row-suppressed = Řádek nadpisu byl potlačen + .toggle-dynamic-input = Přepnout dynamický vstup + .toggle-object-snap = Přepnout uchopení objektu + .toggle-object-snap-tracking = Přepnout sledování uchopení objektu + .toggle-ortho = Přepnout Ortho + .toggle-polar-tracking = Přepnout polární sledování + .tol-decimals-dimalttd = Tol desetinná místa (DIMALTTD) + .tol-zero-supp-dimalttz = Tol zero supp (DIMALTTZ) + .tol-decimals-dimtdec = Tol. desetinná místa (DIMTDEC) + .tol-vert-just-dimtolj =Tol. vert just (DIMTOLJ) + .tol-zero-supp-dimtzin = Tol. nulová podpora (DIMTZIN) + .tolerance = Tolerance + .tolerance-precision = Toleranční přesnost + .tolerances = Tolerance + .tool-palettes = + Nástroj + Palety + .top = Nahoru + .top-center-title-case = Nahoru Střed + .top-left-title-case = Vlevo nahoře + .top-right-title-case = Vpravo nahoře + .top-attachment = Horní příloha: + .top-center-sentence-case = Horní střed + .top-left-sentence-case = Vlevo nahoře + .top-right-sentence-case = Vpravo nahoře + .top-to-bottom = Shora dolů + .topcenter = Horní střed + .topleft = Vlevo nahoře + .topright = Vpravo nahoře + .torus = Torus + .transparency = Transparentnost + .truetype-system = TrueType (systém) + .ttr = Ttr + .tutorials = Tutoriály + .type = Typ + .ucs-world = UCS: Svět + .ucs = UCS: __ocs_fmt_0__ + .unable-to-change-ui-language-error = Nelze změnit jazyk UI: __ocs_fmt_0__ + .undo = Vrátit zpět + .undo-last-label = Zpět: __ocs_fmt_0__ + .uniform-scale = Jednotné měřítko + .unit = Jednotka + .unit-format-dimaltu = Formát jednotky (DIMALTU) + .unit-format-dimunit = Formát jednotky (DIMUNIT) + .unitless = Bez jednotky + .unknown = Neznámý + .unsaved-changes = Neuložené změny + .unsupported-file-type = Nepodporovaný typ souboru: __ocs_fmt_0__ + .up = Nahoru + .update-available = Aktualizace k dispozici + .upside-down = Vzhůru nohama + .v-margin = Okraj V: + .vplayer-frozen-count-layer-s-frozen-in-viewpor = VPLAYER: __ocs_fmt_0__ vrstva(y) zmrazená ve výřezu. + .vplayer-thawed-count-layer-s-thawed-in-viewpor = VPLAYER: Vrstva(y) __ocs_fmt_0__ ve výřezu rozmrzla. + .value = Hodnota + .version = Verze + .vertical = Vertikální + .vertical-pos-dimtad = Vertikální pozice (DIMTAD) + .vertical-pos-dimtvp = Vertikální pozice (DIMTVP) + .videos = videa + .videos-load-from-the-internet = Videa se načítají z internetu. + .view = Zobrazit + .view-home = Pohled: Domů + .view-value = Pohled: __ocs_fmt_0__ + .viewport = Výřez + .viewport-visual-style-label = Vizuální styl výřezu: __ocs_fmt_0__ + .visual-style = Vizuální styl + .visual-style-label = Vizuální styl: __ocs_fmt_0__ + .wblock-saved-block-name = WBLOCK Uloženo "__ocs_fmt_0__" → "__ocs_fmt_1__" + .wblock-save-failed-error = Uložení WBLOCK se nezdařilo: __ocs_fmt_0__ + .web = Web + .wedge =Klín + .weight = Hmotnost + .welcome = Vítejte + .white = Bílá + .width = šířka + .width-factor-title-case = Šířkový faktor + .width-factor-label-title-case = Šířkový faktor: + .width-factor-sentence-case = Šířkový faktor + .window = okno + .wipeout = Wipeout + .wireframe = Drátový model + .with-text = S Textem + .world = svět + .xattach-e = XATTACH: __ocs_fmt_0__ + .xline = XLine + .xref-not-found = XREF Nenalezeno: "__ocs_fmt_0__" (__ocs_fmt_1__) + .xref-unloaded-skipped = XREF Nenačteno (přeskočeno): "__ocs_fmt_0__" + .yards = Yardy + .yellow = Žlutá + .yes = Ano + .zero-suppress-dimaltz = Potlačení nuly (DIMALTZ) + .zero-suppress-dimzin = Potlačení nuly (DIMZIN) + .zoom-extents = Rozsahy přiblížení + .zoom-in = Přiblížit + .zoom-out = Oddálit + .zoom-window = Zoom okno + .command-lowercase = příkaz + .e-g-arial = např. Arial + .linetype-lowercase = typ čáry + .lineweight-lowercase = tloušťka čáry + .object-lowercase = objekt + .options-lowercase = možnosti + .units = jednotek + .count-object-s-added-to-selection = Objekt(y) __ocs_fmt_0__ přidán do výběru. + .count-object-s-pasted = Objekt(y) __ocs_fmt_0__ vložen. + .label-count-noun-created = __ocs_fmt_0__: __ocs_fmt_1__ __ocs_fmt_2__ vytvořeno. + .acquired = Získáno __ocs_fmt_0__. + .alias-es-applied = Byly použity aliasy __ocs_fmt_0__. + .group-s-dissolved = Skupina(y) __ocs_fmt_0__ rozpuštěna. + .layer-s-turned = Vrstva(y) __ocs_fmt_0__ se změnila na __ocs_fmt_1__ + .layer-s = __ocs_fmt_0__ vrstva(y) __ocs_fmt_1__ + .mod-shortcuts = ── __ocs_arg_MOD__ Zkratky ────────────────────────────────└──└─ — + .custom-overrides-shortcuts-set = ── Vlastní přepisy (SHORTCUTS SET) ────────────────── + .function-keys = ── Funkční klávesy ────────────────────────────────└──└─ — + .other-keys = ── Další klávesy ───────────────────── ───────────────────── + .prefix = Předpona + .suffix = Přípona + +errors = + .save-warning = Uložit varování + .unable-to-save-drawing = Nákres nelze uložit + .warning = Varování + +command = + .name-select-objects-then-press-enter = __ocs_arg_name__ vyberte objekty a stiskněte Enter: + .name-which-register-1-5 = __ocs_arg_name__ který registr? [1-5]: + .name-slot-new-value = Nová hodnota __ocs_arg_name____ocs_arg_slot__: + .cancel = *Zrušit* + .add-alias = + Přidat alias + .alias-title-case = Přezdívka + .cannot-add-a-vertex-here =Zde nelze přidat vrchol. + .cannot-break-at-this-vertex = V tomto vrcholu nelze zlomit. + .cannot-place-the-new-vertex = Nelze umístit nový vrchol. + .command = Příkaz + .command-aliases = Aliasy příkazů + .key = klíč + .keyboard-shortcuts = Klávesové zkratky + .layer-0-and-the-current-layer-can-t-be-deleted = Vrstvu "0" a aktuální vrstvu nelze smazat – přeskočit. + .no-drawing-open-use-new-or-open-to-start-a-dra = Žádný otevřený výkres. Ke spuštění kreslení použijte NEW nebo OPEN. + .object-is-on-locked-layer-layer-unlock-the-lay = Objekt je na zamknuté vrstvě "__ocs_fmt_0__" — odemkněte vrstvu a upravte její atributy. + .pan-ended = PAN skončilo. + .polyline-broken-at-vertex = Křivka přerušená ve vrcholu. + .rename-select-the-object-type-to-rename = RENAME Vyberte typ objektu, který chcete přejmenovat: + .rename-type-enter-the-current-name = RENAME __ocs_arg_type__ Enter aktuální název: + .rename-type-rename-old-to = RENAME __ocs_arg_type__ Přejmenujte "__ocs_arg_old__" na: + .specify-new-vertex-location = Zadejte nové umístění vrcholu: + .type-shortcuts-set-key-cmd-to-add-custom-short = Chcete-li přidat vlastní zástupce, zadejte SHORTCUTS SET . + .type-an-alias-and-the-command-it-runs-e-g-l-li = Zadejte alias a příkaz, který se spustí (např. L → LINE). Použít pro uložení do ocad.pgp; uzavřením se zahodí nepoužité úpravy. + .unknown-command-cmd = Neznámý příkaz: __ocs_fmt_0__ + .alias-lowercase = alias + .label = __ocs_fmt_0__: + .expected-a-number-got-raw = __ocs_fmt_0__: očekáváno číslo, mám "__ocs_fmt_1__" + .tcount-select-text-objects-then-press-enter = TCOUNT vyberte textové objekty a stiskněte Enter: + .tcount-starting-number-start = Startovní číslo TCOUNT <__ocs_fmt_0__>: + .tcount-increment-increment = Přírůstek TCOUNT <__ocs_fmt_0__>: + .tcount-placement-overwrite-prefix-suffix-overw = Umístění TCOUNT [přepsat/předpona/přípona] : + +file = + .parse-ms-purge-ms-caches-ms-xref-ms-total-ms = analyzovat __ocs_fmt_0__ms · vyčistit __ocs_fmt_1__ms · mezipaměti __ocs_fmt_2__ms · xref __ocs_fmt_3__ms · celkem __ocs_fmt_4__ms + .archive-save-the-drawing-first-it-has-no-file = ARCHIVE: nejprve uložte výkres (ještě nemá žádnou cestu k souboru). + .autosaved-1-drawing = Automaticky uložen 1 výkres + .background-colour-default-black-darkgray-gray = Barva BACKGROUND [Výchozí/Černá/Tmavě šedá/Šedá/Světlešedá/Bílá] nebo R G B (0–255): + .background-reset-to-default = Pozadí resetováno na výchozí. + .background-rgb = Pozadí: rgb (__ocs_fmt_0__, __ocs_fmt_1__, __ocs_fmt_2__) + .created-new-ctb-table-aci-aci-updated = Byla vytvořena nová tabulka CTB, aktualizováno ACI __ocs_fmt_0__. + .default-save-format = Výchozí formát uložení: + .do-you-want-to-save-changes-to-name =Chcete uložit změny "__ocs_arg_name__"? + .drawing-changed-on-disk = Kresba změněna na disku + .drawing-units-insunits-click-to-change = + Výkresové jednotky (INSUNITS) + Klikněte pro změnu + .drawing-changed-on-disk-while-it-was-opening-s = Kresba se změnila na disku během jeho otevírání; Uložit bude vyžadovat vyřešení konfliktu. + .edit-lease-active-warning-external-change-chec = Upravit pronájem aktivní; __ocs_fmt_0__. Kontroly externí změny zůstávají aktivní. + .files-you-open-will-show-up-here = Zde se zobrazí soubory, které otevřete. + .new-drawing = Nová kresba + .no-plot-style-table-loaded-load-or-create-one = Nebyla načtena žádná tabulka stylu vykreslování. Nejprve načtěte nebo vytvořte jeden. + .nothing-to-plot-model-space-contains-no-printa = Není co vykreslovat: modelový prostor neobsahuje žádnou tisknutelnou geometrii. + .oops-nothing-to-restore = OOPS: není co obnovit. + .oops-restored-n-object-s = OOPS: obnovený objekt(y) __ocs_fmt_0__. + .open-cad-studio-ready = Open CAD Studio připraven. + .open-discussions-on-github = Otevřít diskuse na GitHub + .open-file-title-case = Otevřete soubor + .open-file-picker-title-case = Otevřít soubor… + .open-and-save = Otevřít a uložit + .open-playlist-on-youtube = Otevřít seznam skladeb na YouTube + .opened-name-entity-count-entities = Otevřeno „__ocs_fmt_0__“ — entity __ocs_fmt_1__ + .opened-printer-properties = Otevřené vlastnosti tiskárny. + .opened-read-only-against-other-editors-error = Otevřeno pouze pro čtení proti jiným editorům: __ocs_fmt_0__ + .page-setup-applied-to-the-layout = Nastavení stránky použité na rozvržení. + .path-type = Typ cesty: + .plot-area-is-empty-pick-a-larger-window = Plocha pozemku je prázdná. Vyberte si větší okno. + .plot-style-aci-aci-updated = Styl vykreslování ACI __ocs_fmt_0__ aktualizován. + .plot-style-table-is-not-loaded = Tabulka stylu vykreslování '__ocs_fmt_0__' není načtena. + .quick-print-works-in-model-space = Rychlý tisk funguje v modelovém prostoru. + .reload-failed-error = Opětovné načtení se nezdařilo: __ocs_fmt_0__ + .saveall-save-each-tab-individually-in-the-web = SAVEALL: uložte každou kartu samostatně ve webovém sestavení. + .saveall-saved-saved-drawing-s = SAVEALL: uložený výkres(y) __ocs_fmt_0__ __ocs_fmt_1__. + .saveall-already-has-a-save-running = SAVEALL: __ocs_fmt_0__ již probíhá ukládání + .saveall-failed-e = SAVEALL: __ocs_fmt_0__ se nezdařilo: __ocs_fmt_1__ + .script-path-to-the-scr-file = Cesta SCRIPT k souboru .scr: + .script-cannot-read-p-e = SCRIPT: nelze číst __ocs_fmt_0__: __ocs_fmt_1__ + .script-running-command-s-from-p = SCRIPT: spuštění příkazu(ů) __ocs_fmt_0__ ze __ocs_fmt_1__. + .save-failed-error = Uložení se nezdařilo: __ocs_fmt_0__ + .save-failed-e = Uložení se nezdařilo: __ocs_fmt_0__ + .save-stopped-file-name-changed-outside-open-ca = Ukládání zastaveno: „__ocs_fmt_0__“ změněno mimo Open CAD Studio. + .saved-but-the-refreshed-drawing-is-locked-by-a =Uloženo, ale obnovený výkres je uzamčen jiným editorem: __ocs_fmt_0__ + .saved-filename = Uloženo: __ocs_fmt_0__ + .saved = Uloženo: __ocs_fmt_0__ + .selection-has-no-printable-area = Výběr nemá tisknutelnou oblast. + .selection-has-no-printable-geometry = Výběr nemá tisknutelnou geometrii. + .sending-to-system-printer = Odesílání na systémovou tiskárnu… + .type-a-command-or-use-the-ribbon-open-obj-inse = Zadejte příkaz nebo použijte pás karet. Otevřete OBJ: karta INSERT. + .unable-to-acquire-edit-lock-error = Nelze získat zámek úprav: __ocs_fmt_0__ + .unable-to-save-file-name-file-is-in-use-by-ano = Nelze uložit "__ocs_fmt_0__": soubor je používán jinou aplikací. + .unable-to-save-error = Nelze uložit "__ocs_fmt_0__": __ocs_fmt_1__ + .usage-script-path-to-scr-file = Použití: SCRIPT + .usage-undo-number-of-steps = Použití: UNDO [počet kroků] + .used-for-the-first-save-of-a-new-drawing-exist = Používá se pro první uložení nového výkresu. Stávající výkresy si ponechají svůj typ souboru a verzi. + .visual-style-label = Vizuální styl: __ocs_fmt_0__. + .warning-corrupt-entities-dropped-parser-junk-b = Varování: __ocs_fmt_0__ poškozené entity byly odstraněny (nevyžádaná analyzátor – špatné normály / počty) + .warning-corrupt-xref-entities-dropped = Varování: __ocs_fmt_0__ poškozené entity externí reference byly zrušeny + .xref-loaded = XREF Načteno "__ocs_fmt_0__" + .cmd-cannot-create-folder-e = __ocs_fmt_0__: Nelze vytvořit složku (__ocs_fmt_1__). + .cmd-packaged-copied-file-s-into = __ocs_fmt_0__: zabalené soubory __ocs_fmt_1__ do __ocs_fmt_2__ + .error-external-change-checks-remain-active = __ocs_fmt_0__. Kontroly externí změny zůstávají aktivní. + .warning-external-change-checks-remain-active = __ocs_fmt_0__. Kontroly externí změny zůstávají aktivní. + .opening-a-drawing-by-path-is-not-available-in = Otevření výkresu podle cesty není v prohlížeči k dispozici. + +selection = + .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Vyberte objekty (vybráno __ocs_arg_count__, použít Enter): + .cmd-select-objects = __ocs_arg_cmd__ Vyberte objekty: + .append-to-current-selection = Připojit k aktuálnímu výběru + .last = Poslední + +inquiry = + .handle-x = Rukojeť __ocs_fmt_0__: "__ocs_fmt_1__" + .k-n = __ocs_fmt_0__: __ocs_fmt_1__ + .v = = __ocs_fmt_0__ + .addselected-creating-a-new-kind-is-not-support = ADDSELECTED: Vytvoření nového __ocs_fmt_0__ není podporováno. + .addselected-selected-object-not-found = ADDSELECTED: vybraný objekt nebyl nalezen. + .add-selected-drawing-a-new-kind-on-layer-layer = Přidat vybrané: kreslení nového __ocs_fmt_0__ na vrstvu "__ocs_fmt_1__". + .angle-angle = Úhel = __ocs_arg_angle__° + .angle-0-0000-degenerate-rays = Úhel = 0.0000° (degenerované paprsky) + .area-area-perimeter-perimeter = Plocha = __ocs_arg_area__, obvod = __ocs_arg_perimeter__ + .bedit-block-edit-discarded =BEDIT: Úprava bloku „__ocs_fmt_0__“ byla zahozena. + .bedit-block-saved-all-references-updated = BEDIT: Blok "__ocs_fmt_0__" uložen. Všechny reference aktualizovány. + .bedit-editing-block-use-save-block-or-discard = BEDIT: Editační blok "__ocs_fmt_0__". K dokončení použijte Uložit blok nebo Zahodit. + .bedit-block-not-found = BEDIT: blok "__ocs_fmt_0__" nebyl nalezen. + .bedit-cannot-edit-an-external-reference-xref = BEDIT: nelze upravit externí referenci (xref). + .bedit-finish-the-active-refedit-refclose-first = BEDIT: dokončete nejprve aktivní REFEDIT (REFCLOSE). + .bedit-selected-object-is-not-a-block-reference = BEDIT: vybraný objekt není odkazem na blok. + .bedit-discard-invalid-block-editor-state = BEDIT_DISCARD: neplatný stav editoru bloku. + .bedit-discard-no-block-editor-is-open = BEDIT_DISCARD: není otevřen žádný editor bloků. + .bedit-save-invalid-block-editor-state = BEDIT_SAVE: neplatný stav editoru bloku. + .bedit-save-no-block-editor-is-open = BEDIT_SAVE: není otevřen žádný editor bloků. + .cal-expression-e-g-2-3-4 = Výraz CAL (např. (2+3)*4): + .cal-e = CAL: __ocs_fmt_0__ + .count-tally-all-by-type-by-layer = Shoda COUNT [Vše (podle typu) / podle vrstvy]: + .count-total-entity-ies-total = COUNT: Celkový počet entit __ocs_fmt_0__. + .dblist-drawing-has-no-entities = DBLIST: výkres nemá žádné entity. + .dblist-count-entit = DBLIST: Objekty __ocs_fmt_0__. + .dist-specify-first-point = DIST Zadejte první bod: + .dist-specify-second-point = DIST Zadejte druhý bod: + .divide-enter-number-of-segments = DIVIDE Enter počet segmentů: + .divide-select-object-to-divide = DIVIDE Vyberte objekt k rozdělení: + .deselected = Výběr zrušen. + .distance-dist-angle-in-xy-plane-angle-xy-delta = + Vzdálenost = __ocs_arg_dist__, úhel v rovině XY = __ocs_arg_angle_xy__° + Delta X = __ocs_arg_dx__, Delta Y = __ocs_arg_dy__, Delta Z = __ocs_arg_dz__ + .distance-dist-angle-in-xy-plane-angle-xy-angle = + Vzdálenost = __ocs_arg_dist__, úhel v rovině XY = __ocs_arg_angle_xy__°, úhel od roviny XY = __ocs_arg_angle_z__° + Delta X = __ocs_arg_dx__, Delta Y = __ocs_arg_dy__, Delta Z = __ocs_arg_dz__ + .find-replace-replaced-occurrence-s-of = FIND/REPLACE: nahrazen __ocs_fmt_0__ výskyt(y) "__ocs_fmt_1__" → "__ocs_fmt_2__". + .find-not-found = FIND: "__ocs_fmt_0__" nebyl nalezen. + .find-specify-search-text = FIND: zadejte hledaný text. + .find-match-es-for = FIND: __ocs_fmt_0__ shoda(y) pro "__ocs_fmt_1__". + .findall-text-to-find-add-replace-text-by-typin = FINDALL text k nalezení (přidejte REPLACE zadáním): + .flatten-no-entities =FLATTEN: žádné entity. + .flatten-entity-ies-moved-to-z-0 = FLATTEN: Entita(y) __ocs_fmt_0__ přesunuta do Z=0. + .id-specify-point = ID Určete bod: + .list-no-entities-selected-select-entities-firs = LIST: nebyly vybrány žádné entity. Nejprve vyberte entity. + .massprop-no-entities-selected-select-entities = MASSPROP: nebyly vybrány žádné entity. Nejprve vyberte entity. + .measure-select-object-to-measure = MEASURE Vyberte objekt k měření: + .measure-specify-segment-length = MEASURE Zadejte délku segmentu: + .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter možnost [Vzdálenost/Poloměr/Úhel/Area]: + .measuregeom-select-arc-or-circle = MEASUREGEOM Vyberte oblouk nebo kružnici: + .measuregeom-specify-first-corner-point-enter-t = MEASUREGEOM Určete první rohový bod (zrušení Enter): + .measuregeom-specify-first-point = MEASUREGEOM Zadejte první bod: + .measuregeom-specify-first-ray-point = MEASUREGEOM Určete bod prvního paprsku: + .measuregeom-specify-next-point-n-picked-enter = MEASUREGEOM Zadejte další bod (vybráno __ocs_arg_n__, výpočet Enter): + .measuregeom-specify-second-point = MEASUREGEOM Zadejte druhý bod: + .measuregeom-specify-second-ray-point = MEASUREGEOM Zadejte bod druhého paprsku: + .measuregeom-specify-vertex-point = MEASUREGEOM Zadejte vrcholový bod: + .measure-geometry = Změřte geometrii + .qselect-no-matching-entities = QSELECT: žádné odpovídající entity. + .qselect-entity-ies-selected = QSELECT: Vybrané entity __ocs_fmt_0__. + .refclose-block-saved-all-references-updated = REFCLOSE: Blok "__ocs_fmt_0__" uložen. Všechny reference aktualizovány. + .refclose-changes-discarded = REFCLOSE: Změny byly zahozeny. + .refclose-no-refedit-session-active = REFCLOSE: není aktivní žádná relace REFEDIT. + .refedit-editing-block-run-refclose-to-save-ref = REFEDIT: Editační blok "__ocs_fmt_0__". Spusťte REFCLOSE pro uložení, REFCLOSE_DISCARD pro zrušení. + .refedit-a-session-is-already-active-use-refclo = REFEDIT: relace je již aktivní. Nejprve použijte REFCLOSE. + .refedit-block-not-found = REFEDIT: blok "__ocs_fmt_0__" nebyl nalezen. + .refedit-block-is-empty = REFEDIT: blok je prázdný. + .refedit-selected-object-is-not-an-insert = REFEDIT: vybraný objekt není INSERT. + .radius-radius-diameter-diameter = Rádius = __ocs_arg_radius__, Průměr = __ocs_arg_diameter__ + .selectall-object-s-selected = SELECTALL: Vybrány objekty __ocs_fmt_0__. + .selhandles-no-entities-selected-select-entitie = SELHANDLES: nebyly vybrány žádné entity. Nejprve vyberte entity. + .usage-cal-expression-e-g-cal-2-3-4 = Použití: CAL např. CAL (2+3)*4 + .usage-qselect-type-layer-color-linetype-value = Použití: QSELECT TYPE|LAYER|COLOR|LINETYPE + .x-x-y-y-z-z =X = __ocs_arg_x__, Y = __ocs_arg_y__, Z = __ocs_arg_z__ + .type-name-handle-x-layer-color-lt = __ocs_fmt_0__ Rukojeť:__ocs_fmt_1__ Vrstva:__ocs_fmt_2__ Barva:__ocs_fmt_3__ LT:__ocs_fmt_4____ocs_fmt_5__ + .area-4-perimeter-4-centroid-4-4 = __ocs_fmt_0__ Plocha=__ocs_fmt_1__ Obvod=__ocs_fmt_2__ Centroid=(__ocs_fmt_3__,__ocs_fmt_4__) + +draw = + .insert-x-no-attributes = Vložit __ocs_fmt_0__: žádné atributy. + .tag-val = [__ocs_fmt_0__] = __ocs_fmt_1__ + .no-closed-boundary-found = ⚠ Nebyla nalezena žádná uzavřená hranice. + .inverted = , obráceně + .value-2-point = 2-bodový + .value-2d-solid = 2D pevné + .value-3-point = 3-bodový + .value-3d-polyline = 3D křivka + .value-3dmirror-mirror-plane-x-y-z = 3DMIRROR zrcadlová rovina [X / Y / Z]: + .value-3dpoly-specify-next-point-close-undo = 3DPOLY Určete další bod [Zavřít/Zpět]: + .value-3dpoly-specify-next-point-undo = 3DPOLY Určete další bod [Undo]: + .value-3dpoly-specify-start-point-of-polyline = 3DPOLY Určete počáteční bod křivky: + .value-3drotate-angle-in-degrees = 3Úhel OTÁČENÍ ve stupních: + .value-3drotate-rotation-axis-x-y-z = 3DROTATE rotační osa [X / Y / Z]: + .arc-specify-center = ARC Specifikujte střed: + .arc-specify-end-point-c-cx-cy-r-r = ARC Zadejte koncový bod [c=(__ocs_arg_cx__,__ocs_arg_cy__) r=__ocs_arg_r__]: + .arc-specify-start-point = ARC Zadejte počáteční bod: + .arc-3p-specify-end-point = ARC 3P Zadejte koncový bod: + .arc-3p-specify-second-point-on-arc = ARC 3P Určete druhý bod na oblouku: + .arc-3p-specify-start-point = ARC 3P Zadejte počáteční bod: + .arc-csa-click-end-direction-or-type-arc-span-i = ARC CSA Klikněte na směr konce nebo zadejte rozsah oblouku ve stupních [start=__ocs_arg_sa__]: + .arc-csa-specify-center = ARC CSA Zadejte střed: + .arc-csa-specify-start-point = ARC CSA Zadejte počáteční bod: + .arc-csl-click-chord-end-or-type-chord-length-r = ARC CSL Klikněte na konec akordu nebo zadejte délku akordu [r=__ocs_arg_r__]: + .arc-csl-specify-center = ARC CSL Zadejte střed: + .arc-csl-specify-start-point = ARC CSL Zadejte počáteční bod: + .arc-continue-no-previous-line-or-arc-to-contin = ARC Pokračovat Žádný předchozí řádek nebo oblouk pro pokračování. + .arc-continue-specify-end-point-ctrl-flip-direc = ARC Pokračovat Určete koncový bod [Ctrl = směr převrácení]: + .arc-sca-click-end-direction-or-type-arc-span-i = ARC SCA Klikněte na směr konce nebo zadejte rozsah oblouku ve stupních [start=__ocs_arg_sa__]: + .arc-sca-specify-center =ARC SCA Zadejte střed: + .arc-sca-specify-start-point = ARC SCA Zadejte počáteční bod: + .arc-sce-specify-center = ARC SCE Zadejte střed: + .arc-sce-specify-end-point-r-r = ARC SCE Zadejte koncový bod [r=__ocs_arg_r__]: + .arc-sce-specify-start-point = ARC SCE Zadejte počáteční bod: + .arc-scl-click-chord-end-or-type-chord-length-r = ARC SCL Klikněte na konec akordu nebo zadejte délku akordu [r=__ocs_arg_r__]: + .arc-scl-specify-center = ARC SCL Zadejte střed: + .arc-scl-specify-start-point = ARC SCL Zadejte počáteční bod: + .arc-sea-specify-angle-move-cursor-perpendicula = ARC SEA Zadejte úhel (přesunout kurzor kolmo k tětivě): + .arc-sea-specify-end-point = ARC SEA Zadejte koncový bod: + .arc-sea-specify-start-point = ARC SEA Zadejte počáteční bod: + .arc-sed-specify-end-point = ARC SED Zadejte koncový bod: + .arc-sed-specify-start-point = ARC SED Zadejte počáteční bod: + .arc-sed-specify-tangent-direction-at-start = ARC SED Určete směr tečny na začátku: + .arc-ser-click-radius-point-or-type-radius-valu = ARC SER Klikněte na bod poloměru nebo zadejte hodnotu poloměru: + .arc-ser-specify-end-point = ARC SER Zadejte koncový bod: + .arc-ser-specify-start-point = ARC SER Zadejte počáteční bod: + .area-specify-first-corner-point-enter-to-cance = AREA Určete první rohový bod (zrušení Enter): + .area-specify-next-point-n-picked-enter-to-calc = AREA Zadejte další bod (vybráno __ocs_arg_n__, výpočet Enter): + .attdef-enter-attribute-tag-no-spaces = Značka atributu ATTDEF Enter (bez mezer): + .attdef-enter-default-value-for-tag-enter-blank = Výchozí hodnota ATTDEF Enter pro „__ocs_arg_tag__“ (Enter=prázdné): + .attdef-enter-prompt-for-tag-enter-use-tag = ATTDEF Enter výzva k zadání „__ocs_arg_tag__“ (značka Enter=): + .attdef-specify-insertion-point-for-tag = ATTDEF Zadejte bod vložení pro „__ocs_arg_tag__“: + .attdisp-attribute-display-on-off-normal = Zobrazení atributu ATTDISP [Zapnuto / Vypnuto / Normální]: + .attdisp-sub-count-attribute-definition-s-updat = ATTDISP __ocs_fmt_0__: Aktualizace definic atributů __ocs_fmt_1__. + .attedit-no-insert-entities-in-selection = ATTEDIT: žádné Vkládání entit do výběru. + .attedit-select-an-insert-entity-first = ATTEDIT: nejprve vyberte entitu Vložit. + .attedit-tag-tag-up-not-found-in-selection = ATTEDIT: tag '__ocs_fmt_0__' nebyl ve výběru nalezen. + .attedit-updated-changed-attribute-s-tag-up-new = ATTEDIT: aktualizované atributy __ocs_fmt_0__ [__ocs_fmt_1__] = __ocs_fmt_2__. + .arc-length-symbol-dimarcsym = Symbol délky oblouku (DIMARCSYM) + .axis = Osa + .axis-end =Osa, konec + .boundary-pick-internal-point-miss = BOUNDARY Vyberte vnitřní bod: __ocs_arg_miss__ + .boundary = Hranice + .centerline-select-first-line = CENTERLINE Vyberte první řádek: + .centerline-select-second-line = CENTERLINE Vyberte druhý řádek: + .circle-specify-center-point = CIRCLE Zadejte středový bod: + .circle-specify-radius-or-type-value-r-center-c = CIRCLE Zadejte poloměr nebo hodnotu typu <__ocs_arg_r__> [střed (__ocs_arg_cx__,__ocs_arg_cy__)]: + .circle-2p-specify-first-end-of-diameter = CIRCLE 2P Určete první konec průměru: + .circle-2p-specify-second-end-of-diameter = CIRCLE 2P Zadejte druhý konec průměru: + .circle-3p-specify-first-point = CIRCLE 3P Určete první bod: + .circle-3p-specify-second-point = CIRCLE 3P Zadejte druhý bod: + .circle-3p-specify-third-point = CIRCLE 3P Zadejte třetí bod: + .circle-cd-specify-center-point = CIRCLE CD Zadejte střed: + .circle-cd-specify-diameter-or-type-value-4-cen = CIRCLE CD Zadejte hodnotu průměru nebo typu <__ocs_fmt_0__> [střed (__ocs_fmt_1__,__ocs_fmt_2__)]: + .circle-ttr-select-first-tangent-object = CIRCLE TTR Vyberte první tečný objekt: + .circle-ttr-select-second-tangent-object = CIRCLE TTR Vyberte druhý tečný objekt: + .circle-ttr-specify-radius = CIRCLE TTR Zadejte poloměr: + .circle-ttt-select-first-tangent-object = CIRCLE TTT Vyberte první tečný objekt: + .circle-ttt-select-second-tangent-object = CIRCLE TTT Vyberte druhý tečný objekt: + .circle-ttt-select-third-tangent-object = CIRCLE TTT Vyberte třetí tečný objekt: + .copy-specify-base-point-count-objects = COPY Zadejte základní bod [objekty __ocs_arg_count__]: + .center-line = Středová čára + .center-mark = Středová značka + .center-axes = Střed, Osy + .center-diameter = Střed, Průměr + .center-radius = Střed, Radius + .center-start-angle = Střed, Start, Úhel + .center-start-end = Střed, začátek, konec + .center-start-length = Střed, začátek, délka + .circumscribed = Vymezené + .current-settings-edit-mode = Aktuální nastavení: Režim úprav = __ocs_fmt_0__ + .ddedit-selected-entity-is-not-text = DDEDIT: vybraná entita není text. + .dimcenter-select-arc-or-circle = DIMCENTER Vyberte oblouk nebo kružnici: + .donut-specify-center-of-donut-enter-to-exit = DONUT Zadejte střed prstence (Enter pro ukončení): + .donut-specify-inside-diameter-0 = DONUT Zadejte vnitřní průměr <0>: + .donut-specify-outside-diameter = DONUT Zadejte vnější průměr: + .draw-manually = Kreslit ručně + .ellipse-specify-center = ELLIPSE Specifikujte střed: + .ellipse-specify-major-axis-endpoint = ELLIPSE Zadejte koncový bod hlavní osy: + .ellipse-specify-minor-axis-point-or-type-half =ELLIPSE Zadejte bod vedlejší osy nebo zadejte poloviční délku [hlavní r=__ocs_fmt_0__]: + .ellipse-axis-specify-first-endpoint-of-major-a = ELLIPSE (osa) Zadejte první koncový bod hlavní osy: + .ellipse-axis-specify-minor-axis-point-or-type = ELLIPSE (osa) Zadejte bod vedlejší osy nebo zadejte poloviční délku [hlavní r=__ocs_fmt_0__]: + .ellipse-axis-specify-second-endpoint-of-major = ELLIPSE (osa) Zadejte druhý koncový bod hlavní osy: + .ellipse-arc-specify-center = ELLIPSE ARC Zadejte střed: + .ellipse-arc-specify-end-angle-point-or-type-de = ELLIPSE ARC Zadejte koncový bod úhlu nebo zadejte stupně [start=__ocs_arg_sa__]: + .ellipse-arc-specify-major-axis-endpoint = ELLIPSE ARC Zadejte koncový bod hlavní osy: + .ellipse-arc-specify-minor-axis-point-or-type-h = ELLIPSE ARC Zadejte bod vedlejší osy nebo zadejte poloviční délku [hlavní r=__ocs_arg_r__]: + .ellipse-arc-specify-start-angle-point-or-type = ELLIPSE ARC Určete počáteční bod úhlu nebo zadejte stupně: + .ellipse-arc = Oblouk elipsy + .ext-line-color-aci-dimclre = Barva vnější čáry ACI (DIMCLRE) + .ext-line-weight-dimlwe = Tloušťka vnější čáry (DIMLWE) + .gradient-boundary-point-1 = GRADIENT Hraniční bod 1: + .gradient-point-n = GRADIENT Bod __ocs_arg_n__: + .gradient-kind-invert-pick-internal-point-miss = GRADIENT (__ocs_arg_kind____ocs_arg_invert__) Vyberte vnitřní bod: __ocs_arg_miss__ + .gradient = Gradient + .hatch-boundary-point-1 = HATCH Hraniční bod 1: + .hatch-pick-internal-point-miss = HATCH Vyberte vnitřní bod: __ocs_arg_miss__ + .hatch-point-n = HATCH Bod __ocs_arg_n__: + .hatchedit-pattern-name-scale-scale-angle-angle = HATCHEDIT Vzor:__ocs_arg_name__ Měřítko:__ocs_arg_scale__ Úhel:__ocs_arg_angle__ [P / S / A | Enter uplatnit]: + .hatchedit-select-hatch = HATCHEDIT Vyberte poklop: + .hatchedit-selected-entity-is-not-a-hatch = HATCHEDIT: vybraná entita není šrafování. + .helix-enter-number-of-turns-3 = HELIX Enter počet otáček <3>: + .helix-specify-base-radius = HELIX Zadejte poloměr základny: + .helix-specify-centre-point-of-base = HELIX Určete střed základny: + .helix-specify-helix-height = HELIX Zadejte výšku šroubovice: + .helix-specify-top-radius-same-as-base = HELIX Určete horní poloměr : + .image-specify-insertion-point-name = IMAGE Zadejte bod vložení (__ocs_arg_name__): + .image-specify-width-drag-right = IMAGE Zadejte šířku (přetáhněte doprava): + .invert-off = Invertovat: vypnuto + .invert-on = Invertovat: zapnuto + .line-specify-first-point = LINE Zadejte první bod: + .line-specify-next-point-close-undo = LINE Zadejte další bod [Zavřít/Zpět]: + .line-specify-next-point-undo =LINE Určete další bod [Undo]: + .line-color-aci = Barva čáry (ACI): + .line-type = Typ čáry: + .line-weight = Tloušťka čáry: + .linear-factor-dimlfac = Lineární faktor (DIMLFAC) + .literal-spaces-space-stays-in-the-line-instead = Doslovné mezery: Space zůstane v řádku namísto spuštění příkazu (stejné jako psaní úvodního '>'). Zůstane zapnuté, dokud se nevypne. + .mline-enter-scale-factor = MLINE Enter měřítko: + .mline-specify-next-point-count-pts-enter-to-fi = MLINE Zadejte další bod (__ocs_arg_count__ bodů, Enter pro dokončení, C pro uzavření, S pro nastavení měřítka): + .mline-specify-start-point-scale-scale = MLINE Zadejte počáteční bod (měřítko=__ocs_arg_scale__): + .pline-specify-start-point = PLINE Zadejte počáteční bod: + .pline-mode-next-pt-count-pts = PLINE [__ocs_arg_mode__] Další bod [__ocs_arg_count__pts]: + .point-specify-point = POINT Určete bod: + .polygon-enter-number-of-sides = POLYGON Enter počet stran <__ocs_fmt_0__>: + .polygon-specify-center-sides = POLYGON Zadejte střed [strany __ocs_fmt_0__]: + .polygon-specify-vertex-on-circle-sides-inscrib = POLYGON Určete vrchol na kružnici [vepsané strany __ocs_fmt_0__]: + .polygon-c-enter-number-of-sides = POLYGON C Enter počet stran <__ocs_fmt_0__>: + .polygon-c-specify-center-sides = POLYGON C Zadejte střed [strany __ocs_fmt_0__]: + .polygon-c-specify-edge-midpoint-radius-sides-c = POLYGON C Určete poloměr okraje a středu [opsané strany __ocs_fmt_0__]: + .polygon-e-enter-number-of-sides-n = POLYGON E Enter počet stran <__ocs_arg_n__>: + .polygon-e-specify-first-endpoint-of-edge-n-sid = POLYGON E Určete první koncový bod hrany [strany __ocs_arg_n__]: + .polygon-e-specify-second-endpoint-of-edge-n-si = POLYGON E Zadejte druhý koncový bod hrany [strany __ocs_arg_n__]: + .polysolid-wall-height = POLYSOLID výška stěny: + .polysolid-wall-width = POLYSOLID šířka stěny: + .pyramid-base-radius = Základní poloměr PYRAMID: + .pyramid-height-add-sides-by-typing-a-3rd-numbe = Výška PYRAMID (přidejte strany zadáním 3. čísla): + .polygon-circumscribed = Mnohoúhelník – ohraničený + .polygon-edge = Mnohoúhelník – hrana + .polygon-inscribed = Mnohoúhelník - vepsaný + .ray-specify-start-point = RAY Zadejte počáteční bod: + .ray-specify-through-point = RAY Určete průchozí bod: + .rect-specify-first-corner = RECT Určete první roh: + .rect-specify-opposite-corner = RECT Určete opačný roh: + .rect-cen-specify-center-point = RECT CEN Zadejte střed: + .rect-cen-specify-corner-point =RECT CEN Zadejte rohový bod: + .rect-rot-specify-adjacent-corner-defines-edge = RECT ROT Určete sousední roh (definuje směr hrany): + .rect-rot-specify-first-corner = RECT ROT Určete první roh: + .rect-rot-specify-height-perpendicular-pick = RECT ROT Zadejte výšku (kolmý hrot): + .region-created-count-region-s = REGION: vytvořené oblasti __ocs_fmt_0__. + .region-select-closed-polylines-or-circles = REGION: vyberte uzavřené křivky nebo kružnice. + .revcloud-specify-next-point-count-pts-enter-to = REVCLOUD Zadejte další bod (__ocs_arg_count__ bodů, Enter pro uzavření): + .revcloud-specify-start-point-arc-length-arc-le = REVCLOUD Zadejte počáteční bod (délka oblouku = __ocs_arg_arc_length__): + .rectangle-center = Obdélník - střed + .rectangle-rotated = Obdélník - otočený + .rectangle-two-corners = Obdélník - dva rohy + .rev-cloud = Rev Cloud + .section-cutting-plane-axis-x-y-z = SECTION osa řezné roviny [X / Y / Z]: + .section-offset-along-x = Odsazení SECTION podél X: + .section-offset-along-y = Odsazení SECTION podél Y: + .section-offset-along-z = Odsazení SECTION podél Z: + .sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Perem dolů – pohybem na skicu, kliknutím zvedněte, Enter zaznamenáte: + .sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Pero nahoru – kliknutím spustíte pero, Enter zaznamenáte: + .slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE osa řezné roviny [X / Y / Z] (přidejte TOP/BOTTOM zadáním): + .slice-offset-along-x = Odsazení SLICE podél X: + .slice-offset-along-y = Odsazení SLICE podél Y: + .slice-offset-along-z = Odsazení SLICE podél Z: + .solid-specify-first-point = SOLID Zadejte první bod: + .solid-specify-fourth-point = SOLID Zadejte čtvrtý bod: + .solid-specify-second-point = SOLID Zadejte druhý bod: + .solid-specify-third-point = SOLID Zadejte třetí bod: + .spline-specify-first-control-point = SPLINE Zadejte první kontrolní bod: + .spline-specify-next-point-n-pts = SPLINE Zadejte další bod [__ocs_arg_n__ bodů]: + .sketch = Skica + .start-center-angle = Start, Střed, Úhel + .start-center-end = Začátek, Střed, Konec + .start-center-length = Začátek, Střed, Délka + .start-end-angle = Začátek, Konec, Úhel + .start-end-direction = Začátek, konec, směr + .start-end-radius = Start, End, Radius + .suppress-1st-line-dimsd1 = Potlačit 1. řádek (DIMSD1) + .suppress-1st-line-dimse1 = Potlačit 1. řádek (DIMSE1) + .suppress-2nd-line-dimsd2 =Potlačit 2. řádek (DIMSD2) + .suppress-2nd-line-dimse2 = Potlačit 2. řádek (DIMSE2) + .trace-specify-next-point-undo = TRACE Určete další bod [Undo]: + .trace-specify-start-point = TRACE Zadejte počáteční bod: + .trace-specify-trace-width-w = TRACE Zadejte šířku stopy <__ocs_arg_w__>: + .tan-tan-radius = Tan, Tan, Radius + .tan-tan-tan = Tan, Tan, Tan + .trace = Stopa + .ttt = Ttt + .usage-attdisp-on-off-normal = Použití: ATTDISP ON | OFF | NORMAL + .usage-polysolid-width-height-select-a-polyline = Použití: POLYSOLID (nejprve vyberte křivku) + .usage-pyramid-radius-height-sides-default-4-si = Použití: PYRAMID [sides] (výchozí 4 strany) + .wipeout-specify-first-point-or-polyline = WIPEOUT Zadejte první bod nebo [Polyline]: + .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Zadejte další bod nebo [Undo/Close] (body __ocs_arg_n__): + .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polyline Vyberte uzavřenou rovinnou křivku: + .wipeout-rectangular-specify-first-corner = WIPEOUT Obdélníkový Zadejte první roh: + .wipeout-rectangular-specify-opposite-corner = WIPEOUT Obdélníkový Zadejte protilehlý roh: + .x-ray = X-Ray + .xline-specify-a-point = XLINE Určete bod: + .xline-specify-through-point = XLINE Určete průchozí bod: + .n-object-s-erased = Objekty __ocs_fmt_0__ byly vymazány. + +modify = + .count-objects = (Objekty __ocs_arg_count__) + .edge-extend-indented-title-case = [Okraj: Prodloužit] + .align-scale-objects-based-on-alignment-points = ALIGN Měřítko objektů na základě bodů zarovnání? [Y/N]: + .align-select-objects-count-selected-enter-when = ALIGN Vyberte objekty (vybráno __ocs_arg_count__, po dokončení Enter): + .align-specify-1st-destination-point = ALIGN Zadejte 1. cílový bod: + .align-specify-1st-source-point = ALIGN Zadejte 1. zdrojový bod: + .align-specify-2nd-destination-point = ALIGN Zadejte 2. cílový bod: + .align-specify-2nd-source-point-enter-translate = ALIGN Zadejte 2. zdrojový bod (pouze překlad Enter =): + .array3d-column-spacing-rows-cols-levels-row-rs = ARRAY3D Rozteč sloupců [__ocs_arg_rows__×__ocs_arg_cols__×__ocs_arg_levels__, řádek=__ocs_arg_rs__]: + .array3d-enter-column-count-rows-rows = Počet sloupců ARRAY3D Enter [řádky __ocs_arg_rows__]: + .array3d-enter-level-count-rows-cols = ARRAY3D Enter počet úrovní [__ocs_arg_rows__×__ocs_arg_cols__]: + .array3d-enter-row-count = Počet řádků ARRAY3D Enter: + .array3d-level-spacing-rows-cols-levels-r-rs-c = ARRAY3D Rozteč úrovní [__ocs_arg_rows__×__ocs_arg_cols__×__ocs_arg_levels__, r=__ocs_arg_rs__ c=__ocs_arg_cs__]: + .array3d-row-spacing-rows-cols-levels =ARRAY3D Rozteč řádků [__ocs_arg_rows__×__ocs_arg_cols__×__ocs_arg_levels__]: + .arraypath-enter-item-count = ARRAYPATH Enter počet položek <__ocs_fmt_0__>: + .arraypath-select-path-entity-objects = ARRAYPATH Vyberte entitu cesty [objekty __ocs_fmt_0__]: + .arraypolar-enter-item-count = ARRAYPOLAR Enter počet položek <__ocs_fmt_0__>: + .arraypolar-enter-total-angle-in-degrees-0-coun = ARRAYPOLAR Enter celkový úhel ve stupních <__ocs_fmt_0__> [položky __ocs_fmt_1__]: + .arraypolar-specify-center-point-objects = ARRAYPOLAR Zadejte středový bod [objekty __ocs_fmt_0__]: + .arrayrect-column-spacing-0-rows-cols-row-row-s = ARRAYRECT Rozteč sloupců <__ocs_fmt_0__> [__ocs_fmt_1__×__ocs_fmt_2__, řádek=__ocs_fmt_3__]: + .arrayrect-enter-column-count-rows-rows = ARRAYRECT Enter počet sloupců <__ocs_fmt_0__> [__ocs_fmt_1__ řádků]: + .arrayrect-enter-row-count = ARRAYRECT Enter počet řádků <__ocs_fmt_0__>: + .arrayrect-row-spacing-0-rows-cols = ARRAYRECT Rozteč řádků <__ocs_fmt_0__> [__ocs_fmt_1__×__ocs_fmt_2__]: + .attedit-select-block-with-attributes = ATTEDIT Vyberte blok s atributy: + .annotation-object-scale = Měřítko objektu anotace + .bedit-select-block-reference-to-edit = BEDIT Vyberte referenci bloku, kterou chcete upravit: + .blend-enter-continuity-tangent-curvature-c = BLEND Enter spojitost [Tečna/Zakřivení] <__ocs_arg_c__>: + .blend-select-first-open-curve-continuity-c = BLEND Vyberte první otevřenou křivku [Continuity=__ocs_arg_c__]: + .blend-select-second-open-curve-continuity-c = BLEND Vyberte druhou otevřenou křivku [Continuity=__ocs_arg_c__]: + .break-select-object = BREAK Vyberte objekt: + .break-specify-first-break-point = BREAK Zadejte první bod přerušení: + .break-specify-second-break-point = BREAK Zadejte druhý bod přerušení: + .breakatpoint-select-object = BREAKATPOINT Vyberte objekt: + .breakatpoint-specify-break-point = BREAKATPOINT Zadejte bod přerušení: + .block-scale-x = Blokové měřítko X: + .block-scale-y = Blokové měřítko Y: + .block-scale-z = Blokové měřítko Z: + .boundary-edges = Hranice hranic + .break-gap-size = Velikost lomové mezery: + .chamfer-enter-first-chamfer-distance-d1 = CHAMFER Enter vzdálenost prvního zkosení <__ocs_arg_d1__>: + .chamfer-enter-second-chamfer-distance-d2 = CHAMFER Enter druhá vzdálenost zkosení <__ocs_arg_d2__>: + .chamfer-select-first-line-d1-d1-d2-d2 = CHAMFER Vyberte první řádek [D1=__ocs_arg_d1__ D2=__ocs_arg_d2__]: + .chamfer-select-second-line-d1-d1-d2-d2 = CHAMFER Vyberte druhý řádek [D1=__ocs_arg_d1__ D2=__ocs_arg_d2__]: + .chamfer-select-the-adjacent-polyline-segment-d = CHAMFER Vyberte sousední segment křivky [D1=__ocs_arg_d1__ D2=__ocs_arg_d2__]: + .copy-enter-number-of-items-to-array =COPY Enter počet položek do pole: + .copy-specify-second-point-for-n-item-array-bas = COPY Zadejte druhý bod pro pole položek __ocs_arg_n__ [základ __ocs_arg_bx__,__ocs_arg_by__]: + .continuity = Kontinuita + .curvature = Zakřivení + .cutting-edges = Řezné hrany + .decurve = Decurve + .discard-block-edit = Zahodit úpravy bloku + .explode-select-objects-to-explode = EXPLODE Vyberte objekty, které chcete rozložit: + .extend-select-boundary-edges-picked-enter-done = EXTEND Vyberte hraniční hrany [__ocs_fmt_0__ vybráno, Enter = hotovo]: + .extend-enter-done = EXTEND [Enter = hotovo]: + .extend-edge-click-near-end-of-object-to-extend = EXTEND__ocs_fmt_0__ Kliknutím na blízko konce objektu jej prodloužíte (Shift+kliknutí ořízne): + .extend-edge-crossing-first-corner = EXTEND__ocs_fmt_0__ Křížení: první roh: + .extend-edge-crossing-opposite-corner = EXTEND__ocs_fmt_0__ Křížení: protilehlý roh: + .extend-edge-fence-pick-points-placed-enter-ext = EXTEND__ocs_fmt_0__ Plot: body výběru [__ocs_fmt_1__ umístěno, Enter = prodloužit zkřížené]: + .extrim-click-the-side-to-trim-away = EXTRIM Klikněte na stranu, kterou chcete oříznout: + .extrim-select-cutting-boundary = EXTRIM Vyberte hranici řezu: + .edge-extend-title-case = Okraj: Prodloužit + .erase = Vymazat + .explode = explodovat + .extend = Prodloužit + .fillet-enter-fillet-radius-4 = FILLET Enter poloměr zaoblení <__ocs_fmt_0__>: + .fillet-select-first-object-line-arc-lwpolyline = FILLET Vyberte první objekt (Line/Oblouk/LwPolyline) [R=__ocs_fmt_0__]: + .fillet-select-second-object-line-arc-lwpolylin = FILLET Vyberte druhý objekt (Line/Oblouk/LwPolyline) [R=__ocs_fmt_0__]: + .join-select-objects-to-join-count-selected-ent = JOIN Vyberte objekty ke spojení (vybráno __ocs_arg_count__, použití Enter): + .lengthen-enter-option-de-delta-to-total-p-pct = Možnost LENGTHEN Enter [DE / TO / P ]: + .lengthen-select-object = LENGTHEN Vyberte objekt: + .mirror-erase-source-objects-yes-no-no = MIRROR Vymazat zdrojové objekty? [Ano/Ne] : + .mirror-specify-first-mirror-line-point-count-o = MIRROR Určete první bod zrcadlové čáry [objekty __ocs_arg_count__]: + .mirror-specify-second-point-p1-px-py = MIRROR Zadejte druhý bod [p1=__ocs_arg_px__,__ocs_arg_py__]: + .move-specify-base-point-count-objects = MOVE Zadejte základní bod [objekty __ocs_arg_count__]: + .move-specify-destination-base-bx-by = MOVE Zadejte cíl [základ __ocs_arg_bx__,__ocs_arg_by__]: + .mirror = Zrcadlo + .move-freely = Pohybujte se volně + .move-text-arrows-out = Přesuňte text a šipky ven + .offset-select-object-to-offset-enter-to-finish =OFFSET Vyberte objekt k odsazení (dokončete Enter): + .offset-specify-offset-distance-or-through-d = OFFSET Zadejte vzdálenost odsazení nebo [Through] <__ocs_arg_d__>: + .offset-n-click-side-or-multiple-distance-d = OFFSET__ocs_arg_n__ Strana kliknutí nebo [Multiple] [vzdálenost __ocs_arg_d__]: + .offset-n-click-through-point-or-multiple = OFFSET__ocs_arg_n__ Bod prokliku nebo [Multiple]: + .offset-n-multiple-click-next-side-distance-d = OFFSET__ocs_arg_n__ Vícenásobné kliknutí na další stranu [vzdálenost __ocs_arg_d__]: + .offset-n-multiple-click-next-through-point = OFFSET__ocs_arg_n__ Vícenásobné kliknutí dalším průchozím bodem: + .offset-dimexo = Offset (DIMEXO) + .overall-scale-dimscale = Celková stupnice (DIMSCALE) + .pedit-enter-option = Možnost PEDIT Enter: + .pedit-object-is-not-a-polyline-turn-it-into-on = PEDIT Objekt není křivka. Proměnit to v jedno? [Ano/Ne] : + .pedit-select-polyline-or-a-line-arc-to-convert = PEDIT Vyberte křivku (nebo čáru/oblouk pro převod): + .pedit-specify-new-width = PEDIT Zadejte novou šířku: + .pedit-join-select-objects-to-join-count-picked = PEDIT Spojit Vyberte objekty ke spojení (vybráno __ocs_arg_count__), Enter ke sloučení: + .path-array = Pole cesty + .polar-array = Polární pole + .refclose-save-discard-save = REFCLOSE [Uložit/Zahodit] : + .refedit-select-block-reference-to-edit = REFEDIT Vyberte referenci bloku, kterou chcete upravit: + .reverse-select-line-polyline-or-spline-to-reve = REVERSE Vyberte čáru, křivku nebo spline, které chcete obrátit: + .rotate-specify-first-reference-point-or-type-r = ROTATE Zadejte první referenční bod nebo zadejte referenční úhel: + .rotate-specify-new-absolute-angle-ref-a = ROTATE Zadejte nový absolutní úhel [ref=__ocs_arg_a__]: + .rotate-specify-rotation-angle-a = ROTATE Zadejte úhel natočení <__ocs_arg_a__>: + .rotate-specify-rotation-center-count-objects = ROTATE Zadejte střed otáčení [objekty __ocs_arg_count__]: + .rotate-specify-second-reference-point = ROTATE Zadejte druhý referenční bod: + .rectangular-array = Obdélníkové pole + .scale-specify-base-point-count-objects = SCALE Zadejte základní bod [objekty __ocs_arg_count__]: + .scale-specify-first-reference-point-or-type-re = SCALE Zadejte první referenční bod nebo zadejte referenční délku: + .scale-specify-new-length-from-base-or-type-a-l = SCALE Zadejte novou délku od základny nebo zadejte délku [ref=__ocs_arg_d__]: + .scale-specify-scale-factor-f = SCALE Zadejte měřítko <__ocs_arg_f__>: + .scale-specify-second-reference-point = SCALE Zadejte druhý referenční bod: + .splinedit-select-spline = SPLINEDIT Vyberte spline: + .splinedit-close-open-reverse-exit = SPLINEDIT [CLOSE/OPEN/REVERSE/EXIT]: + .stretch-specify-base-point = STRETCH Zadejte základní bod: + .stretch-specify-first-corner-of-crossing-window-count-objects =STRETCH Určete první roh okna křížení [objekty __ocs_arg_count__]: + .stretch-specify-first-corner-of-crossing-window = STRETCH Určete první roh okna křížení: + .stretch-specify-new-point-base-bx-bz = STRETCH Zadejte nový bod [základ __ocs_arg_bx__,__ocs_arg_bz__]: + .stretch-specify-opposite-corner = STRETCH Určete opačný roh: + .save-block = Uložit blok + .save-block-edit = Uložit Úpravu bloku + .scale-manager = Správce váhy + .scale-factor = Měřítko: + .scale-units = Měřítko/jednotky + .spline-edit = Spline Edit + .torient-new-absolute-rotation-most-readable = TORIENT Nová absolutní rotace : + .torient-specify-second-point = TORIENT Zadejte druhý bod: + .trim-erase-click-objects-to-delete-enter-done = TRIM Vymazání: kliknutím na objekty smažete [Enter = hotovo]: + .trim-select-cutting-edges-picked-enter-done = TRIM Vyberte řezné hrany [__ocs_fmt_0__ vybráno, Enter = hotovo]: + .trim-edge-click-segment-to-remove-shift-click = TRIM__ocs_fmt_0__ Kliknutím na segment odeberete (Shift+kliknutím se rozšíří): + .trim-edge-crossing-first-corner = TRIM__ocs_fmt_0__ Křížení: první roh: + .trim-edge-crossing-opposite-corner = TRIM__ocs_fmt_0__ Křížení: protilehlý roh: + .trim-edge-fence-pick-points-placed-enter-trim = TRIM__ocs_fmt_0__ Plot: body výběru [__ocs_fmt_1__ umístěno, Enter = lem překřížený]: + .through = Skrz + .tol-scale-dimtfac = Tol. měřítko (DIMTFAC) + .trim = Oříznout + .exit = výstup + +groups = + .group-enter-group-name-name = GROUP Enter název skupiny [__ocs_arg_name__]: + .ungroup-select-grouped-objects = UNGROUP Vyberte seskupené objekty: + .ungroup = Zrušit seskupení + +layers = + .n-layers = Vrstvy __ocs_arg_n__ + .n-saved-layers = __ocs_arg_n__ uložené vrstvy + .active-ucs-named = Aktivní UCS: __ocs_fmt_0__ | Pojmenováno: __ocs_fmt_1__ + .active-ucs-no-named-ucss-defined = Aktivní UCS: __ocs_fmt_0__ | Nejsou definovány žádné pojmenované UCS. + .all-layers-thawed = Všechny vrstvy rozmrzly. + .all-layers-turned-on = Všechny vrstvy zapnuty. + .changes-affect-the-saved-state-only-the-drawin = Změny ovlivní pouze uložený stav; výkres se nezmění až do obnovení. + .choose-new-to-capture-the-current-layer-settin = Zvolte Nový, chcete-li zachytit aktuální nastavení vrstvy. + .current-mleader-style = Aktuální styl MLeader: __ocs_fmt_0__ + .current-layer = Aktuální vrstva + .current-layer-set-to-layer = Aktuální vrstva nastavena na "__ocs_fmt_0__". + .current-ucs = Aktuální UC + .dimstyle-style-name-prop-val-3 = DIMSTYLE: '__ocs_fmt_0__'. __ocs_fmt_1__ = __ocs_fmt_2__ + .dimstyle-already-exists = DIMSTYLE: '__ocs_fmt_0__' již existuje. + .dimstyle-created =DIMSTYLE: Vytvořeno „__ocs_fmt_0__“. + .dimstyle-not-found = DIMSTYLE: „__ocs_fmt_0__“ nebyl nalezen. + .dimstyle-unknown-property-try-dimtxt-dimasz-di = DIMSTYLE: neznámá vlastnost '__ocs_fmt_0__'. Zkuste: dimtxt dimasz dimdli dimexo dimexe dimgap dimscale dimlfac dimdle dimcen dimtsz + .delete-layer = Smazat vrstvu + .dimstyles = DimStyles: __ocs_fmt_0__ + .edit = Upravit + .edit-layer-state = Upravit stav vrstvy + .freeze = Zmrazit + .hideobjects-select-the-objects-to-hide-first = HIDEOBJECTS: nejprve vyberte objekty, které chcete skrýt. + .hid-n-object-s-unisolateobjects-to-restore = Skryté objekty __ocs_fmt_0__. UNISOLATEOBJECTS obnovit. + .isolateobjects-select-the-objects-to-isolate-f = ISOLATEOBJECTS: vyberte objekty, které chcete izolovat jako první. + .isolate-layer = Izolovat vrstvu + .isolated-n-object-s-unisolateobjects-to-restor = Izolované objekty __ocs_fmt_0__. UNISOLATEOBJECTS obnovit. + .isolation-ended-all-objects-shown = Izolace ukončena — všechny zobrazené objekty. + .laydel-layer-to-delete = Vrstva LAYDEL ke smazání: + .laydel-deleted-layer-layer-and-n-object-s = LAYDEL: odstraněna vrstva "__ocs_fmt_0__" a objekt(y) __ocs_fmt_1__. + .laydel-layer-0-cannot-be-deleted = LAYDEL: vrstvu "0" nelze smazat. + .laydel-no-layer-named-name = LAYDEL: žádná vrstva s názvem „__ocs_fmt_0__“. + .layer-already-exists = LAYER: '__ocs_fmt_0__' již existuje. + .layer-color-set-to-aci = LAYER: Barva „__ocs_fmt_0__“ nastavena na ACI __ocs_fmt_1__. + .layer-created = LAYER: Vytvořeno „__ocs_fmt_0__“. + .layer-not-found = LAYER: „__ocs_fmt_0__“ nebyl nalezen. + .layer-current-layer-set-to = LAYER: aktuální vrstva nastavena na „__ocs_fmt_0__“. + .layer-layers-frozen = LAYER: vrstvy zmrazeny. + .layer-layers-locked = LAYER: vrstvy uzamčeny. + .layer-layers-thawed = LAYER: vrstvy rozmrzlé. + .layer-layers-turned-off = LAYER: vrstvy jsou vypnuty. + .layer-layers-turned-on = LAYER: vrstvy zapnuty. + .layer-layers-unlocked = LAYER: vrstvy odemčeny. + .layerstate-deleted-arg = LAYERSTATE: odstraněno "__ocs_fmt_0__". + .layerstate-no-saved-state-named-arg = LAYERSTATE: žádný uložený stav s názvem „__ocs_fmt_0__“. + .layerstate-restored-arg-n-layer-s = LAYERSTATE: obnoveno "__ocs_fmt_0__" (vrstvy __ocs_fmt_1__). + .layerstate-saved-arg = LAYERSTATE: uloženo "__ocs_fmt_0__". + .layiso-isolated-layer-s = LAYISO: izolovaná vrstva(y) __ocs_fmt_0__. + .layiso-select-entities-on-the-layers-to-isolat = LAYISO: Vyberte entity ve vrstvách, které chcete izolovat jako první. + .laymatch-select-objects-to-change-layer = LAYMATCH Vyberte objekty pro změnu vrstvy: + .laymatch-select-source-object-to-match-layer-f = LAYMATCH Vyberte zdrojový objekt, který má odpovídat vrstvě: + .laymrg-source-layer-merged-away = Zdrojová vrstva LAYMRG (sloučena): + .laymrg-target-layer-kept =Cílová vrstva LAYMRG (zachována): + .laymrg-layer-0-cannot-be-merged-away = LAYMRG: vrstvu "0" nelze sloučit. + .laymrg-merged-src-into-dst-moved-object-s = LAYMRG: sloučeno „__ocs_fmt_0__“ do „__ocs_fmt_1__“ (objekty __ocs_fmt_2__). + .laymrg-source-and-target-are-the-same-layer = LAYMRG: zdroj a cíl jsou stejné vrstvy. + .laymrg-source-and-target-layers-must-both-exis = LAYMRG: musí existovat zdrojová i cílová vrstva. + .layuniso-all-layers-restored = LAYUNISO: všechny vrstvy obnoveny. + .layer-freeze = Layer Freeze + .layer-lock = Zámek vrstvy + .layer-manager = Správce vrstev + .layer-off = Vrstva vypnuta + .layer-state-manager = Správce stavu vrstvy + .layer-unlock = Odemknutí vrstvy + .layer-state-name = Název stavu vrstvy + .layer-states-are-stored-inside-the-drawing-and = Stavy vrstev jsou uloženy uvnitř výkresu a zůstávají dostupné i po jeho opětovném otevření. + .layer-s-frozen = Vrstva(y) zamrzlá. + .layer-s-locked = Vrstva(y) uzamčena. + .layer-s-turned-off = Vrstva(y) vypnuta. + .layer-s-unlocked = Vrstva(y) odemčena. + .layers = Vrstvy: __ocs_fmt_0__ + .lock = Zámek + .mleaderstyle-style-name-prop-val-3 = MLEADERSTYLE: '__ocs_fmt_0__'. __ocs_fmt_1__ = __ocs_fmt_2__ + .mleaderstyle-already-exists = MLEADERSTYLE: '__ocs_fmt_0__' již existuje. + .mleaderstyle-created = MLEADERSTYLE: Vytvořeno „__ocs_fmt_0__“. + .mleaderstyle-not-found = MLEADERSTYLE: „__ocs_fmt_0__“ nebyl nalezen. + .mleaderstyle-current-style-set-to-name = MLEADERSTYLE: aktuální styl nastaven na „__ocs_fmt_0__“. + .mleaderstyle-unknown-property-try-text-height = MLEADERSTYLE: neznámá vlastnost '__ocs_fmt_0__'. Zkuste: text_height arrowhead_size landing_distance landing_gap + .mlstyle-already-exists = MLSTYLE: '__ocs_fmt_0__' již existuje. + .mlstyle-created = MLSTYLE: Vytvořeno „__ocs_fmt_0__“. + .mlstyle-deleted = MLSTYLE: '__ocs_fmt_0__' smazáno. + .mlstyle-not-found = MLSTYLE: „__ocs_fmt_0__“ nebyl nalezen. + .mlstyle-current-style-set-to = MLSTYLE: aktuální styl nastaven na „__ocs_fmt_0__“. + .mleader-styles-none-active-current = M Styly vůdce: (žádné) aktivní: __ocs_fmt_0__ + .mleader-styles-active-current = M Styly vůdce: __ocs_fmt_0__ aktivní: __ocs_fmt_1__ + .mlinestyles = + MLineStyles: + __ocs_fmt_0__ + .make-current = Nastavit jako aktuální + .match-layer = Match Layer + .named-views = Pojmenovaná zobrazení: __ocs_fmt_0__ + .new-vp = Nový VP + .new-vp-freeze = Nový VP Freeze + .new-layer-state = Nový stav vrstvy + .no-dim-styles-defined = Nejsou definovány žádné tmavé styly. + .no-hidden-objects = Žádné skryté předměty. + .no-layer-states-in-this-drawing =V tomto výkresu nejsou žádné stavy hladiny + .no-matching-layer-states = Žádné odpovídající stavy vrstev + .no-multiline-styles = Žádné víceřádkové styly. + .no-named-views-saved = Nebyla uložena žádná pojmenovaná zobrazení. + .no-table-styles = Žádné styly tabulek. + .no-text-styles-defined = Nejsou definovány žádné styly textu. + .not-set = Není nastaveno + .on-off-spaced-separator-title-case = Zapnuto / Vypnuto + .on-off-title-case = Zapnuto/Vypnuto + .optional-description = Volitelný popis + .plan-current-ucs-ucs-world-current = PLAN [Aktuální UC / Ucs / Svět] : + .plan-ucs-ucs-name = PLAN UCS Název ucs: + .plan-current-ucs-plan-view = PLAN: aktuální půdorys UCS. + .plan-plan-view-of-ucs = PLAN: půdorys UCS '__ocs_fmt_0__'. + .plan-world-plan-view = PLAN: půdorys světa. + .properties-restored-by-this-state = Vlastnosti obnovené tímto stavem + .restored-properties = Obnovené vlastnosti + .save-changes = Uložit změny + .save-new-state = Uložit nový stát + .save-captures-the-current-settings-of-every-la = Uložit zachycuje aktuální nastavení každé vrstvy ve výkresu. + .saved-layer-states = Uložené stavy hladiny: __ocs_fmt_0__ + .saved-layer-values = Uložené hodnoty vrstev + .saved-state-details = Podrobnosti uloženého stavu + .search-layer-states = Hledat stavy vrstvy… + .search = Hledat… + .tablestyle-already-exists = TABLESTYLE: '__ocs_fmt_0__' již existuje. + .tablestyle-created = TABLESTYLE: Vytvořeno „__ocs_fmt_0__“. + .tablestyles = + Styly tabulek: + __ocs_fmt_0__ + .text-styles = Styly textu: __ocs_fmt_0__ + .thaw-all-layers = Rozmrazte všechny vrstvy + .turn-all-layers-on = Zapněte všechny vrstvy + .ucs-name-not-found = UCS „__ocs_fmt_0__“ nebyl nalezen. + .ucs-activated = UCS „__ocs_fmt_0__“ aktivováno. + .ucs-deleted = UCS „__ocs_fmt_0__“ smazáno. + .ucs-not-found = UCS „__ocs_fmt_0__“ nebyl nalezen. + .ucs-not-found-usage-ucs-list-save-name-delete = UCS '__ocs_fmt_0__'nenalezeno. Používání: UCS LIST | SAVE | DELETE | W | ORIGIN x, y, z | X/Y/Z + .ucs-saved = UCS „__ocs_fmt_0__“ uloženo. + .ucs-origin-set-to-4-4-4 = Počátek UCS nastaven na (__ocs_fmt_0__, __ocs_fmt_1__, __ocs_fmt_2__). + .ucs-reset-to-world-coordinate-system = UCS se resetuje na světový souřadnicový systém. + .ucs-rotated-2-around-x = UCS se otočil o __ocs_fmt_0__° kolem X. + .ucs-rotated-2-around-y = UCS se otočil o __ocs_fmt_0__° kolem Y. + .ucs-rotated-2-around-z =UCS se otočil o __ocs_fmt_0__° kolem Z. + .ucs = Ucs + .unisolate-layers = Unisolate Vrstvy + .update-from-drawing = Aktualizace z výkresu + .usage-dimstyle-new-name = Použití: DIMSTYLE NEW + .usage-dimstyle-set-name-property-value = Použití: DIMSTYLE SET + .usage-laydel-layer-name = Použití: LAYDEL + .usage-layer-color-name-aci-index = Použití: LAYER COLOR + .usage-layer-list-new-name-on-off-freeze-thaw-l = Používání: LAYER LIST | NEW | ON/OFF/FREEZE/THAW/LOCK/UNLOCK | COLOR | SET + .usage-layer-new-name = Použití: LAYER NEW + .usage-layerstate-delete-name = Použití: LAYERSTATE DELETE + .usage-layerstate-restore-name = Použití: LAYERSTATE RESTORE + .usage-layerstate-save-name = Použití: LAYERSTATE SAVE + .usage-layerstate-save-restore-delete-name-list = Použití: LAYERSTATE SAVE|RESTORE|DELETE | ? (seznam) + .usage-laymrg-source-layer-target-layer = Použití: LAYMRG + .usage-mleaderstyle-list-new-name-set-name-prop = Používání: MLEADERSTYLE LIST | NEW | SET | CURRENT [] + .usage-mleaderstyle-new-name = Použití: MLEADERSTYLE NEW + .usage-mleaderstyle-set-name-property-value = Použití: MLEADERSTYLE SET + .usage-mlstyle-new-name = Použití: MLSTYLE NEW + .usage-mlstyle-set-name = Použití: MLSTYLE SET + .usage-mlstyle-list-new-name-set-name-del-name = Použití: MLSTYLE [LIST|NEW |SET |DEL ] + .usage-plan-ucs-name = Použití: PLAN UCS + .usage-plan-current-ucs-name-world = Použití: PLAN [CURRENT / UCS / WORLD] + .usage-tablestyle-new-name = Použití: TABLESTYLE NEW + .usage-tablestyle-list-new-name = Použití: TABLESTYLE [LIST|NEW ] + .usage-ucs-delete-name = Použití: UCS DELETE + .usage-ucs-origin-x-y-z = Použití: UCS ORIGIN x,y,z + .usage-ucs-save-name = Použití: UCS SAVE + .usage-ucs-x-angle-degrees = Použití: UCS X + .usage-ucs-y-angle-degrees = Použití: UCS Y + .usage-ucs-z-angle-degrees = Použití: UCS Z + .usage-view-delete-name =Použití: VIEW DELETE + .usage-view-list-view-save-name-view-restore-na = Používání: VIEW LIST | VIEW SAVE | VIEW RESTORE | VIEW DELETE + .usage-view-restore-name = Použití: VIEW RESTORE + .usage-view-save-name = Použití: VIEW SAVE + .usage-prefix-font-style-font-file = Použití: __ocs_fmt_0__ FONT