From a5c17117d97d5fd5c611e6ae75adbc6dd11ac2a5 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:28:38 -0500 Subject: [PATCH 01/16] feat(model): add styled dimension properties and array/mirror constructions with transform expansion pass --- src/model.rs | 54 ++++++ src/transform.rs | 483 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 537 insertions(+) create mode 100644 src/transform.rs diff --git a/src/model.rs b/src/model.rs index 0d6dd2e..16eb5ef 100644 --- a/src/model.rs +++ b/src/model.rs @@ -114,6 +114,12 @@ pub struct CfDim { pub to: [f64; 2], #[serde(default = "default_offset")] pub offset: f64, + /// Label height in world units (default 0.25). + pub text_size: Option, + /// Decimal places for the measured value (default 2). + pub precision: Option, + /// Append the project units to the label (default true). + pub show_units: Option, #[serde(flatten)] pub common: CommonAttrs, } @@ -160,6 +166,50 @@ pub struct CfGroup { pub common: CommonAttrs, } +#[derive(Debug, Clone, Copy, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ArrayMode { + Linear, + Polar, +} + +/// Repeats target primitives: linear (offset per copy) or polar (rotation +/// around a center — spiral stairs, gear teeth, radial columns). +#[derive(Debug, Clone, Deserialize)] +pub struct CfArray { + /// Single target id (alternative to `targets`). + pub target: Option, + /// Multiple target ids. + pub targets: Option>, + pub mode: ArrayMode, + /// Total number of instances, including the original. + pub count: usize, + /// Linear: displacement per copy. + pub offset: Option<[f64; 2]>, + /// Polar: rotation center. + pub center: Option<[f64; 2]>, + /// Polar: degrees per copy (counterclockwise). + pub step_angle: Option, + /// Polar: rotate each copy's geometry (true) or only orbit it (false). + #[serde(default = "default_true")] + pub rotate_items: bool, + #[serde(flatten)] + pub common: CommonAttrs, +} + +/// Mirrors target primitives across an axis defined by two points. +#[derive(Debug, Clone, Deserialize)] +pub struct CfMirror { + /// Single target id (alternative to `targets`). + pub target: Option, + /// Multiple target ids. + pub targets: Option>, + /// Mirror axis: two points [[x1, y1], [x2, y2]]. + pub axis: [[f64; 2]; 2], + #[serde(flatten)] + pub common: CommonAttrs, +} + #[derive(Debug, Clone, Deserialize)] pub struct CfFill { /// Reference to a closed polyline or rect id, or inline points. @@ -211,4 +261,8 @@ pub struct CfFile { pub fills: Vec, #[serde(default, rename = "group")] pub groups: Vec, + #[serde(default, rename = "array")] + pub arrays: Vec, + #[serde(default, rename = "mirror")] + pub mirrors: Vec, } diff --git a/src/transform.rs b/src/transform.rs new file mode 100644 index 0000000..62c0bcc --- /dev/null +++ b/src/transform.rs @@ -0,0 +1,483 @@ +//! Transform pass — expands `[[array]]` and `[[mirror]]` constructions into +//! concrete primitives before compilation. +//! +//! A polar array of a tread polyline is a spiral staircase plan; a polar array +//! of a tooth profile is a gear; a mirror duplicates a wing of a building. +//! Expansion happens at load time, so DXF output, SVG/PNG previews, and entity +//! metadata all see the generated geometry with no special cases. +//! +//! Generated copies get derived ids: `tread@1`, `tread@2`, … for arrays and +//! `tread@m` for mirrors, so they can still be referenced (e.g. by hatches) +//! and highlighted. + +use crate::model::{ArrayMode, CfArray, CfFile, CfMirror}; +use std::collections::HashSet; + +// ── Point operations ──────────────────────────────────────────────────── + +#[derive(Clone, Copy)] +enum PointOp { + Translate { + dx: f64, + dy: f64, + }, + Rotate { + cx: f64, + cy: f64, + angle_rad: f64, + }, + /// Mirror across the line through (x0, y0) with direction angle `axis_rad`. + Mirror { + x0: f64, + y0: f64, + axis_rad: f64, + }, +} + +impl PointOp { + fn apply(&self, p: [f64; 2]) -> [f64; 2] { + match *self { + PointOp::Translate { dx, dy } => [p[0] + dx, p[1] + dy], + PointOp::Rotate { cx, cy, angle_rad } => { + let (s, c) = angle_rad.sin_cos(); + let (x, y) = (p[0] - cx, p[1] - cy); + [cx + x * c - y * s, cy + x * s + y * c] + } + PointOp::Mirror { x0, y0, axis_rad } => { + let (s, c) = (2.0 * axis_rad).sin_cos(); + let (x, y) = (p[0] - x0, p[1] - y0); + [x0 + x * c + y * s, y0 + x * s - y * c] + } + } + } + + /// How a direction angle (degrees) maps under this op. + fn apply_angle_deg(&self, deg: f64) -> f64 { + match *self { + PointOp::Translate { .. } => deg, + PointOp::Rotate { angle_rad, .. } => deg + angle_rad.to_degrees(), + PointOp::Mirror { axis_rad, .. } => 2.0 * axis_rad.to_degrees() - deg, + } + } + + fn flips_orientation(&self) -> bool { + matches!(self, PointOp::Mirror { .. }) + } +} + +// ── Expansion ─────────────────────────────────────────────────────────── + +/// Expand all `[[array]]` and `[[mirror]]` entries of a layer into concrete +/// primitives. The original constructions are consumed. +pub fn expand_cf(cf: &CfFile) -> CfFile { + let mut out = cf.clone(); + let arrays = std::mem::take(&mut out.arrays); + let mirrors = std::mem::take(&mut out.mirrors); + + for array in &arrays { + expand_array(&mut out, array); + } + for mirror in &mirrors { + expand_mirror(&mut out, mirror); + } + out +} + +fn target_set(target: &Option, targets: &Option>) -> HashSet { + let mut set = HashSet::new(); + if let Some(t) = target { + set.insert(t.clone()); + } + if let Some(ts) = targets { + set.extend(ts.iter().cloned()); + } + set +} + +fn expand_array(out: &mut CfFile, array: &CfArray) { + let targets = target_set(&array.target, &array.targets); + if targets.is_empty() || array.count < 2 { + return; + } + + for k in 1..array.count { + let op = match array.mode { + ArrayMode::Linear => { + let [dx, dy] = array.offset.unwrap_or([0.0, 0.0]); + PointOp::Translate { + dx: dx * k as f64, + dy: dy * k as f64, + } + } + ArrayMode::Polar => { + let [cx, cy] = array.center.unwrap_or([0.0, 0.0]); + PointOp::Rotate { + cx, + cy, + angle_rad: (array.step_angle.unwrap_or(0.0) * k as f64).to_radians(), + } + } + }; + let orbit_only = array.mode == ArrayMode::Polar && !array.rotate_items; + copy_targets(out, &targets, op, orbit_only, &format!("@{}", k)); + } +} + +fn expand_mirror(out: &mut CfFile, mirror: &CfMirror) { + let targets = target_set(&mirror.target, &mirror.targets); + if targets.is_empty() { + return; + } + let [a, b] = mirror.axis; + let axis_rad = (b[1] - a[1]).atan2(b[0] - a[0]); + let op = PointOp::Mirror { + x0: a[0], + y0: a[1], + axis_rad, + }; + copy_targets(out, &targets, op, false, "@m"); +} + +/// Clone every targeted primitive, transform it, suffix its id, and append it. +fn copy_targets( + out: &mut CfFile, + targets: &HashSet, + op: PointOp, + orbit_only: bool, + suffix: &str, +) { + fn hit(id: &Option, targets: &HashSet) -> bool { + id.as_deref().is_some_and(|i| targets.contains(i)) + } + fn suffixed(id: &Option, suffix: &str) -> Option { + id.as_ref().map(|i| format!("{}{}", i, suffix)) + } + + let mut new_lines = Vec::new(); + for e in out.lines.iter().filter(|e| hit(&e.common.id, targets)) { + let mut c = e.clone(); + c.from = op.apply(c.from); + c.to = op.apply(c.to); + c.common.id = suffixed(&e.common.id, suffix); + new_lines.push(c); + } + out.lines.extend(new_lines); + + let mut new_polys = Vec::new(); + for e in out.polylines.iter().filter(|e| hit(&e.common.id, targets)) { + let mut c = e.clone(); + for p in &mut c.points { + *p = op.apply(*p); + } + c.common.id = suffixed(&e.common.id, suffix); + new_polys.push(c); + } + out.polylines.extend(new_polys); + + // Rects: a translated copy stays a rect; a rotated or mirrored copy + // becomes a closed polyline (rects are axis-aligned by definition). + let mut rect_polys = Vec::new(); + let mut new_rects = Vec::new(); + for e in out.rects.iter().filter(|e| hit(&e.common.id, targets)) { + let corners = [ + e.origin, + [e.origin[0] + e.width, e.origin[1]], + [e.origin[0] + e.width, e.origin[1] + e.height], + [e.origin[0], e.origin[1] + e.height], + ]; + let keeps_shape = matches!(op, PointOp::Translate { .. }) || orbit_only; + if keeps_shape { + let mut c = e.clone(); + if orbit_only { + // Orbit the rect center, keep the rect axis-aligned. + let center = [e.origin[0] + e.width / 2.0, e.origin[1] + e.height / 2.0]; + let moved = op.apply(center); + c.origin = [moved[0] - e.width / 2.0, moved[1] - e.height / 2.0]; + } else { + c.origin = op.apply(c.origin); + } + c.common.id = suffixed(&e.common.id, suffix); + new_rects.push(c); + } else { + rect_polys.push(crate::model::CfPolyline { + points: corners.iter().map(|&p| op.apply(p)).collect(), + closed: true, + common: crate::model::CommonAttrs { + id: suffixed(&e.common.id, suffix), + ..e.common.clone() + }, + }); + } + } + out.rects.extend(new_rects); + out.polylines.extend(rect_polys); + + let mut new_circles = Vec::new(); + for e in out.circles.iter().filter(|e| hit(&e.common.id, targets)) { + let mut c = e.clone(); + c.center = op.apply(c.center); + c.common.id = suffixed(&e.common.id, suffix); + new_circles.push(c); + } + out.circles.extend(new_circles); + + let mut new_arcs = Vec::new(); + for e in out.arcs.iter().filter(|e| hit(&e.common.id, targets)) { + let mut c = e.clone(); + c.center = op.apply(c.center); + if !orbit_only { + if op.flips_orientation() { + // Reflected sweep: endpoints swap to keep the arc CCW. + let from = op.apply_angle_deg(e.to_angle); + let to = op.apply_angle_deg(e.from_angle); + c.from_angle = from; + c.to_angle = to; + } else { + c.from_angle = op.apply_angle_deg(e.from_angle); + c.to_angle = op.apply_angle_deg(e.to_angle); + } + } + c.common.id = suffixed(&e.common.id, suffix); + new_arcs.push(c); + } + out.arcs.extend(new_arcs); + + // Texts stay upright: only the anchor moves. + let mut new_texts = Vec::new(); + for e in out.texts.iter().filter(|e| hit(&e.common.id, targets)) { + let mut c = e.clone(); + c.position = op.apply(c.position); + c.common.id = suffixed(&e.common.id, suffix); + new_texts.push(c); + } + out.texts.extend(new_texts); + + let mut new_points = Vec::new(); + for e in out.points.iter().filter(|e| hit(&e.common.id, targets)) { + let mut c = e.clone(); + c.position = op.apply(c.position); + c.common.id = suffixed(&e.common.id, suffix); + new_points.push(c); + } + out.points.extend(new_points); + + let mut new_dims = Vec::new(); + for e in out.dims.iter().filter(|e| hit(&e.common.id, targets)) { + let mut c = e.clone(); + c.from = op.apply(c.from); + c.to = op.apply(c.to); + if op.flips_orientation() { + c.offset = -c.offset; + } + c.common.id = suffixed(&e.common.id, suffix); + new_dims.push(c); + } + out.dims.extend(new_dims); + + // Hatches/fills follow their boundary: if the boundary was copied too, + // the copy references the copied boundary id. + let mut new_hatches = Vec::new(); + for e in out.hatches.iter().filter(|e| hit(&e.common.id, targets)) { + let mut c = e.clone(); + if targets.contains(&e.boundary) { + c.boundary = format!("{}{}", e.boundary, suffix); + } + c.common.id = suffixed(&e.common.id, suffix); + new_hatches.push(c); + } + out.hatches.extend(new_hatches); + + let mut new_fills = Vec::new(); + for e in out.fills.iter().filter(|e| hit(&e.common.id, targets)) { + let mut c = e.clone(); + if let Some(boundary) = &e.boundary { + if targets.contains(boundary) { + c.boundary = Some(format!("{}{}", boundary, suffix)); + } + } + if let Some(points) = &mut c.points { + for p in points { + *p = op.apply(*p); + } + } + c.common.id = suffixed(&e.common.id, suffix); + new_fills.push(c); + } + out.fills.extend(new_fills); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(toml: &str) -> CfFile { + toml::from_str(toml).unwrap() + } + + fn assert_close(actual: [f64; 2], expected: [f64; 2]) { + assert!( + (actual[0] - expected[0]).abs() < 1e-9 && (actual[1] - expected[1]).abs() < 1e-9, + "expected {:?}, got {:?}", + expected, + actual + ); + } + + #[test] + fn linear_array_translates_copies() { + let cf = parse( + r#" +[[rect]] +id = "col" +origin = [0.0, 0.0] +width = 0.3 +height = 0.3 + +[[array]] +target = "col" +mode = "linear" +count = 4 +offset = [2.0, 0.0] +"#, + ); + let out = expand_cf(&cf); + assert_eq!(out.rects.len(), 4); + assert!(out.arrays.is_empty()); + assert_eq!(out.rects[3].origin, [6.0, 0.0]); + assert_eq!(out.rects[3].common.id.as_deref(), Some("col@3")); + } + + #[test] + fn polar_array_rotates_polyline_like_a_gear() { + let cf = parse( + r#" +[[polyline]] +id = "tooth" +points = [[10.0, 0.0], [11.0, 0.5], [11.0, -0.5]] +closed = true + +[[array]] +target = "tooth" +mode = "polar" +count = 12 +center = [0.0, 0.0] +step_angle = 30.0 +"#, + ); + let out = expand_cf(&cf); + assert_eq!(out.polylines.len(), 12); + // Copy 3 is rotated 90°: (10, 0) → (0, 10) + let p = out.polylines[3].points[0]; + assert!((p[0]).abs() < 1e-9 && (p[1] - 10.0).abs() < 1e-9); + } + + #[test] + fn polar_orbit_keeps_rect_axis_aligned() { + let cf = parse( + r#" +[[rect]] +id = "silla" +origin = [4.0, -0.5] +width = 1.0 +height = 1.0 + +[[array]] +target = "silla" +mode = "polar" +count = 4 +center = [0.0, 0.0] +step_angle = 90.0 +rotate_items = false +"#, + ); + let out = expand_cf(&cf); + // Rect stays a rect when orbiting + assert_eq!(out.rects.len(), 4); + // Center (4.5, 0) rotated 90° → (0, 4.5); origin = center - half size + assert_close(out.rects[1].origin, [-0.5, 4.0]); + } + + #[test] + fn polar_rotation_converts_rect_to_polyline() { + let cf = parse( + r#" +[[rect]] +id = "huella" +origin = [1.0, 0.0] +width = 1.2 +height = 0.3 + +[[array]] +target = "huella" +mode = "polar" +count = 3 +center = [0.0, 0.0] +step_angle = 20.0 +"#, + ); + let out = expand_cf(&cf); + assert_eq!(out.rects.len(), 1, "original rect stays"); + assert_eq!(out.polylines.len(), 2, "rotated copies become polylines"); + assert!(out.polylines.iter().all(|p| p.closed)); + } + + #[test] + fn mirror_reflects_and_inverts_arc_sweep() { + let cf = parse( + r#" +[[line]] +id = "muro" +from = [1.0, 0.0] +to = [1.0, 5.0] + +[[arc]] +id = "puerta" +center = [1.0, 2.0] +radius = 0.9 +from_angle = 0.0 +to_angle = 90.0 + +[[mirror]] +targets = ["muro", "puerta"] +axis = [[3.0, 0.0], [3.0, 1.0]] +"#, + ); + let out = expand_cf(&cf); + assert_eq!(out.lines.len(), 2); + assert_eq!(out.arcs.len(), 2); + // Vertical axis at x=3: x=1 → x=5 + assert_close(out.lines[1].from, [5.0, 0.0]); + let m = &out.arcs[1]; + assert_close(m.center, [5.0, 2.0]); + // Vertical-axis mirror maps θ → 180−θ, endpoints swapped: [90°,180°] + assert!((m.from_angle - 90.0).abs() < 1e-9); + assert!((m.to_angle - 180.0).abs() < 1e-9); + assert_eq!(m.common.id.as_deref(), Some("puerta@m")); + } + + #[test] + fn array_remaps_hatch_boundary_to_copied_polyline() { + let cf = parse( + r#" +[[polyline]] +id = "zona" +points = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]] +closed = true + +[[hatch]] +id = "zona-hatch" +boundary = "zona" + +[[array]] +targets = ["zona", "zona-hatch"] +mode = "linear" +count = 2 +offset = [3.0, 0.0] +"#, + ); + let out = expand_cf(&cf); + assert_eq!(out.hatches.len(), 2); + assert_eq!(out.hatches[1].boundary, "zona@1"); + assert_eq!(out.polylines[1].points[0], [3.0, 0.0]); + } +} From 97e290bae2eed7e167a3836d4fda3e87ae57f8c6 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:28:38 -0500 Subject: [PATCH 02/16] feat(svg): add full-fidelity vector renderer with grid, hatches, measured dimensions, highlights and entity metadata --- src/svg.rs | 1068 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1068 insertions(+) create mode 100644 src/svg.rs diff --git a/src/svg.rs b/src/svg.rs new file mode 100644 index 0000000..003b2ef --- /dev/null +++ b/src/svg.rs @@ -0,0 +1,1068 @@ +//! SVG renderer — full-fidelity vector rendering of a project. +//! +//! Renders real text, dimension lines with measured values, hatch patterns +//! clipped to their boundary, line styles, and optional highlight markers. +//! It is the single rendering backend: `cadforge serve` displays the SVG +//! directly and the PNG preview rasterizes it. + +use crate::compiler::resolve_boundary; +use crate::model::{CfFile, CommonAttrs, LineStyle, TextAlign}; +use crate::parser::{parse_cf, parse_project}; +use crate::transform::expand_cf; +use anyhow::{Context, Result}; +use std::fmt::Write as _; +use std::path::Path; + +const PADDING: f64 = 1.0; // world units around content +const MAX_HEIGHT_PX: f64 = 4096.0; +const BG_COLOR: &str = "#141414"; +const GRID_COLOR: &str = "#232323"; +const AXIS_COLOR: &str = "#333333"; + +const LAYER_PALETTE: &[&str] = &[ + "#FFFFFF", "#FF5050", "#50FF50", "#50C8FF", "#FFC850", "#C878FF", "#FF9632", +]; + +// ── Public API ────────────────────────────────────────────────────────── + +/// A rendered SVG plus the world→pixel transform used to produce it, so +/// consumers (PNG rasterizer, metadata) can map world coordinates to pixels. +pub struct Scene { + pub svg: String, + /// Pixels per world unit. + pub px_per_unit: f64, + /// World X of the left canvas edge. + pub offset_x: f64, + /// World Y of the bottom canvas edge. + pub offset_y: f64, + /// Canvas height in world units (used for the Y flip). + pub world_h: f64, + pub width_px: f64, + pub height_px: f64, + /// Content bounds (without padding): [min_x, min_y, max_x, max_y]. + pub world_bounds: [f64; 4], +} + +impl Scene { + /// Map a world coordinate to image pixels. + pub fn world_to_px(&self, x: f64, y: f64) -> (f64, f64) { + ( + (x - self.offset_x) * self.px_per_unit, + (self.world_h - (y - self.offset_y)) * self.px_per_unit, + ) + } +} + +/// Parse `project.toml` and its (filtered) layer files in one pass, with +/// `[[array]]`/`[[mirror]]` constructions expanded into concrete primitives. +pub fn load_project_layers( + project_dir: &Path, + layer_filter: Option<&str>, +) -> Result<(crate::parser::ProjectFile, Vec<(String, CfFile)>)> { + let project = parse_project(&project_dir.join("project.toml"))?; + + let layers: Vec<(String, CfFile)> = project + .layers + .iter() + .filter(|(name, _)| layer_filter.is_none_or(|f| f == *name)) + .map(|(name, entry)| { + let cf = parse_cf(&project_dir.join(&entry.file)) + .with_context(|| format!("Failed to parse layer '{}'", name))?; + Ok((name.clone(), expand_cf(&cf))) + }) + .collect::>()?; + + Ok((project, layers)) +} + +/// Render already-parsed layers to a [`Scene`], optionally highlighting ids. +pub fn render_scene_from( + project_name: &str, + units: &str, + layers: &[(String, CfFile)], + width: u32, + highlight: &[String], +) -> Scene { + render_layers(project_name, units, layers, width, highlight) +} + +/// Render the project to a [`Scene`], optionally highlighting entities by id. +pub fn render_scene( + project_dir: &Path, + layer_filter: Option<&str>, + width: u32, + highlight: &[String], +) -> Result { + let (project, layers) = load_project_layers(project_dir, layer_filter)?; + Ok(render_layers( + &project.project.name, + &project.project.units, + &layers, + width, + highlight, + )) +} + +/// Render the project to an SVG string. +pub fn render_svg(project_dir: &Path, layer_filter: Option<&str>, width: u32) -> Result { + Ok(render_scene(project_dir, layer_filter, width, &[])?.svg) +} + +/// Display color of a layer: its declared color, or a palette color by index. +pub fn layer_display_color(cf: &CfFile, index: usize) -> String { + cf.layer_meta + .as_ref() + .and_then(|m| m.color.clone()) + .unwrap_or_else(|| LAYER_PALETTE[index % LAYER_PALETTE.len()].to_string()) +} + +// ── Canvas ────────────────────────────────────────────────────────────── + +struct Canvas { + out: String, + scale: f64, + offset_x: f64, + offset_y: f64, + world_h: f64, + width_px: f64, + height_px: f64, + clip_seq: usize, +} + +impl Canvas { + fn world_to_px(&self, x: f64, y: f64) -> (f64, f64) { + let px = (x - self.offset_x) * self.scale; + let py = (self.world_h - (y - self.offset_y)) * self.scale; + (px, py) + } + + fn points_attr(&self, points: &[(f64, f64)]) -> String { + let mut s = String::with_capacity(points.len() * 16); + for (i, &(x, y)) in points.iter().enumerate() { + if i > 0 { + s.push(' '); + } + let (px, py) = self.world_to_px(x, y); + let _ = write!(s, "{:.2},{:.2}", px, py); + } + s + } +} + +#[derive(Clone)] +struct Style { + color: String, + width_px: f64, + dash: Option<&'static str>, +} + +fn resolve_style(common: &CommonAttrs, layer_color: &str, default_weight: f64) -> Style { + let color = common + .color + .clone() + .unwrap_or_else(|| layer_color.to_string()); + let weight = common.weight.unwrap_or(default_weight); + let width_px = ((weight / 0.35) * 1.4).clamp(0.6, 6.0); + let dash = match common.style { + Some(LineStyle::Dashed) => Some("8,6"), + Some(LineStyle::Dotted) => Some("1.5,5"), + Some(LineStyle::Dashdot) => Some("10,4,1.5,4"), + _ => None, + }; + Style { + color, + width_px, + dash, + } +} + +fn stroke_attrs(s: &Style) -> String { + let mut a = format!( + r#"stroke="{}" stroke-width="{:.2}" fill="none""#, + s.color, s.width_px + ); + if let Some(dash) = s.dash { + let _ = write!(a, r#" stroke-dasharray="{}""#, dash); + } + a +} + +fn id_attr(common: &CommonAttrs) -> String { + match &common.id { + Some(id) => format!(r#" data-id="{}""#, xml_escape(id)), + None => String::new(), + } +} + +fn xml_escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} + +// ── Bounds ────────────────────────────────────────────────────────────── + +struct Bounds { + min_x: f64, + min_y: f64, + max_x: f64, + max_y: f64, +} + +impl Bounds { + fn empty() -> Self { + Self { + min_x: f64::MAX, + min_y: f64::MAX, + max_x: f64::MIN, + max_y: f64::MIN, + } + } + + fn add(&mut self, x: f64, y: f64) { + self.min_x = self.min_x.min(x); + self.min_y = self.min_y.min(y); + self.max_x = self.max_x.max(x); + self.max_y = self.max_y.max(y); + } + + fn is_empty(&self) -> bool { + self.min_x > self.max_x + } +} + +fn compute_bounds(layers: &[(String, CfFile)]) -> Bounds { + let mut b = Bounds::empty(); + for (_, cf) in layers { + for e in &cf.lines { + b.add(e.from[0], e.from[1]); + b.add(e.to[0], e.to[1]); + } + for e in &cf.polylines { + for p in &e.points { + b.add(p[0], p[1]); + } + } + for e in &cf.rects { + b.add(e.origin[0], e.origin[1]); + b.add(e.origin[0] + e.width, e.origin[1] + e.height); + } + for e in &cf.circles { + b.add(e.center[0] - e.radius, e.center[1] - e.radius); + b.add(e.center[0] + e.radius, e.center[1] + e.radius); + } + for e in &cf.arcs { + b.add(e.center[0] - e.radius, e.center[1] - e.radius); + b.add(e.center[0] + e.radius, e.center[1] + e.radius); + } + for e in &cf.texts { + b.add(e.position[0], e.position[1]); + } + for e in &cf.points { + b.add(e.position[0], e.position[1]); + } + for e in &cf.dims { + b.add(e.from[0], e.from[1]); + b.add(e.to[0], e.to[1]); + } + for e in &cf.fills { + if let Some(points) = &e.points { + for p in points { + b.add(p[0], p[1]); + } + } + } + } + if b.is_empty() { + Bounds { + min_x: 0.0, + min_y: 0.0, + max_x: 10.0, + max_y: 10.0, + } + } else { + b + } +} + +// ── Rendering ─────────────────────────────────────────────────────────── + +fn render_layers( + project_name: &str, + units: &str, + layers: &[(String, CfFile)], + width: u32, + highlight: &[String], +) -> Scene { + let bounds = compute_bounds(layers); + let world_w = bounds.max_x - bounds.min_x + 2.0 * PADDING; + let world_h = bounds.max_y - bounds.min_y + 2.0 * PADDING; + + let width_px = width as f64; + let height_px = (width_px * world_h / world_w).min(MAX_HEIGHT_PX); + let scale = (width_px / world_w).min(height_px / world_h); + + let mut canvas = Canvas { + out: String::with_capacity(16 * 1024), + scale, + offset_x: bounds.min_x - PADDING, + offset_y: bounds.min_y - PADDING, + world_h, + width_px, + height_px, + clip_seq: 0, + }; + + let _ = write!( + canvas.out, + r#""#, + w = canvas.width_px, + h = canvas.height_px, + name = xml_escape(project_name), + ); + let _ = write!( + canvas.out, + r#""#, + BG_COLOR + ); + + draw_grid(&mut canvas, &bounds); + + for (idx, (layer_name, cf)) in layers.iter().enumerate() { + let layer_color = layer_display_color(cf, idx); + let default_weight = cf + .layer_meta + .as_ref() + .and_then(|m| m.line_weight) + .unwrap_or(0.35); + let visible = cf.layer_meta.as_ref().map(|m| m.visible).unwrap_or(true); + if !visible { + continue; + } + let _ = write!(canvas.out, r#""#, xml_escape(layer_name)); + render_layer(&mut canvas, cf, &layer_color, default_weight, units); + canvas.out.push_str(""); + } + + if !highlight.is_empty() { + draw_highlights(&mut canvas, layers, highlight); + } + + canvas.out.push_str(""); + Scene { + px_per_unit: canvas.scale, + offset_x: canvas.offset_x, + offset_y: canvas.offset_y, + world_h: canvas.world_h, + width_px: canvas.width_px, + height_px: canvas.height_px, + world_bounds: [bounds.min_x, bounds.min_y, bounds.max_x, bounds.max_y], + svg: canvas.out, + } +} + +// ── Highlights (visual verification markers for agents) ──────────────── + +const HIGHLIGHT_COLOR: &str = "#FFB300"; + +fn draw_highlights(c: &mut Canvas, layers: &[(String, CfFile)], highlight: &[String]) { + c.out.push_str(r#""#); + for (_, cf) in layers { + for rec in enumerate_entities(cf) { + let Some(id) = &rec.id else { continue }; + if !highlight.iter().any(|h| h == id) { + continue; + } + let (x1, y1) = c.world_to_px(rec.bbox[0], rec.bbox[3]); // top-left + let (x2, y2) = c.world_to_px(rec.bbox[2], rec.bbox[1]); // bottom-right + let margin = 8.0; + let _ = write!( + c.out, + r#""#, + x1 - margin, + y1 - margin, + (x2 - x1) + 2.0 * margin, + (y2 - y1) + 2.0 * margin, + HIGHLIGHT_COLOR, + xml_escape(id), + ); + let _ = write!( + c.out, + r#"{}"#, + x1 - margin, + y1 - margin - 6.0, + HIGHLIGHT_COLOR, + xml_escape(id), + ); + } + } + c.out.push_str(""); +} + +// ── Entity enumeration (shared with the PNG preview metadata) ─────────── + +/// A primitive with its world-space bounding box, for metadata and highlights. +pub struct EntityRecord { + pub id: Option, + pub kind: &'static str, + /// [min_x, min_y, max_x, max_y] in world units. + pub bbox: [f64; 4], + /// Text content, for `text` entities. + pub content: Option, +} + +/// Enumerate the drawable primitives of a layer with their world bounds. +pub fn enumerate_entities(cf: &CfFile) -> Vec { + fn bbox_of(points: &[(f64, f64)]) -> [f64; 4] { + let mut b = Bounds::empty(); + for &(x, y) in points { + b.add(x, y); + } + [b.min_x, b.min_y, b.max_x, b.max_y] + } + + let mut out = Vec::new(); + for e in &cf.lines { + out.push(EntityRecord { + id: e.common.id.clone(), + kind: "line", + bbox: bbox_of(&[(e.from[0], e.from[1]), (e.to[0], e.to[1])]), + content: None, + }); + } + for e in &cf.polylines { + let pts: Vec<(f64, f64)> = e.points.iter().map(|p| (p[0], p[1])).collect(); + out.push(EntityRecord { + id: e.common.id.clone(), + kind: "polyline", + bbox: bbox_of(&pts), + content: None, + }); + } + for e in &cf.rects { + out.push(EntityRecord { + id: e.common.id.clone(), + kind: "rect", + bbox: [ + e.origin[0], + e.origin[1], + e.origin[0] + e.width, + e.origin[1] + e.height, + ], + content: None, + }); + } + for e in &cf.circles { + out.push(EntityRecord { + id: e.common.id.clone(), + kind: "circle", + bbox: [ + e.center[0] - e.radius, + e.center[1] - e.radius, + e.center[0] + e.radius, + e.center[1] + e.radius, + ], + content: None, + }); + } + for e in &cf.arcs { + out.push(EntityRecord { + id: e.common.id.clone(), + kind: "arc", + bbox: bbox_of(&arc_points( + e.center[0], + e.center[1], + e.radius, + e.from_angle, + e.to_angle, + )), + content: None, + }); + } + for e in &cf.texts { + // Approximate extent from monospace glyph proportions. + let w = 0.6 * e.size * e.content.chars().count() as f64; + out.push(EntityRecord { + id: e.common.id.clone(), + kind: "text", + bbox: [ + e.position[0], + e.position[1], + e.position[0] + w, + e.position[1] + e.size, + ], + content: Some(e.content.clone()), + }); + } + for e in &cf.points { + out.push(EntityRecord { + id: e.common.id.clone(), + kind: "point", + bbox: [ + e.position[0] - 0.05, + e.position[1] - 0.05, + e.position[0] + 0.05, + e.position[1] + 0.05, + ], + content: None, + }); + } + for e in &cf.dims { + let dx = e.to[0] - e.from[0]; + let dy = e.to[1] - e.from[1]; + let len = (dx * dx + dy * dy).sqrt().max(1e-9); + let (nx, ny) = (-dy / len, dx / len); + out.push(EntityRecord { + id: e.common.id.clone(), + kind: "dim", + bbox: bbox_of(&[ + (e.from[0], e.from[1]), + (e.to[0], e.to[1]), + (e.from[0] + nx * e.offset, e.from[1] + ny * e.offset), + (e.to[0] + nx * e.offset, e.to[1] + ny * e.offset), + ]), + content: None, + }); + } + for e in &cf.hatches { + if let Some(pts) = resolve_boundary(&e.boundary, cf) { + out.push(EntityRecord { + id: e.common.id.clone(), + kind: "hatch", + bbox: bbox_of(&pts), + content: None, + }); + } + } + for e in &cf.fills { + let pts = if let Some(boundary_id) = &e.boundary { + resolve_boundary(boundary_id, cf) + } else { + e.points + .as_ref() + .map(|p| p.iter().map(|v| (v[0], v[1])).collect()) + }; + if let Some(pts) = pts { + out.push(EntityRecord { + id: e.common.id.clone(), + kind: "fill", + bbox: bbox_of(&pts), + content: None, + }); + } + } + out +} + +fn grid_step(world_w: f64) -> f64 { + const STEPS: &[f64] = &[0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 500.0]; + for &s in STEPS { + if world_w / s <= 40.0 { + return s; + } + } + 1000.0 +} + +fn draw_grid(c: &mut Canvas, bounds: &Bounds) { + let step = grid_step(bounds.max_x - bounds.min_x + 2.0 * PADDING); + let x0 = ((bounds.min_x - PADDING) / step).floor() * step; + let x1 = bounds.max_x + PADDING; + let y0 = ((bounds.min_y - PADDING) / step).floor() * step; + let y1 = bounds.max_y + PADDING; + + c.out.push_str(r#""#); + let mut x = x0; + while x <= x1 { + let (px, _) = c.world_to_px(x, 0.0); + let color = if x.abs() < 1e-9 { + AXIS_COLOR + } else { + GRID_COLOR + }; + let _ = write!( + c.out, + r#""#, + h = c.height_px, + ); + x += step; + } + let mut y = y0; + while y <= y1 { + let (_, py) = c.world_to_px(0.0, y); + let color = if y.abs() < 1e-9 { + AXIS_COLOR + } else { + GRID_COLOR + }; + let _ = write!( + c.out, + r#""#, + w = c.width_px, + ); + y += step; + } + c.out.push_str(""); +} + +fn render_layer(c: &mut Canvas, cf: &CfFile, layer_color: &str, default_weight: f64, units: &str) { + for e in cf.lines.iter().filter(|e| e.common.visible) { + let s = resolve_style(&e.common, layer_color, default_weight); + let (x1, y1) = c.world_to_px(e.from[0], e.from[1]); + let (x2, y2) = c.world_to_px(e.to[0], e.to[1]); + let _ = write!( + c.out, + r#""#, + x1, + y1, + x2, + y2, + stroke_attrs(&s), + id_attr(&e.common) + ); + } + + for e in cf.polylines.iter().filter(|e| e.common.visible) { + let s = resolve_style(&e.common, layer_color, default_weight); + let pts: Vec<(f64, f64)> = e.points.iter().map(|p| (p[0], p[1])).collect(); + let tag = if e.closed { "polygon" } else { "polyline" }; + let _ = write!( + c.out, + r#"<{} points="{}" {}{}/>"#, + tag, + c.points_attr(&pts), + stroke_attrs(&s), + id_attr(&e.common) + ); + } + + for e in cf.rects.iter().filter(|e| e.common.visible) { + let s = resolve_style(&e.common, layer_color, default_weight); + let (px, py) = c.world_to_px(e.origin[0], e.origin[1] + e.height); + let _ = write!( + c.out, + r#""#, + px, + py, + e.width * c.scale, + e.height * c.scale, + stroke_attrs(&s), + id_attr(&e.common) + ); + } + + for e in cf.circles.iter().filter(|e| e.common.visible) { + let s = resolve_style(&e.common, layer_color, default_weight); + let (px, py) = c.world_to_px(e.center[0], e.center[1]); + let _ = write!( + c.out, + r#""#, + px, + py, + e.radius * c.scale, + stroke_attrs(&s), + id_attr(&e.common) + ); + } + + for e in cf.arcs.iter().filter(|e| e.common.visible) { + let s = resolve_style(&e.common, layer_color, default_weight); + let pts = arc_points(e.center[0], e.center[1], e.radius, e.from_angle, e.to_angle); + let _ = write!( + c.out, + r#""#, + c.points_attr(&pts), + stroke_attrs(&s), + id_attr(&e.common) + ); + } + + // Fills and hatches go before text so labels stay readable on top. + for e in cf.fills.iter().filter(|e| e.common.visible) { + let pts = if let Some(boundary_id) = &e.boundary { + resolve_boundary(boundary_id, cf) + } else { + e.points + .as_ref() + .map(|p| p.iter().map(|v| (v[0], v[1])).collect()) + }; + if let Some(pts) = pts { + let s = resolve_style(&e.common, layer_color, default_weight); + let _ = write!( + c.out, + r#""#, + c.points_attr(&pts), + s.color, + id_attr(&e.common) + ); + } + } + + for e in cf.hatches.iter().filter(|e| e.common.visible) { + if let Some(boundary) = resolve_boundary(&e.boundary, cf) { + let s = resolve_style(&e.common, layer_color, default_weight); + draw_hatch( + c, + &boundary, + e.pattern.as_str(), + e.angle, + 0.1 * e.scale, + &s, + &e.common, + ); + } + } + + for e in cf.dims.iter().filter(|e| e.common.visible) { + let s = resolve_style(&e.common, layer_color, default_weight); + draw_dim(c, e, &s, units); + } + + for e in cf.points.iter().filter(|e| e.common.visible) { + let s = resolve_style(&e.common, layer_color, default_weight); + let (px, py) = c.world_to_px(e.position[0], e.position[1]); + let _ = write!( + c.out, + r#""#, + x0 = px - 4.0, + x1 = px + 4.0, + y0 = py - 4.0, + y1 = py + 4.0, + x = px, + y = py, + attrs = stroke_attrs(&s), + id = id_attr(&e.common) + ); + } + + for e in cf.texts.iter().filter(|e| e.common.visible) { + let s = resolve_style(&e.common, layer_color, default_weight); + let (px, py) = c.world_to_px(e.position[0], e.position[1]); + let anchor = match e.align { + Some(TextAlign::Center) => "middle", + Some(TextAlign::Right) => "end", + _ => "start", + }; + let font_px = (e.size * c.scale).max(1.0); + let _ = write!( + c.out, + r#"{}"#, + px, + py, + font_px, + anchor, + s.color, + id_attr(&e.common), + xml_escape(&e.content) + ); + } +} + +fn arc_points(cx: f64, cy: f64, radius: f64, from_deg: f64, to_deg: f64) -> Vec<(f64, f64)> { + const STEPS: usize = 48; + let start = from_deg.to_radians(); + let delta = (to_deg.to_radians() - start) / STEPS as f64; + (0..=STEPS) + .map(|i| { + let a = start + delta * i as f64; + (cx + radius * a.cos(), cy + radius * a.sin()) + }) + .collect() +} + +fn draw_dim(c: &mut Canvas, dim: &crate::model::CfDim, s: &Style, units: &str) { + let (from, to, offset) = (dim.from, dim.to, dim.offset); + let dx = to[0] - from[0]; + let dy = to[1] - from[1]; + let len = (dx * dx + dy * dy).sqrt(); + if len < 1e-9 { + return; + } + let (ux, uy) = (dx / len, dy / len); + let (nx, ny) = (-uy, ux); + + // Dimension line endpoints, offset along the normal + let a = (from[0] + nx * offset, from[1] + ny * offset); + let b = (to[0] + nx * offset, to[1] + ny * offset); + + let (ax, ay) = c.world_to_px(a.0, a.1); + let (bx, by) = c.world_to_px(b.0, b.1); + let (fx, fy) = c.world_to_px(from[0], from[1]); + let (tx, ty) = c.world_to_px(to[0], to[1]); + + let _ = write!(c.out, r#""#, id_attr(&dim.common)); + // Extension lines + dimension line + let _ = write!( + c.out, + r#""#, + color = s.color, + w = (s.width_px * 0.8).max(0.6), + ); + // Tick marks (45° slashes) at both ends + let tick = 5.0; + for &(px, py) in &[(ax, ay), (bx, by)] { + let _ = write!( + c.out, + r#""#, + px - tick, + py + tick, + px + tick, + py - tick, + s.color, + (s.width_px * 0.8).max(0.6), + ); + } + // Measured value label at the midpoint + let text_size = dim.text_size.unwrap_or(0.0); + let label_gap = if text_size > 0.0 { + text_size * 0.6 + } else { + 0.15 + }; + let mid = ( + (a.0 + b.0) / 2.0 + nx * label_gap, + (a.1 + b.1) / 2.0 + ny * label_gap, + ); + let (mx, my) = c.world_to_px(mid.0, mid.1); + let font_px = if text_size > 0.0 { + (text_size * c.scale).max(1.0) + } else { + (0.22 * c.scale).clamp(9.0, 28.0) + }; + let label = format_dim_label( + len, + dim.precision.unwrap_or(2) as usize, + dim.show_units, + units, + ); + let _ = write!( + c.out, + r#"{}"#, + mx, + my, + font_px, + s.color, + xml_escape(&label), + ); + c.out.push_str(""); +} + +/// Format a dimension label: measured value with the configured precision, +/// optionally followed by the project units. +pub fn format_dim_label( + len: f64, + precision: usize, + show_units: Option, + units: &str, +) -> String { + if show_units.unwrap_or(true) { + format!("{:.prec$} {}", len, units, prec = precision) + } else { + format!("{:.prec$}", len, prec = precision) + } +} + +fn draw_hatch( + c: &mut Canvas, + boundary: &[(f64, f64)], + pattern: &str, + angle_deg: f64, + spacing: f64, + s: &Style, + common: &CommonAttrs, +) { + if boundary.is_empty() || spacing <= 0.0 { + return; + } + + if pattern == "solid" { + let _ = write!( + c.out, + r#""#, + c.points_attr(boundary), + s.color, + id_attr(common) + ); + return; + } + + // Bounding box of the boundary + let mut b = Bounds::empty(); + for &(x, y) in boundary { + b.add(x, y); + } + let cx = (b.min_x + b.max_x) / 2.0; + let cy = (b.min_y + b.max_y) / 2.0; + let half_diag = (((b.max_x - b.min_x).powi(2) + (b.max_y - b.min_y).powi(2)).sqrt()) / 2.0; + + let theta = angle_deg.to_radians(); + let (dx, dy) = (theta.cos(), theta.sin()); + let (nx, ny) = (-dy, dx); + + let n = ((half_diag / spacing).ceil() as i64).min(2000); + + c.clip_seq += 1; + let clip_id = format!("hatch-clip-{}", c.clip_seq); + let _ = write!( + c.out, + r#""#, + clip_id, + c.points_attr(boundary) + ); + let _ = write!( + c.out, + r#""#, + clip_id, + id_attr(common) + ); + let mut path = String::new(); + for k in -n..=n { + let ox = cx + nx * spacing * k as f64; + let oy = cy + ny * spacing * k as f64; + let (x1, y1) = c.world_to_px(ox - dx * half_diag, oy - dy * half_diag); + let (x2, y2) = c.world_to_px(ox + dx * half_diag, oy + dy * half_diag); + let _ = write!(path, "M {:.2} {:.2} L {:.2} {:.2} ", x1, y1, x2, y2); + } + let _ = write!( + c.out, + r#""#, + path.trim_end(), + s.color + ); + c.out.push_str(""); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_layers() -> Vec<(String, CfFile)> { + let toml = r##" +[layer] +name = "test" +color = "#FFFFFF" + +[[line]] +id = "ln-1" +from = [0.0, 0.0] +to = [10.0, 0.0] +style = "dashed" + +[[rect]] +id = "rc-1" +origin = [1.0, 1.0] +width = 3.0 +height = 2.0 + +[[circle]] +center = [5.0, 5.0] +radius = 1.5 + +[[arc]] +center = [2.0, 2.0] +radius = 1.0 +from_angle = 0.0 +to_angle = 90.0 + +[[text]] +position = [5.0, 5.0] +content = "SALA " +size = 0.3 +align = "center" + +[[dim]] +from = [0.0, 0.0] +to = [10.0, 0.0] +offset = -0.8 + +[[polyline]] +id = "pl-room" +points = [[0.0, 0.0], [4.0, 0.0], [4.0, 3.0], [0.0, 3.0]] +closed = true + +[[hatch]] +boundary = "pl-room" +pattern = "ansi31" +"##; + let cf: CfFile = toml::from_str(toml).unwrap(); + vec![("test".to_string(), cf)] + } + + #[test] + fn renders_all_primitives() { + let svg = render_layers("demo", "m", &sample_layers(), 1200, &[]).svg; + assert!(svg.starts_with("")); + assert!(svg.contains("")); + } + + #[test] + fn dim_label_shows_measured_length() { + let svg = render_layers("demo", "m", &sample_layers(), 1200, &[]).svg; + assert!(svg.contains("10.00 m")); + } + + #[test] + fn empty_project_renders_default_viewport() { + let svg = render_layers("empty", "m", &[], 800, &[]).svg; + assert!(svg.starts_with("rc-1")); + assert!(!scene.svg.contains(r#"data-highlight="missing-id""#)); + } + + #[test] + fn enumerate_entities_covers_bounds_and_content() { + let layers = sample_layers(); + let records = enumerate_entities(&layers[0].1); + // line, rect, circle, arc, text, dim, polyline, hatch + assert_eq!(records.len(), 8); + let text = records.iter().find(|r| r.kind == "text").unwrap(); + assert_eq!(text.content.as_deref(), Some("SALA ")); + let rect = records.iter().find(|r| r.kind == "rect").unwrap(); + assert_eq!(rect.bbox, [1.0, 1.0, 4.0, 3.0]); + } + + #[test] + fn scene_world_to_px_is_consistent_with_canvas() { + let scene = render_layers("demo", "m", &sample_layers(), 1200, &[]); + // Bottom-left content corner with padding maps inside the canvas + let (px, py) = scene.world_to_px(scene.world_bounds[0], scene.world_bounds[1]); + assert!(px > 0.0 && px < scene.width_px); + assert!(py > 0.0 && py <= scene.height_px); + } + + #[test] + fn grid_step_scales_with_world_size() { + assert_eq!(grid_step(10.0), 0.5); + assert_eq!(grid_step(30.0), 1.0); + assert_eq!(grid_step(300.0), 10.0); + } +} From 74ed01a23b0ca25d049e70885ca4d65d83c728b7 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:28:38 -0500 Subject: [PATCH 03/16] feat(preview): rasterize PNG previews from the SVG scene with an embedded monospace font and richer agent metadata --- Cargo.toml | 8 +- assets/fonts/DejaVuSansMono.ttf | Bin 0 -> 343140 bytes assets/fonts/LICENSE-DejaVuSansMono.txt | 78 +++ src/preview.rs | 641 +++++++----------------- 4 files changed, 255 insertions(+), 472 deletions(-) create mode 100644 assets/fonts/DejaVuSansMono.ttf create mode 100644 assets/fonts/LICENSE-DejaVuSansMono.txt diff --git a/Cargo.toml b/Cargo.toml index 3a5e78a..6ce5be2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,5 +18,11 @@ serde_json = "1.0" toml = "0.8" toml_edit = "0.22" indexmap = { version = "2", features = ["serde"] } -tiny-skia = "0.11" notify = "6.1" +resvg = "0.47.0" + +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 1 +strip = "symbols" diff --git a/assets/fonts/DejaVuSansMono.ttf b/assets/fonts/DejaVuSansMono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..538ee272366e7b7c7c00e1151f6fa61671ba601b GIT binary patch literal 343140 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%0DBl$}y?@Gs3DIlgd9MO!}06MwskU{uyCa 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 W(EJNtGW3N1KYv^PkDtmTxBL%2+~ooQ literal 0 HcmV?d00001 diff --git a/assets/fonts/LICENSE-DejaVuSansMono.txt b/assets/fonts/LICENSE-DejaVuSansMono.txt new file mode 100644 index 0000000..b3d93a1 --- /dev/null +++ b/assets/fonts/LICENSE-DejaVuSansMono.txt @@ -0,0 +1,78 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: DejaVu fonts +Upstream-Author: Stepan Roh (original author), + see /usr/share/doc/fonts-dejavu-core/AUTHORS for full list +Source: https://dejavu-fonts.github.io/ + +Files: * +Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. + Bitstream Vera is a trademark of Bitstream, Inc. + DejaVu changes are in public domain. +License: bitstream-vera + Permission is hereby granted, free of charge, to any person obtaining a copy + of the fonts accompanying this license ("Fonts") and associated + documentation files (the "Font Software"), to reproduce and distribute the + Font Software, including without limitation the rights to use, copy, merge, + publish, distribute, and/or sell copies of the Font Software, and to permit + persons to whom the Font Software is furnished to do so, subject to the + following conditions: + . + The above copyright and trademark notices and this permission notice shall + be included in all copies of one or more of the Font Software typefaces. + . + The Font Software may be modified, altered, or added to, and in particular + the designs of glyphs or characters in the Fonts may be modified and + additional glyphs or characters may be added to the Fonts, only if the fonts + are renamed to names not containing either the words "Bitstream" or the word + "Vera". + . + This License becomes null and void to the extent applicable to Fonts or Font + Software that has been modified and is distributed under the "Bitstream + Vera" names. + . + The Font Software may be sold as part of a larger software package but no + copy of one or more of the Font Software typefaces may be sold by itself. + . + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, + TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME + FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING + ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF + THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE + FONT SOFTWARE. + . + Except as contained in this notice, the names of Gnome, the Gnome + Foundation, and Bitstream Inc., shall not be used in advertising or + otherwise to promote the sale, use or other dealings in this Font Software + without prior written authorization from the Gnome Foundation or Bitstream + Inc., respectively. For further information, contact: fonts at gnome dot + org. + +Files: debian/* +Copyright: (C) 2005-2006 Peter Cernak + (C) 2006-2011 Davide Viti + (C) 2011-2013 Christian Perrier + (C) 2013 Fabian Greffrath +License: GPL-2+ + This program is free software; you can redistribute it + and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later + version. + . + This program is distributed in the hope that it will be + useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more + details. + . + You should have received a copy of the GNU General Public + License along with this package; if not, write to the Free + Software Foundation, Inc., 51 Franklin St, Fifth Floor, + Boston, MA 02110-1301 USA + . + On Debian systems, the full text of the GNU General Public + License version 2 can be found in the file + /usr/share/common-licenses/GPL-2'. diff --git a/src/preview.rs b/src/preview.rs index 370b225..da7f5ad 100644 --- a/src/preview.rs +++ b/src/preview.rs @@ -1,24 +1,22 @@ -//! Preview — renders project to PNG + metadata JSON for multimodal AI agents. - -use crate::compiler::resolve_boundary; -use crate::model::{CfFile, CommonAttrs}; -use crate::parser::{parse_cf, parse_project}; +//! Preview — rasterizes the SVG scene to PNG + metadata JSON for multimodal AI agents. +//! +//! The PNG is a faithful raster of the SVG renderer (real text, measured +//! dimensions, hatches, line styles), so what an agent *sees* in the image is +//! exactly what `cadforge serve` shows a human. The metadata JSON maps every +//! entity to world and pixel bounding boxes so agents can locate geometry in +//! the image. + +use crate::model::CfFile; +use crate::svg::{ + enumerate_entities, layer_display_color, load_project_layers, render_scene_from, Scene, +}; use anyhow::{Context, Result}; +use resvg::{tiny_skia, usvg}; use serde::Serialize; use std::path::Path; -use tiny_skia::{Color, Paint, PathBuilder, Pixmap, Stroke, Transform}; - -// ── Configuration ─────────────────────────────────────────────────────── - -const PADDING: f64 = 0.5; // world units padding around content -const STROKE_WIDTH: f32 = 1.5; -const TEXT_MARKER: f64 = 0.05; - -fn bg_color() -> Color { - Color::from_rgba8(20, 20, 20, 255) -} +use std::sync::{Arc, OnceLock}; -// ── Bounds accumulator (DRY: one place to track min/max) ──────────────── +// ── Metadata structures (for the agent) ───────────────────────────────── #[derive(Serialize, Clone, Copy)] pub struct WorldBounds { @@ -28,43 +26,6 @@ pub struct WorldBounds { pub max_y: f64, } -impl WorldBounds { - fn empty() -> Self { - Self { - min_x: f64::MAX, - min_y: f64::MAX, - max_x: f64::MIN, - max_y: f64::MIN, - } - } - - fn add(&mut self, x: f64, y: f64) { - self.min_x = self.min_x.min(x); - self.min_y = self.min_y.min(y); - self.max_x = self.max_x.max(x); - self.max_y = self.max_y.max(y); - } - - fn is_empty(&self) -> bool { - self.min_x > self.max_x - } - - fn as_bbox(&self) -> [f64; 4] { - [self.min_x, self.min_y, self.max_x, self.max_y] - } -} - -/// Compute the bounding box of a set of points. -fn points_bounds(points: &[(f64, f64)]) -> WorldBounds { - let mut b = WorldBounds::empty(); - for &(x, y) in points { - b.add(x, y); - } - b -} - -// ── Metadata structures (for the agent) ───────────────────────────────── - #[derive(Serialize)] pub struct PreviewMeta { pub project_name: String, @@ -72,9 +33,12 @@ pub struct PreviewMeta { pub width_px: u32, pub height_px: u32, pub world_bounds: WorldBounds, + /// Pixels per world unit in the output image. pub scale: f64, + pub units: String, pub layers: Vec, pub entities: Vec, + pub highlighted: Vec, } #[derive(Serialize)] @@ -89,445 +53,182 @@ pub struct EntityInfo { pub id: Option, pub entity_type: String, pub layer: String, + /// Text content (only for `text` entities). + pub content: Option, pub bbox: [f64; 4], // [min_x, min_y, max_x, max_y] in world coords pub pixel_bbox: [u32; 4], // [x, y, w, h] in image coords } -// ── Renderer ──────────────────────────────────────────────────────────── - -struct Renderer { - pixmap: Pixmap, - scale: f64, - offset_x: f64, - offset_y: f64, - world_height: f64, -} - -impl Renderer { - fn new(width: u32, height: u32, bounds: &WorldBounds) -> Result { - let world_w = bounds.max_x - bounds.min_x + 2.0 * PADDING; - let world_h = bounds.max_y - bounds.min_y + 2.0 * PADDING; - - let scale = (width as f64 / world_w).min(height as f64 / world_h); - - let mut pixmap = Pixmap::new(width, height) - .ok_or_else(|| anyhow::anyhow!("Invalid image dimensions {}x{}", width, height))?; - pixmap.fill(bg_color()); - - Ok(Self { - pixmap, - scale, - offset_x: bounds.min_x - PADDING, - offset_y: bounds.min_y - PADDING, - world_height: world_h, - }) - } - - fn world_to_px(&self, x: f64, y: f64) -> (f32, f32) { - let px = ((x - self.offset_x) * self.scale) as f32; - // Flip Y: world Y goes up, pixel Y goes down - let py = ((self.world_height - (y - self.offset_y)) * self.scale) as f32; - (px, py) - } - - /// Single stroke entry point — all draw_* methods funnel through here (DRY). - fn stroke(&mut self, path: tiny_skia::Path, color: Color, width: f32) { - let mut paint = Paint::default(); - paint.set_color(color); - paint.anti_alias = true; - let stroke = Stroke { - width, - ..Default::default() - }; - self.pixmap - .stroke_path(&path, &paint, &stroke, Transform::identity(), None); - } - - fn draw_line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, color: Color, width: f32) { - let (px1, py1) = self.world_to_px(x1, y1); - let (px2, py2) = self.world_to_px(x2, y2); - let mut pb = PathBuilder::new(); - pb.move_to(px1, py1); - pb.line_to(px2, py2); - if let Some(path) = pb.finish() { - self.stroke(path, color, width); - } - } - - fn draw_circle(&mut self, cx: f64, cy: f64, radius: f64, color: Color, width: f32) { - let (pcx, pcy) = self.world_to_px(cx, cy); - let pr = (radius * self.scale) as f32; - let mut pb = PathBuilder::new(); - pb.push_circle(pcx, pcy, pr); - if let Some(path) = pb.finish() { - self.stroke(path, color, width); - } - } - - fn draw_arc(&mut self, arc: ArcSpec, color: Color, width: f32) { - const STEPS: usize = 32; - let start = arc.start_deg.to_radians(); - let delta = (arc.end_deg.to_radians() - start) / STEPS as f64; - - let mut pb = PathBuilder::new(); - for i in 0..=STEPS { - let angle = start + delta * i as f64; - let (px, py) = self.world_to_px( - arc.cx + arc.radius * angle.cos(), - arc.cy + arc.radius * angle.sin(), - ); - if i == 0 { - pb.move_to(px, py); - } else { - pb.line_to(px, py); - } - } - if let Some(path) = pb.finish() { - self.stroke(path, color, width); - } - } - - fn draw_polyline(&mut self, points: &[(f64, f64)], closed: bool, color: Color, width: f32) { - let Some((first, rest)) = points.split_first() else { - return; - }; - let mut pb = PathBuilder::new(); - let (px, py) = self.world_to_px(first.0, first.1); - pb.move_to(px, py); - for &(x, y) in rest { - let (px, py) = self.world_to_px(x, y); - pb.line_to(px, py); - } - if closed { - pb.close(); - } - if let Some(path) = pb.finish() { - self.stroke(path, color, width); - } - } - - fn fill_polygon(&mut self, points: &[(f64, f64)], color: Color) { - let Some((first, rest)) = points.split_first() else { - return; - }; - let mut pb = PathBuilder::new(); - let (px, py) = self.world_to_px(first.0, first.1); - pb.move_to(px, py); - for &(x, y) in rest { - let (px, py) = self.world_to_px(x, y); - pb.line_to(px, py); - } - pb.close(); - if let Some(path) = pb.finish() { - let mut paint = Paint::default(); - // Semi-transparent fill so underlying geometry stays visible - paint.set_color( - Color::from_rgba(color.red(), color.green(), color.blue(), 0.35).unwrap(), - ); - paint.anti_alias = true; - self.pixmap.fill_path( - &path, - &paint, - tiny_skia::FillRule::Winding, - Transform::identity(), - None, - ); - } - } - - fn save_png(&self, path: &Path) -> Result<()> { - self.pixmap - .save_png(path) - .map_err(|e| anyhow::anyhow!("Failed to save PNG: {}", e)) - } - - fn entity_info( - &self, - common: &CommonAttrs, - entity_type: &str, - layer: &str, - bounds: WorldBounds, - ) -> EntityInfo { - let (px1, py1) = self.world_to_px(bounds.min_x, bounds.max_y); // top-left - let (px2, py2) = self.world_to_px(bounds.max_x, bounds.min_y); // bottom-right - EntityInfo { - id: common.id.clone(), - entity_type: entity_type.to_string(), - layer: layer.to_string(), - bbox: bounds.as_bbox(), - pixel_bbox: [ - px1 as u32, - py1 as u32, - (px2 - px1) as u32, - (py2 - py1) as u32, - ], - } - } -} +// ── Public API ────────────────────────────────────────────────────────── +/// Which preview artifacts to write. #[derive(Clone, Copy)] -struct ArcSpec { - cx: f64, - cy: f64, - radius: f64, - start_deg: f64, - end_deg: f64, -} - -// ── Layer color mapping ───────────────────────────────────────────────── - -fn layer_color(index: usize) -> Color { - const PALETTE: &[(u8, u8, u8)] = &[ - (255, 255, 255), // white - (255, 80, 80), // red - (80, 255, 80), // green - (80, 200, 255), // cyan - (255, 200, 80), // yellow - (200, 120, 255), // purple - (255, 150, 50), // orange - ]; - let (r, g, b) = PALETTE[index % PALETTE.len()]; - Color::from_rgba8(r, g, b, 255) +pub struct PreviewOutputs { + /// Write `preview.png` + `preview.meta.json`. + pub png: bool, + /// Write `preview.svg`. + pub svg: bool, } -fn color_to_hex(c: Color) -> String { - format!( - "#{:02X}{:02X}{:02X}", - (c.red() * 255.0) as u8, - (c.green() * 255.0) as u8, - (c.blue() * 255.0) as u8, - ) -} - -// ── Public API ────────────────────────────────────────────────────────── - -/// Generate a preview PNG + metadata JSON for the project. +/// Generate preview artifacts (PNG + metadata JSON, and/or SVG) for the project. +/// +/// Everything is produced from a single parse + render pass. +/// `width`/`height` are treated as a bounding box: the image keeps the +/// project's aspect ratio and fits inside it. pub fn generate_preview( project_dir: &Path, width: u32, height: u32, layer_filter: Option<&str>, + highlight: &[String], + outputs: PreviewOutputs, ) -> Result<()> { - let project = parse_project(&project_dir.join("project.toml"))?; - - // Parse all layer files once - let layers: Vec<(String, CfFile)> = project - .layers - .iter() - .filter(|(name, _)| layer_filter.is_none_or(|f| f == *name)) - .map(|(name, entry)| { - let cf = parse_cf(&project_dir.join(&entry.file)) - .with_context(|| format!("Failed to parse layer '{}'", name))?; - Ok((name.clone(), cf)) - }) - .collect::>()?; - - let bounds = compute_bounds(&layers); - let mut renderer = Renderer::new(width, height, &bounds)?; - let mut entities: Vec = Vec::new(); - let mut layer_infos: Vec = Vec::new(); + let (project, layers) = load_project_layers(project_dir, layer_filter)?; + let scene = render_scene_from( + &project.project.name, + &project.project.units, + &layers, + width, + highlight, + ); + + if outputs.svg { + let svg_path = project_dir.join("preview.svg"); + std::fs::write(&svg_path, &scene.svg) + .with_context(|| format!("Cannot write {}", svg_path.display()))?; + println!("✓ SVG: {}", svg_path.display()); + } - for (idx, (layer_name, cf)) in layers.iter().enumerate() { - let color = layer_color(idx); - let count = render_layer(&mut renderer, cf, layer_name, color, &mut entities); - layer_infos.push(LayerInfo { - name: layer_name.clone(), - entity_count: count, - color: color_to_hex(color), - }); + if !outputs.png { + return Ok(()); } - let png_path = project_dir.join("preview.png"); - renderer.save_png(&png_path)?; - - let meta = PreviewMeta { - project_name: project.project.name, - image_file: "preview.png".to_string(), - width_px: width, - height_px: height, - world_bounds: bounds, - scale: renderer.scale, - layers: layer_infos, - entities, - }; + // Fit the scene inside the requested width × height box. + let fit = (height as f64 / scene.height_px).min(1.0); + let pixmap = rasterize(&scene.svg, fit as f32)?; + let png_path = project_dir.join("preview.png"); + pixmap + .save_png(&png_path) + .map_err(|e| anyhow::anyhow!("Failed to save PNG: {}", e))?; + + let meta = build_meta( + &project.project.name, + &project.project.units, + &layers, + &scene, + fit, + highlight, + &pixmap, + ); let json_path = project_dir.join("preview.meta.json"); std::fs::write(&json_path, serde_json::to_string_pretty(&meta)?)?; - println!("✓ Preview: {} ({}x{})", png_path.display(), width, height); + println!( + "✓ Preview: {} ({}x{})", + png_path.display(), + pixmap.width(), + pixmap.height() + ); println!("✓ Metadata: {}", json_path.display()); Ok(()) } -// ── Rendering per layer ────────────────────────────────────────────────── - -fn render_layer( - r: &mut Renderer, - cf: &CfFile, - layer: &str, - color: Color, - out: &mut Vec, -) -> usize { - let mut count = 0; - - for e in &cf.lines { - r.draw_line(e.from[0], e.from[1], e.to[0], e.to[1], color, STROKE_WIDTH); - let bounds = points_bounds(&[(e.from[0], e.from[1]), (e.to[0], e.to[1])]); - out.push(r.entity_info(&e.common, "line", layer, bounds)); - count += 1; - } - - for e in &cf.polylines { - let pts: Vec<(f64, f64)> = e.points.iter().map(|p| (p[0], p[1])).collect(); - r.draw_polyline(&pts, e.closed, color, STROKE_WIDTH); - out.push(r.entity_info(&e.common, "polyline", layer, points_bounds(&pts))); - count += 1; - } - - for e in &cf.rects { - let pts = rect_points(e.origin[0], e.origin[1], e.width, e.height); - r.draw_polyline(&pts, true, color, STROKE_WIDTH); - out.push(r.entity_info(&e.common, "rect", layer, points_bounds(&pts))); - count += 1; - } - - for e in &cf.circles { - r.draw_circle(e.center[0], e.center[1], e.radius, color, STROKE_WIDTH); - out.push(r.entity_info( - &e.common, - "circle", - layer, - circle_bounds(e.center, e.radius), - )); - count += 1; - } - - for e in &cf.arcs { - r.draw_arc( - ArcSpec { - cx: e.center[0], - cy: e.center[1], - radius: e.radius, - start_deg: e.from_angle, - end_deg: e.to_angle, - }, - color, - STROKE_WIDTH, - ); - out.push(r.entity_info(&e.common, "arc", layer, circle_bounds(e.center, e.radius))); - count += 1; - } - - for e in &cf.texts { - // Render text position as a small marker - r.draw_line( - e.position[0] - TEXT_MARKER, - e.position[1], - e.position[0] + TEXT_MARKER, - e.position[1], - color, - 1.0, - ); - let mut b = WorldBounds::empty(); - b.add(e.position[0], e.position[1]); - b.add(e.position[0] + 0.5, e.position[1] + 0.2); - out.push(r.entity_info(&e.common, "text", layer, b)); - count += 1; - } +// ── Internal ──────────────────────────────────────────────────────────── - // Solid fills — render as semi-transparent filled polygons - for e in &cf.fills { - let pts = fill_points(e, cf); - if let Some(pts) = pts { - r.fill_polygon(&pts, color); - out.push(r.entity_info(&e.common, "fill", layer, points_bounds(&pts))); - count += 1; - } - } +fn build_meta( + project_name: &str, + units: &str, + layers: &[(String, CfFile)], + scene: &Scene, + fit: f64, + highlight: &[String], + pixmap: &tiny_skia::Pixmap, +) -> PreviewMeta { + let mut entities = Vec::new(); + let mut layer_infos = Vec::new(); - // Hatches — render boundary outline (pattern detail omitted in preview) - for e in &cf.hatches { - if let Some(pts) = resolve_boundary(&e.boundary, cf) { - r.draw_polyline(&pts, true, color, 1.0); - out.push(r.entity_info(&e.common, "hatch", layer, points_bounds(&pts))); - count += 1; + for (idx, (layer_name, cf)) in layers.iter().enumerate() { + let records = enumerate_entities(cf); + layer_infos.push(LayerInfo { + name: layer_name.clone(), + entity_count: records.len(), + color: layer_display_color(cf, idx), + }); + for rec in records { + let (x1, y1) = scene.world_to_px(rec.bbox[0], rec.bbox[3]); // top-left + let (x2, y2) = scene.world_to_px(rec.bbox[2], rec.bbox[1]); // bottom-right + entities.push(EntityInfo { + id: rec.id, + entity_type: rec.kind.to_string(), + layer: layer_name.clone(), + content: rec.content, + bbox: rec.bbox, + pixel_bbox: [ + (x1 * fit) as u32, + (y1 * fit) as u32, + ((x2 - x1) * fit) as u32, + ((y2 - y1) * fit) as u32, + ], + }); } } - count -} - -/// Resolve a fill's geometry from inline points or a boundary reference. -fn fill_points(e: &crate::model::CfFill, cf: &CfFile) -> Option> { - if let Some(boundary_id) = &e.boundary { - resolve_boundary(boundary_id, cf) - } else { - e.points - .as_ref() - .map(|p| p.iter().map(|v| (v[0], v[1])).collect()) + PreviewMeta { + project_name: project_name.to_string(), + image_file: "preview.png".to_string(), + width_px: pixmap.width(), + height_px: pixmap.height(), + world_bounds: WorldBounds { + min_x: scene.world_bounds[0], + min_y: scene.world_bounds[1], + max_x: scene.world_bounds[2], + max_y: scene.world_bounds[3], + }, + scale: scene.px_per_unit * fit, + units: units.to_string(), + layers: layer_infos, + entities, + highlighted: highlight.to_vec(), } } -// ── Geometry helpers ───────────────────────────────────────────────────── - -fn rect_points(x: f64, y: f64, w: f64, h: f64) -> [(f64, f64); 4] { - [(x, y), (x + w, y), (x + w, y + h), (x, y + h)] -} - -fn circle_bounds(center: [f64; 2], radius: f64) -> WorldBounds { - let mut b = WorldBounds::empty(); - b.add(center[0] - radius, center[1] - radius); - b.add(center[0] + radius, center[1] + radius); - b +/// Embedded monospace font: zero font-scan latency and identical, deterministic +/// text rendering on any machine — including containers with no fonts at all. +const EMBEDDED_FONT: &[u8] = include_bytes!("../assets/fonts/DejaVuSansMono.ttf"); + +fn fontdb() -> Arc { + static FONTDB: OnceLock> = OnceLock::new(); + FONTDB + .get_or_init(|| { + let mut db = usvg::fontdb::Database::new(); + db.load_font_data(EMBEDDED_FONT.to_vec()); + db.set_monospace_family("DejaVu Sans Mono"); + Arc::new(db) + }) + .clone() } -fn compute_bounds(layers: &[(String, CfFile)]) -> WorldBounds { - let mut b = WorldBounds::empty(); - - for (_, cf) in layers { - for e in &cf.lines { - b.add(e.from[0], e.from[1]); - b.add(e.to[0], e.to[1]); - } - for e in &cf.polylines { - for p in &e.points { - b.add(p[0], p[1]); - } - } - for e in &cf.rects { - b.add(e.origin[0], e.origin[1]); - b.add(e.origin[0] + e.width, e.origin[1] + e.height); - } - for e in &cf.circles { - b.add(e.center[0] - e.radius, e.center[1] - e.radius); - b.add(e.center[0] + e.radius, e.center[1] + e.radius); - } - for e in &cf.arcs { - b.add(e.center[0] - e.radius, e.center[1] - e.radius); - b.add(e.center[0] + e.radius, e.center[1] + e.radius); - } - for e in &cf.texts { - b.add(e.position[0], e.position[1]); - } - for e in &cf.fills { - if let Some(points) = &e.points { - for p in points { - b.add(p[0], p[1]); - } - } - } - } +/// Rasterize an SVG string at the given scale factor. +fn rasterize(svg: &str, scale: f32) -> Result { + let opt = usvg::Options { + fontdb: fontdb(), + ..Default::default() + }; - if b.is_empty() { - WorldBounds { - min_x: 0.0, - min_y: 0.0, - max_x: 10.0, - max_y: 10.0, - } - } else { - b - } + let tree = usvg::Tree::from_str(svg, &opt).context("Failed to parse generated SVG")?; + let size = tree.size(); + let w = ((size.width() * scale).ceil() as u32).max(1); + let h = ((size.height() * scale).ceil() as u32).max(1); + + let mut pixmap = tiny_skia::Pixmap::new(w, h) + .ok_or_else(|| anyhow::anyhow!("Invalid image dimensions {}x{}", w, h))?; + resvg::render( + &tree, + tiny_skia::Transform::from_scale(scale, scale), + &mut pixmap.as_mut(), + ); + Ok(pixmap) } #[cfg(test)] @@ -535,23 +236,21 @@ mod tests { use super::*; #[test] - fn bounds_accumulates_correctly() { - let mut b = WorldBounds::empty(); - assert!(b.is_empty()); - b.add(1.0, 2.0); - b.add(5.0, -1.0); - assert_eq!(b.as_bbox(), [1.0, -1.0, 5.0, 2.0]); - assert!(!b.is_empty()); - } + fn rasterize_produces_scaled_pixmap() { + let svg = r##""##; + let pixmap = rasterize(svg, 1.0).unwrap(); + assert_eq!((pixmap.width(), pixmap.height()), (200, 100)); - #[test] - fn circle_bounds_is_square() { - let b = circle_bounds([5.0, 5.0], 2.0); - assert_eq!(b.as_bbox(), [3.0, 3.0, 7.0, 7.0]); + let half = rasterize(svg, 0.5).unwrap(); + assert_eq!((half.width(), half.height()), (100, 50)); + + // Background must be painted (not transparent) + let px = pixmap.pixel(5, 50).unwrap(); + assert!(px.alpha() == 255); } #[test] - fn color_to_hex_formats() { - assert_eq!(color_to_hex(Color::from_rgba8(255, 0, 128, 255)), "#FF0080"); + fn rasterize_rejects_invalid_svg() { + assert!(rasterize("not an svg", 1.0).is_err()); } } From 0bd540585f7e980a403872d2128f37965c328da6 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:28:49 -0500 Subject: [PATCH 04/16] feat(serve): add live preview server with SSE auto-reload, entity inspector, layer ghost states and 3D stacked view --- src/serve.rs | 849 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 849 insertions(+) create mode 100644 src/serve.rs diff --git a/src/serve.rs b/src/serve.rs new file mode 100644 index 0000000..27d86cf --- /dev/null +++ b/src/serve.rs @@ -0,0 +1,849 @@ +//! Live preview server — `cadforge serve`. +//! +//! Watches the project files and serves an auto-reloading SVG preview in the +//! browser. The vibecoding loop: an agent (or human) edits `.cf` files, the +//! browser refreshes instantly, build errors show as an overlay. +//! +//! Viewer features: pan/zoom, click-to-inspect any entity (shows its source +//! TOML block, copyable for targeted agent edits), per-layer visibility with +//! a ghost mode for tracing over other floors, and a 3D stacked-layers view. +//! +//! Plain `std::net` HTTP — this is a localhost dev server, no framework needed. + +use crate::parser::parse_project; +use crate::svg::{layer_display_color, load_project_layers, render_scene_from}; +use anyhow::{Context, Result}; +use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +use std::io::{BufRead, BufReader, Write}; +use std::net::{TcpListener, TcpStream}; +use std::path::{Path, PathBuf}; +use std::sync::mpsc; +use std::sync::{Arc, Condvar, Mutex}; +use std::time::Duration; + +const SVG_WIDTH: u32 = 1600; +const DEBOUNCE: Duration = Duration::from_millis(80); +const SSE_KEEPALIVE: Duration = Duration::from_secs(15); + +struct LiveState { + /// Arc so request handlers serve the SVG without copying it. + svg: Arc, + error: Option, + version: u64, + project_name: String, + /// (name, color) per layer, for the layer panel. + layers: Vec<(String, String)>, +} + +/// Shared state plus a condvar so SSE clients are woken the instant a rebuild +/// lands, instead of polling. +struct Live { + state: Mutex, + changed: Condvar, + project_dir: PathBuf, +} + +type Shared = Arc; + +/// Start the live preview server (blocks until killed). +pub fn serve_project(project_dir: &Path, port: u16, open: bool) -> Result<()> { + let project = parse_project(&project_dir.join("project.toml"))?; + let project_dir = project_dir + .canonicalize() + .unwrap_or_else(|_| project_dir.to_path_buf()); + + let state: Shared = Arc::new(Live { + state: Mutex::new(LiveState { + svg: Arc::new(String::new()), + error: None, + version: 0, + project_name: project.project.name.clone(), + layers: Vec::new(), + }), + changed: Condvar::new(), + project_dir: project_dir.clone(), + }); + + rebuild(&project_dir, &state); + + let listener = TcpListener::bind(("127.0.0.1", port)) + .with_context(|| format!("Cannot bind 127.0.0.1:{} (port in use?)", port))?; + let url = format!("http://127.0.0.1:{}", port); + + println!("◉ cadforge serve — {}", project.project.name); + println!(" Preview: {}", url); + println!(" Watching: {}", project_dir.display()); + println!(); + println!(" Edit .cf files — the browser updates automatically."); + println!(" Click an entity in the viewer to inspect/copy its TOML."); + println!(" Press Ctrl+C to stop."); + + spawn_watcher(project_dir.clone(), Arc::clone(&state))?; + + if open { + open_browser(&url); + } + + for stream in listener.incoming() { + let Ok(stream) = stream else { continue }; + let state = Arc::clone(&state); + std::thread::spawn(move || { + let _ = handle_connection(stream, &state); + }); + } + Ok(()) +} + +fn rebuild(project_dir: &Path, state: &Shared) { + let result = (|| -> Result<(String, Vec<(String, String)>)> { + let (project, layers) = load_project_layers(project_dir, None)?; + let scene = render_scene_from( + &project.project.name, + &project.project.units, + &layers, + SVG_WIDTH, + &[], + ); + let layer_info = layers + .iter() + .enumerate() + .map(|(i, (name, cf))| (name.clone(), layer_display_color(cf, i))) + .collect(); + Ok((scene.svg, layer_info)) + })(); + + let mut st = state.state.lock().unwrap(); + match result { + Ok((svg, layers)) => { + st.svg = Arc::new(svg); + st.layers = layers; + st.error = None; + } + Err(e) => { + st.error = Some(format!("{:#}", e)); + } + } + st.version += 1; + drop(st); + state.changed.notify_all(); +} + +fn spawn_watcher(project_dir: PathBuf, state: Shared) -> Result<()> { + let (tx, rx) = mpsc::channel(); + let mut watcher = RecommendedWatcher::new( + move |res: Result| { + if let Ok(event) = res { + let _ = tx.send(event); + } + }, + notify::Config::default(), + )?; + watcher.watch(&project_dir, RecursiveMode::NonRecursive)?; + + std::thread::spawn(move || { + // Keep the watcher alive inside the thread. + let _watcher = watcher; + while let Ok(event) = rx.recv() { + if !is_relevant(&event) { + continue; + } + // Debounce: absorb the burst of events an editor save produces. + std::thread::sleep(DEBOUNCE); + while rx.try_recv().is_ok() {} + + rebuild(&project_dir, &state); + let st = state.state.lock().unwrap(); + match &st.error { + None => println!("⟳ rebuilt (v{})", st.version), + Some(e) => println!("✗ build error (v{}): {}", st.version, e), + } + } + }); + Ok(()) +} + +fn is_relevant(event: &Event) -> bool { + match event.kind { + EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {} + _ => return false, + } + event.paths.iter().any(|p| { + p.extension() + .and_then(|e| e.to_str()) + .is_some_and(|e| e == "cf" || e == "toml") + }) +} + +// ── HTTP ──────────────────────────────────────────────────────────────── + +fn handle_connection(stream: TcpStream, state: &Shared) -> std::io::Result<()> { + // Small localhost responses: Nagle's algorithm only adds latency here. + let _ = stream.set_nodelay(true); + let mut reader = BufReader::new(stream.try_clone()?); + let mut request_line = String::new(); + reader.read_line(&mut request_line)?; + + let target = request_line.split_whitespace().nth(1).unwrap_or("/"); + let (path, query) = match target.split_once('?') { + Some((p, q)) => (p, q), + None => (target, ""), + }; + + match path { + "/" => { + let html = index_html(&state.state.lock().unwrap().project_name); + respond( + stream, + "200 OK", + "text/html; charset=utf-8", + html.as_bytes(), + ) + } + "/preview.svg" => { + let svg = Arc::clone(&state.state.lock().unwrap().svg); + respond(stream, "200 OK", "image/svg+xml", svg.as_bytes()) + } + "/state" => { + let st = state.state.lock().unwrap(); + let layers: Vec<_> = st + .layers + .iter() + .map(|(name, color)| serde_json::json!({"name": name, "color": color})) + .collect(); + let body = serde_json::json!({ + "version": st.version, + "project": st.project_name, + "error": st.error, + "layers": layers, + }) + .to_string(); + drop(st); + respond(stream, "200 OK", "application/json", body.as_bytes()) + } + "/entity" => { + let id = query_param(query, "id").unwrap_or_default(); + let body = entity_block_json(&state.project_dir, &id); + respond(stream, "200 OK", "application/json", body.as_bytes()) + } + "/events" => serve_events(stream, state), + _ => respond(stream, "404 Not Found", "text/plain", b"not found"), + } +} + +fn query_param(query: &str, key: &str) -> Option { + query.split('&').find_map(|pair| { + let (k, v) = pair.split_once('=')?; + (k == key).then(|| percent_decode(v)) + }) +} + +fn percent_decode(s: &str) -> String { + let bytes = s.as_bytes(); + let mut out = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + match bytes[i] { + b'%' if i + 2 < bytes.len() => { + if let (Some(h), Some(l)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) { + out.push(h * 16 + l); + i += 3; + } else { + out.push(b'%'); + i += 1; + } + } + b'+' => { + out.push(b' '); + i += 1; + } + b => { + out.push(b); + i += 1; + } + } + } + String::from_utf8_lossy(&out).into_owned() +} + +fn hex_val(b: u8) -> Option { + match b { + b'0'..=b'9' => Some(b - b'0'), + b'a'..=b'f' => Some(b - b'a' + 10), + b'A'..=b'F' => Some(b - b'A' + 10), + _ => None, + } +} + +/// Find the raw TOML block that defines `id` and return it as JSON, so the +/// viewer can hand an agent the exact source to edit. +fn entity_block_json(project_dir: &Path, id: &str) -> String { + // Generated copies (array/mirror) carry an @ suffix; their source is the base id. + let base = id.split('@').next().unwrap_or(id); + + let lookup = || -> Option<(String, String, String)> { + let project = parse_project(&project_dir.join("project.toml")).ok()?; + for (layer, entry) in &project.layers { + let path = project_dir.join(&entry.file); + let Ok(text) = std::fs::read_to_string(&path) else { + continue; + }; + if let Some(block) = find_block(&text, base) { + return Some((layer.clone(), entry.file.clone(), block)); + } + } + None + }; + + match lookup() { + Some((layer, file, block)) => serde_json::json!({ + "id": id, + "base_id": base, + "generated": id != base, + "layer": layer, + "file": file, + "block": block, + }) + .to_string(), + None => serde_json::json!({ "id": id, "error": "not found" }).to_string(), + } +} + +/// Extract the `[[...]]` block (with leading comments) that contains `id = ""`. +/// +/// Uses toml_edit spans, so multi-line values (e.g. `points = [` …) are kept +/// intact instead of being cut at lines that merely look like TOML headers. +fn find_block(text: &str, id: &str) -> Option { + let doc = toml_edit::ImDocument::parse(text).ok()?; + let mut span: Option> = None; + for (_key, item) in doc.iter() { + if let toml_edit::Item::ArrayOfTables(tables) = item { + for table in tables.iter() { + if table.get("id").and_then(|v| v.as_str()) == Some(id) { + span = table.span(); + } + } + } + } + let span = span?; + + let lines: Vec<&str> = text.lines().collect(); + let span_start_line = text[..span.start.min(text.len())].matches('\n').count(); + let span_end_line = text[..span.end.min(text.len())] + .matches('\n') + .count() + .min(lines.len().saturating_sub(1)); + + // The span covers the key/value pairs; step back to the [[header]] line + // and pull in any comment lines directly above it. + let header = lines[..=span_start_line.min(lines.len().saturating_sub(1))] + .iter() + .rposition(|l| l.trim_start().starts_with("[["))?; + let mut start = header; + while start > 0 && lines[start - 1].trim_start().starts_with('#') { + start -= 1; + } + + Some( + lines[start..=span_end_line] + .join("\n") + .trim_end() + .to_string(), + ) +} + +fn respond( + mut stream: TcpStream, + status: &str, + content_type: &str, + body: &[u8], +) -> std::io::Result<()> { + write!( + stream, + "HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n", + status, + content_type, + body.len() + )?; + stream.write_all(body)?; + stream.flush() +} + +/// Server-sent events: the condvar wakes us the instant a rebuild lands, so +/// the browser is notified with sub-millisecond latency instead of polling. +fn serve_events(mut stream: TcpStream, state: &Shared) -> std::io::Result<()> { + write!( + stream, + "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-store\r\nConnection: keep-alive\r\n\r\n" + )?; + stream.flush()?; + + let mut last = 0u64; + loop { + let current = { + let mut st = state.state.lock().unwrap(); + while st.version == last { + let (guard, timeout) = state + .changed + .wait_timeout(st, SSE_KEEPALIVE) + .map_err(|_| std::io::Error::other("state poisoned"))?; + st = guard; + if timeout.timed_out() && st.version == last { + drop(st); + // Keep-alive comment so dead clients are detected. + write!(stream, ": ping\n\n")?; + stream.flush()?; + st = state.state.lock().unwrap(); + } + } + st.version + }; + last = current; + write!(stream, "data: {}\n\n", current)?; + stream.flush()?; + } +} + +fn open_browser(url: &str) { + let result = if cfg!(target_os = "macos") { + std::process::Command::new("open").arg(url).spawn() + } else if cfg!(target_os = "windows") { + std::process::Command::new("cmd") + .args(["/C", "start", url]) + .spawn() + } else { + std::process::Command::new("xdg-open").arg(url).spawn() + }; + if result.is_err() { + println!(" (could not open browser automatically)"); + } +} + +// ── Frontend ──────────────────────────────────────────────────────────── + +fn index_html(project_name: &str) -> String { + INDEX_HTML.replace("{{PROJECT_NAME}}", &html_escape(project_name)) +} + +fn html_escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + +const INDEX_HTML: &str = r##" + + + +{{PROJECT_NAME}} — cadforge live + + + +
+ + {{PROJECT_NAME}} + cadforge live + v0 + + + edit .cf files — preview updates automatically +
+
+ +
+
+

+  
+ +
+
+
click: inspect entity · scroll: zoom · drag: pan · double-click: fit · 1-9: cycle layer (on/ghost/off) · 3: 3D · Esc: deselect
+ + + +"##; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn index_html_injects_project_name() { + let html = index_html("Casa "); + assert!(html.contains("Casa <Lote 12>")); + assert!(!html.contains("{{PROJECT_NAME}}")); + } + + #[test] + fn relevant_events_filter_by_extension() { + use notify::event::{CreateKind, EventAttributes}; + let mut event = Event { + kind: EventKind::Create(CreateKind::File), + paths: vec![PathBuf::from("/p/muros.cf")], + attrs: EventAttributes::new(), + }; + assert!(is_relevant(&event)); + event.paths = vec![PathBuf::from("/p/output.dxf")]; + assert!(!is_relevant(&event)); + event.paths = vec![PathBuf::from("/p/preview.svg")]; + assert!(!is_relevant(&event)); + } + + #[test] + fn find_block_extracts_entity_with_comments() { + let text = r#"[layer] +name = "muros" + +# Puerta principal +[[arc]] +id = "ar-puerta" +center = [0.0, 2.5] +radius = 0.9 + +[[line]] +id = "ln-otro" +from = [0.0, 0.0] +to = [1.0, 0.0] +"#; + let block = find_block(text, "ar-puerta").unwrap(); + assert!(block.starts_with("# Puerta principal")); + assert!(block.contains("[[arc]]")); + assert!(block.contains("radius = 0.9")); + assert!(!block.contains("ln-otro")); + + let last = find_block(text, "ln-otro").unwrap(); + assert!(last.contains("[[line]]")); + assert!(last.contains("to = [1.0, 0.0]")); + + assert!(find_block(text, "missing").is_none()); + } + + #[test] + fn find_block_keeps_multiline_arrays_intact() { + let text = r#"[[polyline]] +id = "pl-huella" +points = [ + [0.30, 0.0], + [1.55, 0.0], +] +closed = true + +[[circle]] +id = "ci-otro" +center = [0.0, 0.0] +radius = 1.0 +"#; + let block = find_block(text, "pl-huella").unwrap(); + assert!(block.contains("[1.55, 0.0],"), "block: {}", block); + assert!(block.contains("closed = true"), "block: {}", block); + assert!(!block.contains("ci-otro")); + } + + #[test] + fn find_block_does_not_match_belongs_to() { + let text = r#"[[rect]] +id = "real" +belongs_to = "fake" +width = 1.0 +"#; + assert!(find_block(text, "fake").is_none()); + assert!(find_block(text, "real").is_some()); + } + + #[test] + fn query_param_decodes_percent_encoding() { + assert_eq!(query_param("id=ln%2D001", "id").as_deref(), Some("ln-001")); + assert_eq!(query_param("a=1&id=tx+1", "id").as_deref(), Some("tx 1")); + assert_eq!(query_param("a=1", "id"), None); + } +} From 74754ba3c60854d9a4761f451de243e905f0746c Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:28:49 -0500 Subject: [PATCH 05/16] feat(compiler): expand constructions at load time, emit styled dimension labels and add JSON project reports --- src/compiler.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++-- src/dxf_writer.rs | 14 ++++++-- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/compiler.rs b/src/compiler.rs index 359635c..48f9518 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -4,8 +4,10 @@ use crate::color::{hex_to_24bit, hex_to_aci, weight_to_dxf}; use crate::dxf_writer::{DxfWriter, EntityStyle}; use crate::model::{CfFile, CommonAttrs, LineStyle}; use crate::parser::{parse_cf, parse_project, LayerEntry, ProjectFile}; +use crate::transform::expand_cf; use anyhow::{bail, Context, Result}; use indexmap::IndexMap; +use serde::Serialize; use std::collections::HashSet; use std::path::Path; @@ -81,7 +83,7 @@ fn load_layers( for (name, entry) in layers { let cf_path = project_dir.join(&entry.file); let cf = parse_cf(&cf_path).with_context(|| format!("Failed to parse layer '{}'", name))?; - loaded.insert(name.clone(), cf); + loaded.insert(name.clone(), expand_cf(&cf)); } Ok(loaded) } @@ -444,7 +446,7 @@ pub fn list_layers(project_dir: &Path) -> Result<()> { for (name, entry) in &project.layers { let cf_path = project_dir.join(&entry.file); let (status, color) = if cf_path.exists() { - let cf = parse_cf(&cf_path)?; + let cf = expand_cf(&parse_cf(&cf_path)?); let count = entity_count(&cf); let col = cf .layer_meta @@ -464,6 +466,77 @@ pub fn list_layers(project_dir: &Path) -> Result<()> { Ok(()) } +// ── Machine-readable report (for AI agents / tooling) ────────────────── + +#[derive(Serialize)] +pub struct LayerReport { + pub name: String, + pub file: String, + pub entities: Option, + pub color: Option, + pub locked: bool, + pub missing: bool, +} + +#[derive(Serialize)] +pub struct ProjectReport { + pub name: String, + pub scale: String, + pub units: String, + pub strict: bool, + pub total_entities: usize, + pub layers: Vec, + pub issues: Vec, +} + +/// Build a structured validation report of the project (used by `--json` flags). +pub fn project_report(project_dir: &Path) -> Result { + let project = parse_project(&project_dir.join("project.toml"))?; + let mut loaded: IndexMap = IndexMap::new(); + let mut layers = Vec::with_capacity(project.layers.len()); + let mut total = 0usize; + + for (name, entry) in &project.layers { + let cf_path = project_dir.join(&entry.file); + if cf_path.exists() { + let cf = expand_cf( + &parse_cf(&cf_path).with_context(|| format!("Failed to parse layer '{}'", name))?, + ); + let count = entity_count(&cf); + total += count; + layers.push(LayerReport { + name: name.clone(), + file: entry.file.clone(), + entities: Some(count), + color: cf.layer_meta.as_ref().and_then(|m| m.color.clone()), + locked: entry.locked, + missing: false, + }); + loaded.insert(name.clone(), cf); + } else { + layers.push(LayerReport { + name: name.clone(), + file: entry.file.clone(), + entities: None, + color: None, + locked: entry.locked, + missing: true, + }); + } + } + + let issues = validate_constraints(&project, &loaded); + Ok(ProjectReport { + name: project.project.name.clone(), + scale: project.project.scale.clone(), + units: project.project.units.clone(), + strict: is_strict(&project), + total_entities: total, + layers, + issues, + }) +} + // ── Internal ──────────────────────────────────────────────────────────── fn entity_count(cf: &CfFile) -> usize { @@ -576,12 +649,19 @@ fn compile_cf(writer: &mut DxfWriter, cf: &CfFile, default_layer: &str) { for e in &cf.dims { let style = resolve_style(&e.common); + let dist = ((e.to[0] - e.from[0]).powi(2) + (e.to[1] - e.from[1]).powi(2)).sqrt(); + let label = + crate::svg::format_dim_label(dist, e.precision.unwrap_or(2) as usize, e.show_units, "") + .trim_end() + .to_string(); writer.dim_linear( e.from[0], e.from[1], e.to[0], e.to[1], e.offset, + &label, + e.text_size.unwrap_or(0.25), resolve_layer(&e.common, default_layer), &style, ); diff --git a/src/dxf_writer.rs b/src/dxf_writer.rs index 2d7d5dc..0fc35d0 100644 --- a/src/dxf_writer.rs +++ b/src/dxf_writer.rs @@ -179,6 +179,7 @@ impl DxfWriter { self.add_entity(EntityType::ModelPoint(pt), layer, style); } + #[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)] pub fn dim_linear( &mut self, @@ -187,6 +188,8 @@ impl DxfWriter { x2: f64, y2: f64, offset: f64, + label: &str, + text_height: f64, layer: &str, style: &EntityStyle, ) { @@ -199,8 +202,6 @@ impl DxfWriter { self.add_entity(EntityType::RotatedDimension(dim), layer, style); // Also emit dimension lines and text as explicit entities for compatibility - let dist = ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt(); - let text_val = format!("{:.2}", dist); let mid_x = (x1 + x2) / 2.0; let mid_y = (y1 + y2) / 2.0 + offset; @@ -211,7 +212,14 @@ impl DxfWriter { self.line(x1, y1 + offset, x2, y2 + offset, layer, style); // Dimension text let text_style = EntityStyle::default(); - self.text(mid_x, mid_y + 0.05, 0.1, &text_val, layer, &text_style); + self.text( + mid_x, + mid_y + text_height * 0.5, + text_height, + label, + layer, + &text_style, + ); } /// Save the drawing to a DXF file. From c9d4308b64a5d8c5bfcb5f07ac205b40f004c599 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:28:49 -0500 Subject: [PATCH 06/16] feat(cli): wire serve, schema, preview formats, entity highlights and JSON flags into the CLI --- src/lib.rs | 4 + src/main.rs | 96 ++++++++++++++++++--- src/schema.rs | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 313 insertions(+), 13 deletions(-) create mode 100644 src/schema.rs diff --git a/src/lib.rs b/src/lib.rs index 3c6a921..022de0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,5 +12,9 @@ pub mod model; pub mod parser; pub mod preview; pub mod scaffold; +pub mod schema; +pub mod serve; +pub mod svg; +pub mod transform; pub mod viewer; pub mod watch; diff --git a/src/main.rs b/src/main.rs index cfd0516..bd4e412 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,15 @@ use anyhow::{bail, Result}; -use cadforge::compiler::{check_project, compile_project, list_layers}; +use cadforge::compiler::{check_project, compile_project, list_layers, project_report}; use cadforge::config::{config_set, config_show}; use cadforge::fmt::format_project; use cadforge::importer::import_dxf; -use cadforge::preview::generate_preview; +use cadforge::preview::{generate_preview, PreviewOutputs}; use cadforge::scaffold::{create_project, init_project}; +use cadforge::schema::print_schema; +use cadforge::serve::serve_project; use cadforge::viewer::view_project; use cadforge::watch::watch_project; -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, ValueEnum}; use std::path::PathBuf; #[derive(Parser)] @@ -50,28 +52,54 @@ enum Commands { /// Project directory (defaults to current dir) #[arg(short, long)] path: Option, + /// Emit a machine-readable JSON report + #[arg(long)] + json: bool, }, /// List project layers with status Layers { /// Project directory (defaults to current dir) #[arg(short, long)] path: Option, + /// Emit a machine-readable JSON report + #[arg(long)] + json: bool, }, - /// Generate PNG preview + metadata JSON for AI agents + /// Generate preview (PNG + metadata JSON, or SVG) for AI agents Preview { /// Project directory (defaults to current dir) #[arg(short, long)] path: Option, /// Image width in pixels - #[arg(short, long, default_value = "2048")] + #[arg(short, long, default_value = "1600")] width: u32, - /// Image height in pixels - #[arg(short, long, default_value = "1536")] + /// Image height in pixels (PNG only; SVG derives it from content) + #[arg(short = 'H', long, default_value = "1200")] height: u32, /// Render only a specific layer #[arg(short, long)] layer: Option, + /// Output format + #[arg(short, long, value_enum, default_value_t = PreviewFormat::Png)] + format: PreviewFormat, + /// Highlight entities by id (comma-separated) with labeled markers + #[arg(long, value_delimiter = ',')] + highlight: Vec, }, + /// Live preview server — browser auto-reloads when .cf files change + Serve { + /// Project directory (defaults to current dir) + #[arg(short, long)] + path: Option, + /// Port to listen on + #[arg(long, default_value = "4377")] + port: u16, + /// Open the browser automatically + #[arg(long)] + open: bool, + }, + /// Print the .cf language reference (markdown, for humans and AI agents) + Schema, /// Format .cf files (sort keys, normalize whitespace) Fmt { /// Project directory (defaults to current dir) @@ -114,6 +142,16 @@ enum Commands { }, } +#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)] +enum PreviewFormat { + /// Raster PNG + preview.meta.json + Png, + /// Vector SVG (real text, dimensions, hatches) + Svg, + /// Both PNG and SVG + All, +} + #[derive(Subcommand)] enum ConfigCommands { /// Set global default value @@ -147,23 +185,55 @@ fn main() -> Result<()> { compile_project(&dir, layer.as_deref(), output.as_deref()) } } - Commands::Check { path } => { + Commands::Check { path, json } => { let dir = resolve_project_dir(path)?; - check_project(&dir)?; - Ok(()) + if json { + let report = project_report(&dir)?; + println!("{}", serde_json::to_string_pretty(&report)?); + if report.strict && !report.issues.is_empty() { + bail!( + "Check failed: {} constraint violation(s) with strict = true", + report.issues.len() + ); + } + Ok(()) + } else { + check_project(&dir)?; + Ok(()) + } } - Commands::Layers { path } => { + Commands::Layers { path, json } => { let dir = resolve_project_dir(path)?; - list_layers(&dir) + if json { + let report = project_report(&dir)?; + println!("{}", serde_json::to_string_pretty(&report)?); + Ok(()) + } else { + list_layers(&dir) + } } Commands::Preview { path, width, height, layer, + format, + highlight, } => { let dir = resolve_project_dir(path)?; - generate_preview(&dir, width, height, layer.as_deref()) + let outputs = PreviewOutputs { + png: matches!(format, PreviewFormat::Png | PreviewFormat::All), + svg: matches!(format, PreviewFormat::Svg | PreviewFormat::All), + }; + generate_preview(&dir, width, height, layer.as_deref(), &highlight, outputs) + } + Commands::Serve { path, port, open } => { + let dir = resolve_project_dir(path)?; + serve_project(&dir, port, open) + } + Commands::Schema => { + print_schema(); + Ok(()) } Commands::Fmt { path, check } => { let dir = resolve_project_dir(path)?; diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..4ebc5b7 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,226 @@ +//! Schema — the `.cf` language reference, printable via `cadforge schema`. +//! +//! This is the self-discovery entry point for AI agents: one command dumps the +//! complete format so any agent can generate valid `.cf` files without prior +//! training. The same text is embedded into AGENTS.md by `cadforge new`. + +/// Complete `.cf` + `project.toml` reference in markdown. +pub const CF_REFERENCE: &str = r##"# CADforge `.cf` Language Reference + +CADforge projects are plain TOML. A project is a directory with a `project.toml` +plus one `.cf` file per layer. Geometry is declared, never drawn: the same files +always compile to the same DXF. + +## project.toml + +```toml +[project] +name = "Vivienda Lote 12" +scale = "1:100" # drawing scale (metadata) +units = "m" # unit label used in dimension labels +strict = false # true: constraint violations fail the build + +[layers] # order defines draw order (later = on top) +muros = { file = "muros.cf", locked = false } +puertas = { file = "puertas.cf", locked = false } + +[constraints] # optional, validated on build/check +puertas.parent = "muros" # child bbox must fit inside parent bbox +cotas.belongs_to = "muros" # child primitives reference parent ids via belongs_to +"muros → puertas" = "spatial_dependency" # movement warning (informational) +``` + +## Layer files (`.cf`) + +Each `.cf` may start with optional layer metadata: + +```toml +[layer] +name = "muros" +color = "#FFFFFF" # default color for the layer +line_weight = 0.35 # default stroke weight in mm +visible = true +locked = false +``` + +### Common attributes (valid on every primitive, all optional) + +```toml +id = "ln-001" # unique within the layer; needed for hatch boundaries / belongs_to +color = "#FF5050" # overrides layer color +weight = 0.50 # line weight in mm (0.13 thin … 0.70 thick) +style = "solid" # solid | dashed | dotted | dashdot +visible = true +locked = false +belongs_to = "id" # reference to a primitive id in the parent layer +``` + +### Primitives + +```toml +[[line]] # straight segment +from = [0.0, 0.0] +to = [8.5, 0.0] + +[[polyline]] # multi-vertex path; closed = true makes it a polygon +points = [[0.0, 0.0], [8.5, 0.0], [8.5, 6.0], [0.0, 6.0]] +closed = true + +[[rect]] # axis-aligned rectangle from bottom-left origin +origin = [1.0, 1.0] +width = 3.5 +height = 4.0 + +[[circle]] +center = [4.0, 3.0] +radius = 0.5 + +[[arc]] # angles in degrees, counterclockwise from +X +center = [2.0, 2.0] +radius = 0.9 +from_angle = 0.0 +to_angle = 90.0 + +[[text]] +position = [4.0, 3.0] +content = "SALA" +size = 0.25 # text height in world units +align = "center" # left | center | right + +[[point]] # reference marker (drawn as a cross) +position = [3.0, 3.0] + +[[dim]] # dimension; the measured distance is labeled automatically +type = "linear" # linear | angular | radial +from = [0.0, 0.0] +to = [8.5, 0.0] +offset = -0.8 # distance from the measured element (sign = side) +text_size = 0.25 # label height in world units (optional) +precision = 2 # decimals in the measured value (default 2) +show_units = true # append project units to the label (default true) + +[[hatch]] # pattern fill inside a closed boundary +boundary = "pl-001" # id of a closed polyline or rect in the same file +pattern = "ansi31" # ansi31 | ansi32 | ansi33 | ansi34 | solid | none +scale = 1.0 +angle = 45.0 + +[[fill]] # solid fill; boundary id or inline points +points = [[0.0, 0.0], [4.0, 0.0], [4.0, 3.0], [0.0, 3.0]] +color = "#808080" + +[[group]] # logical grouping of primitives by id +members = ["ln-001", "rc-001"] +``` + +### Construction tools + +Arrays and mirrors expand into concrete primitives at build time. Copies get +derived ids — `pl-001@1`, `pl-001@2`, … (array) and `pl-001@m` (mirror) — so +they can be highlighted or referenced. Edit the base entity or the +`[[array]]`/`[[mirror]]` block to change all copies at once. + +```toml +[[array]] # repeat targets in a line or around a center +target = "pl-huella" # or targets = ["id-a", "id-b"] +mode = "polar" # polar: spiral stairs, gear teeth, radial columns +count = 16 # total instances, including the original +center = [0.0, 0.0] # polar: rotation center +step_angle = 22.5 # polar: degrees per copy, counterclockwise +rotate_items = true # polar: false = orbit only, keep orientation + +[[array]] +target = "rc-banco" +mode = "linear" # linear: equally spaced series +count = 3 +offset = [1.3, 0.0] # displacement per copy + +[[mirror]] # mirror targets across an axis (two points) +targets = ["pl-nave", "ar-puerta"] +axis = [[2.5, 0.0], [2.5, 1.0]] +``` + +## Conventions + +- Coordinates are world units (see `units`), Y grows upward, origin at [0, 0]. +- Use floats (`8.5`, `0.0`) for all coordinates. +- Give every primitive a short prefixed id: `ln-` lines, `pl-` polylines, + `rc-` rects, `ci-` circles, `ar-` arcs, `tx-` text, `dm-` dims, `ht-` hatches. +- Walls are typically `weight = 0.50`, furniture `0.25`, annotations `0.18`. + +## Workflow + +```bash +cadforge serve # live preview in the browser (auto-reloads on save) +cadforge build # compile to output.dxf +cadforge check --json # machine-readable validation report +cadforge layers --json # machine-readable layer listing +cadforge preview # PNG + metadata JSON (--format svg for vector) +cadforge preview --highlight ln-001,tx-002 # amber markers around those ids +cadforge fmt # normalize .cf formatting +``` + +The feedback loop for agents: edit `.cf` → run `cadforge check --json` to +validate → run `cadforge preview` and **look at `preview.png`** — it is a +faithful render (real text, measured dimension labels, hatches, line styles). +`preview.meta.json` maps every entity id to world and pixel bounding boxes. +After editing specific entities, re-render with +`cadforge preview --highlight ` to visually confirm the change landed +where intended (highlighted entities get labeled amber markers). + +For humans, `cadforge serve` adds: click any entity to inspect its source +TOML block (copyable as an agent prompt for targeted edits), a layer panel +with on/ghost/off states (trace one floor over another), and a 3D stacked +view of the layers. +"##; + +/// Print the `.cf` language reference to stdout. +pub fn print_schema() { + println!("{}", CF_REFERENCE); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reference_covers_all_primitives() { + for primitive in [ + "[[line]]", + "[[polyline]]", + "[[rect]]", + "[[circle]]", + "[[arc]]", + "[[text]]", + "[[point]]", + "[[dim]]", + "[[hatch]]", + "[[fill]]", + "[[group]]", + "[[array]]", + "[[mirror]]", + ] { + assert!(CF_REFERENCE.contains(primitive), "missing {}", primitive); + } + } + + #[test] + fn reference_examples_are_valid_toml() { + // Every fenced toml block in the reference must parse. + let mut in_block = false; + let mut block = String::new(); + for line in CF_REFERENCE.lines() { + if line.starts_with("```toml") { + in_block = true; + block.clear(); + } else if line.starts_with("```") && in_block { + in_block = false; + let parsed: Result = toml::from_str(&block); + assert!(parsed.is_ok(), "invalid TOML block:\n{}", block); + } else if in_block { + block.push_str(line); + block.push('\n'); + } + } + } +} From f2a5385a4e2fb32c18d3d22840bf81fcbdba6919 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:28:49 -0500 Subject: [PATCH 07/16] feat(scaffold): generate an AGENTS.md language guide in new projects --- src/scaffold.rs | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/scaffold.rs b/src/scaffold.rs index afc05e5..856bb94 100644 --- a/src/scaffold.rs +++ b/src/scaffold.rs @@ -1,5 +1,6 @@ //! Scaffold — generates a new CADforge project structure. +use crate::schema::CF_REFERENCE; use anyhow::{bail, Result}; use std::fs; use std::path::Path; @@ -20,8 +21,13 @@ pub fn create_project(name: &str, parent: &Path) -> Result<()> { println!(" → puertas.cf"); println!(" → mobiliario.cf"); println!(" → cotas.cf"); + println!(" → AGENTS.md"); println!(" → .gitignore"); - println!("\n Run `cadforge build --path {}` to compile.", name); + println!( + "\n Run `cadforge serve --path {}` for a live preview,", + name + ); + println!(" or `cadforge build --path {}` to compile to DXF.", name); Ok(()) } @@ -43,7 +49,9 @@ pub fn init_project(dir: &Path) -> Result<()> { println!(" → puertas.cf"); println!(" → mobiliario.cf"); println!(" → cotas.cf"); + println!(" → AGENTS.md"); println!(" → .gitignore"); + println!("\n Run `cadforge serve` for a live preview."); Ok(()) } @@ -63,9 +71,35 @@ cotas = {{ file = "cotas.cf", locked = false }} ); fs::write(project_dir.join("project.toml"), project_toml)?; - let gitignore = "# CADforge output\noutput.dxf\npreview.png\npreview.meta.json\n\n# Rust build artifacts\ntarget/\n"; + let gitignore = "# CADforge output\noutput.dxf\npreview.png\npreview.svg\npreview.meta.json\n\n# Rust build artifacts\ntarget/\n"; fs::write(project_dir.join(".gitignore"), gitignore)?; + let agents_md = format!( + r#"# {name} — Agent Guide + +This is a CADforge project: geometry declared as TOML, compiled to DXF. +Edit the `.cf` layer files listed in `project.toml`; never edit `output.dxf` +or `preview.*` (generated). + +Feedback loop: + +1. Edit `.cf` files (format reference below). +2. `cadforge check --json` — validate and read constraint issues. +3. `cadforge preview` — render `preview.png` (faithful: real text, measured + dimensions, hatches) + `preview.meta.json` (entity bounding boxes in world + and pixel coordinates). Look at the image to verify your work. +4. `cadforge preview --highlight ` — re-render with labeled amber + markers around the entities you just touched, to confirm the change landed + where intended. +5. If a human is watching, `cadforge serve` gives them a live browser preview + that refreshes automatically on every save. + +{reference}"#, + name = name, + reference = CF_REFERENCE + ); + fs::write(project_dir.join("AGENTS.md"), agents_md)?; + let muros_cf = r##"[layer] name = "muros" color = "#FFFFFF" @@ -199,6 +233,7 @@ mod tests { assert!(project_dir.join("puertas.cf").exists()); assert!(project_dir.join("mobiliario.cf").exists()); assert!(project_dir.join("cotas.cf").exists()); + assert!(project_dir.join("AGENTS.md").exists()); assert!(project_dir.join(".gitignore").exists()); let content = fs::read_to_string(project_dir.join("project.toml")).unwrap(); @@ -207,8 +242,13 @@ mod tests { let gitignore = fs::read_to_string(project_dir.join(".gitignore")).unwrap(); assert!(gitignore.contains("output.dxf")); + assert!(gitignore.contains("preview.svg")); assert!(gitignore.contains("target/")); + let agents = fs::read_to_string(project_dir.join("AGENTS.md")).unwrap(); + assert!(agents.contains("mi-proyecto")); + assert!(agents.contains("[[line]]")); + let _ = fs::remove_dir_all(&tmp); } @@ -237,6 +277,7 @@ mod tests { assert!(tmp.join("puertas.cf").exists()); assert!(tmp.join("mobiliario.cf").exists()); assert!(tmp.join("cotas.cf").exists()); + assert!(tmp.join("AGENTS.md").exists()); assert!(tmp.join(".gitignore").exists()); let _ = fs::remove_dir_all(&tmp); From be9528a44604a870c9e6244092a4b2907ae5bb93 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:28:59 -0500 Subject: [PATCH 08/16] feat(examples): add taller example showcasing polar arrays, mirror and styled dimensions --- examples/taller/cotas.cf | 30 ++++++++++++++++++++ examples/taller/engranaje.cf | 43 +++++++++++++++++++++++++++++ examples/taller/escalera.cf | 37 +++++++++++++++++++++++++ examples/taller/planta.cf | 53 ++++++++++++++++++++++++++++++++++++ examples/taller/project.toml | 10 +++++++ 5 files changed, 173 insertions(+) create mode 100644 examples/taller/cotas.cf create mode 100644 examples/taller/engranaje.cf create mode 100644 examples/taller/escalera.cf create mode 100644 examples/taller/planta.cf create mode 100644 examples/taller/project.toml diff --git a/examples/taller/cotas.cf b/examples/taller/cotas.cf new file mode 100644 index 0000000..a83397e --- /dev/null +++ b/examples/taller/cotas.cf @@ -0,0 +1,30 @@ +[layer] +name = "cotas" +color = "#FF5050" + +# Diámetro exterior de la escalera — etiqueta grande, 1 decimal +[[dim]] +id = "dm-escalera" +from = [-1.6, 0.0] +to = [1.6, 0.0] +offset = -2.2 +text_size = 0.28 +precision = 1 + +# Ancho del engranaje — sin unidades, 3 decimales, letra pequeña +[[dim]] +id = "dm-engranaje" +from = [3.55, 0.0] +to = [6.45, 0.0] +offset = -2.2 +text_size = 0.16 +precision = 3 +show_units = false + +# Ancho total de la nave (geometría espejada: 0 → 5) +[[dim]] +id = "dm-nave" +from = [0.0, -4.6] +to = [5.0, -4.6] +offset = -0.6 +text_size = 0.22 diff --git a/examples/taller/engranaje.cf b/examples/taller/engranaje.cf new file mode 100644 index 0000000..740d8b0 --- /dev/null +++ b/examples/taller/engranaje.cf @@ -0,0 +1,43 @@ +[layer] +name = "engranaje" +color = "#50C8FF" +line_weight = 0.35 + +# Diente base; el array polar completa la corona de 16 dientes +[[polyline]] +id = "pl-diente" +points = [ + [6.193, -0.125], + [6.448, -0.076], + [6.448, 0.076], + [6.193, 0.125], +] +closed = true + +[[array]] +id = "ar-corona" +target = "pl-diente" +mode = "polar" +count = 16 +center = [5.0, 0.0] +step_angle = 22.5 + +# Círculo primitivo (referencia, discontinuo) +[[circle]] +id = "ci-primitivo" +center = [5.0, 0.0] +radius = 1.2 +style = "dashed" +weight = 0.18 + +# Cuerpo y agujero del eje +[[circle]] +id = "ci-cuerpo" +center = [5.0, 0.0] +radius = 1.05 +weight = 0.50 + +[[circle]] +id = "ci-eje" +center = [5.0, 0.0] +radius = 0.3 diff --git a/examples/taller/escalera.cf b/examples/taller/escalera.cf new file mode 100644 index 0000000..56456f3 --- /dev/null +++ b/examples/taller/escalera.cf @@ -0,0 +1,37 @@ +[layer] +name = "escalera" +color = "#FFFFFF" +line_weight = 0.35 + +# Columna central +[[circle]] +id = "ci-columna" +center = [0.0, 0.0] +radius = 0.25 + +# Perímetro exterior de la escalera +[[circle]] +id = "ci-borde" +center = [0.0, 0.0] +radius = 1.6 +weight = 0.50 + +# Huella base (un escalón); el array polar genera la helicoidal completa +[[polyline]] +id = "pl-huella" +points = [ + [0.30, 0.0], + [1.55, 0.0], + [1.456, 0.530], + [0.282, 0.103], +] +closed = true +weight = 0.25 + +[[array]] +id = "ar-helicoidal" +target = "pl-huella" +mode = "polar" +count = 16 +center = [0.0, 0.0] +step_angle = 22.5 diff --git a/examples/taller/planta.cf b/examples/taller/planta.cf new file mode 100644 index 0000000..0f3688f --- /dev/null +++ b/examples/taller/planta.cf @@ -0,0 +1,53 @@ +[layer] +name = "planta" +color = "#50FF50" +line_weight = 0.35 + +# Media nave del taller: el espejo completa la planta simétrica respecto a x = 2.5 +[[polyline]] +id = "pl-nave" +points = [ + [0.0, -4.6], + [2.5, -4.6], + [2.5, -2.4], + [0.6, -2.4], + [0.6, -3.0], + [0.0, -3.0], +] +closed = false +weight = 0.50 + +# Puerta con su abatimiento +[[line]] +id = "ln-puerta" +from = [0.9, -2.4] +to = [1.7, -2.4] +color = "#FFC850" + +[[arc]] +id = "ar-puerta" +center = [0.9, -2.4] +radius = 0.8 +from_angle = 0.0 +to_angle = 90.0 +color = "#FFC850" + +# Bancos de trabajo en serie (array lineal) +[[rect]] +id = "rc-banco" +origin = [0.3, -4.4] +width = 0.8 +height = 0.5 +color = "#C878FF" + +[[array]] +id = "ar-bancos" +target = "rc-banco" +mode = "linear" +count = 3 +offset = [1.3, 0.0] + +[[mirror]] +id = "mr-nave" +targets = ["pl-nave", "ln-puerta", "ar-puerta"] +axis = [[2.5, 0.0], [2.5, 1.0]] diff --git a/examples/taller/project.toml b/examples/taller/project.toml new file mode 100644 index 0000000..10537aa --- /dev/null +++ b/examples/taller/project.toml @@ -0,0 +1,10 @@ +[project] +name = "Taller — arrays, espejo y cotas" +scale = "1:50" +units = "m" + +[layers] +escalera = { file = "escalera.cf", locked = false } +engranaje = { file = "engranaje.cf", locked = false } +planta = { file = "planta.cf", locked = false } +cotas = { file = "cotas.cf", locked = false } From ddd93ce05bdb5c8ba3fb8f7cc32b737d6e8f02fe Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:28:59 -0500 Subject: [PATCH 09/16] test: cover SVG rendering, preview pipeline, transform expansion and the taller example --- tests/integration.rs | 123 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/integration.rs b/tests/integration.rs index 19016fe..a33e92c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -282,6 +282,129 @@ fn compile_fails_on_constraint_violation_when_strict() { let _ = fs::remove_dir_all(dir); } +#[test] +fn render_svg_on_example_project() { + let svg = cadforge::svg::render_svg(Path::new("examples/vivienda"), None, 1600).unwrap(); + assert!(svg.starts_with("")); + // Layer groups for every project layer + for layer in ["muros", "puertas", "mobiliario", "cotas", "achurados"] { + assert!( + svg.contains(&format!(r#"data-layer="{}""#, layer)), + "missing layer group {}", + layer + ); + } + // Dimensions are labeled with the measured value in project units + assert!(svg.contains(" m"), "dim labels should include units"); +} + +#[test] +fn project_report_is_serializable_and_complete() { + let report = cadforge::compiler::project_report(Path::new("examples/vivienda")).unwrap(); + assert!(report.total_entities > 0); + assert_eq!(report.layers.len(), 5); + assert!(report.layers.iter().all(|l| !l.missing)); + + let json = serde_json::to_string(&report).unwrap(); + assert!(json.contains("\"total_entities\"")); + assert!(json.contains("\"issues\"")); +} + +#[test] +fn taller_example_expands_arrays_and_mirrors() { + let dir = Path::new("examples/taller"); + + // Expanded entity counts: 16 treads + 16 teeth + mirrored geometry + let report = cadforge::compiler::project_report(dir).unwrap(); + assert_eq!(report.total_entities, 49); + + let svg = cadforge::svg::render_svg(dir, None, 1200).unwrap(); + // 15 generated tread copies with derived ids + let tread_copies = svg.matches(r#"data-id="pl-huella@"#).count(); + assert_eq!(tread_copies, 15); + // Mirrored door arc exists + assert!(svg.contains(r#"data-id="ar-puerta@m""#)); + // Styled dims: 1 decimal with units, 3 decimals without units + assert!(svg.contains("3.2 m")); + assert!(svg.contains(">2.900<")); + + // The DXF compiles with the expanded geometry + let out = Path::new("/tmp/cadforge_taller.dxf"); + let _ = fs::remove_file(out); + compile_project(dir, None, Some(out)).unwrap(); + assert!(out.exists()); + let _ = fs::remove_file(out); +} + +#[test] +fn preview_renders_faithful_png_with_metadata_and_highlights() { + let dir = Path::new("/tmp/cadforge_preview_test"); + let _ = fs::remove_dir_all(dir); + fs::create_dir_all(dir).unwrap(); + fs::write( + dir.join("project.toml"), + r#"[project] +name = "preview-fixture" +units = "m" + +[layers] +plano = { file = "plano.cf", locked = false } +"#, + ) + .unwrap(); + fs::write( + dir.join("plano.cf"), + r##"[[rect]] +id = "rc-room" +origin = [0.0, 0.0] +width = 6.0 +height = 4.0 + +[[text]] +id = "tx-label" +position = [3.0, 2.0] +content = "SALA" +size = 0.4 +align = "center" + +[[dim]] +id = "dm-width" +from = [0.0, 0.0] +to = [6.0, 0.0] +offset = -0.6 +"##, + ) + .unwrap(); + + cadforge::preview::generate_preview( + dir, + 800, + 800, + None, + &["rc-room".to_string()], + cadforge::preview::PreviewOutputs { + png: true, + svg: true, + }, + ) + .unwrap(); + + assert!(dir.join("preview.png").exists()); + assert!(dir.join("preview.svg").exists()); + let meta = fs::read_to_string(dir.join("preview.meta.json")).unwrap(); + assert!(meta.contains(r#""content": "SALA""#)); + assert!(meta.contains(r#""entity_type": "dim""#)); + assert!(meta.contains(r#""highlighted""#)); + assert!(meta.contains("rc-room")); + + // The PNG must fit within the requested box and be non-trivial + let png = fs::read(dir.join("preview.png")).unwrap(); + assert!(png.len() > 1000, "PNG should contain rendered content"); + + let _ = fs::remove_dir_all(dir); +} + #[test] fn import_generated_dxf_creates_cadforge_project() { let source = Path::new("examples/vivienda"); From 7a041acb2f255ca291dbb9356e8852fda058c668 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:28:59 -0500 Subject: [PATCH 10/16] docs: rewrite README around the vibecoding workflow and viewer features --- .gitignore | 1 + README.md | 114 +++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 82 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index de105c7..d49f2d5 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,5 @@ Cargo.lock # End of https://www.toptal.com/developers/gitignore/api/rust output.dxf preview.png +preview.svg preview.meta.json diff --git a/README.md b/README.md index 1bf094b..2f32f4c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,38 @@ License

-cadforge is an **Architecture as Code** CLI tool and Rust library for declarative 2D CAD modeling. Write geometry as code in `.cf` TOML format, compile to DXF, and generate PNG previews for AI agents. +cadforge is an **Architecture as Code** CLI tool and Rust library for declarative 2D CAD modeling. Write geometry as code in `.cf` TOML format, watch it live in the browser, and compile to DXF — built for humans and AI agents working together. + +--- + +## Vibecoding CAD + +The core loop: **describe geometry in TOML, see it instantly, iterate.** + +```bash +cadforge new casa && cd casa +cadforge serve --open # live preview in the browser +``` + +Now edit any `.cf` file — by hand or by asking an AI agent — and the browser +updates on every save. Parse errors and constraint violations appear as an +overlay instead of a crash. When the design is right, `cadforge build` emits +a deterministic, AutoCAD-compatible DXF. + +Agents get first-class support: + +- `cadforge schema` — full `.cf` language reference in one command (also written + to every new project as `AGENTS.md`). +- `cadforge check --json` / `cadforge layers --json` — machine-readable + validation reports. +- `cadforge preview` — a faithful PNG render (real text, measured dimension + labels, hatches, line styles) + `preview.meta.json` with per-entity bounding + boxes in world and pixel coordinates, so multimodal agents can *look* at the + plan and locate every entity in the image. +- `cadforge preview --highlight ln-001,tx-002` — labeled amber markers around + specific entities, so an agent can visually confirm its edit landed where + intended. +- `cadforge preview --format svg` — same render as vector SVG. --- @@ -21,25 +52,28 @@ cadforge is an **Architecture as Code** CLI tool and Rust library for declarativ ### 🎯 Core Platform - **📐 Declarative Geometry** — Define architectural elements (lines, rects, circles, arcs, polylines, text, dimensions) in TOML `.cf` files. Deterministic, reproducible, version-controlled. +- **🛠️ Construction Tools** — `[[array]]` (linear and polar: spiral staircases, gear teeth, repeated columns) and `[[mirror]]` expand into concrete primitives at build time; copies get derived ids (`base@1`, `base@m`). +- **📏 Styled Dimensions** — Auto-measured labels with configurable `text_size`, `precision`, `show_units`, and `offset` per dimension. +- **🔴 Live Preview** — `cadforge serve` runs a local server with pan/zoom, auto-reload on save (SSE), click-to-inspect any entity (copy its source TOML as an agent prompt), per-layer ghost/hide states, a 3D stacked-layers view, and a build-error overlay. Zero config. - **🔗 Layer System** — Organize geometry by layer with custom names, colors, and line weights. Compile single layers or full projects. - **📄 DXF Export** — Compile `.cf` → DXF (AutoCAD-compatible). Full layer support, LWPOLYLINE for polylines, HATCH for solid fills, MTEXT for annotations. -- **🖼️ PNG Preview** — Generate raster previews with metadata JSON for AI agent integration. Renders fills, hatches, strokes, and text with boundary resolution. Configurable resolution and layer filtering. -- **✅ Validation Engine** — `cadforge check` validates geometry without generating output. Shows project metadata, layer colors, and entity counts. +- **🖼️ Previews for Agents** — Raster PNG + metadata JSON (entity bounding boxes) and full-fidelity SVG with real text, auto-measured dimensions, line styles, and clipped hatch patterns. +- **✅ Validation Engine** — `cadforge check` validates geometry and constraints without generating output; `--json` for tooling. ### 🏗️ Project Management -- **Project Scaffolding** — `cadforge new` creates a complete multi-layer project (muros, puertas, mobiliario, cotas) with meaningful architectural examples. +- **Project Scaffolding** — `cadforge new` creates a complete multi-layer project (muros, puertas, mobiliario, cotas) plus an `AGENTS.md` that teaches any AI agent the format. - **Multi-Layer Compilation** — Compile all layers or target specific layers with `--layer`. Custom output path with `--output`. -- **Auto-Rebuild** — `cadforge watch` monitors `.cf` and `.toml` files and auto-rebuilds on changes with 300ms debounce. +- **Auto-Rebuild** — `cadforge watch` monitors `.cf` and `.toml` files and auto-rebuilds DXF on changes with 300ms debounce. - **Code Formatting** — `cadforge fmt` normalizes `.cf` files. `--check` mode for CI validation. -- **Boundary Resolution** — Automatic detection of closed boundaries for hatch generation. Shared boundary resolution across overlapping entities. -- **Polyline Support** — Full LWPOLYLINE support with bulge factors for arcs. Proper vertex handling and closure detection. +- **Constraints** — `parent`, `belongs_to`, and `spatial_dependency` rules between layers; warnings by default, build-blocking with `strict = true`. +- **DXF Import** — `cadforge import plano.dxf` migrates existing drawings into `.cf` layers + `project.toml`. ### 🔧 Architecture - **Compiler Pipeline** — Parse → Resolve → Compile → Emit. Modular design for easy extension. - **DXF Writer** — Direct DXF entity writing with proper AutoCAD compatibility. Layer/color/lineweight mapping. -- **Preview Renderer** — Tiny-skia based raster rendering with anti-aliasing. PNG + JSON metadata output. +- **Renderer** — One handwritten SVG backend; the PNG preview rasterizes it via resvg with an embedded monospace font (deterministic text on any machine, including fontless containers). - **Error Reporting** — Structured errors with file, line, and context. Fast-fail on validation errors. --- @@ -54,10 +88,17 @@ cadforge is an **Architecture as Code** CLI tool and Rust library for declarativ | `cadforge build --check` | Validate project and constraints without generating DXF | | `cadforge build --output ` | Compile to custom output path | | `cadforge build --layer ` | Compile specific layer only | +| `cadforge serve` | Live preview server — browser auto-reloads on save | +| `cadforge serve --open --port 4377` | Open browser automatically on a custom port | | `cadforge check` | Validate with project metadata and layer colors | +| `cadforge check --json` | Machine-readable validation report | | `cadforge layers` | List layers with entity counts and colors | -| `cadforge preview` | Generate PNG preview + metadata JSON | -| `cadforge preview --width 1024 --height 768` | Custom resolution preview | +| `cadforge layers --json` | Machine-readable layer listing | +| `cadforge schema` | Print the full `.cf` language reference (markdown) | +| `cadforge preview` | Faithful PNG render + metadata JSON | +| `cadforge preview --format svg` | Vector SVG preview (same renderer) | +| `cadforge preview --highlight ` | Amber markers around specific entities | +| `cadforge preview --width 1024 -H 768` | Custom resolution preview | | `cadforge preview --layer ` | Preview specific layer only | | `cadforge fmt` | Format .cf files (normalize whitespace) | | `cadforge fmt --check` | Check formatting without modifying (CI) | @@ -69,18 +110,14 @@ cadforge is an **Architecture as Code** CLI tool and Rust library for declarativ | `cadforge config set ` | Set global defaults (`author`, `units`) | | `cadforge config show` | Show global defaults | -### Viewer controls (MVP) +### Live preview controls (`cadforge serve`) -- HUD flotante en pantalla con proyecto, vista, distancia, capas, selección y ayuda de atajos -- `T` / `F` / `V` / `R` → top / front / right / isometric preset views -- `Q` / `E` / `W` / `S` → orbit camera -- Mouse left-drag → orbit -- Mouse right-drag / arrows → pan -- Mouse wheel / `+` / `-` → zoom -- `1`..`9` → toggle layer visibility -- Click entity edge → select primitive id -- Selected entity is highlighted in amber in the viewport HUD context -- `C` → copy selected id to clipboard +- Scroll → zoom (centered on cursor) · drag → pan · double-click / `F` → fit +- **Click an entity** → inspector with its source TOML block; `copy for agent` produces a ready-made targeted-edit prompt +- Layer panel (or keys `1`-`9`) → cycle each layer **on → ghost → off**; ghost mode traces one floor plan over another +- `3D` button (or key `3`) → stacked exploded view of the layers +- Browser auto-reloads on every `.cf` / `project.toml` save (SSE) +- Build errors render as an overlay with file/line detail — the loop never breaks --- @@ -117,7 +154,7 @@ to_angle = 90.0 [[polyline]] id = "pl-001" -vertices = [[0, 0], [5, 0], [5, 3], [0, 3]] +points = [[0.0, 0.0], [5.0, 0.0], [5.0, 3.0], [0.0, 3.0]] closed = true [[text]] @@ -135,7 +172,9 @@ offset = 0.5 ### Supported Primitives -`line`, `polyline`, `rect`, `circle`, `arc`, `text`, `point`, `dim`, `hatch`, `solid` +`line`, `polyline`, `rect`, `circle`, `arc`, `text`, `point`, `dim`, `hatch`, `fill`, `group` + +Run `cadforge schema` for the complete reference with all attributes. --- @@ -160,20 +199,24 @@ offset = 0.5 - **Resolver** — Layer dependency resolution, coordinate validation, boundary detection - **Compiler** — Entity compilation to DXF format, hatch generation, polyline closure - **DXF Writer** — Direct DXF entity emission with proper layer/color/lineweight mapping -- **Preview Renderer** — Tiny-skia raster rendering with hatch/fill support +- **SVG Renderer** — Single vector backend: text, measured dims, hatches, highlights; PNG previews are resvg rasterizations of it --- ## Main Modules -- `compiler/` — Project compilation pipeline, layer targeting, validation, build stats +- `compiler/` — Project compilation pipeline, layer targeting, validation, JSON reports - `dxf_writer/` — DXF entity writing, LWPOLYLINE, HATCH, MTEXT generation - `preview/` — PNG rendering with configurable resolution, layer filtering, metadata JSON +- `svg/` — Vector SVG rendering: real text, measured dimensions, hatch clipping, grid +- `serve/` — Live preview server: file watcher + SSE auto-reload + error overlay +- `schema/` — Embedded `.cf` language reference for humans and agents - `parser/` — TOML parsing, primitive extraction, array-of-tables handling - `model/` — Data structures: Layer, Primitive, Project -- `scaffold/` — Multi-layer project creation with architectural examples +- `scaffold/` — Multi-layer project creation with architectural examples + AGENTS.md - `fmt/` — .cf file formatting and normalization - `watch/` — File system watcher with auto-rebuild and debounce +- `importer/` — DXF → `.cf` migration - `color/` — Color parsing and DXF color mapping --- @@ -183,10 +226,10 @@ offset = 0.5 | Data | Location | Format | |------|----------|--------| | Project files | `./` | TOML (`.cf` + `project.toml`) | -| Build output | `output/` | DXF | -| Preview output | `output/preview.png` | PNG | -| Preview metadata | `output/preview.json` | JSON | -| Build cache | `target/` | Cargo build | +| Build output | `./output.dxf` | DXF | +| Preview output | `./preview.png`, `./preview.svg` | PNG / SVG | +| Preview metadata | `./preview.meta.json` | JSON | +| Agent guide | `./AGENTS.md` | Markdown | --- @@ -198,7 +241,12 @@ cadforge new mi-proyecto cd mi-proyecto ``` -**Edit `.cf` files** (TOML format with your geometry) +**Live preview while you edit:** +```bash +cadforge serve --open # browser refreshes on every save +``` + +**Edit `.cf` files** (TOML format with your geometry — run `cadforge schema` for the reference) **Format and validate:** ```bash @@ -215,7 +263,7 @@ cadforge build --layer muros # compile single layer **Preview:** ```bash -cadforge preview # default 2048x1536 +cadforge preview # default 1600x1200 (fits content aspect) cadforge preview --width 1024 --height 768 # custom resolution cadforge preview --layer muros # single layer preview ``` @@ -229,7 +277,7 @@ cadforge watch # monitors .cf and .toml files ## Tech Stack -| Rust 2021 | clap | toml | toml_edit | tiny-skia | dxf | notify | anyhow | serde | +| Rust 2021 | clap | toml | toml_edit | resvg | dxf | notify | anyhow | serde | --- From e064c2bedef53ad7f184493a73cd6e12e07f9154 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:28:59 -0500 Subject: [PATCH 11/16] docs: add project specification and roadmap --- cadforge-spec.md | 418 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 cadforge-spec.md diff --git a/cadforge-spec.md b/cadforge-spec.md new file mode 100644 index 0000000..0abb8d8 --- /dev/null +++ b/cadforge-spec.md @@ -0,0 +1,418 @@ +# CADforge — Especificación y Roadmap v1.0 + +> Arquitectura como Código — motor determinista de geometría descriptiva para diseño arquitectónico reproducible, versionable y potenciado por agentes de IA. + +--- + +## 1. Visión + +El diseño arquitectónico actual sufre de **entropía gráfica**: los planos son colecciones de líneas sin semántica, imposibles de versionar, comparar o automatizar. CADforge propone un cambio de paradigma: + +**El plano no se dibuja, se declara.** + +Al igual que el código fuente de software, un espacio arquitectónico es el resultado de un lenguaje estructurado. Si el código no cambia, el plano es idéntico bit a bit cada vez que se compila. Esto elimina la ambigüedad del clic humano, habilita `git diff` sobre planos, y permite que los agentes de IA generen y modifiquen diseños con precisión quirúrgica. + +CADforge no es un programa de dibujo. Es la infraestructura para que la arquitectura sea una **ciencia de datos reproducible**. + +--- + +## 2. Ecosistema — Tres Proyectos Separados + +La arquitectura se divide en tres proyectos independientes que se integran entre sí: + +``` +cadforge → motor de geometría (librería Rust, crates.io) +cadforge-cli → interfaz de línea de comandos (binario Rust, crates.io) +cadforge-view → visor gráfico vectorial con modo calco (binario Rust) +``` + +### Separación de responsabilidades + +| Proyecto | Tipo | Responsabilidad | +|---|---|---| +| `cadforge` | Librería | Parser `.cf`, compilador → DXF, motor de geometría, sistema de capas y constraints | +| `cadforge-cli` | Binario | Comandos, wizard, build, watch, import/export, integración con agentes | +| `cadforge-view` | Binario | Visor vectorial estilo consola, modo calco, edición bidireccional → `.cf` | + +La librería `cadforge` es reutilizable por cualquier proyecto Rust — el CLI y el visor son consumidores de ella. + +--- + +## 3. Formato de Proyecto + +Un proyecto CADforge es un directorio con la siguiente estructura: + +``` +mi-proyecto/ +├── project.toml ← archivo raíz: metadatos, capas, constraints +├── capa-a.cf ← capa de primitivos (ej: planta estructural) +├── capa-b.cf ← capa de primitivos (ej: instalaciones) +└── capa-c.cf ← capa de primitivos (ej: acabados y anotaciones) +``` + +El nombre de cada `.cf` lo define el usuario — CADforge no impone nomenclatura ni semántica de capas. Una capa es simplemente un conjunto de primitivos geométricos agrupados. + +### project.toml + +```toml +[project] +name = "Vivienda Unifamiliar Lote 12" +scale = "1:100" +units = "m" +author = "Arq. Nombre Apellido" +version = "0.3.0" + +[layers] +capa-a = { file = "capa-a.cf", locked = false } +capa-b = { file = "capa-b.cf", locked = false } +capa-c = { file = "capa-c.cf", locked = false } + +[constraints] +# Si un primitivo de capa-a se mueve, notificar a capa-b +capa-a → capa-b = "spatial_dependency" + +# Los primitivos de capa-b deben vivir dentro del bbox de capa-a +capa-b.parent = "capa-a" + +# Los primitivos de capa-c referencian explícitamente elementos de capa-b +capa-c.belongs_to = "capa-b" +``` + +--- + +## 4. Lenguaje `.cf` (TOML) + +El formato `.cf` es TOML válido. Se eligió TOML sobre JSON por ser más legible para humanos y agentes — menos contexto, más señal. Los agentes de IA que ya conocen TOML pueden generar y modificar archivos `.cf` sin entrenamiento adicional. + +**Principio clave:** el lenguaje `.cf` trabaja exclusivamente con **primitivos geométricos**. No existe concepto de "muro", "puerta" o "habitación" en el motor base. Esa semántica es responsabilidad del usuario o de capas de abstracción futuras (`cadforge-arch` en v2+). El motor solo sabe de formas, posiciones, atributos visuales y relaciones espaciales. + +### Primitivos soportados en v1 + +| Primitivo | Descripción | +|---|---| +| `[[line]]` | Línea entre dos puntos | +| `[[polyline]]` | Polilínea de múltiples vértices, abierta o cerrada | +| `[[rect]]` | Rectángulo por origen, ancho y alto | +| `[[circle]]` | Círculo por centro y radio | +| `[[arc]]` | Arco por centro, radio y ángulos | +| `[[hatch]]` | Achurado sobre un contorno cerrado | +| `[[text]]` | Texto con posición, tamaño y alineación | +| `[[dim]]` | Cota lineal o angular | +| `[[point]]` | Punto de referencia | +| `[[group]]` | Agrupación de primitivos con id propio | + +### Atributos comunes + +Todos los primitivos comparten atributos visuales opcionales: + +```toml +id = "string" # identificador único en la capa +color = "#FFFFFF" # color en hex +weight = 0.35 # grosor de línea en mm +style = "solid" # solid | dashed | dotted | dashdot +layer = "capa-a" # capa a la que pertenece +visible = true +locked = false +``` + +### Ejemplos + +```toml +# capa-a.cf + +[[line]] +id = "ln-001" +from = [0.0, 0.0] +to = [8.5, 0.0] +weight = 0.50 + +[[polyline]] +id = "pl-001" +points = [[0.0, 0.0], [8.5, 0.0], [8.5, 6.0], [0.0, 6.0]] +closed = true +weight = 0.35 + +[[rect]] +id = "rc-001" +origin = [1.0, 1.0] +width = 3.5 +height = 4.0 +weight = 0.25 + +[[circle]] +id = "ci-001" +center = [4.0, 3.0] +radius = 0.5 + +[[arc]] +id = "ar-001" +center = [2.0, 2.0] +radius = 0.90 +from_angle = 0 +to_angle = 90 + +[[hatch]] +id = "ht-001" +boundary = "pl-001" # referencia al id del contorno cerrado +pattern = "ansi31" # ansi31 | ansi32 | ansi33 | ansi34 | solid | none +scale = 1.0 +angle = 45 + +[[text]] +id = "tx-001" +position = [4.0, 3.0] +content = "SALA" +size = 14 +align = "center" # left | center | right + +[[dim]] +id = "dm-001" +type = "linear" # linear | angular | radial +from = [0.0, 0.0] +to = [8.5, 0.0] +offset = 0.5 # distancia de la cota al elemento + +[[group]] +id = "gr-001" +members = ["ln-001", "rc-001", "tx-001"] +``` + +### Atributos globales de capa + +```toml +[layer] +name = "capa-a" +color = "#FFFFFF" +line_weight = 0.35 +visible = true +locked = false +``` + +--- + +## 5. Sistema de Capas y Constraints + +### Concepto + +Cada archivo `.cf` es una capa independiente. Las capas se orquestan desde `project.toml`. Las **constraints** son reglas declarativas que definen relaciones espaciales y de pertenencia entre capas. + +### Tipos de constraints + +| Constraint | Descripción | +|---|---| +| `spatial_dependency` | Si un objeto de la capa A se mueve, la capa B recibe notificación de conflicto | +| `parent` | Los objetos de la capa hija deben vivir dentro del bbox de la capa padre | +| `belongs_to` | Un objeto de la capa hija referencia explícitamente un objeto de la capa padre | + +### Comportamiento al compilar + +Cuando `cadforge build` detecta una violación de constraints: + +``` +⚠ CONSTRAINT VIOLATION + Layer: capa-b + Object: ln-042 + Constraint: spatial_dependency → capa-a + Detail: pl-001 in capa-a moved 0.30m east — ln-042 in capa-b now outside its bbox + Action: build continues with warning — update capa-b.cf to resolve +``` + +Las constraints no bloquean el build por defecto — emiten warnings. Se puede configurar `strict = true` en `project.toml` para bloquear. + +--- + +## 6. cadforge-cli — Comandos + +```bash +# Inicialización +cadforge new mi-proyecto # crea estructura de proyecto +cadforge init # inicializa en directorio existente + +# Compilación +cadforge build # compila todas las capas → DXF +cadforge build --layer muros # compila una capa específica +cadforge build --check # valida constraints sin generar output + +# Desarrollo +cadforge watch # modo watch: recompila al guardar cualquier .cf + +# Importación / Migración +cadforge import archivo.dxf # convierte DXF existente → .cf (por capas detectadas) +cadforge import archivo.dxf --layer muros # importa a capa específica + +# Visualización +cadforge view # abre cadforge-view con el proyecto actual +cadforge view --layer muros # abre solo una capa + +# Información +cadforge layers # lista capas del proyecto con estado +cadforge check # valida constraints y reporta conflictos + +# Configuración global +cadforge config set author "Arq. Nombre Apellido" +cadforge config set units m +cadforge config show +``` + +--- + +## 7. cadforge-view — Visor Vectorial + +### Filosofía de diseño + +El visor evoca la consola: **fondo negro, líneas vectoriales blancas/grises, tipografía monoespaciada**. No es un editor gráfico pesado — es una ventana de precisión sobre el archivo `.cf`. + +El rendering es **vectorial puro** — las líneas mantienen su calidad a cualquier zoom, sin pérdida de fidelidad como ocurriría en una TUI basada en caracteres. Se construye sobre una librería gráfica de bajo nivel (candidatos: `wgpu`, `femtovg`, `tiny-skia`). + +### Modos de operación + +**Modo lectura:** +- Renderiza el proyecto completo o por capas +- Zoom, pan, toggle de capas +- Muestra constraints activas + +**Modo edición (bidireccional):** +- Herramientas básicas: mover objeto, ajustar dimensión, agregar anotación +- Los cambios se traducen al `.cf` correspondiente **al guardar** (no en tiempo real) +- El archivo `.cf` es siempre la fuente de verdad + +**Modo calco:** +- La ventana del visor se vuelve semi-transparente (alpha configurable) +- Se puede posicionar sobre otra ventana (imagen, plano escaneado, referencia) +- El usuario traza sobre la referencia y los objetos se capturan como `.cf` +- **Auto-calco**: comando que analiza lo que está debajo de la ventana y propone objetos `.cf` detectados (experimental, v2) + +### Atajos de teclado + +``` +Z / X → zoom in / out +Flechas → pan +L → toggle lista de capas +1-9 → toggle visibilidad de capa por número +E → entrar a modo edición +C → entrar a modo calco +S → guardar cambios al .cf (en modo edición) +Esc → salir del modo actual +Q → cerrar visor +``` + +--- + +## 8. Importación DXF → `.cf` + +Una de las propuestas de valor más importantes: **migrar lo que ya existe**. + +```bash +cadforge import plano-existente.dxf +``` + +El importador: +1. Lee las capas del DXF original +2. Mapea cada capa DXF a un archivo `.cf` nuevo +3. Convierte geometría a objetos declarativos cuando es posible (líneas paralelas → `wall`, bloques → `column`, etc.) +4. Lo que no puede inferir lo convierte a `[[line]]` genéricas — siempre importable, siempre editable +5. Genera un `project.toml` con las capas detectadas + +``` +cadforge import plano.dxf + +✓ Detected 4 layers: MUROS, ESTRUCTURA, COTAS, TEXTO +✓ muros.cf — 23 walls inferred, 4 openings +✓ estructura.cf — 8 columns, 1 slab +✓ cotas.cf — 31 annotations (as [[annotation]]) +✓ texto.cf — 12 text objects (as [[annotation]]) +✓ project.toml — generated + +Import complete. Review .cf files and adjust inferred objects. +``` + +--- + +## 9. Integración con el Ecosistema Univerlab + +### gitkit +Los archivos `.cf` y `project.toml` son texto plano — `git diff` muestra exactamente qué cambió: +```diff +- thickness = 0.15 ++ thickness = 0.20 +``` +No hay comparación de binarios, no hay "Plano_Final_v3_ESTE_SI.dwg". + +### agent-canopy +Los agentes pueden leer y escribir archivos `.cf` directamente — TOML es un formato que los LLMs manejan bien. Un agente puede recibir instrucción: +> "Optimiza la distribución de luz natural del salón" + +Y modificar coordenadas y orientaciones en `muros.cf` de forma precisa y auditable. + +### ghscaff +Plantilla `cadforge` en `ghscaff` para inicializar nuevos proyectos CADforge con estructura de repo correcta desde el primer commit. + +--- + +## 10. Stack Técnico + +| Componente | Tecnología | +|---|---| +| Motor de geometría | `truck` (B-rep Rust), `nalgebra` | +| Output DXF | crate `dxf` | +| Parser TOML | `toml` crate | +| CLI | `clap` con derive macros | +| File watcher | `notify` crate | +| Rendering visor | `femtovg` / `tiny-skia` (vectorial, ligero) | +| Ventana visor | `winit` (cross-platform) | +| Transparencia/calco | APIs nativas de ventana por OS | + +--- + +## 11. Roadmap + +### MVP +- [ ] Parser TOML para archivos `.cf` +- [ ] Primitivos base: `line`, `polyline`, `rect`, `circle`, `arc`, `hatch`, `text`, `dim`, `point`, `group` +- [ ] Atributos comunes: color, weight, style, visible, locked +- [ ] Compilador `.cf` → DXF (2D, planos de planta) +- [ ] Sistema de capas con `project.toml` +- [ ] Constraints básicas: `parent`, `belongs_to` +- [ ] `cadforge build` y `cadforge watch` +- [ ] Live preview vía visor externo (LibreCAD, FreeCAD) +- [ ] Publicación en crates.io: `cadforge` (librería) + `cadforge-cli` + +### v1 +- [ ] Importador DXF → `.cf` con detección automática de primitivos +- [ ] `cadforge-view` — visor vectorial propio (fondo negro, líneas vectoriales) + - Modo lectura con zoom/pan y toggle de capas + - Modo edición básico con escritura bidireccional al guardar + - Modo calco con ventana semi-transparente +- [ ] Constraints con warnings en build: `spatial_dependency` +- [ ] Achurados estándar: ansi31, ansi32, ansi33, ansi34, solid +- [ ] Publicación `cadforge-view` en crates.io + +### v2 +- [ ] `cadforge-arch` — capa de abstracción arquitectónica sobre primitivos + - Objetos semánticos: `wall`, `opening`, `room`, `column`, `slab` + - Construidos sobre primitivos del motor base + - Publicado como crate independiente +- [ ] Arch-Linter — validación de normativas arquitectónicas + - Áreas mínimas por tipo de espacio + - Normativas locales configurables por país/región +- [ ] Auto-calco experimental — detección de geometría desde imagen de referencia +- [ ] Output adicional: STL (volumétrico), STEP (intercambio industrial) +- [ ] Constraints `strict = true` que bloquean el build + +--- + +## 12. No-Goals (v1) + +- No es un reemplazo de Revit o AutoCAD para flujos complejos de BIM +- No genera renders fotorrealistas +- No maneja modelos 3D complejos (solo extrusión simple de planta en v1) +- No requiere conexión a internet ni licencias propietarias +- No depende del MCP de Autodesk + +--- + +## 13. Contexto Académico + +`cadforge` es el proyecto de tesis de especialización en IA con enfoque en diseño arquitectónico. La hipótesis central es que tratar la arquitectura como código — con determinismo, versionado y agentes — representa un cambio de paradigma en el flujo de trabajo del diseño arquitectónico. + +El proyecto vive bajo [`univerlab`](https://github.com/univerlab) junto a `texforge`, `gitkit`, `ghscaff` y `agent-canopy`, siguiendo los mismos principios: binario standalone, offline first, sin scope creep. From fecbde805e19d4c58f90c0e597366cea05d219d1 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:31:05 -0500 Subject: [PATCH 12/16] chore: remove CLAUDE.md and ignore agent instruction files --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index d49f2d5..92e211d 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,7 @@ output.dxf preview.png preview.svg preview.meta.json + +# AI agent instruction files — not part of this repo +CLAUDE.md +AGENTS.md From 76faed9be123bf4046690f3c14dbc68a60c1bfb7 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 00:34:50 -0500 Subject: [PATCH 13/16] refactor(scaffold): drop AGENTS.md generation in favor of the cadforge schema command --- README.md | 10 +++++----- src/scaffold.rs | 38 +++----------------------------------- src/schema.rs | 6 +++--- 3 files changed, 11 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 2f32f4c..d39b803 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ a deterministic, AutoCAD-compatible DXF. Agents get first-class support: -- `cadforge schema` — full `.cf` language reference in one command (also written - to every new project as `AGENTS.md`). +- `cadforge schema` — full `.cf` language reference in one command; agents + self-discover the format without prior training. - `cadforge check --json` / `cadforge layers --json` — machine-readable validation reports. - `cadforge preview` — a faithful PNG render (real text, measured dimension @@ -62,7 +62,7 @@ Agents get first-class support: ### 🏗️ Project Management -- **Project Scaffolding** — `cadforge new` creates a complete multi-layer project (muros, puertas, mobiliario, cotas) plus an `AGENTS.md` that teaches any AI agent the format. +- **Project Scaffolding** — `cadforge new` creates a complete multi-layer project (muros, puertas, mobiliario, cotas) with meaningful architectural examples. - **Multi-Layer Compilation** — Compile all layers or target specific layers with `--layer`. Custom output path with `--output`. - **Auto-Rebuild** — `cadforge watch` monitors `.cf` and `.toml` files and auto-rebuilds DXF on changes with 300ms debounce. - **Code Formatting** — `cadforge fmt` normalizes `.cf` files. `--check` mode for CI validation. @@ -213,7 +213,7 @@ Run `cadforge schema` for the complete reference with all attributes. - `schema/` — Embedded `.cf` language reference for humans and agents - `parser/` — TOML parsing, primitive extraction, array-of-tables handling - `model/` — Data structures: Layer, Primitive, Project -- `scaffold/` — Multi-layer project creation with architectural examples + AGENTS.md +- `scaffold/` — Multi-layer project creation with architectural examples - `fmt/` — .cf file formatting and normalization - `watch/` — File system watcher with auto-rebuild and debounce - `importer/` — DXF → `.cf` migration @@ -229,7 +229,7 @@ Run `cadforge schema` for the complete reference with all attributes. | Build output | `./output.dxf` | DXF | | Preview output | `./preview.png`, `./preview.svg` | PNG / SVG | | Preview metadata | `./preview.meta.json` | JSON | -| Agent guide | `./AGENTS.md` | Markdown | +| Language reference | `cadforge schema` (stdout) | Markdown | --- diff --git a/src/scaffold.rs b/src/scaffold.rs index 856bb94..158c0a7 100644 --- a/src/scaffold.rs +++ b/src/scaffold.rs @@ -1,6 +1,5 @@ //! Scaffold — generates a new CADforge project structure. -use crate::schema::CF_REFERENCE; use anyhow::{bail, Result}; use std::fs; use std::path::Path; @@ -21,13 +20,13 @@ pub fn create_project(name: &str, parent: &Path) -> Result<()> { println!(" → puertas.cf"); println!(" → mobiliario.cf"); println!(" → cotas.cf"); - println!(" → AGENTS.md"); println!(" → .gitignore"); println!( "\n Run `cadforge serve --path {}` for a live preview,", name ); println!(" or `cadforge build --path {}` to compile to DXF.", name); + println!(" `cadforge schema` prints the .cf language reference."); Ok(()) } @@ -49,9 +48,9 @@ pub fn init_project(dir: &Path) -> Result<()> { println!(" → puertas.cf"); println!(" → mobiliario.cf"); println!(" → cotas.cf"); - println!(" → AGENTS.md"); println!(" → .gitignore"); println!("\n Run `cadforge serve` for a live preview."); + println!(" `cadforge schema` prints the .cf language reference."); Ok(()) } @@ -74,32 +73,6 @@ cotas = {{ file = "cotas.cf", locked = false }} let gitignore = "# CADforge output\noutput.dxf\npreview.png\npreview.svg\npreview.meta.json\n\n# Rust build artifacts\ntarget/\n"; fs::write(project_dir.join(".gitignore"), gitignore)?; - let agents_md = format!( - r#"# {name} — Agent Guide - -This is a CADforge project: geometry declared as TOML, compiled to DXF. -Edit the `.cf` layer files listed in `project.toml`; never edit `output.dxf` -or `preview.*` (generated). - -Feedback loop: - -1. Edit `.cf` files (format reference below). -2. `cadforge check --json` — validate and read constraint issues. -3. `cadforge preview` — render `preview.png` (faithful: real text, measured - dimensions, hatches) + `preview.meta.json` (entity bounding boxes in world - and pixel coordinates). Look at the image to verify your work. -4. `cadforge preview --highlight ` — re-render with labeled amber - markers around the entities you just touched, to confirm the change landed - where intended. -5. If a human is watching, `cadforge serve` gives them a live browser preview - that refreshes automatically on every save. - -{reference}"#, - name = name, - reference = CF_REFERENCE - ); - fs::write(project_dir.join("AGENTS.md"), agents_md)?; - let muros_cf = r##"[layer] name = "muros" color = "#FFFFFF" @@ -233,7 +206,7 @@ mod tests { assert!(project_dir.join("puertas.cf").exists()); assert!(project_dir.join("mobiliario.cf").exists()); assert!(project_dir.join("cotas.cf").exists()); - assert!(project_dir.join("AGENTS.md").exists()); + assert!(!project_dir.join("AGENTS.md").exists()); assert!(project_dir.join(".gitignore").exists()); let content = fs::read_to_string(project_dir.join("project.toml")).unwrap(); @@ -245,10 +218,6 @@ mod tests { assert!(gitignore.contains("preview.svg")); assert!(gitignore.contains("target/")); - let agents = fs::read_to_string(project_dir.join("AGENTS.md")).unwrap(); - assert!(agents.contains("mi-proyecto")); - assert!(agents.contains("[[line]]")); - let _ = fs::remove_dir_all(&tmp); } @@ -277,7 +246,6 @@ mod tests { assert!(tmp.join("puertas.cf").exists()); assert!(tmp.join("mobiliario.cf").exists()); assert!(tmp.join("cotas.cf").exists()); - assert!(tmp.join("AGENTS.md").exists()); assert!(tmp.join(".gitignore").exists()); let _ = fs::remove_dir_all(&tmp); diff --git a/src/schema.rs b/src/schema.rs index 4ebc5b7..dc5e0a1 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,8 +1,8 @@ //! Schema — the `.cf` language reference, printable via `cadforge schema`. //! -//! This is the self-discovery entry point for AI agents: one command dumps the -//! complete format so any agent can generate valid `.cf` files without prior -//! training. The same text is embedded into AGENTS.md by `cadforge new`. +//! This is the self-discovery entry point for AI agents and humans alike: one +//! command dumps the complete format so any agent can generate valid `.cf` +//! files without prior training. /// Complete `.cf` + `project.toml` reference in markdown. pub const CF_REFERENCE: &str = r##"# CADforge `.cf` Language Reference From 3d63da109ae7b44b11bf051c9bc758b7e33ebc97 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 08:00:29 -0500 Subject: [PATCH 14/16] feat(fmt): make cadforge fmt a real terraform-style formatter with canonical spacing, comment preservation and a semantic safety check --- examples/vivienda/achurados.cf | 2 +- examples/vivienda/cotas.cf | 5 +- examples/vivienda/mobiliario.cf | 8 +- examples/vivienda/muros.cf | 2 +- examples/vivienda/puertas.cf | 2 +- src/fmt.rs | 361 ++++++++++++++++++++++++++++---- 6 files changed, 325 insertions(+), 55 deletions(-) diff --git a/examples/vivienda/achurados.cf b/examples/vivienda/achurados.cf index c1f6105..b8b2dc8 100644 --- a/examples/vivienda/achurados.cf +++ b/examples/vivienda/achurados.cf @@ -67,4 +67,4 @@ from = [0.0, 3.5] to = [0.0, 4.5] style = "dashed" weight = 0.18 -color = "#00CC44" \ No newline at end of file +color = "#00CC44" diff --git a/examples/vivienda/cotas.cf b/examples/vivienda/cotas.cf index 8a4a4dd..6a6dc70 100644 --- a/examples/vivienda/cotas.cf +++ b/examples/vivienda/cotas.cf @@ -3,7 +3,6 @@ name = "cotas" color = "#FF4444" # ── Cotas exteriores ──────────────────────────────────────── - [[dim]] id = "dm-ancho-total" type = "linear" @@ -19,7 +18,6 @@ to = [0.0, 9.0] offset = -1.2 # ── Cotas interiores ─────────────────────────────────────── - [[dim]] id = "dm-sala-ancho" type = "linear" @@ -63,7 +61,6 @@ to = [0.0, 5.0] offset = -0.6 # ── Ejes de referencia ────────────────────────────────────── - [[line]] id = "ln-eje-v" from = [6.0, -0.5] @@ -76,4 +73,4 @@ id = "ln-eje-h" from = [-0.5, 5.0] to = [12.5, 5.0] style = "dashdot" -color = "#00CCCC" \ No newline at end of file +color = "#00CCCC" diff --git a/examples/vivienda/mobiliario.cf b/examples/vivienda/mobiliario.cf index a8b8ca5..7105ccc 100644 --- a/examples/vivienda/mobiliario.cf +++ b/examples/vivienda/mobiliario.cf @@ -3,7 +3,6 @@ name = "mobiliario" color = "#4488FF" # ── Sala ───────────────────────────────────────────────────── - # Sofa [[rect]] id = "rc-sofa" @@ -36,7 +35,6 @@ radius = 0.3 color = "#66AAFF" # ── Dormitorio 1 ──────────────────────────────────────────── - # Cama doble [[rect]] id = "rc-cama-d1" @@ -62,7 +60,6 @@ height = 0.5 color = "#66AAFF" # ── Dormitorio 2 ──────────────────────────────────────────── - # Cama individual [[rect]] id = "rc-cama-d2" @@ -87,7 +84,6 @@ radius = 0.25 color = "#66AAFF" # ── Cocina ────────────────────────────────────────────────── - # Cocina (mesada) [[rect]] id = "rc-mesada" @@ -112,7 +108,6 @@ height = 0.7 color = "#AADDFF" # ── Bano ───────────────────────────────────────────────────── - # Inodoro [[circle]] id = "ci-inodoro" @@ -136,7 +131,6 @@ height = 0.8 color = "#88CCFF" # ── Etiquetas de ambientes ────────────────────────────────── - [[text]] id = "tx-sala" position = [7.5, 6.8] @@ -165,4 +159,4 @@ size = 0.22 id = "tx-bano" position = [0.5, 3.8] content = "BANO" -size = 0.18 \ No newline at end of file +size = 0.18 diff --git a/examples/vivienda/muros.cf b/examples/vivienda/muros.cf index 6f4d37f..3043dcc 100644 --- a/examples/vivienda/muros.cf +++ b/examples/vivienda/muros.cf @@ -86,4 +86,4 @@ id = "rc-closet-d1" origin = [0.0, 7.0] width = 1.2 height = 2.0 -weight = 0.15 \ No newline at end of file +weight = 0.15 diff --git a/examples/vivienda/puertas.cf b/examples/vivienda/puertas.cf index 5fb8b13..f200d78 100644 --- a/examples/vivienda/puertas.cf +++ b/examples/vivienda/puertas.cf @@ -75,4 +75,4 @@ id = "ln-puerta-cocina" from = [8.0, 5.0] to = [8.8, 5.0] weight = 0.12 -style = "dashed" \ No newline at end of file +style = "dashed" diff --git a/src/fmt.rs b/src/fmt.rs index 70197f3..8f5bb4d 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -1,57 +1,282 @@ -//! Formatter — normalizes .cf files (sort keys, consistent spacing). +//! Formatter — normalizes `.cf` and `project.toml` files, analogous to +//! `terraform fmt`: canonical `key = value` spacing, exactly one blank line +//! between blocks, tidy arrays and inline tables. Comments are preserved. +//! Files that fail to parse — or whose formatted output would not reparse to +//! the same values — are left untouched. use crate::parser::parse_project; use anyhow::Result; -use std::path::Path; +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; +use toml_edit::{Array, Decor, DocumentMut, InlineTable, Item, KeyMut, RawString, Table, Value}; -/// Format all .cf files in a project. +/// Format `project.toml` and every `.cf` file in a project. pub fn format_project(project_dir: &Path, check_only: bool) -> Result<()> { let project = parse_project(&project_dir.join("project.toml"))?; - let mut changed = 0; - for (_name, entry) in &project.layers { - let cf_path = project_dir.join(&entry.file); - if !cf_path.exists() { - continue; + let mut files: BTreeSet = BTreeSet::new(); + files.insert(project_dir.join("project.toml")); + for entry in project.layers.values() { + files.insert(project_dir.join(&entry.file)); + } + if let Ok(dir) = std::fs::read_dir(project_dir) { + for entry in dir.flatten() { + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "cf") { + files.insert(path); + } } + } - let original = std::fs::read_to_string(&cf_path)?; - let formatted = format_cf(&original); - - if original != formatted { - if check_only { - println!("✗ {} — needs formatting", entry.file); - changed += 1; - } else { - std::fs::write(&cf_path, &formatted)?; - println!("✓ {} — formatted", entry.file); - changed += 1; - } + let mut changed = 0; + for path in files { + if !path.exists() { + continue; + } + let display = path + .strip_prefix(project_dir) + .unwrap_or(&path) + .display() + .to_string(); + let original = std::fs::read_to_string(&path)?; + let formatted = format_source(&original); + if original == formatted { + println!(" {display} — ok"); + } else if check_only { + println!("✗ {display} — needs formatting"); + changed += 1; } else { - println!(" {} — ok", entry.file); + std::fs::write(&path, &formatted)?; + println!("✓ {display} — formatted"); + changed += 1; } } if check_only && changed > 0 { - anyhow::bail!( - "{} file(s) need formatting. Run `cadforge fmt` to fix.", - changed - ); + anyhow::bail!("{changed} file(s) need formatting. Run `cadforge fmt` to fix."); } - if !check_only { - println!("✓ {} file(s) formatted", changed); + println!("✓ {changed} file(s) formatted"); } Ok(()) } -/// Format a single .cf file content. -fn format_cf(content: &str) -> String { - let doc: toml_edit::DocumentMut = match content.parse() { - Ok(d) => d, - Err(_) => return content.to_string(), +/// Format TOML source, guaranteeing the result parses to the same values. +fn format_source(content: &str) -> String { + let formatted = format_toml(content); + let before: Result = toml::from_str(content); + let after: Result = toml::from_str(&formatted); + match (before, after) { + (Ok(b), Ok(a)) if b == a => formatted, + _ => content.to_string(), + } +} + +fn format_toml(content: &str) -> String { + let Ok(mut doc) = content.parse::() else { + return content.to_string(); }; - doc.to_string() + + let mut first = true; + for (mut key, item) in doc.as_table_mut().iter_mut() { + match item { + Item::Table(table) => { + normalize_header(table.decor_mut(), first); + normalize_table(table); + } + Item::ArrayOfTables(tables) => { + for table in tables.iter_mut() { + normalize_header(table.decor_mut(), first); + normalize_table(table); + first = false; + } + } + Item::Value(value) => { + normalize_key(&mut key); + normalize_value(value); + } + Item::None => {} + } + first = false; + } + + let trailing = comments_only(&raw_text(Some(doc.trailing())), true); + doc.set_trailing(trailing); + + let out = doc.to_string(); + let trimmed = out.trim_end_matches('\n'); + if trimmed.is_empty() { + out + } else { + format!("{trimmed}\n") + } +} + +fn normalize_table(table: &mut Table) { + for (mut key, item) in table.iter_mut() { + match item { + Item::Value(value) => { + normalize_key(&mut key); + normalize_value(value); + } + Item::Table(child) => { + if child.is_dotted() { + // dotted key (e.g. `cotas.parent = "muros"`): only touch values + normalize_dotted(child); + } else { + normalize_header(child.decor_mut(), false); + normalize_table(child); + } + } + Item::ArrayOfTables(tables) => { + for child in tables.iter_mut() { + normalize_header(child.decor_mut(), false); + normalize_table(child); + } + } + Item::None => {} + } + } +} + +fn normalize_dotted(table: &mut Table) { + for (_, item) in table.iter_mut() { + match item { + Item::Value(value) => normalize_value(value), + Item::Table(child) if child.is_dotted() => normalize_dotted(child), + _ => {} + } + } +} + +/// `[header]` / `[[header]]` prefix: one blank line between blocks (none for +/// the first), preceding comments preserved one per line. +fn normalize_header(decor: &mut Decor, first: bool) { + let prefix = comments_only(&raw_text(decor.prefix()), !first); + let suffix = trailing_comment(decor.suffix()); + decor.set_prefix(prefix); + decor.set_suffix(suffix); +} + +/// Key of a `key = value` pair: comments kept, a single leading blank line +/// kept if the author separated the pair from the previous one. +fn normalize_key(key: &mut KeyMut) { + let raw = raw_text(key.leaf_decor().prefix()); + let blank = raw.split('#').next().unwrap_or("").contains('\n'); + let prefix = comments_only(&raw, blank); + let decor = key.leaf_decor_mut(); + decor.set_prefix(prefix); + decor.set_suffix(" "); +} + +fn normalize_value(value: &mut Value) { + let suffix = trailing_comment(value.decor().suffix()); + match value { + Value::Array(array) => { + normalize_array(array); + } + Value::InlineTable(table) => { + normalize_inline_table(table); + } + _ => {} + } + let decor = value.decor_mut(); + decor.set_prefix(" "); + decor.set_suffix(suffix); +} + +/// Arrays keep the author's single-line vs multi-line choice; multi-line +/// arrays get one element per line, four-space indent and a trailing comma. +/// Arrays containing comments are left untouched. +fn normalize_array(array: &mut Array) { + if array_has_comment(array) { + return; + } + let multiline = raw_text(Some(array.trailing())).contains('\n') + || array.iter().any(|v| { + raw_text(v.decor().prefix()).contains('\n') + || raw_text(v.decor().suffix()).contains('\n') + }); + + if multiline { + for value in array.iter_mut() { + if let Value::Array(inner) = value { + normalize_inline_array(inner); + } + let decor = value.decor_mut(); + decor.set_prefix("\n "); + decor.set_suffix(""); + } + array.set_trailing("\n"); + array.set_trailing_comma(true); + } else { + normalize_inline_array(array); + } +} + +fn normalize_inline_array(array: &mut Array) { + if array_has_comment(array) { + return; + } + let mut first = true; + for value in array.iter_mut() { + if let Value::Array(inner) = value { + normalize_inline_array(inner); + } + let decor = value.decor_mut(); + decor.set_prefix(if first { "" } else { " " }); + decor.set_suffix(""); + first = false; + } + array.set_trailing(""); + array.set_trailing_comma(false); +} + +fn normalize_inline_table(table: &mut InlineTable) { + let len = table.len(); + for (i, (mut key, value)) in table.iter_mut().enumerate() { + let decor = key.leaf_decor_mut(); + decor.set_prefix(" "); + decor.set_suffix(" "); + let decor = value.decor_mut(); + decor.set_prefix(" "); + decor.set_suffix(if i + 1 == len { " " } else { "" }); + } +} + +fn array_has_comment(array: &Array) -> bool { + raw_text(Some(array.trailing())).contains('#') + || array.iter().any(|v| { + raw_text(v.decor().prefix()).contains('#') || raw_text(v.decor().suffix()).contains('#') + }) +} + +/// Extract `#` comment lines from raw decor text, optionally preceded by one +/// blank line; everything else (stray whitespace, extra blanks) is dropped. +fn comments_only(raw: &str, leading_blank: bool) -> String { + let mut out = String::new(); + if leading_blank { + out.push('\n'); + } + for line in raw.lines().map(str::trim).filter(|l| l.starts_with('#')) { + out.push_str(line); + out.push('\n'); + } + out +} + +/// Keep a same-line trailing comment (` # like this`), drop plain whitespace. +fn trailing_comment(raw: Option<&RawString>) -> String { + let text = raw_text(raw); + if text.contains('#') { + format!(" {}", text.trim()) + } else { + String::new() + } +} + +fn raw_text(raw: Option<&RawString>) -> String { + raw.and_then(|r| r.as_str()).unwrap_or("").to_string() } #[cfg(test)] @@ -59,17 +284,71 @@ mod tests { use super::*; #[test] - fn format_preserves_valid_toml() { - let input = "[layer]\nname = \"test\"\ncolor = \"#FFFFFF\"\n\n[[line]]\nid = \"ln-001\"\nfrom = [0.0, 0.0]\nto = [10.0, 0.0]\n"; - let output = format_cf(input); - assert!(output.contains("[layer]")); - assert!(output.contains("[[line]]")); + fn normalizes_spacing_and_blank_lines() { + let input = "[layer]\nname=\"test\"\ncolor = \"#FFFFFF\"\n\n\n\n[[line]]\nid=\"ln-001\"\nfrom=[0.0,0.0]\nto = [ 10.0 , 0.0 ]\n[[line]]\nid = \"ln-002\"\nfrom = [0.0, 1.0]\nto = [10.0, 1.0]\n"; + let expected = "[layer]\nname = \"test\"\ncolor = \"#FFFFFF\"\n\n[[line]]\nid = \"ln-001\"\nfrom = [0.0, 0.0]\nto = [10.0, 0.0]\n\n[[line]]\nid = \"ln-002\"\nfrom = [0.0, 1.0]\nto = [10.0, 1.0]\n"; + assert_eq!(format_source(input), expected); + } + + #[test] + fn preserves_comments() { + let input = "[layer]\nname = \"muros\"\n\n# Perímetro exterior\n[[polyline]]\nid = \"pl-001\" # principal\npoints = [[0.0, 0.0], [5.0, 0.0]]\n"; + let output = format_source(input); + assert!(output.contains("# Perímetro exterior\n[[polyline]]")); + assert!(output.contains("id = \"pl-001\" # principal")); + } + + #[test] + fn normalizes_multiline_point_arrays() { + let input = "[[polyline]]\nid = \"pl-001\"\npoints = [\n [0.30,0.0],\n [1.55, 0.0],\n [1.456, 0.530]\n]\nclosed = true\n"; + let expected = "[[polyline]]\nid = \"pl-001\"\npoints = [\n [0.30, 0.0],\n [1.55, 0.0],\n [1.456, 0.530],\n]\nclosed = true\n"; + assert_eq!(format_source(input), expected); + } + + #[test] + fn normalizes_inline_tables() { + let input = + "[project]\nname = \"x\"\n\n[layers]\nmuros = {file=\"muros.cf\",locked=false}\n"; + let output = format_source(input); + assert!(output.contains("muros = { file = \"muros.cf\", locked = false }")); + } + + #[test] + fn keeps_dotted_constraint_keys_intact() { + let input = "[constraints]\ncotas.parent = \"muros\"\ncotas.belongs_to = \"muros\"\n"; + let output = format_source(input); + assert!(output.contains("cotas.parent = \"muros\"")); + assert!(output.contains("cotas.belongs_to = \"muros\"")); + } + + #[test] + fn keeps_blank_line_groups_inside_blocks() { + let input = "[layer]\nname = \"x\"\n\n\ncolor = \"#FFFFFF\"\n"; + let output = format_source(input); + assert!(output.contains("name = \"x\"\n\ncolor")); + } + + #[test] + fn is_idempotent_and_meaning_preserving() { + for path in [ + concat!(env!("CARGO_MANIFEST_DIR"), "/examples/taller/escalera.cf"), + concat!(env!("CARGO_MANIFEST_DIR"), "/examples/taller/planta.cf"), + concat!(env!("CARGO_MANIFEST_DIR"), "/examples/taller/cotas.cf"), + concat!(env!("CARGO_MANIFEST_DIR"), "/examples/taller/project.toml"), + ] { + let original = std::fs::read_to_string(path).unwrap(); + let once = format_source(&original); + let twice = format_source(&once); + assert_eq!(once, twice, "fmt not idempotent for {path}"); + let before: toml::Value = toml::from_str(&original).unwrap(); + let after: toml::Value = toml::from_str(&once).unwrap(); + assert_eq!(before, after, "fmt changed meaning of {path}"); + } } #[test] - fn format_returns_original_on_parse_error() { + fn returns_original_on_parse_error() { let input = "invalid [[[ toml"; - let output = format_cf(input); - assert_eq!(output, input); + assert_eq!(format_source(input), input); } } From 18f5766b3e2fe1cb3cf6055a2394b53db9d059a1 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 08:31:10 -0500 Subject: [PATCH 15/16] fix(import): dedup dimension companion graphics, recover layer and entity styles, and export dim offsets perpendicular to the measured axis --- src/color.rs | 24 +++ src/dxf_writer.rs | 29 ++- src/importer.rs | 427 ++++++++++++++++++++++++++++++++----------- tests/integration.rs | 88 +++++++++ 4 files changed, 453 insertions(+), 115 deletions(-) diff --git a/src/color.rs b/src/color.rs index 9c97039..ba1da47 100644 --- a/src/color.rs +++ b/src/color.rs @@ -16,6 +16,22 @@ pub fn hex_to_aci(hex: &str) -> u8 { } } +/// Hex color from an ACI color index (inverse of `hex_to_aci`). +pub fn aci_to_hex(index: u8) -> &'static str { + match index { + 1 => "#FF0000", // red + 2 => "#FFFF00", // yellow + 3 => "#00FF00", // green + 4 => "#00FFFF", // cyan + 5 => "#0000FF", // blue + 6 => "#FF00FF", // magenta + 7 => "#FFFFFF", // white + 8 => "#808080", // dark grey + 9 => "#C0C0C0", // light grey + _ => "#FFFFFF", // default white + } +} + /// Convert hex color string to 24-bit integer for DXF true color. pub fn hex_to_24bit(hex: &str) -> i32 { let hex = hex.trim_start_matches('#'); @@ -39,6 +55,14 @@ mod tests { assert_eq!(hex_to_aci("#123456"), 7); // unknown → white } + #[test] + fn aci_roundtrips_standard_palette() { + for index in 1..=9u8 { + assert_eq!(hex_to_aci(aci_to_hex(index)), index); + } + assert_eq!(aci_to_hex(42), "#FFFFFF"); // unknown → white + } + #[test] fn hex_to_24bit_parses_correctly() { assert_eq!(hex_to_24bit("#FF0000"), 0xFF0000); diff --git a/src/dxf_writer.rs b/src/dxf_writer.rs index 0fc35d0..e3f4dd3 100644 --- a/src/dxf_writer.rs +++ b/src/dxf_writer.rs @@ -193,28 +193,37 @@ impl DxfWriter { layer: &str, style: &EntityStyle, ) { + let dx = x2 - x1; + let dy = y2 - y1; + let len = (dx * dx + dy * dy).sqrt(); + if len < 1e-9 { + return; + } + // Offset along the normal, matching the preview renderer + let (nx, ny) = (-dy / len, dx / len); + let (ax, ay) = (x1 + nx * offset, y1 + ny * offset); + let (bx, by) = (x2 + nx * offset, y2 + ny * offset); + let dim = dxf::entities::RotatedDimension { definition_point_2: Point::new(x1, y1, 0.0), definition_point_3: Point::new(x2, y2, 0.0), - insertion_point: Point::new((x1 + x2) / 2.0, y1 + offset, 0.0), + insertion_point: Point::new((ax + bx) / 2.0, (ay + by) / 2.0, 0.0), + rotation_angle: dy.atan2(dx).to_degrees(), ..Default::default() }; self.add_entity(EntityType::RotatedDimension(dim), layer, style); // Also emit dimension lines and text as explicit entities for compatibility - let mid_x = (x1 + x2) / 2.0; - let mid_y = (y1 + y2) / 2.0 + offset; - // Extension lines - self.line(x1, y1, x1, y1 + offset, layer, style); - self.line(x2, y2, x2, y2 + offset, layer, style); + self.line(x1, y1, ax, ay, layer, style); + self.line(x2, y2, bx, by, layer, style); // Dimension line - self.line(x1, y1 + offset, x2, y2 + offset, layer, style); - // Dimension text + self.line(ax, ay, bx, by, layer, style); + // Dimension text, raised half its height off the dimension line let text_style = EntityStyle::default(); self.text( - mid_x, - mid_y + text_height * 0.5, + (ax + bx) / 2.0 + nx * text_height * 0.5, + (ay + by) / 2.0 + ny * text_height * 0.5, text_height, label, layer, diff --git a/src/importer.rs b/src/importer.rs index 88ff6a4..306b384 100644 --- a/src/importer.rs +++ b/src/importer.rs @@ -1,5 +1,6 @@ //! DXF importer — converts DXF layers/entities into CADforge `.cf` + `project.toml`. +use crate::color::aci_to_hex; use anyhow::{anyhow, Context, Result}; use dxf::entities::EntityType; use dxf::Drawing; @@ -7,23 +8,98 @@ use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; +/// Per-entity style recovered from DXF (true color, lineweight, line type). #[derive(Default)] -struct LayerFile { - entities: Vec, - counters: BTreeMap<&'static str, usize>, +struct StyleAttrs { + color: Option, + weight: Option, + line_style: Option<&'static str>, } -impl LayerFile { - fn next_id(&mut self, prefix: &'static str) -> String { - let n = self - .counters - .entry(prefix) - .and_modify(|v| *v += 1) - .or_insert(1); - format!("{prefix}-{n:03}") +impl StyleAttrs { + fn from_common(common: &dxf::entities::EntityCommon) -> Self { + let color = if common.color_24_bit > 0 { + Some(format!("#{:06X}", common.color_24_bit)) + } else { + common + .color + .index() + .filter(|i| (1..=9).contains(i)) + .map(|i| aci_to_hex(i).to_string()) + }; + let weight = (common.lineweight_enum_value > 0) + .then(|| f64::from(common.lineweight_enum_value) / 100.0); + let line_style = match common.line_type_name.to_ascii_uppercase().as_str() { + "DASHED" => Some("dashed"), + "DOTTED" => Some("dotted"), + "DASHDOT" => Some("dashdot"), + _ => None, + }; + StyleAttrs { + color, + weight, + line_style, + } + } + + fn emit(&self, out: &mut String) { + if let Some(c) = &self.color { + out.push_str(&format!("color = \"{}\"\n", c)); + } + if let Some(w) = self.weight { + out.push_str(&format!("weight = {}\n", w)); + } + if let Some(s) = self.line_style { + out.push_str(&format!("style = \"{}\"\n", s)); + } } } +enum Shape { + Line { + from: [f64; 2], + to: [f64; 2], + }, + Polyline { + points: Vec<[f64; 2]>, + closed: bool, + }, + Circle { + center: [f64; 2], + radius: f64, + }, + Arc { + center: [f64; 2], + radius: f64, + from_angle: f64, + to_angle: f64, + }, + Text { + position: [f64; 2], + content: String, + size: f64, + }, + Point { + position: [f64; 2], + }, + Dim { + from: [f64; 2], + to: [f64; 2], + offset: f64, + }, +} + +struct Imported { + shape: Shape, + style: StyleAttrs, +} + +#[derive(Default)] +struct LayerFile { + entities: Vec, + counters: BTreeMap<&'static str, usize>, +} + pub fn import_dxf(input: &Path, output_dir: &Path, layer_filter: Option<&str>) -> Result<()> { if !input.exists() { return Err(anyhow!( @@ -36,10 +112,20 @@ pub fn import_dxf(input: &Path, output_dir: &Path, layer_filter: Option<&str>) - .with_context(|| format!("Cannot create output dir {}", output_dir.display()))?; let mut layers: BTreeMap = BTreeMap::new(); + let mut layer_colors: BTreeMap = BTreeMap::new(); let mut unsupported = 0usize; match Drawing::load_file(input) { Ok(drawing) => { + for layer in drawing.layers() { + if let Some(index) = layer.color.index() { + layer_colors.insert( + normalize_layer_name(&layer.name), + aci_to_hex(index).to_string(), + ); + } + } + for entity in drawing.entities() { let layer_name = normalize_layer_name(&entity.common.layer); if let Some(filter) = layer_filter { @@ -48,100 +134,57 @@ pub fn import_dxf(input: &Path, output_dir: &Path, layer_filter: Option<&str>) - } } - let layer = layers.entry(layer_name.clone()).or_default(); - match &entity.specific { - EntityType::Line(e) => { - let id = layer.next_id("ln"); - layer.entities.push(format!( - "[[line]]\nid = \"{}\"\nfrom = [{}, {}]\nto = [{}, {}]\n", - id, - n(e.p1.x), - n(e.p1.y), - n(e.p2.x), - n(e.p2.y) - )); - } - EntityType::LwPolyline(e) => { - if e.vertices.len() >= 2 { - let id = layer.next_id("pl"); - let points = e - .vertices - .iter() - .map(|v| format!("[{}, {}]", n(v.x), n(v.y))) - .collect::>() - .join(", "); - layer.entities.push(format!( - "[[polyline]]\nid = \"{}\"\npoints = [{}]\nclosed = {}\n", - id, - points, - e.is_closed() - )); - } - } - EntityType::Circle(e) => { - let id = layer.next_id("ci"); - layer.entities.push(format!( - "[[circle]]\nid = \"{}\"\ncenter = [{}, {}]\nradius = {}\n", - id, - n(e.center.x), - n(e.center.y), - n(e.radius) - )); - } - EntityType::Arc(e) => { - let id = layer.next_id("ar"); - layer.entities.push(format!( - "[[arc]]\nid = \"{}\"\ncenter = [{}, {}]\nradius = {}\nfrom_angle = {}\nto_angle = {}\n", - id, - n(e.center.x), - n(e.center.y), - n(e.radius), - n(e.start_angle), - n(e.end_angle) - )); - } - EntityType::Text(e) => { - let id = layer.next_id("tx"); - layer.entities.push(format!( - "[[text]]\nid = \"{}\"\nposition = [{}, {}]\ncontent = \"{}\"\nsize = {}\n", - id, - n(e.location.x), - n(e.location.y), - escape_string(&e.value), - n(e.text_height.max(0.1)) - )); - } - EntityType::ModelPoint(e) => { - let id = layer.next_id("pt"); - layer.entities.push(format!( - "[[point]]\nid = \"{}\"\nposition = [{}, {}]\n", - id, - n(e.location.x), - n(e.location.y) - )); - } + let style = StyleAttrs::from_common(&entity.common); + let shape = match &entity.specific { + EntityType::Line(e) => Some(Shape::Line { + from: [e.p1.x, e.p1.y], + to: [e.p2.x, e.p2.y], + }), + EntityType::LwPolyline(e) => (e.vertices.len() >= 2).then(|| Shape::Polyline { + points: e.vertices.iter().map(|v| [v.x, v.y]).collect(), + closed: e.is_closed(), + }), + EntityType::Circle(e) => Some(Shape::Circle { + center: [e.center.x, e.center.y], + radius: e.radius, + }), + EntityType::Arc(e) => Some(Shape::Arc { + center: [e.center.x, e.center.y], + radius: e.radius, + from_angle: e.start_angle, + to_angle: e.end_angle, + }), + EntityType::Text(e) => Some(Shape::Text { + position: [e.location.x, e.location.y], + content: e.value.clone(), + size: e.text_height.max(0.1), + }), + EntityType::ModelPoint(e) => Some(Shape::Point { + position: [e.location.x, e.location.y], + }), EntityType::RotatedDimension(e) => { - let id = layer.next_id("dm"); - let from_x = e.definition_point_2.x; - let from_y = e.definition_point_2.y; - let to_x = e.definition_point_3.x; - let to_y = e.definition_point_3.y; - let offset = e.insertion_point.y - (from_y + to_y) / 2.0; - layer.entities.push(format!( - "[[dim]]\nid = \"{}\"\ntype = \"linear\"\nfrom = [{}, {}]\nto = [{}, {}]\noffset = {}\n", - id, - n(from_x), - n(from_y), - n(to_x), - n(to_y), - n(offset) - )); + let from = [e.definition_point_2.x, e.definition_point_2.y]; + let to = [e.definition_point_3.x, e.definition_point_3.y]; + dim_offset(from, to, [e.insertion_point.x, e.insertion_point.y]) + .map(|offset| Shape::Dim { from, to, offset }) } _ => { unsupported += 1; + None } + }; + if let Some(shape) = shape { + layers + .entry(layer_name) + .or_default() + .entities + .push(Imported { shape, style }); } } + + for layer in layers.values_mut() { + remove_dim_companions(&mut layer.entities); + } } Err(_) => { let content = fs::read_to_string(input) @@ -197,23 +240,42 @@ pub fn import_dxf(input: &Path, output_dir: &Path, layer_filter: Option<&str>) - ); let mut imported_layers = 0usize; - for (layer_name, layer_file) in &layers { + for (layer_name, layer_file) in &mut layers { imported_layers += 1; let file_name = format!("{}.cf", sanitize_for_filename(layer_name)); + let color = layer_colors + .get(layer_name) + .map(String::as_str) + .unwrap_or("#FFFFFF"); let mut cf = format!( - "[layer]\nname = \"{}\"\ncolor = \"#FFFFFF\"\n\n", - escape_string(layer_name) + "[layer]\nname = \"{}\"\ncolor = \"{}\"\n\n", + escape_string(layer_name), + color ); if layer_file.entities.is_empty() { cf.push_str("[[line]]\nfrom = [0.0, 0.0]\nto = [1.0, 0.0]\n"); } else { - for e in &layer_file.entities { - cf.push_str(e); + for entity in &layer_file.entities { + let (prefix, body) = emit_shape(&entity.shape); + let count = layer_file + .counters + .entry(prefix) + .and_modify(|v| *v += 1) + .or_insert(1); + cf.push_str(&format!( + "[[{}]]\nid = \"{prefix}-{count:03}\"\n", + header_for(prefix) + )); + cf.push_str(&body); + entity.style.emit(&mut cf); cf.push('\n'); } } - fs::write(output_dir.join(&file_name), cf) - .with_context(|| format!("Cannot write layer file {}", file_name))?; + fs::write( + output_dir.join(&file_name), + cf.trim_end().to_string() + "\n", + ) + .with_context(|| format!("Cannot write layer file {}", file_name))?; project_toml.push_str(&format!( "\"{}\" = {{ file = \"{}\", locked = false }}\n", escape_string(layer_name), @@ -235,6 +297,161 @@ pub fn import_dxf(input: &Path, output_dir: &Path, layer_filter: Option<&str>) - Ok(()) } +/// Recover the perpendicular dimension offset from the insertion point. +fn dim_offset(from: [f64; 2], to: [f64; 2], insertion: [f64; 2]) -> Option { + let dx = to[0] - from[0]; + let dy = to[1] - from[1]; + let len = (dx * dx + dy * dy).sqrt(); + if len < 1e-9 { + return None; + } + let (nx, ny) = (-dy / len, dx / len); + let mid = [(from[0] + to[0]) / 2.0, (from[1] + to[1]) / 2.0]; + Some((insertion[0] - mid[0]) * nx + (insertion[1] - mid[1]) * ny) +} + +/// Drop the extension/dimension lines and label text that `cadforge build` +/// emits alongside each DIMENSION entity for viewer compatibility; the +/// re-created `[[dim]]` regenerates all of them. Foreign DXFs are unaffected +/// (their dimension graphics live in blocks, not loose entities). +fn remove_dim_companions(entities: &mut Vec) { + const TOL: f64 = 1e-6; + let close = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).abs() < TOL && (a[1] - b[1]).abs() < TOL; + + let dims: Vec<([f64; 2], [f64; 2], f64)> = entities + .iter() + .filter_map(|e| match e.shape { + Shape::Dim { from, to, offset } => Some((from, to, offset)), + _ => None, + }) + .collect(); + + let mut keep = vec![true; entities.len()]; + for (from, to, offset) in dims { + let dx = to[0] - from[0]; + let dy = to[1] - from[1]; + let len = (dx * dx + dy * dy).sqrt(); + if len < 1e-9 { + continue; + } + let (nx, ny) = (-dy / len, dx / len); + let a = [from[0] + nx * offset, from[1] + ny * offset]; + let b = [to[0] + nx * offset, to[1] + ny * offset]; + let mid = [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0]; + + for (cf, ct) in [(from, a), (to, b), (a, b)] { + if let Some(i) = (0..entities.len()).find(|&i| { + keep[i] + && matches!(entities[i].shape, Shape::Line { from: lf, to: lt } + if (close(lf, cf) && close(lt, ct)) || (close(lf, ct) && close(lt, cf))) + }) { + keep[i] = false; + } + } + if let Some(i) = (0..entities.len()).find(|&i| { + keep[i] + && matches!(entities[i].shape, Shape::Text { position, size, .. } + if close(position, [mid[0] + nx * size * 0.5, mid[1] + ny * size * 0.5])) + }) { + keep[i] = false; + } + } + + let mut it = keep.into_iter(); + entities.retain(|_| it.next().unwrap_or(true)); +} + +/// Render a shape body (without `[[header]]`/`id`); returns (id prefix, body). +fn emit_shape(shape: &Shape) -> (&'static str, String) { + match shape { + Shape::Line { from, to } => ( + "ln", + format!( + "from = [{}, {}]\nto = [{}, {}]\n", + n(from[0]), + n(from[1]), + n(to[0]), + n(to[1]) + ), + ), + Shape::Polyline { points, closed } => { + let pts = points + .iter() + .map(|p| format!("[{}, {}]", n(p[0]), n(p[1]))) + .collect::>() + .join(", "); + ("pl", format!("points = [{}]\nclosed = {}\n", pts, closed)) + } + Shape::Circle { center, radius } => ( + "ci", + format!( + "center = [{}, {}]\nradius = {}\n", + n(center[0]), + n(center[1]), + n(*radius) + ), + ), + Shape::Arc { + center, + radius, + from_angle, + to_angle, + } => ( + "ar", + format!( + "center = [{}, {}]\nradius = {}\nfrom_angle = {}\nto_angle = {}\n", + n(center[0]), + n(center[1]), + n(*radius), + n(*from_angle), + n(*to_angle) + ), + ), + Shape::Text { + position, + content, + size, + } => ( + "tx", + format!( + "position = [{}, {}]\ncontent = \"{}\"\nsize = {}\n", + n(position[0]), + n(position[1]), + escape_string(content), + n(*size) + ), + ), + Shape::Point { position } => ( + "pt", + format!("position = [{}, {}]\n", n(position[0]), n(position[1])), + ), + Shape::Dim { from, to, offset } => ( + "dm", + format!( + "type = \"linear\"\nfrom = [{}, {}]\nto = [{}, {}]\noffset = {}\n", + n(from[0]), + n(from[1]), + n(to[0]), + n(to[1]), + n(*offset) + ), + ), + } +} + +fn header_for(prefix: &str) -> &'static str { + match prefix { + "ln" => "line", + "pl" => "polyline", + "ci" => "circle", + "ar" => "arc", + "tx" => "text", + "pt" => "point", + "dm" => "dim", + _ => "line", + } +} + fn n(v: f64) -> String { format!("{:.4}", v) } diff --git a/tests/integration.rs b/tests/integration.rs index a33e92c..cce4778 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -426,3 +426,91 @@ fn import_generated_dxf_creates_cadforge_project() { let _ = fs::remove_dir_all(imported); } + +#[test] +fn import_roundtrip_recovers_dims_styles_and_colors() { + let dir = Path::new("/tmp/cadforge_roundtrip_fidelity"); + let _ = fs::remove_dir_all(dir); + fs::create_dir_all(dir).unwrap(); + fs::write( + dir.join("project.toml"), + "[project]\nname = \"rt\"\nunits = \"m\"\n\n[layers]\nplano = { file = \"plano.cf\", locked = false }\n", + ) + .unwrap(); + fs::write( + dir.join("plano.cf"), + r##"[layer] +name = "plano" +color = "#FF0000" + +[[line]] +id = "ln-base" +from = [0.0, 0.0] +to = [8.0, 0.0] +color = "#FF5050" +weight = 0.5 +style = "dashed" + +[[text]] +id = "tx-sala" +position = [4.0, 3.0] +content = "SALA" +size = 0.25 + +[[dim]] +id = "dm-h" +from = [0.0, 0.0] +to = [8.0, 0.0] +offset = -0.8 + +[[dim]] +id = "dm-v" +from = [0.0, 0.0] +to = [0.0, 6.0] +offset = -1.2 +"##, + ) + .unwrap(); + + let dxf_path = dir.join("output.dxf"); + compile_project(dir, None, Some(&dxf_path)).unwrap(); + + let imported = Path::new("/tmp/cadforge_roundtrip_fidelity_out"); + let _ = fs::remove_dir_all(imported); + import_dxf(&dxf_path, imported, None).unwrap(); + let cf = fs::read_to_string(imported.join("plano.cf")).unwrap(); + + // Layer color survives via the DXF layer table (ACI) + assert!( + cf.contains("color = \"#FF0000\""), + "layer color lost:\n{cf}" + ); + // Entity style survives via true color, lineweight and line type + assert!( + cf.contains("color = \"#FF5050\""), + "entity color lost:\n{cf}" + ); + assert!(cf.contains("weight = 0.5"), "entity weight lost:\n{cf}"); + assert!(cf.contains("style = \"dashed\""), "line style lost:\n{cf}"); + // Both dims come back as dims with their perpendicular offsets intact + assert_eq!(cf.matches("[[dim]]").count(), 2, "dims lost:\n{cf}"); + assert!(cf.contains("offset = -0.8000"), "horizontal offset:\n{cf}"); + assert!(cf.contains("offset = -1.2000"), "vertical offset:\n{cf}"); + // Companion graphics (3 lines + 1 label per dim) are deduplicated + assert_eq!( + cf.matches("[[line]]").count(), + 1, + "dim companion lines not deduped:\n{cf}" + ); + assert_eq!( + cf.matches("[[text]]").count(), + 1, + "dim label texts not deduped:\n{cf}" + ); + + // The reimported project must still compile + compile_project(imported, None, None).unwrap(); + + let _ = fs::remove_dir_all(dir); + let _ = fs::remove_dir_all(imported); +} From 5e1d4eee29dfaf4b69f9992ec975889cee0567aa Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Fri, 12 Jun 2026 13:55:28 -0500 Subject: [PATCH 16/16] docs(readme): replace the garbled banner with cadForge ascii art inside a code fence --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d39b803..ebe8a10 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,16 @@ -██████ ████████ ██████ ████████ ██ ██ ██ ███████ ████████ -██░░███ ░░███░░███░░░░░███ ░░███░░███░███ ░███ ██░███░░░░░░░███░ -░███ ░░░ ░███ ░███ ███████ ░███ ░░░ ░███ ░██████░░█████ ░███ -░███ ███ ░███ ░███ ███░░███ ░███ ░███ ░███░░░ ░███░░█ ░███ -░░██████ ░███████░░████████ ░███ ░███████████ ███████ ░████████ - ░░░░░░ ░░░░░░░ ░░░░░░░░ ░░░ ░░░░░░░░░░░ ░░░░░░ ░░░░░░░ +```text + █████ ███████████ + ░░███ ░░███░░░░░░█ + ██████ ██████ ███████ ░███ █ ░ ██████ ████████ ███████ ██████ + ███░░███ ░░░░░███ ███░░███ ░███████ ███░░███░░███░░███ ███░░███ ███░░███ +░███ ░░░ ███████ ░███ ░███ ░███░░░█ ░███ ░███ ░███ ░░░ ░███ ░███░███████ +░███ ███ ███░░███ ░███ ░███ ░███ ░ ░███ ░███ ░███ ░███ ░███░███░░░ +░░██████ ░░████████░░████████ █████ ░░██████ █████ ░░███████░░██████ + ░░░░░░ ░░░░░░░░ ░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░███ ░░░░░░ + ███ ░███ + ░░██████ + ░░░░░░ +```

CI