diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 05efa29..e7719a6 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -134,13 +134,11 @@ is committed and listed here with its exports. | assets/icons/ui.sun.svg, assets/icons/ui.sun.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.sun.json` (`cargo xtask icons`) | MIT OR Apache-2.0 | | assets/icons/ui.views.svg, assets/icons/ui.views.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.views.json` (`cargo xtask icons`) | MIT OR Apache-2.0 | | assets/icons/ui.zoom.svg, assets/icons/ui.zoom.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.zoom.json` (`cargo xtask icons`) | MIT OR Apache-2.0 | -| assets/app-icon/icon.vectorcraft, assets/app-icon/icon.svg, assets/app-icon/icon.png | FieldCraft contributors | Original application icon (a hexahedral-meshed cube with a banded colour fringe on its faces and speed-coloured flow streamlines with arrowheads that run into its mesh lines or wrap around it, on a rounded-square backdrop), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs); editable source, SVG and 1024 × 1024 PNG export | MIT OR Apache-2.0 | -| assets/app-icon/icon-small.vectorcraft, assets/app-icon/icon-small.svg, assets/app-icon/icon-small.png | FieldCraft contributors | Original simplified application icon for 16–32 px (fringe cube and one coloured streamline with an arrowhead; no mesh or floor grid), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 | +| assets/app-icon/icon.vectorcraft, assets/app-icon/icon.svg, assets/app-icon/icon.png | FieldCraft contributors | Original application icon (a hexahedral-meshed cube with a banded colour fringe on its faces on a rounded-square backdrop), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs); editable source, SVG and 1024 × 1024 PNG export | MIT OR Apache-2.0 | +| assets/app-icon/icon-small.vectorcraft, assets/app-icon/icon-small.svg, assets/app-icon/icon-small.png | FieldCraft contributors | Original simplified application icon for 16–32 px (the fringe cube alone; no mesh or floor grid), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 | | assets/app-icon/layers/01-floor.vectorcraft, assets/app-icon/FieldCraft.icon/Assets/01-floor.svg | FieldCraft contributors | Original isometric floor grid with a radial fade (Default appearance), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 | | assets/app-icon/layers/01-floor-dark.vectorcraft, assets/app-icon/FieldCraft.icon/Assets/01-floor-dark.svg | FieldCraft contributors | Original isometric floor grid with a radial fade (Dark appearance), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 | -| assets/app-icon/layers/02-flow-behind.vectorcraft, assets/app-icon/FieldCraft.icon/Assets/02-flow-behind.svg | FieldCraft contributors | Original speed-coloured flow streamlines with arrowheads (the runs behind the cube), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 | -| assets/app-icon/layers/03-part.vectorcraft, assets/app-icon/FieldCraft.icon/Assets/03-part.svg | FieldCraft contributors | Original cube faces with a banded colour fringe and transparent gaps at the shared edges, and the hexahedral mesh lines clipped to them, drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 | -| assets/app-icon/layers/04-flow-front.vectorcraft, assets/app-icon/FieldCraft.icon/Assets/04-flow-front.svg | FieldCraft contributors | Original speed-coloured flow streamlines with arrowheads (the runs in front of the cube), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 | +| assets/app-icon/layers/02-part.vectorcraft, assets/app-icon/FieldCraft.icon/Assets/02-part.svg | FieldCraft contributors | Original cube faces with a banded colour fringe and transparent gaps at the shared edges, and the hexahedral mesh lines clipped to them, drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 | | assets/app-icon/FieldCraft.icon/icon.json | FieldCraft contributors | Icon Composer composition of the layers above (Default and Dark floor grid), written by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 | | assets/app-icon/Assets.car | FieldCraft contributors | Apple actool compilation of assets/app-icon/FieldCraft.icon; contains original artwork only | MIT OR Apache-2.0 | | assets/app-icon/icon.icns | FieldCraft contributors | iconutil compilation of assets/app-icon/icon.iconset | MIT OR Apache-2.0 | diff --git a/assets/app-icon/Assets.car b/assets/app-icon/Assets.car index a00bd0b..d4182cc 100644 Binary files a/assets/app-icon/Assets.car and b/assets/app-icon/Assets.car differ diff --git a/assets/app-icon/FieldCraft.icon/Assets/01-floor-dark.svg b/assets/app-icon/FieldCraft.icon/Assets/01-floor-dark.svg index 89b8906..64b7f13 100644 --- a/assets/app-icon/FieldCraft.icon/Assets/01-floor-dark.svg +++ b/assets/app-icon/FieldCraft.icon/Assets/01-floor-dark.svg @@ -4,7 +4,7 @@ - + @@ -13,6 +13,6 @@ FieldCraft icon floor grid (Dark) - + diff --git a/assets/app-icon/FieldCraft.icon/Assets/01-floor.svg b/assets/app-icon/FieldCraft.icon/Assets/01-floor.svg index 9d4203e..02a3e2a 100644 --- a/assets/app-icon/FieldCraft.icon/Assets/01-floor.svg +++ b/assets/app-icon/FieldCraft.icon/Assets/01-floor.svg @@ -4,7 +4,7 @@ - + @@ -13,6 +13,6 @@ FieldCraft icon floor grid - + diff --git a/assets/app-icon/FieldCraft.icon/Assets/02-flow-behind.svg b/assets/app-icon/FieldCraft.icon/Assets/02-flow-behind.svg deleted file mode 100644 index 6612704..0000000 --- a/assets/app-icon/FieldCraft.icon/Assets/02-flow-behind.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - FieldCraft icon flow behind the cube - - - - - - - - - diff --git a/assets/app-icon/FieldCraft.icon/Assets/02-part.svg b/assets/app-icon/FieldCraft.icon/Assets/02-part.svg new file mode 100644 index 0000000..1c742d4 --- /dev/null +++ b/assets/app-icon/FieldCraft.icon/Assets/02-part.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + FieldCraft icon cube + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/app-icon/FieldCraft.icon/Assets/03-part.svg b/assets/app-icon/FieldCraft.icon/Assets/03-part.svg deleted file mode 100644 index fcdd684..0000000 --- a/assets/app-icon/FieldCraft.icon/Assets/03-part.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - FieldCraft icon cube - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/app-icon/FieldCraft.icon/Assets/04-flow-front.svg b/assets/app-icon/FieldCraft.icon/Assets/04-flow-front.svg deleted file mode 100644 index 508d749..0000000 --- a/assets/app-icon/FieldCraft.icon/Assets/04-flow-front.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - FieldCraft icon flow in front of the cube - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/app-icon/FieldCraft.icon/icon.json b/assets/app-icon/FieldCraft.icon/icon.json index bcb26e7..06dc90f 100644 --- a/assets/app-icon/FieldCraft.icon/icon.json +++ b/assets/app-icon/FieldCraft.icon/icon.json @@ -10,8 +10,8 @@ "value": true } ], - "image-name": "04-flow-front.svg", - "name": "04-flow-front", + "image-name": "02-part.svg", + "name": "02-part", "position": { "scale": 0.9, "translation-in-points": [ @@ -21,38 +21,7 @@ } } ], - "name": "04 Flow front", - "shadow": { - "kind": "none", - "opacity": 0 - }, - "specular": false, - "translucency": { - "enabled": false, - "value": 0.3 - } - }, - { - "layers": [ - { - "glass-specializations": [ - { - "appearance": "tinted", - "value": true - } - ], - "image-name": "03-part.svg", - "name": "03-part", - "position": { - "scale": 0.9, - "translation-in-points": [ - 0, - 0 - ] - } - } - ], - "name": "03 Part", + "name": "02 Part", "shadow": { "kind": "none", "opacity": 0 @@ -63,37 +32,6 @@ "value": 0.3 } }, - { - "layers": [ - { - "glass-specializations": [ - { - "appearance": "tinted", - "value": true - } - ], - "image-name": "02-flow-behind.svg", - "name": "02-flow-behind", - "position": { - "scale": 0.9, - "translation-in-points": [ - 0, - 0 - ] - } - } - ], - "name": "02 Flow behind", - "shadow": { - "kind": "none", - "opacity": 0 - }, - "specular": false, - "translucency": { - "enabled": false, - "value": 0.3 - } - }, { "layers": [ { diff --git a/assets/app-icon/README.md b/assets/app-icon/README.md index d02a285..02da28b 100644 --- a/assets/app-icon/README.md +++ b/assets/app-icon/README.md @@ -2,9 +2,7 @@ FieldCraft's mark is a cube in isometric view, covered by a structured hexahedral mesh, whose faces carry a banded colour fringe (blue to red, hottest at the front corner) as a stress or -temperature result would. Streamlines coloured by flow speed in the same bands, with arrowheads, -sweep in from the lower left: two run into the top face and narrow into its mesh lines, so the -flow becomes the structure, and one wraps around the cube. Its faces are separated by +temperature result would. Its faces are separated by transparent gaps rather than painted edge lines, so the backdrop shows through them in every appearance. It is original artwork, attributed in `ATTRIBUTION.md`, and shares the Craft family's layout (rounded-square backdrop, isometric floor grid, lit top faces). @@ -17,10 +15,10 @@ hand. It runs on macOS (sips, iconutil, `xcrun actool`) with ImageMagick (`magic | File | What it is | |---|---| | `icon.vectorcraft`, `icon.svg`, `icon.png` | The full icon on its backdrop, 1024 × 1024 | -| `icon-small.vectorcraft`, `icon-small.svg`, `icon-small.png` | Small-size artwork used at 16–32 px: the fringe cube and one bold coloured streamline with an arrowhead, no mesh or floor grid, faces edge to edge | +| `icon-small.vectorcraft`, `icon-small.svg`, `icon-small.png` | Small-size artwork used at 16–32 px: the fringe cube alone, no mesh or floor grid, faces edge to edge | | `icon.iconset/`, `icon.icns` | macOS iconset (sips) and its iconutil compilation | | `icon.ico` | Windows icon, 16–256 px | -| `FieldCraft.icon/` | Icon Composer document: floor grid (Default and Dark), flow behind, cube (faces and mesh) and flow in front layers, each scaled to 90% | +| `FieldCraft.icon/` | Icon Composer document: floor grid (Default and Dark), and cube (faces and mesh) layers, each scaled to 90% | | `layers/*.vectorcraft` | Editable sources of the Icon Composer layers | | `Assets.car` | `actool` compilation of `FieldCraft.icon` for macOS 26 and later (needs Xcode 26 or later; the task skips it with a note otherwise) | diff --git a/assets/app-icon/icon-small.png b/assets/app-icon/icon-small.png index 1454fec..cf183b3 100644 Binary files a/assets/app-icon/icon-small.png and b/assets/app-icon/icon-small.png differ diff --git a/assets/app-icon/icon-small.svg b/assets/app-icon/icon-small.svg index d6c883d..3e3fe7d 100644 --- a/assets/app-icon/icon-small.svg +++ b/assets/app-icon/icon-small.svg @@ -7,13 +7,13 @@ - + - + - + FieldCraft application icon, small sizes @@ -22,49 +22,34 @@ - - - - - - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + diff --git a/assets/app-icon/icon-small.vectorcraft b/assets/app-icon/icon-small.vectorcraft index 3241283..6be9558 100644 Binary files a/assets/app-icon/icon-small.vectorcraft and b/assets/app-icon/icon-small.vectorcraft differ diff --git a/assets/app-icon/icon.icns b/assets/app-icon/icon.icns index 6eec029..4cd5253 100644 Binary files a/assets/app-icon/icon.icns and b/assets/app-icon/icon.icns differ diff --git a/assets/app-icon/icon.ico b/assets/app-icon/icon.ico index 057f1d7..6a995dc 100644 Binary files a/assets/app-icon/icon.ico and b/assets/app-icon/icon.ico differ diff --git a/assets/app-icon/icon.iconset/icon_128x128.png b/assets/app-icon/icon.iconset/icon_128x128.png index d945c19..0096299 100644 Binary files a/assets/app-icon/icon.iconset/icon_128x128.png and b/assets/app-icon/icon.iconset/icon_128x128.png differ diff --git a/assets/app-icon/icon.iconset/icon_128x128@2x.png b/assets/app-icon/icon.iconset/icon_128x128@2x.png index a8638c1..af995a0 100644 Binary files a/assets/app-icon/icon.iconset/icon_128x128@2x.png and b/assets/app-icon/icon.iconset/icon_128x128@2x.png differ diff --git a/assets/app-icon/icon.iconset/icon_16x16.png b/assets/app-icon/icon.iconset/icon_16x16.png index 1cf2e49..42055d5 100644 Binary files a/assets/app-icon/icon.iconset/icon_16x16.png and b/assets/app-icon/icon.iconset/icon_16x16.png differ diff --git a/assets/app-icon/icon.iconset/icon_16x16@2x.png b/assets/app-icon/icon.iconset/icon_16x16@2x.png index 317996b..03f2277 100644 Binary files a/assets/app-icon/icon.iconset/icon_16x16@2x.png and b/assets/app-icon/icon.iconset/icon_16x16@2x.png differ diff --git a/assets/app-icon/icon.iconset/icon_256x256.png b/assets/app-icon/icon.iconset/icon_256x256.png index a8638c1..af995a0 100644 Binary files a/assets/app-icon/icon.iconset/icon_256x256.png and b/assets/app-icon/icon.iconset/icon_256x256.png differ diff --git a/assets/app-icon/icon.iconset/icon_256x256@2x.png b/assets/app-icon/icon.iconset/icon_256x256@2x.png index ae93e80..9c775a5 100644 Binary files a/assets/app-icon/icon.iconset/icon_256x256@2x.png and b/assets/app-icon/icon.iconset/icon_256x256@2x.png differ diff --git a/assets/app-icon/icon.iconset/icon_32x32.png b/assets/app-icon/icon.iconset/icon_32x32.png index 317996b..03f2277 100644 Binary files a/assets/app-icon/icon.iconset/icon_32x32.png and b/assets/app-icon/icon.iconset/icon_32x32.png differ diff --git a/assets/app-icon/icon.iconset/icon_32x32@2x.png b/assets/app-icon/icon.iconset/icon_32x32@2x.png index f28b46b..17a7dd0 100644 Binary files a/assets/app-icon/icon.iconset/icon_32x32@2x.png and b/assets/app-icon/icon.iconset/icon_32x32@2x.png differ diff --git a/assets/app-icon/icon.iconset/icon_512x512.png b/assets/app-icon/icon.iconset/icon_512x512.png index ae93e80..9c775a5 100644 Binary files a/assets/app-icon/icon.iconset/icon_512x512.png and b/assets/app-icon/icon.iconset/icon_512x512.png differ diff --git a/assets/app-icon/icon.iconset/icon_512x512@2x.png b/assets/app-icon/icon.iconset/icon_512x512@2x.png index 969190d..b409e75 100644 Binary files a/assets/app-icon/icon.iconset/icon_512x512@2x.png and b/assets/app-icon/icon.iconset/icon_512x512@2x.png differ diff --git a/assets/app-icon/icon.png b/assets/app-icon/icon.png index 969190d..b409e75 100644 Binary files a/assets/app-icon/icon.png and b/assets/app-icon/icon.png differ diff --git a/assets/app-icon/icon.svg b/assets/app-icon/icon.svg index c4f5208..c2cd6ca 100644 --- a/assets/app-icon/icon.svg +++ b/assets/app-icon/icon.svg @@ -9,23 +9,23 @@ - + - + - + - + - + FieldCraft application icon @@ -35,76 +35,39 @@ - - - - - - - - - + - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/assets/app-icon/icon.vectorcraft b/assets/app-icon/icon.vectorcraft index 881258f..24937e7 100644 Binary files a/assets/app-icon/icon.vectorcraft and b/assets/app-icon/icon.vectorcraft differ diff --git a/assets/app-icon/layers/01-floor-dark.vectorcraft b/assets/app-icon/layers/01-floor-dark.vectorcraft index 6b7c84d..45da97c 100644 Binary files a/assets/app-icon/layers/01-floor-dark.vectorcraft and b/assets/app-icon/layers/01-floor-dark.vectorcraft differ diff --git a/assets/app-icon/layers/01-floor.vectorcraft b/assets/app-icon/layers/01-floor.vectorcraft index 2728d26..f2eaa7e 100644 Binary files a/assets/app-icon/layers/01-floor.vectorcraft and b/assets/app-icon/layers/01-floor.vectorcraft differ diff --git a/assets/app-icon/layers/02-flow-behind.vectorcraft b/assets/app-icon/layers/02-flow-behind.vectorcraft deleted file mode 100644 index 14d5600..0000000 Binary files a/assets/app-icon/layers/02-flow-behind.vectorcraft and /dev/null differ diff --git a/assets/app-icon/layers/02-part.vectorcraft b/assets/app-icon/layers/02-part.vectorcraft new file mode 100644 index 0000000..c0d65d5 Binary files /dev/null and b/assets/app-icon/layers/02-part.vectorcraft differ diff --git a/assets/app-icon/layers/03-part.vectorcraft b/assets/app-icon/layers/03-part.vectorcraft deleted file mode 100644 index 9f82458..0000000 Binary files a/assets/app-icon/layers/03-part.vectorcraft and /dev/null differ diff --git a/assets/app-icon/layers/04-flow-front.vectorcraft b/assets/app-icon/layers/04-flow-front.vectorcraft deleted file mode 100644 index 40bc088..0000000 Binary files a/assets/app-icon/layers/04-flow-front.vectorcraft and /dev/null differ diff --git a/xtask/src/app_icon.rs b/xtask/src/app_icon.rs index e1189ed..70fdc31 100644 --- a/xtask/src/app_icon.rs +++ b/xtask/src/app_icon.rs @@ -2,28 +2,24 @@ //! //! The mark is a hexahedral-meshed cube in isometric view whose faces carry a banded colour //! fringe (blue to red, hottest at the cube's front corner) as a stress or temperature result -//! would. Streamlines coloured by flow speed in the same bands, with arrowheads, sweep in: two -//! run into the top face and narrow into its mesh lines (the flow becomes the structure), one -//! wraps around the cube. The faces are -//! separated by transparent gaps instead of painted edge lines, so the backdrop shows through -//! them in every appearance. All geometry is computed here, turned into VectorCraft commands and +//! would. The faces are separated by transparent gaps instead of painted edge lines, so the +//! backdrop shows through them in every appearance. All geometry is computed here, turned into VectorCraft commands and //! run headlessly with `vectorcraft-cli run` (`VECTORCRAFT_CLI`, else `vectorcraft-cli` on the //! PATH), which saves each document's editable `.vectorcraft` source and exports its SVG (and PNG). //! //! Outputs, all under `assets/app-icon/` (see its README.md): //! - `icon.vectorcraft`, `icon.svg`, `icon.png`: the full icon on its backdrop, 1024 × 1024. //! - `icon-small.vectorcraft`, `icon-small.svg`, `icon-small.png`: simplified artwork for 16–32 px -//! (no mesh or floor grid, faces edge to edge, one bold streamline with an arrowhead). +//! (no mesh or floor grid, faces edge to edge). //! - `icon.iconset/` (sips), `icon.icns` (iconutil) and `icon.ico` (ImageMagick). //! - `FieldCraft.icon/`: an Icon Composer document with separate floor-grid (Default and Dark), -//! flow (behind and in front) and cube layers (the mesh shares the cube's: Icon Composer allows -//! four groups); their editable sources are `layers/*.vectorcraft`. +//! and cube layers (the mesh shares the cube's); their editable sources are `layers/*.vectorcraft`. //! - `Assets.car`: Apple's actool compilation of `FieldCraft.icon`, when Xcode 26 or later is //! installed (otherwise the step is skipped with a note and the checked-in catalog stays). //! //! macOS only (sips, iconutil); ImageMagick (`magick`) writes the Windows icon. -use std::f64::consts::{FRAC_PI_2, PI}; +use std::f64::consts::FRAC_PI_2; use std::path::{Path, PathBuf}; use std::process::Command; @@ -35,7 +31,7 @@ type P3 = [f64; 3]; /// Canvas side in pixels; every document is one square artboard. const CANVAS: f64 = 1024.0; /// Pixels per model unit, and the screen point the model origin projects to. -const SCALE: f64 = 286.0; +const SCALE: f64 = 300.0; const CENTRE: P2 = [512.0, 500.0]; const COS30: f64 = 0.866_025_403_784_438_6; @@ -55,17 +51,13 @@ const BAND_RADII: [f64; 6] = [1.28, 1.07, 0.87, 0.67, 0.47, 0.27]; /// The macOS icon grid: the backdrop's square, inset 100 px on a 1024 canvas. const BACKDROP: [f64; 2] = [100.0, 924.0]; -/// The streamlines' halo colour (the same in both appearances: it keeps the fringe-coloured -/// cores readable on the light backdrop and over the cube). -const FLOW_HALO: &str = "#0b2236"; - #[derive(Clone, Copy, PartialEq)] enum Appearance { Light, Dark, } -/// How much of the mark is drawn: the full mesh, flow and floor grid, or the small-size artwork. +/// How much of the mark is drawn: the full mesh and floor grid, or the small-size artwork. #[derive(Clone, Copy, PartialEq)] enum Detail { Full, @@ -308,144 +300,6 @@ fn mesh_d() -> String { segs.iter().map(|&(a, b)| polyline(&[project(a), project(b)], false)).collect::>().join(" ") } -/// A streamline's path: a swept curve around the cube's vertical axis, or a feed that sweeps in -/// from the lower left, climbs over the left face and runs into the top face's mesh line -/// `x = c`, carrying on along it as the hex edge. -enum Path3 { - /// Radius `r`, height `z0 + tilt · sin(θ − 45°)`, from angle `from` to `to` (degrees, - /// decreasing: the flow passes in front of the cube from screen left to right). - Swirl { r: f64, z0: f64, tilt: f64, from: f64, to: f64 }, - /// In the plane `x = c`: a cubic from `start` (y, z) to the top face's front-left edge, - /// arriving level so it runs straight on into the mesh line. - Feed { c: f64, start: P2, bend: P2 }, -} - -struct Stream { - path: Path3, - /// Widest core, in pixels on the 1024 canvas. - width: f64, - /// Where the arrowheads sit, as fractions of the path. - arrows: &'static [f64], -} - -const STREAMS: [Stream; 3] = [ - Stream { path: Path3::Feed { c: -0.25, start: [1.25, -0.7], bend: [0.95, -0.6] }, width: 17.0, arrows: &[0.45] }, - Stream { path: Path3::Feed { c: 0.25, start: [1.05, -0.6], bend: [0.9, -0.45] }, width: 17.0, arrows: &[0.5] }, - Stream { path: Path3::Swirl { r: 0.98, z0: -0.08, tilt: 0.22, from: 215.0, to: -60.0 }, width: 17.0, arrows: &[0.3, 1.0] }, -]; - -/// The small-size artwork's one streamline. -const SMALL_STREAM: Stream = Stream { path: Path3::Swirl { r: 1.0, z0: -0.12, tilt: 0.14, from: 190.0, to: -40.0 }, width: 44.0, arrows: &[1.0] }; - -/// Samples along each streamline. -const STREAM_SAMPLES: usize = 160; - -/// The fringe band of the flow speed at fraction `t` along a streamline: slow (blue) at its -/// tail, fastest (red) where it reaches the cube or its head. -fn speed_band(t: f64) -> usize { - ((t * FRINGE.len() as f64) as usize).min(FRINGE.len() - 1) -} - -impl Path3 { - fn at(&self, t: f64) -> P3 { - match *self { - Path3::Swirl { r, z0, tilt, from, to } => { - let a = (from + (to - from) * t).to_radians(); - [r * a.cos(), r * a.sin(), z0 + tilt * (a - PI / 4.0).sin()] - } - Path3::Feed { c, start, bend } => { - let (p0, p1, p2, p3) = (start, bend, [A + 0.35, A], [A, A]); - let u = 1.0 - t; - let b = |k: usize| u * u * u * p0[k] + 3.0 * u * u * t * p1[k] + 3.0 * u * t * t * p2[k] + t * t * t * p3[k]; - [c, b(0), b(1)] - } - } - } - - /// The mesh line a feed runs on into, across the top face (none for a swirl). - fn continuation(&self) -> Option<(P3, P3)> { - match *self { - Path3::Swirl { .. } => None, - Path3::Feed { c, .. } => Some(([c, A, A], [c, -A, A])), - } - } -} - -/// A filled shape of a streamline: its path data and colour (`None` for the halo colour). -type Piece = (String, Option<&'static str>); - -/// A streamline drawn as tapered ribbons: the halo (`grow` pixels wider each side) and the core -/// split into speed bands, then the arrowheads. Returns (behind the cube, in front of it). -fn stream_pieces(s: &Stream, grow: f64) -> (Vec, Vec) { - let n = STREAM_SAMPLES; - let ts: Vec = (0..=n).map(|i| i as f64 / n as f64).collect(); - let pts3: Vec = ts.iter().map(|&t| s.path.at(t)).collect(); - let pts: Vec = pts3.iter().map(|&p| project(p)).collect(); - // A tail that grows to full width over the first third. - let half: Vec = ts.iter().map(|&t| s.width / 2.0 * (t / 0.3).min(1.0).powf(0.6) + grow).collect(); - let dir = |i: usize| { - let v = sub(pts[(i + 1).min(n)], pts[i.saturating_sub(1)]); - let len = v[0].hypot(v[1]).max(1e-9); - [v[0] / len, v[1] / len] - }; - let ribbon = |lo: usize, hi: usize| { - let side = |i: usize, k: f64| { - let d = dir(i); - [pts[i][0] - d[1] * half[i] * k, pts[i][1] + d[0] * half[i] * k] - }; - let mut poly: Vec = (lo..=hi).map(|i| side(i, 1.0)).collect(); - poly.extend((lo..=hi).rev().map(|i| side(i, -1.0))); - polyline(&poly, true) - }; - let colour = |i: usize| if grow > 0.0 { None } else { Some(FRINGE[speed_band(ts[i])]) }; - let front = |i: usize| pts3[i][0] + pts3[i][1] > 0.0; - let (mut behind, mut ahead) = (Vec::new(), Vec::new()); - // Runs of one side of the cube and one colour; neighbours share their boundary sample, so - // they meet without a seam. - let mut lo = 0; - for i in 1..=n { - if i == n || front(i) != front(lo) || colour(i) != colour(lo) { - let piece = (ribbon(lo, i), colour(lo)); - if front(lo) { - ahead.push(piece) - } else { - behind.push(piece) - } - lo = i; - } - } - // A feed carries on along its mesh line, narrowing to the mesh's stroke: the flow becomes - // the hex edge. - if let Some((a, b)) = s.path.continuation() { - { - let (p, q) = (project(a), project(b)); - let v = sub(q, p); - let len = v[0].hypot(v[1]).max(1e-9); - let m = [-v[1] / len, v[0] / len]; - let (w0, w1) = (half[n], 0.6 + grow * 0.6); - let poly = [ - [p[0] + m[0] * w0, p[1] + m[1] * w0], - [q[0] + m[0] * w1, q[1] + m[1] * w1], - [q[0] - m[0] * w1, q[1] - m[1] * w1], - [p[0] - m[0] * w0, p[1] - m[1] * w0], - ]; - ahead.push((polyline(&poly, true), colour(n))); - } - } - for &f in s.arrows { - let i = ((f * n as f64).round() as usize).min(n); - let (d, p, w) = (dir(i), pts[i], s.width); - let g = grow * 1.6; - let tip = [p[0] + d[0] * (1.5 * w + g), p[1] + d[1] * (1.5 * w + g)]; - let back = [p[0] - d[0] * (0.7 * w + g), p[1] - d[1] * (0.7 * w + g)]; - let wing = 1.25 * w + g; - let poly = [tip, [back[0] - d[1] * wing, back[1] + d[0] * wing], [back[0] + d[1] * wing, back[1] - d[0] * wing]]; - let piece = (polyline(&poly, true), colour(i)); - if front(i) { ahead.push(piece) } else { behind.push(piece) } - } - (behind, ahead) -} - /// Clip a segment to the axis-aligned square [lo, hi]² (Liang–Barsky). fn clip_to_square(a: P2, b: P2, lo: f64, hi: f64) -> Option<(P2, P2)> { let d = sub(b, a); @@ -556,28 +410,6 @@ fn mesh_layer() -> Result { Ok(Layer { name: "Mesh", shapes: vec![Shape::stroked("hexahedral mesh", mesh_d(), stroke, 0.45)], clip: Some(clip) }) } -/// The streamlines' runs behind the cube (`front == false`) or in front of it. -fn flow_layer(detail: Detail, front: bool) -> Layer { - let streams: Vec<&Stream> = match detail { - Detail::Full => STREAMS.iter().collect(), - Detail::Small => vec![&SMALL_STREAM], - }; - let mut shapes = Vec::new(); - for (k, s) in streams.into_iter().enumerate() { - let pick = |(behind, ahead): (Vec, Vec)| if front { ahead } else { behind }; - let halo = pick(stream_pieces(s, (s.width * 0.18).max(3.0))); - if halo.is_empty() { - continue; - } - let d = halo.into_iter().map(|p| p.0).collect::>().join(" "); - shapes.push(Shape::filled(&format!("streamline {} halo", k + 1), d, colour(FLOW_HALO))); - for (j, (d, c)) in pick(stream_pieces(s, 0.0)).into_iter().enumerate() { - shapes.push(Shape::filled(&format!("streamline {} part {}", k + 1, j + 1), d, colour(c.unwrap_or(FLOW_HALO)))); - } - } - Layer { name: if front { "Flow in front" } else { "Flow behind" }, shapes, clip: None } -} - fn floor_layer(appearance: Appearance) -> Layer { let colour = match appearance { Appearance::Light => "#8aa0b4", @@ -608,12 +440,10 @@ fn composite(detail: Detail, appearance: Appearance) -> Result { if detail == Detail::Full { layers.push(floor_layer(appearance)); } - layers.push(flow_layer(detail, false)); layers.extend(part_layers(detail)?); if detail == Detail::Full { layers.push(mesh_layer()?); } - layers.push(flow_layer(detail, true)); let title = match (detail, appearance) { (Detail::Full, Appearance::Light) => "FieldCraft application icon", (Detail::Full, Appearance::Dark) => "FieldCraft application icon (dark preview)", @@ -723,9 +553,7 @@ fn icon_json() -> Value { json!({ "fill": "system-light", "groups": [ - group("04 Flow front", plain("04-flow-front"), false), - group("03 Part", plain("03-part"), true), - group("02 Flow behind", plain("02-flow-behind"), false), + group("02 Part", plain("02-part"), true), group("01 Floor", json!({"image-name-specializations": [{"value": "01-floor.svg"}, {"appearance": "dark", "value": "01-floor-dark.svg"}], "name": "01-floor", "fill": "none", "position": position, "glass-specializations": [{"value": false}, {"appearance": "tinted", "value": true}]}), false), @@ -757,16 +585,14 @@ pub fn run(root: &Path) -> Result<(), String> { vectorcraft(&composite(Detail::Small, Appearance::Dark)?, &work.join("icon-small-dark.vectorcraft"), &[work.join("icon-small-dark.png")])?; // Icon Composer layers: the system supplies the backdrop. - // Icon Composer allows four groups, so the mesh shares the cube's. + // The mesh shares the cube's Icon Composer group. let single = |title, layer| Doc { title, layers: vec![layer] }; let mut part_with_mesh = part_layers(Detail::Full)?; part_with_mesh.push(mesh_layer()?); let layer_docs = [ ("01-floor", single("FieldCraft icon floor grid", floor_layer(Appearance::Light))), ("01-floor-dark", single("FieldCraft icon floor grid (Dark)", floor_layer(Appearance::Dark))), - ("02-flow-behind", single("FieldCraft icon flow behind the cube", flow_layer(Detail::Full, false))), - ("03-part", Doc { title: "FieldCraft icon cube", layers: part_with_mesh }), - ("04-flow-front", single("FieldCraft icon flow in front of the cube", flow_layer(Detail::Full, true))), + ("02-part", Doc { title: "FieldCraft icon cube", layers: part_with_mesh }), ]; for (name, doc) in &layer_docs { vectorcraft(doc, &layers.join(format!("{name}.vectorcraft")), &[assets.join(format!("{name}.svg"))])?; @@ -850,18 +676,6 @@ mod tests { } } - #[test] - fn streamlines_pass_where_they_should() { - for s in &STREAMS { - let (behind, ahead) = stream_pieces(s, 0.0); - assert!(!ahead.is_empty()); - // Only the swirl goes round the back; a feed stays in front and ends on the mesh. - assert_eq!(behind.is_empty(), s.path.continuation().is_some()); - } - assert_eq!(speed_band(0.0), 0); - assert_eq!(speed_band(1.0), FRINGE.len() - 1); - } - #[test] fn segments_clip_to_the_square() { assert_eq!(clip_to_square([-5.0, 5.0], [15.0, 5.0], 0.0, 10.0), Some(([0.0, 5.0], [10.0, 5.0]))); @@ -874,8 +688,8 @@ mod tests { let steps = commands(&doc, Path::new("/tmp/icon.vectorcraft")); assert_eq!(steps.first().map(|s| s.0), Some("file.new")); assert_eq!(steps.last().map(|s| s.0), Some("document.save")); - // Floor, flow behind, three faces, mesh, flow in front. - assert_eq!(steps.iter().filter(|s| s.0 == "layer.new").count(), 7); + // Floor, three faces, mesh. + assert_eq!(steps.iter().filter(|s| s.0 == "layer.new").count(), 5); // The floor grid is clipped to the backdrop, each face's fringe and the mesh to the faces. assert_eq!(steps.iter().filter(|s| s.0 == "layer.clippingMask.toggle").count(), 5); }