1use crate::parser::ast::*;
10use std::collections::HashSet;
11use std::fmt::Write;
12
13const SVG_W: f32 = 1640.0;
16const SIDEBAR_W: f32 = 200.0;
17const CARD_W: f32 = 370.0;
18const CARD_GAP: f32 = 14.0;
19const GRID_X: f32 = SIDEBAR_W + 14.0;
20const COLS: usize = 3;
21const HEADER_H: f32 = 74.0;
22const LEGEND_H: f32 = 64.0;
23const CONTENT_Y: f32 = HEADER_H + LEGEND_H;
24const ICON_SZ: f32 = 22.0; const ICON_GAP: f32 = 4.0;
26const ICONS_ROW: usize = 11;
27const CARD_PAD: f32 = 12.0;
28const CARD_ROUNDING: f32 = 7.0;
29
30const BG: &str = "#0b0b1a";
33const CARD_BG: &str = "#11112a";
34const CARD_BD: &str = "#22225a";
35const TEXT: &str = "#c0c0e0";
36const TEXT_DIM: &str = "#52528a";
37const GOLD: &str = "#ffd700";
38const SIDEBAR_BG:&str = "#0d0d22";
39
40#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
43#[allow(dead_code)]
44pub(crate) enum Cat {
45 Rings, Spiral, Star, Flower, Lotus, Chakra, Yantra,
47 Hyper, Tess, Rain, Grid, Halftone, Pagoda, Torii, Cog,
48 Fractal,
50 Tone, Vol, Listen, Sfx, Spectrum,
52 Music, Note,
54 Hash, Cipher, Sign, Kem, Shard,
56 Rigid, Soft, Liquid, Force, Collide,
58 Mesh, Draw3D, Draw2D,
60 Trig, MathFn, Noise,
62 Net, Neural, Behavior,
64 Widget, Hud, Dialog, Holo,
66 Str, Font, Vector,
68 Color, Shade, Camera, Light, Fill, Present, Window,
70 Key, Mouse,
72 Print, File, Sys, Anim,
74 User, Loop, Const,
76}
77
78impl Cat {
79 pub(crate) fn color(self) -> &'static str {
80 match self {
81 Cat::Rings => "#00e5ff",
83 Cat::Spiral => "#00ffb3",
84 Cat::Star => "#ffd700",
85 Cat::Flower => "#ff79c6",
86 Cat::Lotus => "#ff5e8a",
87 Cat::Chakra => "#bd93f9",
88 Cat::Yantra => "#ffb86c",
89 Cat::Hyper => "#5575c8",
90 Cat::Tess => "#50fa7b",
91 Cat::Rain => "#f1fa8c",
92 Cat::Grid => "#8be9fd",
93 Cat::Halftone => "#888899",
94 Cat::Pagoda => "#ff9a3c",
95 Cat::Torii => "#ff5e5e",
96 Cat::Cog => "#b0b0c8",
97 Cat::Fractal => "#c77dff",
99 Cat::Tone => "#ff1493",
101 Cat::Vol => "#ff69b4",
102 Cat::Listen => "#db77d9",
103 Cat::Sfx => "#ff85c0",
104 Cat::Spectrum => "#ff4fa3",
105 Cat::Music => "#b388ff",
107 Cat::Note => "#d8a7ff",
108 Cat::Hash => "#0fd9b0",
110 Cat::Cipher => "#12b48c",
111 Cat::Sign => "#5ce0c0",
112 Cat::Kem => "#2aa98a",
113 Cat::Shard => "#7ff0d8",
114 Cat::Rigid => "#ff8c42",
116 Cat::Soft => "#ffb37b",
117 Cat::Liquid => "#4aa3ff",
118 Cat::Force => "#ff6b35",
119 Cat::Collide => "#ff4d4d",
120 Cat::Mesh => "#6aa9ff",
122 Cat::Draw3D => "#8fb8ff",
123 Cat::Draw2D => "#5fd0ff",
124 Cat::Trig => "#b6e36b",
126 Cat::MathFn => "#d4e157",
127 Cat::Noise => "#9ccc65",
128 Cat::Net => "#18b6e0",
130 Cat::Neural => "#ffd54f",
131 Cat::Behavior => "#ffca6b",
132 Cat::Widget => "#7a8fb0",
134 Cat::Hud => "#92a8c8",
135 Cat::Dialog => "#e0b97d",
136 Cat::Holo => "#66ffe0",
137 Cat::Str => "#a0b4d0",
139 Cat::Font => "#c0c8e0",
140 Cat::Vector => "#88d8c0",
141 Cat::Color => "#ff7eb6",
143 Cat::Shade => "#9d8df1",
144 Cat::Camera => "#ffe066",
145 Cat::Light => "#ffe4b5",
146 Cat::Fill => "#3a3a6a",
147 Cat::Present => "#555580",
148 Cat::Window => "#7fd4ff",
149 Cat::Key => "#ff9e7d",
151 Cat::Mouse => "#ffbfa3",
152 Cat::Print => "#9aa0b5",
154 Cat::File => "#7d8aa8",
155 Cat::Sys => "#6b7488",
156 Cat::Anim => "#ffd28a",
157 Cat::User => "#6ab0f5",
159 Cat::Loop => "#ff7f50",
160 Cat::Const => "#50fa7b",
161 }
162 }
163
164 pub(crate) fn label(self) -> &'static str {
165 match self {
166 Cat::Rings => "rings",
167 Cat::Spiral => "spiral",
168 Cat::Star => "star",
169 Cat::Flower => "flower",
170 Cat::Lotus => "lotus",
171 Cat::Chakra => "chakra",
172 Cat::Yantra => "yantra",
173 Cat::Hyper => "hyperbolic",
174 Cat::Tess => "tessellated",
175 Cat::Rain => "letter rain",
176 Cat::Grid => "grid",
177 Cat::Halftone => "halftone",
178 Cat::Pagoda => "pagoda",
179 Cat::Torii => "torii",
180 Cat::Cog => "spiked cog",
181 Cat::Fractal => "fractal tex",
182 Cat::Tone => "audio tone",
183 Cat::Vol => "audio vol",
184 Cat::Listen => "listener",
185 Cat::Sfx => "sfx",
186 Cat::Spectrum => "spectrum",
187 Cat::Music => "music",
188 Cat::Note => "note",
189 Cat::Hash => "hash",
190 Cat::Cipher => "cipher",
191 Cat::Sign => "signature",
192 Cat::Kem => "key exch",
193 Cat::Shard => "secret share",
194 Cat::Rigid => "rigid body",
195 Cat::Soft => "soft body",
196 Cat::Liquid => "liquid",
197 Cat::Force => "force",
198 Cat::Collide => "collision",
199 Cat::Mesh => "mesh",
200 Cat::Draw3D => "draw 3d",
201 Cat::Draw2D => "draw 2d",
202 Cat::Trig => "trig",
203 Cat::MathFn => "math",
204 Cat::Noise => "noise",
205 Cat::Net => "network",
206 Cat::Neural => "neural net",
207 Cat::Behavior => "behavior tree",
208 Cat::Widget => "ui widget",
209 Cat::Hud => "hud",
210 Cat::Dialog => "dialog",
211 Cat::Holo => "hologram",
212 Cat::Str => "string",
213 Cat::Font => "font",
214 Cat::Vector => "svg vector",
215 Cat::Color => "color",
216 Cat::Shade => "shading",
217 Cat::Camera => "camera",
218 Cat::Light => "light",
219 Cat::Fill => "fill",
220 Cat::Present => "render",
221 Cat::Window => "window",
222 Cat::Key => "keyboard",
223 Cat::Mouse => "mouse",
224 Cat::Print => "print",
225 Cat::File => "file io",
226 Cat::Sys => "system",
227 Cat::Anim => "animation",
228 Cat::User => "fn call",
229 Cat::Loop => "loop",
230 Cat::Const => "const",
231 }
232 }
233
234 pub(crate) fn domain(self) -> &'static str {
236 if is_vtex(self) { return "texture"; }
237 if is_audio(self) { return "audio"; }
238 if is_crypto(self) { return "crypto"; }
239 if is_physics(self) { return "physics"; }
240 if is_ai(self) { return "ai"; }
241 match self {
242 Cat::Music | Cat::Note => "music",
243 Cat::Mesh | Cat::Draw3D | Cat::Draw2D | Cat::Vector => "geometry",
244 Cat::Trig | Cat::MathFn | Cat::Noise | Cat::Fractal => "math",
245 Cat::Widget | Cat::Hud | Cat::Dialog | Cat::Holo => "ui",
246 Cat::Str | Cat::Font => "text",
247 Cat::Color | Cat::Shade | Cat::Camera | Cat::Light
248 | Cat::Fill | Cat::Present | Cat::Window => "scene",
249 Cat::Key | Cat::Mouse => "input",
250 Cat::Print | Cat::File | Cat::Sys | Cat::Anim | Cat::Net=> "system",
251 _ => "code",
252 }
253 }
254}
255
256pub(crate) fn categorize(name: &str) -> Cat {
257 if name.starts_with("vtex_rings") || name == "ลายวงซ้อน" || name == "纹环" || name == "輪模様" || name == "輪무늬" { return Cat::Rings; }
259 if name.starts_with("vtex_spiral") || name == "ลายก้นหอย" || name == "ลายเกลียว" || name == "ลายเกลียวหมุน" || name == "纹螺" || name == "螺旋模様" || name == "나선무늬" { return Cat::Spiral; }
260 if name.starts_with("vtex_star") || name == "ลายดาว" || name == "纹星" || name == "星模様" || name == "별무늬" { return Cat::Star; }
261 if name.starts_with("vtex_flower") || name == "ลายดอกไม้" || name == "ลายดอก" || name == "纹花" || name == "花模様" || name == "꽃무늬" { return Cat::Flower; }
262 if name.starts_with("vtex_lotus") || name == "ลายบัว" || name == "ลายดอกบัว" || name == "纹莲" || name == "蓮模様" || name == "연꽃무늬" { return Cat::Lotus; }
263 if name.starts_with("vtex_chakra") || name == "ลายจักร" || name == "纹轮" || name == "輪模様" || name == "바퀴무늬" { return Cat::Chakra; }
264 if name.starts_with("vtex_yantra") || name == "ลายยันต์" || name == "纹咒" || name == "護符模様" || name == "부적무늬" { return Cat::Yantra; }
265 if name.starts_with("vtex_hyper") || name == "ลายไฮเพอร์โบลิก" || name == "ลายไฮเปอร์" || name == "纹超" || name == "超次元模様" || name == "초차원무늬" { return Cat::Hyper; }
266 if name.starts_with("vtex_tessellated") || name == "ลายตาข่าย" || name == "纹镶嵌" || name == "網目模様" { return Cat::Tess; }
267 if name.starts_with("vtex_letter_rain") || name.starts_with("vtex_rain") || name == "ลายอักษรไหล" || name == "ลายฝน" || name == "纹雨" || name == "文字雨" || name == "비무늬" { return Cat::Rain; }
268 if name.starts_with("vtex_grid") || name == "ลายตาราง" || name == "纹格" || name == "格子模様" || name == "격자무늬" { return Cat::Grid; }
269 if name.starts_with("vtex_halftone") || name == "ลายจุด" || name == "ลายฮาล์ฟโทน" || name == "纹半调" || name == "網点模様" || name == "망점" { return Cat::Halftone; }
270 if name.starts_with("vtex_pagoda") || name == "ลายเจดีย์" || name == "เจดีย์" || name == "纹塔" || name == "탑" { return Cat::Pagoda; }
271 if name.starts_with("vtex_torii") || name == "ประตูโทริอิ" || name == "纹鸟居" || name == "鳥居" || name == "도리이" { return Cat::Torii; }
272 if name.starts_with("vtex_spiked_cog") || name == "ฟันเฟืองหนาม" || name == "纹棘轮" || name == "歯車模様" || name == "톱니바퀴" { return Cat::Cog; }
273 if name.starts_with("vtex_") { return Cat::Tess; }
274
275 if name.starts_with("tex_") { return Cat::Fractal; }
277
278 if name == "audio_tone" || name == "เสียงโทน" || name == "音調" || name == "音调" || name == "음조" { return Cat::Tone; }
280 if name == "audio_volume" || name == "audio_bgm_volume" || name == "ระดับเสียง" || name == "音量" || name == "음량" { return Cat::Vol; }
281 if name == "audio_listener" || name == "音声リスナー" || name == "오디오리스너" || name == "音频监听" { return Cat::Listen; }
282 if name.starts_with("audio_") { return Cat::Sfx; }
283 if name.starts_with("mic_") || name.starts_with("fft_") { return Cat::Spectrum; }
284
285 if name == "music_note_on" || name == "music_note_off" || name == "music_note" || name == "music_note_name" { return Cat::Note; }
287 if name.starts_with("music_") || name.starts_with("midi_") || name.starts_with("MIDI") { return Cat::Music; }
288
289 if name == "crypto_hash" || name == "sha3_512" || name == "blake3" || name == "hash_int" || name == "hash_str" { return Cat::Hash; }
291 if name == "crypto_seal" || name == "crypto_open" || name == "encrypt" || name == "aes_gcm_256" { return Cat::Cipher; }
292 if name == "ed25519" || name == "schnorr_verify" || name == "vrf_verify" || name == "derive" || name == "argon2id" { return Cat::Sign; }
293 if name == "mlkem768" || name.starts_with("hybrid_") || name.starts_with("knot_") { return Cat::Kem; }
294 if name == "shamir_reconstruct" { return Cat::Shard; }
295
296 if name.starts_with("rb_") || name == "rigidbody" { return Cat::Rigid; }
298 if name.starts_with("soft_") { return Cat::Soft; }
299 if name.starts_with("liquid_") { return Cat::Liquid; }
300 if matches!(name, "force"|"torque"|"spring"|"friction"|"elasticity"|"acceleration"|"apply_impulse"|"gyro") { return Cat::Force; }
301 if matches!(name, "collision"|"raycast"|"aabb"|"AABB"|"constraint") { return Cat::Collide; }
302
303 if matches!(name, "cube"|"box"|"capsule"|"capsule_chain"|"cylinder"|"pyramid"|"icosphere"|"icosahedron"|"octahedron"|"orb_shell"|"stairs"|"frustum") { return Cat::Mesh; }
305 if matches!(name, "draw_line_3d"|"draw_triangle_3d"|"line3d"|"triangle3d"|"project_3d"|"render_3d"|"flush_3d"|"font_text_3d") { return Cat::Draw3D; }
306
307 if name.starts_with("nn_") { return Cat::Neural; }
309 if name.starts_with("bt_") { return Cat::Behavior; }
310 if name.starts_with("dialog_") { return Cat::Dialog; }
311
312 if name.starts_with("net_") { return Cat::Net; }
314
315 if matches!(name, "ui_radar"|"ui_radar3d"|"ui_minimap"|"ui_compass"|"ui_healthbar"|"ui_reticle"|"ui_target"|"ui_gauge"|"ui_gauge3d"|"ui_vu"|"ui_battery"|"ui_segbar"|"ui_bar"|"ui_progress"|"ui_cooldown"|"ui_scanlines"|"ui_vignette"|"ui_spark"|"ui_ring"|"ui_counter") { return Cat::Hud; }
317 if name.starts_with("ui_") { return Cat::Widget; }
318
319 if matches!(name, "holo_points"|"holo_fragment_count"|"knot_points"|"sparkle"|"neon"|"psychedelic") { return Cat::Holo; }
321
322 if name.starts_with("font_") { return Cat::Font; }
324 if name.starts_with("svg_") { return Cat::Vector; }
325
326 if name.starts_with("str_") || matches!(name, "split"|"join"|"trim"|"substr"|"format"|"to_str"|"num_str"|"starts_with"|"ends_with") { return Cat::Str; }
328
329 if matches!(name, "sin"|"cos"|"tan"|"asin"|"acos"|"atan"|"atan2"|"arcsin"|"arccos"|"arctan"|"arctan2"|"tanh"|"tanhf"|"hypot") { return Cat::Trig; }
331 if matches!(name, "perlin"|"perlin3"|"noise2"|"fbm"|"vnoise"|"smoothstep") { return Cat::Noise; }
332 if matches!(name, "sqrt"|"cbrt"|"pow"|"exp"|"ln"|"log"|"log2"|"log10"|"abs"|"floor"|"ceil"|"round"|"trunc"|"fract"|"sign"|"clamp"|"lerp"|"min"|"max"|"rand"|"pi"|"tau") { return Cat::MathFn; }
333
334 if matches!(name, "hsl_color"|"hsv_to_rgb"|"set_color"|"set_color_hsl"|"color"|"lerp_color"|"gfx_color") { return Cat::Color; }
336 if matches!(name, "set_shade_mode"|"set_rim"|"set_fog"|"set_ambient"|"set_cel_bands"|"set_blend"|"set_shadow_color"|"set_projection"|"set_zdist") { return Cat::Shade; }
337
338 if matches!(name, "set_camera"|"set_camera_pos"|"move_camera") { return Cat::Camera; }
340 if matches!(name, "add_light"|"clear_lights") { return Cat::Light; }
341
342 if matches!(name, "open_window"|"open_fullscreen"|"gfx_window"|"fullscreen"|"windowed"|"wait_window"|"is_open"|"window_is_open"|"gfx_is_open"|"gfx_wait"|"resolution"|"get_width"|"get_height") { return Cat::Window; }
344 if matches!(name, "present"|"แสดงผล"|"gfx_present"|"render"|"render_3d"|"flush_3d") { return Cat::Present; }
345 if matches!(name, "fill"|"เติม"|"gfx_fill"|"clear") { return Cat::Fill; }
346
347 if name.starts_with("draw_") || matches!(name, "gfx_line"|"gfx_pixel"|"gfx_triangle"|"line"|"triangle"|"pixel") { return Cat::Draw2D; }
349
350 if name.starts_with("mouse_") || matches!(name, "capture_mouse"|"release_mouse") { return Cat::Mouse; }
352 if name.starts_with("key_") || matches!(name, "keys"|"text_poll"|"text_get") { return Cat::Key; }
353
354 if matches!(name, "print"|"println"|"print_file"|"imprimir"|"afficher"|"вывести"|"พิมพ์"|"打印") { return Cat::Print; }
356 if matches!(name, "read_file"|"write_file"|"อ่านไฟล์"|"เขียนไฟล์") { return Cat::File; }
357 if matches!(name, "system"|"get_args"|"time_now"|"sleep"|"sleep_ms") { return Cat::Sys; }
358
359 if matches!(name, "animation"|"ease"|"tick"|"delta_time"|"frame_count"|"frame"|"record_frame"|"record_count"
361 |"tween"|"tween_ease"|"breathe"|"wobble"|"gait_phase"|"gait_swing"|"gait_lift"|"spring_to"|"ik2") { return Cat::Anim; }
362 if matches!(name, "gear_couple"|"gear_train"|"cam_lift"|"piston"|"rack") { return Cat::Cog; }
364
365 Cat::User
366}
367
368pub(crate) fn is_vtex(c: Cat) -> bool {
369 matches!(c, Cat::Rings|Cat::Spiral|Cat::Star|Cat::Flower|Cat::Lotus|
370 Cat::Chakra|Cat::Yantra|Cat::Hyper|Cat::Tess|Cat::Rain|
371 Cat::Grid|Cat::Halftone|Cat::Pagoda|Cat::Torii|Cat::Cog)
372}
373pub(crate) fn is_audio(c: Cat) -> bool {
374 matches!(c, Cat::Tone|Cat::Vol|Cat::Listen|Cat::Sfx|Cat::Spectrum)
375}
376pub(crate) fn is_crypto(c: Cat) -> bool {
377 matches!(c, Cat::Hash|Cat::Cipher|Cat::Sign|Cat::Kem|Cat::Shard)
378}
379pub(crate) fn is_physics(c: Cat) -> bool {
380 matches!(c, Cat::Rigid|Cat::Soft|Cat::Liquid|Cat::Force|Cat::Collide)
381}
382pub(crate) fn is_ai(c: Cat) -> bool {
383 matches!(c, Cat::Neural|Cat::Behavior)
384}
385
386const ENTRY_NAMES: &[&str] = &[
387 "start","main","启","เริ่ม","시작","начать","начало",
388 "inicio","comenzar","début","commencer","anfang","starten","início",
389];
390fn is_entry(name: &str) -> bool { ENTRY_NAMES.contains(&name) }
391
392struct GlobalConst { name: String, value: String }
395
396#[derive(Clone)]
397struct CallItem { name: String, cat: Cat, count: usize }
398
399struct FuncCard {
400 name: String,
401 params: Vec<String>,
402 calls: Vec<CallItem>,
403 has_loop: bool,
404 is_entry: bool,
405 vtex_count: usize,
406 audio_count: usize,
407}
408
409struct Document {
410 filename: String,
411 globals: Vec<GlobalConst>,
412 funcs: Vec<FuncCard>,
413 #[allow(dead_code)]
414 fn_names: HashSet<String>,
415}
416
417struct RawCall { name: String, cat: Cat }
420
421fn walk_stmts(stmts: &[Stmt], fns: &HashSet<String>, out: &mut Vec<RawCall>, loop_: &mut bool) {
422 for s in stmts {
423 let e = match s { Stmt::Expr(e)|Stmt::Return(e) => e, Stmt::Bind(_,e) => e };
424 walk_expr(e, fns, out, loop_);
425 }
426}
427
428fn walk_expr(e: &Expr, fns: &HashSet<String>, out: &mut Vec<RawCall>, loop_: &mut bool) {
429 match e {
430 Expr::Call(func, args) => {
431 if let Expr::Ident(name) = func.as_ref() {
432 let cat = if fns.contains(name.as_str()) {
433 let base = categorize(name);
434 if base == Cat::User { Cat::User } else { base }
435 } else {
436 categorize(name)
437 };
438 out.push(RawCall { name: name.clone(), cat });
439 }
440 for a in args { walk_expr(a, fns, out, loop_); }
441 }
442 Expr::While { cond, body } => {
443 *loop_ = true;
444 walk_expr(cond, fns, out, loop_);
445 walk_stmts(body, fns, out, loop_);
446 }
447 Expr::Do(ss) => walk_stmts(ss, fns, out, loop_),
448 Expr::If { cond, then, elseifs, else_body } => {
449 walk_expr(cond, fns, out, loop_);
450 walk_stmts(then, fns, out, loop_);
451 for (c,b) in elseifs { walk_expr(c,fns,out,loop_); walk_stmts(b,fns,out,loop_); }
452 if let Some(b) = else_body { walk_stmts(b, fns, out, loop_); }
453 }
454 Expr::For { iter, body, .. } => {
455 walk_expr(iter, fns, out, loop_);
456 walk_stmts(body, fns, out, loop_);
457 }
458 Expr::BinOp(_, a, b) => { walk_expr(a, fns, out, loop_); walk_expr(b, fns, out, loop_); }
459 Expr::Array(es) => { for a in es { walk_expr(a, fns, out, loop_); } }
460 _ => {}
461 }
462}
463
464fn aggregate(raw: Vec<RawCall>) -> Vec<CallItem> {
465 let mut out: Vec<CallItem> = Vec::new();
466 for r in raw {
467 if let Some(last) = out.last_mut() {
468 if last.name == r.name { last.count += 1; continue; }
469 }
470 out.push(CallItem { name: r.name, cat: r.cat, count: 1 });
471 }
472 out
473}
474
475fn make_card(name: String, params: Vec<String>, raw: Vec<RawCall>,
476 has_loop: bool, is_entry: bool) -> FuncCard {
477 let calls = aggregate(raw);
478 let vtex_count = calls.iter().filter(|c| is_vtex(c.cat)).map(|c| c.count).sum();
479 let audio_count = calls.iter().filter(|c| is_audio(c.cat)).map(|c| c.count).sum();
480 FuncCard { name, params, calls, has_loop, is_entry, vtex_count, audio_count }
481}
482
483impl Document {
484 fn build(filename: &str, prog: &Program) -> Self {
485 let fn_names: HashSet<String> = prog.items.iter().filter_map(|i| {
486 if let Item::Fn(f) = i { Some(f.name.clone()) } else { None }
487 }).collect();
488
489 let mut globals = Vec::new();
490 let mut entries = Vec::new();
491 let mut funcs = Vec::new();
492
493 for item in &prog.items {
494 match item {
495 Item::Bind(name, expr) => match expr {
496 Expr::Number(n) => globals.push(GlobalConst {
497 name: name.clone(),
498 value: if n.fract() == 0.0 { format!("{}", *n as i64) }
499 else { format!("{:.2}", n) },
500 }),
501 Expr::Do(body) => {
502 let mut raw = Vec::new();
503 let mut lp = false;
504 for s in body {
506 if let Stmt::Expr(Expr::While { body: wb, .. }) = s {
507 lp = true;
508 walk_stmts(wb, &fn_names, &mut raw, &mut lp);
509 }
510 }
511 walk_stmts(body, &fn_names, &mut raw, &mut lp);
512 entries.push(make_card(name.clone(), vec![], raw, lp, is_entry(name)));
513 }
514 _ => {}
515 },
516 Item::Fn(f) => {
517 let mut raw = Vec::new();
518 let mut lp = false;
519 walk_stmts(&f.body, &fn_names, &mut raw, &mut lp);
520 funcs.push(make_card(f.name.clone(), f.params.clone(), raw, lp, false));
521 }
522 _ => {}
523 }
524 }
525
526 entries.extend(funcs);
527 Document { filename: filename.to_string(), globals, funcs: entries, fn_names }
528 }
529}
530
531fn xe(s: &str) -> String { s.replace('&',"&").replace('<',"<").replace('>',">") }
535fn p(v: f32) -> String { format!("{:.2}", v) }
536
537pub(crate) fn icon(cat: Cat, cx: f32, cy: f32, r: f32) -> String {
538 let c = cat.color();
539 match cat {
540 Cat::Rings => {
541 format!(
542 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.9"/>
543 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.65"/>
544 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.4"/>"#,
545 p(cx),p(cy),p(r), p(cx),p(cy),p(r*0.63), p(cx),p(cy),p(r*0.33)
546 )
547 }
548 Cat::Spiral => {
549 let (x0,y0) = (cx, cy - r*0.1);
551 let (x1,y1) = (cx + r*0.85, cy);
552 let (x2,y2) = (cx, cy + r*0.9);
553 let (x3,y3) = (cx - r*0.85, cy + r*0.1);
554 let (x4,y4) = (cx, cy - r*0.9);
555 format!(
556 r#"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{} S {},{} {},{} S {},{} {},{}"
557 fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9" stroke-linecap="round"/>"#,
558 p(x0),p(y0),
559 p(cx+r),p(y0), p(x1),p(cy-r*0.5), p(x1),p(y1),
560 p(cx+r*0.3),p(y2), p(x2),p(y2),
561 p(x3),p(y3-r*0.4), p(x3),p(y3),
562 p(cx),p(y4), p(x4),p(y4)
563 )
564 }
565 Cat::Star => {
566 let pts: String = (0..5).flat_map(|i| {
568 let ao = (i as f32 * 72.0 - 90.0).to_radians();
569 let ai = ao + 36.0_f32.to_radians();
570 let ri = r * 0.42;
571 vec![
572 format!("{},{}", p(cx + r*ao.cos()), p(cy + r*ao.sin())),
573 format!("{},{}", p(cx + ri*ai.cos()), p(cy + ri*ai.sin())),
574 ]
575 }).collect::<Vec<_>>().join(" ");
576 format!(r#"<polygon points="{pts}" fill="{c}" opacity="0.85"/>"#)
577 }
578 Cat::Flower => {
579 (0..6).map(|i| {
581 let angle = i as f32 * 60.0;
582 format!(
583 r#"<ellipse cx="{}" cy="{}" rx="{}" ry="{}"
584 fill="{c}" opacity="0.5"
585 transform="rotate({angle},{},{})"/>"#,
586 p(cx), p(cy - r*0.45), p(r*0.28), p(r*0.52),
587 p(cx), p(cy)
588 )
589 }).collect::<String>()
590 }
591 Cat::Lotus => {
592 (0..8).map(|i| {
594 let angle = i as f32 * 45.0;
595 format!(
596 r#"<ellipse cx="{}" cy="{}" rx="{}" ry="{}"
597 fill="{c}" opacity="0.45"
598 transform="rotate({angle},{},{})"/>"#,
599 p(cx), p(cy - r*0.50), p(r*0.22), p(r*0.55),
600 p(cx), p(cy)
601 )
602 }).collect::<String>()
603 + &format!(r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"#,
604 p(cx), p(cy), p(r*0.22))
605 }
606 Cat::Chakra => {
607 let spokes: String = (0..8).map(|i| {
609 let a = (i as f32 * 45.0).to_radians();
610 format!(r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.8"/>"#,
611 p(cx + r*0.28*a.cos()), p(cy + r*0.28*a.sin()),
612 p(cx + r*0.88*a.cos()), p(cy + r*0.88*a.sin()))
613 }).collect();
614 spokes + &format!(
615 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.7"/>
616 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"#,
617 p(cx), p(cy), p(r*0.88),
618 p(cx), p(cy), p(r*0.2)
619 )
620 }
621 Cat::Yantra => {
622 let h = r * 0.866;
624 let t1 = format!("{},{} {},{} {},{}",
625 p(cx), p(cy - r),
626 p(cx + h), p(cy + r*0.5),
627 p(cx - h), p(cy + r*0.5));
628 let t2 = format!("{},{} {},{} {},{}",
629 p(cx), p(cy + r),
630 p(cx - h), p(cy - r*0.5),
631 p(cx + h), p(cy - r*0.5));
632 format!(
633 r#"<polygon points="{t1}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
634 <polygon points="{t2}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
635 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.25"/>"#,
636 p(cx), p(cy), p(r*0.38)
637 )
638 }
639 Cat::Hyper => {
640 let rays: String = (0..6).map(|i| {
642 let a = (i as f32 * 30.0).to_radians();
643 format!(r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.55"/>"#,
644 p(cx - r*a.cos()), p(cy - r*a.sin()),
645 p(cx + r*a.cos()), p(cy + r*a.sin()))
646 }).collect();
647 rays + &format!(
648 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.8"/>
649 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.5"/>
650 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.3"/>"#,
651 p(cx),p(cy),p(r), p(cx),p(cy),p(r*0.6), p(cx),p(cy),p(r*0.3)
652 )
653 }
654 Cat::Tess => {
655 (0..4).map(|i| {
657 let y = cy - r*0.7 + i as f32 * r*0.46;
658 let amp = r * 0.15;
659 format!(
660 r#"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{}"
661 fill="none" stroke="{c}" stroke-width="1.1" opacity="0.7"/>"#,
662 p(cx-r), p(y),
663 p(cx-r+r*0.4), p(y-amp), p(cx-r*0.1), p(y-amp), p(cx), p(y),
664 p(cx+r*0.5), p(y+amp), p(cx+r), p(y)
665 )
666 }).collect::<String>()
667 }
668 Cat::Rain => {
669 (0..5).flat_map(|col| {
671 let x = cx - r*0.9 + col as f32 * r*0.45;
672 (0..3).map(move |row| {
673 let y = cy - r*0.8 + row as f32 * r*0.55;
674 let opa = 0.9 - row as f32 * 0.22;
675 format!(r#"<rect x="{}" y="{}" width="{}" height="{}" rx="1" fill="{c}" opacity="{:.2}"/>"#,
676 p(x - r*0.06), p(y), p(r*0.12), p(r*0.30), opa)
677 })
678 }).collect::<String>()
679 }
680 Cat::Grid => {
681 let n = 3;
683 let step = r * 2.0 / n as f32;
684 let mut s = String::new();
685 for i in 0..=n {
686 let off = -r + i as f32 * step;
687 write!(s, r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.65"/>
688 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.65"/>"#,
689 p(cx+off),p(cy-r), p(cx+off),p(cy+r),
690 p(cx-r),p(cy+off), p(cx+r),p(cy+off)).ok();
691 }
692 s
693 }
694 Cat::Halftone => {
695 (0..4).flat_map(|row| (0..4).map(move |col| {
697 let x = cx - r*0.75 + col as f32 * r*0.5;
698 let y = cy - r*0.75 + row as f32 * r*0.5;
699 format!(r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.6"/>"#,
700 p(x), p(y), p(r*0.14))
701 })).collect::<String>()
702 }
703 Cat::Tone => {
704 let x0 = cx - r; let x3 = cx + r; let xm = cx;
706 let amp = r * 0.65;
707 format!(
708 r#"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{}"
709 fill="none" stroke="{c}" stroke-width="1.6" opacity="0.9"/>"#,
710 p(x0), p(cy),
711 p(x0 + r*0.5), p(cy - amp), p(xm - r*0.1), p(cy - amp), p(xm), p(cy),
712 p(xm + r*0.6), p(cy + amp), p(x3), p(cy)
713 )
714 }
715 Cat::Vol | Cat::Listen => {
716 let arcs: String = (1..=3).map(|i| {
718 let ri = r * 0.3 * i as f32;
719 format!(
720 r#"<path d="M {},{} A {ri},{ri} 0 0,1 {},{}"
721 fill="none" stroke="{c}" stroke-width="1.3" opacity="{:.2}"/>"#,
722 p(cx), p(cy - ri),
723 p(cx), p(cy + ri),
724 1.0 - i as f32 * 0.22
725 )
726 }).collect();
727 arcs + &format!(r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.7"/>"#,
728 p(cx - r*0.15), p(cy), p(r*0.22))
729 }
730 Cat::Camera => {
731 format!(
733 r#"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.5" opacity="0.8"/>
734 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.55"/>
735 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"#,
736 p(cx), p(cy), p(r),
737 p(cx), p(cy), p(r*0.6),
738 p(cx), p(cy), p(r*0.18)
739 )
740 }
741 Cat::Light => {
742 let rays: String = (0..8).map(|i| {
744 let a = (i as f32 * 45.0).to_radians();
745 let r1 = r * 0.38;
746 let r2 = r * 0.90;
747 format!(r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.75"/>"#,
748 p(cx + r1*a.cos()), p(cy + r1*a.sin()),
749 p(cx + r2*a.cos()), p(cy + r2*a.sin()))
750 }).collect();
751 rays + &format!(r#"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"#,
752 p(cx), p(cy), p(r*0.32))
753 }
754 Cat::Fill => {
755 format!(r#"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="{c}" opacity="0.5"/>"#,
756 p(cx - r*0.9), p(cy - r*0.7), p(r*1.8), p(r*1.4))
757 }
758 Cat::Present => {
759 let pts = format!("{},{} {},{} {},{}",
761 p(cx - r*0.7), p(cy - r*0.8),
762 p(cx + r*0.8), p(cy),
763 p(cx - r*0.7), p(cy + r*0.8));
764 format!(r#"<polygon points="{pts}" fill="{c}" opacity="0.7"/>"#)
765 }
766 Cat::User => {
767 format!(
769 r#"<path d="M {},{} L {},{} L {},{}" fill="none" stroke="{c}" stroke-width="1.8"
770 stroke-linecap="round" stroke-linejoin="round" opacity="0.85"/>
771 <path d="M {},{} L {},{} L {},{}" fill="none" stroke="{c}" stroke-width="1.8"
772 stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/>"#,
773 p(cx-r*0.6), p(cy-r*0.7), p(cx+r*0.5), p(cy), p(cx-r*0.6), p(cy+r*0.7),
774 p(cx), p(cy-r*0.7), p(cx+r*0.95), p(cy), p(cx), p(cy+r*0.7)
775 )
776 }
777 Cat::Loop => {
778 format!(
780 r#"<path d="M {},{} A {},{} 0 1,1 {},{}"
781 fill="none" stroke="{c}" stroke-width="1.6" opacity="0.9"/>
782 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.9"/>"#,
783 p(cx), p(cy - r*0.9),
784 p(r*0.9), p(r*0.9),
785 p(cx + r*0.4), p(cy - r*0.9),
786 p(cx+r*0.4), p(cy-r*0.9),
787 p(cx+r*0.1), p(cy-r*0.55),
788 p(cx+r*0.8), p(cy-r*0.62)
789 )
790 }
791 Cat::Pagoda => {
793 let mut s = String::new();
794 for i in 0..3 {
795 let yy = cy - r*0.7 + i as f32 * r*0.55;
796 let hw = r*(0.4 + i as f32*0.22);
797 write!(s, r##"<polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85" stroke-linejoin="round"/>"##,
798 p(cx - hw), p(yy), p(cx), p(yy - r*0.35), p(cx + hw), p(yy)).ok();
799 }
800 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.55"/>"##,
801 p(cx), p(cy - r*1.05), p(cx), p(cy + r*0.55)).ok();
802 s
803 }
804 Cat::Torii => {
805 format!(
806 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.6" opacity="0.9"/>
807 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
808 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.5" opacity="0.9"/>
809 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.5" opacity="0.9"/>"##,
810 p(cx - r*0.95), p(cy - r*0.6), p(cx + r*0.95), p(cy - r*0.6),
811 p(cx - r*0.75), p(cy - r*0.2), p(cx + r*0.75), p(cy - r*0.2),
812 p(cx - r*0.55), p(cy - r*0.6), p(cx - r*0.55), p(cy + r*0.85),
813 p(cx + r*0.55), p(cy - r*0.6), p(cx + r*0.55), p(cy + r*0.85)
814 )
815 }
816 Cat::Cog => {
817 let mut s = String::new();
818 for i in 0..8 {
819 write!(s, r##"<rect x="{}" y="{}" width="{}" height="{}" fill="{c}" opacity="0.8" transform="rotate({},{},{})"/>"##,
820 p(cx - r*0.13), p(cy - r*1.0), p(r*0.26), p(r*0.3), p(i as f32 * 45.0), p(cx), p(cy)).ok();
821 }
822 write!(s, r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.55"/>
823 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.9"/>"##,
824 p(cx), p(cy), p(r*0.7), p(cx), p(cy), p(r*0.3)).ok();
825 s
826 }
827 Cat::Fractal => {
828 let outer = format!("{},{} {},{} {},{}",
829 p(cx), p(cy - r*0.9), p(cx + r*0.85), p(cy + r*0.6), p(cx - r*0.85), p(cy + r*0.6));
830 let inner = format!("{},{} {},{} {},{}",
831 p(cx), p(cy + r*0.6), p(cx - r*0.42), p(cy - r*0.12), p(cx + r*0.42), p(cy - r*0.12));
832 format!(
833 r##"<polygon points="{outer}" fill="{c}" opacity="0.4" stroke="{c}" stroke-width="1.0"/>
834 <polygon points="{inner}" fill="#0b0b1a" opacity="0.9"/>"##
835 )
836 }
837 Cat::Sfx => {
839 format!(
840 r##"<polygon points="{},{} {},{} {},{} {},{} {},{} {},{}" fill="{c}" opacity="0.85"/>"##,
841 p(cx + r*0.2), p(cy - r*0.9),
842 p(cx - r*0.5), p(cy + r*0.1),
843 p(cx - r*0.05), p(cy + r*0.1),
844 p(cx - r*0.2), p(cy + r*0.9),
845 p(cx + r*0.5), p(cy - r*0.1),
846 p(cx + r*0.05), p(cy - r*0.1)
847 )
848 }
849 Cat::Spectrum => {
850 let hs = [0.5f32, 0.9, 0.35, 0.7, 0.55];
851 let mut s = String::new();
852 for (i, &hh) in hs.iter().enumerate() {
853 let xx = cx - r*0.8 + i as f32 * r*0.4;
854 let bh = r*1.6*hh;
855 write!(s, r##"<rect x="{}" y="{}" width="{}" height="{}" rx="1" fill="{c}" opacity="0.8"/>"##,
856 p(xx - r*0.12), p(cy + r*0.8 - bh), p(r*0.24), p(bh)).ok();
857 }
858 s
859 }
860 Cat::Music => {
862 format!(
863 r##"<ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{c}" opacity="0.85" transform="rotate(-20,{},{})"/>
864 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
865 <path d="M {},{} C {},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
866 p(cx - r*0.35), p(cy + r*0.55), p(r*0.32), p(r*0.24), p(cx - r*0.35), p(cy + r*0.55),
867 p(cx - r*0.05), p(cy + r*0.5), p(cx - r*0.05), p(cy - r*0.8),
868 p(cx - r*0.05), p(cy - r*0.8), p(cx + r*0.5), p(cy - r*0.65), p(cx + r*0.55), p(cy - r*0.2), p(cx + r*0.3), p(cy - r*0.05)
869 )
870 }
871 Cat::Note => {
872 format!(
873 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>
874 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>
875 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.85"/>
876 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.85"/>
877 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="2.0" opacity="0.85"/>"##,
878 p(cx - r*0.55), p(cy + r*0.55), p(r*0.24),
879 p(cx + r*0.55), p(cy + r*0.3), p(r*0.24),
880 p(cx - r*0.33), p(cy + r*0.55), p(cx - r*0.33), p(cy - r*0.6),
881 p(cx + r*0.77), p(cy + r*0.3), p(cx + r*0.77), p(cy - r*0.85),
882 p(cx - r*0.33), p(cy - r*0.6), p(cx + r*0.77), p(cy - r*0.85)
883 )
884 }
885 Cat::Hash => {
887 let mut s = String::new();
888 for i in 0..3 {
889 let rr = r * (0.35 + i as f32 * 0.22);
890 write!(s, r##"<path d="M {},{} A {},{} 0 1,1 {},{}" fill="none" stroke="{c}" stroke-width="1.1" opacity="{:.2}"/>"##,
891 p(cx - rr), p(cy), p(rr), p(rr), p(cx + rr), p(cy), 0.85 - i as f32*0.18).ok();
892 }
893 write!(s, r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##, p(cx), p(cy), p(r*0.12)).ok();
894 s
895 }
896 Cat::Cipher => {
897 format!(
898 r##"<path d="M {},{} A {},{} 0 0,1 {},{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.85"/>
899 <rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="{c}" opacity="0.5" stroke="{c}" stroke-width="1.1"/>
900 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.9"/>"##,
901 p(cx - r*0.45), p(cy - r*0.05), p(r*0.45), p(r*0.45), p(cx + r*0.45), p(cy - r*0.05),
902 p(cx - r*0.7), p(cy - r*0.05), p(r*1.4), p(r*0.95),
903 p(cx), p(cy + r*0.4), p(r*0.14)
904 )
905 }
906 Cat::Sign => {
907 format!(
908 r##"<path d="M {},{} C {},{} {},{} {},{} S {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9" stroke-linecap="round"/>
909 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.5"/>"##,
910 p(cx - r*0.9), p(cy + r*0.4),
911 p(cx - r*0.4), p(cy - r*0.7), p(cx), p(cy + r*0.6), p(cx + r*0.3), p(cy - r*0.1),
912 p(cx + r*0.7), p(cy - r*0.7), p(cx + r*0.9), p(cy + r*0.3),
913 p(cx - r), p(cy + r*0.75), p(cx + r), p(cy + r*0.75)
914 )
915 }
916 Cat::Kem => {
917 format!(
918 r##"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
919 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
920 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
921 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
922 p(cx - r*0.5), p(cy - r*0.4), p(r*0.4),
923 p(cx - r*0.18), p(cy - r*0.12), p(cx + r*0.8), p(cy + r*0.75),
924 p(cx + r*0.55), p(cy + r*0.5), p(cx + r*0.8), p(cy + r*0.25),
925 p(cx + r*0.8), p(cy + r*0.75), p(cx + r*1.05), p(cy + r*0.5)
926 )
927 }
928 Cat::Shard => {
929 format!(
930 r##"<polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.5"/>
931 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.3"/>
932 <polygon points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.8"/>"##,
933 p(cx), p(cy - r), p(cx - r*0.85), p(cy + r*0.5), p(cx), p(cy + r*0.2),
934 p(cx), p(cy - r), p(cx + r*0.85), p(cy + r*0.5), p(cx), p(cy + r*0.2),
935 p(cx), p(cy - r), p(cx + r*0.85), p(cy + r*0.5), p(cx - r*0.85), p(cy + r*0.5)
936 )
937 }
938 Cat::Rigid => {
940 format!(
941 r##"<polygon points="{},{} {},{} {},{} {},{}" fill="{c}" opacity="0.35" stroke="{c}" stroke-width="1.0"/>
942 <path d="M {},{} L {},{} L {},{} L {},{} Z" fill="{c}" opacity="0.2" stroke="{c}" stroke-width="1.0"/>
943 <path d="M {},{} L {},{} L {},{} L {},{} Z" fill="{c}" opacity="0.28" stroke="{c}" stroke-width="1.0"/>"##,
944 p(cx), p(cy - r*0.9), p(cx + r*0.85), p(cy - r*0.4), p(cx), p(cy + r*0.1), p(cx - r*0.85), p(cy - r*0.4),
945 p(cx - r*0.85), p(cy - r*0.4), p(cx), p(cy + r*0.1), p(cx), p(cy + r*0.95), p(cx - r*0.85), p(cy + r*0.45),
946 p(cx + r*0.85), p(cy - r*0.4), p(cx), p(cy + r*0.1), p(cx), p(cy + r*0.95), p(cx + r*0.85), p(cy + r*0.45)
947 )
948 }
949 Cat::Soft => {
950 format!(
951 r##"<path d="M {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} Z" fill="{c}" opacity="0.45" stroke="{c}" stroke-width="1.0"/>"##,
952 p(cx), p(cy - r*0.85),
953 p(cx + r*0.7), p(cy - r*0.9), p(cx + r*0.95), p(cy - r*0.2), p(cx + r*0.8), p(cy + r*0.4),
954 p(cx + r*0.6), p(cy + r*0.95), p(cx - r*0.1), p(cy + r*0.9), p(cx - r*0.6), p(cy + r*0.7),
955 p(cx - r*0.95), p(cy + r*0.5), p(cx - r*0.9), p(cy - r*0.3), p(cx - r*0.7), p(cy - r*0.6),
956 p(cx - r*0.5), p(cy - r*0.85), p(cx - r*0.2), p(cy - r*0.95), p(cx), p(cy - r*0.85)
957 )
958 }
959 Cat::Liquid => {
960 format!(
961 r##"<path d="M {},{} C {},{} {},{} {},{} C {},{} {},{} {},{} Z" fill="{c}" opacity="0.5" stroke="{c}" stroke-width="1.0"/>
962 <ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="#ffffff" opacity="0.25"/>"##,
963 p(cx), p(cy - r*0.9),
964 p(cx + r*0.75), p(cy - r*0.1), p(cx + r*0.6), p(cy + r*0.8), p(cx), p(cy + r*0.85),
965 p(cx - r*0.6), p(cy + r*0.8), p(cx - r*0.75), p(cy - r*0.1), p(cx), p(cy - r*0.9),
966 p(cx - r*0.25), p(cy + r*0.25), p(r*0.16), p(r*0.28)
967 )
968 }
969 Cat::Force => {
970 format!(
971 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.8" opacity="0.9" stroke-linecap="round"/>
972 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.9"/>"##,
973 p(cx - r*0.7), p(cy + r*0.7), p(cx + r*0.45), p(cy - r*0.45),
974 p(cx + r*0.8), p(cy - r*0.8), p(cx + r*0.2), p(cy - r*0.7), p(cx + r*0.7), p(cy - r*0.15)
975 )
976 }
977 Cat::Collide => {
978 let mut s = String::new();
979 for i in 0..8 {
980 let a = (i as f32 * 45.0).to_radians();
981 let r1 = r * 0.3; let r2 = if i % 2 == 0 { r * 0.95 } else { r * 0.6 };
982 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
983 p(cx + r1*a.cos()), p(cy + r1*a.sin()), p(cx + r2*a.cos()), p(cy + r2*a.sin())).ok();
984 }
985 write!(s, r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##, p(cx), p(cy), p(r*0.18)).ok();
986 s
987 }
988 Cat::Mesh => {
990 format!(
991 r##"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.1" opacity="0.85"/>
992 <ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="none" stroke="{c}" stroke-width="0.9" opacity="0.6"/>
993 <ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="none" stroke="{c}" stroke-width="0.9" opacity="0.6"/>
994 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.9" opacity="0.6"/>"##,
995 p(cx), p(cy), p(r*0.9),
996 p(cx), p(cy), p(r*0.38), p(r*0.9),
997 p(cx), p(cy), p(r*0.9), p(r*0.38),
998 p(cx - r*0.9), p(cy), p(cx + r*0.9), p(cy)
999 )
1000 }
1001 Cat::Draw3D => {
1002 format!(
1003 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
1004 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.75"/>
1005 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.4" opacity="0.6"/>
1006 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##,
1007 p(cx), p(cy + r*0.3), p(cx), p(cy - r*0.9),
1008 p(cx), p(cy + r*0.3), p(cx + r*0.9), p(cy + r*0.7),
1009 p(cx), p(cy + r*0.3), p(cx - r*0.85), p(cy + r*0.6),
1010 p(cx), p(cy + r*0.3), p(r*0.13)
1011 )
1012 }
1013 Cat::Draw2D => {
1014 format!(
1015 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.6" opacity="0.9" stroke-linecap="round"/>
1016 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>
1017 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##,
1018 p(cx - r*0.8), p(cy + r*0.8), p(cx + r*0.8), p(cy - r*0.8),
1019 p(cx - r*0.8), p(cy + r*0.8), p(r*0.16),
1020 p(cx + r*0.8), p(cy - r*0.8), p(r*0.16)
1021 )
1022 }
1023 Cat::Trig => {
1025 format!(
1026 r##"<polygon points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1027 <path d="M {},{} A {},{} 0 0,0 {},{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
1028 p(cx - r*0.8), p(cy + r*0.7), p(cx + r*0.8), p(cy + r*0.7), p(cx - r*0.8), p(cy - r*0.7),
1029 p(cx - r*0.35), p(cy + r*0.7), p(r*0.45), p(r*0.45), p(cx - r*0.8), p(cy + r*0.38)
1030 )
1031 }
1032 Cat::MathFn => {
1033 format!(
1034 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.4"/>
1035 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.4"/>
1036 <path d="M {},{} Q {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.5" opacity="0.9"/>"##,
1037 p(cx - r*0.9), p(cy), p(cx + r*0.9), p(cy),
1038 p(cx), p(cy - r*0.9), p(cx), p(cy + r*0.9),
1039 p(cx - r*0.8), p(cy - r*0.7), p(cx), p(cy + r*1.1), p(cx + r*0.8), p(cy - r*0.7)
1040 )
1041 }
1042 Cat::Noise => {
1043 format!(
1044 r##"<polyline points="{},{} {},{} {},{} {},{} {},{} {},{} {},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85" stroke-linejoin="round"/>"##,
1045 p(cx - r*0.9), p(cy + r*0.1),
1046 p(cx - r*0.65), p(cy - r*0.6),
1047 p(cx - r*0.4), p(cy + r*0.5),
1048 p(cx - r*0.15), p(cy - r*0.4),
1049 p(cx + r*0.1), p(cy + r*0.7),
1050 p(cx + r*0.35), p(cy - r*0.5),
1051 p(cx + r*0.6), p(cy + r*0.3),
1052 p(cx + r*0.8), p(cy - r*0.3),
1053 p(cx + r*0.95), p(cy + r*0.2)
1054 )
1055 }
1056 Cat::Net => {
1058 let mut s = String::new();
1059 let nodes = [(0.0f32, -0.85f32), (0.8, 0.4), (-0.8, 0.4)];
1060 for &(nx, ny) in nodes.iter() {
1061 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>"##,
1062 p(cx), p(cy), p(cx + nx*r), p(cy + ny*r)).ok();
1063 }
1064 write!(s, r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##, p(cx), p(cy), p(r*0.2)).ok();
1065 for &(nx, ny) in nodes.iter() {
1066 write!(s, r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##, p(cx + nx*r), p(cy + ny*r), p(r*0.18)).ok();
1067 }
1068 s
1069 }
1070 Cat::Neural => {
1071 let mut s = String::new();
1072 let l0 = [-0.6f32, 0.0, 0.6];
1073 let l1 = [-0.3f32, 0.3];
1074 let x0 = cx - r*0.7; let x1 = cx; let x2 = cx + r*0.7;
1075 for &a in l0.iter() { for &b in l1.iter() {
1076 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.6" opacity="0.4"/>"##,
1077 p(x0), p(cy + a*r), p(x1), p(cy + b*r)).ok();
1078 }}
1079 for &b in l1.iter() {
1080 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.6" opacity="0.4"/>"##,
1081 p(x1), p(cy + b*r), p(x2), p(cy)).ok();
1082 }
1083 for &a in l0.iter() { write!(s, r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##, p(x0), p(cy + a*r), p(r*0.14)).ok(); }
1084 for &b in l1.iter() { write!(s, r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##, p(x1), p(cy + b*r), p(r*0.14)).ok(); }
1085 write!(s, r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##, p(x2), p(cy), p(r*0.14)).ok();
1086 s
1087 }
1088 Cat::Behavior => {
1089 format!(
1090 r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
1091 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
1092 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>
1093 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>
1094 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.8"/>"##,
1095 p(cx), p(cy - r*0.6), p(cx - r*0.7), p(cy + r*0.6),
1096 p(cx), p(cy - r*0.6), p(cx + r*0.7), p(cy + r*0.6),
1097 p(cx), p(cy - r*0.6), p(r*0.2),
1098 p(cx - r*0.7), p(cy + r*0.6), p(r*0.18),
1099 p(cx + r*0.7), p(cy + r*0.6), p(r*0.18)
1100 )
1101 }
1102 Cat::Widget => {
1104 format!(
1105 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.85"/>
1106 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>
1107 <rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="{c}" opacity="0.5"/>"##,
1108 p(cx - r*0.85), p(cy - r*0.75), p(r*1.7), p(r*1.5),
1109 p(cx - r*0.85), p(cy - r*0.3), p(cx + r*0.85), p(cy - r*0.3),
1110 p(cx - r*0.5), p(cy + r*0.1), p(r*1.0), p(r*0.45)
1111 )
1112 }
1113 Cat::Hud => {
1114 format!(
1115 r##"<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.1" opacity="0.85"/>
1116 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.6"/>
1117 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.8" opacity="0.6"/>
1118 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.9"/>
1119 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.9"/>"##,
1120 p(cx), p(cy), p(r*0.9),
1121 p(cx - r*0.9), p(cy), p(cx + r*0.9), p(cy),
1122 p(cx), p(cy - r*0.9), p(cx), p(cy + r*0.9),
1123 p(cx), p(cy), p(cx + r*0.64), p(cy - r*0.64),
1124 p(cx + r*0.4), p(cy - r*0.4), p(r*0.13)
1125 )
1126 }
1127 Cat::Dialog => {
1128 format!(
1129 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="4" fill="{c}" opacity="0.4" stroke="{c}" stroke-width="1.0"/>
1130 <polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.4"/>
1131 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.85"/>
1132 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.85"/>
1133 <circle cx="{}" cy="{}" r="{}" fill="#0b0b1a" opacity="0.85"/>"##,
1134 p(cx - r*0.9), p(cy - r*0.8), p(r*1.8), p(r*1.2),
1135 p(cx - r*0.4), p(cy + r*0.4), p(cx - r*0.1), p(cy + r*0.4), p(cx - r*0.55), p(cy + r*0.9),
1136 p(cx - r*0.4), p(cy - r*0.2), p(r*0.1),
1137 p(cx), p(cy - r*0.2), p(r*0.1),
1138 p(cx + r*0.4), p(cy - r*0.2), p(r*0.1)
1139 )
1140 }
1141 Cat::Holo => {
1142 let mut s = String::new();
1143 for i in 0..3 {
1144 let off = (i as f32 - 1.0) * r*0.22;
1145 write!(s, r##"<polygon points="{},{} {},{} {},{}" fill="{c}" opacity="0.3" stroke="{c}" stroke-width="0.8" stroke-opacity="0.7"/>"##,
1146 p(cx + off), p(cy - r*0.8), p(cx + off + r*0.8), p(cy + r*0.6), p(cx + off - r*0.8), p(cy + r*0.6)).ok();
1147 }
1148 s
1149 }
1150 Cat::Str => {
1152 let mut s = String::new();
1153 for &dx in [-0.5f32, 0.3].iter() {
1154 write!(s, r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>
1155 <circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.85"/>"##,
1156 p(cx + dx*r), p(cy - r*0.4), p(r*0.16),
1157 p(cx + dx*r + r*0.3), p(cy - r*0.4), p(r*0.16)).ok();
1158 }
1159 for j in 0..2 {
1160 let yy = cy + r*0.3 + j as f32 * r*0.45;
1161 let ww = if j == 0 { r*1.6 } else { r*1.0 };
1162 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.2" opacity="0.6"/>"##,
1163 p(cx - r*0.8), p(yy), p(cx - r*0.8 + ww), p(yy)).ok();
1164 }
1165 s
1166 }
1167 Cat::Font => {
1168 format!(
1169 r##"<polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.5" opacity="0.9" stroke-linejoin="round"/>
1170 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.85"/>"##,
1171 p(cx - r*0.7), p(cy + r*0.8), p(cx), p(cy - r*0.85), p(cx + r*0.7), p(cy + r*0.8),
1172 p(cx - r*0.38), p(cy + r*0.15), p(cx + r*0.38), p(cy + r*0.15)
1173 )
1174 }
1175 Cat::Vector => {
1176 format!(
1177 r##"<path d="M {},{} C {},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.4" opacity="0.9"/>
1178 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.7" opacity="0.5"/>
1179 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="0.7" opacity="0.5"/>
1180 <rect x="{}" y="{}" width="{}" height="{}" fill="{c}" opacity="0.85"/>
1181 <rect x="{}" y="{}" width="{}" height="{}" fill="{c}" opacity="0.85"/>
1182 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
1183 <circle cx="{}" cy="{}" r="{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
1184 p(cx - r*0.8), p(cy + r*0.7), p(cx - r*0.3), p(cy - r*0.9), p(cx + r*0.3), p(cy - r*0.9), p(cx + r*0.8), p(cy + r*0.7),
1185 p(cx - r*0.8), p(cy + r*0.7), p(cx - r*0.3), p(cy - r*0.9),
1186 p(cx + r*0.8), p(cy + r*0.7), p(cx + r*0.3), p(cy - r*0.9),
1187 p(cx - r*0.9), p(cy + r*0.6), p(r*0.2), p(r*0.2),
1188 p(cx + r*0.7), p(cy + r*0.6), p(r*0.2), p(r*0.2),
1189 p(cx - r*0.3), p(cy - r*0.9), p(r*0.14),
1190 p(cx + r*0.3), p(cy - r*0.9), p(r*0.14)
1191 )
1192 }
1193 Cat::Color => {
1195 format!(
1196 r##"<circle cx="{}" cy="{}" r="{}" fill="#ff5e8a" opacity="0.5"/>
1197 <circle cx="{}" cy="{}" r="{}" fill="#50fa7b" opacity="0.5"/>
1198 <circle cx="{}" cy="{}" r="{}" fill="#6ab0f5" opacity="0.5"/>"##,
1199 p(cx), p(cy - r*0.4), p(r*0.6),
1200 p(cx - r*0.45), p(cy + r*0.35), p(r*0.6),
1201 p(cx + r*0.45), p(cy + r*0.35), p(r*0.6)
1202 )
1203 }
1204 Cat::Shade => {
1205 format!(
1206 r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="0.25" stroke="{c}" stroke-width="1.0"/>
1207 <path d="M {},{} A {},{} 0 0,1 {},{} A {},{} 0 0,0 {},{} Z" fill="{c}" opacity="0.6"/>
1208 <circle cx="{}" cy="{}" r="{}" fill="#ffffff" opacity="0.3"/>"##,
1209 p(cx), p(cy), p(r*0.9),
1210 p(cx), p(cy - r*0.9), p(r*0.9), p(r*0.9), p(cx), p(cy + r*0.9), p(r*0.45), p(r*0.9), p(cx), p(cy - r*0.9),
1211 p(cx + r*0.35), p(cy - r*0.35), p(r*0.18)
1212 )
1213 }
1214 Cat::Window => {
1215 format!(
1216 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.85"/>
1217 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>
1218 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
1219 p(cx - r*0.85), p(cy - r*0.85), p(r*1.7), p(r*1.7),
1220 p(cx), p(cy - r*0.85), p(cx), p(cy + r*0.85),
1221 p(cx - r*0.85), p(cy), p(cx + r*0.85), p(cy)
1222 )
1223 }
1224 Cat::Key => {
1226 format!(
1227 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="3" fill="{c}" opacity="0.35" stroke="{c}" stroke-width="1.2"/>
1228 <rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.7"/>"##,
1229 p(cx - r*0.8), p(cy - r*0.8), p(r*1.6), p(r*1.6),
1230 p(cx - r*0.45), p(cy - r*0.45), p(r*0.9), p(r*0.9)
1231 )
1232 }
1233 Cat::Mouse => {
1234 format!(
1235 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="{}" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.85"/>
1236 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>
1237 <rect x="{}" y="{}" width="{}" height="{}" rx="1" fill="{c}" opacity="0.85"/>"##,
1238 p(cx - r*0.55), p(cy - r*0.85), p(r*1.1), p(r*1.7), p(r*0.55),
1239 p(cx), p(cy - r*0.85), p(cx), p(cy - r*0.1),
1240 p(cx - r*0.08), p(cy - r*0.7), p(r*0.16), p(r*0.4)
1241 )
1242 }
1243 Cat::Print => {
1245 let mut s = format!(
1246 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="1.5" fill="none" stroke="{c}" stroke-width="1.2" opacity="0.85"/>"##,
1247 p(cx - r*0.6), p(cy - r*0.85), p(r*1.2), p(r*1.7));
1248 for j in 0..4 {
1249 let yy = cy - r*0.45 + j as f32 * r*0.35;
1250 write!(s, r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.0" opacity="0.6"/>"##,
1251 p(cx - r*0.4), p(yy), p(cx + r*0.4), p(yy)).ok();
1252 }
1253 s
1254 }
1255 Cat::File => {
1256 format!(
1257 r##"<path d="M {},{} L {},{} L {},{} L {},{} L {},{} Z" fill="{c}" opacity="0.35" stroke="{c}" stroke-width="1.1"/>
1258 <polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.0" opacity="0.8"/>"##,
1259 p(cx - r*0.6), p(cy - r*0.85),
1260 p(cx + r*0.25), p(cy - r*0.85),
1261 p(cx + r*0.6), p(cy - r*0.45),
1262 p(cx + r*0.6), p(cy + r*0.85),
1263 p(cx - r*0.6), p(cy + r*0.85),
1264 p(cx + r*0.25), p(cy - r*0.85), p(cx + r*0.25), p(cy - r*0.45), p(cx + r*0.6), p(cy - r*0.45)
1265 )
1266 }
1267 Cat::Sys => {
1268 format!(
1269 r##"<rect x="{}" y="{}" width="{}" height="{}" rx="2" fill="none" stroke="{c}" stroke-width="1.1" opacity="0.8"/>
1270 <polyline points="{},{} {},{} {},{}" fill="none" stroke="{c}" stroke-width="1.3" opacity="0.9" stroke-linecap="round" stroke-linejoin="round"/>
1271 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.3" opacity="0.9" stroke-linecap="round"/>"##,
1272 p(cx - r*0.9), p(cy - r*0.7), p(r*1.8), p(r*1.4),
1273 p(cx - r*0.45), p(cy - r*0.2), p(cx - r*0.1), p(cy + r*0.15), p(cx - r*0.45), p(cy + r*0.5),
1274 p(cx + r*0.1), p(cy + r*0.5), p(cx + r*0.5), p(cy + r*0.5)
1275 )
1276 }
1277 Cat::Anim => {
1278 let mut s = String::new();
1279 for i in 0..6 {
1280 let a = (i as f32 * 60.0).to_radians();
1281 let rad = r*0.7;
1282 let dotr = r*(0.08 + 0.16*(i as f32/6.0));
1283 write!(s, r##"<circle cx="{}" cy="{}" r="{}" fill="{c}" opacity="{:.2}"/>"##,
1284 p(cx + rad*a.cos()), p(cy + rad*a.sin()), p(dotr), 0.35 + i as f32*0.1).ok();
1285 }
1286 s
1287 }
1288 Cat::Const => {
1289 format!(r#"<rect x="{}" y="{}" width="{}" height="{}" rx="2"
1290 fill="none" stroke="{c}" stroke-width="1.2" opacity="0.7"/>
1291 <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{c}" stroke-width="1.1" opacity="0.5"/>"#,
1292 p(cx-r*0.7), p(cy-r*0.55), p(r*1.4), p(r*1.1),
1293 p(cx-r*0.4), p(cy), p(cx+r*0.4), p(cy))
1294 }
1295 }
1296}
1297
1298fn card_height(card: &FuncCard) -> f32 {
1301 let icon_rows = (card.calls.len() + ICONS_ROW - 1).max(1) / ICONS_ROW + 1;
1302 let base = CARD_PAD * 2.0 + 32.0 + 20.0; base + icon_rows as f32 * (ICON_SZ + ICON_GAP) + ICON_GAP
1306}
1307
1308fn render_card(card: &FuncCard, x: f32, y: f32) -> String {
1309 let h = card_height(card);
1310 let w = CARD_W;
1311 let r = CARD_ROUNDING;
1312
1313 let dominant = card.calls.iter()
1315 .max_by_key(|c| c.count)
1316 .map(|c| c.cat.color())
1317 .unwrap_or(Cat::User.color());
1318
1319 let border_color = if card.is_entry { GOLD } else { dominant };
1320 let border_w = if card.is_entry { 2.5 } else { 1.2 };
1321 let glow_filter = if card.is_entry { r#" filter="url(#glow-gold)""# } else { "" };
1322
1323 let mut s = String::new();
1324
1325 write!(s, r#"<rect x="{}" y="{}" width="{w}" height="{h}" rx="{r}"
1327 fill="{CARD_BG}" stroke="{border_color}" stroke-width="{border_w}"{glow_filter}/>
1328 <line x1="{}" y1="{}" x2="{}" y2="{}"
1329 stroke="{border_color}" stroke-width="3" opacity="0.6"/>
1330"#,
1331 p(x), p(y),
1332 p(x+r), p(y+h), p(x+r), p(y) ).ok();
1334
1335 let name_y = y + CARD_PAD + 18.0;
1337 let entry_badge = if card.is_entry {
1338 format!(r#" <text x="{}" y="{}" fill="{GOLD}" font-size="9" font-weight="bold" opacity="0.8">⬡ ENTRY</text>"#,
1339 p(x + w - CARD_PAD - 48.0), p(name_y))
1340 } else { String::new() };
1341
1342 write!(s, r#"<text x="{}" y="{}" fill="{}" font-size="13" font-weight="bold">{}</text>{}"#,
1343 p(x + CARD_PAD + 10.0), p(name_y),
1344 if card.is_entry { GOLD } else { TEXT },
1345 xe(&card.name),
1346 entry_badge
1347 ).ok();
1348
1349 let stats_y = name_y + 18.0;
1351 let params_str = if card.params.is_empty() { String::new() }
1352 else { format!("({})", card.params.join(", ")) };
1353 let stats_str = {
1354 let mut parts = Vec::new();
1355 if card.vtex_count > 0 { parts.push(format!("{} vtex", card.vtex_count)); }
1356 if card.audio_count > 0 { parts.push(format!("{} audio", card.audio_count)); }
1357 if card.has_loop { parts.push("↺ loop".into()); }
1358 parts.join(" · ")
1359 };
1360 write!(s, r#"<text x="{}" y="{}" fill="{TEXT_DIM}" font-size="10">{}</text>
1361 <text x="{}" y="{}" fill="{TEXT_DIM}" font-size="10" text-anchor="end">{}</text>
1362"#,
1363 p(x + CARD_PAD + 10.0), p(stats_y), xe(¶ms_str),
1364 p(x + w - CARD_PAD), p(stats_y), xe(&stats_str)
1365 ).ok();
1366
1367 let div_y = stats_y + 6.0;
1369 write!(s, r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{CARD_BD}" stroke-width="1"/>"#,
1370 p(x + CARD_PAD), p(div_y), p(x + w - CARD_PAD), p(div_y)).ok();
1371
1372 let icon_y0 = div_y + ICON_GAP + ICON_SZ / 2.0;
1374 let icon_x0 = x + CARD_PAD + ICON_SZ / 2.0 + 6.0;
1375 let ir = ICON_SZ / 2.0 * 0.82; for (i, call) in card.calls.iter().enumerate() {
1378 let row = i / ICONS_ROW;
1379 let col = i % ICONS_ROW;
1380 let ix = icon_x0 + col as f32 * (ICON_SZ + ICON_GAP);
1381 let iy = icon_y0 + row as f32 * (ICON_SZ + ICON_GAP);
1382
1383 write!(s, r#"<rect x="{}" y="{}" width="{ICON_SZ}" height="{ICON_SZ}" rx="3"
1385 fill="{}" opacity="0.12"/>
1386"#,
1387 p(ix - ICON_SZ/2.0), p(iy - ICON_SZ/2.0),
1388 call.cat.color()
1389 ).ok();
1390
1391 s.push_str(&icon(call.cat, ix, iy, ir));
1393
1394 if call.count > 1 {
1396 write!(s, r##"<rect x="{}" y="{}" width="13" height="10" rx="3" fill="{}" opacity="0.9"/>
1397 <text x="{}" y="{}" fill="#0a0a1a" font-size="8" font-weight="bold" text-anchor="middle">{}</text>"##,
1398 p(ix + ir - 2.0), p(iy - ir - 1.0), call.cat.color(),
1399 p(ix + ir + 4.5), p(iy - ir + 7.0), call.count
1400 ).ok();
1401 }
1402 }
1403
1404 s
1405}
1406
1407fn render_header(filename: &str, funcs: &[FuncCard]) -> String {
1410 let name = std::path::Path::new(filename)
1411 .file_name().map(|n| n.to_string_lossy().into_owned())
1412 .unwrap_or_else(|| filename.to_string());
1413 let n_fns = funcs.len();
1414 let count = |pred: fn(Cat) -> bool| -> usize {
1415 funcs.iter().flat_map(|f| &f.calls).filter(|c| pred(c.cat)).map(|c| c.count).sum()
1416 };
1417 let n_vtex = count(is_vtex);
1418 let n_audio = count(is_audio);
1419 let n_crypto = count(is_crypto);
1420 let n_physics = count(is_physics);
1421 let n_ai = count(is_ai);
1422 let n_calls: usize = funcs.iter().map(|f| f.calls.len()).sum();
1423
1424 let mut stats = vec![format!("{n_fns} fn"), format!("{n_calls} call types")];
1426 for (n, lbl) in [(n_vtex, "vtex"), (n_audio, "audio"), (n_crypto, "crypto"),
1427 (n_physics, "physics"), (n_ai, "ai")] {
1428 if n > 0 { stats.push(format!("{n} {lbl}")); }
1429 }
1430
1431 format!(
1432 r##"<rect x="0" y="0" width="{SVG_W}" height="{HEADER_H}" fill="#080814"/>
1433 <line x1="0" y1="{HEADER_H}" x2="{SVG_W}" y2="{HEADER_H}" stroke="#1a1a3a" stroke-width="1"/>
1434 <text x="20" y="22" fill="{GOLD}" font-size="9" font-weight="bold" opacity="0.6" letter-spacing="2">LING VISUALIZER</text>
1435 <text x="20" y="56" fill="{TEXT}" font-size="26" font-weight="bold">{}</text>
1436 <text x="{}" y="56" fill="{TEXT_DIM}" font-size="12" text-anchor="end">{}</text>"##,
1437 xe(&name),
1438 SVG_W - 20.0,
1439 stats.join(" · ")
1440 )
1441}
1442
1443fn render_legend(funcs: &[FuncCard]) -> String {
1444 let mut present: Vec<Cat> = Vec::new();
1447 let mut seen = HashSet::new();
1448 for f in funcs {
1449 for c in &f.calls {
1450 if seen.insert(c.cat) { present.push(c.cat); }
1451 }
1452 }
1453 present.sort_by(|a, b| a.domain().cmp(b.domain()).then(a.label().cmp(b.label())));
1454
1455 let mut s = format!(
1456 r##"<rect x="0" y="{HEADER_H}" width="{SVG_W}" height="{LEGEND_H}" fill="#0a0a18"/>
1457 <line x1="0" y1="{}" x2="{SVG_W}" y2="{}" stroke="#171730" stroke-width="1"/>"##,
1458 HEADER_H + LEGEND_H, HEADER_H + LEGEND_H
1459 );
1460
1461 let row_h = 21.0;
1462 let x_start = GRID_X + 8.0;
1463 let x_max = SVG_W - 24.0;
1464 let mut lx = x_start;
1465 let mut row = 0usize;
1466 let mut cur_domain = "";
1467 let row_y = |r: usize| HEADER_H + 17.0 + r as f32 * row_h;
1468
1469 for cat in present {
1470 let c = cat.color();
1471 let lbl = cat.label();
1472 let dom = cat.domain();
1473
1474 if dom != cur_domain {
1476 let tag_w = dom.len() as f32 * 6.0 + 12.0;
1477 if lx > x_start && lx + tag_w > x_max { row += 1; lx = x_start; }
1478 if row > 1 { break; } let ly = row_y(row);
1480 s.push_str(&format!(
1481 r##"<text x="{}" y="{}" fill="#3f3f66" font-size="8" font-weight="bold" letter-spacing="1">{}</text>"##,
1482 p(lx), p(ly + 3.0), dom.to_uppercase()
1483 ));
1484 lx += tag_w;
1485 cur_domain = dom;
1486 }
1487
1488 let item_w = 20.0 + lbl.len() as f32 * 6.2 + 12.0;
1489 if lx + item_w > x_max { row += 1; lx = x_start; }
1490 if row > 1 { break; }
1491 let ly = row_y(row);
1492 let icon_svg = icon(cat, lx + 7.0, ly, 6.0);
1493 s.push_str(&format!(
1494 r#"<rect x="{}" y="{}" width="14" height="14" rx="3" fill="{c}" opacity="0.12"/>
1495 {}
1496 <text x="{}" y="{}" fill="{TEXT_DIM}" font-size="10">{lbl}</text>"#,
1497 p(lx), p(ly - 7.0),
1498 icon_svg,
1499 p(lx + 18.0), p(ly + 4.0)
1500 ));
1501 lx += item_w;
1502 }
1503 s
1504}
1505
1506fn render_sidebar(globals: &[GlobalConst], total_h: f32) -> String {
1507 let h = total_h - CONTENT_Y;
1508 let mut s = format!(
1509 r##"<rect x="0" y="{CONTENT_Y}" width="{SIDEBAR_W}" height="{h}" fill="{SIDEBAR_BG}"/>
1510 <line x1="{SIDEBAR_W}" y1="{CONTENT_Y}" x2="{SIDEBAR_W}" y2="{total_h}" stroke="#1a1a3a" stroke-width="1"/>
1511 <text x="14" y="{}" fill="{TEXT_DIM}" font-size="9" font-weight="bold" letter-spacing="2">CONSTANTS</text>"##,
1512 CONTENT_Y + 20.0
1513 );
1514
1515 let mut gy = CONTENT_Y + 38.0;
1516 for g in globals {
1517 write!(s,
1519 r#"<rect x="{}" y="{}" width="8" height="8" rx="1" fill="{}" opacity="0.7"
1520 transform="rotate(45,{},{})"/>
1521 <text x="{}" y="{}" fill="{}" font-size="12" font-weight="bold">{}</text>
1522 <text x="{}" y="{}" fill="{TEXT_DIM}" font-size="12" text-anchor="end">{}</text>"#,
1523 p(14.0), p(gy - 7.0), Cat::Star.color(),
1524 p(18.0), p(gy - 3.0),
1525 p(28.0), p(gy), Cat::Grid.color(), xe(&g.name),
1526 p(SIDEBAR_W - 10.0), p(gy), xe(&g.value)
1527 ).ok();
1528 gy += 22.0;
1529 }
1530 s
1531}
1532
1533const DEFS: &str = r##"<defs>
1536 <filter id="glow-gold" x="-30%" y="-30%" width="160%" height="160%">
1537 <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
1538 <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
1539 </filter>
1540 <pattern id="grid-pat" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
1541 <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#ffffff" stroke-width="0.4"/>
1542 </pattern>
1543</defs>"##;
1544
1545pub fn render(filename: &str, program: &Program) -> String {
1548 let doc = Document::build(filename, program);
1549
1550 let heights: Vec<f32> = doc.funcs.iter().map(|f| card_height(f)).collect();
1552 let mut col_y = vec![CONTENT_Y; COLS];
1553 let mut positions: Vec<(f32, f32)> = Vec::with_capacity(doc.funcs.len());
1554
1555 for &h in &heights {
1556 let (col, &cy) = col_y.iter().enumerate()
1557 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap()).unwrap();
1558 let cx = GRID_X + col as f32 * (CARD_W + CARD_GAP);
1559 positions.push((cx, cy));
1560 col_y[col] += h + CARD_GAP;
1561 }
1562
1563 let total_h = col_y.iter().cloned().fold(0.0f32, f32::max) + 40.0;
1564
1565 let mut svg = String::new();
1566 write!(svg,
1567 r#"<?xml version="1.0" encoding="UTF-8"?>
1568<svg xmlns="http://www.w3.org/2000/svg" width="{SVG_W}" height="{h}" viewBox="0 0 {SVG_W} {h}"
1569 style="font-family:'JetBrains Mono','Fira Code',monospace,sans-serif;background:{BG}">"#,
1570 h = total_h
1571 ).ok();
1572
1573 svg.push_str(DEFS);
1574
1575 write!(svg, r#"<rect width="{SVG_W}" height="{total_h}" fill="{BG}"/>
1577 <rect width="{SVG_W}" height="{total_h}" fill="url(#grid-pat)" opacity="0.05"/>"#,
1578 total_h = total_h).ok();
1579
1580 svg.push_str(&render_header(&doc.filename, &doc.funcs));
1581 svg.push_str(&render_legend(&doc.funcs));
1582 svg.push_str(&render_sidebar(&doc.globals, total_h));
1583
1584 for (card, &(cx, cy)) in doc.funcs.iter().zip(positions.iter()) {
1585 svg.push_str(&render_card(card, cx, cy));
1586 }
1587
1588 svg.push_str("\n</svg>");
1589 svg
1590}