diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8403e84..47b58a0 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2210,6 +2210,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -2217,9 +2218,6 @@ name = "objc-sys" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" -dependencies = [ - "cc", -] [[package]] name = "objc2" @@ -2247,30 +2245,6 @@ dependencies = [ "objc2-quartz-core", ] -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.6.0", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", -] - -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", -] - [[package]] name = "objc2-core-data" version = "0.2.2" @@ -2295,18 +2269,6 @@ dependencies = [ "objc2-metal", ] -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2", - "objc2", - "objc2-contacts", - "objc2-foundation", -] - [[package]] name = "objc2-encode" version = "4.0.3" @@ -2326,18 +2288,6 @@ dependencies = [ "objc2", ] -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", -] - [[package]] name = "objc2-metal" version = "0.2.2" @@ -2364,71 +2314,21 @@ dependencies = [ ] [[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" -dependencies = [ - "bitflags 2.6.0", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" +name = "objc_exception" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" dependencies = [ - "bitflags 2.6.0", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", + "cc", ] [[package]] -name = "objc2-web-kit" -version = "0.2.2" +name = "objc_id" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" dependencies = [ - "bitflags 2.6.0", - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc", ] [[package]] @@ -3699,9 +3599,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.0.3" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd96d46534b10765ce0c6208f9451d98ea38636364a41b272d3610c70dd0e4c3" +checksum = "5920aad0804ea5e86808d4b6e8753d3bcbae7efc8f4e41a4da00b45427559868" dependencies = [ "anyhow", "bytes", @@ -3890,9 +3790,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8f437293d6f5e5dce829250f4dbdce4e0b52905e297a6689cc2963eb53ac728" +checksum = "af12ad1af974b274ef1d32a94e6eba27a312b429ef28fcb98abc710df7f9151d" dependencies = [ "dpi", "gtk", @@ -3909,9 +3809,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaac63b65df8e85570993eaf93ae1dd73a6fb66d8bd99674ce65f41dc3c63e7d" +checksum = "e45e88aa0b11b302d836e6ea3e507a6359044c4a8bc86b865ba99868c695753d" dependencies = [ "gtk", "http", @@ -5059,12 +4959,14 @@ dependencies = [ [[package]] name = "wry" -version = "0.46.0" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469a3765ecc3e8aa9ccdf3c5a52c82697ec03037cd60494488763880d31a1b3a" +checksum = "440600584cfbd8b0d28eace95c1f2c253db05dae43780b79380aa1e868f04c73" dependencies = [ "base64 0.22.1", - "block2", + "block", + "cocoa", + "core-graphics", "crossbeam-channel", "dpi", "dunce", @@ -5077,11 +4979,8 @@ dependencies = [ "kuchikiki", "libc", "ndk", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", + "objc", + "objc_id", "once_cell", "percent-encoding", "raw-window-handle", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a8afe4f..6a55cd3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,7 +16,7 @@ tauri-build = { version = "2.0.1", features = [] } [dependencies] # Tauri -tauri = { version = "2.0.3", features = ["test"] } +tauri = { version = "2.0.2", features = ["test"] } tauri-plugin-dialog = "2.0.1" tauri-plugin-log = "2.0.1" diff --git a/src-tauri/src/commands/pattern.rs b/src-tauri/src/commands/pattern.rs index 48b9632..159d850 100644 --- a/src-tauri/src/commands/pattern.rs +++ b/src-tauri/src/commands/pattern.rs @@ -1,7 +1,7 @@ use crate::{ error::CommandResult, parser::{self, PatternFormat}, - pattern::Pattern, + pattern::{display::DisplaySettings, print::PrintSettings, Pattern, PatternProject}, state::{AppStateType, PatternKey}, }; @@ -12,7 +12,7 @@ pub fn load_pattern(file_path: std::path::PathBuf, state: tauri::State { - log::trace!("Pattern already loaded"); + log::trace!("Pattern has been already loaded"); pattern.to_owned() } None => { @@ -20,10 +20,11 @@ pub fn load_pattern(file_path: std::path::PathBuf, state: tauri::State parser::xsd::parse_pattern(file_path)?, PatternFormat::Oxs => parser::oxs::parse_pattern(file_path)?, - PatternFormat::Embx => { - let mut reader = std::fs::File::open(file_path)?; - borsh::from_reader(&mut reader)? - } + // PatternFormat::EmbProj => { + // let mut reader = std::fs::File::open(file_path)?; + // borsh::from_reader(&mut reader)? + // } + PatternFormat::EmbProj => todo!(), }; state.patterns.insert(pattern_key, pattern.clone()); pattern @@ -38,10 +39,15 @@ pub fn create_pattern(state: tauri::State) -> (PatternKey, Vec log::trace!("Creating new pattern"); let mut state = state.write().unwrap(); let file_path = std::path::PathBuf::from(format!("Untitled-{:?}.json", std::time::Instant::now())); - let pattern_key = PatternKey::from(file_path); - let pattern = Pattern::default(); + let pattern_key = PatternKey::from(file_path.clone()); + let pattern = PatternProject { + file_path: Some(file_path), + pattern: Pattern::default(), + display_settings: DisplaySettings::new(2), + print_settings: PrintSettings::default(), + }; state.patterns.insert(pattern_key.clone(), pattern.clone()); - log::trace!("Pattern created"); + log::trace!("Pattern has been created"); // It is safe to unwrap here, because the pattern is always serializable. (pattern_key, borsh::to_vec(&pattern).unwrap()) } diff --git a/src-tauri/src/events/pattern.rs b/src-tauri/src/events/pattern.rs index b872004..bdd02a1 100644 --- a/src-tauri/src/events/pattern.rs +++ b/src-tauri/src/events/pattern.rs @@ -31,7 +31,7 @@ pub fn setup_event_handlers(window: &WebviewWindow, app_handle: &AppHandle) { // This is safe because the event is only emitted when the pattern exists. let pattern = state.patterns.get_mut(&pattern_key).unwrap(); - emit_remove_stitches(&win, pattern_key, pattern.add_stitch(payload)); + emit_remove_stitches(&win, pattern_key, pattern.pattern.add_stitch(payload)); }); let handle = app_handle.clone(); @@ -44,7 +44,7 @@ pub fn setup_event_handlers(window: &WebviewWindow, app_handle: &AppHandle) { serde_json::from_str::>(e.payload()).unwrap(); // This is safe because the event is only emitted when the pattern exists. let pattern = state.patterns.get_mut(&pattern_key).unwrap(); - pattern.remove_stitch(payload); + pattern.pattern.remove_stitch(payload); }); } diff --git a/src-tauri/src/parser/format.rs b/src-tauri/src/parser/format.rs index f831d9b..0f80d24 100644 --- a/src-tauri/src/parser/format.rs +++ b/src-tauri/src/parser/format.rs @@ -1,9 +1,19 @@ use std::ffi::OsStr; pub enum PatternFormat { + /// Probably, stands for `Cross-Stitch Design`. + /// Only **read-only** mode is currently available. Xsd, + + /// Stands for `Open Cross-Stitch`. + /// It is just an XML document. + /// This format is intended to be a lingua franca in the embroidery world. Oxs, - Embx, + + /// Stands for `Embroidery Project`. + /// It is a ZIP archive with a pack of binary files. + /// This format is not recommended for other applications. + EmbProj, } impl TryFrom> for PatternFormat { @@ -15,7 +25,7 @@ impl TryFrom> for PatternFormat { match extension.to_lowercase().as_str() { "xsd" => Ok(Self::Xsd), "oxs" | "xml" => Ok(Self::Oxs), - "embx" => Ok(Self::Embx), + "embproj" => Ok(Self::EmbProj), _ => anyhow::bail!("Unsupported pattern type: {extension}."), } } else { diff --git a/src-tauri/src/parser/oxs/parser.rs b/src-tauri/src/parser/oxs/parser.rs index e204b7d..65e22c1 100644 --- a/src-tauri/src/parser/oxs/parser.rs +++ b/src-tauri/src/parser/oxs/parser.rs @@ -1,17 +1,17 @@ use anyhow::{bail, Result}; use quick_xml::events::Event; -use crate::pattern::Pattern; +use crate::pattern::PatternProject; use super::{ utils::{process_attributes, OxsVersion, Software}, v1_0, }; -pub fn parse_pattern(path: impl AsRef) -> Result { +pub fn parse_pattern(file_path: std::path::PathBuf) -> Result { log::trace!("Parsing the OXS pattern"); - let mut reader = quick_xml::Reader::from_file(path.as_ref())?; + let mut reader = quick_xml::Reader::from_file(&file_path)?; let mut buf = Vec::new(); let (oxs_version, software) = loop { match reader.read_event_into(&mut buf) { @@ -33,9 +33,9 @@ pub fn parse_pattern(path: impl AsRef) -> Result { buf.clear(); }; - let pattern = match oxs_version { - OxsVersion::V1_0 => v1_0::parse_pattern(path.as_ref(), software)?, + let pattern_project = match oxs_version { + OxsVersion::V1_0 => v1_0::parse_pattern(file_path, software)?, }; - Ok(pattern) + Ok(pattern_project) } diff --git a/src-tauri/src/parser/oxs/v1_0.rs b/src-tauri/src/parser/oxs/v1_0.rs index 6322970..6aae7c1 100644 --- a/src-tauri/src/parser/oxs/v1_0.rs +++ b/src-tauri/src/parser/oxs/v1_0.rs @@ -1,18 +1,17 @@ use quick_xml::events::Event; -use crate::pattern::*; - use super::utils::{process_attributes, Software}; +use crate::pattern::{display::DisplaySettings, print::PrintSettings, *}; #[cfg(test)] #[path = "v1_0.test.rs"] mod tests; // TODO: Implement the comprehensive parser for the OXS 1.0 format -pub fn parse_pattern(path: &std::path::Path, software: Software) -> anyhow::Result { +pub fn parse_pattern(file_path: std::path::PathBuf, software: Software) -> anyhow::Result { log::trace!("OXS version is 1.0 in the {software:?} edition"); - let mut reader = quick_xml::Reader::from_file(path)?; + let mut reader = quick_xml::Reader::from_file(&file_path)?; let mut buf = Vec::new(); let mut properties = PatternProperties::default(); @@ -39,11 +38,12 @@ pub fn parse_pattern(path: &std::path::Path, software: Software) -> anyhow::Resu info = PatternInfo { title: attributes.get("charttitle").unwrap().to_owned(), author: attributes.get("author").unwrap().to_owned(), + company: String::from(""), copyright: attributes.get("copyright").unwrap().to_owned(), description: attributes.get("instructions").unwrap().to_owned(), }; - fabric.stitches_per_inch = ( + fabric.spi = ( attributes.get("stitchesperinch").unwrap().parse()?, attributes.get("stitchesperinch_y").unwrap().parse()?, ); @@ -63,6 +63,8 @@ pub fn parse_pattern(path: &std::path::Path, software: Software) -> anyhow::Resu name: attributes.get("name").unwrap().to_owned(), color: attributes.get("color").unwrap().to_owned(), blends: None, + bead: None, + strands: StitchStrands::default(), }); } } @@ -162,14 +164,21 @@ pub fn parse_pattern(path: &std::path::Path, software: Software) -> anyhow::Resu buf.clear(); } - Ok(Pattern { - properties, - info, - palette, - fabric, - fullstitches: Stitches::from_iter(fullstitches), - partstitches: Stitches::from_iter(partstitches), - nodes: Stitches::from_iter(nodes), - lines: Stitches::from_iter(backstitches), + Ok(PatternProject { + file_path: Some(file_path), + display_settings: DisplaySettings::new(palette.len()), + print_settings: PrintSettings::default(), + pattern: Pattern { + properties, + info, + palette, + fabric, + fullstitches: Stitches::from_iter(fullstitches), + partstitches: Stitches::from_iter(partstitches), + nodes: Stitches::from_iter(nodes), + lines: Stitches::from_iter(backstitches), + specialstitches: Stitches::new(), + special_stitch_models: Vec::new(), + }, }) } diff --git a/src-tauri/src/parser/oxs/v1_0.test.rs b/src-tauri/src/parser/oxs/v1_0.test.rs index ca31341..62ecb21 100644 --- a/src-tauri/src/parser/oxs/v1_0.test.rs +++ b/src-tauri/src/parser/oxs/v1_0.test.rs @@ -1,13 +1,9 @@ -use super::{parse_pattern, Software}; -use crate::pattern::*; +use super::*; #[test] fn parses_oxs_v1_0_pattern() { - let pathbuf = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/patterns/piggies.oxs"); - let pattern = parse_pattern(pathbuf.as_path(), Software::Ursa); - assert!(pattern.is_ok()); - - let pattern = pattern.unwrap(); + let file_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/patterns/piggies.oxs"); + let pattern = parse_pattern(file_path, Software::Ursa).unwrap().pattern; assert_eq!(pattern.properties, PatternProperties { width: 69, height: 73 }); @@ -16,6 +12,7 @@ fn parses_oxs_v1_0_pattern() { PatternInfo { title: String::from(""), author: String::from(""), + company: String::from(""), copyright: String::from("by Ursa Software"), description: String::from(""), } @@ -30,6 +27,8 @@ fn parses_oxs_v1_0_pattern() { name: String::from("Turquoise VY DK"), color: String::from("23725C"), blends: None, + bead: None, + strands: StitchStrands::default() } ); assert_eq!( @@ -40,13 +39,15 @@ fn parses_oxs_v1_0_pattern() { name: String::from("Pistachio Green dark"), color: String::from("406647"), blends: None, + bead: None, + strands: StitchStrands::default() } ); assert_eq!( pattern.fabric, Fabric { - stitches_per_inch: (14, 14), + spi: (14, 14), kind: String::from("Aida"), name: String::from("cloth"), color: String::from("FFFFFF"), diff --git a/src-tauri/src/parser/xsd.rs b/src-tauri/src/parser/xsd.rs deleted file mode 100644 index 9aa2f33..0000000 --- a/src-tauri/src/parser/xsd.rs +++ /dev/null @@ -1,702 +0,0 @@ -//! A parser for the proprietary XSD pattern format. -//! -//! The specification of this format was obtained by reverse engineering several applications, including Pattern Maker. -//! Therefore, it is rather incomplete, but it contains all the knowledge to be able to extract enough data to display the pattern. - -use std::{ - ffi::CStr, - fs, - io::{self, Read, Seek, SeekFrom}, - path::Path, - sync::LazyLock, -}; - -use anyhow::Result; -use byteorder::{LittleEndian, ReadBytesExt}; -use memchr::memchr; -use ordered_float::NotNan; - -use crate::pattern::*; - -#[cfg(test)] -#[path = "xsd.test.rs"] -mod tests; - -static PM_FLOSS_BRANDS: LazyLock> = LazyLock::new(|| { - let pm_floss_brands = include_str!("./pm_floss_brands.json"); - serde_json::from_str(pm_floss_brands).unwrap() -}); - -const XSD_VALID_SIGNATURE: u16 = 1296; -const XSD_COLOR_NUMBER_LENGTH: usize = 11; -const XSD_COLOR_NAME_LENGTH: usize = 41; -const XSD_PATTERN_NAME_LENGTH: usize = 41; -const XSD_AUTHOR_NAME_LENGTH: usize = 41; -const XSD_COPYRIGHT_LENGTH: usize = 201; -const XSD_PATTERN_NOTES_LENGTH: usize = 2049; -const XSD_FABRIC_COLOR_NAME_LENGTH: usize = 41; -const XSD_FABRIC_KIND_LENGTH: usize = 41; -const XSD_FORMAT_LENGTH: usize = 240; -const XSD_BLEND_COLORS_NUMBER: usize = 4; -const XSD_STITCH_TYPES_NUMBER: usize = 9; -const XSD_SPECIAL_STITCH_NAME_LENGTH: usize = 256; - -#[derive(Debug, PartialEq)] -struct XsdPatternProperties { - width: u16, - height: u16, - small_stitches_count: u32, - joints_count: u16, - stitches_per_inch: (u16, u16), - palette_size: usize, -} - -#[derive(Debug, PartialEq)] -struct XsdFabric { - name: String, - color: String, -} - -#[derive(Debug, PartialEq)] -enum XsdSmallStitchKind { - HalfTop, - HalfBottom, - QuarterTopLeft, - QuarterBottomLeft, - QuarterTopRight, - QuarterBottomRight, - PetiteTopLeft, - PetiteBottomLeft, - PetiteTopRight, - PetiteBottomRight, -} - -#[derive(Debug, PartialEq)] -enum XsdJointKind { - FrenchKnot, - Back, - Curve, - Special, - Straight, - Bead, -} - -impl From for XsdJointKind { - fn from(value: u16) -> Self { - match value { - 1 => XsdJointKind::FrenchKnot, - 2 => XsdJointKind::Back, - 3 => XsdJointKind::Curve, - 4 => XsdJointKind::Special, - 5 => XsdJointKind::Straight, - 6 => XsdJointKind::Bead, - _ => { - log::warn!("Unknown joint kind {}", value); - panic!("An unknown type of XsdJointKind was encountered.") - } - } - } -} - -// (significant_byte_index, bitand_arg, palindex_index, kind) -type SmallStitchData = (usize, u8, usize, XsdSmallStitchKind); - -// The next two arrays are used only in the `map_stitches_data_into_stitches` function -// to map values from bytes buffer into stitches. -const PART_STITCH_DATA: [SmallStitchData; 6] = [ - (0, 1, 2, XsdSmallStitchKind::HalfTop), - (0, 2, 3, XsdSmallStitchKind::HalfBottom), - (0, 4, 4, XsdSmallStitchKind::QuarterTopLeft), - (0, 8, 5, XsdSmallStitchKind::QuarterBottomLeft), - (0, 16, 6, XsdSmallStitchKind::QuarterTopRight), - (0, 32, 7, XsdSmallStitchKind::QuarterBottomRight), -]; -const PETITE_STITCH_DATA: [SmallStitchData; 4] = [ - (1, 1, 4, XsdSmallStitchKind::PetiteTopLeft), - (1, 2, 5, XsdSmallStitchKind::PetiteBottomLeft), - (1, 4, 6, XsdSmallStitchKind::PetiteTopRight), - (1, 8, 7, XsdSmallStitchKind::PetiteBottomRight), -]; - -type XsdRandomNumbers = [i32; 4]; -type SmallStitchBuffer = [u8; 10]; -type XsdDecodingNumbers = [u32; 16]; - -type Cursor = io::Cursor>; - -/// Provides additional methods for reading XSD data. -trait XsdRead: Read + Seek { - /// Reads a C-style string with a specified length. - /// The string can be in UTF-8 or CP1251 encoding. - fn read_cstring(&mut self, length: usize) -> Result; - - /// Reads a hex color as `String`. - fn read_hex_color(&mut self) -> Result; - - /// Reads node and line coordinates. - fn read_fractional_coors(&mut self) -> Result<(Coord, Coord)>; -} - -impl XsdRead for Cursor { - fn read_cstring(&mut self, length: usize) -> Result { - let mut buf = vec![0; length]; - self.read_exact(&mut buf)?; - - // It is an edge case when the string is full of trash data. - if memchr(0, &buf).is_none() { - return Ok(String::from("")); - } - - // It is safe to unwrap because we have checked the presence of the null terminator. - let cstr = CStr::from_bytes_until_nul(&buf).unwrap(); - let string = match cstr.to_str() { - // The string is in UTF-8 (English). - Ok(str) => String::from(str), - - // The string is in CP1251 (Russian). - Err(_) => encoding_rs::WINDOWS_1251.decode(cstr.to_bytes()).0.to_string(), - }; - - Ok(string) - } - - fn read_hex_color(&mut self) -> Result { - let mut buf: [u8; 3] = [0; 3]; - self.read_exact(&mut buf)?; - Ok(hex::encode_upper(buf)) - } - - fn read_fractional_coors(&mut self) -> Result<(Coord, Coord)> { - // The resolution of coordinates is 1/2 of a pattern cell. - let x = NotNan::new(self.read_u16::()? as f32)? / 2.0; - let y = NotNan::new(self.read_u16::()? as f32)? / 2.0; - Ok((x, y)) - } -} - -// TODO: Implement the rest of the parser. -pub fn parse_pattern(path: impl AsRef) -> Result { - log::info!("Parsing the XSD pattern file"); - let buf = fs::read(path)?; - let mut cursor = io::Cursor::new(buf); - - let signature = read_signature(&mut cursor)?; - if signature != XSD_VALID_SIGNATURE { - log::error!("The file has an invalid signature {:?}", signature); - anyhow::bail!("The signature of Pattern Maker v4 is incorrect"); - } - - cursor.seek(SeekFrom::Current(739))?; // Skip the unknown data. - let xsd_pattern_properties = read_pattern_properties(&mut cursor)?; - let palette_size = xsd_pattern_properties.palette_size; - let palette = read_palette(&mut cursor, palette_size)?; - cursor.seek(SeekFrom::Current((palette_size * 2) as i64))?; // Skip the palette item positions. - skip_palette_items_notes(&mut cursor, palette_size)?; - cursor.seek(SeekFrom::Current((palette_size * 16) as i64))?; // Skip the strands. - cursor.seek(SeekFrom::Current((XSD_FORMAT_LENGTH * 10) as i64))?; // Skip the symbol formats. - cursor.seek(SeekFrom::Current((XSD_FORMAT_LENGTH * 10) as i64))?; // Skip the back stitch formats. - cursor.seek(SeekFrom::Current((XSD_FORMAT_LENGTH * 4) as i64))?; // Skip the unknown formats. - cursor.seek(SeekFrom::Current((XSD_FORMAT_LENGTH * 10) as i64))?; // Skip the special stitch formats. - cursor.seek(SeekFrom::Current((XSD_FORMAT_LENGTH * 10) as i64))?; // Skip the straight stitch formats. - cursor.seek(SeekFrom::Current((XSD_FORMAT_LENGTH * 10) as i64))?; // Skip the french knot formats. - cursor.seek(SeekFrom::Current((XSD_FORMAT_LENGTH * 10) as i64))?; // Skip the bead formats. - cursor.seek(SeekFrom::Current((XSD_FORMAT_LENGTH * 53) as i64))?; // Skip the font formats. - cursor.seek(SeekFrom::Current((palette_size * 12) as i64))?; // Skip the palette item symbols. - cursor.seek(SeekFrom::Current(380))?; // Skip the pattern settings. - cursor.seek(SeekFrom::Current(56))?; // Skip the grid settings. - let xsd_fabric = read_fabric_info(&mut cursor)?; - let (pattern_info, fabric_kind) = read_pattern_info(&mut cursor)?; - cursor.seek(SeekFrom::Current(52))?; // Skip the stitch settings. - cursor.seek(SeekFrom::Current(30))?; // Skip the symbol settings. - cursor.seek(SeekFrom::Current(16414))?; // Skip the library info. - cursor.seek(SeekFrom::Current(512))?; // Skip the machine export info. - let (fullstitches, partstitches) = read_stitches(&mut cursor, &xsd_pattern_properties)?; - skip_special_stitch_models(&mut cursor)?; - let (nodes, lines) = read_joints(&mut cursor, xsd_pattern_properties.joints_count)?; - - Ok(Pattern { - properties: PatternProperties { - width: xsd_pattern_properties.width, - height: xsd_pattern_properties.height, - }, - info: pattern_info, - palette, - fabric: Fabric { - kind: fabric_kind, - name: xsd_fabric.name, - color: xsd_fabric.color, - stitches_per_inch: xsd_pattern_properties.stitches_per_inch, - }, - fullstitches: Stitches::from_iter(fullstitches), - partstitches: Stitches::from_iter(partstitches), - nodes: Stitches::from_iter(nodes), - lines: Stitches::from_iter(lines), - }) -} - -/// Reads the signature of the XSD file. -/// This function is potentially underdeveloped due to lack of knowledge about the XSD format. -fn read_signature(cursor: &mut Cursor) -> Result { - let signature = cursor.read_u16::()?; - Ok(signature) -} - -/// Reads the pattern properties that are necessarry for further parsing. -fn read_pattern_properties(cursor: &mut Cursor) -> Result { - log::trace!("Reading the pattern properties"); - let width = cursor.read_u16::()?; - let height = cursor.read_u16::()?; - let small_stitches_count = cursor.read_u32::()?; - let joints_count = cursor.read_u16::()?; - let stitches_per_inch = (cursor.read_u16::()?, cursor.read_u16::()?); - cursor.seek(SeekFrom::Current(6))?; - let palette_size: usize = cursor.read_u16::()?.into(); - Ok(XsdPatternProperties { - width, - height, - small_stitches_count, - joints_count, - stitches_per_inch, - palette_size, - }) -} - -/// Reads the color palette of the pattern. -fn read_palette(cursor: &mut Cursor, palette_size: usize) -> Result> { - log::trace!("Reading the palette with {} items", palette_size); - let mut palette = Vec::with_capacity(palette_size); - for _ in 0..palette_size { - palette.push(read_palette_item(cursor)?); - } - Ok(palette) -} - -/// Reads a single palette item. -fn read_palette_item(cursor: &mut Cursor) -> Result { - cursor.seek(SeekFrom::Current(2))?; - let brand_id = cursor.read_u8()?; - let brand = PM_FLOSS_BRANDS.get(&brand_id).unwrap().to_owned(); - let number = cursor.read_cstring(XSD_COLOR_NUMBER_LENGTH)?; - let name = cursor.read_cstring(XSD_COLOR_NAME_LENGTH)?; - let color = cursor.read_hex_color()?; - cursor.seek(SeekFrom::Current(1))?; - let blends = read_blends(cursor)?; - cursor.seek(SeekFrom::Current(10))?; - Ok(PaletteItem { - brand, - name, - number, - color, - blends, - }) -} - -/// Reads the blend colors of the palette item. -/// Used only in the `read_palette_item` function. -fn read_blends(cursor: &mut Cursor) -> Result>> { - let blends_count: usize = cursor.read_u16::()?.into(); - if blends_count == 0 { - cursor.seek(SeekFrom::Current(4 * 12 + 4))?; - return Ok(None); - } - let mut blends: Vec = Vec::with_capacity(blends_count); - for i in 0..XSD_BLEND_COLORS_NUMBER { - let blend_color = read_blend_item(cursor)?; - // PM reserves and stores 4 blend colors, but we do not want to store empty blends. - // Although, we must read all blend colors to keep the cursor in the right position. - if i < blends_count { - blends.push(blend_color); - } - } - read_blend_strands(cursor, &mut blends)?; - Ok(Some(blends)) -} - -/// Reads a single blend color. -/// Used only in the `read_blends` function. -fn read_blend_item(cursor: &mut Cursor) -> Result { - let brand_id = cursor.read_u8()?; - let brand_id = if brand_id == 255 { 0 } else { brand_id }; - Ok(Blend { - brand: PM_FLOSS_BRANDS.get(&brand_id).unwrap().to_owned(), - number: cursor.read_cstring(XSD_COLOR_NUMBER_LENGTH)?, - strands: 0, // The actual value will be set when calling `read_blend_strands`. - }) -} - -/// Reads the number of strands for each blend color. -/// Used only in the `read_blends` function. -/// The function modifies the `blends` vector in place. -fn read_blend_strands(cursor: &mut Cursor, blends: &mut [Blend]) -> Result<()> { - for i in 0..XSD_BLEND_COLORS_NUMBER { - let strands = cursor.read_u8()?; - if let Some(blend_color) = blends.get_mut(i) { - blend_color.strands = strands; - } - } - Ok(()) -} - -/// Skips the notes of the palette items. -fn skip_palette_items_notes(cursor: &mut Cursor, palette_size: usize) -> Result<()> { - for _ in 0..palette_size { - for _ in 0..XSD_STITCH_TYPES_NUMBER { - let note_length = cursor.read_u16::()?; - cursor.seek(SeekFrom::Current(note_length.into()))?; - } - } - Ok(()) -} - -/// Reads a part of the fabric information. -fn read_fabric_info(cursor: &mut Cursor) -> Result { - log::trace!("Reading the fabric info"); - let fabric_info = XsdFabric { - name: cursor.read_cstring(XSD_FABRIC_COLOR_NAME_LENGTH)?, - color: cursor.read_hex_color()?, - }; - cursor.seek(SeekFrom::Current(65))?; - Ok(fabric_info) -} - -/// Reads the necessarry pattern information. -fn read_pattern_info(cursor: &mut Cursor) -> Result<(PatternInfo, String)> { - log::trace!("Reading the pattern info"); - let title = cursor.read_cstring(XSD_PATTERN_NAME_LENGTH)?; - let author = cursor.read_cstring(XSD_AUTHOR_NAME_LENGTH)?; - cursor.seek(SeekFrom::Current(41))?; // Skip company name. - let copyright = cursor.read_cstring(XSD_COPYRIGHT_LENGTH)?; - let description = cursor.read_cstring(XSD_PATTERN_NOTES_LENGTH)?; - let pattern_info = PatternInfo { - title, - author, - copyright, - description, - }; - cursor.seek(SeekFrom::Current(6))?; - let fabric_kind = cursor.read_cstring(XSD_FABRIC_KIND_LENGTH)?; - cursor.seek(SeekFrom::Current(206))?; - Ok((pattern_info, fabric_kind)) -} - -/// Reads the stitches of the pattern. -fn read_stitches( - cursor: &mut Cursor, - xsd_pattern_properties: &XsdPatternProperties, -) -> Result<(Vec, Vec)> { - log::trace!("Reading the stitches"); - let total_stitches_count = ((xsd_pattern_properties.width as u64) * (xsd_pattern_properties.height as u64)) as usize; - let small_stitches_count = xsd_pattern_properties.small_stitches_count as usize; - let stitches_data = read_stitches_data(cursor, total_stitches_count)?; - let small_stitch_buffers = read_small_stitch_buffers(cursor, small_stitches_count)?; - let stitches = map_stitches_data_into_stitches( - stitches_data, - small_stitch_buffers, - xsd_pattern_properties.width as usize, - )?; - Ok(stitches) -} - -/// Reads the bytes buffer that contains the decoded stitches data. -fn read_stitches_data(cursor: &mut Cursor, total_stitches_count: usize) -> Result> { - log::trace!("Reading the stitches data"); - let mut stitches_data = Vec::with_capacity(total_stitches_count); - let mut xsd_random_numbers = read_xsd_random_numbers(cursor)?; - let (mut decoding_key, decoding_numbers) = reproduce_decoding_values(&xsd_random_numbers)?; - let mut decoding_number_index = 0; - let mut stitch_index = 0; - - while stitch_index < total_stitches_count { - let stitches_data_length = cursor.read_u32::()? as usize; - - if stitches_data_length == 0 { - continue; - } - - let mut decoded_stitches_data = Vec::with_capacity(stitches_data_length); - - // Decoding. - for _ in 0..stitches_data_length { - let stitch_data = cursor.read_i32::()? ^ decoding_key ^ xsd_random_numbers[0]; - decoded_stitches_data.push(stitch_data); - decoding_key = decoding_key.rotate_left(decoding_numbers[decoding_number_index]); - xsd_random_numbers[0] = xsd_random_numbers[0].wrapping_add(xsd_random_numbers[1]); - decoding_number_index = (decoding_number_index + 1) % 16; - } - - // Copying. - let mut stitch_data_index = 0; - while stitch_data_index < stitches_data_length { - let mut copy_count = 1; - let elem = decoded_stitches_data[stitch_data_index]; - - if elem & (i32::MAX / 2 + 1) != 0 { - copy_count = (elem & (i32::MAX / 2)) >> 16; - stitch_data_index += 1; - } - - while copy_count > 0 { - stitches_data.push(decoded_stitches_data[stitch_data_index]); - stitch_index += 1; - copy_count -= 1; - } - - stitch_data_index += 1; - } - } - - Ok(stitches_data) -} - -/// Reads the random numbers that are necessarry for decoding the stitches data. -fn read_xsd_random_numbers(cursor: &mut Cursor) -> Result { - log::trace!("Reading the XSD random numbers"); - let mut xsd_random_numbers = [0; 4]; - for number in &mut xsd_random_numbers { - *number = cursor.read_i32::()?; - } - Ok(xsd_random_numbers) -} - -/// Reproduces the decoding values that are used for decoding the stitches data. -fn reproduce_decoding_values(xsd_random_numbers: &XsdRandomNumbers) -> Result<(i32, XsdDecodingNumbers)> { - log::trace!("Reproducing the decoding values"); - let val1 = xsd_random_numbers[1].to_le_bytes()[1] as i32; - let val2 = xsd_random_numbers[0] << 8; - let val3 = (val2 | val1) << 8; - let val4 = xsd_random_numbers[2].to_le_bytes()[2] as i32; - let val5 = (val4 | val3) << 8; - let val6 = xsd_random_numbers[3] & 0xFF; - let decoding_key = val6 | val5; - - let mut decoding_buffer = [0; 16]; - - for i in 0..4 { - let buf = xsd_random_numbers[i].to_le_bytes(); - for j in 0..4 { - decoding_buffer[i * 4 + j] = buf[j]; - } - } - - let mut decoding_buffer = io::Cursor::new(decoding_buffer); - let mut decoding_numbers: XsdDecodingNumbers = [0; 16]; - - for i in 0..16 { - let offset = (i / 4) * 4; // 0, 4, 8, 12. - decoding_buffer.seek(SeekFrom::Start(offset))?; - let shift = decoding_buffer.read_u32::()? >> (i % 4); - decoding_numbers[i as usize] = shift % 32; - } - - Ok((decoding_key, decoding_numbers)) -} - -/// Reads the small stitch buffers that are used containe the small stitches data. -fn read_small_stitch_buffers(cursor: &mut Cursor, small_stitches_count: usize) -> Result> { - log::trace!("Reading the small stitch buffers"); - let mut small_stitch_buffers = Vec::with_capacity(small_stitches_count); - for _ in 0..small_stitches_count { - let mut buf = [0; 10]; - cursor.read_exact(&mut buf)?; - small_stitch_buffers.push(buf); - } - Ok(small_stitch_buffers) -} - -/// Maps the stitches data into the full- and partstitches . -fn map_stitches_data_into_stitches( - stitches_data: Vec, - small_stitch_buffers: Vec, - pattern_width: usize, -) -> Result<(Vec, Vec)> { - let mut fullstitches = Vec::new(); - let mut partstitches = Vec::new(); - - log::trace!("Mapping the stitches data into stitches"); - for (i, stitch_data) in stitches_data.iter().enumerate() { - let stitch_buffer = stitch_data.to_le_bytes(); - - // Empty cell. - if stitch_buffer[3] == 15 { - continue; - } - - let x = NotNan::new((i % pattern_width) as f32)?; - let y = NotNan::new((i / pattern_width) as f32)?; - - if stitch_buffer[3] == 0 { - fullstitches.push(FullStitch { - x, - y, - palindex: stitch_buffer[2], - kind: FullStitchKind::Full, - }); - continue; - } - - let position = (stitches_data[i] >> 16) & ((u16::MAX / 2) as i32); - let small_stitch_buffer = small_stitch_buffers.get(position as usize).unwrap(); - - for (significant_byte_index, bitand_arg, palindex_index, kind) in PETITE_STITCH_DATA { - let (x, y) = adjust_small_stitch_coors(x, y, &kind); - if small_stitch_buffer[significant_byte_index] & bitand_arg != 0 { - fullstitches.push(FullStitch { - x, - y, - palindex: small_stitch_buffer[palindex_index], - kind: FullStitchKind::Petite, - }) - } - } - - for (significant_byte_index, bitand_arg, palindex_index, kind) in PART_STITCH_DATA { - if small_stitch_buffer[significant_byte_index] & bitand_arg != 0 { - let (x, y) = adjust_small_stitch_coors(x, y, &kind); - let direction = match kind { - XsdSmallStitchKind::HalfTop | XsdSmallStitchKind::QuarterTopLeft | XsdSmallStitchKind::QuarterBottomRight => { - PartStitchDirection::Backward - } - _ => PartStitchDirection::Forward, - }; - let kind = match kind { - XsdSmallStitchKind::HalfTop | XsdSmallStitchKind::HalfBottom => PartStitchKind::Half, - _ => PartStitchKind::Quarter, - }; - partstitches.push(PartStitch { - x, - y, - palindex: small_stitch_buffer[palindex_index], - direction, - kind, - }) - } - } - } - - Ok((fullstitches, partstitches)) -} - -/// Calculates the coordinates of the small stitch. -/// The XSD format contains coordinates without additional offsets relative to the cell. -/// But this is important for us. -fn adjust_small_stitch_coors(x: Coord, y: Coord, kind: &XsdSmallStitchKind) -> (Coord, Coord) { - match kind { - XsdSmallStitchKind::QuarterTopLeft | XsdSmallStitchKind::PetiteTopLeft => (x, y), - XsdSmallStitchKind::QuarterTopRight | XsdSmallStitchKind::PetiteTopRight => (x + 0.5, y), - XsdSmallStitchKind::QuarterBottomLeft | XsdSmallStitchKind::PetiteBottomLeft => (x, y + 0.5), - XsdSmallStitchKind::QuarterBottomRight | XsdSmallStitchKind::PetiteBottomRight => (x + 0.5, y + 0.5), - _ => (x, y), - } -} - -/// Skips the special stitch models. -fn skip_special_stitch_models(cursor: &mut Cursor) -> Result<()> { - cursor.seek(SeekFrom::Current(2))?; - let special_stith_models_count: usize = cursor.read_u16::()?.into(); - - for _ in 0..special_stith_models_count { - if cursor.read_u16::()? != 4 { - continue; - } - - cursor.seek(SeekFrom::Current(2))?; - let mut special_stitch_kind_buf = vec![0; 4]; - cursor.read_exact(&mut special_stitch_kind_buf)?; - - if String::from_utf8(special_stitch_kind_buf)? != "sps1" { - continue; - } - - cursor.seek(SeekFrom::Current((XSD_SPECIAL_STITCH_NAME_LENGTH * 2 + 2) as i64))?; - - for _ in 0..3 { - cursor.seek(SeekFrom::Current(10))?; - - if read_signature(cursor)? != XSD_VALID_SIGNATURE { - break; - } - - let joints_count = cursor.read_u16::()?; - - if joints_count == 0 { - continue; - } - - read_joints(cursor, joints_count)?; - } - } - - Ok(()) -} - -/// Reads the french knots, beads, back, straight and special stitches and curves that used in the pattern. -fn read_joints(cursor: &mut Cursor, joints_count: u16) -> Result<(Vec, Vec)> { - let mut nodes = Vec::new(); - let mut lines = Vec::new(); - - log::trace!("Reading the joints"); - for _ in 0..joints_count { - let joint_kind = XsdJointKind::from(cursor.read_u16::()?); - match joint_kind { - XsdJointKind::FrenchKnot => { - cursor.seek(SeekFrom::Current(2))?; - let (x, y) = cursor.read_fractional_coors()?; - cursor.seek(SeekFrom::Current(4))?; - let palindex = cursor.read_u8()?; - cursor.seek(SeekFrom::Current(1))?; - nodes.push(Node { - x, - y, - rotated: false, - palindex, - kind: NodeKind::FrenchKnot, - }); - } - - XsdJointKind::Back | XsdJointKind::Straight => { - cursor.seek(SeekFrom::Current(2))?; - let (x1, y1) = cursor.read_fractional_coors()?; - let (x2, y2) = cursor.read_fractional_coors()?; - let palindex = cursor.read_u8()?; - cursor.seek(SeekFrom::Current(1))?; - let kind = if joint_kind == XsdJointKind::Back { - LineKind::Back - } else { - LineKind::Straight - }; - lines.push(Line { - x: (x1, x2), - y: (y1, y2), - palindex, - kind, - }); - } - - XsdJointKind::Curve => { - cursor.seek(SeekFrom::Current(3))?; - let points_count: usize = cursor.read_u16::()?.into(); - cursor.seek(SeekFrom::Current((points_count * 4) as i64))?; - } - - XsdJointKind::Special => { - cursor.seek(SeekFrom::Current(23))?; - } - - XsdJointKind::Bead => { - cursor.seek(SeekFrom::Current(2))?; - let (x, y) = cursor.read_fractional_coors()?; - let palindex = cursor.read_u8()?; - cursor.seek(SeekFrom::Current(1))?; - let rotated = matches!(cursor.read_u16::()?, 90 | 270); - nodes.push(Node { - x, - y, - rotated, - palindex, - kind: NodeKind::Bead, - }); - } - } - } - Ok((nodes, lines)) -} diff --git a/src-tauri/src/parser/xsd/mod.rs b/src-tauri/src/parser/xsd/mod.rs new file mode 100644 index 0000000..0fda4fc --- /dev/null +++ b/src-tauri/src/parser/xsd/mod.rs @@ -0,0 +1,5 @@ +mod read; + +#[allow(clippy::module_inception)] +mod xsd; +pub use xsd::parse_pattern; diff --git a/src-tauri/src/parser/pm_floss_brands.json b/src-tauri/src/parser/xsd/pmaker_floss_brands.json similarity index 100% rename from src-tauri/src/parser/pm_floss_brands.json rename to src-tauri/src/parser/xsd/pmaker_floss_brands.json diff --git a/src-tauri/src/parser/xsd/read.rs b/src-tauri/src/parser/xsd/read.rs new file mode 100644 index 0000000..f258f9b --- /dev/null +++ b/src-tauri/src/parser/xsd/read.rs @@ -0,0 +1,45 @@ +use std::io::{Read, Result}; + +use byteorder::ReadBytesExt; +use memchr::memchr; + +#[cfg(test)] +#[path = "read.test.rs"] +mod tests; + +/// Provides additional methods for reading XSD data. +pub trait ReadXsdExt: Read + ReadBytesExt { + /// Reads a C-style string with a specified length. + /// The string can be in UTF-8 or CP1251 encoding. + fn read_cstring(&mut self, length: usize) -> Result { + let mut buf = vec![0; length + 1]; // +1 for the null terminator. + self.read_exact(&mut buf)?; + + // It is an edge case when the string is full of trash data. + if memchr(0, &buf).is_none() { + return Ok(String::from("")); + } + + // It is safe to unwrap because we have checked the presence of the null terminator. + let cstr = std::ffi::CStr::from_bytes_until_nul(&buf).unwrap(); + let string = match cstr.to_str() { + // The string is in UTF-8 (English). + Ok(str) => String::from(str), + + // The string is in CP1251 (Russian). + Err(_) => encoding_rs::WINDOWS_1251.decode(cstr.to_bytes()).0.to_string(), + }; + + Ok(string) + } + + /// Reads a hex color as `String`. + fn read_hex_color(&mut self) -> Result { + let mut buf: [u8; 3] = [0; 3]; + self.read_exact(&mut buf)?; + Ok(hex::encode_upper(buf)) + } +} + +/// All types that implement `Read` get methods defined in `ReadXsdExt`. +impl ReadXsdExt for R {} diff --git a/src-tauri/src/parser/xsd/read.test.rs b/src-tauri/src/parser/xsd/read.test.rs new file mode 100644 index 0000000..7398f3b --- /dev/null +++ b/src-tauri/src/parser/xsd/read.test.rs @@ -0,0 +1,39 @@ +use std::io::Cursor; + +use super::*; + +#[test] +fn reads_cstring() { + let utf8_buf = vec![0x57, 0x68, 0x69, 0x74, 0x65, 0x00, 0x00, 0x00]; + assert_eq!(Cursor::new(utf8_buf).read_cstring(7).unwrap(), String::from("White")); + + let cp1251_buf = vec![0xE3, 0xEE, 0xEB, 0xF3, 0xE1, 0xEE, 0xE9, 0x00]; + assert_eq!( + Cursor::new(cp1251_buf).read_cstring(7).unwrap(), + String::from("голубой") + ); +} + +#[test] +fn returns_empty_string_on_non_null_terminated_cstring() { + let not_nul_terminated_buf = vec![0x43, 0x6F, 0x66, 0x66, 0x65, 0x65]; + assert_eq!( + Cursor::new(not_nul_terminated_buf).read_cstring(5).unwrap(), + String::from("") + ); +} + +#[test] +fn reads_hex_color() { + let black_color_buf = vec![0x00, 0x00, 0x00]; + assert_eq!( + Cursor::new(black_color_buf).read_hex_color().unwrap(), + String::from("000000") + ); + + let white_color_buf = vec![0xff, 0xff, 0xff]; + assert_eq!( + Cursor::new(white_color_buf).read_hex_color().unwrap(), + String::from("FFFFFF") + ); +} diff --git a/src-tauri/src/parser/xsd/xsd.rs b/src-tauri/src/parser/xsd/xsd.rs new file mode 100644 index 0000000..9ce9c74 --- /dev/null +++ b/src-tauri/src/parser/xsd/xsd.rs @@ -0,0 +1,1023 @@ +//! A parser for the proprietary XSD pattern format. +//! +//! The specification of this format was obtained by reverse engineering several applications, including Pattern Maker. +//! Therefore, it is rather incomplete, but it contains all the knowledge to be able to extract enough data to display the pattern. + +use std::{ + io::{self, Read, Seek, SeekFrom}, + sync::LazyLock, +}; + +use anyhow::Result; +use byteorder::{LittleEndian, ReadBytesExt}; +use ordered_float::NotNan; + +use super::read::ReadXsdExt; +use crate::pattern::{display::*, print::*, *}; + +#[cfg(test)] +#[path = "xsd.test.rs"] +mod tests; + +static PM_FLOSS_BRANDS: LazyLock> = LazyLock::new(|| { + let pm_floss_brands = include_str!("./pmaker_floss_brands.json"); + serde_json::from_str(pm_floss_brands).expect("Failed to parse the PM floss brands") +}); + +const VALID_SIGNATURE: u16 = 0x0510; + +const COLOR_NUMBER_LENGTH: usize = 10; +const COLOR_NAME_LENGTH: usize = 40; +/// Pattern Maker limits blends up to 4 colors. The minimum is 2 if they are present. +const BLEND_COLORS_NUMBER: usize = 4; + +const PATTERN_NAME_LENGTH: usize = 40; +const AUTHOR_NAME_LENGTH: usize = 40; +const COMPANY_NAME_LENGTH: usize = 40; +const COPYRIGHT_LENGTH: usize = 200; +const PATTERN_NOTES_LENGTH: usize = 2048; + +const FABRIC_COLOR_NAME_LENGTH: usize = 40; +const FABRIC_KIND_NAME_LENGTH: usize = 40; + +const FONT_NAME_LENGTH: usize = 32; + +/// It is the maximum size of the palette. +const FORMAT_LENGTH: usize = 240; + +/// Full, half, quarter, back, french knot, straight, special, petite, bead. +const STITCH_TYPES_NUMBER: usize = 9; + +const PAGE_HEADER_AND_FOOTER_LENGTH: usize = 119; + +const SPECIAL_STITCH_NAME_LENGTH: usize = 255; + +pub fn parse_pattern(file_path: std::path::PathBuf) -> Result { + log::info!("Parsing the XSD pattern file"); + let buf = std::fs::read(&file_path)?; + let mut cursor = std::io::Cursor::new(buf); + + let signature = read_signature(&mut cursor)?; + if signature != VALID_SIGNATURE { + log::error!("The file has an invalid signature. Expected {VALID_SIGNATURE:#06X}, but got {signature:#06X}"); + anyhow::bail!("The signature of Pattern Maker v4 is incorrect"); + } + + cursor.seek_relative(739)?; // Skip the unknown data. + + let pattern_properties = PatternProperties { + width: cursor.read_u16::()?, + height: cursor.read_u16::()?, + }; + + let coord_factor = pattern_properties.width as usize; + let total_stitches_count = (pattern_properties.width as usize) * (pattern_properties.height as usize); + let small_stitches_count = cursor.read_u32::()? as usize; + let joints_count = cursor.read_u16::()?; + + let spi = (cursor.read_u16::()?, cursor.read_u16::()?); + cursor.seek_relative(6)?; + + let palette = read_palette(&mut cursor)?; + let formats = read_formats(&mut cursor, palette.len())?; + let symbols = read_symbols(&mut cursor, palette.len())?; + + let pattern_settings = read_pattern_settings(&mut cursor)?; + let grid = read_grid_settings(&mut cursor)?; + + let fabric_color_name = cursor.read_cstring(FABRIC_COLOR_NAME_LENGTH)?; + let fabric_color = cursor.read_hex_color()?; + cursor.seek_relative(65)?; + let pattern_info = read_pattern_info(&mut cursor)?; + cursor.seek_relative(6)?; + let fabric_kind_name = cursor.read_cstring(FABRIC_KIND_NAME_LENGTH)?; + cursor.seek_relative(206)?; + + let (stitch_settings, outlined_stitches, stitch_outline) = read_stitch_settings(&mut cursor)?; + let symbol_settings = read_symbol_settings(&mut cursor)?; + + cursor.seek_relative(16412)?; // Skip library info. + cursor.seek_relative(512)?; // Skip machine export info. + + let (fullstitches, partstitches) = + read_stitches(&mut cursor, coord_factor, total_stitches_count, small_stitches_count)?; + + let special_stitch_models = read_special_stitch_models(&mut cursor)?; + + let (nodes, lines, _curves, specialstitches) = read_joints(&mut cursor, joints_count)?; + + Ok(PatternProject { + file_path: Some(file_path), + pattern: Pattern { + properties: pattern_properties, + info: pattern_info, + palette, + fabric: Fabric { + kind: fabric_kind_name, + name: fabric_color_name, + color: fabric_color, + spi, + }, + fullstitches: Stitches::from_iter(fullstitches), + partstitches: Stitches::from_iter(partstitches), + nodes: Stitches::from_iter(nodes), + lines: Stitches::from_iter(lines), + specialstitches: Stitches::from_iter(specialstitches), + special_stitch_models, + }, + display_settings: DisplaySettings { + default_stitch_font: pattern_settings.stitch_font_name, + symbols, + symbol_settings, + formats, + grid, + view: pattern_settings.view, + zoom: pattern_settings.zoom, + show_grid: pattern_settings.show_grid, + show_rulers: pattern_settings.show_rulers, + show_centering_marks: pattern_settings.show_centering_marks, + show_fabric_colors_with_symbols: pattern_settings.show_fabric_colors_with_symbols, + gaps_between_stitches: pattern_settings.gaps_between_stitches, + outlined_stitches, + stitch_outline, + stitch_settings, + }, + print_settings: PrintSettings { + font: pattern_settings.font, + header: pattern_settings.page_header, + footer: pattern_settings.page_footer, + margins: pattern_settings.page_margins, + show_page_numbers: pattern_settings.show_page_numbers, + show_adjacent_page_numbers: pattern_settings.show_adjacent_page_numbers, + center_chart_on_pages: pattern_settings.center_chart_on_pages, + }, + }) +} + +/// Reads the signature of the XSD file. +fn read_signature(reader: &mut R) -> Result { + let signature = reader.read_u16::()?; + Ok(signature) +} + +/// Reads the color palette of the pattern. +fn read_palette(reader: &mut R) -> Result> { + log::trace!("Reading palette"); + let palette_size: usize = reader.read_u16::()?.into(); + let mut palette = Vec::with_capacity(palette_size); + + for _ in 0..palette_size { + palette.push(read_palette_item(reader)?); + } + + reader.seek_relative((palette_size * 2) as i64)?; // Skip palette item's position. + skip_palette_items_notes(reader, palette_size)?; + + for pi in palette.iter_mut() { + pi.strands = read_palette_item_strands(reader)?; + } + + Ok(palette) +} + +/// Reads a single palette item. +fn read_palette_item(reader: &mut R) -> Result { + /// Reads the blend colors of the palette item. + fn read_blends(reader: &mut R) -> Result>> { + let blends_count: usize = reader.read_u16::()?.into(); + let mut blends: Vec = Vec::with_capacity(blends_count); + + // Read blends. + for _ in 0..blends_count { + let brand_id = reader.read_u8()?; + let brand_id = if brand_id == 255 { 0 } else { brand_id }; + blends.push(Blend { + brand: PM_FLOSS_BRANDS.get(&brand_id).unwrap().to_owned(), + number: reader.read_cstring(COLOR_NUMBER_LENGTH)?, + strands: 0, // The actual value will be set when calling `read_blend_strands`. + }); + } + reader.seek_relative(((BLEND_COLORS_NUMBER - blends_count) * 12) as i64)?; // Skip empty blends. + + // Read blend's strands. + for blend in blends.iter_mut() { + blend.strands = reader.read_u8()?; + } + reader.seek_relative((BLEND_COLORS_NUMBER - blends_count) as i64)?; // Skip empty blend's strands. + + Ok(if blends.is_empty() { None } else { Some(blends) }) + } + + reader.seek_relative(2)?; + let brand_id = reader.read_u8()?; + let brand = PM_FLOSS_BRANDS.get(&brand_id).unwrap().to_owned(); + let number = reader.read_cstring(COLOR_NUMBER_LENGTH)?; + let name = reader.read_cstring(COLOR_NAME_LENGTH)?; + let color = reader.read_hex_color()?; + reader.seek_relative(1)?; + let blends = read_blends(reader)?; + let is_bead = reader.read_u32::()? == 1; + let bead = if is_bead { + Some(Bead { + diameter: NotNan::new(reader.read_u16::()? as f32)? / 10.0, + length: NotNan::new(reader.read_u16::()? as f32)? / 10.0, + }) + } else { + // Prevent reading a trash data. + reader.seek_relative(4)?; + None + }; + reader.seek_relative(2)?; + + Ok(PaletteItem { + brand, + name, + number, + color, + blends, + bead, + strands: StitchStrands::default(), + }) +} + +/// Skips the notes of the palette items. +fn skip_palette_items_notes(reader: &mut R, palette_size: usize) -> Result<()> { + for _ in 0..palette_size { + for _ in 0..STITCH_TYPES_NUMBER { + let note_length = reader.read_u16::()?; + reader.seek_relative(note_length.into())?; + } + } + Ok(()) +} + +fn read_palette_item_strands(reader: &mut R) -> Result { + fn map_strands(value: u16) -> Option { + if value == 0 { + None + } else { + Some(value) + } + } + + // Order is important! + Ok(StitchStrands { + full: map_strands(reader.read_u16::()?), + half: map_strands(reader.read_u16::()?), + quarter: map_strands(reader.read_u16::()?), + back: map_strands(reader.read_u16::()?), + french_knot: map_strands(reader.read_u16::()?), + petite: map_strands(reader.read_u16::()?), + special: map_strands(reader.read_u16::()?), + straight: map_strands(reader.read_u16::()?), + }) +} + +fn read_formats(reader: &mut R, palette_size: usize) -> io::Result> { + let symbol_formats = read_symbol_formats(reader, palette_size)?; + let back_stitch_formats = read_line_formats(reader, palette_size)?; + reader.seek_relative((FORMAT_LENGTH * 4) as i64)?; // Skip unknown formats. + let special_stitch_formats = read_line_formats(reader, palette_size)?; + let straight_stitch_formats = read_line_formats(reader, palette_size)?; + let french_knot_formats = read_node_formats(reader, palette_size)?; + let bead_formats = read_node_formats(reader, palette_size)?; + let font_formats = read_font_formats(reader, palette_size)?; + + let mut formats = Vec::with_capacity(palette_size); + for i in 0..palette_size { + formats.push(Formats { + symbol: symbol_formats[i].clone(), + back: back_stitch_formats[i].clone(), + straight: straight_stitch_formats[i].clone(), + french: french_knot_formats[i].clone(), + bead: bead_formats[i].clone(), + special: special_stitch_formats[i].clone(), + font: font_formats[i].clone(), + }); + } + + Ok(formats) +} + +fn read_symbol_formats(reader: &mut R, palette_size: usize) -> io::Result> { + let mut formats = Vec::with_capacity(palette_size); + for _ in 0..palette_size { + let use_alt_bg_color = reader.read_u16::()? == 1; + let bg_color = reader.read_hex_color()?; + reader.seek_relative(1)?; + let fg_color = reader.read_hex_color()?; + reader.seek_relative(1)?; + formats.push(SymbolFormat { + use_alt_bg_color, + bg_color, + fg_color, + }); + } + reader.seek_relative(((FORMAT_LENGTH - palette_size) * 10) as i64)?; + Ok(formats) +} + +fn read_line_formats(reader: &mut R, palette_size: usize) -> io::Result> { + let mut formats = Vec::with_capacity(palette_size); + for _ in 0..palette_size { + let use_alt_color = reader.read_u16::()? == 1; + let color = reader.read_hex_color()?; + reader.seek_relative(1)?; + let style: LineStyle = reader.read_u16::()?.into(); + let thickness = NotNan::new(reader.read_u16::()? as f32)? / 10.0; + formats.push(LineFormat { + use_alt_color, + color, + style, + thickness, + }); + } + reader.seek_relative(((FORMAT_LENGTH - palette_size) * 10) as i64)?; + Ok(formats) +} + +fn read_node_formats(reader: &mut R, palette_size: usize) -> io::Result> { + let mut formats = Vec::with_capacity(palette_size); + for _ in 0..palette_size { + let use_dot_style = reader.read_u16::()? == 1; + let color = reader.read_hex_color()?; + reader.seek_relative(1)?; + let use_alt_color = reader.read_u16::()? == 1; + let diameter = NotNan::new(reader.read_u16::()? as f32)? / 10.0; + formats.push(NodeFormat { + use_dot_style, + color, + use_alt_color, + diameter, + }); + } + reader.seek_relative(((FORMAT_LENGTH - palette_size) * 10) as i64)?; + Ok(formats) +} + +fn read_font_formats(reader: &mut R, palette_size: usize) -> io::Result> { + let mut formats = Vec::with_capacity(palette_size); + for _ in 0..palette_size { + let font_name = reader.read_cstring(FONT_NAME_LENGTH)?; + let font_name = if font_name == "default" { None } else { Some(font_name) }; + reader.seek_relative(2)?; + let bold = reader.read_u16::()? == 700; + let italic = reader.read_u8()? == 1; + reader.seek_relative(11)?; + let stitch_size = reader.read_u16::()?; + let small_stitch_size = reader.read_u16::()?; + formats.push(FontFormat { + font_name, + bold, + italic, + stitch_size, + small_stitch_size, + }); + } + reader.seek_relative(((FORMAT_LENGTH - palette_size) * 53) as i64)?; + Ok(formats) +} + +fn read_symbols(reader: &mut R, palette_size: usize) -> io::Result> { + fn map_symbol(value: u16) -> Option { + if value == 0xFFFF { + None + } else { + Some(value) + } + } + + let mut symbols = Vec::with_capacity(palette_size); + for _ in 0..palette_size { + symbols.push(Symbols { + full: map_symbol(reader.read_u16::()?), + petite: map_symbol(reader.read_u16::()?), + half: map_symbol(reader.read_u16::()?), + quarter: map_symbol(reader.read_u16::()?), + french_knot: map_symbol(reader.read_u16::()?), + bead: map_symbol(reader.read_u16::()?), + }); + } + + Ok(symbols) +} + +#[derive(Debug, PartialEq)] +struct XsdPatternSettings { + stitch_font_name: String, + font: Font, + + view: View, + zoom: u16, + + show_grid: bool, + show_rulers: bool, + show_centering_marks: bool, + show_fabric_colors_with_symbols: bool, + gaps_between_stitches: bool, + + page_header: String, + page_footer: String, + page_margins: PageMargins, + show_page_numbers: bool, + show_adjacent_page_numbers: bool, + center_chart_on_pages: bool, +} + +fn read_pattern_settings(reader: &mut R) -> Result { + let stitch_font_name = reader.read_cstring(FONT_NAME_LENGTH)?; + reader.seek_relative(20)?; + let font = Font { + name: reader.read_cstring(FONT_NAME_LENGTH)?, + size: reader.read_u16::()?, + weight: reader.read_u16::()?, + italic: reader.read_u16::()? == 1, + }; + reader.seek_relative(10)?; + + let view: View = reader.read_u16::()?.into(); + // Match a zoom variant into a percentage value. + let zoom = match reader.read_u16::()? { + 0 => 400, + 1 => 350, + 2 => 300, + 3 => 250, + 4 => 200, + 5 => 175, + 6 => 150, + 7 => 125, + 8 => 100, + 9 => 75, + 10 => 50, + 11 => 33, + 12 => 25, + 13 => 10, + _ => unreachable!("Invalid zoom value"), + }; + + let show_grid = reader.read_u16::()? == 1; + let show_rulers = reader.read_u16::()? == 1; + let show_centering_marks = reader.read_u16::()? == 1; + let show_fabric_colors_with_symbols = reader.read_u16::()? == 1; + reader.seek_relative(4)?; + let gaps_between_stitches = reader.read_u16::()? == 1; + + let page_header = reader.read_cstring(PAGE_HEADER_AND_FOOTER_LENGTH)?; + let page_footer = reader.read_cstring(PAGE_HEADER_AND_FOOTER_LENGTH)?; + let page_margins = PageMargins { + left: NotNan::new(reader.read_u16::()? as f32)? / 100.0, + right: NotNan::new(reader.read_u16::()? as f32)? / 100.0, + top: NotNan::new(reader.read_u16::()? as f32)? / 100.0, + bottom: NotNan::new(reader.read_u16::()? as f32)? / 100.0, + header: NotNan::new(reader.read_u16::()? as f32)? / 100.0, + footer: NotNan::new(reader.read_u16::()? as f32)? / 100.0, + }; + let show_page_numbers = reader.read_u16::()? == 1; + let show_adjacent_page_numbers = reader.read_u16::()? == 1; + let center_chart_on_pages = reader.read_u16::()? == 1; + reader.seek_relative(2)?; + + Ok(XsdPatternSettings { + stitch_font_name, + font, + view, + zoom, + show_grid, + show_rulers, + show_centering_marks, + show_fabric_colors_with_symbols, + gaps_between_stitches, + page_header, + page_footer, + page_margins, + show_page_numbers, + show_adjacent_page_numbers, + center_chart_on_pages, + }) +} + +fn read_grid_settings(reader: &mut R) -> Result { + fn read_grid_line_style(reader: &mut R) -> Result { + let thickness = reader.read_u16::()?; + let thickness = NotNan::new(thickness as f32)? * (72.0 / 1000.0); // Convert to points. + reader.seek_relative(2)?; + let color = reader.read_hex_color()?; + reader.seek_relative(3)?; + Ok(GridLineStyle { color, thickness }) + } + + let major_line_every_stitches = reader.read_u16::()?; + reader.seek_relative(2)?; + let minor_screen_lines = read_grid_line_style(reader)?; + let major_screen_lines = read_grid_line_style(reader)?; + let minor_printer_lines = read_grid_line_style(reader)?; + let major_printer_lines = read_grid_line_style(reader)?; + reader.seek_relative(12)?; + + Ok(Grid { + major_line_every_stitches, + minor_screen_lines, + major_screen_lines, + minor_printer_lines, + major_printer_lines, + }) +} + +/// Reads the necessarry pattern information. +fn read_pattern_info(reader: &mut R) -> Result { + log::trace!("Reading the pattern info"); + Ok(PatternInfo { + title: reader.read_cstring(PATTERN_NAME_LENGTH)?, + author: reader.read_cstring(AUTHOR_NAME_LENGTH)?, + company: reader.read_cstring(COMPANY_NAME_LENGTH)?, + copyright: reader.read_cstring(COPYRIGHT_LENGTH)?, + description: reader.read_cstring(PATTERN_NOTES_LENGTH)?, + }) +} + +fn read_stitch_settings(reader: &mut R) -> Result<(StitchSettings, bool, StitchOutline)> { + log::trace!("Reading stitch settings"); + + let stitch_settings = StitchSettings { + default_strands: DefaultStitchStrands { + full: reader.read_u16::()?, + half: reader.read_u16::()?, + quarter: reader.read_u16::()?, + back: reader.read_u16::()?, + petite: reader.read_u16::()?, + special: reader.read_u16::()?, + straight: reader.read_u16::()?, + }, + display_thickness: { + let mut buf = [NotNan::default(); 13]; + for thickness in buf.iter_mut() { + *thickness = NotNan::new(reader.read_u16::()? as f32)? / 10.0; + } + buf + }, + }; + + let outlined_stitches = reader.read_u16::()? == 1; + let use_specified_color = reader.read_u16::()? == 1; + let stitch_outline = StitchOutline { + color_percentage: reader.read_u16::()?, + color: if use_specified_color { + let color = reader.read_hex_color()?; + reader.seek_relative(1)?; + Some(color) + } else { + reader.seek_relative(4)?; + None + }, + thickness: NotNan::new(reader.read_u16::()? as f32)? / 10.0, + }; + + Ok((stitch_settings, outlined_stitches, stitch_outline)) +} + +fn read_symbol_settings(reader: &mut R) -> Result { + log::trace!("Reading symbol settings"); + Ok(SymbolSettings { + screen_spacing: (reader.read_u16::()?, reader.read_u16::()?), + printer_spacing: (reader.read_u16::()?, reader.read_u16::()?), + scale_using_maximum_font_width: reader.read_u16::()? == 1, + scale_using_font_height: reader.read_u16::()? == 1, + small_stitch_size: reader.read_u16::()?, + show_stitch_color: reader.read_u16::()? == 1, + use_large_half_stitch_symbol: { + let use_large_half_stitch_symbol = reader.read_u16::()? == 1; + reader.seek_relative(6)?; + use_large_half_stitch_symbol + }, + stitch_size: reader.read_u16::()?, + use_triangles_behind_quarter_stitches: reader.read_u16::()? == 1, + draw_symbols_over_backstitches: { + let draw_symbols_over_backstitches = reader.read_u16::()? == 1; + reader.seek_relative(2)?; + draw_symbols_over_backstitches + }, + }) +} + +/// Reads the stitches of the pattern. +fn read_stitches( + reader: &mut R, + coord_factor: usize, + total_stitches_count: usize, + small_stitches_count: usize, +) -> Result<(Vec, Vec)> { + log::trace!("Reading the stitches"); + let stitches_data = read_stitches_data(reader, total_stitches_count)?; + let small_stitch_buffers = read_small_stitch_buffers(reader, small_stitches_count)?; + let stitches = map_stitches_data_into_stitches(stitches_data, small_stitch_buffers, coord_factor)?; + Ok(stitches) +} + +/// Reads the bytes buffer that contains the decoded stitches data. +fn read_stitches_data(reader: &mut R, total_stitches_count: usize) -> Result> { + log::trace!("Reading the stitches data"); + let mut stitches_data = Vec::with_capacity(total_stitches_count); + let mut xsd_random_numbers = read_xsd_random_numbers(reader)?; + let (mut decoding_key, decoding_numbers) = reproduce_decoding_values(&xsd_random_numbers)?; + let mut decoding_number_index = 0; + let mut stitch_index = 0; + + while stitch_index < total_stitches_count { + let stitches_data_length = reader.read_u32::()? as usize; + + if stitches_data_length == 0 { + continue; + } + + let mut decoded_stitches_data = Vec::with_capacity(stitches_data_length); + + // Decoding. + for _ in 0..stitches_data_length { + let stitch_data = reader.read_i32::()? ^ decoding_key ^ xsd_random_numbers[0]; + decoded_stitches_data.push(stitch_data); + decoding_key = decoding_key.rotate_left(decoding_numbers[decoding_number_index]); + xsd_random_numbers[0] = xsd_random_numbers[0].wrapping_add(xsd_random_numbers[1]); + decoding_number_index = (decoding_number_index + 1) % 16; + } + + // Copying. + let mut stitch_data_index = 0; + while stitch_data_index < stitches_data_length { + let mut copy_count = 1; + let elem = decoded_stitches_data[stitch_data_index]; + + if elem & (i32::MAX / 2 + 1) != 0 { + copy_count = (elem & (i32::MAX / 2)) >> 16; + stitch_data_index += 1; + } + + while copy_count > 0 { + stitches_data.push(decoded_stitches_data[stitch_data_index]); + stitch_index += 1; + copy_count -= 1; + } + + stitch_data_index += 1; + } + } + + Ok(stitches_data) +} + +/// Reads the random numbers that are necessarry for decoding the stitches data. +fn read_xsd_random_numbers(reader: &mut R) -> Result<[i32; 4]> { + log::trace!("Reading the XSD random numbers"); + let mut xsd_random_numbers = [0; 4]; + for number in &mut xsd_random_numbers { + *number = reader.read_i32::()?; + } + Ok(xsd_random_numbers) +} + +/// Reproduces the decoding values that are used for decoding the stitches data. +fn reproduce_decoding_values(xsd_random_numbers: &[i32; 4]) -> Result<(i32, [u32; 16])> { + log::trace!("Reproducing the decoding values"); + let val1 = xsd_random_numbers[1].to_le_bytes()[1] as i32; + let val2 = xsd_random_numbers[0] << 8; + let val3 = (val2 | val1) << 8; + let val4 = xsd_random_numbers[2].to_le_bytes()[2] as i32; + let val5 = (val4 | val3) << 8; + let val6 = xsd_random_numbers[3] & 0xFF; + let decoding_key = val6 | val5; + + let mut decoding_buffer = [0; 16]; + + for i in 0..4 { + let buf = xsd_random_numbers[i].to_le_bytes(); + for j in 0..4 { + decoding_buffer[i * 4 + j] = buf[j]; + } + } + + let mut decoding_buffer = io::Cursor::new(decoding_buffer); + let mut decoding_numbers = [0; 16]; + + for i in 0..16 { + let offset = (i / 4) * 4; // 0, 4, 8, 12. + decoding_buffer.seek(SeekFrom::Start(offset))?; + let shift = decoding_buffer.read_u32::()? >> (i % 4); + decoding_numbers[i as usize] = shift % 32; + } + + Ok((decoding_key, decoding_numbers)) +} + +/// Reads the small stitch buffers that are used containe the small stitches data. +fn read_small_stitch_buffers(reader: &mut R, small_stitches_count: usize) -> Result> { + log::trace!("Reading the small stitch buffers"); + let mut small_stitch_buffers = Vec::with_capacity(small_stitches_count); + for _ in 0..small_stitches_count { + let mut buf = [0; 10]; + reader.read_exact(&mut buf)?; + small_stitch_buffers.push(buf); + } + Ok(small_stitch_buffers) +} + +#[derive(Debug, PartialEq)] +enum XsdSmallStitchKind { + HalfTop, + HalfBottom, + QuarterTopLeft, + QuarterBottomLeft, + QuarterTopRight, + QuarterBottomRight, + PetiteTopLeft, + PetiteBottomLeft, + PetiteTopRight, + PetiteBottomRight, +} + +/// Maps the stitches data into the full- and partstitches . +fn map_stitches_data_into_stitches( + stitches_data: Vec, + small_stitch_buffers: Vec<[u8; 10]>, + coord_factor: usize, +) -> Result<(Vec, Vec)> { + let mut fullstitches = Vec::new(); + let mut partstitches = Vec::new(); + + log::trace!("Mapping the stitches data into stitches"); + for (i, stitch_data) in stitches_data.iter().enumerate() { + let stitch_buffer = stitch_data.to_le_bytes(); + + // Empty cell. + if stitch_buffer[3] == 15 { + continue; + } + + let x = NotNan::new((i % coord_factor) as f32)?; + let y = NotNan::new((i / coord_factor) as f32)?; + + if stitch_buffer[3] == 0 { + fullstitches.push(FullStitch { + x, + y, + palindex: stitch_buffer[2], + kind: FullStitchKind::Full, + }); + continue; + } + + let position = (stitches_data[i] >> 16) & ((u16::MAX / 2) as i32); + let small_stitch_buffer = small_stitch_buffers.get(position as usize).unwrap(); + + for (significant_byte_index, bitand_arg, palindex_index, kind) in [ + (1, 1, 4, XsdSmallStitchKind::PetiteTopLeft), + (1, 2, 5, XsdSmallStitchKind::PetiteBottomLeft), + (1, 4, 6, XsdSmallStitchKind::PetiteTopRight), + (1, 8, 7, XsdSmallStitchKind::PetiteBottomRight), + ] { + let (x, y) = adjust_small_stitch_coors(x, y, &kind); + if small_stitch_buffer[significant_byte_index] & bitand_arg != 0 { + fullstitches.push(FullStitch { + x, + y, + palindex: small_stitch_buffer[palindex_index], + kind: FullStitchKind::Petite, + }) + } + } + + for (significant_byte_index, bitand_arg, palindex_index, kind) in [ + (0, 1, 2, XsdSmallStitchKind::HalfTop), + (0, 2, 3, XsdSmallStitchKind::HalfBottom), + (0, 4, 4, XsdSmallStitchKind::QuarterTopLeft), + (0, 8, 5, XsdSmallStitchKind::QuarterBottomLeft), + (0, 16, 6, XsdSmallStitchKind::QuarterTopRight), + (0, 32, 7, XsdSmallStitchKind::QuarterBottomRight), + ] { + if small_stitch_buffer[significant_byte_index] & bitand_arg != 0 { + let (x, y) = adjust_small_stitch_coors(x, y, &kind); + let direction = match kind { + XsdSmallStitchKind::HalfTop | XsdSmallStitchKind::QuarterTopLeft | XsdSmallStitchKind::QuarterBottomRight => { + PartStitchDirection::Backward + } + _ => PartStitchDirection::Forward, + }; + let kind = match kind { + XsdSmallStitchKind::HalfTop | XsdSmallStitchKind::HalfBottom => PartStitchKind::Half, + _ => PartStitchKind::Quarter, + }; + partstitches.push(PartStitch { + x, + y, + palindex: small_stitch_buffer[palindex_index], + direction, + kind, + }) + } + } + } + + Ok((fullstitches, partstitches)) +} + +/// Adjusts the coordinates of the small stitch. +/// The XSD format contains coordinates without additional offsets relative to the cell. +/// But this is important for us. +fn adjust_small_stitch_coors(x: Coord, y: Coord, kind: &XsdSmallStitchKind) -> (Coord, Coord) { + match kind { + XsdSmallStitchKind::QuarterTopLeft | XsdSmallStitchKind::PetiteTopLeft => (x, y), + XsdSmallStitchKind::QuarterTopRight | XsdSmallStitchKind::PetiteTopRight => (x + 0.5, y), + XsdSmallStitchKind::QuarterBottomLeft | XsdSmallStitchKind::PetiteBottomLeft => (x, y + 0.5), + XsdSmallStitchKind::QuarterBottomRight | XsdSmallStitchKind::PetiteBottomRight => (x + 0.5, y + 0.5), + _ => (x, y), + } +} + +/// Reads the special stitch models. +fn read_special_stitch_models(reader: &mut R) -> Result> { + reader.seek_relative(2)?; + let special_stith_models_count = reader.read_u16::()? as usize; + let mut special_stitch_models = Vec::with_capacity(special_stith_models_count); + + for _ in 0..special_stith_models_count { + if reader.read_u16::()? != 4 { + continue; + } + + reader.seek_relative(2)?; + let mut special_stitch_kind_buf = vec![0; 4]; + reader.read_exact(&mut special_stitch_kind_buf)?; + + if String::from_utf8(special_stitch_kind_buf)? != "sps1" { + continue; + } + + let mut special_stitch_model = SpecialStitchModel { + unique_name: reader.read_cstring(SPECIAL_STITCH_NAME_LENGTH)?, + name: reader.read_cstring(SPECIAL_STITCH_NAME_LENGTH)?, + width: 0, + height: 0, + nodes: Vec::new(), + lines: Vec::new(), + curves: Vec::new(), + }; + reader.seek_relative(2)?; + + for i in 0..3 { + if i == 0 { + reader.seek_relative(6)?; + // let shift = (reader.read_u16::()?, reader.read_u16::()?); + special_stitch_model.width = reader.read_u16::()?; + special_stitch_model.height = reader.read_u16::()?; + } else { + reader.seek_relative(10)?; + } + + if read_signature(reader)? != VALID_SIGNATURE { + break; + } + + let joints_count = reader.read_u16::()?; + if joints_count == 0 { + continue; + } + + if i == 0 || i == 2 { + let (nodes, lines, curves, _) = read_joints(reader, joints_count)?; + special_stitch_model.nodes.extend(nodes); + special_stitch_model.lines.extend(lines); + special_stitch_model.curves.extend(curves); + } else { + read_joints(reader, joints_count)?; + } + } + + special_stitch_models.push(special_stitch_model); + } + + Ok(special_stitch_models) +} + +#[derive(Debug, PartialEq)] +enum XsdJointKind { + FrenchKnot, + Back, + Curve, + Special, + Straight, + Bead, +} + +impl From for XsdJointKind { + fn from(value: u16) -> Self { + match value { + 1 => XsdJointKind::FrenchKnot, + 2 => XsdJointKind::Back, + 3 => XsdJointKind::Curve, + 4 => XsdJointKind::Special, + 5 => XsdJointKind::Straight, + 6 => XsdJointKind::Bead, + _ => unreachable!("Invalid joint kind {value}"), + } + } +} + +type Joints = (Vec, Vec, Vec, Vec); + +/// Reads the french knots, beads, back, straight and special stitches and curves that used in the pattern. +fn read_joints(reader: &mut R, joints_count: u16) -> io::Result { + let mut nodes = Vec::new(); + let mut lines = Vec::new(); + let mut curves = Vec::new(); + let mut specials = Vec::new(); + + log::trace!("Reading the joints"); + for _ in 0..joints_count { + let joint_kind = XsdJointKind::from(reader.read_u16::()?); + match joint_kind { + XsdJointKind::FrenchKnot => { + reader.seek_relative(2)?; + let x = NotNan::new(reader.read_u16::()? as f32)? / 2.0; + let y = NotNan::new(reader.read_u16::()? as f32)? / 2.0; + reader.seek_relative(4)?; + let palindex = reader.read_u8()?; + reader.seek_relative(1)?; + nodes.push(Node { + x, + y, + rotated: false, + palindex, + kind: NodeKind::FrenchKnot, + }); + } + + XsdJointKind::Back | XsdJointKind::Straight => { + reader.seek_relative(2)?; + let x1 = NotNan::new(reader.read_u16::()? as f32)? / 2.0; + let y1 = NotNan::new(reader.read_u16::()? as f32)? / 2.0; + let x2 = NotNan::new(reader.read_u16::()? as f32)? / 2.0; + let y2 = NotNan::new(reader.read_u16::()? as f32)? / 2.0; + let palindex = reader.read_u8()?; + reader.seek_relative(1)?; + let kind = if joint_kind == XsdJointKind::Back { + LineKind::Back + } else { + LineKind::Straight + }; + lines.push(Line { + x: (x1, x2), + y: (y1, y2), + palindex, + kind, + }); + } + + XsdJointKind::Curve => { + reader.seek_relative(3)?; + let points_count = reader.read_u16::()? as usize; + let mut curve = Curve { + points: Vec::with_capacity(points_count), + palindex: 0, + }; + for _ in 0..points_count { + let x = NotNan::new(reader.read_u16::()? as f32)? / 15.0; + let y = NotNan::new(reader.read_u16::()? as f32)? / 15.0; + curve.points.push((x, y)); + } + curves.push(curve); + } + + XsdJointKind::Special => { + reader.seek_relative(2)?; + let palindex = reader.read_u8()?; + reader.seek_relative(4)?; + // let shift = (reader.read_u8()?, reader.read_u8()?); + let x = NotNan::new(reader.read_u16::()? as f32)? / 2.0; + let y = NotNan::new(reader.read_u16::()? as f32)? / 2.0; + // let param = reader.read_u16::()?; + // let param = reader.read_u16::()?; + // let param = reader.read_u16::()?; + // let param = reader.read_u16::()?; + reader.seek_relative(10)?; + let modindex = reader.read_u16::()?; + specials.push(SpecialStitch { x, y, palindex, modindex }); + } + + XsdJointKind::Bead => { + reader.seek_relative(2)?; + let x = NotNan::new(reader.read_u16::()? as f32)? / 2.0; + let y = NotNan::new(reader.read_u16::()? as f32)? / 2.0; + let palindex = reader.read_u8()?; + reader.seek_relative(1)?; + let rotated = matches!(reader.read_u16::()?, 90 | 270); + nodes.push(Node { + x, + y, + rotated, + palindex, + kind: NodeKind::Bead, + }); + } + } + } + + Ok((nodes, lines, curves, specials)) +} diff --git a/src-tauri/src/parser/xsd.test.rs b/src-tauri/src/parser/xsd/xsd.test.rs similarity index 65% rename from src-tauri/src/parser/xsd.test.rs rename to src-tauri/src/parser/xsd/xsd.test.rs index 744bf68..f31f77f 100644 --- a/src-tauri/src/parser/xsd.test.rs +++ b/src-tauri/src/parser/xsd/xsd.test.rs @@ -1,95 +1,26 @@ -use crate::parser::xsd::*; +use std::{fs::File, io::Cursor}; -#[cfg(test)] -mod xsd_read_tests { - use super::*; +use super::*; - #[test] - fn reads_cstring() { - let utf8_buf = vec![0x57, 0x68, 0x69, 0x74, 0x65, 0x00, 0x00, 0x00]; - assert_eq!(Cursor::new(utf8_buf).read_cstring(8).unwrap(), String::from("White")); - - let cp1251_buf = vec![0xE3, 0xEE, 0xEB, 0xF3, 0xE1, 0xEE, 0xE9, 0x00]; - assert_eq!( - Cursor::new(cp1251_buf).read_cstring(8).unwrap(), - String::from("голубой") - ); - } - - #[test] - fn returns_empty_string_on_non_null_terminated_cstring() { - let not_nul_terminated_buf = vec![0x43, 0x6F, 0x66, 0x66, 0x65, 0x65]; - assert_eq!( - Cursor::new(not_nul_terminated_buf).read_cstring(6).unwrap(), - String::from("") - ); - } - - #[test] - fn reads_hex_color() { - let black_color_buf = vec![0x00, 0x00, 0x00]; - assert_eq!( - Cursor::new(black_color_buf).read_hex_color().unwrap(), - String::from("000000") - ); - - let white_color_buf = vec![0xff, 0xff, 0xff]; - assert_eq!( - Cursor::new(white_color_buf).read_hex_color().unwrap(), - String::from("FFFFFF") - ); - } - - #[test] - fn reads_fractional_coors() { - assert_eq!( - Cursor::new(vec![0x08, 0x00, 0x09, 0x00]) - .read_fractional_coors() - .unwrap(), - (NotNan::new(4.0).unwrap(), NotNan::new(4.5).unwrap()) - ); - } -} - -fn load_fixture(name: &str) -> Cursor { +fn load_fixture(name: &str) -> File { let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests/fixtures/xsd") + .join("testdata/xsd") .join(name); - let buf = std::fs::read(path).unwrap(); - Cursor::new(buf) + File::open(path).unwrap() } #[test] fn reads_signature() { let buf: Vec = vec![0x10, 0x05]; - assert_eq!(read_signature(&mut Cursor::new(buf)).unwrap(), XSD_VALID_SIGNATURE); + assert_eq!(read_signature(&mut Cursor::new(buf)).unwrap(), VALID_SIGNATURE); let buf = vec![0x00, 0x00]; - assert_ne!(read_signature(&mut Cursor::new(buf)).unwrap(), XSD_VALID_SIGNATURE); -} - -#[test] -fn reads_pattern_properties() { - let buf = vec![ - 0x64, 0x00, 0x64, 0x00, 0xA6, 0x01, 0x00, 0x00, 0x89, 0x03, 0x0E, 0x00, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, - 0x16, 0x4B, 0x00, - ]; - let loaded = read_pattern_properties(&mut Cursor::new(buf)).unwrap(); - let expected = XsdPatternProperties { - width: 100, - height: 100, - small_stitches_count: 422, - joints_count: 905, - stitches_per_inch: (14, 14), - palette_size: 75, - }; - assert_eq!(loaded, expected); + assert_ne!(read_signature(&mut Cursor::new(buf)).unwrap(), VALID_SIGNATURE); } #[test] fn reads_palette() { - let mut cursor = load_fixture("test_palette"); - let loaded_palette = read_palette(&mut cursor, 4).unwrap(); + let loaded_palette = read_palette(&mut load_fixture("palette")).unwrap(); let expected_palette = vec![ PaletteItem { brand: String::from("DMC"), @@ -97,6 +28,8 @@ fn reads_palette() { name: String::from("Black"), color: String::from("2C3225"), blends: None, + bead: None, + strands: StitchStrands::default(), }, PaletteItem { brand: String::from("PNK Kirova"), @@ -104,6 +37,8 @@ fn reads_palette() { name: String::from("ПНК Кирова"), color: String::from("B40032"), blends: None, + bead: None, + strands: StitchStrands::default(), }, PaletteItem { brand: String::from("Mill Hill Frosted Glass Seed Bead"), @@ -111,10 +46,15 @@ fn reads_palette() { name: String::from("Frosted Aquamarine"), color: String::from("A6D3D9"), blends: None, + bead: Some(Bead { + length: NotNan::new(1.5).unwrap(), + diameter: NotNan::new(2.5).unwrap(), + }), + strands: StitchStrands::default(), }, PaletteItem { brand: String::from("Blend"), - number: String::from("57"), + number: String::from("11"), name: String::from(""), color: String::from("93D0D3"), blends: Some(vec![ @@ -129,6 +69,17 @@ fn reads_palette() { strands: 1, }, ]), + bead: None, + strands: StitchStrands { + full: Some(2), + petite: Some(2), + half: Some(2), + quarter: Some(2), + back: Some(2), + straight: Some(2), + french_knot: Some(2), + special: Some(2), + }, }, ]; for (loaded, expected) in loaded_palette.iter().zip(expected_palette.iter()) { @@ -136,66 +87,22 @@ fn reads_palette() { } } -#[test] -fn reads_blend() { - let buf = vec![0x00, 0x33, 0x31, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; - let loaded = read_blend_item(&mut Cursor::new(buf)).unwrap(); - let expected = Blend { - brand: String::from("DMC"), - number: String::from("310"), - strands: 0, - }; - assert_eq!(loaded, expected); -} - -#[test] -fn reads_blends_strands() { - let buf = vec![0x01, 0x02, 0x00, 0x00]; - let mut blends = vec![ - Blend { - brand: String::new(), - number: String::new(), - strands: 0, - }, - Blend { - brand: String::new(), - number: String::new(), - strands: 0, - }, - ]; - read_blend_strands(&mut Cursor::new(buf), &mut blends).unwrap(); - assert_eq!(blends[0].strands, 1); - assert_eq!(blends[1].strands, 2); -} - -#[test] -fn reads_fabric_info() { - let mut cursor = load_fixture("test_fabric_info"); - let loaded = read_fabric_info(&mut cursor).unwrap(); - let expected = XsdFabric { - name: String::from("White"), - color: String::from("FFFFFF"), - }; - assert_eq!(loaded, expected); -} - #[test] fn reads_pattern_info() { - let mut cursor = load_fixture("test_pattern_info"); - let (loaded_pattern_info, loaded_fabric_kind) = read_pattern_info(&mut cursor).unwrap(); + let loaded_pattern_info = read_pattern_info(&mut load_fixture("pattern_info")).unwrap(); let expected_pattern_info = PatternInfo { title: String::from("Embroidery Studio Demo"), author: String::from("Nazar Antoniuk"), + company: String::from("Embroidery Studio"), copyright: String::from("Embroidery Studio"), description: String::from("Shows different stitch types"), }; assert_eq!(loaded_pattern_info, expected_pattern_info); - assert_eq!(loaded_fabric_kind, String::from("Aida")); } #[test] fn reproduces_decoding_values() { - let xsd_random_numbers: XsdRandomNumbers = [498347506, 626547637, 1679951037, 2146703145]; + let xsd_random_numbers = [498347506, 626547637, 1679951037, 2146703145]; let (decoding_key, decoding_values) = reproduce_decoding_values(&xsd_random_numbers).unwrap(); assert_eq!(decoding_key, -228908503); assert_eq!( @@ -206,16 +113,8 @@ fn reproduces_decoding_values() { #[test] fn reads_stitches() { - let mut cursor = load_fixture("test_stitches_data"); - let pattern_properties = XsdPatternProperties { - width: 10, - height: 10, - small_stitches_count: 8, - joints_count: 0, - stitches_per_inch: (14, 14), - palette_size: 7, - }; - let (loaded_fullstitches, loaded_partstitches) = read_stitches(&mut cursor, &pattern_properties).unwrap(); + let (loaded_fullstitches, loaded_partstitches) = + read_stitches(&mut load_fixture("stitches"), 10, 10 * 10, 8).unwrap(); let expected_fullstitches = [ FullStitch { x: NotNan::new(0.0).unwrap(), @@ -335,9 +234,7 @@ fn reads_stitches() { #[test] fn reads_joints() { - let joints_count = 8; - let mut cursor = load_fixture("test_joints_data"); - let (loaded_nodes, loaded_lines) = read_joints(&mut cursor, joints_count).unwrap(); + let (loaded_nodes, loaded_lines, ..) = read_joints(&mut load_fixture("joints"), 8).unwrap(); let expected_nodes = [ Node { x: NotNan::new(3.0).unwrap(), @@ -404,11 +301,8 @@ fn reads_joints() { #[test] fn parses_xsd_pattern() { - let pathbuf = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/patterns/piggies.xsd"); - let pattern = parse_pattern(pathbuf.as_path()); - assert!(pattern.is_ok()); - - let pattern = pattern.unwrap(); + let file_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/patterns/piggies.xsd"); + let pattern = parse_pattern(file_path).unwrap().pattern; assert_eq!(pattern.properties, PatternProperties { width: 69, height: 73 }); @@ -417,6 +311,7 @@ fn parses_xsd_pattern() { PatternInfo { title: String::from("Piggies"), author: String::from(""), + company: String::from(""), copyright: String::from("by Ursa Software"), description: String::from(""), } @@ -431,6 +326,8 @@ fn parses_xsd_pattern() { name: String::from("Bright Green-MD"), color: String::from("1B997F"), blends: None, + bead: None, + strands: StitchStrands::default() } ); assert_eq!( @@ -441,13 +338,18 @@ fn parses_xsd_pattern() { name: String::from("Red"), color: String::from("C74761"), blends: None, + bead: Some(Bead { + length: NotNan::new(1.5).unwrap(), + diameter: NotNan::new(2.5).unwrap() + }), + strands: StitchStrands::default() } ); assert_eq!( pattern.fabric, Fabric { - stitches_per_inch: (14, 14), + spi: (14, 14), kind: String::from("Aida"), name: String::from("White"), color: String::from("FFFFFF"), diff --git a/src-tauri/src/pattern/display.rs b/src-tauri/src/pattern/display.rs new file mode 100644 index 0000000..3f994b2 --- /dev/null +++ b/src-tauri/src/pattern/display.rs @@ -0,0 +1,339 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use ordered_float::NotNan; + +pub type Points = NotNan; + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct DisplaySettings { + pub default_stitch_font: String, + pub symbols: Vec, + pub symbol_settings: SymbolSettings, + pub formats: Vec, + pub grid: Grid, + pub view: View, + pub zoom: u16, + pub show_grid: bool, + pub show_rulers: bool, + pub show_centering_marks: bool, + pub show_fabric_colors_with_symbols: bool, + pub gaps_between_stitches: bool, + pub outlined_stitches: bool, + pub stitch_outline: StitchOutline, + pub stitch_settings: StitchSettings, +} + +impl DisplaySettings { + pub fn new(palette_size: usize) -> Self { + Self { + default_stitch_font: String::from("CrossStitch3"), + symbols: vec![Symbols::default(); palette_size], + symbol_settings: SymbolSettings::default(), + formats: vec![Formats::default(); palette_size], + grid: Grid::default(), + view: View::Solid, + zoom: 100, + show_grid: true, + show_rulers: true, + show_centering_marks: true, + show_fabric_colors_with_symbols: false, + gaps_between_stitches: false, + outlined_stitches: true, + stitch_outline: StitchOutline::default(), + stitch_settings: StitchSettings::default(), + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct Symbols { + pub full: Option, + pub petite: Option, + pub half: Option, + pub quarter: Option, + pub french_knot: Option, + pub bead: Option, +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct SymbolSettings { + pub screen_spacing: (u16, u16), + pub printer_spacing: (u16, u16), + pub scale_using_maximum_font_width: bool, + pub scale_using_font_height: bool, + pub stitch_size: u16, + pub small_stitch_size: u16, + pub draw_symbols_over_backstitches: bool, + pub show_stitch_color: bool, + pub use_large_half_stitch_symbol: bool, + pub use_triangles_behind_quarter_stitches: bool, +} + +impl Default for SymbolSettings { + fn default() -> Self { + Self { + screen_spacing: (1, 1), + printer_spacing: (1, 1), + scale_using_maximum_font_width: true, + scale_using_font_height: true, + stitch_size: 100, + small_stitch_size: 60, + draw_symbols_over_backstitches: false, + show_stitch_color: false, + use_large_half_stitch_symbol: false, + use_triangles_behind_quarter_stitches: false, + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct Formats { + pub symbol: SymbolFormat, + pub back: LineFormat, + pub straight: LineFormat, + pub french: NodeFormat, + pub bead: NodeFormat, + pub special: LineFormat, + pub font: FontFormat, +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct SymbolFormat { + pub use_alt_bg_color: bool, + pub bg_color: String, + pub fg_color: String, +} + +impl Default for SymbolFormat { + fn default() -> Self { + Self { + use_alt_bg_color: false, + bg_color: String::from("FFFFFF"), + fg_color: String::from("000000"), + } + } +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct LineFormat { + pub use_alt_color: bool, + pub color: String, + pub style: LineStyle, + pub thickness: Points, +} + +impl Default for LineFormat { + fn default() -> Self { + Self { + use_alt_color: false, + color: String::from("000000"), + style: LineStyle::Solid, + thickness: NotNan::new(1.0).unwrap(), + } + } +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +#[borsh(use_discriminant = true)] +pub enum LineStyle { + Solid = 0, + Barred = 1, + Dotted = 2, + ChainDotted = 3, + Dashed = 4, + Outlined = 5, + Zebra = 6, + ZigZag = 7, + Morse = 8, +} + +impl From for LineStyle { + fn from(value: u16) -> Self { + match value { + // These are the values used by Pattern Maker. + 0 | 5 => LineStyle::Solid, + 1 | 7 => LineStyle::Barred, + 2 | 6 => LineStyle::Dotted, + 11 => LineStyle::ChainDotted, + 3 | 8 => LineStyle::Dashed, + 9 => LineStyle::Outlined, + 10 => LineStyle::Zebra, + 12 => LineStyle::ZigZag, + 4 => LineStyle::Morse, + _ => panic!("Invalid LineStyle value: {value}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct NodeFormat { + pub use_dot_style: bool, + pub use_alt_color: bool, + pub color: String, + pub diameter: Points, +} + +impl Default for NodeFormat { + fn default() -> Self { + Self { + use_dot_style: true, + use_alt_color: false, + color: String::from("000000"), + diameter: NotNan::new(1.0).unwrap(), + } + } +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct FontFormat { + pub font_name: Option, + pub bold: bool, + pub italic: bool, + pub stitch_size: u16, + pub small_stitch_size: u16, +} + +impl Default for FontFormat { + fn default() -> Self { + Self { + font_name: None, + bold: false, + italic: false, + stitch_size: 100, + small_stitch_size: 60, + } + } +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct Grid { + pub major_line_every_stitches: u16, + pub minor_screen_lines: GridLineStyle, + pub major_screen_lines: GridLineStyle, + pub minor_printer_lines: GridLineStyle, + pub major_printer_lines: GridLineStyle, +} + +impl Default for Grid { + fn default() -> Self { + Self { + major_line_every_stitches: 10, + minor_screen_lines: GridLineStyle { + color: String::from("000000"), + thickness: NotNan::new(0.072).unwrap(), + }, + major_screen_lines: GridLineStyle { + color: String::from("000000"), + thickness: NotNan::new(0.072).unwrap(), + }, + minor_printer_lines: GridLineStyle { + color: String::from("000000"), + thickness: NotNan::new(0.144).unwrap(), + }, + major_printer_lines: GridLineStyle { + color: String::from("000000"), + thickness: NotNan::new(0.504).unwrap(), + }, + } + } +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct GridLineStyle { + pub color: String, + pub thickness: Points, +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +#[borsh(use_discriminant = true)] +pub enum View { + Stitches = 0, + Symbols = 1, + Solid = 2, + Information = 3, + MachineEmbInfo = 4, +} + +impl From for View { + fn from(value: u16) -> Self { + match value { + 0 => View::Stitches, + 1 => View::Symbols, + 2 => View::Solid, + 3 => View::Information, + 5 => View::MachineEmbInfo, + _ => panic!("Invalid View value: {value}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct StitchOutline { + pub color: Option, + pub color_percentage: u16, + pub thickness: Points, +} + +impl Default for StitchOutline { + fn default() -> Self { + Self { + color: None, + color_percentage: 80, + thickness: NotNan::new(0.2).unwrap(), + } + } +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct StitchSettings { + pub default_strands: DefaultStitchStrands, + /// 1..=12 - strands, 13 - french knot. + pub display_thickness: [Points; 13], +} + +impl Default for StitchSettings { + fn default() -> Self { + Self { + default_strands: DefaultStitchStrands::default(), + display_thickness: [ + NotNan::new(1.0).unwrap(), // 1 strand + NotNan::new(1.5).unwrap(), // 2 strands + NotNan::new(2.5).unwrap(), // 3 strands + NotNan::new(3.0).unwrap(), // 4 strands + NotNan::new(3.5).unwrap(), // 5 strands + NotNan::new(4.0).unwrap(), // 6 strands + NotNan::new(4.5).unwrap(), // 7 strands + NotNan::new(5.0).unwrap(), // 8 strands + NotNan::new(5.5).unwrap(), // 9 strands + NotNan::new(6.0).unwrap(), // 10 strands + NotNan::new(6.5).unwrap(), // 11 strands + NotNan::new(7.0).unwrap(), // 12 strands + NotNan::new(4.0).unwrap(), // French knot + ], + } + } +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct DefaultStitchStrands { + pub full: u16, + pub petite: u16, + pub half: u16, + pub quarter: u16, + pub back: u16, + pub straight: u16, + pub special: u16, +} + +impl Default for DefaultStitchStrands { + fn default() -> Self { + Self { + full: 2, + petite: 2, + half: 2, + quarter: 2, + back: 1, + straight: 1, + special: 2, + } + } +} diff --git a/src-tauri/src/pattern/mod.rs b/src-tauri/src/pattern/mod.rs index 7984f07..9095635 100644 --- a/src-tauri/src/pattern/mod.rs +++ b/src-tauri/src/pattern/mod.rs @@ -4,3 +4,9 @@ pub use pattern::*; mod stitches; pub use stitches::*; + +pub mod display; +pub mod print; + +mod project; +pub use project::*; diff --git a/src-tauri/src/pattern/pattern.rs b/src-tauri/src/pattern/pattern.rs index 04fee3f..9160e2e 100644 --- a/src-tauri/src/pattern/pattern.rs +++ b/src-tauri/src/pattern/pattern.rs @@ -2,8 +2,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use super::stitches::*; -pub type Coord = ordered_float::NotNan; - #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] pub struct Pattern { pub properties: PatternProperties, @@ -14,6 +12,8 @@ pub struct Pattern { pub partstitches: Stitches, pub nodes: Stitches, pub lines: Stitches, + pub specialstitches: Stitches, + pub special_stitch_models: Vec, } impl Pattern { @@ -66,6 +66,7 @@ impl Default for Pattern { info: PatternInfo { title: "Untitled".to_string(), author: "".to_string(), + company: "".to_string(), copyright: "".to_string(), description: "".to_string(), }, @@ -76,6 +77,8 @@ impl Default for Pattern { name: "Black".to_string(), color: "000000".to_string(), blends: None, + bead: None, + strands: StitchStrands::default(), }, PaletteItem { brand: "DMC".to_string(), @@ -83,10 +86,12 @@ impl Default for Pattern { name: "Coral-DK".to_string(), color: "C23131".to_string(), blends: None, + bead: None, + strands: StitchStrands::default(), }, ]), fabric: Fabric { - stitches_per_inch: (14, 14), + spi: (14, 14), kind: "Aida".to_string(), name: "White".to_string(), color: "FFFFFF".to_string(), @@ -95,11 +100,13 @@ impl Default for Pattern { partstitches: Stitches::new(), nodes: Stitches::new(), lines: Stitches::new(), + specialstitches: Stitches::new(), + special_stitch_models: Vec::new(), } } } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] pub struct PatternProperties { pub width: u16, pub height: u16, @@ -111,10 +118,11 @@ impl Default for PatternProperties { } } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] pub struct PatternInfo { pub title: String, pub author: String, + pub company: String, pub copyright: String, pub description: String, } @@ -124,31 +132,54 @@ impl Default for PatternInfo { Self { title: String::from("Untitled"), author: String::new(), + company: String::new(), copyright: String::new(), description: String::new(), } } } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] pub struct PaletteItem { pub brand: String, pub number: String, pub name: String, pub color: String, pub blends: Option>, + pub bead: Option, + pub strands: StitchStrands, } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] pub struct Blend { pub brand: String, pub number: String, pub strands: u8, } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, BorshSerialize, BorshDeserialize)] +pub type Millimetres = ordered_float::NotNan; + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct Bead { + pub length: Millimetres, + pub diameter: Millimetres, +} + +#[derive(Debug, Default, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct StitchStrands { + pub full: Option, + pub petite: Option, + pub half: Option, + pub quarter: Option, + pub back: Option, + pub straight: Option, + pub french_knot: Option, + pub special: Option, +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] pub struct Fabric { - pub stitches_per_inch: (u16, u16), + pub spi: (u16, u16), pub kind: String, pub name: String, pub color: String, @@ -157,7 +188,7 @@ pub struct Fabric { impl Default for Fabric { fn default() -> Self { Self { - stitches_per_inch: (14, 14), + spi: (14, 14), kind: String::from("Aida"), name: String::from("White"), color: String::from("FFFFFF"), diff --git a/src-tauri/src/pattern/print.rs b/src-tauri/src/pattern/print.rs new file mode 100644 index 0000000..193b5d2 --- /dev/null +++ b/src-tauri/src/pattern/print.rs @@ -0,0 +1,71 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use ordered_float::NotNan; + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct PrintSettings { + pub font: Font, + pub header: String, + pub footer: String, + pub margins: PageMargins, + pub show_page_numbers: bool, + pub show_adjacent_page_numbers: bool, + pub center_chart_on_pages: bool, +} + +impl Default for PrintSettings { + fn default() -> Self { + Self { + font: Font::default(), + header: String::new(), + footer: String::new(), + margins: PageMargins::default(), + show_page_numbers: true, + show_adjacent_page_numbers: true, + center_chart_on_pages: true, + } + } +} + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct Font { + pub name: String, + pub size: u16, + pub weight: u16, + pub italic: bool, +} + +impl Default for Font { + fn default() -> Self { + Self { + name: String::from("Arial"), + size: 12, + weight: 400, + italic: false, + } + } +} + +pub type Inches = NotNan; + +#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct PageMargins { + pub left: Inches, + pub right: Inches, + pub top: Inches, + pub bottom: Inches, + pub header: Inches, + pub footer: Inches, +} + +impl Default for PageMargins { + fn default() -> Self { + Self { + left: NotNan::new(0.5).unwrap(), + right: NotNan::new(0.5).unwrap(), + top: NotNan::new(0.5).unwrap(), + bottom: NotNan::new(0.5).unwrap(), + header: NotNan::new(0.5).unwrap(), + footer: NotNan::new(0.5).unwrap(), + } + } +} diff --git a/src-tauri/src/pattern/project.rs b/src-tauri/src/pattern/project.rs new file mode 100644 index 0000000..6360ca7 --- /dev/null +++ b/src-tauri/src/pattern/project.rs @@ -0,0 +1,12 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + +use super::{display::DisplaySettings, print::PrintSettings, Pattern}; + +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] +pub struct PatternProject { + #[borsh(skip)] + pub file_path: Option, + pub pattern: Pattern, + pub display_settings: DisplaySettings, + pub print_settings: PrintSettings, +} diff --git a/src-tauri/src/pattern/stitches/mod.rs b/src-tauri/src/pattern/stitches/mod.rs index 9f45d0a..ae676c3 100644 --- a/src-tauri/src/pattern/stitches/mod.rs +++ b/src-tauri/src/pattern/stitches/mod.rs @@ -10,6 +10,9 @@ pub use node::*; mod line; pub use line::*; +mod special; +pub use special::*; + #[allow(clippy::module_inception)] mod stitches; pub use stitches::*; diff --git a/src-tauri/src/pattern/stitches/special.rs b/src-tauri/src/pattern/stitches/special.rs new file mode 100644 index 0000000..f68dc1e --- /dev/null +++ b/src-tauri/src/pattern/stitches/special.rs @@ -0,0 +1,44 @@ +use std::cmp::Ordering; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +use super::{Line, Node}; +use crate::pattern::Coord; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct SpecialStitch { + pub x: Coord, + pub y: Coord, + pub palindex: u8, + pub modindex: u16, +} + +impl PartialOrd for SpecialStitch { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for SpecialStitch { + fn cmp(&self, other: &Self) -> Ordering { + self.y.cmp(&other.y).then(self.x.cmp(&other.x)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct SpecialStitchModel { + pub unique_name: String, + pub name: String, + pub width: u16, + pub height: u16, + pub nodes: Vec, + pub lines: Vec, + pub curves: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct Curve { + pub points: Vec<(Coord, Coord)>, + pub palindex: u8, +} diff --git a/src-tauri/src/pattern/stitches/stitches.rs b/src-tauri/src/pattern/stitches/stitches.rs index 425508e..ff994d8 100644 --- a/src-tauri/src/pattern/stitches/stitches.rs +++ b/src-tauri/src/pattern/stitches/stitches.rs @@ -10,6 +10,8 @@ use super::*; #[path = "./stitches.test.rs"] mod tests; +pub type Coord = ordered_float::NotNan; + #[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "lowercase")] pub enum Stitch { diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 184164f..e2d026a 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, path::PathBuf}; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -use crate::pattern::Pattern; +use crate::pattern::PatternProject; #[derive(Debug, Hash, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[repr(transparent)] @@ -16,7 +16,7 @@ impl From for PatternKey { } pub struct AppState { - pub patterns: HashMap, + pub patterns: HashMap, } impl AppState { diff --git a/src-tauri/tests/fixtures/xsd/test_fabric_info b/src-tauri/testdata/xsd/fabric_info similarity index 100% rename from src-tauri/tests/fixtures/xsd/test_fabric_info rename to src-tauri/testdata/xsd/fabric_info diff --git a/src-tauri/tests/fixtures/xsd/test_joints_data b/src-tauri/testdata/xsd/joints similarity index 100% rename from src-tauri/tests/fixtures/xsd/test_joints_data rename to src-tauri/testdata/xsd/joints diff --git a/src-tauri/testdata/xsd/palette b/src-tauri/testdata/xsd/palette new file mode 100644 index 0000000..f8f15f9 Binary files /dev/null and b/src-tauri/testdata/xsd/palette differ diff --git a/src-tauri/tests/fixtures/xsd/test_pattern_info b/src-tauri/testdata/xsd/pattern_info similarity index 100% rename from src-tauri/tests/fixtures/xsd/test_pattern_info rename to src-tauri/testdata/xsd/pattern_info diff --git a/src-tauri/testdata/xsd/pattern_properties b/src-tauri/testdata/xsd/pattern_properties new file mode 100644 index 0000000..bac9cb5 Binary files /dev/null and b/src-tauri/testdata/xsd/pattern_properties differ diff --git a/src-tauri/tests/fixtures/xsd/test_stitches_data b/src-tauri/testdata/xsd/stitches similarity index 100% rename from src-tauri/tests/fixtures/xsd/test_stitches_data rename to src-tauri/testdata/xsd/stitches diff --git a/src-tauri/tests/fixtures/xsd/test_palette b/src-tauri/tests/fixtures/xsd/test_palette deleted file mode 100755 index 485ab18..0000000 Binary files a/src-tauri/tests/fixtures/xsd/test_palette and /dev/null differ diff --git a/src/App.vue b/src/App.vue index bde8666..73c3a2a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -29,12 +29,12 @@ - + - +

Open a pattern or create a new one to get started.

@@ -74,13 +74,13 @@ import { usePreferencesStore } from "./stores/preferences"; import { studioDocumentDir } from "./utils/path"; import * as patternApi from "./api/pattern"; - import type { Pattern } from "./types/pattern"; + import type { PatternProject } from "./types/pattern/project"; const appStateStore = useAppStateStore(); const preferencesStore = usePreferencesStore(); const loading = ref(false); - const pattern = ref(); + const patproj = ref(); const confirm = useConfirm(); @@ -97,8 +97,8 @@ multiple: false, filters: [ { - name: "Cross Stitch Pattern", - extensions: ["xsd", "oxs", "xml", "embx"], + name: "Cross-Stitch Pattern", + extensions: ["xsd", "oxs", "xml", "embproj"], }, ], }); @@ -138,7 +138,7 @@ if (!appStateStore.state.currentPattern) return; await patternApi.closePattern(appStateStore.state.currentPattern.key); appStateStore.removeCurrentPattern(); - if (!appStateStore.state.currentPattern) pattern.value = undefined; + if (!appStateStore.state.currentPattern) patproj.value = undefined; else await loadPattern(appStateStore.state.currentPattern.key); }, }, @@ -182,8 +182,8 @@ async function loadPattern(path: string) { try { loading.value = true; - pattern.value = await patternApi.loadPattern(path); - appStateStore.addOpenedPattern(pattern.value!.info.title, path); + patproj.value = await patternApi.loadPattern(path); + appStateStore.addOpenedPattern(patproj.value!.pattern.info.title, path); } catch (err) { confirm.require({ header: "Error", @@ -203,8 +203,8 @@ async function createPattern() { loading.value = true; const { key, pattern: pat } = await patternApi.createPattern(); - pattern.value = pat; - appStateStore.addOpenedPattern(pattern.value!.info.title, key); + patproj.value = pat; + appStateStore.addOpenedPattern(patproj.value!.pattern.info.title, key); loading.value = false; } diff --git a/src/api/pattern.ts b/src/api/pattern.ts index 590f284..e7c1baf 100644 --- a/src/api/pattern.ts +++ b/src/api/pattern.ts @@ -1,16 +1,16 @@ import { invoke } from "@tauri-apps/api/core"; import { borshDeserialize } from "borsher"; -import { PatternSchema } from "#/schemas/pattern"; -import type { Pattern } from "#/types/pattern"; +import { PatternProjectSchema } from "#/schemas/"; +import type { PatternProject } from "#/types/pattern/project"; export const loadPattern = async (filePath: string) => { const bytes = await invoke("load_pattern", { filePath }); - return borshDeserialize(PatternSchema, bytes); + return borshDeserialize(PatternProjectSchema, bytes); }; export const createPattern = async () => { const [key, bytes] = await invoke<[string, Uint8Array]>("create_pattern"); - return { key, pattern: borshDeserialize(PatternSchema, bytes) }; + return { key, pattern: borshDeserialize(PatternProjectSchema, bytes) }; }; export const savePattern = (patternKey: string, filePath: string) => { diff --git a/src/components/CanvasPanel.vue b/src/components/CanvasPanel.vue index 512eede..5c2594f 100644 --- a/src/components/CanvasPanel.vue +++ b/src/components/CanvasPanel.vue @@ -8,12 +8,13 @@ import { CanvasService } from "#/services/canvas"; import { useAppStateStore } from "#/stores/state"; import { emitStitchCreated, emitStitchRemoved } from "#/services/events/pattern"; - import { PartStitchDirection, StitchKind } from "#/types/pattern"; - import type { FullStitch, Line, Node, PartStitch, Pattern } from "#/types/pattern"; + import { PartStitchDirection, StitchKind } from "#/types/pattern/pattern"; import type { RemovedStitchPayload, StitchEventPayload } from "#/types/events/pattern"; + import type { FullStitch, Line, Node, PartStitch } from "#/types/pattern/pattern"; + import type { PatternProject } from "#/types/pattern/project"; interface CanvasPanelProps { - pattern: Pattern; + patproj: PatternProject; } const props = defineProps(); @@ -28,12 +29,12 @@ canvasService.resize(canvasContainer.value!.getBoundingClientRect()); window.addEventListener("resize", () => canvasService.resize(canvasContainer.value!.getBoundingClientRect())); canvasContainer.value!.appendChild(canvasService.view as HTMLCanvasElement); - canvasService.drawPattern(props.pattern); + canvasService.drawPattern(props.patproj); }); watch( - () => props.pattern, - (pattern) => canvasService.drawPattern(pattern), + () => props.patproj, + (patproj) => canvasService.drawPattern(patproj), ); // A start point is needed to draw the lines. @@ -54,7 +55,7 @@ // The current pattern is always available here. const patternKey = appStateStore.state.currentPattern!.key; const palitem = appStateStore.state.selectedPaletteItem; - const palindex = props.pattern.palette.findIndex((pi) => pi.color === palitem.color); + const palindex = props.patproj.pattern.palette.findIndex((pi) => pi.color === palitem.color); const tool = appStateStore.state.selectedStitchTool; const kind = tool % 2; // Get 0 or 1. @@ -139,7 +140,7 @@ // The current pattern is always available here. const patternKey = appStateStore.state.currentPattern!.key; const palitem = appStateStore.state.selectedPaletteItem; - const palindex = props.pattern.palette.findIndex((pi) => pi.color === palitem.color); + const palindex = props.patproj.pattern.palette.findIndex((pi) => pi.color === palitem.color); const tool = appStateStore.state.selectedStitchTool; const kind = tool % 2; // Get 0 or 1. diff --git a/src/components/PalettePanel.test.ts b/src/components/PalettePanel.test.ts index b4f977e..cd2fb0c 100644 --- a/src/components/PalettePanel.test.ts +++ b/src/components/PalettePanel.test.ts @@ -4,7 +4,7 @@ import { createTestingPinia } from "@pinia/testing"; import { PrimeVue } from "@primevue/core"; import Popover from "primevue/popover"; import PalettePanel from "./PalettePanel.vue"; -import type { Blend, PaletteItem } from "#/types/pattern"; +import type { Blend, PaletteItem } from "#/types/pattern/pattern"; import ToggleSwitch from "primevue/toggleswitch"; import Checkbox from "primevue/checkbox"; @@ -27,18 +27,21 @@ describe("PalettePanel", () => { number: "310", name: "Black", color: "2C3225", + strands: {}, }, { brand: "Anchor", number: "9159", name: "Glacier Blue", color: "B2D8E5", + strands: {}, }, { brand: "Madeira", number: "0705", name: "Plum-DK", color: "901b6b", + strands: {}, }, { brand: "Blends", @@ -46,6 +49,7 @@ describe("PalettePanel", () => { name: "", color: "A382AE", blends: BLENDS, + strands: {}, }, ]; @@ -100,8 +104,6 @@ describe("PalettePanel", () => { const paletteItemsTitles = wrapper.findAll("ul > li > div > div"); const initialTexts = paletteItemsTitles.map((title) => title.text()); - console.log(initialTexts); - // Open the popover. await wrapper.get("button").trigger("click"); const checkboxes = popover.findAllComponents(Checkbox); diff --git a/src/components/PalettePanel.vue b/src/components/PalettePanel.vue index f167472..7620d24 100644 --- a/src/components/PalettePanel.vue +++ b/src/components/PalettePanel.vue @@ -92,7 +92,7 @@ import { contrastColor } from "#/utils/color"; import { paletteItemTitle, type PaletteItemDisplayOptions } from "#/utils/paletteItem"; import { useAppStateStore } from "#/stores/state"; - import type { PaletteItem } from "#/types/pattern"; + import type { PaletteItem } from "#/types/pattern/pattern"; interface PalettePanelProps { palette?: PaletteItem[]; diff --git a/src/components/toolbar/StitchToolSelector.vue b/src/components/toolbar/StitchToolSelector.vue index 2603b02..3728708 100644 --- a/src/components/toolbar/StitchToolSelector.vue +++ b/src/components/toolbar/StitchToolSelector.vue @@ -12,7 +12,7 @@ import { ref } from "vue"; import Select from "primevue/select"; import { useAppStateStore } from "#/stores/state"; - import { StitchKind } from "#/types/pattern"; + import { StitchKind } from "#/types/pattern/pattern"; const appState = useAppStateStore(); const tools = ref([ diff --git a/src/schemas/index.ts b/src/schemas/index.ts new file mode 100644 index 0000000..750be88 --- /dev/null +++ b/src/schemas/index.ts @@ -0,0 +1 @@ +export { PatternProjectSchema } from "./pattern/project"; diff --git a/src/schemas/pattern.ts b/src/schemas/pattern.ts deleted file mode 100644 index cc432f7..0000000 --- a/src/schemas/pattern.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { BorshSchema } from "borsher"; - -export const PatternPropertiesSchema = BorshSchema.Struct({ - width: BorshSchema.u16, - height: BorshSchema.u16, -}); - -export const PatternInfoSchema = BorshSchema.Struct({ - title: BorshSchema.String, - author: BorshSchema.String, - copyright: BorshSchema.String, - description: BorshSchema.String, -}); - -export const BlendSchema = BorshSchema.Struct({ - brand: BorshSchema.String, - number: BorshSchema.String, - strands: BorshSchema.u8, -}); - -export const PaletteItemSchema = BorshSchema.Struct({ - brand: BorshSchema.String, - number: BorshSchema.String, - name: BorshSchema.String, - color: BorshSchema.String, - blends: BorshSchema.Option(BorshSchema.Vec(BlendSchema)), -}); - -export const FabricSchema = BorshSchema.Struct({ - spi: BorshSchema.Array(BorshSchema.u16, 2), - kind: BorshSchema.String, - name: BorshSchema.String, - color: BorshSchema.String, -}); - -export const FullStitchSchema = BorshSchema.Struct({ - x: BorshSchema.f32, - y: BorshSchema.f32, - palindex: BorshSchema.u8, - kind: BorshSchema.u8, -}); - -export const PartStitchSchema = BorshSchema.Struct({ - x: BorshSchema.f32, - y: BorshSchema.f32, - palindex: BorshSchema.u8, - direction: BorshSchema.u8, - kind: BorshSchema.u8, -}); - -export const NodeSchema = BorshSchema.Struct({ - x: BorshSchema.f32, - y: BorshSchema.f32, - rotated: BorshSchema.bool, - palindex: BorshSchema.u8, - kind: BorshSchema.u8, -}); - -export const LineSchema = BorshSchema.Struct({ - x: BorshSchema.Array(BorshSchema.f32, 2), - y: BorshSchema.Array(BorshSchema.f32, 2), - palindex: BorshSchema.u8, - kind: BorshSchema.u8, -}); - -export const PatternSchema = BorshSchema.Struct({ - properties: PatternPropertiesSchema, - info: PatternInfoSchema, - palette: BorshSchema.Vec(PaletteItemSchema), - fabric: FabricSchema, - fullstitches: BorshSchema.Vec(FullStitchSchema), - partstitches: BorshSchema.Vec(PartStitchSchema), - nodes: BorshSchema.Vec(NodeSchema), - lines: BorshSchema.Vec(LineSchema), -}); diff --git a/src/schemas/pattern/display.ts b/src/schemas/pattern/display.ts new file mode 100644 index 0000000..938579f --- /dev/null +++ b/src/schemas/pattern/display.ts @@ -0,0 +1,113 @@ +import { BorshSchema } from "borsher"; + +const SymbolsSchema = BorshSchema.Struct({ + full: BorshSchema.Option(BorshSchema.u16), + petite: BorshSchema.Option(BorshSchema.u16), + half: BorshSchema.Option(BorshSchema.u16), + quarter: BorshSchema.Option(BorshSchema.u16), + french_knot: BorshSchema.Option(BorshSchema.u16), + bead: BorshSchema.Option(BorshSchema.u16), +}); + +const SymbolSettingsSchema = BorshSchema.Struct({ + screenSpacing: BorshSchema.Array(BorshSchema.u16, 2), + printerSpacing: BorshSchema.Array(BorshSchema.u16, 2), + scaleUsingMaximumFontWidth: BorshSchema.bool, + scaleUsingFontHeight: BorshSchema.bool, + stitchSize: BorshSchema.u16, + smallStitchSize: BorshSchema.u16, + drawSymbolsOverBackstitches: BorshSchema.bool, + showStitchColor: BorshSchema.bool, + useLargeHalfStitchSymbol: BorshSchema.bool, + useTrianglesBehindQuarterStitches: BorshSchema.bool, +}); + +const SymbolFormatSchema = BorshSchema.Struct({ + useAltBgColor: BorshSchema.bool, + bgColor: BorshSchema.String, + fgColor: BorshSchema.String, +}); + +const LineFormatSchema = BorshSchema.Struct({ + useAltColor: BorshSchema.bool, + color: BorshSchema.String, + style: BorshSchema.u8, + thickness: BorshSchema.f32, +}); + +const NodeFormatSchema = BorshSchema.Struct({ + useDotStyle: BorshSchema.bool, + useAltColor: BorshSchema.bool, + color: BorshSchema.String, + diameter: BorshSchema.f32, +}); + +const FontFormatSchema = BorshSchema.Struct({ + fontName: BorshSchema.Option(BorshSchema.String), + bold: BorshSchema.bool, + italic: BorshSchema.bool, + stitchSize: BorshSchema.u16, + smallStitchSize: BorshSchema.u16, +}); + +const FormatsSchema = BorshSchema.Struct({ + symbol: SymbolFormatSchema, + back: LineFormatSchema, + straight: LineFormatSchema, + french: NodeFormatSchema, + bead: NodeFormatSchema, + special: LineFormatSchema, + font: FontFormatSchema, +}); + +const GridLineStyleSchema = BorshSchema.Struct({ + color: BorshSchema.String, + thickness: BorshSchema.f32, +}); + +const GridSchema = BorshSchema.Struct({ + majorLineEveryStitches: BorshSchema.u16, + minorScreenLines: GridLineStyleSchema, + majorScreenLines: GridLineStyleSchema, + minorPrinterLines: GridLineStyleSchema, + majorPrinterLines: GridLineStyleSchema, +}); + +const StitchOutlineSchema = BorshSchema.Struct({ + color: BorshSchema.Option(BorshSchema.String), + colorPercentage: BorshSchema.u16, + thickness: BorshSchema.f32, +}); + +const DefaultStitchStrandsSchema = BorshSchema.Struct({ + full: BorshSchema.u16, + petite: BorshSchema.u16, + half: BorshSchema.u16, + quarter: BorshSchema.u16, + back: BorshSchema.u16, + straight: BorshSchema.u16, + special: BorshSchema.u16, +}); + +const StitchSettingsSchema = BorshSchema.Struct({ + defaultStrands: DefaultStitchStrandsSchema, + displayThickness: BorshSchema.Array(BorshSchema.f32, 13), +}); + +export const DisplaySettingsSchema = BorshSchema.Struct({ + defaultStitchFont: BorshSchema.String, + symbols: BorshSchema.Vec(SymbolsSchema), + symbolSettings: SymbolSettingsSchema, + formats: BorshSchema.Vec(FormatsSchema), + grid: GridSchema, + view: BorshSchema.u8, + zoom: BorshSchema.u16, + showGrid: BorshSchema.bool, + showRulers: BorshSchema.bool, + showCenteringMarks: BorshSchema.bool, + showFabricColorsWithSymbols: BorshSchema.bool, + gapsBetweenStitches: BorshSchema.bool, + outlinedStitches: BorshSchema.bool, + stitchOutline: StitchOutlineSchema, + stitchSettings: StitchSettingsSchema, +}); diff --git a/src/schemas/pattern/pattern.ts b/src/schemas/pattern/pattern.ts new file mode 100644 index 0000000..d299fec --- /dev/null +++ b/src/schemas/pattern/pattern.ts @@ -0,0 +1,118 @@ +import { BorshSchema } from "borsher"; + +const PatternPropertiesSchema = BorshSchema.Struct({ + width: BorshSchema.u16, + height: BorshSchema.u16, +}); + +const PatternInfoSchema = BorshSchema.Struct({ + title: BorshSchema.String, + author: BorshSchema.String, + company: BorshSchema.String, + copyright: BorshSchema.String, + description: BorshSchema.String, +}); + +const StitchStrandsSchema = BorshSchema.Struct({ + full: BorshSchema.Option(BorshSchema.u16), + petite: BorshSchema.Option(BorshSchema.u16), + half: BorshSchema.Option(BorshSchema.u16), + quarter: BorshSchema.Option(BorshSchema.u16), + back: BorshSchema.Option(BorshSchema.u16), + straight: BorshSchema.Option(BorshSchema.u16), + frenchKnot: BorshSchema.Option(BorshSchema.u16), + special: BorshSchema.Option(BorshSchema.u16), +}); + +const BlendSchema = BorshSchema.Struct({ + brand: BorshSchema.String, + number: BorshSchema.String, + strands: BorshSchema.u8, +}); + +const BeadSchema = BorshSchema.Struct({ + lenght: BorshSchema.f32, + diameter: BorshSchema.f32, +}); + +const PaletteItemSchema = BorshSchema.Struct({ + brand: BorshSchema.String, + number: BorshSchema.String, + name: BorshSchema.String, + color: BorshSchema.String, + blends: BorshSchema.Option(BorshSchema.Vec(BlendSchema)), + bead: BorshSchema.Option(BeadSchema), + strands: StitchStrandsSchema, +}); + +const FabricSchema = BorshSchema.Struct({ + spi: BorshSchema.Array(BorshSchema.u16, 2), + kind: BorshSchema.String, + name: BorshSchema.String, + color: BorshSchema.String, +}); + +const FullStitchSchema = BorshSchema.Struct({ + x: BorshSchema.f32, + y: BorshSchema.f32, + palindex: BorshSchema.u8, + kind: BorshSchema.u8, +}); + +const PartStitchSchema = BorshSchema.Struct({ + x: BorshSchema.f32, + y: BorshSchema.f32, + palindex: BorshSchema.u8, + direction: BorshSchema.u8, + kind: BorshSchema.u8, +}); + +const NodeSchema = BorshSchema.Struct({ + x: BorshSchema.f32, + y: BorshSchema.f32, + rotated: BorshSchema.bool, + palindex: BorshSchema.u8, + kind: BorshSchema.u8, +}); + +const LineSchema = BorshSchema.Struct({ + x: BorshSchema.Array(BorshSchema.f32, 2), + y: BorshSchema.Array(BorshSchema.f32, 2), + palindex: BorshSchema.u8, + kind: BorshSchema.u8, +}); + +const CurveSchema = BorshSchema.Struct({ + points: BorshSchema.Vec(BorshSchema.Array(BorshSchema.f32, 2)), + palindex: BorshSchema.u8, +}); + +const SpecialStitchSchema = BorshSchema.Struct({ + x: BorshSchema.f32, + y: BorshSchema.f32, + palindex: BorshSchema.u8, + modindex: BorshSchema.u16, +}); + +const SpecialStitchModelSchema = BorshSchema.Struct({ + uniqueName: BorshSchema.String, + name: BorshSchema.String, + width: BorshSchema.u16, + height: BorshSchema.u16, + nodes: BorshSchema.Vec(NodeSchema), + lines: BorshSchema.Vec(LineSchema), + curves: BorshSchema.Vec(CurveSchema), +}); + +export const PatternSchema = BorshSchema.Struct({ + properties: PatternPropertiesSchema, + info: PatternInfoSchema, + palette: BorshSchema.Vec(PaletteItemSchema), + fabric: FabricSchema, + fullstitches: BorshSchema.Vec(FullStitchSchema), + partstitches: BorshSchema.Vec(PartStitchSchema), + nodes: BorshSchema.Vec(NodeSchema), + lines: BorshSchema.Vec(LineSchema), + specialstitches: BorshSchema.Vec(SpecialStitchSchema), + specialStitchModels: BorshSchema.Vec(SpecialStitchModelSchema), +}); diff --git a/src/schemas/pattern/print.ts b/src/schemas/pattern/print.ts new file mode 100644 index 0000000..47c5967 --- /dev/null +++ b/src/schemas/pattern/print.ts @@ -0,0 +1,27 @@ +import { BorshSchema } from "borsher"; + +const FontSchema = BorshSchema.Struct({ + name: BorshSchema.String, + size: BorshSchema.u16, + weight: BorshSchema.u16, + italic: BorshSchema.bool, +}); + +const PageMarginsSchema = BorshSchema.Struct({ + left: BorshSchema.f32, + right: BorshSchema.f32, + top: BorshSchema.f32, + bottom: BorshSchema.f32, + header: BorshSchema.f32, + footer: BorshSchema.f32, +}); + +export const PrintSettingsSchema = BorshSchema.Struct({ + font: FontSchema, + header: BorshSchema.String, + footer: BorshSchema.String, + margins: PageMarginsSchema, + show_page_numbers: BorshSchema.bool, + show_adjacent_page_numbers: BorshSchema.bool, + center_chart_on_pages: BorshSchema.bool, +}); diff --git a/src/schemas/pattern/project.ts b/src/schemas/pattern/project.ts new file mode 100644 index 0000000..22781aa --- /dev/null +++ b/src/schemas/pattern/project.ts @@ -0,0 +1,10 @@ +import { BorshSchema } from "borsher"; +import { PatternSchema } from "./pattern"; +import { DisplaySettingsSchema } from "./display"; +import { PrintSettingsSchema } from "./print"; + +export const PatternProjectSchema = BorshSchema.Struct({ + pattern: PatternSchema, + displaySettings: DisplaySettingsSchema, + printSettings: PrintSettingsSchema, +}); diff --git a/src/services/canvas.ts b/src/services/canvas.ts index 4a3aae9..1c4ab4f 100644 --- a/src/services/canvas.ts +++ b/src/services/canvas.ts @@ -2,21 +2,10 @@ import { Application, Container, Graphics, LINE_CAP, Point, Polygon } from "pixi import type { FederatedMouseEvent, ColorSource } from "pixi.js"; import { Viewport } from "pixi-viewport"; import { SpatialHash as SpatialHashCuller } from "pixi-cull"; -import { FullStitchKind, NodeKind, PartStitchDirection, PartStitchKind } from "#/types/pattern"; -import type { FullStitch, Line, Node, PartStitch, Pattern, PatternProperties } from "#/types/pattern"; -import type { GridSettings } from "#/types/view"; - -const GRID_SETTINGS: GridSettings = { - majorLinesEveryStitches: 10, - minorLines: { - thickness: 0.05, - color: "000000", - }, - majorLines: { - thickness: 0.1, - color: "000000", - }, -}; +import type { PatternProject } from "#/types/pattern/project"; +import type { Grid } from "#/types/pattern/display"; +import type { FullStitch, Line, Node, PartStitch, PatternProperties } from "#/types/pattern/pattern"; +import { FullStitchKind, NodeKind, PartStitchDirection, PartStitchKind } from "#/types/pattern/pattern"; const FULL_STITCH_GEOMETRIES = { [FullStitchKind.Full]: new Graphics().beginFill("FFFFFF").drawRect(0, 0, 1, 1).endFill().geometry, @@ -135,11 +124,11 @@ export class CanvasService extends EventTarget { } } - drawPattern(pattern: Pattern) { + drawPattern({ pattern, displaySettings }: PatternProject) { this.clearPattern(); this.#viewport.moveCenter(pattern.properties.width / 2, pattern.properties.height / 2); this.drawFabric(pattern.properties, pattern.fabric.color); - this.drawGrid(pattern.properties, GRID_SETTINGS); + this.drawGrid(pattern.properties, displaySettings.grid); // prettier-ignore for (const fullstitch of pattern.fullstitches) this.drawFullStitch(fullstitch, pattern.palette[fullstitch.palindex]!.color); // prettier-ignore @@ -152,35 +141,43 @@ export class CanvasService extends EventTarget { this.#stages.fabric.beginFill(color).drawRect(0, 0, width, height).endFill(); } - drawGrid({ width, height }: PatternProperties, gridSettings: GridSettings) { + drawGrid({ width, height }: PatternProperties, grid: Grid) { const graphics = this.#stages.grid; { - // Drawing major grid lines. - const interval = gridSettings.majorLinesEveryStitches; - const { thickness, color } = gridSettings.majorLines; - graphics.lineStyle({ width: thickness, color }); - for (let i = 0; i < width / interval; i++) { - graphics.moveTo(i * interval, 0); - graphics.lineTo(i * interval, height); - } - for (let i = 0; i < height / interval; i++) { - graphics.moveTo(0, i * interval); - graphics.lineTo(width, i * interval); - } - } - { - // Drawing minor grid lines. - const { thickness, color } = gridSettings.minorLines; - graphics.lineStyle({ width: thickness, color }); - for (let i = 0; i < width; i++) { + const { thickness, color } = grid.minorScreenLines; + graphics.lineStyle({ width: thickness, color: color as ColorSource }); + + // Draw horizontal lines. + for (let i = 1; i < width; i++) { graphics.moveTo(i, 0); graphics.lineTo(i, height); } - for (let i = 0; i < height; i++) { + + // Draw vertical lines. + for (let i = 1; i < height; i++) { graphics.moveTo(0, i); graphics.lineTo(width, i); } } + { + const interval = grid.majorLineEveryStitches; + const { thickness, color } = grid.majorScreenLines; + graphics.lineStyle({ width: thickness, color: color as ColorSource }); + + // Draw horizontal lines. + for (let i = 0; i <= Math.ceil(height / interval); i++) { + const point = Math.min(i * interval, height); + graphics.moveTo(0, point); + graphics.lineTo(width, point); + } + + // Draw vertical lines. + for (let i = 0; i <= Math.ceil(width / interval); i++) { + const point = Math.min(i * interval, width); + graphics.moveTo(point, 0); + graphics.lineTo(point, height); + } + } } drawFullStitch(fullstitch: FullStitch, color: ColorSource) { diff --git a/src/stores/state.ts b/src/stores/state.ts index 85515e7..939e957 100644 --- a/src/stores/state.ts +++ b/src/stores/state.ts @@ -1,6 +1,6 @@ import { reactive } from "vue"; import { defineStore } from "pinia"; -import { StitchKind, type PaletteItem } from "#/types/pattern"; +import { StitchKind, type PaletteItem } from "#/types/pattern/pattern"; interface OpenedPattern { title: string; diff --git a/src/types/events/pattern.ts b/src/types/events/pattern.ts index 858e6eb..51cfd4d 100644 --- a/src/types/events/pattern.ts +++ b/src/types/events/pattern.ts @@ -1,4 +1,4 @@ -import type { FullStitch, Line, Node, PartStitch } from "../pattern"; +import type { FullStitch, Line, Node, PartStitch } from "../pattern/pattern"; export interface StitchEventPayload { patternKey: string; diff --git a/src/types/pattern/display.ts b/src/types/pattern/display.ts new file mode 100644 index 0000000..c1ea629 --- /dev/null +++ b/src/types/pattern/display.ts @@ -0,0 +1,149 @@ +export interface DisplaySettings { + defaultStitchFont: String; + symbols: Symbols[]; + symbolSettings: SymbolSettings; + formats: Formats[]; + grid: Grid; + view: View; + zoom: number; + showGrid: boolean; + showRulers: boolean; + showCenteringMarks: boolean; + showFabricColorsWithSymbols: boolean; + gapsBetweenStitches: boolean; + outlinedStitches: boolean; + stitchOutline: StitchOutline; + stitchSettings: StitchSettings; +} + +export interface Symbols { + full?: number; + petite?: number; + half?: number; + quarter?: number; + french_knot?: number; + bead?: number; +} + +export interface SymbolSettings { + screenSpacing: [number, number]; + printerSpacing: [number, number]; + scaleUsingMaximumFontWidth: boolean; + scaleUsingFontHeight: boolean; + stitchSize: number; + smallStitchSize: number; + drawSymbolsOverBackstitches: boolean; + showStitchColor: boolean; + useLargeHalfStitchSymbol: boolean; + useTrianglesBehindQuarterStitches: boolean; +} + +export interface Formats { + symbol: SymbolFormat; + back: LineFormat; + straight: LineFormat; + french: NodeFormat; + bead: NodeFormat; + special: LineFormat; + font: FontFormat; +} + +export interface SymbolFormat { + useAltBgColor: boolean; + bgColor: String; + fgColor: String; +} + +export interface LineFormat { + useAltColor: boolean; + color: String; + style: LineStyle; + thickness: number; +} + +export const enum LineStyle { + Solid = 0, + Barred = 1, + Dotted = 2, + ChainDotted = 3, + Dashed = 4, + Outlined = 5, + Zebra = 6, + ZigZag = 7, + Morse = 8, +} + +export interface NodeFormat { + useDotStyle: boolean; + useAltColor: boolean; + color: String; + diameter: number; +} + +export interface FontFormat { + fontName?: String; + bold: boolean; + italic: boolean; + stitctSize: number; + smallStitchSize: number; +} + +export interface Grid { + majorLineEveryStitches: number; + minorScreenLines: GridLineStyle; + majorScreenLines: GridLineStyle; + minorPrinterLines: GridLineStyle; + majorPrinterLines: GridLineStyle; +} + +export interface GridLineStyle { + color: String; + thickness: number; +} + +export const enum View { + Stitches = 0, + Symbols = 1, + Solid = 2, + Information = 3, + MachineEmbInfo = 4, +} + +export interface StitchOutline { + color?: String; + colorPercentage: number; + thickness: number; +} + +export interface StitchSettings { + defaultStrands: DefaultStitchStrands; + + /** + * 1..=12 - strands, 13 - french knot. + */ + displayThickness: [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + ]; +} + +export interface DefaultStitchStrands { + full: number; + petite: number; + half: number; + quarter: number; + back: number; + straight: number; + special: number; +} diff --git a/src/types/pattern.ts b/src/types/pattern/pattern.ts similarity index 68% rename from src/types/pattern.ts rename to src/types/pattern/pattern.ts index 16d3112..865d13c 100644 --- a/src/types/pattern.ts +++ b/src/types/pattern/pattern.ts @@ -7,6 +7,8 @@ export interface Pattern { partstitches: PartStitch[]; nodes: Node[]; lines: Line[]; + specialstitches: SpecialStitch[]; + special_stitch_models: SpecialModelStitch[]; } export interface PatternProperties { @@ -17,6 +19,7 @@ export interface PatternProperties { export interface PatternInfo { title: string; author: string; + company: string; copyright: string; description: string; } @@ -27,6 +30,8 @@ export interface PaletteItem { name: string; color: string; blends?: Blend[]; + bead?: Bead; + strands: StitchStrands; } export interface Blend { @@ -35,6 +40,22 @@ export interface Blend { strands: number; } +export interface Bead { + length: number; + diameter: number; +} + +export interface StitchStrands { + full?: number; + petite?: number; + half?: number; + quarter?: number; + back?: number; + straight?: number; + french_knot?: number; + special?: number; +} + export interface Fabric { spi: [number, number]; kind: string; @@ -97,6 +118,28 @@ export const enum LineKind { Straight = 1, } +export interface SpecialStitch { + x: number; + y: number; + palindex: number; + modindex: number; +} + +export interface SpecialModelStitch { + uniqueName: string; + name: string; + width: number; + height: number; + nodes: Node[]; + lines: Line[]; + curves: Curve[]; +} + +export interface Curve { + points: [number, number][]; + palindex: number; +} + export const enum StitchKind { Full = 0, Petite = 1, diff --git a/src/types/pattern/print.ts b/src/types/pattern/print.ts new file mode 100644 index 0000000..674bf81 --- /dev/null +++ b/src/types/pattern/print.ts @@ -0,0 +1,25 @@ +export interface PrintSettings { + font: Font; + header: String; + footer: String; + margins: PageMargins; + showPageNumbers: boolean; + showAdjacentPageNumbers: boolean; + centerChartOnPages: boolean; +} + +export interface Font { + name: String; + size: number; + weight: number; + italic: boolean; +} + +export interface PageMargins { + left: number; + right: number; + top: number; + bottom: number; + header: number; + footer: number; +} diff --git a/src/types/pattern/project.ts b/src/types/pattern/project.ts new file mode 100644 index 0000000..6c9a600 --- /dev/null +++ b/src/types/pattern/project.ts @@ -0,0 +1,9 @@ +import type { DisplaySettings } from "./display"; +import type { Pattern } from "./pattern"; +import type { PrintSettings } from "./print"; + +export interface PatternProject { + pattern: Pattern; + displaySettings: DisplaySettings; + printSettings: PrintSettings; +} diff --git a/src/types/view.ts b/src/types/view.ts deleted file mode 100644 index 6269bbf..0000000 --- a/src/types/view.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface GridSettings { - majorLinesEveryStitches: number; - minorLines: GridLineStyle; - majorLines: GridLineStyle; -} - -export interface GridLineStyle { - thickness: number; - color: string; -} diff --git a/src/utils/paletteItem.test.ts b/src/utils/paletteItem.test.ts index 1228d66..b7d927f 100644 --- a/src/utils/paletteItem.test.ts +++ b/src/utils/paletteItem.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; import { blendTitle, paletteItemTitle, type PaletteItemDisplayOptions } from "./paletteItem"; -import type { Blend, PaletteItem } from "#/types/pattern"; +import type { Blend, PaletteItem } from "#/types/pattern/pattern"; const BLENDS: Blend[] = [ { @@ -20,18 +20,21 @@ const PALETTE: PaletteItem[] = [ number: "310", name: "Black", color: "2C3225", + strands: {}, }, { brand: "Anchor", number: "9159", name: "Glacier Blue", color: "B2D8E5", + strands: {}, }, { brand: "Madeira", number: "0705", name: "Plum-DK", color: "901b6b", + strands: {}, }, { brand: "Blends", @@ -39,6 +42,7 @@ const PALETTE: PaletteItem[] = [ name: "", color: "A382AE", blends: BLENDS, + strands: {}, }, ]; diff --git a/src/utils/paletteItem.ts b/src/utils/paletteItem.ts index 62380d4..c0bacdb 100644 --- a/src/utils/paletteItem.ts +++ b/src/utils/paletteItem.ts @@ -1,4 +1,4 @@ -import type { Blend, PaletteItem } from "#/types/pattern"; +import type { Blend, PaletteItem } from "#/types/pattern/pattern"; /** Interface representing display options for a palette item's title. */ export interface PaletteItemDisplayOptions {