diff --git a/Cargo.toml b/Cargo.toml index d48495a..3e8bfa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,6 @@ cxx = { version = "1.0", optional = true } delaunator = "1.0" eyre = "0.6.12" geo = "0.31" -hex = "0.4" itertools = "0.14" kdtree = "0.7" noise = "0.9" @@ -87,11 +86,10 @@ petgraph = "0.8" rand = "0.9" rand_distr = "0.5" rectangle-pack = "0.4" -rhai = { version = "1.23", features = ["only_i64", "no_index", "no_object", "no_time", "no_function", "no_module", "no_custom_syntax", "sync"] } +rune = "0.14" svg = "0.18" tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } -wkb = "0.7" # Hard to update. See #183 wkt = "0.14" [build-dependencies] @@ -117,3 +115,10 @@ cxx-tests = ["cxx"] cxx-bindings = ["cxx", "dep:cxx", "dep:cxx-build"] default = ["cxx-bindings"] + +[patch.crates-io] +# Unreleased changes add support for Trig functions +# +# Unfortunately 0.14.1 was a hotfix branch, and master is still 0.14.0 with breaking API changes +# since 0.14.0 was released. +rune = { git = "https://github.com/rune-rs/rune.git", branch = "main" } diff --git a/README.md b/README.md index 4320978..36b242b 100644 --- a/README.md +++ b/README.md @@ -468,7 +468,9 @@ $ point-cloud \ --max-y=1 \ --delta-h=0.1 \ --time-steps=20 \ - --function "let temp = sqrt(x ** 2.0 + y ** 2.0 + 4.0); x = -sin(x) / temp; y = y / temp;" \ + --function "let temp = f64::sqrt(x.powf(2.0) + y.powf(2.0) + 4.0);" \ + --function "let x_new = -f64::sin(x) / temp;" \ + --function "let y_new = y / temp;" \ --draw-vector-field \ --vector-field-style="STROKE(gray)" \ --vector-field-style="STROKEDASHARRAY(1)" \ diff --git a/examples/streamline/field2.svg b/examples/streamline/field2.svg index 308a538..dcfa9d3 100644 --- a/examples/streamline/field2.svg +++ b/examples/streamline/field2.svg @@ -45,12 +45,12 @@ svg { stroke:black; stroke-width:2; fill:none;} - + - - + + - + @@ -62,7 +62,7 @@ svg { stroke:black; stroke-width:2; fill:none;} - + @@ -92,11 +92,11 @@ svg { stroke:black; stroke-width:2; fill:none;} - + - + @@ -106,7 +106,7 @@ svg { stroke:black; stroke-width:2; fill:none;} - + @@ -128,8 +128,8 @@ svg { stroke:black; stroke-width:2; fill:none;} - - + + @@ -157,7 +157,7 @@ svg { stroke:black; stroke-width:2; fill:none;} - + @@ -187,7 +187,7 @@ svg { stroke:black; stroke-width:2; fill:none;} - + @@ -250,7 +250,7 @@ svg { stroke:black; stroke-width:2; fill:none;} - + @@ -351,7 +351,7 @@ svg { stroke:black; stroke-width:2; fill:none;} - + @@ -363,7 +363,7 @@ svg { stroke:black; stroke-width:2; fill:none;} - + @@ -378,7 +378,7 @@ svg { stroke:black; stroke-width:2; fill:none;} - + @@ -389,7 +389,7 @@ svg { stroke:black; stroke-width:2; fill:none;} - + @@ -444,21 +444,21 @@ svg { stroke:black; stroke-width:2; fill:none;} - + - + - + - - - + + + @@ -467,28 +467,28 @@ svg { stroke:black; stroke-width:2; fill:none;} - + - + - + - + - + - - - + + + diff --git a/examples/streamline/generate.sh b/examples/streamline/generate.sh index 169aa02..bdf0edd 100755 --- a/examples/streamline/generate.sh +++ b/examples/streamline/generate.sh @@ -19,7 +19,9 @@ point-cloud \ --max-y=1 \ --delta-h=0.1 \ --time-steps=20 \ - --function "let temp = sqrt(x ** 2.0 + y ** 2.0 + 4.0); x = -sin(x) / temp; y = y / temp;" \ + --function "let temp = f64::sqrt(x.powf(2.0) + y.powf(2.0) + 4.0);" \ + --function "let x_new = -f64::sin(x) / temp;" \ + --function "let y_new = y / temp;" \ --draw-vector-field \ --vector-field-style="STROKE(gray)" \ --vector-field-style="STROKEDASHARRAY(1)" \ diff --git a/generative/io/mod.rs b/generative/io/mod.rs index 7276030..1e2b5eb 100644 --- a/generative/io/mod.rs +++ b/generative/io/mod.rs @@ -8,6 +8,6 @@ pub use stdio::{get_input_reader, get_output_writer}; pub use tgf::{GraphFormat, read_tgf_graph, write_graph, write_tgf_graph}; pub use self::wkt::{ - GeometryAndStyle, GeometryFormat, SvgStyle, read_geometries, read_wkt_geometries, + GeometryAndStyle, SvgStyle, read_geometries, read_wkt_geometries, read_wkt_geometries_and_styles, write_geometries, write_wkt_geometries, }; diff --git a/generative/io/wkt.rs b/generative/io/wkt.rs index 81b8686..2971b75 100644 --- a/generative/io/wkt.rs +++ b/generative/io/wkt.rs @@ -1,38 +1,12 @@ use std::io::{BufRead, BufReader, Lines, Read, Write}; use std::str::FromStr; -use clap::ValueEnum; use geo::{ CoordNum, Geometry, GeometryCollection, Line, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, Rect, Triangle, }; -use hex::{decode, encode_upper}; -use wkb::{geom_to_wkb, wkb_to_geom, write_geom_to_wkb}; use wkt::{ToWkt, Wkt}; -#[derive(Debug, Clone, Copy, ValueEnum)] -pub enum GeometryFormat { - /// One WKT geometry per line. Ignores trailing garbage; does not skip over leading garbage. - Wkt, - /// Stringified hex encoded WKB, one geometry per line - WkbHex, - /// Raw WKB bytes with no separator between geometries - WkbRaw, - // TODO: Flat? - // TODO: Splines? -} - -impl std::fmt::Display for GeometryFormat { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - // important: Should match clap::ValueEnum format - GeometryFormat::Wkt => write!(f, "wkt"), - GeometryFormat::WkbHex => write!(f, "wkb-hex"), - GeometryFormat::WkbRaw => write!(f, "wkb-raw"), - } - } -} - #[derive(PartialEq, Clone, Debug)] pub enum SvgStyle { PointRadius(f64), @@ -152,30 +126,19 @@ where } } -pub fn read_geometries( - reader: R, - format: &GeometryFormat, -) -> Box>> +pub fn read_geometries(reader: R) -> Box>> where R: Read + 'static, { - match format { - GeometryFormat::Wkt => Box::new(read_wkt_geometries(reader)), - GeometryFormat::WkbHex => Box::new(read_wkbhex_geometries(reader)), - GeometryFormat::WkbRaw => Box::new(read_wkbraw_geometries(reader)), - } + Box::new(read_wkt_geometries(reader)) } -pub fn write_geometries(writer: W, geometries: G, format: GeometryFormat) -> eyre::Result<()> +pub fn write_geometries(writer: W, geometries: G) -> eyre::Result<()> where W: Write, G: IntoIterator>, { - match format { - GeometryFormat::Wkt => write_wkt_geometries(writer, geometries), - GeometryFormat::WkbHex => write_wkbhex_geometries(writer, geometries), - GeometryFormat::WkbRaw => write_wkbraw_geometries(writer, geometries), - } + write_wkt_geometries(writer, geometries) } pub struct WktGeometries @@ -192,20 +155,6 @@ where lines: Lines>, } -pub struct WkbHexGeometries -where - R: Read, -{ - lines: Lines>, -} - -pub struct WkbRawGeometries -where - R: Read, -{ - reader: BufReader, -} - impl Iterator for WktGeometries where R: Read, @@ -271,69 +220,6 @@ where } } -impl Iterator for WkbRawGeometries -where - R: Read, -{ - type Item = Geometry; - - fn next(&mut self) -> Option { - // This is the only way to tell if a BufRead is exhausted without using the nightly only - // unstable has_data_left() API. - match self.reader.fill_buf() { - Ok(buf) => { - if buf.is_empty() { - return None; - } - } - Err(e) => { - tracing::warn!("Failed to read WKB: {e:?}"); - return None; - } - } - - match wkb_to_geom(&mut self.reader) { - Ok(geom) => Some(geom), - Err(e) => { - tracing::warn!("Failed to parse WKB: {e:?}"); - None - } - } - } -} - -impl Iterator for WkbHexGeometries -where - R: Read, -{ - type Item = Geometry; - - fn next(&mut self) -> Option { - match self.lines.next() { - Some(line) => match line { - Ok(line) => match decode(line) { - Ok(buf) => match wkb_to_geom(&mut &buf[..]) { - Ok(geom) => Some(geom), - Err(e) => { - tracing::warn!("Failed to parse WKB(hex): {e:?}"); - None - } - }, - Err(e) => { - tracing::warn!("Failed to decode WKB(hex): {e:?}"); - None - } - }, - Err(e) => { - tracing::warn!("Failed to read WKB(hex) from line: {e:?}"); - None - } - }, - None => None, - } - } -} - /// Return an iterator to the WKT geometries passed in through the given BufReader /// /// Expects one geometry per line (LF or CRLF). Parsing any given line ends after either the first @@ -349,24 +235,6 @@ where } } -fn read_wkbhex_geometries(reader: R) -> WkbHexGeometries -where - R: Read, -{ - WkbHexGeometries { - lines: BufReader::new(reader).lines(), - } -} - -fn read_wkbraw_geometries(reader: R) -> WkbRawGeometries -where - R: Read, -{ - WkbRawGeometries { - reader: BufReader::new(reader), - } -} - /// Write the given geometries with the given Writer in WKT format /// /// Each geometry will be written on its own line. @@ -382,37 +250,6 @@ where Ok(()) } -fn write_wkbhex_geometries(mut writer: W, geometries: G) -> eyre::Result<()> -where - W: Write, - G: IntoIterator>, -{ - for geom in geometries { - match geom_to_wkb(&geom) { - Ok(buffer) => { - writeln!(writer, "{}", encode_upper(buffer))?; - } - Err(e) => { - tracing::warn!("Failed to serialize geometry to WKB: {e:?}"); - } - } - } - Ok(()) -} - -fn write_wkbraw_geometries(mut writer: W, geometries: G) -> eyre::Result<()> -where - W: Write, - G: IntoIterator>, -{ - for geom in geometries { - // TODO: What's this about the endianity byte? - write_geom_to_wkb(&geom, &mut writer) - .map_err(|e| eyre::eyre!("Failed to write WKB geometry: {e:?}"))?; - } - Ok(()) -} - pub fn read_wkt_geometries_and_styles(reader: R) -> WktGeometriesAndStyles where R: Read, @@ -474,75 +311,6 @@ mod tests { assert_eq!(actual, expected); } - #[test] - fn test_wkb_single_input() { - let input_wkt = b"POINT(2 3.5)"; - let input_wkbhex = b"010100000000000000000000400000000000000C40"; - let input_wkbraw = decode(input_wkbhex).unwrap(); - - let expected = Geometry::Point(Point::new(2.0, 3.5)); - - let mut wkt_geometries = read_wkt_geometries(&input_wkt[..]); - assert_eq!(wkt_geometries.next().unwrap(), expected); - - let mut wkbraw_geometries = read_wkbraw_geometries(input_wkbraw.as_slice()); - assert_eq!(wkbraw_geometries.next().unwrap(), expected); - - let mut wkbhex_geometries = read_wkbhex_geometries(&input_wkbhex[..]); - assert_eq!(wkbhex_geometries.next().unwrap(), expected); - } - - #[test] - fn test_wkb_multi_input() { - let input_wkt = b"POINT(1 1)\nPOINT(2 3.5)"; - let input_wkbhex = b"0101000000000000000000F03F000000000000F03F\n010100000000000000000000400000000000000C40"; - let buffer: Vec = input_wkbhex[..] - .lines() - .flat_map(|l| decode(l.unwrap()).unwrap()) - .collect(); - - let expected1 = Geometry::Point(Point::new(1.0, 1.0)); - let expected2 = Geometry::Point(Point::new(2.0, 3.5)); - - let mut wkt_geometries = read_wkt_geometries(&input_wkt[..]); - assert_eq!(wkt_geometries.next().unwrap(), expected1); - assert_eq!(wkt_geometries.next().unwrap(), expected2); - - let mut wkbhex_geometries = read_wkbhex_geometries(&input_wkbhex[..]); - assert_eq!(wkbhex_geometries.next().unwrap(), expected1); - assert_eq!(wkbhex_geometries.next().unwrap(), expected2); - - let mut wkbraw_geometries = read_wkbraw_geometries(buffer.as_slice()); - assert_eq!(wkbraw_geometries.next().unwrap(), expected1); - assert_eq!(wkbraw_geometries.next().unwrap(), expected2); - } - - #[test] - fn test_wkbhex_output() { - let input_wkbhex = b"0101000000000000000000F03F000000000000F03F\n010100000000000000000000400000000000000C40\n"; - let geometries = read_wkbhex_geometries(&input_wkbhex[..]); - - let mut output_buffer = Vec::::new(); - write_wkbhex_geometries(&mut output_buffer, geometries).unwrap(); - - assert_eq!(output_buffer, input_wkbhex); - } - - #[test] - fn test_wkbraw_output() { - let input_wkbhex = b"0101000000000000000000F03F000000000000F03F\n010100000000000000000000400000000000000C40\n"; - let buffer: Vec = input_wkbhex[..] - .lines() - .flat_map(|l| decode(l.unwrap()).unwrap()) - .collect(); - let geometries = read_wkbraw_geometries(buffer.as_slice()); - - let mut output_buffer = Vec::::new(); - write_wkbraw_geometries(&mut output_buffer, geometries).unwrap(); - - assert_eq!(output_buffer, buffer); - } - #[test] fn test_can_parse_3d() { let wkt = b"POINT Z(1 2 3)"; diff --git a/tools/bitwise.rs b/tools/bitwise.rs index 84b5d20..91d3f9d 100644 --- a/tools/bitwise.rs +++ b/tools/bitwise.rs @@ -2,10 +2,9 @@ use std::io::Write; use std::path::PathBuf; use clap::{Parser, ValueEnum}; -use generative::io::{GeometryFormat, get_output_writer, write_geometries}; +use generative::io::{get_output_writer, write_geometries}; use geo::{Geometry, Line, Point}; use itertools::Itertools; -use rhai::{AST, Engine, EvalAltResult, Scope}; /// Perform bitwise operations on a grid /// @@ -21,10 +20,6 @@ struct CmdlineOptions { #[clap(short, long)] output: Option, - /// Output geometry format. - #[clap(short = 'O', long, default_value_t = GeometryFormat::Wkt)] - output_format: GeometryFormat, - /// Whether to output points or connected lines #[clap(long, default_value_t = false)] points: bool, @@ -58,22 +53,42 @@ struct CmdlineOptions { expression: String, } -fn expression(engine: &Engine, ast: &AST, x: i64, y: i64) -> Result> { - let mut scope = Scope::new(); - scope.push("x", x); - scope.push("y", y); +type BitwiseExpression = Box i64>; + +fn build_expression(expr: &str) -> eyre::Result { + // Ok(Box::new(|x, y| (x & y) & ((x ^ y) % 13))) + + let context = rune::Context::with_default_modules()?; + let runtime = rune::sync::Arc::try_new(context.runtime()?)?; + let mut script = rune::Sources::new(); + script.insert(build_function(expr)?)?; + let mut diagnostics = rune::Diagnostics::new(); + let maybe_unit = rune::prepare(&mut script) + .with_context(&context) + .with_diagnostics(&mut diagnostics) + .build(); + if !diagnostics.is_empty() { + let mut writer = + rune::termcolor::StandardStream::stderr(rune::termcolor::ColorChoice::Always); + diagnostics.emit(&mut writer, &script)?; + } + let unit = rune::sync::Arc::try_new(maybe_unit?)?; + let vm = rune::Vm::new(runtime, unit); + let eval_expr = vm.lookup_function(["eval_expr"])?; + let eval_expr = move |x: i64, y: i64| -> i64 { + eval_expr.call((x, y)).expect("Failed to call eval_expr()") + }; + Ok(Box::new(eval_expr)) +} - engine.eval_ast_with_scope::(&mut scope, ast) +fn build_function(expr: &str) -> eyre::Result { + let lines = ["pub fn eval_expr(x, y) {", expr, "}"]; + let script = lines.join("\n"); + let source = rune::Source::memory(script)?; + Ok(source) } -fn write_line( - writer: W, - format: GeometryFormat, - x1: i64, - y1: i64, - x2: i64, - y2: i64, -) -> eyre::Result<()> +fn write_line(writer: W, x1: i64, y1: i64, x2: i64, y2: i64) -> eyre::Result<()> where W: Write, { @@ -82,19 +97,19 @@ where Point::new(x2 as f64, y2 as f64), ); let geometries = std::iter::once(Geometry::Line(line)); - write_geometries(writer, geometries, format) + write_geometries(writer, geometries) } -fn write_point(writer: W, format: GeometryFormat, x1: i64, y1: i64) -> eyre::Result<()> +fn write_point(writer: W, x1: i64, y1: i64) -> eyre::Result<()> where W: Write, { let point = Point::new(x1 as f64, y1 as f64); let geometries = std::iter::once(Geometry::Point(point)); - write_geometries(writer, geometries, format) + write_geometries(writer, geometries) } -#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] enum Neighbor { North, NorthEast, @@ -147,8 +162,7 @@ fn main() -> eyre::Result<()> { .with_writer(std::io::stderr) .init(); - let engine = Engine::new(); - let ast = engine.compile_expression(&args.expression)?; + let expr_fn = build_expression(&args.expression)?; let xs = args.x_min..args.x_max; let ys = args.y_min..args.y_max; @@ -157,38 +171,32 @@ fn main() -> eyre::Result<()> { let mut writer = get_output_writer(&args.output)?; if args.points { let geometries = cross.filter_map(|(x, y)| { - if let Ok(value) = expression(&engine, &ast, x, y) { - if value > 0 { - return Some(Geometry::Point(Point::new(x as f64, y as f64))); - } - } else { - tracing::error!( - "Failed to evaluate expression '{}' given x={x}, y={y}", - args.expression, - ); + let value = expr_fn(x, y); + if value > 0 { + return Some(Geometry::Point(Point::new(x as f64, y as f64))); } None }); - write_geometries(writer, geometries, args.output_format)?; + write_geometries(writer, geometries)?; } else { tracing::info!( "Searching neighbors in order: {:?}", args.neighbor_search_order ); for (x, y) in cross { - if expression(&engine, &ast, x, y)? > 0 { + if expr_fn(x, y) > 0 { let mut wrote_line = false; for n in args.neighbor_search_order.iter() { - let (x2, y2) = neighbor(x, y, n.clone()); - if expression(&engine, &ast, x2, y2)? > 0 { - write_line(&mut writer, args.output_format, x, y, x2, y2)?; + let (x2, y2) = neighbor(x, y, *n); + if expr_fn(x2, y2) > 0 { + write_line(&mut writer, x, y, x2, y2)?; wrote_line = true; break; } } if !wrote_line { - write_point(&mut writer, args.output_format, x, y)?; + write_point(&mut writer, x, y)?; } } } diff --git a/tools/bundle.rs b/tools/bundle.rs index 2f34552..fb4f8c2 100644 --- a/tools/bundle.rs +++ b/tools/bundle.rs @@ -1,9 +1,7 @@ use std::path::PathBuf; use clap::Parser; -use generative::io::{ - GeometryFormat, get_input_reader, get_output_writer, read_geometries, write_geometries, -}; +use generative::io::{get_input_reader, get_output_writer, read_geometries, write_geometries}; /// Bundle the given geometries into a GEOMETRYCOLLECTION #[derive(Debug, Parser)] @@ -17,17 +15,9 @@ struct CmdlineOptions { #[clap(short, long)] input: Option, - /// Input geometry format. - #[clap(short = 'I', long, default_value_t = GeometryFormat::Wkt)] - input_format: GeometryFormat, - /// Output file to write result to. Defaults to stdout. #[clap(short, long)] output: Option, - - /// Output geometry format. - #[clap(short = 'O', long, default_value_t = GeometryFormat::Wkt)] - output_format: GeometryFormat, } fn main() -> eyre::Result<()> { @@ -44,11 +34,11 @@ fn main() -> eyre::Result<()> { .init(); let reader = get_input_reader(&args.input)?; - let geometries = read_geometries(reader, &args.input_format); + let geometries = read_geometries(reader); let bundle: geo::GeometryCollection = geometries.collect(); let geometries = std::iter::once(geo::Geometry::GeometryCollection(bundle)); let writer = get_output_writer(&args.output)?; - write_geometries(writer, geometries, args.output_format) + write_geometries(writer, geometries) } diff --git a/tools/geom2graph.rs b/tools/geom2graph.rs index 54cff84..ea3b2b7 100644 --- a/tools/geom2graph.rs +++ b/tools/geom2graph.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use clap::{Parser, ValueEnum}; use generative::graph::GeometryGraph; use generative::io::{ - GeometryFormat, GraphFormat, get_input_reader, get_output_writer, read_geometries, - read_tgf_graph, write_geometries, write_graph, + GraphFormat, get_input_reader, get_output_writer, read_geometries, read_tgf_graph, + write_geometries, write_graph, }; use generative::noding::{node, polygonize}; use generative::snap::{SnappingStrategy, snap_geoms, snap_graph}; @@ -42,10 +42,6 @@ struct CmdlineOptions { #[clap(short, long)] output: Option, - /// Format to output --graph2geom geometries as - #[clap(long, default_value_t = GeometryFormat::Wkt)] - geometry_format: GeometryFormat, - /// Format to output (the default) --graph2geom graphs as #[clap(long, default_value_t = GraphFormat::Tgf)] graph_format: GraphFormat, @@ -93,7 +89,7 @@ fn main() -> eyre::Result<()> { }; if args.geom2graph || !args.graph2geom { - let geometries = read_geometries(reader, &args.geometry_format); + let geometries = read_geometries(reader); let graph = node::<_, petgraph::Undirected>(geometries); let graph = if args.tolerance.is_some() { @@ -116,6 +112,6 @@ fn main() -> eyre::Result<()> { Box::new(geometries) }; - write_geometries(writer, geometries, args.geometry_format) + write_geometries(writer, geometries) } } diff --git a/tools/grid.rs b/tools/grid.rs index 0ab948f..6df1bf7 100644 --- a/tools/grid.rs +++ b/tools/grid.rs @@ -2,9 +2,7 @@ use std::path::PathBuf; use clap::{Parser, ValueEnum}; use generative::graph::GeometryGraph; -use generative::io::{ - GeometryFormat, GraphFormat, get_output_writer, write_geometries, write_graph, -}; +use generative::io::{GraphFormat, get_output_writer, write_geometries, write_graph}; #[cfg(feature = "cxx-bindings")] use generative::noding::{node, polygonize}; use generative::snap::{SnappingStrategy, snap_geoms}; @@ -631,7 +629,7 @@ fn main() -> eyre::Result<()> { let geoms = rings.chain(spokes); match args.output_format { - GridFormat::Lines => write_geometries(writer, geoms, GeometryFormat::Wkt), + GridFormat::Lines => write_geometries(writer, geoms), GridFormat::Points => { let mut points = Vec::new(); for geom in geoms { @@ -642,7 +640,7 @@ fn main() -> eyre::Result<()> { } // Snap points as a way of deduplicating vertices let points = snap_geoms(points.into_iter(), SnappingStrategy::ClosestPoint(0.0)); - write_geometries(writer, points, GeometryFormat::Wkt) + write_geometries(writer, points) } #[cfg(feature = "cxx-bindings")] GridFormat::Graph | GridFormat::Cells => { @@ -654,7 +652,7 @@ fn main() -> eyre::Result<()> { let polygons = polygons.into_iter().map(Geometry::Polygon); let dangles = dangles.into_iter().map(Geometry::LineString); let geoms = polygons.chain(dangles); - write_geometries(writer, geoms, GeometryFormat::Wkt) + write_geometries(writer, geoms) } } #[cfg(not(feature = "cxx-bindings"))] @@ -668,18 +666,16 @@ fn main() -> eyre::Result<()> { match args.output_format { GridFormat::Graph => write_graph(writer, &graph, &GraphFormat::Tgf), GridFormat::Lines => write_graph(writer, &graph, &GraphFormat::Wkt), - GridFormat::Points => write_geometries( - writer, - graph.node_weights().map(|p| Geometry::Point(*p)), - GeometryFormat::Wkt, - ), + GridFormat::Points => { + write_geometries(writer, graph.node_weights().map(|p| Geometry::Point(*p))) + } #[cfg(feature = "cxx-bindings")] GridFormat::Cells => { let (polygons, dangles) = polygonize(&graph); let polygons = polygons.into_iter().map(Geometry::Polygon); let dangles = dangles.into_iter().map(Geometry::LineString); let geoms = polygons.chain(dangles); - write_geometries(writer, geoms, GeometryFormat::Wkt) + write_geometries(writer, geoms) } } } diff --git a/tools/pack.rs b/tools/pack.rs index fa23e36..34ec5c1 100644 --- a/tools/pack.rs +++ b/tools/pack.rs @@ -2,9 +2,7 @@ use std::collections::BTreeMap; use std::path::PathBuf; use clap::Parser; -use generative::io::{ - GeometryFormat, get_input_reader, get_output_writer, read_geometries, write_geometries, -}; +use generative::io::{get_input_reader, get_output_writer, read_geometries, write_geometries}; use geo::{BoundingRect, Coord, Translate}; use rectangle_pack::{ GroupedRectsToPlace, RectToInsert, TargetBin, contains_smallest_box, pack_rects, @@ -26,18 +24,10 @@ struct CmdlineOptions { #[clap(short, long)] input: Option, - /// Input geometry format. - #[clap(short = 'I', long, default_value_t = GeometryFormat::Wkt)] - input_format: GeometryFormat, - /// Output file to write result to. Defaults to stdout. #[clap(short, long)] output: Option, - /// Output geometry format. - #[clap(short = 'O', long, default_value_t = GeometryFormat::Wkt)] - output_format: GeometryFormat, - /// The width of the target rectangle #[clap(long, default_value_t = 100)] width: u32, @@ -65,7 +55,7 @@ fn main() -> eyre::Result<()> { .init(); let reader = get_input_reader(&args.input)?; - let mut geometries: Vec<_> = read_geometries(reader, &args.input_format).collect(); + let mut geometries: Vec<_> = read_geometries(reader).collect(); let padding = Coord { x: args.padding, y: args.padding, @@ -111,5 +101,5 @@ fn main() -> eyre::Result<()> { } let writer = get_output_writer(&args.output)?; - write_geometries(writer, geometries, args.output_format) + write_geometries(writer, geometries) } diff --git a/tools/smooth.rs b/tools/smooth.rs index 59addf2..6e53a1d 100644 --- a/tools/smooth.rs +++ b/tools/smooth.rs @@ -2,9 +2,7 @@ use std::path::PathBuf; use clap::Parser; use generative::flatten::flatten_nested_geometries; -use generative::io::{ - GeometryFormat, get_input_reader, get_output_writer, read_geometries, write_geometries, -}; +use generative::io::{get_input_reader, get_output_writer, read_geometries, write_geometries}; use geo::{ChaikinSmoothing, Geometry}; /// Smooth the given geometries @@ -19,18 +17,10 @@ struct CmdlineOptions { #[clap(short, long)] input: Option, - /// Input geometry format. - #[clap(short = 'I', long, default_value_t = GeometryFormat::Wkt)] - input_format: GeometryFormat, - /// Output file to write result to. Defaults to stdout. #[clap(short, long)] output: Option, - /// Output geometry format. - #[clap(short = 'O', long, default_value_t = GeometryFormat::Wkt)] - output_format: GeometryFormat, - /// Number of iterations to run Chaikins smoothing algorithm #[clap(short = 'n', long, default_value_t = 10)] iterations: usize, @@ -50,7 +40,7 @@ fn main() -> eyre::Result<()> { .init(); let reader = get_input_reader(&args.input)?; - let geometries = read_geometries(reader, &args.input_format); + let geometries = read_geometries(reader); let geometries = flatten_nested_geometries(geometries); let geometries = geometries.map(|g| match g { Geometry::Point(_) @@ -68,5 +58,5 @@ fn main() -> eyre::Result<()> { }); let writer = get_output_writer(&args.output)?; - write_geometries(writer, geometries, args.output_format) + write_geometries(writer, geometries) } diff --git a/tools/snap.rs b/tools/snap.rs index 347f485..8ec7070 100644 --- a/tools/snap.rs +++ b/tools/snap.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use clap::{Parser, ValueEnum}; use generative::graph::GeometryGraph; use generative::io::{ - GeometryFormat, get_input_reader, get_output_writer, read_geometries, read_tgf_graph, - write_geometries, write_tgf_graph, + get_input_reader, get_output_writer, read_geometries, read_tgf_graph, write_geometries, + write_tgf_graph, }; use generative::snap::{SnappingStrategy, snap_geoms, snap_graph}; use petgraph::Undirected; @@ -13,10 +13,6 @@ use petgraph::Undirected; enum InputFormat { /// One WKT geometry per line. Ignores trailing garbage; does not skip over leading garbage. Wkt, - /// Stringified hex encoded WKB, one geometry per line - WkbHex, - /// Raw WKB bytes with no separator between geometries - WkbRaw, /// A geometry graph in TGF format Tgf, } @@ -26,24 +22,11 @@ impl std::fmt::Display for InputFormat { match self { // important: Should match clap::ValueEnum format InputFormat::Wkt => write!(f, "wkt"), - InputFormat::WkbHex => write!(f, "wkb-hex"), - InputFormat::WkbRaw => write!(f, "wkb-raw"), InputFormat::Tgf => write!(f, "tgf"), } } } -impl From for GeometryFormat { - fn from(f: InputFormat) -> GeometryFormat { - match f { - InputFormat::Wkt => GeometryFormat::Wkt, - InputFormat::WkbHex => GeometryFormat::WkbHex, - InputFormat::WkbRaw => GeometryFormat::WkbRaw, - InputFormat::Tgf => unreachable!(), - } - } -} - #[derive(Debug, Clone, ValueEnum)] enum CliSnappingStrategy { ClosestPoint, @@ -110,10 +93,10 @@ fn main() -> eyre::Result<()> { }; match args.input_format { - InputFormat::Wkt | InputFormat::WkbHex | InputFormat::WkbRaw => { - let geometries = read_geometries(reader, &args.input_format.clone().into()); + InputFormat::Wkt => { + let geometries = read_geometries(reader); let geometries = snap_geoms(geometries, strategy); - write_geometries(writer, geometries, args.input_format.into()) + write_geometries(writer, geometries) } InputFormat::Tgf => { let graph: GeometryGraph = read_tgf_graph(reader); diff --git a/tools/streamline.rs b/tools/streamline.rs index 33e5729..e4a1944 100644 --- a/tools/streamline.rs +++ b/tools/streamline.rs @@ -3,16 +3,13 @@ use std::path::PathBuf; use clap::{Parser, ValueEnum}; use generative::MapCoordsInPlaceMut; -use generative::io::{ - GeometryFormat, get_input_reader, get_output_writer, read_geometries, write_geometries, -}; +use generative::io::{get_input_reader, get_output_writer, read_geometries, write_geometries}; use geo::{AffineOps, AffineTransform, Centroid, Coord, Geometry, Line, LineString}; // use noise::Billow; use noise::{NoiseFn, Perlin}; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; use rand_distr::{Binomial, Distribution}; -use rhai::{Engine, Scope}; #[derive(Debug, Clone, Copy, ValueEnum)] enum StreamlineKind { @@ -48,30 +45,24 @@ struct CmdlineOptions { #[clap(short, long)] input: Option, - /// Input geometry format. - #[clap(short = 'I', long, default_value_t = GeometryFormat::Wkt)] - input_format: GeometryFormat, - /// Output file to write result to. Defaults to stdout. #[clap(short, long)] output: Option, - /// Output geometry format. - #[clap(short = 'O', long, default_value_t = GeometryFormat::Wkt)] - output_format: GeometryFormat, - - /// A Rhai script that defines the vector field. If not given, a Perlin noise field will be + /// A Rune script that defines the vector field. If not given, a Perlin noise field will be /// used instead. /// /// Example: /// - /// let temp = sqrt(x ** 2.0 + y ** 2.0 + 4.0); - /// x = -sin(x) / temp; - /// y = y / temp; + /// let temp = f64::sqrt(x.powf(2.0) + y.powf(2.0) + 4.0); + /// let x_new = -f64::sin(x) / temp; + /// let y_new = y / temp; + /// + /// I.e., the f64 x, y variables are inputs, and the x_new, y_new variables are outputs. /// - /// I.e., the f64 x and y variables are both input and output. + /// May be given multiple times for multi-line functions. #[clap(short, long)] - function: Option, + function: Vec, /// The random seed to use. Use zero to let the tool pick its own random seed. #[clap(long, default_value_t = 0)] @@ -147,8 +138,47 @@ fn generate_random_seed_if_not_specified(seed: u64) -> u64 { } } +type VectorFieldFunction = Box (f64, f64)>; + +fn build_function_from_lines(lines: &[String]) -> eyre::Result { + let context = rune::Context::with_default_modules()?; + let runtime = rune::sync::Arc::try_new(context.runtime()?)?; + let mut script = build_script_from_lines(lines)?; + let mut diagnostics = rune::Diagnostics::new(); + let maybe_unit = rune::prepare(&mut script) + .with_context(&context) + .with_diagnostics(&mut diagnostics) + .build(); + if !diagnostics.is_empty() { + let mut writer = + rune::termcolor::StandardStream::stderr(rune::termcolor::ColorChoice::Always); + diagnostics.emit(&mut writer, &script)?; + } + let unit = rune::sync::Arc::try_new(maybe_unit?)?; + let vm = rune::Vm::new(runtime, unit); + let eval_field = vm.lookup_function(["eval_field"])?; + let eval_expr = move |x: f64, y: f64| -> (f64, f64) { + eval_field + .call((x, y)) + .expect("Failed to call eval_field()") + }; + Ok(Box::new(eval_expr)) +} + +fn build_script_from_lines(lines: &[String]) -> eyre::Result { + let mut sources = rune::Sources::new(); + let mut all_lines = vec!["pub fn eval_field(x, y) {".to_string()]; + all_lines.extend_from_slice(lines); + all_lines.push("return (x_new, y_new); }".to_string()); + + let script = all_lines.join("\n"); + let source = rune::Source::memory(script)?; + sources.insert(source)?; + Ok(sources) +} + struct VectorField { - function: Box [f64; 2]>, + function: VectorFieldFunction, min_x: f64, max_x: f64, @@ -164,7 +194,7 @@ impl VectorField { min_y: f64, max_y: f64, stride: f64, - function: impl Fn(f64, f64) -> [f64; 2] + 'static, + function: VectorFieldFunction, ) -> Self { Self { function: Box::new(function), @@ -192,7 +222,7 @@ impl VectorField { ((y - self.min_y) / self.stride) as usize } - fn write(&self, writer: &mut W, format: GeometryFormat) -> eyre::Result<()> + fn write(&self, writer: &mut W) -> eyre::Result<()> where W: std::io::Write, { @@ -212,8 +242,8 @@ impl VectorField { // Vector field visualizations don't look good if the vectors use the same scale as the // uniform grid they're drawn on. So we scale by the delta-h. - let dx = vector[0] * self.stride; - let dy = vector[1] * self.stride; + let dx = vector.0 * self.stride; + let dy = vector.1 * self.stride; let x2 = x1 + dx; let y2 = y1 + dy; @@ -223,7 +253,7 @@ impl VectorField { }) }); - write_geometries(writer, vectors, format) + write_geometries(writer, vectors) } } @@ -310,8 +340,8 @@ fn simulate_coordinate( let current_vector = (field.function)(current.x, current.y); - current.x += timestep * current_vector[0]; - current.y += timestep * current_vector[1]; + current.x += timestep * current_vector.0; + current.y += timestep * current_vector.1; if record_streamlines { streamline.push(current); @@ -396,30 +426,13 @@ fn main() -> eyre::Result<()> { // let perlin = Billow::::new(seed as u32); let perlin = Perlin::new(seed as u32); - let function: Box [f64; 2]> = match args.function { - Some(string) => { - let engine = Engine::new(); - let ast = engine.compile(string)?; - - let func = move |x: f64, y: f64| -> [f64; 2] { - let mut scope = Scope::new(); - scope.push("x", x); - scope.push("y", y); - - engine.eval_ast_with_scope::<()>(&mut scope, &ast).unwrap(); - - let new_x = scope.get_value::("x").unwrap(); - let new_y = scope.get_value::("y").unwrap(); - - [new_x, new_y] - }; - - Box::new(func) - } - None => Box::new(move |x, y| { + let function: VectorFieldFunction = if args.function.is_empty() { + Box::new(move |x, y| { let angle = perlin.get([x, y]); - [f64::cos(angle), f64::sin(angle)] - }), + (f64::cos(angle), f64::sin(angle)) + }) + } else { + build_function_from_lines(&args.function)? }; let field = VectorField::new( @@ -438,10 +451,10 @@ fn main() -> eyre::Result<()> { for style in args.vector_field_style { writeln!(&mut writer, "{style}")?; } - field.write(&mut writer, args.output_format)?; + field.write(&mut writer)?; } - let geometries = read_geometries(reader, &args.input_format); + let geometries = read_geometries(reader); let geoms_and_streamlines = simulate( geometries, @@ -460,13 +473,13 @@ fn main() -> eyre::Result<()> { for style in args.streamline_style { writeln!(&mut writer, "{style}")?; } - write_geometries(&mut writer, streamlines, args.output_format)?; + write_geometries(&mut writer, streamlines)?; } if args.draw_geometries { for style in args.geometry_style { writeln!(&mut writer, "{style}")?; } - write_geometries(&mut writer, geometries, args.output_format)?; + write_geometries(&mut writer, geometries)?; } Ok(()) } diff --git a/tools/template.rs b/tools/template.rs index e72aaeb..1d7a355 100644 --- a/tools/template.rs +++ b/tools/template.rs @@ -1,9 +1,7 @@ use std::path::PathBuf; use clap::Parser; -use generative::io::{ - GeometryFormat, get_input_reader, get_output_writer, read_geometries, write_geometries, -}; +use generative::io::{get_input_reader, get_output_writer, read_geometries, write_geometries}; /// A template tool /// @@ -19,17 +17,9 @@ struct CmdlineOptions { #[clap(short, long)] input: Option, - /// Input geometry format. - #[clap(short = 'I', long, default_value_t = GeometryFormat::Wkt)] - input_format: GeometryFormat, - /// Output file to write result to. Defaults to stdout. #[clap(short, long)] output: Option, - - /// Output geometry format. - #[clap(short = 'O', long, default_value_t = GeometryFormat::Wkt)] - output_format: GeometryFormat, } fn main() -> eyre::Result<()> { @@ -46,10 +36,10 @@ fn main() -> eyre::Result<()> { .init(); let reader = get_input_reader(&args.input)?; - let geometries = read_geometries(reader, &args.input_format); // lazily loaded + let geometries = read_geometries(reader); // lazily loaded // Do some kind of transformation to the geometries here. let writer = get_output_writer(&args.output)?; - write_geometries(writer, geometries, args.output_format) + write_geometries(writer, geometries) } diff --git a/tools/transform.rs b/tools/transform.rs index 9468a90..3615055 100644 --- a/tools/transform.rs +++ b/tools/transform.rs @@ -1,9 +1,7 @@ use std::path::PathBuf; use clap::{Parser, ValueEnum}; -use generative::io::{ - GeometryFormat, get_input_reader, get_output_writer, read_geometries, write_geometries, -}; +use generative::io::{get_input_reader, get_output_writer, read_geometries, write_geometries}; use geo::{ AffineOps, AffineTransform, BoundingRect, Coord, Geometry, MapCoordsInPlace, Rect, coord, }; @@ -44,18 +42,10 @@ struct CmdlineOptions { #[clap(short, long)] output: Option, - /// Output geometry format. - #[clap(short = 'O', long, default_value_t = GeometryFormat::Wkt)] - output_format: GeometryFormat, - /// Input file to read input from. Defaults to stdin. #[clap(short, long)] input: Option, - /// Input geometry format. - #[clap(short = 'I', long, default_value_t = GeometryFormat::Wkt)] - input_format: GeometryFormat, - /// How to center the affine transformation #[clap(long, default_value = "origin")] center: TransformCenter, @@ -299,7 +289,7 @@ fn main() -> eyre::Result<()> { let reader = get_input_reader(&args.input)?; let writer = get_output_writer(&args.output)?; - let geometries = read_geometries(reader, &args.input_format); + let geometries = read_geometries(reader); let mut transformed = affine_transform(geometries, &args); if args.range1.len() == 2 || args.range2.len() == 2 { @@ -337,5 +327,5 @@ fn main() -> eyre::Result<()> { transformed = Box::new(geoms_coordwise(transformed, from_polar)); } - write_geometries(writer, transformed, args.output_format) + write_geometries(writer, transformed) } diff --git a/tools/traverse.rs b/tools/traverse.rs index f9740e1..0bcb8f9 100644 --- a/tools/traverse.rs +++ b/tools/traverse.rs @@ -3,9 +3,7 @@ use std::path::PathBuf; use clap::Parser; use generative::graph::GeometryGraph; -use generative::io::{ - GeometryFormat, get_input_reader, get_output_writer, read_tgf_graph, write_geometries, -}; +use generative::io::{get_input_reader, get_output_writer, read_tgf_graph, write_geometries}; use geo::{Geometry, LineString, Point}; use petgraph::{EdgeType, Undirected}; use rand::rngs::StdRng; @@ -32,10 +30,6 @@ struct CmdlineOptions { #[clap(short, long)] output: Option, - /// The output geometry format - #[clap(short='O', long, default_value_t=GeometryFormat::Wkt)] - output_format: GeometryFormat, - /// The random seed to use. Use zero to let the tool pick its own random seed. #[clap(long, default_value_t = 0)] seed: u64, @@ -203,14 +197,13 @@ fn main() -> eyre::Result<()> { .map(Geometry::LineString); let mut writer = get_output_writer(&args.output)?; - write_geometries(&mut writer, traversals, args.output_format)?; + write_geometries(&mut writer, traversals)?; // dump the remaining nodes if args.untraversed { write_geometries( &mut writer, graph.node_weights().map(|p| Geometry::Point(*p)), - args.output_format, )?; } Ok(()) diff --git a/tools/triangulate.rs b/tools/triangulate.rs index a8838c4..883f71d 100644 --- a/tools/triangulate.rs +++ b/tools/triangulate.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use clap::{Parser, ValueEnum}; use generative::flatten::flatten_geometries_into_points; use generative::io::{ - GeometryFormat, GraphFormat, get_input_reader, get_output_writer, read_geometries, write_graph, + GraphFormat, get_input_reader, get_output_writer, read_geometries, write_graph, }; use generative::triangulation::triangulate; @@ -37,10 +37,6 @@ struct CmdlineOptions { #[clap(short, long)] input: Option, - /// Input geometry format. - #[clap(short = 'I', long, default_value_t = GeometryFormat::Wkt)] - input_format: GeometryFormat, - /// How to triangulate the input geometries #[clap(short, long, default_value = "whole-collection")] strategy: TriangulationStrategy, @@ -61,7 +57,7 @@ fn main() -> eyre::Result<()> { let reader = get_input_reader(&args.input)?; let mut writer = get_output_writer(&args.output)?; - let geometries = read_geometries(reader, &args.input_format); // lazily loaded + let geometries = read_geometries(reader); // lazily loaded match args.strategy { TriangulationStrategy::EachGeometry => { diff --git a/tools/urquhart.rs b/tools/urquhart.rs index 7e4f1bf..5271c45 100644 --- a/tools/urquhart.rs +++ b/tools/urquhart.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use clap::Parser; use generative::flatten::flatten_geometries_into_points; use generative::io::{ - GeometryFormat, GraphFormat, get_input_reader, get_output_writer, read_geometries, write_graph, + GraphFormat, get_input_reader, get_output_writer, read_geometries, write_graph, }; use generative::triangulation::triangulate; @@ -28,10 +28,6 @@ struct CmdlineOptions { /// Input file to read input from. Defaults to stdin. #[clap(short, long)] input: Option, - - /// Input geometry format. - #[clap(short = 'I', long, default_value_t = GeometryFormat::Wkt)] - input_format: GeometryFormat, } fn main() -> eyre::Result<()> { @@ -48,7 +44,7 @@ fn main() -> eyre::Result<()> { .init(); let reader = get_input_reader(&args.input)?; - let geometries = read_geometries(reader, &args.input_format); // lazily loaded + let geometries = read_geometries(reader); // lazily loaded let points = flatten_geometries_into_points(geometries); if let Some(triangulation) = triangulate(points) {