2026-03-22 14:56:27 +03:00
|
|
|
/// Tag for pre-baked snap candidates stored inside a WireModel.
|
|
|
|
|
/// Kept separate from `snap::SnapType` to avoid circular module dependencies.
|
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
|
pub enum SnapHint {
|
|
|
|
|
/// Geometric center of a circle, arc, or ellipse.
|
|
|
|
|
Center,
|
|
|
|
|
/// Point entity location.
|
|
|
|
|
Node,
|
|
|
|
|
/// 0 / 90 / 180 / 270 ° point on a circle/arc (within arc span).
|
|
|
|
|
Quadrant,
|
|
|
|
|
/// Insertion point of text or block.
|
|
|
|
|
Insertion,
|
2026-05-29 01:39:02 +03:00
|
|
|
/// Midpoint of a curve that has one well-defined midpoint (an arc's
|
|
|
|
|
/// arc-length centre, a spline's `t = 0.5`). Lines / polylines do
|
|
|
|
|
/// not use this — their midpoints are derived from `key_vertices`.
|
|
|
|
|
Midpoint,
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Geometric primitive used by the tangent-snap engine.
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub enum TangentGeom {
|
|
|
|
|
/// Infinite line through these two world-space points.
|
|
|
|
|
Line { p1: [f32; 3], p2: [f32; 3] },
|
|
|
|
|
/// Circle/arc.
|
|
|
|
|
Circle { center: [f32; 3], radius: f32 },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A 1-D entity (line, arc, polyline) represented as an ordered set of
|
|
|
|
|
/// world-space points rendered as a quad strip (TriangleList).
|
|
|
|
|
///
|
|
|
|
|
/// Linetype is encoded as a GPU-side dash pattern so the CPU never needs to
|
|
|
|
|
/// split wires into per-dash segments. `pattern_length = 0.0` means solid.
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct WireModel {
|
|
|
|
|
/// Unique identifier — the handle value as a decimal string.
|
|
|
|
|
pub name: String,
|
2026-06-23 17:08:54 +03:00
|
|
|
/// Ordered world-space positions forming a strip of quads. Each entry is
|
|
|
|
|
/// the "high" half of a double-single f32 pair; [`points_low`] carries the
|
|
|
|
|
/// matching residual so the shader can reconstruct the f64 source.
|
2026-03-22 14:56:27 +03:00
|
|
|
pub points: Vec<[f32; 3]>,
|
2026-06-23 17:08:54 +03:00
|
|
|
/// Low-bit residual paired index-for-index with [`points`]. Empty means
|
|
|
|
|
/// "all-zero residual" (interactive draw / preview wires whose coordinates
|
|
|
|
|
/// don't need sub-f32 precision). Tessellation from CAD f64 fills it.
|
|
|
|
|
pub points_low: Vec<[f32; 3]>,
|
2026-03-22 14:56:27 +03:00
|
|
|
/// RGBA colour in [0, 1].
|
|
|
|
|
pub color: [f32; 4],
|
|
|
|
|
/// Whether this wire is currently selected.
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
pub selected: bool,
|
|
|
|
|
/// Total length of one pattern repeat (world units). 0 = solid line.
|
|
|
|
|
pub pattern_length: f32,
|
|
|
|
|
/// Up to 8 pattern elements: positive = dash length, negative = gap length.
|
|
|
|
|
/// Unused slots must be 0.0 (acts as end-of-pattern sentinel in shader).
|
|
|
|
|
pub pattern: [f32; 8],
|
|
|
|
|
/// Rendered line width in screen pixels (half-width = line_weight_px / 2).
|
|
|
|
|
pub line_weight_px: f32,
|
2026-04-06 14:37:23 +03:00
|
|
|
/// ACI color index (1-255). 0 means true-color or unknown (no CTB lookup).
|
|
|
|
|
pub aci: u8,
|
2026-03-22 14:56:27 +03:00
|
|
|
/// Pre-baked snap candidates (Center, Node, Quadrant, Insertion).
|
2026-06-23 18:52:42 +03:00
|
|
|
pub snap_pts: Vec<(glam::DVec3, SnapHint)>,
|
2026-03-22 14:56:27 +03:00
|
|
|
/// Per-segment tangent geometry for Tangent snap.
|
|
|
|
|
/// Line/Arc entities: 1 entry. LwPolyline: 1 entry per segment.
|
|
|
|
|
pub tangent_geoms: Vec<TangentGeom>,
|
|
|
|
|
/// True polyline vertices used for Endpoint/Midpoint snap.
|
|
|
|
|
/// Non-empty only for entities with distinct vertex positions (Line, LwPolyline).
|
|
|
|
|
/// Empty for tessellated curves (Circle, Arc, Ellipse) which use snap_pts instead.
|
2026-06-23 22:23:56 +03:00
|
|
|
pub key_vertices: Vec<[f64; 3]>,
|
2026-04-26 12:21:51 +03:00
|
|
|
/// World-space 2-D bounding box [min_x, min_y, max_x, max_y].
|
|
|
|
|
/// Set from acadrust `bounding_box()` in `tessellate_entity()`.
|
|
|
|
|
/// Preview / interim wires use `UNBOUNDED_AABB` so they are never pre-rejected
|
|
|
|
|
/// by the snap world-space filter.
|
|
|
|
|
pub aabb: [f32; 4],
|
2026-05-06 06:55:28 +03:00
|
|
|
/// When false the linetype pattern restarts at each NaN-separated segment
|
|
|
|
|
/// (DXF PLINEGEN=0). When true the pattern runs continuously (PLINEGEN=1).
|
|
|
|
|
pub plinegen: bool,
|
2026-07-17 01:49:18 +03:00
|
|
|
/// DGN line-style marker. When false (every standard linetype) the dash
|
|
|
|
|
/// pattern uses the normal phase: A-type end alignment for dash-first
|
|
|
|
|
/// patterns, else centred. When true (DGN pipe walls) the pattern is drawn
|
|
|
|
|
/// from the START vertex with continuous phase and no A-type end forcing —
|
|
|
|
|
/// DGN line styles are not end-aligned.
|
|
|
|
|
pub dash_from_start: bool,
|
2026-07-17 02:59:11 +03:00
|
|
|
/// Shared "A"-type end-dash length for MLINE elements. `Some(len)` makes the
|
|
|
|
|
/// dash shader use `len` as the begin/end solid-dash length for EVERY
|
|
|
|
|
/// parallel element (derived once from the multiline's centre-line length),
|
|
|
|
|
/// while `align_total` stays each element's own length — so all elements
|
|
|
|
|
/// share one interior phase (perpendicular dashes line up) yet each still
|
|
|
|
|
/// ends on a dash. `None` (the default) = per-wire A-type / from-start.
|
|
|
|
|
pub dash_align_end: Option<f32>,
|
2026-05-06 07:12:05 +03:00
|
|
|
/// Paper-space bounding box [x0, y0, x1, y1] for GPU scissor clipping.
|
|
|
|
|
/// Set only for viewport-projected wires in paper-space layouts.
|
|
|
|
|
pub vp_scissor: Option<[f32; 4]>,
|
2026-05-06 23:24:26 +03:00
|
|
|
/// Pre-triangulated solid fill: flat vertex list, 3 per triangle (world-offset applied).
|
|
|
|
|
/// Non-empty only for PolyfaceMesh / PolygonMesh entities.
|
|
|
|
|
pub fill_tris: Vec<[f32; 3]>,
|
2026-06-23 18:03:40 +03:00
|
|
|
/// Low residual paired with [`fill_tris`] (double-single). Empty = all-zero;
|
|
|
|
|
/// tessellation from CAD f64 fills it so fills stay precise at UTM scale.
|
|
|
|
|
pub fill_tris_low: Vec<[f32; 3]>,
|
refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).
- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
cached by the tess memo, cloned on hit, and transformed by the block
expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
the strokes; render gathers text_verts from the viewport wire set
(`gather_text_verts`, cached on wire content id) — the old collector is
removed. Covers model, dimension, table, block-internal and paper-sheet
text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
entity_aabb which mis-places the box for MTEXT; composite paths no longer
clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
highlight, keyed on selection_generation) mirroring the selected-wire
xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:01:36 +03:00
|
|
|
/// SDF glyph quads for this entity's text (TEXT / MTEXT / dimension text /
|
|
|
|
|
/// block-internal text). Non-empty only when SDF text is enabled and this
|
|
|
|
|
/// wire carries a text run. Rides with the wire so it is cached by the
|
|
|
|
|
/// tess memo, cloned on hit, and transformed by the block-expand loop
|
|
|
|
|
/// exactly like `points` — no separate collector pass. The renderer
|
|
|
|
|
/// gathers these across all wires into the text vertex buffer.
|
|
|
|
|
pub text_verts: Vec<crate::scene::pipeline::text_gpu::TextVertex>,
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WireModel {
|
|
|
|
|
pub const WHITE: [f32; 4] = [1.00, 1.00, 1.00, 1.0];
|
|
|
|
|
pub const CYAN: [f32; 4] = [0.25, 0.85, 1.00, 1.0];
|
|
|
|
|
pub const SELECTED: [f32; 4] = [0.15, 0.55, 1.00, 1.0];
|
2026-06-19 09:33:53 +03:00
|
|
|
/// Rollover (hover) highlight — orange, distinct from the blue selection.
|
|
|
|
|
pub const HOVER: [f32; 4] = [0.95, 0.55, 0.10, 1.0];
|
2026-04-26 12:21:51 +03:00
|
|
|
/// Sentinel AABB that never rejects any snap query.
|
2026-05-12 10:36:15 +03:00
|
|
|
pub const UNBOUNDED_AABB: [f32; 4] = [
|
|
|
|
|
f32::NEG_INFINITY,
|
|
|
|
|
f32::NEG_INFINITY,
|
|
|
|
|
f32::INFINITY,
|
|
|
|
|
f32::INFINITY,
|
|
|
|
|
];
|
2026-03-22 14:56:27 +03:00
|
|
|
|
2026-06-24 04:26:05 +03:00
|
|
|
/// Double-single split: `high + low ≈ v` to ~f64 precision in two f32s.
|
|
|
|
|
/// Matches the renderer's relative-to-eye reconstruction.
|
|
|
|
|
#[inline]
|
|
|
|
|
pub fn split_ds(v: f64) -> (f32, f32) {
|
|
|
|
|
let high = v as f32;
|
|
|
|
|
(high, (v - high as f64) as f32)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a solid preview wire from f64 points, filling the double-single
|
|
|
|
|
/// `points_low` buffer so the line stays precise at UTM-scale coordinates.
|
|
|
|
|
/// Rubber-band previews built straight from f32 absolute points jitter
|
|
|
|
|
/// ~0.5 m at UTM because the wire pass is relative-to-eye and expects the
|
|
|
|
|
/// low residual; this keeps the preview glued to the cursor.
|
|
|
|
|
pub fn solid_f64(name: String, points: Vec<[f64; 3]>, color: [f32; 4], selected: bool) -> Self {
|
|
|
|
|
let mut hi = Vec::with_capacity(points.len());
|
|
|
|
|
let mut lo = Vec::with_capacity(points.len());
|
|
|
|
|
for [x, y, z] in points {
|
|
|
|
|
let (hx, lx) = Self::split_ds(x);
|
|
|
|
|
let (hy, ly) = Self::split_ds(y);
|
|
|
|
|
let (hz, lz) = Self::split_ds(z);
|
|
|
|
|
hi.push([hx, hy, hz]);
|
|
|
|
|
lo.push([lx, ly, lz]);
|
|
|
|
|
}
|
|
|
|
|
let mut w = Self::solid(name, hi, color, selected);
|
|
|
|
|
w.points_low = lo;
|
|
|
|
|
w
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
/// Create a solid wire (no dash pattern, 1px weight).
|
|
|
|
|
pub fn solid(name: String, points: Vec<[f32; 3]>, color: [f32; 4], selected: bool) -> Self {
|
|
|
|
|
Self {
|
refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).
- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
cached by the tess memo, cloned on hit, and transformed by the block
expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
the strokes; render gathers text_verts from the viewport wire set
(`gather_text_verts`, cached on wire content id) — the old collector is
removed. Covers model, dimension, table, block-internal and paper-sheet
text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
entity_aabb which mis-places the box for MTEXT; composite paths no longer
clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
highlight, keyed on selection_generation) mirroring the selected-wire
xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:01:36 +03:00
|
|
|
text_verts: Vec::new(),
|
2026-03-22 14:56:27 +03:00
|
|
|
name,
|
|
|
|
|
points,
|
2026-06-23 17:08:54 +03:00
|
|
|
points_low: Vec::new(),
|
2026-03-22 14:56:27 +03:00
|
|
|
color,
|
|
|
|
|
selected,
|
2026-04-06 14:37:23 +03:00
|
|
|
aci: 0,
|
2026-03-22 14:56:27 +03:00
|
|
|
pattern_length: 0.0,
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
line_weight_px: 1.0,
|
|
|
|
|
snap_pts: vec![],
|
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
key_vertices: vec![],
|
2026-04-26 12:21:51 +03:00
|
|
|
aabb: Self::UNBOUNDED_AABB,
|
2026-05-06 06:55:28 +03:00
|
|
|
plinegen: true,
|
2026-07-17 01:49:18 +03:00
|
|
|
dash_from_start: false,
|
2026-07-17 02:59:11 +03:00
|
|
|
dash_align_end: None,
|
2026-05-06 07:12:05 +03:00
|
|
|
vp_scissor: None,
|
2026-05-06 23:24:26 +03:00
|
|
|
fill_tris: vec![],
|
2026-06-23 18:03:40 +03:00
|
|
|
fill_tris_low: Vec::new(),
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Return a clone with every point translated by `delta`.
|
|
|
|
|
pub fn translated(&self, delta: glam::Vec3) -> Self {
|
|
|
|
|
let mut out = self.clone();
|
|
|
|
|
out.name = format!("preview_{}", self.name);
|
|
|
|
|
out.color = Self::CYAN;
|
|
|
|
|
out.selected = false;
|
|
|
|
|
for p in &mut out.points {
|
|
|
|
|
p[0] += delta.x;
|
|
|
|
|
p[1] += delta.y;
|
|
|
|
|
p[2] += delta.z;
|
|
|
|
|
}
|
fix(text): show text in command previews (move/copy/etc) (#316)
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY,
PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/
scaled/stretched/mirrored, but those helpers moved only `points`, never
`text_verts`, and the ghosts' glyphs were never uploaded — so text
vanished (or froze at its source) during the preview, same root cause as
the grip-drag case.
Transform text_verts alongside points in all five helpers (shared
map_text_verts, reconstructing + re-splitting the double-single world
position), and gather preview_wires' glyphs into the per-frame
preview-text buffer in the render path. This covers every command
preview automatically. The grip-drag else branch no longer sets
preview_text explicitly (the render gather handles it); the grip-slide
fast path keeps its own scene.preview_text channel (empty preview_wires,
so no double-draw).
MIRROR honours MIRRTEXT: on -> true glyph mirror via mirrored(); off ->
readable relocation via translated().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:49:13 +03:00
|
|
|
if !out.text_verts.is_empty() {
|
|
|
|
|
let (dx, dy, dz) = (delta.x as f64, delta.y as f64, delta.z as f64);
|
|
|
|
|
out.text_verts =
|
|
|
|
|
map_text_verts(&self.text_verts, |x, y, z| (x + dx, y + dy, z + dz));
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Return a clone with every point rotated around `center` by `angle_rad`.
|
|
|
|
|
pub fn rotated(&self, center: glam::Vec3, angle_rad: f32) -> Self {
|
|
|
|
|
let (s, c) = angle_rad.sin_cos();
|
|
|
|
|
let mut out = self.clone();
|
|
|
|
|
out.name = format!("preview_{}", self.name);
|
|
|
|
|
out.color = Self::CYAN;
|
|
|
|
|
out.selected = false;
|
|
|
|
|
for p in &mut out.points {
|
|
|
|
|
let dx = p[0] - center.x;
|
|
|
|
|
let dy = p[1] - center.y;
|
|
|
|
|
p[0] = center.x + dx * c - dy * s;
|
|
|
|
|
p[1] = center.y + dx * s + dy * c;
|
|
|
|
|
}
|
fix(text): show text in command previews (move/copy/etc) (#316)
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY,
PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/
scaled/stretched/mirrored, but those helpers moved only `points`, never
`text_verts`, and the ghosts' glyphs were never uploaded — so text
vanished (or froze at its source) during the preview, same root cause as
the grip-drag case.
Transform text_verts alongside points in all five helpers (shared
map_text_verts, reconstructing + re-splitting the double-single world
position), and gather preview_wires' glyphs into the per-frame
preview-text buffer in the render path. This covers every command
preview automatically. The grip-drag else branch no longer sets
preview_text explicitly (the render gather handles it); the grip-slide
fast path keeps its own scene.preview_text channel (empty preview_wires,
so no double-draw).
MIRROR honours MIRRTEXT: on -> true glyph mirror via mirrored(); off ->
readable relocation via translated().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:49:13 +03:00
|
|
|
if !out.text_verts.is_empty() {
|
|
|
|
|
let (cx, cy) = (center.x as f64, center.y as f64);
|
|
|
|
|
let (s, c) = (s as f64, c as f64);
|
|
|
|
|
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
|
|
|
|
|
let (dx, dy) = (x - cx, y - cy);
|
|
|
|
|
(cx + dx * c - dy * s, cy + dx * s + dy * c, z)
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Return a clone with every point uniformly scaled from `center` by `factor`.
|
|
|
|
|
pub fn scaled(&self, center: glam::Vec3, factor: f32) -> Self {
|
|
|
|
|
let mut out = self.clone();
|
|
|
|
|
out.name = format!("preview_{}", self.name);
|
|
|
|
|
out.color = Self::CYAN;
|
|
|
|
|
out.selected = false;
|
|
|
|
|
for p in &mut out.points {
|
|
|
|
|
p[0] = center.x + (p[0] - center.x) * factor;
|
|
|
|
|
p[1] = center.y + (p[1] - center.y) * factor;
|
|
|
|
|
p[2] = center.z + (p[2] - center.z) * factor;
|
|
|
|
|
}
|
fix(text): show text in command previews (move/copy/etc) (#316)
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY,
PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/
scaled/stretched/mirrored, but those helpers moved only `points`, never
`text_verts`, and the ghosts' glyphs were never uploaded — so text
vanished (or froze at its source) during the preview, same root cause as
the grip-drag case.
Transform text_verts alongside points in all five helpers (shared
map_text_verts, reconstructing + re-splitting the double-single world
position), and gather preview_wires' glyphs into the per-frame
preview-text buffer in the render path. This covers every command
preview automatically. The grip-drag else branch no longer sets
preview_text explicitly (the render gather handles it); the grip-slide
fast path keeps its own scene.preview_text channel (empty preview_wires,
so no double-draw).
MIRROR honours MIRRTEXT: on -> true glyph mirror via mirrored(); off ->
readable relocation via translated().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:49:13 +03:00
|
|
|
if !out.text_verts.is_empty() {
|
|
|
|
|
let (cx, cy, cz) = (center.x as f64, center.y as f64, center.z as f64);
|
|
|
|
|
let f = factor as f64;
|
|
|
|
|
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
|
|
|
|
|
(cx + (x - cx) * f, cy + (y - cy) * f, cz + (z - cz) * f)
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 23:43:07 +03:00
|
|
|
/// Return a clone for a stretch preview: every point whose XY lies inside
|
|
|
|
|
/// the crossing window `[win_min, win_max]` is translated by `delta`; points
|
|
|
|
|
/// outside stay put. Exact for line/polyline vertices (the primary stretch
|
|
|
|
|
/// targets); curve tessellation points may deform where a window edge cuts
|
|
|
|
|
/// through them, matching the per-vertex nature of the operation.
|
|
|
|
|
pub fn stretched(&self, win_min: glam::Vec3, win_max: glam::Vec3, delta: glam::Vec3) -> Self {
|
|
|
|
|
let mut out = self.clone();
|
|
|
|
|
out.name = format!("preview_{}", self.name);
|
|
|
|
|
out.color = Self::CYAN;
|
|
|
|
|
out.selected = false;
|
|
|
|
|
for p in &mut out.points {
|
|
|
|
|
if p[0] >= win_min.x && p[0] <= win_max.x && p[1] >= win_min.y && p[1] <= win_max.y {
|
|
|
|
|
p[0] += delta.x;
|
|
|
|
|
p[1] += delta.y;
|
|
|
|
|
p[2] += delta.z;
|
|
|
|
|
}
|
|
|
|
|
}
|
fix(text): show text in command previews (move/copy/etc) (#316)
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY,
PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/
scaled/stretched/mirrored, but those helpers moved only `points`, never
`text_verts`, and the ghosts' glyphs were never uploaded — so text
vanished (or froze at its source) during the preview, same root cause as
the grip-drag case.
Transform text_verts alongside points in all five helpers (shared
map_text_verts, reconstructing + re-splitting the double-single world
position), and gather preview_wires' glyphs into the per-frame
preview-text buffer in the render path. This covers every command
preview automatically. The grip-drag else branch no longer sets
preview_text explicitly (the render gather handles it); the grip-slide
fast path keeps its own scene.preview_text channel (empty preview_wires,
so no double-draw).
MIRROR honours MIRRTEXT: on -> true glyph mirror via mirrored(); off ->
readable relocation via translated().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:49:13 +03:00
|
|
|
if !out.text_verts.is_empty() {
|
|
|
|
|
let (mnx, mny) = (win_min.x as f64, win_min.y as f64);
|
|
|
|
|
let (mxx, mxy) = (win_max.x as f64, win_max.y as f64);
|
|
|
|
|
let (dx, dy, dz) = (delta.x as f64, delta.y as f64, delta.z as f64);
|
|
|
|
|
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
|
|
|
|
|
if x >= mnx && x <= mxx && y >= mny && y <= mxy {
|
|
|
|
|
(x + dx, y + dy, z + dz)
|
|
|
|
|
} else {
|
|
|
|
|
(x, y, z)
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-06-15 23:43:07 +03:00
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
/// Return a clone mirrored across the line through `p1`→`p2`.
|
|
|
|
|
pub fn mirrored(&self, p1: glam::Vec3, p2: glam::Vec3) -> Self {
|
|
|
|
|
let ax = p2.x - p1.x;
|
|
|
|
|
let ay = p2.y - p1.y;
|
|
|
|
|
let len2 = ax * ax + ay * ay;
|
|
|
|
|
let mut out = self.clone();
|
|
|
|
|
out.name = format!("preview_{}", self.name);
|
|
|
|
|
out.color = Self::CYAN;
|
|
|
|
|
out.selected = false;
|
|
|
|
|
if len2 < 1e-12 {
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
for p in &mut out.points {
|
|
|
|
|
let dx = p[0] - p1.x;
|
|
|
|
|
let dy = p[1] - p1.y;
|
|
|
|
|
let t = (dx * ax + dy * ay) / len2;
|
|
|
|
|
p[0] = p1.x + 2.0 * t * ax - dx;
|
|
|
|
|
p[1] = p1.y + 2.0 * t * ay - dy;
|
|
|
|
|
}
|
2026-07-03 12:42:55 +03:00
|
|
|
// World position is the double-single sum `points + points_low` (text /
|
|
|
|
|
// UTM wires split it), so the residual must reflect too — as a direction
|
|
|
|
|
// (linear reflection about the axis, no `p1` offset).
|
|
|
|
|
for p in &mut out.points_low {
|
|
|
|
|
let t = (p[0] * ax + p[1] * ay) / len2;
|
|
|
|
|
p[0] = 2.0 * t * ax - p[0];
|
|
|
|
|
p[1] = 2.0 * t * ay - p[1];
|
|
|
|
|
}
|
fix(text): show text in command previews (move/copy/etc) (#316)
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY,
PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/
scaled/stretched/mirrored, but those helpers moved only `points`, never
`text_verts`, and the ghosts' glyphs were never uploaded — so text
vanished (or froze at its source) during the preview, same root cause as
the grip-drag case.
Transform text_verts alongside points in all five helpers (shared
map_text_verts, reconstructing + re-splitting the double-single world
position), and gather preview_wires' glyphs into the per-frame
preview-text buffer in the render path. This covers every command
preview automatically. The grip-drag else branch no longer sets
preview_text explicitly (the render gather handles it); the grip-slide
fast path keeps its own scene.preview_text channel (empty preview_wires,
so no double-draw).
MIRROR honours MIRRTEXT: on -> true glyph mirror via mirrored(); off ->
readable relocation via translated().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:49:13 +03:00
|
|
|
// Glyph quads reflect wholesale (true mirror) — the caller only routes
|
|
|
|
|
// text through here for MIRRTEXT-on; MIRRTEXT-off relocates via
|
|
|
|
|
// `translated` so glyphs stay readable.
|
|
|
|
|
if !out.text_verts.is_empty() {
|
|
|
|
|
let (ax, ay, len2) = (ax as f64, ay as f64, len2 as f64);
|
|
|
|
|
let (p1x, p1y) = (p1.x as f64, p1.y as f64);
|
|
|
|
|
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
|
|
|
|
|
let (dx, dy) = (x - p1x, y - p1y);
|
|
|
|
|
let t = (dx * ax + dy * ay) / len2;
|
|
|
|
|
(p1x + 2.0 * t * ax - dx, p1y + 2.0 * t * ay - dy, z)
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Total arc-length of this wire (sum of segment lengths).
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
pub fn length(&self) -> f32 {
|
|
|
|
|
self.points
|
|
|
|
|
.windows(2)
|
|
|
|
|
.map(|w| {
|
|
|
|
|
let dx = w[1][0] - w[0][0];
|
|
|
|
|
let dy = w[1][1] - w[0][1];
|
|
|
|
|
let dz = w[1][2] - w[0][2];
|
|
|
|
|
(dx * dx + dy * dy + dz * dz).sqrt()
|
|
|
|
|
})
|
|
|
|
|
.sum()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-26 12:21:51 +03:00
|
|
|
|
fix(text): show text in command previews (move/copy/etc) (#316)
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY,
PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/
scaled/stretched/mirrored, but those helpers moved only `points`, never
`text_verts`, and the ghosts' glyphs were never uploaded — so text
vanished (or froze at its source) during the preview, same root cause as
the grip-drag case.
Transform text_verts alongside points in all five helpers (shared
map_text_verts, reconstructing + re-splitting the double-single world
position), and gather preview_wires' glyphs into the per-frame
preview-text buffer in the render path. This covers every command
preview automatically. The grip-drag else branch no longer sets
preview_text explicitly (the render gather handles it); the grip-slide
fast path keeps its own scene.preview_text channel (empty preview_wires,
so no double-draw).
MIRROR honours MIRRTEXT: on -> true glyph mirror via mirrored(); off ->
readable relocation via translated().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:49:13 +03:00
|
|
|
/// Map every glyph vertex's double-single world position through `f`, re-
|
|
|
|
|
/// splitting the result. The preview transforms above move `points`, but SDF
|
|
|
|
|
/// glyph quads live in `text_verts` (absolute-world double-single) — so a text
|
|
|
|
|
/// ghost (MOVE / COPY / ROTATE / SCALE / STRETCH / MIRROR preview) must carry
|
|
|
|
|
/// these along or the dragged text renders frozen at its source (issue #316).
|
fix(export): project viewport text into paper space
Review follow-ups to #390, which fixed PDF/print text for model space but
left the layout-plot path — the one issue #385 was filed from — broken.
viewport_content_wires rewrote points/snap_pts/key_vertices/aabb and then
cloned text_verts through untouched, so paper-space viewport text stayed at
model (UTM) coordinates: a dimension at model (25000, 12000) plotted its
lines onto the sheet and its glyphs kilometres off the page. A text-only
wire fared worse — TEXT/MTEXT carry no stroke points since text went
SDF-only, so it was dropped outright by the empty-clip and AABB rejections.
Project the glyphs through the same proj_abs the snap points use (via the
existing map_text_verts helper), cull per glyph on the quad centroid, count
them towards the paper AABB, and dim them like the wire colour.
Also in emit_text, which diverged from the wire pass it mirrors:
- divide the pen by `scale`, or a Fit/windowed plot prints text as
near-invisible hairlines while its own lines stay correct;
- apply the CTB plot style, or a monochrome.ctb plot renders the lines
black and leaves the text on its screen colour;
- reset the dash pattern, which is persistent PDF state the wire pass
leaves set — a HIDDEN-linetype last wire printed glyph outlines dashed.
export_table's doc claimed it honoured TEXTFILL and widened the bold pen
itself; it did neither. Gate fill_tris on textfill() so hollow-on-screen
text exports hollow, carry the bold flag and widen the pen by the 1.7x the
bake uses, and resolve Face once per family instead of once per glyph (the
atlas mutex is held for that whole walk).
uv_key identifies a glyph by its tile's uv_min, but grow_height rescales
every entry's V and reset rewinds the packer, leaving already-built quads
addressing the wrong tile — garbage on screen and a silent miss in the
export table, i.e. #385 again under #347's grow-the-atlas conditions. Bump
an atlas generation on both and fold it into the tessellation memo guard so
the text re-lays-out instead. Quads built earlier in the same pass that
grows are still stale for that pass; the guard heals them on the next one.
Tests: the integration test's "some wire has text_verts" assert passed on
pre-fix code too (the exporter ignored them; the scene always carried
them), and its dimension half was satisfied by the TEXT alone — assert per
entity handle instead, and stop colliding on fixed names in the shared temp
root. The unit test's bbox came from the same quads the mapping reads, so
it held by construction; pin the run's own world box instead. That still
cannot catch a mirrored corner assignment — verified by mirroring it — so
say so rather than claim coverage the assert doesn't have.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:06:15 +03:00
|
|
|
/// Paper-space viewport projection uses it for the same reason (issue #385).
|
|
|
|
|
pub(crate) fn map_text_verts(
|
fix(text): show text in command previews (move/copy/etc) (#316)
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY,
PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/
scaled/stretched/mirrored, but those helpers moved only `points`, never
`text_verts`, and the ghosts' glyphs were never uploaded — so text
vanished (or froze at its source) during the preview, same root cause as
the grip-drag case.
Transform text_verts alongside points in all five helpers (shared
map_text_verts, reconstructing + re-splitting the double-single world
position), and gather preview_wires' glyphs into the per-frame
preview-text buffer in the render path. This covers every command
preview automatically. The grip-drag else branch no longer sets
preview_text explicitly (the render gather handles it); the grip-slide
fast path keeps its own scene.preview_text channel (empty preview_wires,
so no double-draw).
MIRROR honours MIRRTEXT: on -> true glyph mirror via mirrored(); off ->
readable relocation via translated().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:49:13 +03:00
|
|
|
verts: &[crate::scene::pipeline::text_gpu::TextVertex],
|
|
|
|
|
f: impl Fn(f64, f64, f64) -> (f64, f64, f64),
|
|
|
|
|
) -> Vec<crate::scene::pipeline::text_gpu::TextVertex> {
|
|
|
|
|
use crate::scene::pipeline::text_gpu::split_ds;
|
|
|
|
|
verts
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|v| {
|
|
|
|
|
let (nx, ny, nz) = f(
|
|
|
|
|
v.pos[0] as f64 + v.pos_low[0] as f64,
|
|
|
|
|
v.pos[1] as f64 + v.pos_low[1] as f64,
|
|
|
|
|
v.pos[2] as f64 + v.pos_low[2] as f64,
|
|
|
|
|
);
|
|
|
|
|
let (xh, xl) = split_ds(nx);
|
|
|
|
|
let (yh, yl) = split_ds(ny);
|
|
|
|
|
let (zh, zl) = split_ds(nz);
|
|
|
|
|
crate::scene::pipeline::text_gpu::TextVertex {
|
|
|
|
|
pos: [xh, yh, zh],
|
|
|
|
|
pos_low: [xl, yl, zl],
|
|
|
|
|
..*v
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 12:21:51 +03:00
|
|
|
impl Default for WireModel {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).
- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
cached by the tess memo, cloned on hit, and transformed by the block
expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
the strokes; render gathers text_verts from the viewport wire set
(`gather_text_verts`, cached on wire content id) — the old collector is
removed. Covers model, dimension, table, block-internal and paper-sheet
text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
entity_aabb which mis-places the box for MTEXT; composite paths no longer
clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
highlight, keyed on selection_generation) mirroring the selected-wire
xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:01:36 +03:00
|
|
|
text_verts: Vec::new(),
|
2026-04-26 12:21:51 +03:00
|
|
|
name: String::new(),
|
|
|
|
|
points: Vec::new(),
|
2026-06-23 17:08:54 +03:00
|
|
|
points_low: Vec::new(),
|
2026-04-26 12:21:51 +03:00
|
|
|
color: Self::WHITE,
|
|
|
|
|
selected: false,
|
|
|
|
|
pattern_length: 0.0,
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
line_weight_px: 1.0,
|
|
|
|
|
aci: 0,
|
|
|
|
|
snap_pts: Vec::new(),
|
|
|
|
|
tangent_geoms: Vec::new(),
|
|
|
|
|
key_vertices: Vec::new(),
|
|
|
|
|
aabb: Self::UNBOUNDED_AABB,
|
2026-05-06 06:55:28 +03:00
|
|
|
plinegen: true,
|
2026-07-17 01:49:18 +03:00
|
|
|
dash_from_start: false,
|
2026-07-17 02:59:11 +03:00
|
|
|
dash_align_end: None,
|
2026-05-06 07:12:05 +03:00
|
|
|
vp_scissor: None,
|
2026-05-06 23:24:26 +03:00
|
|
|
fill_tris: Vec::new(),
|
2026-06-23 18:03:40 +03:00
|
|
|
fill_tris_low: Vec::new(),
|
2026-04-26 12:21:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|