├── .gitignore ├── .gitmodules ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── core ├── Cargo.toml ├── build.rs └── src │ ├── algorithm.rs │ ├── axis.rs │ ├── component │ ├── attrs.rs │ ├── comb.rs │ ├── mod.rs │ ├── strategy.rs │ ├── struc.rs │ └── view.rs │ ├── config │ ├── interval.rs │ ├── mod.rs │ └── process.rs │ ├── construct │ ├── data.rs │ ├── error.rs │ ├── mod.rs │ ├── space.rs │ └── types.rs │ ├── fas.rs │ ├── lib.rs │ └── service.rs └── editor ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── editor.html ├── src-tauri ├── .gitignore ├── Cargo.toml ├── build.rs ├── capabilities │ └── default.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ ├── context.rs │ ├── lib.rs │ └── main.rs └── tauri.conf.json ├── src ├── App.css ├── App.jsx ├── editor │ ├── Editor.jsx │ └── main.jsx ├── lib │ ├── action.js │ ├── construct.js │ └── storageld.js ├── main.jsx ├── theme.js └── widget │ ├── MainMenu.jsx │ ├── left │ └── Left.jsx │ ├── middle │ ├── Middle.jsx │ └── Scroll.jsx │ └── right │ └── Right.jsx └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | tmp/ 3 | */tmp/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "hanzi-jiegou"] 2 | path = hanzi-jiegou 3 | url = https://github.com/chilingg/hanzi-jiegou.git 4 | branch = dynamic 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch App Debug", 9 | "type": "cppvsdbg", 10 | "request": "launch", 11 | // change the exe name to your actual exe name 12 | // (to debug release builds, change `target/debug` to `release/debug`) 13 | "program": "${workspaceRoot}/target/debug/fasing-editor.exe", 14 | "cwd": "${workspaceRoot}", 15 | "requireExactSource": false, 16 | "preLaunchTask": "ui:dev" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.showUnlinkedFileNotification": false 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build:debug", 8 | "type": "cargo", 9 | "command": "build" 10 | }, 11 | { 12 | "label": "ui:dev", 13 | "type": "shell", 14 | // `dev` keeps running in the background 15 | // ideally you should also configure a `problemMatcher` 16 | // see https://code.visualstudio.com/docs/editor/tasks#_can-a-background-task-be-used-as-a-prelaunchtask-in-launchjson 17 | "isBackground": true, 18 | // change this to your `beforeDevCommand`: 19 | "command": "npm run dev --prefix editor", 20 | "args": [] 21 | }, 22 | { 23 | "label": "dev", 24 | "dependsOn": [ 25 | "build:debug", 26 | "ui:dev" 27 | ], 28 | "group": { 29 | "kind": "build" 30 | } 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "core", 6 | "editor/src-tauri", 7 | ] 8 | 9 | exclude = ["editor/src-tauri"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 chilingg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 繁星-动态组字系统 2 | 开发中 3 | 4 | ## 进度 5 | [知乎专栏](https://www.zhihu.com/column/c_1693641443804229633) 6 | 7 | ## 成果 8 | [余繁丹青宋](https://github.com/chilingg/YufanDanQingSong) 9 | ![余繁丹青宋](https://raw.githubusercontent.com/chilingg/YufanDanQingSong/main/img/info0.png) 10 | 11 | [余繁真素体](https://github.com/chilingg/yufanzhensu) 12 | ![余繁真素体](https://raw.githubusercontent.com/chilingg/yufanzhensu/main/img/info.png) 13 | 14 | [余繁细柳体](https://github.com/chilingg/yufanxiliu) 15 | ![余繁细柳体](https://raw.githubusercontent.com/chilingg/yufanxiliu/main/img/info1.png) -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fasing" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | 7 | [dependencies] 8 | serde_derive = "1.0.217" 9 | serde_json = "1.0.138" 10 | serde = { version = "1.0.217", features = ["derive"] } 11 | 12 | anyhow = "1.0.95" 13 | euclid = { version = "0.22.11", features = ["serde"] } 14 | num-traits = "0.2.19" 15 | regex = "1.11.1" 16 | once_cell = "1.20.3" 17 | 18 | [build-dependencies] 19 | serde_json = "1.0.138" 20 | 21 | anyhow = "1.0.95" 22 | -------------------------------------------------------------------------------- /core/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::Path; 3 | 4 | use anyhow::Result; 5 | 6 | fn process_data( 7 | value: &mut serde_json::Value, 8 | format_maps: &std::collections::HashMap, 9 | ) { 10 | if value.is_object() { 11 | let attr = value.as_object_mut().unwrap(); 12 | let mut array: Vec = vec![]; 13 | array.push(serde_json::Value::String( 14 | format_maps[attr["format"].as_str().unwrap()].clone(), 15 | )); 16 | array.push(attr.get("components").unwrap().clone()); 17 | 18 | for comp in array[1].as_array_mut().unwrap() { 19 | if comp.is_object() { 20 | process_data(comp, format_maps); 21 | } else { 22 | let comp_str = comp.as_str().unwrap().to_owned(); 23 | let mut chars = comp_str.chars(); 24 | if let Some('>') = chars.nth(1) { 25 | *comp = serde_json::Value::String(chars.next().unwrap().to_string()); 26 | } 27 | } 28 | } 29 | *value = serde_json::Value::Array(array); 30 | } 31 | } 32 | 33 | fn main() -> Result<()> { 34 | let format_maps: std::collections::HashMap = std::collections::HashMap::from([ 35 | ("单体".to_string(), "".to_string()), 36 | ("上三包围".to_string(), "⿵".to_string()), 37 | ("上下".to_string(), "⿱".to_string()), 38 | ("上中下".to_string(), "⿱".to_string()), 39 | ("下三包围".to_string(), "⿶".to_string()), 40 | ("全包围".to_string(), "⿴".to_string()), 41 | ("右上包围".to_string(), "⿹".to_string()), 42 | ("左三包围".to_string(), "⿷".to_string()), 43 | ("左上包围".to_string(), "⿸".to_string()), 44 | ("左下包围".to_string(), "⿺".to_string()), 45 | ("左中右".to_string(), "⿰".to_string()), 46 | ("左右".to_string(), "⿰".to_string()), 47 | ("右下包围".to_string(), "⿽".to_string()), 48 | ("右三包围".to_string(), "⿼".to_string()), 49 | ]); 50 | 51 | let src_path = 52 | Path::new(&env::var("CARGO_MANIFEST_DIR")?).join("../hanzi-jiegou/hanzi-jiegou.json"); 53 | println!("cargo:rerun-if-changed={}", src_path.display()); 54 | 55 | let out_dir = env::var("OUT_DIR")?; 56 | let dest_path = Path::new(&out_dir).join("fasing_1_0.json"); 57 | 58 | if src_path.exists() { 59 | let mut src_data: serde_json::Value = 60 | serde_json::from_str(&std::fs::read_to_string(src_path)?)?; 61 | src_data 62 | .as_object_mut() 63 | .unwrap() 64 | .iter_mut() 65 | .for_each(|(_, attr)| { 66 | process_data(attr, &format_maps); 67 | }); 68 | 69 | std::fs::write(dest_path, serde_json::to_string(&src_data).unwrap())?; 70 | } 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /core/src/algorithm.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | axis::{Axis, DataHV, ValueHV}, 3 | component::comb::AssignVal, 4 | construct::space::*, 5 | }; 6 | 7 | pub const NORMAL_OFFSET: f32 = 0.0001; 8 | 9 | #[derive(Debug, Clone, Copy)] 10 | enum CorrAction { 11 | Shrink, 12 | Expand, 13 | } 14 | 15 | // return (x1, x2, height, area) 16 | pub fn find_reactangle_three(length: &[usize]) -> (usize, usize, usize, usize) { 17 | if length.len() < 2 { 18 | (0, 0, 0, 0) 19 | } else { 20 | let (split_x, &min_height) = length 21 | .iter() 22 | .enumerate() 23 | .min_by_key(|(_, height)| *height) 24 | .unwrap(); 25 | let x2 = length.len() - 1; 26 | let area = x2 * min_height; 27 | 28 | let (x1_l, x2_l, height_l, area_l) = find_reactangle_three(&length[..split_x]); 29 | 30 | let (x1_r, x2_r, height_r, area_r) = find_reactangle_three(&length[split_x + 1..]); 31 | 32 | if area >= area_r { 33 | if area >= area_l { 34 | (0, x2, min_height, area) 35 | } else { 36 | (x1_l, x2_l, height_l, area_l) 37 | } 38 | } else { 39 | if area_r > area_l { 40 | (x1_r + split_x + 1, x2_r + split_x + 1, height_r, area_r) 41 | } else { 42 | (x1_l, x2_l, height_l, area_l) 43 | } 44 | } 45 | } 46 | } 47 | 48 | pub fn intersection( 49 | p11: WorkPoint, 50 | p12: WorkPoint, 51 | p21: WorkPoint, 52 | p22: WorkPoint, 53 | ) -> Option<(WorkPoint, [f32; 2])> { 54 | fn offset_corection(val: f32) -> f32 { 55 | if val > 0.0 - NORMAL_OFFSET && val < 1.0 + NORMAL_OFFSET { 56 | val.max(0.0).min(1.0) 57 | } else { 58 | val 59 | } 60 | } 61 | 62 | let [x1, y1] = p11.to_array(); 63 | let [x2, y2] = p12.to_array(); 64 | let [x3, y3] = p21.to_array(); 65 | let [x4, y4] = p22.to_array(); 66 | 67 | let a1 = x2 - x1; 68 | let b1 = y2 - y1; 69 | let a2 = x4 - x3; 70 | let b2 = y4 - y3; 71 | 72 | if a1 * b2 == b1 * a2 { 73 | return None; 74 | } else { 75 | let t1 = offset_corection(((x1 - x3) * b2 - a2 * (y1 - y3)) / (b1 * a2 - a1 * b2)); 76 | if t1 >= 0.0 && t1 <= 1.0 { 77 | let t2 = offset_corection(((x3 - x1) * b1 - (y3 - y1) * a1) / (b2 * a1 - a2 * b1)); 78 | if t2 >= 0.0 && t2 <= 1.0 { 79 | Some(( 80 | WorkPoint::new(x1 + t1 * (x2 - x1), y1 + t1 * (y2 - y1)), 81 | [t1, t2], 82 | )) 83 | } else { 84 | None 85 | } 86 | } else { 87 | None 88 | } 89 | } 90 | } 91 | 92 | pub fn split_intersect(paths: &mut Vec, min_len: f32) { 93 | let min_len_square = min_len.powi(2); 94 | let mut paths: Vec<_> = paths.iter_mut().map(|path| &mut path.points).collect(); 95 | 96 | for i in (1..paths.len()).rev() { 97 | for j in 0..i { 98 | let mut p1_i = 1; 99 | while p1_i < paths[i].len() { 100 | let p11 = paths[i][p1_i - 1]; 101 | let mut p12 = paths[i][p1_i]; 102 | 103 | let mut p2_i = 1; 104 | while p2_i < paths[j].len() { 105 | let p21 = paths[j][p2_i - 1]; 106 | let p22 = paths[j][p2_i]; 107 | 108 | match intersection(p11, p12, p21, p22) { 109 | Some((new_point, t)) => { 110 | let p = new_point; 111 | 112 | if 0.0 < t[0] 113 | && t[0] < 1.0 114 | && (p11.x - p.x).powi(2) + (p11.y - p.y).powi(2) + NORMAL_OFFSET 115 | >= min_len_square 116 | && (p.x - p12.x).powi(2) + (p.y - p12.y).powi(2) + NORMAL_OFFSET 117 | >= min_len_square 118 | { 119 | p12 = p; 120 | paths[i].insert(p1_i, p); 121 | } 122 | if 0.0 < t[1] 123 | && t[1] < 1.0 124 | && (p21.x - p.x).powi(2) + (p21.y - p.y).powi(2) + NORMAL_OFFSET 125 | >= min_len_square 126 | && (p.x - p22.x).powi(2) + (p.y - p22.y).powi(2) + NORMAL_OFFSET 127 | >= min_len_square 128 | { 129 | paths[j].insert(p2_i, p); 130 | p2_i += 1; 131 | } 132 | } 133 | _ => {} 134 | } 135 | p2_i += 1; 136 | } 137 | p1_i += 1; 138 | } 139 | } 140 | } 141 | } 142 | 143 | pub fn visual_center(paths: &Vec) -> WorkPoint { 144 | let mut size = DataHV::splat([f32::MAX, f32::MIN]); 145 | let (pos, count) = 146 | paths 147 | .iter() 148 | .fold((DataHV::splat(0.0), 0.0), |(mut pos, mut count), path| { 149 | path.points 150 | .iter() 151 | .zip(path.points.iter().skip(1)) 152 | .for_each(|(kp1, kp2)| { 153 | Axis::list().into_iter().for_each(|axis| { 154 | let val1 = *kp1.hv_get(axis); 155 | let val2 = *kp2.hv_get(axis); 156 | 157 | if !path.hide { 158 | *pos.hv_get_mut(axis) += (val1 + val2) * 0.5; 159 | } 160 | 161 | let len = size.hv_get_mut(axis); 162 | len[0] = len[0].min(val1).min(val2); 163 | len[1] = len[1].max(val1).max(val2); 164 | }); 165 | 166 | if !path.hide { 167 | count += 1.0; 168 | } 169 | }); 170 | 171 | (pos, count) 172 | }); 173 | 174 | let center = pos 175 | .zip(size) 176 | .into_map(|(v, len)| { 177 | let l = len[1] - len[0]; 178 | if count == 0.0 || l <= 0.0 { 179 | 0.0 180 | } else { 181 | let center = (v / count - len[0]) / l; 182 | if (center - 0.5).abs() < NORMAL_OFFSET { 183 | 0.5 184 | } else { 185 | center 186 | } 187 | } 188 | }) 189 | .to_array() 190 | .into(); 191 | 192 | center 193 | } 194 | 195 | pub fn visual_center_length( 196 | mut paths: Vec, 197 | min_len: f32, 198 | stroke_width: f32, 199 | ) -> WorkPoint { 200 | split_intersect(&mut paths, min_len); 201 | 202 | let mut size = DataHV::splat([f32::MAX, f32::MIN]); 203 | let (pos, count) = 204 | paths 205 | .iter() 206 | .fold((DataHV::splat(0.0), 0.0), |(mut pos, mut count), path| { 207 | path.points 208 | .iter() 209 | .zip(path.points.iter().skip(1)) 210 | .for_each(|(&kp1, &kp2)| { 211 | let length = (kp1 - kp2).length() + stroke_width; 212 | Axis::list().into_iter().for_each(|axis| { 213 | let val1 = *kp1.hv_get(axis); 214 | let val2 = *kp2.hv_get(axis); 215 | 216 | if !path.hide { 217 | *pos.hv_get_mut(axis) += (val1 + val2) * 0.5 * length; 218 | } 219 | 220 | let len = size.hv_get_mut(axis); 221 | len[0] = len[0].min(val1).min(val2); 222 | len[1] = len[1].max(val1).max(val2); 223 | }); 224 | 225 | if !path.hide { 226 | count += length; 227 | } 228 | }); 229 | 230 | (pos, count) 231 | }); 232 | 233 | let center = pos 234 | .zip(size) 235 | .into_map(|(v, len)| { 236 | let l = len[1] - len[0]; 237 | if count == 0.0 || l <= 0.0 { 238 | 0.0 239 | } else { 240 | let center = (v / count - len[0]) / l; 241 | if (center - 0.5).abs() < NORMAL_OFFSET { 242 | 0.5 243 | } else { 244 | center 245 | } 246 | } 247 | }) 248 | .to_array() 249 | .into(); 250 | 251 | center 252 | } 253 | 254 | fn base_value_correction( 255 | bases: &Vec, 256 | mut values: Vec, 257 | split_index: usize, 258 | action: CorrAction, 259 | ) -> Vec { 260 | let process = |mut difference: f32, (v, &b): (&mut f32, &f32)| { 261 | let v_abs = v.abs(); 262 | let b_abs = b.abs(); 263 | if v_abs < b_abs { 264 | difference += b_abs - v_abs; 265 | *v = 0.0; 266 | } else { 267 | let allowance = v_abs - b_abs; 268 | if allowance >= difference { 269 | *v = (allowance - difference) * v.signum(); 270 | difference = 0.0; 271 | } else { 272 | difference -= allowance; 273 | *v = 0.0; 274 | } 275 | } 276 | difference 277 | }; 278 | 279 | match action { 280 | CorrAction::Shrink => { 281 | values[0..split_index] 282 | .iter_mut() 283 | .zip(bases[0..split_index].iter()) 284 | .rev() 285 | .fold(0.0, process); 286 | values[split_index..] 287 | .iter_mut() 288 | .zip(bases[split_index..].iter()) 289 | .fold(0.0, process); 290 | } 291 | CorrAction::Expand => { 292 | values[0..split_index] 293 | .iter_mut() 294 | .zip(bases[0..split_index].iter()) 295 | .fold(0.0, process); 296 | values[split_index..] 297 | .iter_mut() 298 | .zip(bases[split_index..].iter()) 299 | .rev() 300 | .fold(0.0, process); 301 | } 302 | } 303 | 304 | values 305 | } 306 | 307 | pub fn scale_correction(vlist: &mut Vec, assign: f32) -> bool { 308 | let old_assign: AssignVal = vlist.iter().sum(); 309 | if old_assign.base > assign { 310 | vlist.iter_mut().for_each(|v| v.excess = 0.0); 311 | false 312 | } else { 313 | let total = old_assign.total(); 314 | let scale = assign / total; 315 | let mut debt = 0.0; 316 | vlist.iter_mut().for_each(|v| { 317 | v.excess = v.total() * scale - v.base; 318 | if v.excess < 0.0 { 319 | debt -= v.excess; 320 | v.excess = 0.0; 321 | } 322 | }); 323 | 324 | while debt != 0.0 { 325 | let targets: Vec<_> = vlist.iter_mut().filter(|v| v.excess != 0.0).collect(); 326 | let sub_val = debt / targets.len() as f32; 327 | debt = 0.0; 328 | targets.into_iter().for_each(|v| { 329 | v.excess -= sub_val; 330 | if v.excess < 0.0 { 331 | debt -= v.excess; 332 | v.excess = 0.0; 333 | } 334 | }); 335 | } 336 | true 337 | } 338 | } 339 | 340 | // center & target: range = 0..1 341 | // deviation: range = -1..1 342 | pub fn center_correction( 343 | vlist: &Vec, 344 | bases: &Vec, 345 | center: f32, 346 | target: f32, 347 | corr_val: f32, 348 | ) -> Vec { 349 | if center == 0.0 { 350 | return vlist.iter().zip(bases).map(|(v, a)| v - a).collect(); 351 | } 352 | 353 | let total = vlist.iter().sum::(); 354 | let split_val = total * center; 355 | let deviation = { 356 | let (target, corr_val) = if corr_val < 0.0 { 357 | let mut t = (target - 0.5).abs(); 358 | t = if center <= 0.5 { 0.5 - t } else { 0.5 + t }; 359 | (t, -corr_val) 360 | } else { 361 | (target, corr_val) 362 | }; 363 | 364 | match target - center { 365 | v if v.is_sign_negative() => v / center * corr_val, 366 | v => v / (1.0 - center) * corr_val, 367 | } 368 | }; 369 | 370 | let (l_ratio, r_ratio) = if deviation.is_sign_negative() { 371 | let ratio = deviation + 1.0; 372 | (ratio, (1.0 - center * ratio) / (1.0 - center)) 373 | } else { 374 | let ratio = 1.0 - deviation; 375 | ((1.0 - (1.0 - center) * ratio) / center, ratio) 376 | }; 377 | 378 | let mut advance = 0.0; 379 | let mut pre = 0.0; 380 | let r = vlist 381 | .iter() 382 | .map(|&v| { 383 | advance += v; 384 | advance 385 | }) 386 | .map(|v| { 387 | let new_val = if v < split_val { 388 | v * l_ratio - pre 389 | } else { 390 | (v - split_val) * r_ratio + split_val * l_ratio - pre 391 | }; 392 | pre += new_val; 393 | new_val 394 | }) 395 | .collect(); 396 | 397 | match deviation.partial_cmp(&0.0).unwrap() { 398 | std::cmp::Ordering::Greater => base_value_correction(bases, r, 0, CorrAction::Expand), 399 | std::cmp::Ordering::Less => base_value_correction(bases, r, 0, CorrAction::Shrink), 400 | std::cmp::Ordering::Equal => base_value_correction(bases, r, 0, CorrAction::Shrink), 401 | } 402 | } 403 | 404 | #[cfg(test)] 405 | mod tests { 406 | use super::*; 407 | 408 | #[test] 409 | fn test_visual_center_in_split() { 410 | let mut paths = vec![ 411 | KeyWorkPath::from([WorkPoint::new(1.0, 0.0), WorkPoint::new(1.0, 2.0)]), 412 | KeyWorkPath { 413 | points: vec![WorkPoint::new(0.0, 1.0), WorkPoint::new(2.0, 1.0)], 414 | hide: true, 415 | }, 416 | ]; 417 | split_intersect(&mut paths, 0.0); 418 | assert_eq!(paths[0].points.len(), 3); 419 | assert_eq!(paths[0].points.len(), paths[1].points.len()); 420 | let center = visual_center(&paths); 421 | assert_eq!(center, WorkPoint::new(0.5, 0.5)); 422 | 423 | let mut paths = vec![ 424 | KeyWorkPath::from([WorkPoint::new(1.0, 0.0), WorkPoint::new(4.0, 2.0)]), 425 | KeyWorkPath::from([WorkPoint::new(0.0, 2.0), WorkPoint::new(3.0, 0.0)]), 426 | ]; 427 | split_intersect(&mut paths, 0.0); 428 | assert_eq!(paths[0].points.len(), 3); 429 | assert_eq!(paths[0].points.len(), paths[1].points.len()); 430 | let center = visual_center(&paths); 431 | assert_eq!(center.x, 0.5); 432 | assert!(center.y < 0.5); 433 | 434 | let mut paths = vec![ 435 | KeyWorkPath::from([WorkPoint::new(1.0, 0.0), WorkPoint::new(1.0, 2.0)]), 436 | KeyWorkPath::from([WorkPoint::new(0.0, 1.0), WorkPoint::new(2.0, 1.0)]), 437 | ]; 438 | split_intersect(&mut paths, 1.1); 439 | assert_eq!(paths[0].points.len(), 2); 440 | assert_eq!(paths[0].points.len(), paths[1].points.len()); 441 | } 442 | 443 | #[test] 444 | fn test_scale_correction() { 445 | fn check_eq(a: f32, b: f32) -> bool { 446 | (a - b).abs() < NORMAL_OFFSET 447 | } 448 | 449 | let mut list = vec![ 450 | AssignVal::new(1.0, 3.0), 451 | AssignVal::new(2.0, 2.0), 452 | AssignVal::new(3.0, 1.0), 453 | ]; 454 | 455 | scale_correction(&mut list, 9.0); 456 | assert!(check_eq(list.iter().sum::().total(), 9.0)); 457 | assert!(check_eq(list[0].excess, 2.0)); 458 | assert!(check_eq(list[1].excess, 1.0)); 459 | assert!(check_eq(list[2].excess, 0.0)); 460 | 461 | scale_correction(&mut list, 8.0); 462 | assert!(check_eq(list.iter().sum::().total(), 8.0)); 463 | assert!(check_eq(list[0].excess, 1.5)); 464 | assert!(check_eq(list[1].excess, 0.5)); 465 | assert!(check_eq(list[2].excess, 0.0)); 466 | 467 | scale_correction(&mut list, 4.0); 468 | assert!(check_eq(list.iter().sum::().total(), 6.0)); 469 | assert!(check_eq(list[0].excess, 0.0)); 470 | assert!(check_eq(list[1].excess, 0.0)); 471 | assert!(check_eq(list[2].excess, 0.0)); 472 | 473 | scale_correction(&mut list, 12.0); 474 | assert!(check_eq(list.iter().sum::().total(), 12.0)); 475 | assert!(check_eq(list[0].excess, 1.0)); 476 | assert!(check_eq(list[1].excess, 2.0)); 477 | assert!(check_eq(list[2].excess, 3.0)); 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /core/src/axis.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, PartialOrd, Ord)] 4 | pub enum Axis { 5 | Horizontal, 6 | Vertical, 7 | } 8 | 9 | impl Axis { 10 | pub fn inverse(&self) -> Self { 11 | match self { 12 | Axis::Horizontal => Axis::Vertical, 13 | Axis::Vertical => Axis::Horizontal, 14 | } 15 | } 16 | 17 | pub fn list() -> [Axis; 2] { 18 | [Axis::Horizontal, Axis::Vertical] 19 | } 20 | 21 | pub fn hv() -> DataHV { 22 | DataHV { 23 | h: Axis::Horizontal, 24 | v: Axis::Vertical, 25 | } 26 | } 27 | } 28 | 29 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Debug)] 30 | pub enum Place { 31 | Start, 32 | Middle, 33 | End, 34 | } 35 | 36 | impl Place { 37 | pub fn from_range(val: T, range: std::ops::RangeInclusive) -> Self { 38 | if !range.contains(&val) { 39 | panic!("The value is not within the range!"); 40 | } else { 41 | if range.start().eq(&val) { 42 | Self::Start 43 | } else if range.end().eq(&val) { 44 | Self::End 45 | } else { 46 | Self::Middle 47 | } 48 | } 49 | } 50 | 51 | pub fn inverse(&self) -> Self { 52 | match self { 53 | Self::Start => Self::End, 54 | Self::Middle => Self::Middle, 55 | Self::End => Self::Start, 56 | } 57 | } 58 | 59 | pub fn start_and_end() -> [Place; 2] { 60 | [Place::Start, Place::End] 61 | } 62 | } 63 | 64 | #[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] 65 | pub struct DataHV { 66 | pub h: T, 67 | pub v: T, 68 | } 69 | 70 | impl From<(T, T)> for DataHV { 71 | fn from(value: (T, T)) -> Self { 72 | Self { 73 | h: value.0, 74 | v: value.1, 75 | } 76 | } 77 | } 78 | 79 | impl Copy for DataHV {} 80 | 81 | impl DataHV { 82 | pub fn vh(&mut self) -> &mut Self { 83 | std::mem::swap(&mut self.h, &mut self.v); 84 | self 85 | } 86 | } 87 | 88 | impl DataHV { 89 | pub fn to_vh(self) -> Self { 90 | Self { 91 | h: self.v, 92 | v: self.h, 93 | } 94 | } 95 | } 96 | 97 | impl DataHV { 98 | pub fn new(h: T, v: T) -> Self { 99 | Self { h, v } 100 | } 101 | 102 | pub fn splat(val: T) -> Self 103 | where 104 | T: Clone, 105 | { 106 | Self { 107 | h: val.clone(), 108 | v: val, 109 | } 110 | } 111 | 112 | pub fn in_axis(&self, f: F) -> Option 113 | where 114 | F: Fn(&T) -> bool, 115 | { 116 | if f(&self.h) { 117 | Some(Axis::Horizontal) 118 | } else if f(&self.v) { 119 | Some(Axis::Vertical) 120 | } else { 121 | None 122 | } 123 | } 124 | 125 | pub fn map(&self, f: F) -> DataHV 126 | where 127 | F: Fn(&T) -> T2, 128 | { 129 | DataHV { 130 | h: f(&self.h), 131 | v: f(&self.v), 132 | } 133 | } 134 | 135 | pub fn into_map(self, mut f: F) -> DataHV 136 | where 137 | F: FnMut(T) -> T2, 138 | { 139 | DataHV { 140 | h: f(self.h), 141 | v: f(self.v), 142 | } 143 | } 144 | 145 | pub fn zip<'a, T2>(self, other: DataHV) -> DataHV<(T, T2)> { 146 | DataHV { 147 | h: (self.h, other.h), 148 | v: (self.v, other.v), 149 | } 150 | } 151 | 152 | pub fn as_ref(&self) -> DataHV<&T> { 153 | DataHV { 154 | h: &self.h, 155 | v: &self.v, 156 | } 157 | } 158 | 159 | pub fn as_mut(&mut self) -> DataHV<&mut T> { 160 | DataHV { 161 | h: &mut self.h, 162 | v: &mut self.v, 163 | } 164 | } 165 | 166 | pub fn into_iter(self) -> std::array::IntoIter { 167 | [self.h, self.v].into_iter() 168 | } 169 | 170 | pub fn to_array(self) -> [T; 2] { 171 | [self.h, self.v] 172 | } 173 | } 174 | 175 | impl DataHV<(T, U)> { 176 | pub fn unzip(self) -> (DataHV, DataHV) { 177 | ( 178 | DataHV { 179 | h: self.h.0, 180 | v: self.v.0, 181 | }, 182 | DataHV { 183 | h: self.h.1, 184 | v: self.v.1, 185 | }, 186 | ) 187 | } 188 | } 189 | 190 | pub trait ValueHV { 191 | fn hv_get(&self, axis: Axis) -> &T; 192 | fn hv_get_mut(&mut self, axis: Axis) -> &mut T; 193 | 194 | fn hv_iter(&self) -> std::array::IntoIter<&T, 2> { 195 | [self.hv_get(Axis::Horizontal), self.hv_get(Axis::Vertical)].into_iter() 196 | } 197 | 198 | fn hv_axis_iter(&self) -> std::array::IntoIter<(Axis, &T), 2> { 199 | [ 200 | (Axis::Horizontal, self.hv_get(Axis::Horizontal)), 201 | (Axis::Vertical, self.hv_get(Axis::Vertical)), 202 | ] 203 | .into_iter() 204 | } 205 | 206 | fn to_hv_data(&self) -> DataHV 207 | where 208 | T: Clone, 209 | { 210 | DataHV { 211 | h: self.hv_get(Axis::Horizontal).clone(), 212 | v: self.hv_get(Axis::Vertical).clone(), 213 | } 214 | } 215 | } 216 | 217 | impl ValueHV for DataHV { 218 | fn hv_get(&self, axis: Axis) -> &T { 219 | match axis { 220 | Axis::Horizontal => &self.h, 221 | Axis::Vertical => &self.v, 222 | } 223 | } 224 | 225 | fn hv_get_mut(&mut self, axis: Axis) -> &mut T { 226 | match axis { 227 | Axis::Horizontal => &mut self.h, 228 | Axis::Vertical => &mut self.v, 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /core/src/component/attrs.rs: -------------------------------------------------------------------------------- 1 | use crate::{axis::*, config::interval::IntervalRule, construct::space::*}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | extern crate serde_json as sj; 5 | 6 | use std::collections::{BTreeMap, BTreeSet}; 7 | 8 | pub trait CompAttrData { 9 | type Data; 10 | 11 | fn key() -> &'static str; 12 | 13 | fn from_sj_value(attr: sj::Value) -> Option 14 | where 15 | Self::Data: serde::de::DeserializeOwned, 16 | { 17 | match sj::from_value::(attr) { 18 | Ok(data) => Some(data), 19 | Err(e) => { 20 | eprintln!("Error parsing attributes `{}`: \n{}", Self::key(), e); 21 | None 22 | } 23 | } 24 | } 25 | 26 | fn to_sj_value(attr: &Self::Data) -> Option 27 | where 28 | Self::Data: serde::Serialize, 29 | { 30 | match sj::to_value(attr) { 31 | Ok(data) => Some(data), 32 | Err(e) => { 33 | eprintln!("Error attributes `{}`: \n{}", Self::key(), e); 34 | None 35 | } 36 | } 37 | } 38 | } 39 | 40 | #[derive(Serialize, Deserialize, Default, Clone)] 41 | pub struct CompAttrs(BTreeMap); 42 | 43 | impl CompAttrs { 44 | pub fn get(&self) -> Option<::Data> 45 | where 46 | ::Data: serde::de::DeserializeOwned, 47 | { 48 | self.0 49 | .get(T::key()) 50 | .and_then(|v| ::from_sj_value(v.clone())) 51 | } 52 | 53 | pub fn set(&mut self, attr: &T::Data) 54 | where 55 | ::Data: serde::Serialize, 56 | { 57 | if let Some(value) = T::to_sj_value(attr) { 58 | self.0.insert(T::key().to_string(), value); 59 | } 60 | } 61 | } 62 | 63 | pub struct Allocs; 64 | impl CompAttrData for Allocs { 65 | type Data = DataHV>; 66 | fn key() -> &'static str { 67 | "allocs" 68 | } 69 | } 70 | 71 | pub struct Adjacencies; 72 | impl CompAttrData for Adjacencies { 73 | type Data = DataHV<[bool; 2]>; 74 | fn key() -> &'static str { 75 | "adjacencies" 76 | } 77 | } 78 | 79 | pub struct InPlaceAllocs; 80 | impl CompAttrData for InPlaceAllocs { 81 | type Data = Vec<(String, DataHV>)>; 82 | fn key() -> &'static str { 83 | "in_place" 84 | } 85 | } 86 | 87 | pub struct CharBox; 88 | impl CompAttrData for CharBox { 89 | type Data = WorkBox; 90 | fn key() -> &'static str { 91 | "char_box" 92 | } 93 | 94 | fn from_sj_value(attr: serde_json::Value) -> Option 95 | where 96 | Self::Data: serde::de::DeserializeOwned, 97 | { 98 | if let Some(cbox_str) = attr.as_str() { 99 | match cbox_str { 100 | "left" => Some(WorkBox::new( 101 | WorkPoint::new(0.0, 0.0), 102 | WorkPoint::new(0.5, 1.0), 103 | )), 104 | "right" => Some(WorkBox::new( 105 | WorkPoint::new(0.5, 0.0), 106 | WorkPoint::new(1.0, 1.0), 107 | )), 108 | "top" => Some(WorkBox::new( 109 | WorkPoint::new(0.0, 0.0), 110 | WorkPoint::new(1.0, 0.5), 111 | )), 112 | "bottom" => Some(WorkBox::new( 113 | WorkPoint::new(0.0, 0.5), 114 | WorkPoint::new(1.0, 1.0), 115 | )), 116 | _ => { 117 | eprintln!("Unknown character box label: {}", cbox_str); 118 | None 119 | } 120 | } 121 | } else if let Ok(cbox) = serde_json::from_value::(attr) { 122 | Some(cbox) 123 | } else { 124 | None 125 | } 126 | } 127 | } 128 | 129 | pub struct ReduceAlloc; 130 | impl CompAttrData for ReduceAlloc { 131 | type Data = DataHV>>; 132 | fn key() -> &'static str { 133 | "reduce_alloc" 134 | } 135 | } 136 | 137 | pub struct PresetCenter; 138 | impl CompAttrData for PresetCenter { 139 | type Data = DataHV>; 140 | fn key() -> &'static str { 141 | "preset_center" 142 | } 143 | 144 | fn from_sj_value(attr: serde_json::Value) -> Option 145 | where 146 | Self::Data: serde::de::DeserializeOwned, 147 | { 148 | attr.as_object().map(|data| { 149 | DataHV::new( 150 | data.get("h").and_then(|v| v.as_f64().map(|f| f as f32)), 151 | data.get("v").and_then(|v| v.as_f64().map(|f| f as f32)), 152 | ) 153 | }) 154 | } 155 | 156 | fn to_sj_value(attr: &Self::Data) -> Option 157 | where 158 | Self::Data: serde::Serialize, 159 | { 160 | let mut data = sj::Map::new(); 161 | if let Some(val) = attr.h { 162 | data.insert("h".to_string(), sj::json!(val)); 163 | } 164 | if let Some(val) = attr.v { 165 | data.insert("v".to_string(), sj::json!(val)); 166 | } 167 | Some(sj::Value::Object(data)) 168 | } 169 | } 170 | 171 | #[derive(Serialize, Deserialize)] 172 | pub struct IntervalAlloc { 173 | pub interval: usize, 174 | pub rules: Vec, 175 | pub allocs: DataHV>, 176 | } 177 | 178 | impl CompAttrData for IntervalAlloc { 179 | type Data = BTreeMap>; 180 | fn key() -> &'static str { 181 | "interval_alloc" 182 | } 183 | } 184 | 185 | pub struct FixedAlloc; 186 | impl CompAttrData for FixedAlloc { 187 | type Data = DataHV>; 188 | fn key() -> &'static str { 189 | "fixed_Alloc" 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /core/src/component/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod attrs; 2 | pub mod comb; 3 | pub mod strategy; 4 | pub mod struc; 5 | pub mod view; 6 | -------------------------------------------------------------------------------- /core/src/component/strategy.rs: -------------------------------------------------------------------------------- 1 | #[derive( 2 | Clone, Copy, Hash, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord, 3 | )] 4 | pub enum PlaceMain { 5 | NoPlane, 6 | NonLess, 7 | Equal, 8 | Acute, 9 | AlignPlane, 10 | Contain, 11 | InContain, 12 | Both, 13 | Second, 14 | 15 | Only, 16 | Surround, 17 | BeSurround, 18 | NoSurround, 19 | NoBeSurround, 20 | } 21 | -------------------------------------------------------------------------------- /core/src/component/struc.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | algorithm, 3 | axis::*, 4 | component::attrs::{self, CompAttrs}, 5 | config::place_match, 6 | construct::space::*, 7 | }; 8 | 9 | use serde::{Deserialize, Serialize}; 10 | use std::collections::{BTreeMap, BTreeSet}; 11 | 12 | #[derive(Default, Serialize, Deserialize, Clone)] 13 | pub struct StrucProto { 14 | pub paths: Vec, 15 | pub attrs: CompAttrs, 16 | } 17 | 18 | impl> From for StrucProto { 19 | fn from(paths: T) -> Self { 20 | StrucProto { 21 | paths: paths.into_iter().collect(), 22 | attrs: Default::default(), 23 | } 24 | } 25 | } 26 | 27 | impl StrucProto { 28 | fn values(&self) -> DataHV> { 29 | self.paths 30 | .iter() 31 | .fold(DataHV::>::default(), |mut set, path| { 32 | path.points.iter().for_each(|p| { 33 | for axis in Axis::list() { 34 | set.hv_get_mut(axis).insert(*p.hv_get(axis)); 35 | } 36 | }); 37 | set 38 | }) 39 | .into_map(|set| set.into_iter().collect()) 40 | } 41 | 42 | pub fn values_map(&self) -> DataHV> { 43 | let values = self.values(); 44 | match self.attrs.get::() { 45 | Some(allocs) => values.zip(allocs).into_map(|(values, allocs)| { 46 | let mut sum = 0; 47 | values 48 | .into_iter() 49 | .zip(std::iter::once(0).chain(allocs)) 50 | .map(|(v, a)| { 51 | sum += a; 52 | (v, sum) 53 | }) 54 | .collect() 55 | }), 56 | None => values.into_map(|values| values.iter().map(|v| (*v, *v - values[0])).collect()), 57 | } 58 | } 59 | 60 | pub fn value_index_in_axis(&self, vals: &[usize], axis: Axis) -> Vec { 61 | let mut values: Vec = self.values_map().hv_get(axis).values().copied().collect(); 62 | values.dedup(); 63 | vals.iter() 64 | .map(|v| values.iter().position(|x| x == v).unwrap()) 65 | .collect() 66 | } 67 | 68 | pub fn allocation_values(&self) -> DataHV> { 69 | self.attrs.get::().unwrap_or_else(|| { 70 | self.values().into_map(|values| { 71 | values 72 | .iter() 73 | .zip(values.iter().skip(1)) 74 | .map(|(&n1, &n2)| n2 - n1) 75 | .collect() 76 | }) 77 | }) 78 | } 79 | 80 | pub fn allocation_space(&self) -> DataHV> { 81 | self.allocation_values() 82 | .into_map(|l| l.into_iter().filter(|n| *n != 0).collect()) 83 | } 84 | 85 | pub fn set_allocs_in_adjacency(&mut self, adjacencies: DataHV<[bool; 2]>) { 86 | let mut allocs_proto = self.allocation_values(); 87 | if let Some(ipa) = self.attrs.get::() { 88 | ipa.into_iter() 89 | .filter_map(|(rule, allocs)| match place_match(&rule, adjacencies) { 90 | true => Some(allocs), 91 | false => None, 92 | }) 93 | .for_each(|allocs| { 94 | Axis::list().into_iter().for_each(|axis| { 95 | allocs_proto 96 | .hv_get_mut(axis) 97 | .iter_mut() 98 | .zip(allocs.hv_get(axis)) 99 | .for_each(|(val, exp)| { 100 | if *val > *exp { 101 | *val = *exp 102 | } 103 | }) 104 | }); 105 | }); 106 | } 107 | 108 | self.attrs.set::(&adjacencies); 109 | self.attrs.set::(&allocs_proto); 110 | } 111 | 112 | pub fn reduce(&mut self, axis: Axis, check: bool) -> bool { 113 | let mut ok = false; 114 | let mut allocs = self.allocation_values(); 115 | if let Some(reduce_list) = self.attrs.get::() { 116 | let fiexd_alloc = self.attrs.get::().unwrap_or_default(); 117 | 118 | reduce_list.hv_get(axis).iter().find(|rl| { 119 | for (i, (r, l)) in rl 120 | .iter() 121 | .zip(allocs.hv_get_mut(axis).iter_mut()) 122 | .enumerate() 123 | { 124 | if !fiexd_alloc.hv_get(axis).contains(&i) && *r < *l { 125 | if !check { 126 | *l -= 1; 127 | } 128 | ok = true; 129 | } 130 | } 131 | ok 132 | }); 133 | } 134 | 135 | if ok { 136 | self.attrs.set::(&allocs); 137 | } 138 | ok 139 | } 140 | 141 | pub fn to_path_in_range( 142 | &self, 143 | start: WorkPoint, 144 | assigns: DataHV>, 145 | range: DataHV>>, 146 | ) -> Vec { 147 | let range = Axis::hv().into_map(|axis| { 148 | let r = range.hv_get(axis).clone().unwrap_or_else(|| { 149 | let o = *start.hv_get(axis); 150 | o..=(o + assigns.hv_get(axis).iter().sum::()) 151 | }); 152 | (*r.start(), *r.end()) 153 | }); 154 | let mut paths = self.to_paths(start, assigns); 155 | 156 | paths.push(KeyWorkPath { 157 | points: vec![ 158 | WorkPoint::new(range.h.0, range.v.0), 159 | WorkPoint::new(range.h.1, range.v.0), 160 | WorkPoint::new(range.h.1, range.v.1), 161 | WorkPoint::new(range.h.0, range.v.1), 162 | WorkPoint::new(range.h.0, range.v.0), 163 | ], 164 | hide: true, 165 | }); 166 | 167 | algorithm::split_intersect(&mut paths, 0.); 168 | paths.pop(); 169 | 170 | paths 171 | .into_iter() 172 | .map(|path| { 173 | let mut new_paths = vec![]; 174 | let mut outside = true; 175 | path.points.into_iter().for_each(|pos| { 176 | if pos.x >= range.h.0 177 | && pos.x <= range.h.1 178 | && pos.y >= range.v.0 179 | && pos.y <= range.v.1 180 | { 181 | if outside { 182 | new_paths.push(vec![]); 183 | outside = false 184 | } 185 | new_paths.last_mut().unwrap().push(pos); 186 | } else { 187 | outside = true; 188 | } 189 | }); 190 | new_paths 191 | .into_iter() 192 | .map(|new_path| KeyWorkPath { 193 | points: new_path, 194 | hide: path.hide, 195 | }) 196 | .collect::>() 197 | }) 198 | .flatten() 199 | .filter(|path| path.points.len() > 1) 200 | .collect() 201 | } 202 | 203 | pub fn to_path_in_index( 204 | &self, 205 | start: WorkPoint, 206 | assigns: DataHV>, 207 | range: DataHV>>, 208 | ) -> Vec { 209 | let alloc_to_assign: DataHV> = assigns 210 | .clone() 211 | .zip(self.allocation_space()) 212 | .into_map(|(assigns, allocs)| { 213 | let mut origin = (0, 0.0); 214 | std::iter::once(origin) 215 | .chain(allocs.into_iter().zip(assigns).map(|(alloc, assig)| { 216 | origin.0 += alloc; 217 | origin.1 += assig; 218 | origin 219 | })) 220 | .collect() 221 | }); 222 | 223 | let range = range 224 | .zip(alloc_to_assign) 225 | .into_map(|(range, map)| range.map(|range| map[range.start()]..=map[range.end()])); 226 | 227 | self.to_path_in_range(start, assigns, range) 228 | } 229 | 230 | pub fn to_paths(&self, start: WorkPoint, assigns: DataHV>) -> Vec { 231 | let pos_to_alloc = self.values_map(); 232 | let alloc_to_assign: DataHV> = start 233 | .to_hv_data() 234 | .zip(assigns) 235 | .zip(self.allocation_space()) 236 | .into_map(|((start, assigns), allocs)| { 237 | let mut origin = (0, start); 238 | std::iter::once(origin) 239 | .chain(allocs.into_iter().zip(assigns).map(|(alloc, assig)| { 240 | origin.0 += alloc; 241 | origin.1 += assig; 242 | origin 243 | })) 244 | .collect() 245 | }); 246 | self.paths 247 | .iter() 248 | .map(|path| { 249 | let points = path 250 | .points 251 | .iter() 252 | .map(|p| { 253 | let pos = p 254 | .to_hv_data() 255 | .zip(pos_to_alloc.as_ref()) 256 | .zip(alloc_to_assign.as_ref()) 257 | .into_map(|((v, m1), m2)| m2[&m1[&v]]); 258 | WorkPoint::new(pos.h, pos.v) 259 | }) 260 | .collect(); 261 | 262 | KeyWorkPath { 263 | points, 264 | hide: path.hide, 265 | } 266 | }) 267 | .collect() 268 | } 269 | } 270 | 271 | #[cfg(test)] 272 | mod tests { 273 | use super::*; 274 | 275 | #[test] 276 | fn test_allocs() { 277 | let struc = StrucProto { 278 | paths: vec![ 279 | KeyPath::from([ 280 | IndexPoint::new(0, 0), 281 | IndexPoint::new(2, 0), 282 | IndexPoint::new(2, 2), 283 | ]), 284 | KeyPath::from([IndexPoint::new(1, 1), IndexPoint::new(1, 1)]), 285 | ], 286 | attrs: CompAttrs::default(), 287 | }; 288 | let values = struc.values(); 289 | assert_eq!(values.h, vec![0, 1, 2]); 290 | assert_eq!(values.v, vec![0, 1, 2]); 291 | 292 | let mut struc = StrucProto { 293 | paths: vec![ 294 | KeyPath::from([ 295 | IndexPoint::new(1, 1), 296 | IndexPoint::new(2, 1), 297 | IndexPoint::new(2, 2), 298 | ]), 299 | KeyPath::from([IndexPoint::new(4, 1), IndexPoint::new(2, 1)]), 300 | ], 301 | attrs: CompAttrs::default(), 302 | }; 303 | 304 | assert_eq!(struc.allocation_values(), DataHV::new(vec![1, 2], vec![1])); 305 | assert_eq!( 306 | struc.values_map().h, 307 | BTreeMap::from([(1, 0), (2, 1), (4, 3)]) 308 | ); 309 | 310 | struc 311 | .attrs 312 | .set::(&DataHV::new(vec![0, 1], vec![2])); 313 | assert_eq!(struc.allocation_values(), DataHV::new(vec![0, 1], vec![2])); 314 | assert_eq!(struc.allocation_space(), DataHV::new(vec![1], vec![2])); 315 | assert_eq!( 316 | struc.values_map().h, 317 | BTreeMap::from([(1, 0), (2, 0), (4, 1)]) 318 | ); 319 | } 320 | 321 | #[test] 322 | fn test_to_path_in() { 323 | let assigns = DataHV::splat(vec![1.0, 1.0]); 324 | let struc = StrucProto { 325 | paths: vec![ 326 | KeyPath::from([IndexPoint::new(1, 0), IndexPoint::new(2, 0)]), 327 | KeyPath::from([IndexPoint::new(1, 1), IndexPoint::new(3, 1)]), 328 | KeyPath::from([IndexPoint::new(1, 2), IndexPoint::new(3, 2)]), 329 | ], 330 | attrs: CompAttrs::default(), 331 | }; 332 | 333 | let paths = struc.to_path_in_index( 334 | WorkPoint::zero(), 335 | assigns.clone(), 336 | DataHV::new(Some(0..=1), None), 337 | ); 338 | assert_eq!(paths.len(), 3); 339 | assert_eq!( 340 | paths[0].points, 341 | vec![WorkPoint::new(0., 0.), WorkPoint::new(1., 0.)] 342 | ); 343 | assert_eq!( 344 | paths[1].points, 345 | vec![WorkPoint::new(0., 1.), WorkPoint::new(1., 1.)] 346 | ); 347 | assert_eq!( 348 | paths[2].points, 349 | vec![WorkPoint::new(0., 2.), WorkPoint::new(1., 2.)] 350 | ); 351 | 352 | let paths = struc.to_path_in_index( 353 | WorkPoint::zero(), 354 | assigns.clone(), 355 | DataHV::new(Some(1..=2), None), 356 | ); 357 | assert_eq!(paths.len(), 2); 358 | assert_eq!( 359 | paths[0].points, 360 | vec![WorkPoint::new(1., 1.), WorkPoint::new(2., 1.)] 361 | ); 362 | assert_eq!( 363 | paths[1].points, 364 | vec![WorkPoint::new(1., 2.), WorkPoint::new(2., 2.)] 365 | ); 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /core/src/config/interval.rs: -------------------------------------------------------------------------------- 1 | use crate::{axis::*, component::view::StandardEdge}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::cmp::Ordering; 5 | 6 | #[derive(Clone)] 7 | pub struct IntervalRule { 8 | dots: [Option; 5], 9 | faces: [(Ordering, f32); 4], 10 | } 11 | 12 | impl IntervalRule { 13 | pub fn match_edge(&self, edge: &StandardEdge) -> bool { 14 | for i in 0..5 { 15 | if let Some(b) = self.dots[i] { 16 | if b != edge.dots[i] { 17 | return false; 18 | } 19 | } 20 | } 21 | for i in 0..4 { 22 | if edge.faces[i].partial_cmp(&self.faces[i].1).unwrap() != self.faces[i].0 { 23 | return false; 24 | } 25 | } 26 | 27 | true 28 | } 29 | } 30 | 31 | impl Serialize for IntervalRule { 32 | fn serialize(&self, serializer: S) -> Result 33 | where 34 | S: serde::Serializer, 35 | { 36 | use serde::ser::SerializeSeq; 37 | 38 | fn to_dot_str(d: Option) -> &'static str { 39 | match d { 40 | None => "*", 41 | Some(true) => "|", 42 | Some(false) => "x", 43 | } 44 | } 45 | 46 | fn to_fase_str(ord: Ordering) -> &'static str { 47 | match ord { 48 | Ordering::Less => "<", 49 | Ordering::Greater => ">", 50 | Ordering::Equal => "", 51 | } 52 | } 53 | 54 | let mut seq = serializer.serialize_seq(Some(9))?; 55 | for i in 0..4 { 56 | seq.serialize_element(to_dot_str(self.dots[i]))?; 57 | seq.serialize_element(&format!( 58 | "{}{:.3}", 59 | to_fase_str(self.faces[i].0), 60 | self.faces[i].1 61 | ))?; 62 | } 63 | seq.serialize_element(to_dot_str(self.dots[4]))?; 64 | seq.end() 65 | } 66 | } 67 | 68 | impl<'de> Deserialize<'de> for IntervalRule { 69 | fn deserialize(deserializer: D) -> Result 70 | where 71 | D: serde::Deserializer<'de>, 72 | { 73 | fn from_dot_str(s: &str) -> Option { 74 | match s { 75 | "|" => Some(true), 76 | "x" => Some(false), 77 | _ => None, 78 | } 79 | } 80 | 81 | fn from_fase_str(s: &str) -> Result<(Ordering, f32), std::num::ParseFloatError> { 82 | let r = match s.chars().next() { 83 | Some('<') => (Ordering::Less, s[1..].parse::()?), 84 | Some('>') => (Ordering::Greater, s[1..].parse::()?), 85 | _ => (Ordering::Equal, s.parse::()?), 86 | }; 87 | Ok(r) 88 | } 89 | 90 | match Deserialize::deserialize(deserializer)? { 91 | serde_json::Value::Array(array) => { 92 | if array.len() != 9 { 93 | Err(serde::de::Error::custom(format!( 94 | "Standard edge element is not {}", 95 | array.len() 96 | ))) 97 | } else if !array.iter().all(|ele| ele.is_string()) { 98 | Err(serde::de::Error::custom(format!( 99 | "Failed convert to IntervalRule in {:?}", 100 | array 101 | ))) 102 | } else { 103 | let list: Vec<_> = array.iter().map(|ele| ele.as_str().unwrap()).collect(); 104 | 105 | let dots = [ 106 | from_dot_str(list[0]), 107 | from_dot_str(list[2]), 108 | from_dot_str(list[4]), 109 | from_dot_str(list[6]), 110 | from_dot_str(list[8]), 111 | ]; 112 | let faces = [ 113 | from_fase_str(list[1]) 114 | .map_err(|e| serde::de::Error::custom(e.to_string()))?, 115 | from_fase_str(list[3]) 116 | .map_err(|e| serde::de::Error::custom(e.to_string()))?, 117 | from_fase_str(list[5]) 118 | .map_err(|e| serde::de::Error::custom(e.to_string()))?, 119 | from_fase_str(list[7]) 120 | .map_err(|e| serde::de::Error::custom(e.to_string()))?, 121 | ]; 122 | Ok(Self { dots, faces }) 123 | } 124 | } 125 | val => Err(serde::de::Error::custom(format!( 126 | "Failed convert to IntervalRule in {}", 127 | val 128 | ))), 129 | } 130 | } 131 | } 132 | 133 | #[derive(Serialize, Deserialize, Clone)] 134 | pub struct IntervalMatch { 135 | pub inverse: bool, 136 | pub axis: Option, 137 | pub val: usize, 138 | pub note: String, 139 | pub rule1: IntervalRule, 140 | pub rule2: IntervalRule, 141 | } 142 | 143 | impl IntervalMatch { 144 | pub fn is_match( 145 | &self, 146 | edge1: &StandardEdge, 147 | edge2: &StandardEdge, 148 | axis: Axis, 149 | ) -> Option { 150 | let mut r = None; 151 | if self.axis.unwrap_or(axis) == axis { 152 | if self.rule1.match_edge(edge1) && self.rule2.match_edge(edge2) { 153 | r = Some(self.val) 154 | } else if self.inverse && self.rule1.match_edge(edge2) && self.rule2.match_edge(edge1) { 155 | r = Some(self.val) 156 | } 157 | } 158 | r 159 | } 160 | } 161 | 162 | #[derive(Serialize, Deserialize, Clone)] 163 | pub struct Interval { 164 | pub rules: Vec, 165 | pub limit: f32, 166 | } 167 | 168 | impl Default for Interval { 169 | fn default() -> Self { 170 | Self { 171 | rules: Default::default(), 172 | limit: 1.0, 173 | } 174 | } 175 | } 176 | 177 | #[cfg(test)] 178 | mod tests { 179 | use super::*; 180 | use serde_json::json; 181 | 182 | #[test] 183 | fn test_interval_rule() { 184 | let js = json!(["x", "1.000", "|", ">2.000", "*", "<3.000", "*", "4.000", "x"]); 185 | let str = serde_json::to_string(&js).unwrap(); 186 | let rule: IntervalRule = serde_json::from_value(js).unwrap(); 187 | assert_eq!( 188 | rule.dots, 189 | [Some(false), Some(true), None, None, Some(false)] 190 | ); 191 | assert_eq!( 192 | rule.faces, 193 | [ 194 | (Ordering::Equal, 1.0), 195 | (Ordering::Greater, 2.0), 196 | (Ordering::Less, 3.0), 197 | (Ordering::Equal, 4.0), 198 | ] 199 | ); 200 | assert_eq!(str, serde_json::to_string(&rule).unwrap()); 201 | } 202 | 203 | #[test] 204 | fn test_interval_match() { 205 | let edge = StandardEdge { 206 | dots: [true, false, false, false, true], 207 | faces: [0., 0.5, 1., 0.], 208 | }; 209 | let val = json!(["|", "0", "x", "<0.501", "x", "1", "x", "0", "|"]); 210 | let rule: IntervalRule = serde_json::from_value(val).unwrap(); 211 | assert!(rule.match_edge(&edge)); 212 | 213 | let val = json!(["|", "0", "x", "0.501", "x", "1", "x", "0", "|"]); 214 | let rule: IntervalRule = serde_json::from_value(val).unwrap(); 215 | assert!(!rule.match_edge(&edge)); 216 | 217 | let val = json!(["|", "0", "x", "<0.501", "x", "1", "x", "0", "*"]); 218 | let rule: IntervalRule = serde_json::from_value(val).unwrap(); 219 | assert!(rule.match_edge(&edge)); 220 | 221 | let val = json!(["|", "0", "x", "<0.501", "|", "1", "x", "0", "|"]); 222 | let rule: IntervalRule = serde_json::from_value(val).unwrap(); 223 | assert!(!rule.match_edge(&edge)); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /core/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | axis::*, 3 | component::strategy::PlaceMain, 4 | construct::{Component, CpAttrs, CstType}, 5 | }; 6 | pub mod interval; 7 | use interval::Interval; 8 | pub mod process; 9 | use process::SpaceProcess; 10 | 11 | use serde::{Deserialize, Serialize}; 12 | use std::collections::{BTreeMap, BTreeSet}; 13 | 14 | #[derive(Serialize, Deserialize, Clone, Copy, Default)] 15 | pub struct WhiteArea { 16 | pub fixed: f32, 17 | pub allocated: f32, 18 | pub value: f32, 19 | } 20 | 21 | impl WhiteArea { 22 | pub fn is_zero(&self) -> bool { 23 | (self.fixed + self.allocated + self.value) == 0.0 24 | } 25 | } 26 | 27 | #[derive(Serialize, Deserialize, Clone, Default)] 28 | pub struct TypeDate { 29 | pub axis: A, 30 | pub surround: S, 31 | } 32 | 33 | #[derive(Serialize, Deserialize, Clone)] 34 | pub struct Config { 35 | pub size: DataHV, 36 | pub min_val: DataHV>, 37 | pub white: DataHV, 38 | pub comp_white: DataHV, 39 | pub strok_width: f32, 40 | 41 | pub supplement: BTreeMap, 42 | // 结构-位-字-部件 43 | pub type_replace: BTreeMap>>, 44 | pub place_replace: BTreeMap>, 45 | 46 | pub interval: Interval, 47 | 48 | pub process_control: Vec, 49 | 50 | pub strategy: TypeDate< 51 | DataHV>>>, 52 | BTreeMap>>, 53 | >, 54 | 55 | pub reduce_replace: DataHV>, 56 | pub reduce_trigger: DataHV, 57 | } 58 | 59 | impl Default for Config { 60 | fn default() -> Self { 61 | Self { 62 | size: DataHV::splat(1.0), 63 | min_val: DataHV::splat(vec![Self::DEFAULT_MIN_VALUE]), 64 | white: Default::default(), 65 | comp_white: Default::default(), 66 | strok_width: Self::DEFAULT_MIN_VALUE, 67 | supplement: Default::default(), 68 | type_replace: Default::default(), 69 | place_replace: Default::default(), 70 | interval: Default::default(), 71 | process_control: Default::default(), 72 | strategy: Default::default(), 73 | reduce_replace: Default::default(), 74 | reduce_trigger: DataHV::splat(0.0), 75 | } 76 | } 77 | } 78 | 79 | impl Config { 80 | pub const DEFAULT_MIN_VALUE: f32 = 0.05; 81 | 82 | pub fn type_replace_name(&self, name: &str, tp: CstType, in_tp: Place) -> Option { 83 | fn process<'a>( 84 | cfg: &'a Config, 85 | name: &str, 86 | tp: CstType, 87 | in_tp: Place, 88 | ) -> Option<&'a Component> { 89 | cfg.type_replace 90 | .get(&tp.symbol()) 91 | .and_then(|pm| pm.get(&in_tp).and_then(|map| map.get(name))) 92 | } 93 | 94 | process(self, name, tp, in_tp) 95 | .map(|mut map_comp| { 96 | while let Some(mc) = process(self, &map_comp.name(), tp, in_tp) { 97 | map_comp = mc 98 | } 99 | map_comp 100 | }) 101 | .cloned() 102 | } 103 | 104 | pub fn place_replace_name(&self, name: &str, places: DataHV<[bool; 2]>) -> Option { 105 | self.place_replace 106 | .get(name) 107 | .and_then(|pm| { 108 | pm.iter().find_map(|(r, c)| match place_match(r, places) { 109 | true => Some(c), 110 | false => None, 111 | }) 112 | }) 113 | .cloned() 114 | } 115 | } 116 | 117 | pub fn place_match(rule: &str, places: DataHV<[bool; 2]>) -> bool { 118 | fn is_match(attr: &str, exist: bool) -> bool { 119 | match attr { 120 | "x" => !exist, 121 | "o" => exist, 122 | "*" => true, 123 | _ => false, 124 | } 125 | } 126 | 127 | rule.split(';') 128 | .into_iter() 129 | .find(|r| { 130 | let place_attr: Vec<&str> = r.split(' ').collect(); 131 | match place_attr.len() { 132 | 1 => places 133 | .hv_iter() 134 | .flatten() 135 | .all(|e| is_match(place_attr[0], *e)), 136 | 2 => places 137 | .hv_iter() 138 | .zip(place_attr.iter()) 139 | .all(|(place, attr)| place.iter().all(|e| is_match(attr, *e))), 140 | 4 => places 141 | .hv_iter() 142 | .flatten() 143 | .zip(place_attr.iter()) 144 | .all(|(e, attr)| is_match(attr, *e)), 145 | _ => { 146 | eprintln!( 147 | "Excess number {} of symbols! {:?}", 148 | place_attr.len(), 149 | place_attr 150 | ); 151 | false 152 | } 153 | } 154 | }) 155 | .is_some() 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use super::*; 161 | 162 | #[test] 163 | fn test_type_replace_name() { 164 | let mut cfg = Config::default(); 165 | cfg.type_replace.insert( 166 | '□', 167 | std::collections::BTreeMap::from([( 168 | crate::axis::Place::Start, 169 | std::collections::BTreeMap::from([( 170 | "丯".to_string(), 171 | crate::construct::Component::from_name("丰"), 172 | )]), 173 | )]), 174 | ); 175 | 176 | let r = cfg 177 | .type_replace_name("丯", CstType::Single, Place::Start) 178 | .unwrap(); 179 | match r { 180 | Component::Char(name) => assert_eq!(name, "丰".to_string()), 181 | Component::Complex(_) => unreachable!(), 182 | } 183 | } 184 | 185 | #[test] 186 | fn test_place_match() { 187 | let mut places = DataHV::from(([true, false], [true, false])); 188 | assert!(place_match("*", places)); 189 | assert!(!place_match("x", places)); 190 | assert!(!place_match("o", places)); 191 | assert!(place_match("o x o x", places)); 192 | assert!(!place_match("o x", places)); 193 | 194 | places.h = [true, true]; 195 | places.v = [false, false]; 196 | assert!(place_match("*", places)); 197 | assert!(!place_match("x", places)); 198 | assert!(!place_match("o", places)); 199 | assert!(place_match("o o x x", places)); 200 | assert!(place_match("o x", places)); 201 | 202 | places.v = [true, true]; 203 | assert!(place_match("*", places)); 204 | assert!(!place_match("x", places)); 205 | assert!(place_match("o", places)); 206 | assert!(!place_match("o o x x", places)); 207 | assert!(place_match("o o", places)); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /core/src/config/process.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | algorithm as al, 3 | axis::*, 4 | component::comb::{AssignVal, StrucComb}, 5 | construct::CstType, 6 | }; 7 | 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Serialize, Deserialize, Clone, Default)] 11 | pub struct Operation { 12 | pub operation: O, 13 | pub execution: E, 14 | } 15 | 16 | impl Operation { 17 | pub fn new(operation: O, execution: E) -> Self { 18 | Self { 19 | operation, 20 | execution, 21 | } 22 | } 23 | } 24 | 25 | #[derive(Serialize, Deserialize, Clone)] 26 | pub enum SpaceProcess { 27 | Center(DataHV>), 28 | CompCenter(DataHV>), 29 | CenterArea(DataHV>), 30 | EdgeShrink(DataHV<[bool; 2]>), 31 | } 32 | 33 | impl SpaceProcess { 34 | pub fn process_space(&self, comb: &mut StrucComb, cfg: &super::Config) { 35 | match self { 36 | SpaceProcess::EdgeShrink(setting) => match comb { 37 | StrucComb::Single { 38 | assign_vals, 39 | proto, 40 | view, 41 | .. 42 | } => { 43 | let allocs = proto.allocation_space(); 44 | for axis in Axis::list() { 45 | let shrink = setting.hv_get(axis); 46 | let allocs = allocs.hv_get(axis); 47 | let assign = assign_vals.hv_get_mut(axis); 48 | 49 | if allocs.len() > shrink.iter().filter(|b| **b).count() { 50 | let targets = [0, 1].map(|mut i| { 51 | let mut idx = None; 52 | if shrink[i] { 53 | let place = if i == 0 { 54 | Place::Start 55 | } else { 56 | i = allocs.len() - 1; 57 | Place::End 58 | }; 59 | 60 | if allocs[i] == 1 { 61 | let edge = view.read_lines(axis, place).to_edge(); 62 | if edge.faces.iter().find(|f| **f == 1.0).is_none() 63 | && edge.dots.iter().filter(|d| **d).count() < 3 64 | { 65 | idx = Some(i) 66 | } 67 | } 68 | } 69 | idx 70 | }); 71 | 72 | let excess = targets.iter().fold(0.0, |mut e, t| { 73 | if let Some(i) = *t { 74 | e += assign[i].excess; 75 | assign[i].excess = 0.0; 76 | } 77 | e 78 | }); 79 | let range = targets[0].map(|i| i + 1).unwrap_or(0) 80 | ..=targets[1].map(|i| i - 1).unwrap_or(allocs.len() - 1); 81 | let total = assign[range.clone()].iter().sum::().total(); 82 | assign[range] 83 | .iter_mut() 84 | .for_each(|v| v.excess += v.total() / total * excess); 85 | } 86 | } 87 | } 88 | StrucComb::Complex { combs, tp, .. } => match tp { 89 | CstType::Scale(_) => combs.iter_mut().for_each(|c| self.process_space(c, cfg)), 90 | CstType::Surround(_) => { 91 | self.process_space(&mut combs[1], cfg); 92 | } 93 | CstType::Single => unreachable!(), 94 | }, 95 | }, 96 | SpaceProcess::Center(operation) => match comb { 97 | StrucComb::Single { .. } => { 98 | let center = comb.get_visual_center(al::NORMAL_OFFSET, cfg.strok_width); 99 | if let StrucComb::Single { assign_vals, .. } = comb { 100 | for axis in Axis::list() { 101 | let assign_vals = assign_vals.hv_get_mut(axis); 102 | let center_opt = operation.hv_get(axis); 103 | let new_vals = al::center_correction( 104 | &assign_vals.iter().map(|av| av.total()).collect(), 105 | &assign_vals.iter().map(|av| av.base).collect(), 106 | *center.hv_get(axis), 107 | center_opt.operation, 108 | center_opt.execution, 109 | ); 110 | new_vals.into_iter().zip(assign_vals.iter_mut()).for_each( 111 | |(nval, aval)| { 112 | aval.excess = nval; 113 | }, 114 | ); 115 | } 116 | } 117 | } 118 | StrucComb::Complex { combs, tp, .. } => match tp { 119 | CstType::Surround(_) => self.process_space(&mut combs[1], cfg), 120 | _ => combs.iter_mut().for_each(|c| self.process_space(c, cfg)), 121 | }, 122 | }, 123 | SpaceProcess::CompCenter(operation) => match comb { 124 | StrucComb::Complex { 125 | tp, 126 | intervals, 127 | combs, 128 | edge_main, 129 | offsets: ofs, 130 | .. 131 | } => match *tp { 132 | CstType::Scale(comb_axis) => { 133 | for i in 1..combs.len() { 134 | let mut paths = vec![]; 135 | let mut start = Default::default(); 136 | let mut vlist = Vec::with_capacity(i * 2 + 1); 137 | let mut bases = Vec::with_capacity(i * 2 + 1); 138 | 139 | for j in 0..=i { 140 | let size = combs[j].merge_to(start, &mut paths); 141 | *start.hv_get_mut(comb_axis) += *size.hv_get(comb_axis); 142 | 143 | let c_assgin = combs[j].get_assign_length(comb_axis); 144 | vlist.push(c_assgin.total()); 145 | bases.push(c_assgin.base); 146 | 147 | if j < i { 148 | *start.hv_get_mut(comb_axis) += intervals[j].total(); 149 | vlist.push(intervals[j].total()); 150 | bases.push(intervals[j].base); 151 | } 152 | } 153 | 154 | let center = 155 | al::visual_center_length(paths, al::NORMAL_OFFSET, cfg.strok_width); 156 | let center_opt = operation.hv_get(comb_axis); 157 | let new_vals = al::center_correction( 158 | &vlist, 159 | &bases, 160 | *center.hv_get(comb_axis), 161 | center_opt.operation, 162 | center_opt.execution, 163 | ); 164 | 165 | for j in 0..=i { 166 | if j != i { 167 | intervals[j].excess = new_vals[j * 2 + 1]; 168 | } 169 | 170 | let mut assign: DataHV> = Default::default(); 171 | let mut offsets: DataHV<[Option; 2]> = 172 | Default::default(); 173 | 174 | *assign.hv_get_mut(comb_axis) = 175 | Some(new_vals[j * 2] + bases[j * 2]); 176 | if j != 0 { 177 | offsets.hv_get_mut(comb_axis)[0] = Some(AssignVal { 178 | base: bases[j * 2 - 1] * 0.5, 179 | excess: new_vals[j * 2 - 1] * 0.5, 180 | }); 181 | } 182 | if j != i { 183 | offsets.hv_get_mut(comb_axis)[1] = Some(AssignVal { 184 | base: bases[j * 2 + 1] * 0.5, 185 | excess: new_vals[j * 2 + 1] * 0.5, 186 | }); 187 | } 188 | 189 | combs[j].scale_space(assign, offsets); 190 | } 191 | } 192 | } 193 | CstType::Surround(surround) => { 194 | let mut paths = vec![]; 195 | combs.iter().for_each(|c| { 196 | c.merge_to(Default::default(), &mut paths); 197 | }); 198 | let center = 199 | al::visual_center_length(paths, al::NORMAL_OFFSET, cfg.strok_width); 200 | let secondary_len = 201 | Axis::hv().into_map(|axis| combs[1].get_assign_length(axis)); 202 | 203 | let mut s_offsets = combs[1].get_offsets(); 204 | let s_assign = Axis::hv().into_map(|axis| { 205 | if let StrucComb::Single { 206 | view, 207 | proto, 208 | assign_vals, 209 | .. 210 | } = &mut combs[0] 211 | { 212 | let surr_area = view.surround_area(surround).unwrap(); 213 | let area = *surr_area.hv_get(axis); 214 | let area_idx = proto.value_index_in_axis(&area, axis); 215 | let mut surr_idx = area_idx.clone(); 216 | 217 | let primary_len = [ 218 | &assign_vals.hv_get(axis)[..area_idx[0]], 219 | &assign_vals.hv_get(axis)[area_idx[0]..area_idx[1]], 220 | &assign_vals.hv_get(axis)[area_idx[1]..], 221 | ]; 222 | 223 | let secondary_len = *secondary_len.hv_get(axis); 224 | let i_ofs = match axis { 225 | Axis::Horizontal => 0, 226 | Axis::Vertical => 2, 227 | }; 228 | let interval_len = &intervals[0 + i_ofs..2 + i_ofs]; 229 | 230 | let (aval_list, is_p): (Vec, bool) = 231 | if primary_len[1].iter().map(|val| val.base).sum::() 232 | > interval_len[0].base 233 | + secondary_len.base 234 | + interval_len[1].base 235 | { 236 | ( 237 | primary_len 238 | .iter() 239 | .map(|slice| slice.iter()) 240 | .flatten() 241 | .copied() 242 | .collect(), 243 | true, 244 | ) 245 | } else { 246 | surr_idx[1] = surr_idx[0] + 3; 247 | ( 248 | primary_len[0] 249 | .iter() 250 | .copied() 251 | .chain([ 252 | interval_len[0], 253 | secondary_len, 254 | interval_len[1], 255 | ]) 256 | .chain(primary_len[2].iter().copied()) 257 | .collect(), 258 | false, 259 | ) 260 | }; 261 | 262 | let center_opt = operation.hv_get(axis); 263 | let vlist = aval_list.iter().map(|av| av.total()).collect(); 264 | let bases = aval_list.iter().map(|av| av.base).collect(); 265 | let new_vals = al::center_correction( 266 | &vlist, 267 | &bases, 268 | *center.hv_get(axis), 269 | center_opt.operation, 270 | center_opt.execution, 271 | ); 272 | 273 | let (mut new_surr, old_surr) = (surr_idx[0]..surr_idx[1]) 274 | .map(|i| (new_vals[i] + bases[i], vlist[i])) 275 | .reduce(|a, b| (a.0 + b.0, a.1 + b.1)) 276 | .unwrap(); 277 | let scale = new_surr / old_surr; 278 | 279 | if is_p { 280 | assign_vals 281 | .hv_get_mut(axis) 282 | .iter_mut() 283 | .zip(new_vals.iter()) 284 | .for_each(|(old, new)| { 285 | old.excess = *new; 286 | }); 287 | 288 | for i in i_ofs..i_ofs + 2 { 289 | intervals[i].excess = (intervals[i].total() * scale 290 | - intervals[i].base) 291 | .max(0.0); 292 | } 293 | } else { 294 | for i in 0..assign_vals.hv_get(axis).len() { 295 | let av = &mut assign_vals.hv_get_mut(axis)[i]; 296 | if i < area_idx[0] { 297 | av.excess = new_vals[i]; 298 | } else if i < area_idx[1] { 299 | av.excess = av.total() * scale - av.base; 300 | } else { 301 | av.excess = new_vals[i - area_idx[1] + surr_idx[1]]; 302 | } 303 | } 304 | 305 | intervals[i_ofs].excess = new_vals[surr_idx[0]]; 306 | intervals[i_ofs + 1].excess = new_vals[surr_idx[0] + 2]; 307 | } 308 | 309 | let edge_main = edge_main.hv_get(axis); 310 | for i in 0..2 { 311 | let (place, range) = match i { 312 | 0 => (Place::End, 0..area_idx[0]), 313 | _ => ( 314 | Place::Start, 315 | area_idx[1]..assign_vals.hv_get(axis).len(), 316 | ), 317 | }; 318 | 319 | let s_white = 320 | if edge_main.get(&place).map(|ep| ep[1]).unwrap_or(true) { 321 | AssignVal::default() 322 | } else { 323 | let mut c_ofs = s_offsets.hv_get(axis)[i]; 324 | c_ofs.base -= ofs.hv_get(axis)[i].total(); 325 | c_ofs.excess = 326 | (c_ofs.total() * scale - c_ofs.base).max(0.0); 327 | c_ofs 328 | }; 329 | 330 | if *surround.hv_get(axis) == place { 331 | s_offsets.hv_get_mut(axis)[i] = 332 | s_white + s_offsets.hv_get(axis)[i]; 333 | } else { 334 | s_offsets.hv_get_mut(axis)[i] = AssignVal::new( 335 | assign_vals.hv_get(axis)[range] 336 | .iter() 337 | .sum::() 338 | .total() 339 | + ofs.hv_get(axis)[i].total() 340 | + intervals[i + i_ofs].base 341 | + intervals[i + i_ofs].excess / 2.0, 342 | intervals[i + i_ofs].excess / 2.0, 343 | ); 344 | } 345 | 346 | new_surr -= intervals[i + i_ofs].total() + s_white.total(); 347 | } 348 | Some(new_surr) 349 | } else { 350 | panic!() 351 | } 352 | }); 353 | 354 | combs[1] 355 | .scale_space(s_assign, s_offsets.into_map(|ofs| ofs.map(|v| Some(v)))); 356 | } 357 | CstType::Single => unreachable!(), 358 | }, 359 | StrucComb::Single { .. } => {} 360 | }, 361 | _ => {} 362 | } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /core/src/construct/data.rs: -------------------------------------------------------------------------------- 1 | use super::types::CstType; 2 | 3 | use serde::{ser::SerializeStruct, Deserialize, Serialize}; 4 | extern crate serde_json as sj; 5 | use std::{ 6 | collections::{HashMap, HashSet}, 7 | ops::{Deref, DerefMut}, 8 | }; 9 | 10 | #[derive(Clone)] 11 | pub struct CpAttrs { 12 | pub tp: CstType, 13 | pub components: Vec, 14 | } 15 | 16 | impl CpAttrs { 17 | pub fn single() -> Self { 18 | CpAttrs { 19 | tp: CstType::Single, 20 | components: vec![], 21 | } 22 | } 23 | 24 | pub fn comps_name(&self) -> String { 25 | format!( 26 | "{}({})", 27 | self.tp.symbol(), 28 | self.components 29 | .iter() 30 | .map(|comp| comp.name()) 31 | .collect::>() 32 | .join("+") 33 | ) 34 | } 35 | } 36 | 37 | impl Serialize for CpAttrs { 38 | fn serialize(&self, serializer: S) -> Result 39 | where 40 | S: serde::Serializer, 41 | { 42 | match self.tp { 43 | CstType::Single => serializer.serialize_str(""), 44 | tp => { 45 | let mut s = serializer.serialize_struct("CpAttrs", 2)?; 46 | s.serialize_field("tp", &tp.symbol())?; 47 | s.serialize_field("components", &self.components)?; 48 | s.end() 49 | } 50 | } 51 | } 52 | } 53 | 54 | impl<'de> Deserialize<'de> for CpAttrs { 55 | fn deserialize(deserializer: D) -> Result 56 | where 57 | D: serde::Deserializer<'de>, 58 | { 59 | match Deserialize::deserialize(deserializer)? { 60 | serde_json::Value::String(symbol) => { 61 | let tp = CstType::from_symbol(&symbol).ok_or(serde::de::Error::custom(format!( 62 | "Unkonw construct type: {}", 63 | symbol 64 | )))?; 65 | Ok(Self { 66 | tp, 67 | components: vec![], 68 | }) 69 | } 70 | serde_json::Value::Object(data) => { 71 | let tp = match data.get("tp") { 72 | Some(val) if val.is_string() => { 73 | CstType::from_symbol(val.as_str().unwrap()).ok_or( 74 | serde::de::Error::custom(format!("Unkonw construct type: {}", val)), 75 | )? 76 | } 77 | _ => Err(serde::de::Error::custom("Missing field `tp`!"))?, 78 | }; 79 | let components = match data.get("components") { 80 | Some(val) if val.is_array() => sj::from_value(val.clone()) 81 | .map_err(|e| serde::de::Error::custom(e.to_string()))?, 82 | _ => Err(serde::de::Error::custom("Missing field `components`!"))?, 83 | }; 84 | Ok(Self { tp, components }) 85 | } 86 | val => Err(serde::de::Error::custom(format!( 87 | "Failed convert to CpAttrs in {}", 88 | val 89 | ))), 90 | } 91 | } 92 | } 93 | 94 | #[derive(Clone)] 95 | pub enum Component { 96 | Char(String), 97 | Complex(CpAttrs), 98 | } 99 | 100 | impl Serialize for Component { 101 | fn serialize(&self, serializer: S) -> Result 102 | where 103 | S: serde::Serializer, 104 | { 105 | match self { 106 | Self::Char(name) => serializer.serialize_str(name), 107 | Self::Complex(attrs) => serializer.serialize_newtype_struct("attrs", attrs), 108 | } 109 | } 110 | } 111 | 112 | impl<'de> Deserialize<'de> for Component { 113 | fn deserialize(deserializer: D) -> Result 114 | where 115 | D: serde::Deserializer<'de>, 116 | { 117 | match Deserialize::deserialize(deserializer)? { 118 | serde_json::Value::String(str) => Ok(Self::Char(str)), 119 | serde_json::Value::Object(data) => { 120 | match sj::from_value::(sj::Value::Object(data)) { 121 | Ok(attrs) => Ok(Self::Complex(attrs)), 122 | _ => Err(serde::de::Error::custom("Conversion fails!")), 123 | } 124 | } 125 | val => Err(serde::de::Error::custom(format!( 126 | "Failed convert to Component in {}", 127 | val 128 | ))), 129 | } 130 | } 131 | } 132 | 133 | impl Component { 134 | pub fn from_name(value: T) -> Self { 135 | return Self::Char(value.to_string()); 136 | } 137 | 138 | pub fn name(&self) -> String { 139 | match self { 140 | Self::Char(name) => name.clone(), 141 | Self::Complex(attr) => attr.comps_name(), 142 | } 143 | } 144 | } 145 | 146 | #[derive(Serialize, Deserialize)] 147 | pub struct CharTree { 148 | pub name: String, 149 | pub tp: CstType, 150 | pub children: Vec, 151 | } 152 | 153 | impl CharTree { 154 | pub fn new_single(name: String) -> Self { 155 | Self { 156 | name, 157 | tp: CstType::Single, 158 | children: vec![], 159 | } 160 | } 161 | 162 | pub fn get_comb_name(&self) -> String { 163 | match self.tp { 164 | CstType::Single => self.name.clone(), 165 | tp => { 166 | format!( 167 | "{} {}({})", 168 | self.name.clone(), 169 | tp.symbol(), 170 | self.children 171 | .iter() 172 | .map(|c| c.get_comb_name()) 173 | .collect::>() 174 | .join("+") 175 | ) 176 | } 177 | } 178 | } 179 | } 180 | 181 | #[derive(Clone, Serialize, Deserialize)] 182 | pub struct CstTable(HashMap); 183 | 184 | impl Deref for CstTable { 185 | type Target = HashMap; 186 | 187 | fn deref(&self) -> &Self::Target { 188 | &self.0 189 | } 190 | } 191 | 192 | impl DerefMut for CstTable { 193 | fn deref_mut(&mut self) -> &mut Self::Target { 194 | &mut self.0 195 | } 196 | } 197 | 198 | impl Default for CstTable { 199 | fn default() -> Self { 200 | const TABLE_STRING: &str = include_str!(concat!(env!("OUT_DIR"), "/fasing_1_0.json")); 201 | Self::from_json_array(serde_json::from_str(TABLE_STRING).unwrap()) 202 | } 203 | } 204 | 205 | impl CstTable { 206 | pub fn target_chars(&self) -> Vec { 207 | self.keys() 208 | .filter_map(|key| { 209 | let mut iter = key.chars(); 210 | iter.next().and_then(|chr| match iter.next() { 211 | Some(_) => None, 212 | None => Some(chr), 213 | }) 214 | }) 215 | .collect() 216 | } 217 | 218 | fn attr_from_json_array(array: &Vec) -> CpAttrs { 219 | let format = CstType::from_symbol(array[0].as_str().unwrap()).unwrap(); 220 | let components = array[1] 221 | .as_array() 222 | .unwrap() 223 | .iter() 224 | .fold(vec![], |mut comps, v| { 225 | match v { 226 | sj::Value::String(c) => comps.push(Component::Char(c.clone())), 227 | sj::Value::Array(array) => { 228 | comps.push(Component::Complex(Self::attr_from_json_array(array))) 229 | } 230 | _ => panic!("Unknow data: {}", v.to_string()), 231 | } 232 | comps 233 | }); 234 | 235 | CpAttrs { 236 | tp: format, 237 | components, 238 | } 239 | } 240 | 241 | pub fn empty() -> Self { 242 | Self(Default::default()) 243 | } 244 | 245 | pub fn from_json_array(obj: sj::Value) -> CstTable { 246 | let obj = obj.as_object().unwrap(); 247 | let table = CstTable(HashMap::with_capacity(obj.len())); 248 | 249 | obj.into_iter().fold(table, |mut table, (chr, attr)| { 250 | if let Some(a) = table.insert( 251 | chr.clone(), 252 | Self::attr_from_json_array(attr.as_array().unwrap()), 253 | ) { 254 | eprintln!( 255 | "Duplicate character `{}`:\n{}\n{:?}", 256 | chr, 257 | attr, 258 | a.comps_name() 259 | ); 260 | } 261 | table 262 | }) 263 | } 264 | 265 | pub fn all_necessary_components(&self) -> HashSet { 266 | fn find_until( 267 | comp: &Component, 268 | table: &HashMap, 269 | requis: &mut HashSet, 270 | ) { 271 | match comp { 272 | Component::Char(str) => match table.get(str) { 273 | Some(attrs) => { 274 | if attrs.tp == CstType::Single { 275 | requis.insert(str.clone()); 276 | } else { 277 | attrs 278 | .components 279 | .iter() 280 | .for_each(|comp| find_until(comp, table, requis)); 281 | } 282 | } 283 | None => { 284 | requis.insert(str.clone()); 285 | } 286 | }, 287 | Component::Complex(ref attrs) => attrs 288 | .components 289 | .iter() 290 | .for_each(|comp| find_until(comp, table, requis)), 291 | } 292 | } 293 | 294 | self.iter() 295 | .fold(HashSet::new(), |mut requis, (chr, attrs)| { 296 | if attrs.tp == CstType::Single { 297 | requis.insert(chr.to_string()); 298 | } else { 299 | attrs 300 | .components 301 | .iter() 302 | .for_each(|comp| find_until(comp, self, &mut requis)); 303 | } 304 | 305 | requis 306 | }) 307 | } 308 | } 309 | 310 | #[cfg(test)] 311 | mod tests { 312 | use super::*; 313 | 314 | #[test] 315 | fn test_completeness() { 316 | let table = CstTable::default(); 317 | let requests = table.all_necessary_components(); 318 | 319 | let mut misses = std::collections::HashSet::new(); 320 | 321 | requests.into_iter().for_each(|name| { 322 | let mut chars = name.chars(); 323 | let chr = chars.next().unwrap(); 324 | if chars.next().is_none() && !table.contains_key(&name) { 325 | misses.insert(chr); 326 | } 327 | }); 328 | 329 | assert_eq!(misses, std::collections::HashSet::new()); 330 | } 331 | 332 | #[test] 333 | fn test_tartget_chars() { 334 | let mut table = CstTable::empty(); 335 | table.insert( 336 | String::from("艹"), 337 | CpAttrs { 338 | tp: CstType::Single, 339 | components: vec![], 340 | }, 341 | ); 342 | table.insert( 343 | String::from("艹字头"), 344 | CpAttrs { 345 | tp: CstType::Single, 346 | components: vec![], 347 | }, 348 | ); 349 | 350 | assert_eq!(table.len(), 2); 351 | assert_eq!(table.target_chars(), vec!['艹']); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /core/src/construct/error.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::{error, fmt}; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub enum CstError { 6 | Empty(String), 7 | AxisTransform { 8 | axis: crate::axis::Axis, 9 | length: f32, 10 | base_len: usize, 11 | }, 12 | Surround { 13 | tp: char, 14 | comp: String, 15 | }, 16 | } 17 | 18 | impl fmt::Display for CstError { 19 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | match self { 21 | Self::Empty(name) => write!(f, "`{name}` is empty!"), 22 | Self::AxisTransform { 23 | axis, 24 | length, 25 | base_len, 26 | } => write!( 27 | f, 28 | "The minimum length {} greater than {:.3} in {:?}!", 29 | base_len, length, axis 30 | ), 31 | Self::Surround { tp, comp } => { 32 | write!(f, "Components `{}` cannot be surrounded by {}", comp, tp) 33 | } 34 | } 35 | } 36 | } 37 | 38 | impl error::Error for CstError {} 39 | -------------------------------------------------------------------------------- /core/src/construct/mod.rs: -------------------------------------------------------------------------------- 1 | mod data; 2 | pub use data::*; 3 | 4 | pub mod space; 5 | 6 | mod types; 7 | pub use types::*; 8 | 9 | mod error; 10 | pub use error::*; 11 | 12 | pub type Components = std::collections::BTreeMap; 13 | -------------------------------------------------------------------------------- /core/src/construct/space.rs: -------------------------------------------------------------------------------- 1 | use crate::axis::*; 2 | 3 | use euclid::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Default, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] 7 | pub struct IndexSpace; 8 | pub type IndexPoint = Point2D; 9 | pub type IndexSize = Size2D; 10 | 11 | #[derive(Default, Serialize, Deserialize, PartialEq, Debug, Clone, Copy)] 12 | pub struct WorkSpace; 13 | pub type WorkPoint = Point2D; 14 | pub type WorkSize = Size2D; 15 | pub type WorkVec = Vector2D; 16 | pub type WorkRect = Rect; 17 | pub type WorkBox = Box2D; 18 | 19 | pub trait BoxExpand { 20 | fn contains_include(&self, p: Point2D, offset: f32) -> bool; 21 | } 22 | 23 | impl BoxExpand for Box2D { 24 | fn contains_include(&self, p: Point2D, offset: f32) -> bool { 25 | self.min.x - offset <= p.x 26 | && p.x <= self.max.x + offset 27 | && self.min.y - offset <= p.y 28 | && p.y <= self.max.y + offset 29 | } 30 | } 31 | 32 | #[derive(Default, Serialize, Deserialize, Clone, Debug)] 33 | pub struct KeyWorkPath { 34 | pub points: Vec, 35 | pub hide: bool, 36 | } 37 | 38 | impl> From for KeyWorkPath { 39 | fn from(value: T) -> Self { 40 | KeyWorkPath { 41 | points: value.into_iter().collect(), 42 | hide: false, 43 | } 44 | } 45 | } 46 | 47 | #[derive(Default, Serialize, Deserialize, Clone, Debug)] 48 | pub struct KeyPath { 49 | pub points: Vec, 50 | pub hide: bool, 51 | } 52 | 53 | impl> From for KeyPath { 54 | fn from(value: T) -> Self { 55 | KeyPath { 56 | points: value.into_iter().collect(), 57 | hide: false, 58 | } 59 | } 60 | } 61 | 62 | impl ValueHV for euclid::Point2D { 63 | fn hv_get(&self, axis: Axis) -> &T { 64 | match axis { 65 | Axis::Horizontal => &self.x, 66 | Axis::Vertical => &self.y, 67 | } 68 | } 69 | 70 | fn hv_get_mut(&mut self, axis: Axis) -> &mut T { 71 | match axis { 72 | Axis::Horizontal => &mut self.x, 73 | Axis::Vertical => &mut self.y, 74 | } 75 | } 76 | } 77 | 78 | impl ValueHV for euclid::Vector2D { 79 | fn hv_get(&self, axis: Axis) -> &T { 80 | match axis { 81 | Axis::Horizontal => &self.x, 82 | Axis::Vertical => &self.y, 83 | } 84 | } 85 | 86 | fn hv_get_mut(&mut self, axis: Axis) -> &mut T { 87 | match axis { 88 | Axis::Horizontal => &mut self.x, 89 | Axis::Vertical => &mut self.y, 90 | } 91 | } 92 | } 93 | 94 | impl ValueHV for euclid::Size2D { 95 | fn hv_get(&self, axis: Axis) -> &T { 96 | match axis { 97 | Axis::Horizontal => &self.width, 98 | Axis::Vertical => &self.height, 99 | } 100 | } 101 | 102 | fn hv_get_mut(&mut self, axis: Axis) -> &mut T { 103 | match axis { 104 | Axis::Horizontal => &mut self.width, 105 | Axis::Vertical => &mut self.height, 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /core/src/construct/types.rs: -------------------------------------------------------------------------------- 1 | use crate::axis::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)] 5 | pub enum CstType { 6 | Single, 7 | Scale(Axis), 8 | Surround(DataHV), 9 | } 10 | 11 | impl CstType { 12 | pub fn symbol(&self) -> char { 13 | match self { 14 | Self::Single => '□', 15 | Self::Scale(Axis::Horizontal) => '⿰', 16 | Self::Scale(Axis::Vertical) => '⿱', 17 | Self::Surround(DataHV { 18 | h: Place::Start, 19 | v: Place::Start, 20 | }) => '⿸', 21 | Self::Surround(DataHV { 22 | h: Place::End, 23 | v: Place::Start, 24 | }) => '⿹', 25 | Self::Surround(DataHV { 26 | h: Place::Start, 27 | v: Place::End, 28 | }) => '⿺', 29 | Self::Surround(DataHV { 30 | h: Place::End, 31 | v: Place::End, 32 | }) => '⿽', 33 | Self::Surround(DataHV { 34 | h: Place::Middle, 35 | v: Place::Start, 36 | }) => '⿵', 37 | Self::Surround(DataHV { 38 | h: Place::Middle, 39 | v: Place::End, 40 | }) => '⿶', 41 | Self::Surround(DataHV { 42 | h: Place::Start, 43 | v: Place::Middle, 44 | }) => '⿷', 45 | Self::Surround(DataHV { 46 | h: Place::End, 47 | v: Place::Middle, 48 | }) => '⿼', 49 | Self::Surround(DataHV { 50 | h: Place::Middle, 51 | v: Place::Middle, 52 | }) => '⿴', 53 | // _ => panic!("Unkonw construct type: {:?}", self), 54 | } 55 | } 56 | 57 | pub fn from_symbol(symbol: &str) -> Option { 58 | let tp = match symbol { 59 | "" | "□" => Self::Single, 60 | "⿰" | "⿲" => Self::Scale(Axis::Horizontal), 61 | "⿱" | "⿳" => Self::Scale(Axis::Vertical), 62 | "⿸" => Self::Surround(DataHV { 63 | h: Place::Start, 64 | v: Place::Start, 65 | }), 66 | "⿹" => Self::Surround(DataHV { 67 | h: Place::End, 68 | v: Place::Start, 69 | }), 70 | "⿺" => Self::Surround(DataHV { 71 | h: Place::Start, 72 | v: Place::End, 73 | }), 74 | "⿽" => Self::Surround(DataHV { 75 | h: Place::End, 76 | v: Place::End, 77 | }), 78 | "⿵" => Self::Surround(DataHV { 79 | h: Place::Middle, 80 | v: Place::Start, 81 | }), 82 | "⿶" => Self::Surround(DataHV { 83 | h: Place::Middle, 84 | v: Place::End, 85 | }), 86 | "⿷" => Self::Surround(DataHV { 87 | h: Place::Start, 88 | v: Place::Middle, 89 | }), 90 | "⿼" => Self::Surround(DataHV { 91 | h: Place::End, 92 | v: Place::Middle, 93 | }), 94 | "⿴" => Self::Surround(DataHV { 95 | h: Place::Middle, 96 | v: Place::Middle, 97 | }), 98 | _ => return None, 99 | }; 100 | Some(tp) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/fas.rs: -------------------------------------------------------------------------------- 1 | use crate::{config::Config, construct::Components}; 2 | 3 | use anyhow::Result; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | pub struct FasFile { 8 | pub name: String, 9 | pub version: String, 10 | pub components: Components, 11 | pub config: Config, 12 | } 13 | 14 | impl FasFile { 15 | pub fn from_file(path: &str) -> Result { 16 | Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) 17 | } 18 | 19 | pub fn save(&self, path: &str) -> Result { 20 | let texts = serde_json::to_string(self)?; 21 | Ok(std::fs::write(path, &texts).and_then(|_| Ok(texts.len()))?) 22 | } 23 | 24 | pub fn save_pretty(&self, path: &str) -> Result { 25 | let texts = serde_json::to_string_pretty(self)?; 26 | Ok(std::fs::write(path, &texts).and_then(|_| Ok(texts.len()))?) 27 | } 28 | } 29 | 30 | impl std::default::Default for FasFile { 31 | fn default() -> Self { 32 | Self { 33 | name: "untile".to_string(), 34 | version: "0.1".to_string(), 35 | components: Default::default(), 36 | config: Default::default(), 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn test_fas_file() { 47 | use crate::{component::struc::StrucProto, construct::space::*}; 48 | 49 | let mut test_file = FasFile::default(); 50 | 51 | let struc = StrucProto { 52 | paths: vec![KeyPath::from([ 53 | IndexPoint::new(0, 0), 54 | IndexPoint::new(2, 0), 55 | IndexPoint::new(2, 2), 56 | IndexPoint::new(0, 2), 57 | IndexPoint::new(0, 0), 58 | ])], 59 | attrs: Default::default(), 60 | }; 61 | test_file.components.insert("口".to_string(), struc); 62 | 63 | test_file.config.type_replace.insert( 64 | '⿰', 65 | std::collections::BTreeMap::from([( 66 | crate::axis::Place::Start, 67 | std::collections::BTreeMap::from([( 68 | "王".to_string(), 69 | crate::construct::Component::from_name("王字旁"), 70 | )]), 71 | )]), 72 | ); 73 | 74 | test_file.config.supplement.insert( 75 | "無".to_string(), 76 | crate::construct::CpAttrs { 77 | tp: crate::construct::CstType::Scale(crate::axis::Axis::Vertical), 78 | components: vec![ 79 | crate::construct::Component::from_name("無字头"), 80 | crate::construct::Component::from_name("灬"), 81 | ], 82 | }, 83 | ); 84 | 85 | test_file 86 | .config 87 | .interval 88 | .rules 89 | .push(crate::config::interval::IntervalMatch { 90 | axis: None, 91 | inverse: false, 92 | rule1: serde_json::from_value(serde_json::json!([ 93 | "*", ">-1", "*", ">-1", "*", ">-1", "*", ">-1", "*" 94 | ])) 95 | .unwrap(), 96 | rule2: serde_json::from_value(serde_json::json!([ 97 | "*", ">-1", "*", ">-1", "*", ">-1", "*", ">-1", "*" 98 | ])) 99 | .unwrap(), 100 | note: "default".to_string(), 101 | val: 2, 102 | }); 103 | 104 | test_file 105 | .config 106 | .process_control 107 | .push(crate::config::process::SpaceProcess::CompCenter( 108 | Default::default(), 109 | )); 110 | 111 | let tmp_dir = std::path::Path::new("tmp"); 112 | if !tmp_dir.exists() { 113 | std::fs::create_dir(tmp_dir).unwrap(); 114 | } 115 | std::fs::write( 116 | tmp_dir.join("fas_file.fas"), 117 | serde_json::to_string_pretty(&test_file).unwrap(), 118 | ) 119 | .unwrap(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod algorithm; 2 | pub mod axis; 3 | pub mod component; 4 | pub mod config; 5 | pub mod construct; 6 | pub mod fas; 7 | pub mod service; 8 | -------------------------------------------------------------------------------- /core/src/service.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | axis::*, 3 | component::{ 4 | comb::{CharInfo, StrucComb}, 5 | struc::*, 6 | }, 7 | config::Config, 8 | construct::{self, space::*, CharTree, Component, CpAttrs, CstError, CstTable, CstType}, 9 | fas::FasFile, 10 | }; 11 | 12 | pub mod combination { 13 | use super::*; 14 | 15 | fn check_name( 16 | name: &str, 17 | tp: CstType, 18 | in_tp: Place, 19 | adjacency: DataHV<[bool; 2]>, 20 | cfg: &Config, 21 | ) -> Option { 22 | match cfg.type_replace_name(name, tp, in_tp) { 23 | Some(comp) => cfg 24 | .place_replace_name(&comp.name(), adjacency) 25 | .or(Some(comp)), 26 | None => cfg.place_replace_name(name, adjacency), 27 | } 28 | } 29 | 30 | fn get_current_attr( 31 | name: String, 32 | tp: CstType, 33 | in_tp: Place, 34 | adjacency: DataHV<[bool; 2]>, 35 | table: &CstTable, 36 | cfg: &Config, 37 | ) -> (String, CpAttrs) { 38 | let comp = check_name(&name, tp, in_tp, adjacency, cfg).unwrap_or(Component::Char(name)); 39 | 40 | match comp { 41 | Component::Char(c_name) => { 42 | let attrs = cfg 43 | .supplement 44 | .get(&c_name) 45 | .or(table.get(&c_name)) 46 | .cloned() 47 | .unwrap_or(CpAttrs::single()); 48 | (c_name, attrs) 49 | } 50 | Component::Complex(attrs) => (attrs.comps_name(), attrs), 51 | } 52 | } 53 | 54 | pub fn get_char_tree(name: String, table: &CstTable, cfg: &Config) -> CharTree { 55 | get_component_in( 56 | name, 57 | CstType::Single, 58 | Place::Start, 59 | Default::default(), 60 | table, 61 | cfg, 62 | ) 63 | } 64 | 65 | fn get_component_in( 66 | name: String, 67 | tp: CstType, 68 | in_tp: Place, 69 | adjacency: DataHV<[bool; 2]>, 70 | table: &CstTable, 71 | cfg: &Config, 72 | ) -> CharTree { 73 | let (name, attrs) = get_current_attr(name, tp, in_tp, adjacency, table, cfg); 74 | get_component_from_attr(name, attrs, adjacency, table, cfg) 75 | } 76 | 77 | fn get_component_from_attr( 78 | name: String, 79 | attrs: CpAttrs, 80 | adjacency: DataHV<[bool; 2]>, 81 | table: &CstTable, 82 | cfg: &Config, 83 | ) -> CharTree { 84 | match attrs.tp { 85 | CstType::Single => CharTree::new_single(name), 86 | CstType::Scale(axis) => { 87 | let end = attrs.components.len(); 88 | let children = attrs 89 | .components 90 | .into_iter() 91 | .enumerate() 92 | .map(|(i, c)| { 93 | let mut c_in_place = adjacency.clone(); 94 | if i != 0 { 95 | c_in_place.hv_get_mut(axis)[0] = true; 96 | } 97 | if i + 1 != end { 98 | c_in_place.hv_get_mut(axis)[1] = true; 99 | } 100 | let in_tp = match i { 101 | 0 => Place::Start, 102 | n if n + 1 == end => Place::End, 103 | _ => Place::Middle, 104 | }; 105 | match c { 106 | Component::Char(c_name) => get_component_in( 107 | c_name.to_string(), 108 | attrs.tp, 109 | in_tp, 110 | c_in_place, 111 | table, 112 | cfg, 113 | ), 114 | Component::Complex(c_attrs) => match check_name( 115 | &c_attrs.comps_name(), 116 | attrs.tp, 117 | in_tp, 118 | adjacency, 119 | cfg, 120 | ) { 121 | Some(Component::Char(map_name)) => get_component_in( 122 | map_name, attrs.tp, in_tp, c_in_place, table, cfg, 123 | ), 124 | Some(Component::Complex(map_attrs)) => get_component_from_attr( 125 | map_attrs.comps_name(), 126 | c_attrs, 127 | c_in_place, 128 | table, 129 | cfg, 130 | ), 131 | None => get_component_from_attr( 132 | c_attrs.comps_name(), 133 | c_attrs, 134 | c_in_place, 135 | table, 136 | cfg, 137 | ), 138 | }, 139 | } 140 | }) 141 | .collect(); 142 | CharTree { 143 | name, 144 | tp: attrs.tp, 145 | children, 146 | } 147 | } 148 | CstType::Surround(surround_place) => { 149 | fn remap_comp( 150 | comp: &Component, 151 | tp: CstType, 152 | in_tp: Place, 153 | in_place: DataHV<[bool; 2]>, 154 | table: &CstTable, 155 | cfg: &Config, 156 | ) -> (String, CpAttrs) { 157 | match comp { 158 | Component::Char(c_name) => { 159 | get_current_attr(c_name.to_string(), tp, in_tp, in_place, table, cfg) 160 | } 161 | Component::Complex(c_attrs) => { 162 | match check_name(&c_attrs.comps_name(), tp, in_tp, in_place, cfg) { 163 | Some(Component::Char(map_name)) => { 164 | get_current_attr(map_name, tp, in_tp, in_place, table, cfg) 165 | } 166 | Some(Component::Complex(map_attrs)) => { 167 | (map_attrs.comps_name(), map_attrs) 168 | } 169 | None => (c_attrs.comps_name(), c_attrs.clone()), 170 | } 171 | } 172 | } 173 | } 174 | 175 | let mut primary: (String, CpAttrs) = remap_comp( 176 | &attrs.components[0], 177 | attrs.tp, 178 | Place::Start, 179 | adjacency, 180 | table, 181 | cfg, 182 | ); 183 | match primary.1.tp { 184 | CstType::Scale(c_axis) => { 185 | let index = match surround_place.hv_get(c_axis) { 186 | Place::Start => primary.1.components.len() - 1, 187 | Place::End => 0, 188 | Place::Middle => panic!( 189 | "{} is surround component in {}", 190 | primary.1.tp.symbol(), 191 | CstType::Surround(surround_place).symbol() 192 | ), 193 | }; 194 | primary.1.components[index] = Component::Complex(CpAttrs { 195 | tp: CstType::Surround(surround_place), 196 | components: vec![ 197 | primary.1.components[index].clone(), 198 | attrs.components[1].clone(), 199 | ], 200 | }); 201 | get_component_from_attr(name, primary.1, adjacency, table, cfg) 202 | } 203 | CstType::Surround(c_surround) => { 204 | if c_surround == surround_place { 205 | let sc1 = primary.1.components.pop().unwrap(); 206 | let pc = primary.1.components.pop().unwrap(); 207 | let sc = if c_surround.v == Place::End { 208 | vec![attrs.components[1].clone(), sc1] 209 | } else { 210 | vec![sc1, attrs.components[1].clone()] 211 | }; 212 | let new_attrs = CpAttrs { 213 | tp: attrs.tp, 214 | components: vec![ 215 | pc, 216 | Component::Complex(CpAttrs { 217 | tp: CstType::Scale(Axis::Vertical), 218 | components: sc, 219 | }), 220 | ], 221 | }; 222 | get_component_from_attr(name, new_attrs, adjacency, table, cfg) 223 | } else { 224 | panic!( 225 | "{} is surround component in {}", 226 | primary.1.tp.symbol(), 227 | CstType::Surround(surround_place).symbol() 228 | ) 229 | } 230 | } 231 | CstType::Single => { 232 | let mut in_place = [adjacency.clone(); 2]; 233 | Axis::list().into_iter().for_each(|axis| { 234 | let surround_place = *surround_place.hv_get(axis); 235 | if surround_place != Place::End { 236 | // in_place[0].hv_get_mut(axis)[1] = true; 237 | in_place[1].hv_get_mut(axis)[0] = true; 238 | } 239 | if surround_place != Place::Start { 240 | // in_place[0].hv_get_mut(axis)[0] = true; 241 | in_place[1].hv_get_mut(axis)[1] = true; 242 | } 243 | }); 244 | let secondery = remap_comp( 245 | &attrs.components[1], 246 | attrs.tp, 247 | Place::End, 248 | in_place[1], 249 | table, 250 | cfg, 251 | ); 252 | 253 | CharTree { 254 | name, 255 | tp: attrs.tp, 256 | children: vec![ 257 | get_component_from_attr( 258 | primary.0, 259 | primary.1, 260 | in_place[0], 261 | table, 262 | cfg, 263 | ), 264 | get_component_from_attr( 265 | secondery.0, 266 | secondery.1, 267 | in_place[1], 268 | table, 269 | cfg, 270 | ), 271 | ], 272 | } 273 | } 274 | } 275 | } 276 | } 277 | } 278 | 279 | pub fn gen_comb_proto( 280 | target: CharTree, 281 | table: &CstTable, 282 | fas: &FasFile, 283 | ) -> Result { 284 | gen_comb_proto_in(target, DataHV::splat([false; 2]), table, fas) 285 | } 286 | 287 | pub fn gen_comb_proto_in( 288 | mut target: CharTree, 289 | adjacency: DataHV<[bool; 2]>, 290 | table: &CstTable, 291 | fas: &FasFile, 292 | ) -> Result { 293 | let components = &fas.components; 294 | match target.tp { 295 | CstType::Single => { 296 | let mut proto = match components.get(&target.name) { 297 | Some(proto) if !proto.paths.is_empty() => proto.clone(), 298 | _ => { 299 | return Err(CstError::Empty(target.name)); 300 | } 301 | }; 302 | 303 | proto.set_allocs_in_adjacency(adjacency); 304 | 305 | Ok(StrucComb::new_single(target.name, proto)) 306 | } 307 | CstType::Scale(axis) => { 308 | let mut combs = vec![]; 309 | let children = target.children; 310 | 311 | let end = children.len(); 312 | for (i, c_target) in children.into_iter().enumerate() { 313 | let mut c_in_place = adjacency.clone(); 314 | if i != 0 { 315 | c_in_place.hv_get_mut(axis)[0] = true; 316 | } 317 | if i + 1 != end { 318 | c_in_place.hv_get_mut(axis)[1] = true; 319 | } 320 | 321 | combs.push(gen_comb_proto_in(c_target, c_in_place, table, fas)?); 322 | } 323 | Ok(StrucComb::new_complex(target.name, target.tp, combs)) 324 | } 325 | CstType::Surround(surround_place) => { 326 | let mut in_place = [adjacency.clone(); 2]; 327 | Axis::list().into_iter().for_each(|axis| { 328 | let surround_place = *surround_place.hv_get(axis); 329 | if surround_place != Place::End { 330 | in_place[0].hv_get_mut(axis)[1] = true; 331 | // in_place[1].hv_get_mut(axis)[0] = true; 332 | } 333 | if surround_place != Place::Start { 334 | // in_place[0].hv_get_mut(axis)[0] = true; 335 | in_place[1].hv_get_mut(axis)[1] = true; 336 | } 337 | }); 338 | 339 | let secondery = target.children.pop().unwrap(); 340 | let primary = target.children.pop().unwrap(); 341 | Ok(StrucComb::new_complex( 342 | target.name, 343 | target.tp, 344 | vec![ 345 | gen_comb_proto_in(primary, in_place[0], table, fas)?, 346 | gen_comb_proto_in(secondery, in_place[1], table, fas)?, 347 | ], 348 | )) 349 | } 350 | } 351 | } 352 | } 353 | 354 | pub struct LocalService { 355 | changed: bool, 356 | source: Option, 357 | pub construct_table: construct::CstTable, 358 | } 359 | 360 | impl LocalService { 361 | pub fn new() -> Self { 362 | Self { 363 | changed: false, 364 | source: None, 365 | construct_table: construct::CstTable::default(), 366 | } 367 | } 368 | 369 | pub fn is_changed(&self) -> bool { 370 | self.changed 371 | } 372 | 373 | pub fn save(&mut self, path: &str) -> anyhow::Result<()> { 374 | match &self.source { 375 | Some(source) => source.save_pretty(path).map(|_| { 376 | self.changed = false; 377 | () 378 | }), 379 | None => Ok(()), 380 | } 381 | } 382 | 383 | pub fn save_struc(&mut self, name: String, struc: StrucProto) { 384 | if let Some(source) = &mut self.source { 385 | source.components.insert(name, struc); 386 | self.changed = true; 387 | } 388 | } 389 | 390 | pub fn set_config(&mut self, cfg: Config) { 391 | if let Some(source) = &mut self.source { 392 | source.config = cfg; 393 | self.changed = true; 394 | } 395 | } 396 | 397 | pub fn load_fas(&mut self, data: FasFile) { 398 | data.config.supplement.iter().for_each(|(ch, attr)| { 399 | self.construct_table.insert(ch.to_string(), attr.clone()); 400 | }); 401 | self.source = Some(data); 402 | self.changed = false; 403 | } 404 | 405 | pub fn load_file(&mut self, path: &str) -> Result<(), String> { 406 | match FasFile::from_file(path) { 407 | Ok(data) => { 408 | self.load_fas(data); 409 | Ok(()) 410 | } 411 | Err(e) => Err(format!("{:?}", e)), 412 | } 413 | } 414 | 415 | pub fn source(&self) -> Option<&FasFile> { 416 | self.source.as_ref() 417 | } 418 | 419 | pub fn get_struc_proto(&self, name: &str) -> StrucProto { 420 | match &self.source { 421 | Some(source) => source.components.get(name).cloned().unwrap_or_default(), 422 | None => Default::default(), 423 | } 424 | } 425 | 426 | pub fn gen_char_tree(&self, name: String) -> CharTree { 427 | combination::get_char_tree( 428 | name, 429 | &self.construct_table, 430 | self.source() 431 | .map(|s| &s.config) 432 | .unwrap_or(&Default::default()), 433 | ) 434 | } 435 | 436 | pub fn gen_comp_visible_path( 437 | &self, 438 | target: CharTree, 439 | ) -> Result<(Vec>, CharTree), CstError> { 440 | match self.source() { 441 | Some(source) => { 442 | let mut comb = combination::gen_comb_proto(target, &self.construct_table, &source)?; 443 | comb.expand_comb_proto(&source, &self.construct_table, false)?; 444 | let paths = comb 445 | .to_paths() 446 | .into_iter() 447 | .filter_map(|path| match path.hide { 448 | true => None, 449 | false => Some(path.points), 450 | }) 451 | .collect(); 452 | let tree = comb.get_char_tree(); 453 | 454 | Ok((paths, tree)) 455 | } 456 | None => Err(CstError::Empty("Source".to_string())), 457 | } 458 | } 459 | 460 | pub fn gen_char_info(&self, name: String) -> Result { 461 | match self.source() { 462 | Some(source) => { 463 | let target = self.gen_char_tree(name); 464 | let comp_name = target.get_comb_name(); 465 | 466 | let info = combination::gen_comb_proto(target, &self.construct_table, &source) 467 | .map(|mut comb| { 468 | match comb.expand_comb_proto(&source, &self.construct_table, true) { 469 | Ok(info) => info.unwrap(), 470 | Err(_) => { 471 | let mut info = CharInfo::default(); 472 | info.comb_name = comb.get_comb_name(); 473 | info 474 | } 475 | } 476 | }) 477 | .unwrap_or_else(|_| { 478 | let mut info = CharInfo::default(); 479 | info.comb_name = comp_name; 480 | info 481 | }); 482 | 483 | Ok(info) 484 | } 485 | None => Err(CstError::Empty("Source".to_string())), 486 | } 487 | } 488 | } 489 | 490 | mod tests { 491 | #[test] 492 | fn test_correction_table() { 493 | use super::*; 494 | 495 | let mut data = FasFile::default(); 496 | let attr = CpAttrs { 497 | tp: CstType::Scale(Axis::Horizontal), 498 | components: vec![ 499 | Component::Char("一".to_string()), 500 | Component::Char("一".to_string()), 501 | ], 502 | }; 503 | data.config 504 | .supplement 505 | .insert(String::from("二"), attr.clone()); 506 | 507 | let mut service = LocalService::new(); 508 | service.load_fas(data); 509 | 510 | let cur_attr = &service.construct_table["二"]; 511 | assert_eq!(cur_attr.tp, attr.tp); 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /editor/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/ 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | */backup/ -------------------------------------------------------------------------------- /editor/README.md: -------------------------------------------------------------------------------- 1 | # Tauri + React 2 | 3 | This template should help get you started developing with Tauri and React in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | - [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) 8 | -------------------------------------------------------------------------------- /editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Fasing 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fasing-editor", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri" 11 | }, 12 | "dependencies": { 13 | "@tauri-apps/api": "^2", 14 | "@tauri-apps/plugin-dialog": "^2.2.0", 15 | "@tauri-apps/plugin-opener": "^2", 16 | "antd": "^5.24.1", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1", 19 | "use-immer": "^0.11.0" 20 | }, 21 | "devDependencies": { 22 | "@tauri-apps/cli": "^2", 23 | "@vitejs/plugin-react": "^4.3.4", 24 | "vite": "^6.1.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /editor/public/editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Struc Editor 9 | 10 | 11 | 12 |
13 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /editor/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /editor/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fasing-editor" 3 | version = "0.1.0" 4 | description = "Fasing Editor" 5 | authors = ["chilingg"] 6 | license = "MIT" 7 | edition = "2021" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [lib] 12 | # The `_lib` suffix may seem redundant but it is necessary 13 | # to make the lib name unique and wouldn't conflict with the bin name. 14 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 15 | name = "fasing_editor_lib" 16 | crate-type = ["staticlib", "cdylib", "rlib"] 17 | 18 | [build-dependencies] 19 | tauri-build = { version = "2", features = [] } 20 | 21 | [dependencies] 22 | tauri = { version = "2", features = [] } 23 | tauri-plugin-opener = "2" 24 | serde = { version = "1", features = ["derive"] } 25 | serde_json = "1" 26 | tauri-plugin-dialog = "2.2.0" 27 | 28 | fasing = { path = "../../core" } 29 | directories-next = "2.0.0" 30 | -------------------------------------------------------------------------------- /editor/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /editor/src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "main", 7 | "struc-editor" 8 | ], 9 | "permissions": [ 10 | "core:default", 11 | "core:window:default", 12 | "dialog:allow-open" 13 | ] 14 | } -------------------------------------------------------------------------------- /editor/src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /editor/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /editor/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /editor/src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilingg/fasing/463c41065614f8b96e0404e824b1be1870c4bdfa/editor/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /editor/src-tauri/src/context.rs: -------------------------------------------------------------------------------- 1 | use directories_next::ProjectDirs; 2 | use std::{fs, path::PathBuf}; 3 | 4 | extern crate serde_json as sj; 5 | use serde_json::json; 6 | 7 | pub struct Context { 8 | path: PathBuf, 9 | data: sj::Map, 10 | } 11 | 12 | impl Default for Context { 13 | fn default() -> Self { 14 | let proj_dirs = ProjectDirs::from("com", "fasing", "Fasing Editor") 15 | .expect("Error accessing Project context directory!"); 16 | fs::create_dir_all(proj_dirs.data_dir()) 17 | .expect("Error creating project context directory!"); 18 | 19 | let path = proj_dirs.data_dir().join("context.json").to_path_buf(); 20 | let data = match fs::read_to_string(&path) { 21 | Ok(str) => sj::from_str::(&str) 22 | .map(|obj| match obj { 23 | sj::Value::Object(o) => o, 24 | _ => Default::default(), 25 | }) 26 | .unwrap_or_default(), 27 | Err(e) => { 28 | match e.kind() { 29 | std::io::ErrorKind::NotFound => {} 30 | kind => println!("{:?}", kind), 31 | } 32 | Default::default() 33 | } 34 | }; 35 | 36 | Self { path, data } 37 | } 38 | } 39 | 40 | impl Context { 41 | fn recursion( 42 | source: &serde_json::Map, 43 | index: &[serde_json::Value], 44 | ) -> Option { 45 | match &index[0] { 46 | serde_json::Value::String(key) => match source.get(key) { 47 | Some(value) => match index.len() { 48 | 1 => Some(value.clone()), 49 | _ if value.is_object() => { 50 | Self::recursion(value.as_object().unwrap(), &index[1..]) 51 | } 52 | _ => None, 53 | }, 54 | None => None, 55 | }, 56 | _ => None, 57 | } 58 | } 59 | 60 | fn recursion_set<'a>( 61 | source: &'a mut serde_json::Map, 62 | index: &[serde_json::Value], 63 | value: serde_json::Value, 64 | ) -> bool { 65 | match &index[0] { 66 | serde_json::Value::String(key) => { 67 | let next = source.entry(key).or_insert(json!({})); 68 | match index.len() { 69 | 1 => { 70 | *next = value; 71 | true 72 | } 73 | _ if next.is_object() => { 74 | Self::recursion_set(next.as_object_mut().unwrap(), &index[1..], value) 75 | } 76 | _ => false, 77 | } 78 | } 79 | _ => false, 80 | } 81 | } 82 | 83 | pub fn get(&self, index: serde_json::Value) -> Option { 84 | match &index { 85 | serde_json::Value::Array(array) => { 86 | if array.is_empty() { 87 | None 88 | } else { 89 | Self::recursion(&self.data, array) 90 | } 91 | } 92 | serde_json::Value::String(key) => self.data.get(key).cloned(), 93 | _ => None, 94 | } 95 | } 96 | 97 | pub fn set(&mut self, index: serde_json::Value, value: serde_json::Value) -> bool { 98 | match &index { 99 | serde_json::Value::Array(array) => { 100 | if array.is_empty() { 101 | false 102 | } else { 103 | Self::recursion_set(&mut self.data, array, value) 104 | } 105 | } 106 | serde_json::Value::String(key) => { 107 | self.data.insert(key.clone(), value); 108 | true 109 | } 110 | _ => false, 111 | } 112 | } 113 | 114 | pub fn save(&self) -> std::io::Result<()> { 115 | fs::write( 116 | &self.path, 117 | serde_json::to_string_pretty(&self.data).expect("Error pasing context!"), 118 | ) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /editor/src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod context; 2 | use fasing::{ 3 | component::struc::StrucProto, 4 | construct::{space::WorkPoint, CharTree, CstError, CstTable}, 5 | service::LocalService, 6 | }; 7 | 8 | use serde_json::json; 9 | use std::sync::Mutex; 10 | use tauri::{AppHandle, Emitter, Manager, State}; 11 | use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; 12 | 13 | type Context = Mutex; 14 | type Service = Mutex; 15 | 16 | mod signal { 17 | pub const SOURCE: &'static str = "source"; 18 | pub const CHANGED: &'static str = "changed"; 19 | pub const SAVED: &'static str = "saved"; 20 | 21 | #[derive(Debug, Clone, serde::Serialize)] 22 | pub struct Payload { 23 | target: String, 24 | value: T, 25 | } 26 | 27 | impl Payload { 28 | pub fn new(target: String, value: T) -> Self { 29 | Self { target, value } 30 | } 31 | } 32 | } 33 | 34 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 35 | 36 | #[tauri::command] 37 | fn new_source( 38 | path: &str, 39 | service: State, 40 | context: State, 41 | app: AppHandle, 42 | ) -> Result<(), String> { 43 | let mut service = service.lock().unwrap(); 44 | match service.load_file(path) { 45 | Ok(_) => { 46 | set_window_title_as_serviceinfo( 47 | &app.get_webview_window("main").unwrap(), 48 | &ServiceInfo::new(&service, path), 49 | ); 50 | app.emit( 51 | signal::SOURCE, 52 | signal::Payload::new("open".to_string(), path.to_string()), 53 | ) 54 | .expect("Emit event `source_load` error!"); 55 | context.lock().unwrap().set(json!("source"), json!(path)); 56 | 57 | Ok(()) 58 | } 59 | Err(e) => Err(e.to_string()), 60 | } 61 | } 62 | 63 | #[tauri::command] 64 | fn reload(service: State, context: State, app: AppHandle) { 65 | match context.lock().unwrap().get(json!("source")) { 66 | Some(path) if path.is_string() => { 67 | let mut service = service.lock().unwrap(); 68 | if service.is_changed() { 69 | if !app 70 | .dialog() 71 | .message("文件未保存,是否重新载入?") 72 | .title("Warning!") 73 | .blocking_show() 74 | { 75 | return; 76 | } 77 | } 78 | let _ = service.load_file(path.as_str().unwrap()); 79 | app.emit( 80 | signal::SOURCE, 81 | signal::Payload::new("reload".to_string(), path.to_string()), 82 | ) 83 | .expect("Emit event `source_load` error!"); 84 | } 85 | _ => {} 86 | } 87 | } 88 | 89 | #[tauri::command] 90 | fn target_chars(service: State) -> Vec { 91 | let mut list = vec![]; 92 | let s = service.lock().unwrap(); 93 | if s.source().is_some() { 94 | list = s.construct_table.target_chars(); 95 | list.sort(); 96 | } 97 | list 98 | } 99 | 100 | #[tauri::command] 101 | fn get_char_tree(service: State, name: String) -> CharTree { 102 | service.lock().unwrap().gen_char_tree(name) 103 | } 104 | 105 | #[tauri::command] 106 | fn get_cst_table(service: State) -> CstTable { 107 | service.lock().unwrap().construct_table.clone() 108 | } 109 | 110 | #[tauri::command] 111 | fn gen_comp_path( 112 | service: State, 113 | target: CharTree, 114 | ) -> Result<(Vec>, CharTree), CstError> { 115 | service.lock().unwrap().gen_comp_visible_path(target) 116 | } 117 | 118 | #[tauri::command(async)] 119 | fn open_struc_editor(app: tauri::AppHandle, name: String) { 120 | tauri::WebviewWindowBuilder::new( 121 | &app, 122 | "struc-editor", 123 | tauri::WebviewUrl::App("editor.html".into()), 124 | ) 125 | .title(&name) 126 | .center() 127 | .inner_size(800.0, 600.0) 128 | .build() 129 | .expect("Unable to create editing window!"); 130 | } 131 | 132 | #[tauri::command] 133 | fn get_struc_editor_data(app: AppHandle) -> (String, StrucProto) { 134 | let name = app 135 | .get_webview_window("struc-editor") 136 | .unwrap() 137 | .title() 138 | .unwrap(); 139 | let service = app.state::(); 140 | let proto = service.lock().unwrap().get_struc_proto(&name); 141 | (name, proto) 142 | } 143 | 144 | #[tauri::command] 145 | fn save_struc(service: State, handle: tauri::AppHandle, name: String, struc: StrucProto) { 146 | let mut service = service.lock().unwrap(); 147 | service.save_struc(name.clone(), struc); 148 | 149 | let main_window = handle.get_webview_window("main").unwrap(); 150 | main_window 151 | .emit( 152 | signal::CHANGED, 153 | signal::Payload::new("struc".to_string(), name), 154 | ) 155 | .unwrap(); 156 | } 157 | 158 | #[tauri::command] 159 | fn save_fas_file(service: State, context: State, window: tauri::Window) { 160 | use tauri_plugin_dialog::DialogExt; 161 | 162 | let mut service_data = service.lock().unwrap(); 163 | 164 | let r = match context.lock().unwrap().get(json!("source")) { 165 | Some(path) if path.is_string() => service_data.save(path.as_str().unwrap()), 166 | _ => { 167 | if let Some(path) = window.dialog().file().blocking_pick_file() { 168 | service_data.save(&path.to_string()) 169 | } else { 170 | Ok(()) 171 | } 172 | } 173 | }; 174 | 175 | match r { 176 | Ok(_) => window.emit(signal::SAVED, 0).unwrap(), 177 | Err(e) => eprintln!("{}", e), 178 | } 179 | } 180 | 181 | #[tauri::command] 182 | fn is_changed(service: State) -> bool { 183 | service.lock().unwrap().is_changed() 184 | } 185 | 186 | #[tauri::command] 187 | fn get_config(service: State) -> Option { 188 | service.lock().unwrap().source().map(|s| s.config.clone()) 189 | } 190 | 191 | #[tauri::command] 192 | fn set_config(service: State, cfg: fasing::config::Config, window: tauri::Window) { 193 | let mut service = service.lock().unwrap(); 194 | service.set_config(cfg); 195 | 196 | window.emit(signal::CHANGED, "config").unwrap(); 197 | } 198 | 199 | #[tauri::command] 200 | fn get_char_info( 201 | service: State, 202 | name: String, 203 | ) -> Result { 204 | service.lock().unwrap().gen_char_info(name) 205 | } 206 | 207 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 208 | pub fn run() { 209 | let (context, service, win_state, service_info) = init(); 210 | 211 | tauri::Builder::default() 212 | .setup(|app| { 213 | app.manage(service); 214 | app.manage(context); 215 | 216 | let main_window = app.get_webview_window("main").unwrap(); 217 | 218 | if let Some(win_state) = win_state { 219 | main_window 220 | .set_size(win_state.size) 221 | .expect("Unable to set size!"); 222 | main_window 223 | .set_position(win_state.pos) 224 | .expect("Unable to set position!"); 225 | if win_state.maximized { 226 | main_window.maximize().expect("Unable to set maximize!"); 227 | } 228 | } 229 | if let Some(info) = service_info { 230 | set_window_title_as_serviceinfo(&main_window, &info); 231 | } 232 | 233 | Ok(()) 234 | }) 235 | .on_window_event(|window, event| match event { 236 | tauri::WindowEvent::CloseRequested { api, .. } => { 237 | if window.label() == "main" { 238 | let app = window.app_handle(); 239 | let service = app.state::(); 240 | let context = app.state::(); 241 | if service.lock().unwrap().is_changed() { 242 | api.prevent_close(); 243 | if window 244 | .dialog() 245 | .message("文件未保存,确定是否关闭当前应用?") 246 | .kind(MessageDialogKind::Warning) 247 | .blocking_show() 248 | { 249 | exit_save(context, &window); 250 | window.destroy().unwrap(); 251 | } 252 | } else { 253 | exit_save(context, &window); 254 | } 255 | } 256 | } 257 | _ => {} 258 | }) 259 | .plugin(tauri_plugin_dialog::init()) 260 | .invoke_handler(tauri::generate_handler![ 261 | new_source, 262 | reload, 263 | target_chars, 264 | get_char_tree, 265 | get_cst_table, 266 | gen_comp_path, 267 | open_struc_editor, 268 | get_struc_editor_data, 269 | save_struc, 270 | save_fas_file, 271 | is_changed, 272 | get_config, 273 | set_config, 274 | get_char_info 275 | ]) 276 | .run(tauri::generate_context!()) 277 | .expect("error while running tauri application"); 278 | } 279 | 280 | struct ServiceInfo { 281 | file_name: String, 282 | name: String, 283 | major_version: u32, 284 | minor_version: u32, 285 | } 286 | 287 | impl ServiceInfo { 288 | pub fn new(service: &LocalService, path: &str) -> ServiceInfo { 289 | let source = service.source().unwrap(); 290 | let versions = { 291 | let mut iter = source 292 | .version 293 | .split('.') 294 | .map(|n| n.parse::().unwrap_or_default()); 295 | let mut versions = vec![]; 296 | for _ in 0..2 { 297 | versions.push(iter.next().unwrap_or_default()) 298 | } 299 | versions 300 | }; 301 | ServiceInfo { 302 | file_name: std::path::Path::new(path) 303 | .file_name() 304 | .unwrap() 305 | .to_str() 306 | .unwrap() 307 | .to_string(), 308 | name: source.name.clone(), 309 | major_version: versions[0], 310 | minor_version: versions[1], 311 | } 312 | } 313 | } 314 | 315 | #[derive(serde::Serialize, serde::Deserialize)] 316 | struct WindowState { 317 | maximized: bool, 318 | size: tauri::LogicalSize, 319 | pos: tauri::LogicalPosition, 320 | } 321 | 322 | impl WindowState { 323 | pub fn new(window: &tauri::Window) -> tauri::Result { 324 | let factor = window.scale_factor()?; 325 | 326 | Ok(Self { 327 | maximized: window.is_maximized()?, 328 | size: window.inner_size()?.to_logical::(factor), 329 | pos: window.outer_position()?.to_logical::(factor), 330 | }) 331 | } 332 | } 333 | 334 | fn init() -> (Context, Service, Option, Option) { 335 | let mut service = LocalService::new(); 336 | let context = context::Context::default(); 337 | 338 | let sinfo = context 339 | .get(json!("source")) 340 | .and_then(|path| match path { 341 | serde_json::Value::String(s) => Some(s), 342 | _ => None, 343 | }) 344 | .and_then(|path| match service.load_file(&path) { 345 | Err(e) => { 346 | eprintln!("Failed open service source: {:?}", e); 347 | None 348 | } 349 | Ok(_) => Some(ServiceInfo::new(&service, &path)), 350 | }); 351 | 352 | let wstate = context 353 | .get(json!("window")) 354 | .and_then(|data| serde_json::from_value::(data).ok()); 355 | 356 | (Mutex::new(context), Mutex::new(service), wstate, sinfo) 357 | } 358 | 359 | fn exit_save(context: State<'_, Context>, window: &tauri::Window) { 360 | let mut guard = context.lock(); 361 | let context = guard.as_mut().unwrap(); 362 | if let Ok(win_state) = WindowState::new(window) { 363 | context.set(json!("window"), serde_json::to_value(win_state).unwrap()); 364 | } 365 | 366 | if let Err(e) = context.save() { 367 | eprintln!("{:?}: {}", e, "Save context error!"); 368 | }; 369 | } 370 | 371 | fn set_window_title_as_serviceinfo(window: &tauri::WebviewWindow, info: &ServiceInfo) { 372 | window 373 | .set_title( 374 | format!( 375 | "{} - {} {}.{} - 繁星", 376 | info.file_name, info.name, info.major_version, info.minor_version 377 | ) 378 | .as_str(), 379 | ) 380 | .expect("Unable to set title!"); 381 | } 382 | -------------------------------------------------------------------------------- /editor/src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | fasing_editor_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /editor/src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "fasing-editor", 4 | "version": "0.1.0", 5 | "identifier": "com.fasing-editor.app", 6 | "build": { 7 | "beforeDevCommand": "npm run dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "npm run build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "fullscreen": false, 16 | "height": 800, 17 | "resizable": true, 18 | "title": "繁星", 19 | "width": 1024 20 | } 21 | ], 22 | "security": { 23 | "csp": null 24 | } 25 | }, 26 | "bundle": { 27 | "active": true, 28 | "targets": "all", 29 | "icon": [ 30 | "icons/32x32.png", 31 | "icons/128x128.png", 32 | "icons/128x128@2x.png", 33 | "icons/icon.icns", 34 | "icons/icon.ico" 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /editor/src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } -------------------------------------------------------------------------------- /editor/src/App.jsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import Middle from './widget/middle/Middle'; 3 | import Right from './widget/right/Right'; 4 | import Left from './widget/left/Left'; 5 | import MainMenu from './widget/MainMenu'; 6 | import { TYPE_FILTERS } from './lib/construct'; 7 | import { Context, STORAGE_ID } from "./lib/storageld"; 8 | 9 | import { listen } from '@tauri-apps/api/event'; 10 | import { invoke } from '@tauri-apps/api/core'; 11 | import { useState, useEffect } from "react"; 12 | import { useImmer } from 'use-immer'; 13 | import { Splitter } from 'antd'; 14 | 15 | import { theme } from 'antd'; 16 | const { useToken } = theme; 17 | 18 | const DEFAULT_CHAR_DISPLAY = { 19 | background: '#ffffff', 20 | color: '#000000', 21 | size: 48, 22 | charName: true, 23 | } 24 | 25 | const App = () => { 26 | const { token } = useToken(); 27 | const [cstTable, setCstTable] = useState({}) 28 | const [targetChars, setTargetChars] = useState([]); 29 | const [config, updateConfigProto] = useImmer(); 30 | const [selectedChar, setSelectedChar] = useState(); 31 | 32 | const [charFilter, setCharFilterProto] = useState(() => { 33 | const value = Context.getItem(STORAGE_ID.left.filter); 34 | return value || { text: "", types: [] } 35 | }); 36 | const [charDisplay, setCharDisplayProto] = useState(() => { 37 | const value = Context.getItem(STORAGE_ID.left.charDisplay); 38 | return value || DEFAULT_CHAR_DISPLAY 39 | }); 40 | 41 | useEffect(() => { 42 | function update() { 43 | invoke("target_chars", {}).then(list => setTargetChars(list)); 44 | invoke("get_cst_table", {}).then(table => setCstTable(table)); 45 | invoke("get_config", {}).then(config => updateConfigProto(draft => draft = config)); 46 | } 47 | 48 | update(); 49 | 50 | let unlistenStrucChange = listen("source", (e) => { 51 | update() 52 | }); 53 | 54 | return () => unlistenStrucChange.then(f => f()); 55 | }, []); 56 | 57 | function updateConfig(f) { 58 | let newCfg = JSON.parse(JSON.stringify(config)); 59 | f(newCfg); 60 | invoke("set_config", { cfg: newCfg }); 61 | updateConfigProto(draft => draft = newCfg); 62 | } 63 | 64 | function setCharFilter(filter) { 65 | setCharFilterProto(filter) 66 | Context.setItem(STORAGE_ID.left.filter, filter) 67 | } 68 | 69 | function setCharDisplay(value) { 70 | Context.setItem(STORAGE_ID.left.charDisplay, value); 71 | setCharDisplayProto(value); 72 | } 73 | 74 | function handleSideResize(left, right) { 75 | if (left !== leftDefaultSize) { 76 | Context.setItem(STORAGE_ID.left.width, left); 77 | } 78 | if (right !== rightDefaultSize) { 79 | Context.setItem(STORAGE_ID.right.width, right); 80 | } 81 | } 82 | 83 | let leftDefaultSize = Context.getItem(STORAGE_ID.left.width); 84 | let rightDefaultSize = Context.getItem(STORAGE_ID.right.width); 85 | 86 | let charList; 87 | let isFilter = false; 88 | if (charFilter.text.length) { 89 | let temp = charFilter.text.split(' '); 90 | charList = [...temp[0], ...temp.slice(1)]; 91 | isFilter = true; 92 | } else { 93 | charList = targetChars; 94 | } 95 | if (charFilter.types.length) { 96 | isFilter = true; 97 | let filters = charFilter.types.map(tp => TYPE_FILTERS.get(tp)); 98 | charList = charList.filter(char => { 99 | let attrs = cstTable[char]; 100 | return ((attrs || attrs == "") && (filters.find(f => f(attrs))) || attrs == undefined) 101 | }) 102 | } 103 | // charList = ['口'] 104 | let sideBarStyle = { boxShadow: token.boxShadow, padding: token.containerPadding, backgroundColor: token.colorBgElevated }; 105 | 106 | return <> 107 | handleSideResize(left, right)} 110 | > 111 | 115 | 121 | 122 | 123 | 124 | 132 | 133 | 134 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | }; 145 | export default App; -------------------------------------------------------------------------------- /editor/src/editor/main.jsx: -------------------------------------------------------------------------------- 1 | import "../App.css"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom/client"; 4 | 5 | import { ConfigProvider } from 'antd'; 6 | 7 | import fasingTheme from '../theme'; 8 | import Editor from "./Editor"; 9 | 10 | ReactDOM.createRoot(document.getElementById("root")).render( 11 | 12 | 13 | 14 | 15 | , 16 | ); 17 | -------------------------------------------------------------------------------- /editor/src/lib/action.js: -------------------------------------------------------------------------------- 1 | const MODF_CTRL = 1; 2 | const MODF_SHIFT = 2; 3 | const MODF_ALT = 4; 4 | 5 | export const SHORTCUT = { 6 | save: { 7 | code: "KeyS", 8 | modf: MODF_CTRL 9 | }, 10 | open: { 11 | code: "KeyO", 12 | modf: MODF_CTRL 13 | }, 14 | reload: { 15 | code: "KeyD", 16 | modf: MODF_CTRL 17 | } 18 | } 19 | 20 | export function isKeydown(e, shortcut) { 21 | return e.code == shortcut.code 22 | && e.altKey == (shortcut.modf & MODF_ALT) 23 | && e.shiftKey == (shortcut.modf & MODF_SHIFT) 24 | && e.ctrlKey == (shortcut.modf & MODF_CTRL) 25 | } 26 | 27 | function simpleCode(code) { 28 | let match = code.match(/Key(.*)/); 29 | return match[1] || code; 30 | } 31 | 32 | export function shortcutText(shortcut) { 33 | return ((shortcut.modf & MODF_CTRL) ? "Ctrl+" : "") 34 | + ((shortcut.modf & MODF_SHIFT) ? "Shift+" : "") 35 | + ((shortcut.modf & MODF_ALT) ? "Alt+" : "") 36 | + simpleCode(shortcut.code); 37 | } 38 | -------------------------------------------------------------------------------- /editor/src/lib/construct.js: -------------------------------------------------------------------------------- 1 | export const CHAR_GROUP_LIST = [ 2 | { 3 | value: "Single", 4 | label: "单", 5 | filter: attrs => attrs === "" 6 | }, 7 | { 8 | value: "LeftToRight", 9 | label: "⿰", 10 | filter: attrs => attrs.tp === "⿰" && attrs.components.length == 2 11 | }, 12 | { 13 | value: "LeftToMiddleAndRight", 14 | label: "⿲", 15 | filter: attrs => attrs.tp === "⿰" && attrs.components.length == 3 16 | }, 17 | { 18 | value: "AboveToBelow", 19 | label: "⿱", 20 | filter: attrs => attrs.tp === "⿱" && attrs.components.length == 2 21 | }, 22 | { 23 | value: "AboveToMiddleAndBelow", 24 | label: "⿳", 25 | filter: attrs => attrs.tp === "⿱" && attrs.components.length == 3 26 | }, 27 | { 28 | value: "SurroundFromAbove", 29 | label: "⿵", 30 | filter: attrs => attrs.tp === "⿵" 31 | }, 32 | { 33 | value: "SurroundFromBelow", 34 | label: "⿶", 35 | filter: attrs => attrs.tp === "⿶" 36 | }, 37 | { 38 | value: "FullSurround", 39 | label: "⿴", 40 | filter: attrs => attrs.tp === "⿴" 41 | }, 42 | { 43 | value: "SurroundFromUpperRight", 44 | label: "⿹", 45 | filter: attrs => attrs.tp === "⿹" 46 | }, 47 | { 48 | value: "SurroundFromLeft", 49 | label: "⿷", 50 | filter: attrs => attrs.tp === "⿷" 51 | }, 52 | { 53 | value: "SurroundFromUpperLeft", 54 | label: "⿸", 55 | filter: attrs => attrs.tp === "⿸" 56 | }, 57 | { 58 | value: "SurroundFromLowerLeft", 59 | label: "⿺", 60 | filter: attrs => attrs.tp === "⿺" 61 | }, 62 | // { 63 | // value: "Letter", 64 | // label: "A", 65 | // filter: attrs => false 66 | // }, 67 | // { 68 | // value: "Number", 69 | // label: "0", 70 | // filter: attrs => false 71 | // }, 72 | ]; 73 | 74 | export const TYPE_FILTERS = new Map(CHAR_GROUP_LIST.map(({ value, filter }) => [value, filter])) 75 | -------------------------------------------------------------------------------- /editor/src/lib/storageld.js: -------------------------------------------------------------------------------- 1 | const { stringify, parse } = JSON 2 | JSON.stringify = function (value, replacer, space) { 3 | const _replacer = 4 | typeof replacer === 'function' 5 | ? replacer 6 | : function (_, value) { 7 | return value 8 | } 9 | replacer = function (key, value) { 10 | value = _replacer(key, value) 11 | if (value instanceof Set) value = `Set{${stringify([...value])}}` 12 | else if (value instanceof Map) value = `Map{${stringify([...value])}}` 13 | return value 14 | } 15 | return stringify(value, replacer, space) 16 | } 17 | JSON.parse = function (value, reviver) { 18 | if (!reviver) 19 | reviver = function (key, value) { 20 | if (/Set\{\[.*\]\}/.test(value)) 21 | value = new Set(parse(value.replace(/Set\{\[(.*)\]\}/, '[$1]'))) 22 | else if (/Map\{\[.*\]\}/.test(value)) 23 | value = new Map(parse(value.replace(/Map\{\[(.*)\]\}/, '[$1]'))) 24 | return value 25 | } 26 | return parse(value, reviver) 27 | } // 作者:死皮赖脸的喵子 https://www.bilibili.com/read/cv20325492 出处:bilibili 28 | 29 | export const STORAGE_ID = { 30 | left: { 31 | width: 'left-width', 32 | charDisplay: 'left-char-display', 33 | filter: 'left-filter', 34 | }, 35 | middle: { 36 | offset: 'middle-offset', 37 | }, 38 | right: { 39 | width: 'right-width', 40 | }, 41 | editor: { 42 | gridIndex: 'gridIndex' 43 | } 44 | }; 45 | 46 | function getContextItem(id) { 47 | try { 48 | return JSON.parse(localStorage.getItem(id)); 49 | } catch (e) { 50 | console.error(e) 51 | return null 52 | } 53 | } 54 | 55 | function setContextItem(id, value) { 56 | return localStorage.setItem(id, JSON.stringify(value)); 57 | } 58 | 59 | export const Context = { 60 | getItem: getContextItem, 61 | setItem: setContextItem, 62 | } -------------------------------------------------------------------------------- /editor/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import { ConfigProvider } from 'antd'; 5 | import fasingTheme from './theme'; 6 | 7 | ReactDOM.createRoot(document.getElementById("root")).render( 8 | 9 | 10 | 11 | 12 | , 13 | ); 14 | -------------------------------------------------------------------------------- /editor/src/theme.js: -------------------------------------------------------------------------------- 1 | import { theme } from 'antd'; 2 | 3 | const fasingTheme = { 4 | algorithm: [theme.darkAlgorithm, theme.compactAlgorithm], 5 | components: { 6 | Splitter: { 7 | splitBarSize: 0 8 | }, 9 | InputNumber: { 10 | handleWidth: 8, 11 | }, 12 | Button: { 13 | defaultBg: '#222222', 14 | defaultBorderColor: '#444444' 15 | }, 16 | }, 17 | token: { 18 | colorPrimary: '#18bcc6', 19 | colorBgBase: '#222222', 20 | colorBgContainer: '#141414', 21 | colorBgElevated: '#333333', 22 | colorBgSolid: '#444444', 23 | colorPrimaryBorder: '#207276', 24 | 25 | boxShadow: '0 0 10px rgba(0, 0, 0, 0.7)', 26 | boxShadowSecondary: '0 0 10px rgba(0, 0, 0, 0.4)', 27 | containerPadding: '10px' 28 | }, 29 | }; 30 | 31 | export default fasingTheme; -------------------------------------------------------------------------------- /editor/src/widget/MainMenu.jsx: -------------------------------------------------------------------------------- 1 | import { SHORTCUT, shortcutText, isKeydown } from '../lib/action'; 2 | import { open } from '@tauri-apps/plugin-dialog'; 3 | import { invoke } from '@tauri-apps/api/core'; 4 | import { listen } from '@tauri-apps/api/event'; 5 | 6 | import { FloatButton, Menu, theme } from 'antd'; 7 | import { useEffect, useState } from 'react'; 8 | 9 | const { useToken } = theme; 10 | 11 | const ShortcutText = (props) => { 12 | const { token } = useToken(); 13 | 14 | return <> 15 | {props.children} 16 | {shortcutText(props.shortcut)} 17 | 18 | } 19 | 20 | const menuItems = [ 21 | { 22 | key: 'save', 23 | label: 保存, 24 | icon: , 25 | shortcut: SHORTCUT.save, 26 | onClick: () => { 27 | invoke("save_fas_file", {}).catch(e => console.error(e)); 28 | } 29 | }, 30 | { 31 | key: 'reload', 32 | label: 重载, 33 | icon: , 34 | shortcut: SHORTCUT.reload, 35 | onClick: () => { 36 | invoke("reload", {}).catch(e => console.error(e)); 37 | } 38 | }, 39 | { 40 | key: 'open', 41 | label: 打开, 42 | icon: , 43 | shortcut: SHORTCUT.open, 44 | onClick: () => { 45 | open({ 46 | multiple: false, 47 | directory: false, 48 | }).then(file => { 49 | if (file) { 50 | invoke("new_source", { path: file }).catch(e => console.error(e)); 51 | } 52 | }) 53 | } 54 | }, 55 | ]; 56 | 57 | const MainMenu = () => { 58 | const [changed, setChanged] = useState(false); 59 | 60 | function handleKeyDown(e) { 61 | for (let i = 0; i < menuItems.length; ++i) { 62 | if (isKeydown(e, menuItems[i].shortcut)) { 63 | menuItems[i].onClick(); 64 | } 65 | } 66 | } 67 | 68 | useEffect(() => { 69 | invoke("is_changed", {}).then(b => { 70 | b !== changed && setChanged(!changed) 71 | }); 72 | 73 | window.addEventListener("keydown", handleKeyDown); 74 | 75 | let unlistenChanged = listen("changed", (e) => { 76 | setChanged(true); 77 | }); 78 | let unlistenSaved = listen("saved", (e) => { 79 | setChanged(false); 80 | }); 81 | let unlistenSource = listen("source", (e) => { 82 | invoke("is_changed", {}).then(b => setChanged(b)); 83 | }); 84 | 85 | 86 | return () => { 87 | window.removeEventListener("keydown", handleKeyDown); 88 | unlistenChanged.then(f => f()); 89 | unlistenSaved.then(f => f()); 90 | unlistenSource.then(f => f()); 91 | } 92 | }, []); 93 | 94 | return <> 95 | } 100 | > 101 | 102 | 103 | 104 | 105 | } 106 | 107 | export default MainMenu; 108 | 109 | export function MenuIcon(props) { 110 | return ( 111 | 112 | ) 113 | } 114 | 115 | export function FileIcon(props) { 116 | return ( 117 | 118 | ) 119 | } 120 | 121 | export function ReloadIcon(props) { 122 | return ( 123 | 124 | ) 125 | } 126 | 127 | export function SvaeIcon(props) { 128 | return ( 129 | 130 | ) 131 | } 132 | -------------------------------------------------------------------------------- /editor/src/widget/left/Left.jsx: -------------------------------------------------------------------------------- 1 | import { CHAR_GROUP_LIST } from '../../lib/construct'; 2 | 3 | import { useRef } from "react"; 4 | import { Input, InputNumber, Space, Divider, ColorPicker, Switch, Checkbox, Button } from 'antd'; 5 | const { TextArea } = Input; 6 | 7 | const FONT_SIZE_RANGE = [8, 128] 8 | const DIsplaySettings = ({ charDisplay, setCharDisplay, strokWidth }) => { 9 | function handleSizeChange(e) { 10 | let value = Math.min(Math.max(e.target.value, FONT_SIZE_RANGE[0]), FONT_SIZE_RANGE[1]); 11 | setCharDisplay({ ...charDisplay, size: value }); 12 | } 13 | 14 | return 15 | 16 |
字色: setCharDisplay({ ...charDisplay, color: color.toCssString() })} 19 | />
20 |
背景: setCharDisplay({ ...charDisplay, background: color.toCssString() })} 23 | />
24 |
字号:
33 |
字名: setCharDisplay({ ...charDisplay, charName: checked })} 37 | /> 38 |
39 |
40 |
线宽:{strokWidth} | {Math.round(charDisplay.size * strokWidth)} px
41 |
42 | } 43 | 44 | const Filters = ({ charFilter, setCharFilter, cstTable }) => { 45 | const compRef = useRef(); 46 | 47 | function handleCompClick() { 48 | function recursion(target, comp, attrs) { 49 | if (comp == target) { 50 | return true; 51 | } else if (typeof attrs == "object") { 52 | return attrs.components.find(c => { 53 | if (typeof c == "object") { 54 | return recursion(target, "temp", c); 55 | } else { 56 | return recursion(target, c, cstTable[c]) 57 | } 58 | }) !== undefined; 59 | } 60 | return false; 61 | } 62 | 63 | 64 | let targetComp = compRef.current.input.value; 65 | let targetList = []; 66 | if (targetComp) { 67 | for (let chr in cstTable) { 68 | if (recursion(targetComp, chr, cstTable[chr])) { 69 | targetList.push(chr) 70 | } 71 | } 72 | setCharFilter({ ...charFilter, text: targetList.join('') }) 73 | } 74 | } 75 | 76 | function handleChange(e) { 77 | setCharFilter({ ...charFilter, text: e.target.value }) 78 | } 79 | 80 | return 81 |