Files
vectorcraft/crates/cad/src/aci.rs
T
bflatasticandClaude Opus 5.5 a17cc428d1 M4.77: DXF export (R12 to 2018) with DXF Options; DWG and PICT say what to use instead
New crates/cad (L3): a hand-written ASCII DXF writer. Layers become DXF
layers (hidden: off, locked, non-printing: not plotted), straight paths
LWPOLYLINE (POLYLINE before R14), curves cubic SPLINEs through every
anchor, fills solid HATCHes (outlines in R12), strokes lines with a
lineweight and a linetype per dash pattern, type TEXT or glyph outlines,
placed images IMAGE entities linked to PNG or JPEG files. Units and
scale ($INSUNITS), scaled lineweights, 8/16/256 indexed or true colour
(420), transparency (440), Preserve Appearance / Maximize Editability,
Alter Paths for Appearance, Outline Text.

document.export {format: dxf} and document.exportDxf; useArtboards
writes one drawing per artboard; selectedOnly (any export) writes the
selection in its layers. Export As lists DXF (then DXF Options,
ui.dxfOptionsDialog for agents, remembered between exports) and DWG
greyed out; document.formats lists DWG and PICT as unsupported with a
hint, and exporting or opening them answers with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:00:17 +07:00

106 lines
4.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The 256-colour index palette CAD apps share, generated rather than tabulated: indices 1–9 are
//! the standard colours, 10–249 run through 24 hues at five brightnesses each in a full and a
//! half-saturated tone, and 250–255 are greys.
use crate::ColorDepth;
/// The nine standard colours (index 7 shows white on a dark background and black on paper).
const STANDARD: [[u8; 3]; 9] =
[[255, 0, 0], [255, 255, 0], [0, 255, 0], [0, 255, 255], [0, 0, 255], [255, 0, 255], [255, 255, 255], [128, 128, 128], [192, 192, 192]];
/// The brightness of the five shades of each hue.
const SHADES: [f64; 5] = [1.0, 0.65, 0.5, 0.3, 0.15];
/// The RGB of colour index `i` (1–255; anything else is index 7's white).
pub fn aci_rgb(i: u8) -> [u8; 3] {
match i {
1..=9 => STANDARD[usize::from(i - 1)],
10..=249 => {
let hue = f64::from(i / 10 - 1) * 15.0;
let k = i % 10;
let v = SHADES[usize::from(k / 2)];
let pastel = k % 2 == 1;
hue_rgb(hue).map(|f| {
let f = if pastel { f + (1.0 - f) / 2.0 } else { f };
(255.0 * v * f).floor() as u8
})
}
250..=255 => {
let g = (51.0 + 204.0 * f64::from(i - 250) / 5.0).floor() as u8;
[g; 3]
}
_ => STANDARD[6],
}
}
/// The fully saturated, full-brightness colour of `hue` degrees as 0–1 channels.
fn hue_rgb(hue: f64) -> [f64; 3] {
let h = hue.rem_euclid(360.0) / 60.0;
let x = 1.0 - (h % 2.0 - 1.0).abs();
match h as u8 {
0 => [1.0, x, 0.0],
1 => [x, 1.0, 0.0],
2 => [0.0, 1.0, x],
3 => [0.0, x, 1.0],
4 => [x, 0.0, 1.0],
_ => [1.0, 0.0, x],
}
}
/// The indices a colour depth may use (true colour: all of them, for the fallback index).
fn candidates(depth: ColorDepth) -> Box<dyn Iterator<Item = u8>> {
match depth {
ColorDepth::Aci8 => Box::new(1..=8),
ColorDepth::Aci16 => Box::new((1..=9).chain([30]).chain(250..=255)),
ColorDepth::Aci256 | ColorDepth::True => Box::new(1..=255),
}
}
/// The index of `depth` nearest to `rgb` (squared RGB distance; ties go to the lower index).
/// Index 7 stands for both white and black, as CAD apps draw it in the background's contrast.
pub fn nearest_aci(rgb: [u8; 3], depth: ColorDepth) -> u8 {
let dist = |c: [u8; 3]| rgb.iter().zip(c).map(|(a, b)| (i32::from(*a) - i32::from(b)).pow(2)).sum::<i32>();
candidates(depth).min_by_key(|i| if *i == 7 { dist([255; 3]).min(dist([0; 3])) } else { dist(aci_rgb(*i)) }).unwrap_or(7)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_palette_matches_its_known_entries() {
let known: [(u8, [u8; 3]); 14] = [
(1, [255, 0, 0]),
(10, [255, 0, 0]),
(11, [255, 127, 127]),
(12, [165, 0, 0]),
(13, [165, 82, 82]),
(17, [76, 38, 38]),
(19, [38, 19, 19]),
(20, [255, 63, 0]),
(21, [255, 159, 127]),
(30, [255, 127, 0]),
(140, [0, 191, 255]),
(250, [51, 51, 51]),
(252, [132, 132, 132]),
(255, [255, 255, 255]),
];
for (i, rgb) in known {
assert_eq!(aci_rgb(i), rgb, "index {i}");
}
}
#[test]
fn nearest_index_by_depth() {
assert_eq!(nearest_aci([255, 0, 0], ColorDepth::Aci8), 1);
assert_eq!(nearest_aci([0, 0, 0], ColorDepth::Aci256), 7, "black is index 7");
assert_eq!(nearest_aci([255, 255, 255], ColorDepth::Aci16), 7);
assert_eq!(nearest_aci([255, 127, 0], ColorDepth::Aci256), 30);
assert_eq!(nearest_aci([255, 127, 0], ColorDepth::Aci16), 30);
assert_eq!(nearest_aci([255, 127, 0], ColorDepth::Aci8), 1, "8 colours: red is nearest");
assert_eq!(nearest_aci([60, 60, 60], ColorDepth::Aci16), 250);
assert_eq!(nearest_aci([100, 100, 100], ColorDepth::Aci8), 8);
assert_eq!(nearest_aci([60, 60, 60], ColorDepth::Aci8), 7, "dark greys go to black");
}
}