1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// Options for constructing a [`ms.Symbol`][1] in the JavaScript milsymbol library.
///
/// ## How JS routing works
/// The JS `setOptions()` function routes each key from this flat object into one of two internal
/// buckets — `this.style` or `this.options` — by checking which one owns the key via
/// `hasOwnProperty`. Both are read at render time. From the Rust side this is transparent: you
/// just set fields here and they land in the right JS bucket automatically.
///
/// ## Serde strategy
/// All fields are `Option<T>` with `#[serde(skip_serializing_if = "Option::is_none")]`. Only
/// fields you explicitly set are serialised, so JS defaults are never silently overridden.
///
/// ## Color fields (`ColorMode | string`)
/// Several options accept either a named color-mode string (e.g. `"Light"`, `"Dark"`) **or** a
/// full [`ColorMode`][crate::types::ColorMode] object. These are typed as
/// `Option<serde_json::Value>`. Use the `*_str` builder variant for a named string, and the
/// `*_obj` builder variant (or construct the `Value` directly) for a full object.
///
/// ## Escape hatch
/// The `extra` field accepts any key/value pair not covered by the typed fields — useful for
/// options added in future milsymbol releases before this crate catches up.
///
/// [1]: https://github.com/spatialillusions/milsymbol
#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MilsymbolOptions {
// -------------------------------------------------------------------------
// Style / rendering options
// (routed to `this.style` by the JS setOptions() function)
// -------------------------------------------------------------------------
/// Use the alternate MEDAL icon (MIL-STD-2525D). Defaults to `false`.
#[serde(skip_serializing_if = "Option::is_none")]
pub alternate_medal: Option<bool>,
/// Apply civilian purple colour to civilian symbols. Defaults to `true`.
#[serde(skip_serializing_if = "Option::is_none")]
pub civilian_color: Option<bool>,
/// Active colour mode.
///
/// Accepts either a named mode string (e.g. `"Light"`, `"Dark"`, `"Medium"`) or a full
/// [`ColorMode`][crate::types::ColorMode] object serialised as JSON. Use
/// [`color_mode_str`][Self::color_mode_str] for a named string or
/// [`color_mode_obj`][Self::color_mode_obj] for a typed object.
///
/// JS default: `"Light"`.
#[serde(skip_serializing_if = "Option::is_none")]
pub color_mode: Option<serde_json::Value>,
/// Whether the symbol is filled with colour. Defaults to `true`.
#[serde(skip_serializing_if = "Option::is_none")]
pub fill: Option<bool>,
/// Fill colour override (CSS colour string). When set, overrides the colour-mode fill.
#[serde(skip_serializing_if = "Option::is_none")]
pub fill_color: Option<String>,
/// Fill opacity (0.0–1.0). Defaults to `1.0`.
#[serde(skip_serializing_if = "Option::is_none")]
pub fill_opacity: Option<f64>,
/// Font family used for text modifier fields. Defaults to `"Arial"`.
///
/// # Note on naming
/// The JS wire key is `"fontfamily"` (all lowercase) — not the camelCase `"fontFamily"`.
/// The field is therefore named `fontfamily` directly so that `rename_all = "camelCase"`
/// produces the correct key without an explicit `rename` attribute.
#[serde(skip_serializing_if = "Option::is_none")]
pub fontfamily: Option<String>,
/// Whether the symbol frame is drawn. Defaults to `true`.
#[serde(skip_serializing_if = "Option::is_none")]
pub frame: Option<bool>,
/// Frame colour override.
///
/// Accepts either a named mode string or a [`ColorMode`][crate::types::ColorMode] object.
/// Use [`frame_color_str`][Self::frame_color_str] or [`frame_color_obj`][Self::frame_color_obj].
#[serde(skip_serializing_if = "Option::is_none")]
pub frame_color: Option<serde_json::Value>,
/// Per-symbol HQ staff length override (pixels). `0` means use the global default.
#[serde(skip_serializing_if = "Option::is_none")]
pub hq_staff_length: Option<f64>,
/// Whether the icon is drawn inside the frame. Defaults to `true`.
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<bool>,
/// Icon colour override.
///
/// Accepts either a named mode string or a [`ColorMode`][crate::types::ColorMode] object.
/// Use [`icon_color_str`][Self::icon_color_str] or [`icon_color_obj`][Self::icon_color_obj].
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_color: Option<serde_json::Value>,
/// Background colour for info text fields.
///
/// Accepts either a named mode string or a [`ColorMode`][crate::types::ColorMode] object.
/// Use [`info_background_str`][Self::info_background_str] or
/// [`info_background_obj`][Self::info_background_obj].
#[serde(skip_serializing_if = "Option::is_none")]
pub info_background: Option<serde_json::Value>,
/// Frame colour for the info text field background.
///
/// Accepts either a named mode string or a [`ColorMode`][crate::types::ColorMode] object.
/// Use [`info_background_frame_str`][Self::info_background_frame_str] or
/// [`info_background_frame_obj`][Self::info_background_frame_obj].
#[serde(skip_serializing_if = "Option::is_none")]
pub info_background_frame: Option<serde_json::Value>,
/// Colour override for all info text fields.
///
/// Accepts either a named mode string or a [`ColorMode`][crate::types::ColorMode] object.
/// Use [`info_color_str`][Self::info_color_str] or [`info_color_obj`][Self::info_color_obj].
#[serde(skip_serializing_if = "Option::is_none")]
pub info_color: Option<serde_json::Value>,
/// Whether info text modifier fields are rendered. Defaults to `true`.
#[serde(skip_serializing_if = "Option::is_none")]
pub info_fields: Option<bool>,
/// Colour of the text outline for info fields (CSS colour string).
#[serde(skip_serializing_if = "Option::is_none")]
pub info_outline_color: Option<String>,
/// Sets the info field text outline width in pixels (`0` disables it).
///
/// Note: the JS engine treats this field as `false | number` internally. When reading the
/// effective options back (e.g. from [`SymbolOutput::options`]), the value may be the
/// boolean `false` rather than a number.
#[serde(skip_serializing_if = "Option::is_none")]
pub info_outline_width: Option<serde_json::Value>,
/// Relative size of the info text modifier fields as a percentage of the symbol size.
/// Defaults to `40`.
#[serde(skip_serializing_if = "Option::is_none")]
pub info_size: Option<f64>,
/// Monochrome colour override. When non-empty the entire symbol is rendered in this colour.
#[serde(skip_serializing_if = "Option::is_none")]
pub mono_color: Option<String>,
/// Outline colour override.
///
/// Accepts either a named mode string or a [`ColorMode`][crate::types::ColorMode] object.
/// Use [`outline_color_str`][Self::outline_color_str] or
/// [`outline_color_obj`][Self::outline_color_obj].
#[serde(skip_serializing_if = "Option::is_none")]
pub outline_color: Option<serde_json::Value>,
/// Stroke width of the symbol outline in pixels. `0` disables the outline.
#[serde(skip_serializing_if = "Option::is_none")]
pub outline_width: Option<f64>,
/// Extra padding added around the symbol bounding box in pixels.
#[serde(skip_serializing_if = "Option::is_none")]
pub padding: Option<f64>,
/// Force use of simple (non-standard) status modifiers. Defaults to `false`.
#[serde(skip_serializing_if = "Option::is_none")]
pub simple_status_modifier: Option<bool>,
/// The size of the symbol (corresponds to the `L` variable in MIL-STD geometry).
/// Defaults to `100`.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<f64>,
/// Force the symbol bounding box to a square. Defaults to `false`.
#[serde(skip_serializing_if = "Option::is_none")]
pub square: Option<bool>,
/// Standard override (`"2525"` or `"APP6"`). Omit to use the global default.
#[serde(skip_serializing_if = "Option::is_none")]
pub standard: Option<String>,
/// Stroke width of the symbol frame in pixels. Defaults to `4`.
#[serde(skip_serializing_if = "Option::is_none")]
pub stroke_width: Option<f64>,
// -------------------------------------------------------------------------
// Options routed to `this.options` in JS (text modifiers + a few others)
// -------------------------------------------------------------------------
/// The number of items present (FieldID C).
#[serde(skip_serializing_if = "Option::is_none")]
pub quantity: Option<String>,
/// Reinforced or reduced indicator (FieldID F).
#[serde(skip_serializing_if = "Option::is_none")]
pub reinforced_reduced: Option<String>,
/// Text modifier for staff comments (FieldID G).
#[serde(skip_serializing_if = "Option::is_none")]
pub staff_comments: Option<String>,
/// Additional information modifier (FieldID H).
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_information: Option<String>,
/// Evaluation rating modifier (FieldID J).
#[serde(skip_serializing_if = "Option::is_none")]
pub evaluation_rating: Option<String>,
/// Combat effectiveness modifier (FieldID K).
#[serde(skip_serializing_if = "Option::is_none")]
pub combat_effectiveness: Option<String>,
/// Signature equipment modifier (FieldID L).
#[serde(skip_serializing_if = "Option::is_none")]
pub signature_equipment: Option<String>,
/// Higher formation modifier (FieldID M).
#[serde(skip_serializing_if = "Option::is_none")]
pub higher_formation: Option<String>,
/// Hostile modifier (FieldID N).
#[serde(skip_serializing_if = "Option::is_none")]
pub hostile: Option<String>,
/// IFF/SIF modifier (FieldID P).
#[serde(skip_serializing_if = "Option::is_none")]
pub iff_sif: Option<String>,
/// Direction of movement or pointing in degrees (FieldID Q).
#[serde(skip_serializing_if = "Option::is_none")]
pub direction: Option<f64>,
/// SIGINT modifier (FieldID R2).
#[serde(skip_serializing_if = "Option::is_none")]
pub sigint: Option<String>,
/// Unique designation modifier (FieldID T).
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_designation: Option<String>,
/// Type of equipment or unit modifier (FieldID V).
#[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>,
/// Date-Time Group modifier (FieldID W).
#[serde(skip_serializing_if = "Option::is_none")]
pub dtg: Option<String>,
/// Altitude/depth modifier (FieldID X).
#[serde(skip_serializing_if = "Option::is_none")]
pub altitude_depth: Option<String>,
/// Location modifier (FieldID Y).
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
/// Speed modifier (FieldID Z).
#[serde(skip_serializing_if = "Option::is_none")]
pub speed: Option<String>,
/// Length of the speed leader line as a multiplier of symbol size.
#[serde(skip_serializing_if = "Option::is_none")]
pub speed_leader: Option<f64>,
/// Special headquarters modifier (FieldID AA).
#[serde(skip_serializing_if = "Option::is_none")]
pub special_headquarters: Option<String>,
/// Country code modifier (FieldID AC).
#[serde(skip_serializing_if = "Option::is_none")]
pub country: Option<String>,
/// Platform type modifier (FieldID AD).
#[serde(skip_serializing_if = "Option::is_none")]
pub platform_type: Option<String>,
/// Equipment teardown time modifier (FieldID AE).
#[serde(skip_serializing_if = "Option::is_none")]
pub equipment_teardown_time: Option<String>,
/// Common identifier modifier (FieldID AF).
#[serde(skip_serializing_if = "Option::is_none")]
pub common_identifier: Option<String>,
/// Auxiliary equipment indicator modifier (FieldID AG).
#[serde(skip_serializing_if = "Option::is_none")]
pub auxiliary_equipment_indicator: Option<String>,
/// Headquarters element modifier (FieldID AH).
#[serde(skip_serializing_if = "Option::is_none")]
pub headquarters_element: Option<String>,
/// Installation composition modifier (FieldID AI).
#[serde(skip_serializing_if = "Option::is_none")]
pub installation_composition: Option<String>,
/// Engagement bar modifier (FieldID AO).
#[serde(skip_serializing_if = "Option::is_none")]
pub engagement_bar: Option<String>,
/// Engagement bar type (`"TARGET"`, `"NON-TARGET"`, or `"EXPIRED"`).
#[serde(skip_serializing_if = "Option::is_none")]
pub engagement_type: Option<String>,
/// Guarded unit modifier (FieldID AQ).
#[serde(skip_serializing_if = "Option::is_none")]
pub guarded_unit: Option<String>,
/// Special designator modifier (FieldID AR).
#[serde(skip_serializing_if = "Option::is_none")]
pub special_designator: Option<String>,
/// Stack number — controls which symbol in a stack is shown (0-based index).
#[serde(skip_serializing_if = "Option::is_none")]
pub stack: Option<f64>,
// -------------------------------------------------------------------------
// Escape hatch for future / custom options
// -------------------------------------------------------------------------
/// Any additional options not explicitly typed above.
///
/// Keys are serialised as-is (no camelCase conversion). Use this for options added by
/// future milsymbol releases before this crate provides typed fields for them.
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
impl MilsymbolOptions {
/// Creates a new default set of options (all fields unset).
pub fn new() -> Self {
Self::default()
}
// --- Style / rendering options ---
/// Sets whether the alternate MEDAL icon is used.
pub fn alternate_medal(mut self, alternate_medal: bool) -> Self {
self.alternate_medal = Some(alternate_medal);
self
}
/// Sets whether civilian purple colour is applied to civilian symbols.
pub fn civilian_color(mut self, civilian_color: bool) -> Self {
self.civilian_color = Some(civilian_color);
self
}
/// Sets the colour mode by name (e.g. `"Light"`, `"Dark"`, `"Medium"`).
pub fn color_mode_str(mut self, name: impl Into<String>) -> Self {
self.color_mode = Some(serde_json::Value::String(name.into()));
self
}
/// Sets the colour mode from a [`ColorMode`][crate::types::ColorMode] object.
pub fn color_mode_obj(mut self, mode: &crate::types::ColorMode) -> Self {
self.color_mode = Some(serde_json::to_value(mode).expect("ColorMode is always serialisable"));
self
}
/// Sets the colour mode from a raw JSON value (string or object).
pub fn color_mode(mut self, value: serde_json::Value) -> Self {
self.color_mode = Some(value);
self
}
/// Sets whether the symbol is filled with colour.
pub fn fill(mut self, fill: bool) -> Self {
self.fill = Some(fill);
self
}
/// Sets the fill colour override (CSS colour string).
pub fn fill_color(mut self, fill_color: impl Into<String>) -> Self {
self.fill_color = Some(fill_color.into());
self
}
/// Sets the fill opacity (0.0–1.0).
pub fn fill_opacity(mut self, fill_opacity: f64) -> Self {
self.fill_opacity = Some(fill_opacity);
self
}
/// Sets the font family for text modifier fields.
pub fn fontfamily(mut self, fontfamily: impl Into<String>) -> Self {
self.fontfamily = Some(fontfamily.into());
self
}
/// Sets whether the symbol frame is drawn.
pub fn frame(mut self, frame: bool) -> Self {
self.frame = Some(frame);
self
}
/// Sets the frame colour by mode name.
pub fn frame_color_str(mut self, name: impl Into<String>) -> Self {
self.frame_color = Some(serde_json::Value::String(name.into()));
self
}
/// Sets the frame colour from a [`ColorMode`][crate::types::ColorMode] object.
pub fn frame_color_obj(mut self, mode: &crate::types::ColorMode) -> Self {
self.frame_color = Some(serde_json::to_value(mode).expect("ColorMode is always serialisable"));
self
}
/// Sets the frame colour from a raw JSON value (string or object).
pub fn frame_color(mut self, value: serde_json::Value) -> Self {
self.frame_color = Some(value);
self
}
/// Sets the per-symbol HQ staff length override in pixels.
pub fn hq_staff_length(mut self, hq_staff_length: f64) -> Self {
self.hq_staff_length = Some(hq_staff_length);
self
}
/// Sets whether the icon is drawn inside the frame.
pub fn icon(mut self, icon: bool) -> Self {
self.icon = Some(icon);
self
}
/// Sets the icon colour by mode name.
pub fn icon_color_str(mut self, name: impl Into<String>) -> Self {
self.icon_color = Some(serde_json::Value::String(name.into()));
self
}
/// Sets the icon colour from a [`ColorMode`][crate::types::ColorMode] object.
pub fn icon_color_obj(mut self, mode: &crate::types::ColorMode) -> Self {
self.icon_color = Some(serde_json::to_value(mode).expect("ColorMode is always serialisable"));
self
}
/// Sets the icon colour from a raw JSON value (string or object).
pub fn icon_color(mut self, value: serde_json::Value) -> Self {
self.icon_color = Some(value);
self
}
/// Sets the info field background colour by mode name.
pub fn info_background_str(mut self, name: impl Into<String>) -> Self {
self.info_background = Some(serde_json::Value::String(name.into()));
self
}
/// Sets the info field background colour from a [`ColorMode`][crate::types::ColorMode] object.
pub fn info_background_obj(mut self, mode: &crate::types::ColorMode) -> Self {
self.info_background = Some(serde_json::to_value(mode).expect("ColorMode is always serialisable"));
self
}
/// Sets the info field background colour from a raw JSON value (string or object).
pub fn info_background(mut self, value: serde_json::Value) -> Self {
self.info_background = Some(value);
self
}
/// Sets the info field background frame colour by mode name.
pub fn info_background_frame_str(mut self, name: impl Into<String>) -> Self {
self.info_background_frame = Some(serde_json::Value::String(name.into()));
self
}
/// Sets the info field background frame colour from a [`ColorMode`][crate::types::ColorMode] object.
pub fn info_background_frame_obj(mut self, mode: &crate::types::ColorMode) -> Self {
self.info_background_frame = Some(
serde_json::to_value(mode).expect("ColorMode is always serialisable"),
);
self
}
/// Sets the info field background frame colour from a raw JSON value (string or object).
pub fn info_background_frame(mut self, value: serde_json::Value) -> Self {
self.info_background_frame = Some(value);
self
}
/// Sets the info field text colour by mode name.
pub fn info_color_str(mut self, name: impl Into<String>) -> Self {
self.info_color = Some(serde_json::Value::String(name.into()));
self
}
/// Sets the info field text colour from a [`ColorMode`][crate::types::ColorMode] object.
pub fn info_color_obj(mut self, mode: &crate::types::ColorMode) -> Self {
self.info_color = Some(serde_json::to_value(mode).expect("ColorMode is always serialisable"));
self
}
/// Sets the info field text colour from a raw JSON value (string or object).
pub fn info_color(mut self, value: serde_json::Value) -> Self {
self.info_color = Some(value);
self
}
/// Sets whether info text modifier fields are rendered.
pub fn info_fields(mut self, info_fields: bool) -> Self {
self.info_fields = Some(info_fields);
self
}
/// Sets the info field text outline colour (CSS colour string).
pub fn info_outline_color(mut self, info_outline_color: impl Into<String>) -> Self {
self.info_outline_color = Some(info_outline_color.into());
self
}
/// Sets the info field text outline width in pixels (`0` disables it).
pub fn info_outline_width(mut self, info_outline_width: f64) -> Self {
self.info_outline_width = Some(serde_json::json!(info_outline_width));
self
}
/// Sets the relative size of info text modifier fields (percentage of symbol size).
pub fn info_size(mut self, info_size: f64) -> Self {
self.info_size = Some(info_size);
self
}
/// Sets the monochrome colour override (CSS colour string). When set, the entire symbol
/// is rendered in this single colour.
pub fn mono_color(mut self, mono_color: impl Into<String>) -> Self {
self.mono_color = Some(mono_color.into());
self
}
/// Sets the symbol outline colour by mode name.
pub fn outline_color_str(mut self, name: impl Into<String>) -> Self {
self.outline_color = Some(serde_json::Value::String(name.into()));
self
}
/// Sets the symbol outline colour from a [`ColorMode`][crate::types::ColorMode] object.
pub fn outline_color_obj(mut self, mode: &crate::types::ColorMode) -> Self {
self.outline_color =
Some(serde_json::to_value(mode).expect("ColorMode is always serialisable"));
self
}
/// Sets the symbol outline colour from a raw JSON value (string or object).
pub fn outline_color(mut self, value: serde_json::Value) -> Self {
self.outline_color = Some(value);
self
}
/// Sets the symbol outline stroke width in pixels (`0` disables it).
pub fn outline_width(mut self, outline_width: f64) -> Self {
self.outline_width = Some(outline_width);
self
}
/// Sets the extra padding around the symbol bounding box in pixels.
pub fn padding(mut self, padding: f64) -> Self {
self.padding = Some(padding);
self
}
/// Sets whether simple (non-standard) status modifiers are used.
pub fn simple_status_modifier(mut self, simple_status_modifier: bool) -> Self {
self.simple_status_modifier = Some(simple_status_modifier);
self
}
/// Sets the symbol size (the `L` variable in MIL-STD geometry). Defaults to `100`.
pub fn size(mut self, size: f64) -> Self {
self.size = Some(size);
self
}
/// Sets whether the symbol bounding box is forced to a square.
pub fn square(mut self, square: bool) -> Self {
self.square = Some(square);
self
}
/// Sets the standard override (`"2525"` or `"APP6"`).
pub fn standard(mut self, standard: impl Into<String>) -> Self {
self.standard = Some(standard.into());
self
}
/// Sets the symbol frame stroke width in pixels.
pub fn stroke_width(mut self, stroke_width: f64) -> Self {
self.stroke_width = Some(stroke_width);
self
}
// --- Text modifier options ---
/// Sets the quantity option (FieldID C).
pub fn quantity(mut self, quantity: impl Into<String>) -> Self {
self.quantity = Some(quantity.into());
self
}
/// Sets the reinforced/reduced indicator (FieldID F).
pub fn reinforced_reduced(mut self, reinforced_reduced: impl Into<String>) -> Self {
self.reinforced_reduced = Some(reinforced_reduced.into());
self
}
/// Sets the staff comments modifier (FieldID G).
pub fn staff_comments(mut self, staff_comments: impl Into<String>) -> Self {
self.staff_comments = Some(staff_comments.into());
self
}
/// Sets the additional information modifier (FieldID H).
pub fn additional_information(mut self, additional_information: impl Into<String>) -> Self {
self.additional_information = Some(additional_information.into());
self
}
/// Sets the evaluation rating modifier (FieldID J).
pub fn evaluation_rating(mut self, evaluation_rating: impl Into<String>) -> Self {
self.evaluation_rating = Some(evaluation_rating.into());
self
}
/// Sets the combat effectiveness modifier (FieldID K).
pub fn combat_effectiveness(mut self, combat_effectiveness: impl Into<String>) -> Self {
self.combat_effectiveness = Some(combat_effectiveness.into());
self
}
/// Sets the signature equipment modifier (FieldID L).
pub fn signature_equipment(mut self, signature_equipment: impl Into<String>) -> Self {
self.signature_equipment = Some(signature_equipment.into());
self
}
/// Sets the higher formation modifier (FieldID M).
pub fn higher_formation(mut self, higher_formation: impl Into<String>) -> Self {
self.higher_formation = Some(higher_formation.into());
self
}
/// Sets the hostile modifier (FieldID N).
pub fn hostile(mut self, hostile: impl Into<String>) -> Self {
self.hostile = Some(hostile.into());
self
}
/// Sets the IFF/SIF modifier (FieldID P).
pub fn iff_sif(mut self, iff_sif: impl Into<String>) -> Self {
self.iff_sif = Some(iff_sif.into());
self
}
/// Sets the direction of movement or pointing in degrees (FieldID Q).
pub fn direction(mut self, direction: f64) -> Self {
self.direction = Some(direction);
self
}
/// Sets the SIGINT modifier (FieldID R2).
pub fn sigint(mut self, sigint: impl Into<String>) -> Self {
self.sigint = Some(sigint.into());
self
}
/// Sets the unique designation modifier (FieldID T).
pub fn unique_designation(mut self, unique_designation: impl Into<String>) -> Self {
self.unique_designation = Some(unique_designation.into());
self
}
/// Sets the type-of-equipment-or-unit modifier (FieldID V).
pub fn r#type(mut self, r#type: impl Into<String>) -> Self {
self.r#type = Some(r#type.into());
self
}
/// Sets the Date-Time Group modifier (FieldID W).
pub fn dtg(mut self, dtg: impl Into<String>) -> Self {
self.dtg = Some(dtg.into());
self
}
/// Sets the altitude/depth modifier (FieldID X).
pub fn altitude_depth(mut self, altitude_depth: impl Into<String>) -> Self {
self.altitude_depth = Some(altitude_depth.into());
self
}
/// Sets the location modifier (FieldID Y).
pub fn location(mut self, location: impl Into<String>) -> Self {
self.location = Some(location.into());
self
}
/// Sets the speed modifier (FieldID Z).
pub fn speed(mut self, speed: impl Into<String>) -> Self {
self.speed = Some(speed.into());
self
}
/// Sets the speed leader line length as a multiplier of the symbol size.
pub fn speed_leader(mut self, speed_leader: f64) -> Self {
self.speed_leader = Some(speed_leader);
self
}
/// Sets the special headquarters modifier (FieldID AA).
pub fn special_headquarters(mut self, special_headquarters: impl Into<String>) -> Self {
self.special_headquarters = Some(special_headquarters.into());
self
}
/// Sets the country code modifier (FieldID AC).
pub fn country(mut self, country: impl Into<String>) -> Self {
self.country = Some(country.into());
self
}
/// Sets the platform type modifier (FieldID AD).
pub fn platform_type(mut self, platform_type: impl Into<String>) -> Self {
self.platform_type = Some(platform_type.into());
self
}
/// Sets the equipment teardown time modifier (FieldID AE).
pub fn equipment_teardown_time(mut self, equipment_teardown_time: impl Into<String>) -> Self {
self.equipment_teardown_time = Some(equipment_teardown_time.into());
self
}
/// Sets the common identifier modifier (FieldID AF).
pub fn common_identifier(mut self, common_identifier: impl Into<String>) -> Self {
self.common_identifier = Some(common_identifier.into());
self
}
/// Sets the auxiliary equipment indicator modifier (FieldID AG).
pub fn auxiliary_equipment_indicator(
mut self,
auxiliary_equipment_indicator: impl Into<String>,
) -> Self {
self.auxiliary_equipment_indicator = Some(auxiliary_equipment_indicator.into());
self
}
/// Sets the headquarters element modifier (FieldID AH).
pub fn headquarters_element(mut self, headquarters_element: impl Into<String>) -> Self {
self.headquarters_element = Some(headquarters_element.into());
self
}
/// Sets the installation composition modifier (FieldID AI).
pub fn installation_composition(mut self, installation_composition: impl Into<String>) -> Self {
self.installation_composition = Some(installation_composition.into());
self
}
/// Sets the engagement bar modifier (FieldID AO).
pub fn engagement_bar(mut self, engagement_bar: impl Into<String>) -> Self {
self.engagement_bar = Some(engagement_bar.into());
self
}
/// Sets the engagement bar type (`"TARGET"`, `"NON-TARGET"`, or `"EXPIRED"`).
pub fn engagement_type(mut self, engagement_type: impl Into<String>) -> Self {
self.engagement_type = Some(engagement_type.into());
self
}
/// Sets the guarded unit modifier (FieldID AQ).
pub fn guarded_unit(mut self, guarded_unit: impl Into<String>) -> Self {
self.guarded_unit = Some(guarded_unit.into());
self
}
/// Sets the special designator modifier (FieldID AR).
pub fn special_designator(mut self, special_designator: impl Into<String>) -> Self {
self.special_designator = Some(special_designator.into());
self
}
/// Sets the stack index (0-based) for stacked symbol sets.
pub fn stack(mut self, stack: f64) -> Self {
self.stack = Some(stack);
self
}
// --- Escape hatch ---
/// Inserts an arbitrary option not explicitly typed in this struct.
///
/// The key is used verbatim in JSON — no camelCase conversion is applied.
pub fn extra(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.extra.insert(key.into(), value);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_milsymbol_options_builder() {
let options = MilsymbolOptions::new()
.size(50.0)
.quantity("5")
.reinforced_reduced("-")
.staff_comments("Test comment")
.additional_information("More info")
.evaluation_rating("A1")
.combat_effectiveness("Effective")
.signature_equipment("Radar")
.higher_formation("1st Division")
.hostile("Enemy")
.iff_sif("Mode 4")
.direction(180.0)
.sigint("SIGINT-1")
.unique_designation("Alpha-1")
.r#type("Tank")
.dtg("2024-03-20")
.altitude_depth("100m")
.location("NY")
.speed("Fast")
.speed_leader(10.0)
.special_headquarters("HQ")
.country("USA")
.platform_type("Tracked")
.equipment_teardown_time("5m")
.common_identifier("T-72")
.auxiliary_equipment_indicator("N/A")
.headquarters_element("HQ-1")
.installation_composition("Mixed")
.engagement_bar("Target")
.engagement_type("EXPIRED")
.guarded_unit("Unit-1")
.special_designator("Elite")
.extra("custom", json!(true));
assert_eq!(options.size, Some(50.0));
assert_eq!(options.quantity, Some("5".to_string()));
assert_eq!(options.direction, Some(180.0));
assert_eq!(options.extra.get("custom").unwrap(), &json!(true));
let json = serde_json::to_value(&options).unwrap();
assert_eq!(json["size"], 50.0);
assert_eq!(json["quantity"], "5");
assert_eq!(json["custom"], true);
assert_eq!(json["engagementType"], "EXPIRED");
}
#[test]
fn test_milsymbol_options_extra_determinism() {
let options1 = MilsymbolOptions::new()
.extra("a", json!(1))
.extra("b", json!(2));
let options2 = MilsymbolOptions::new()
.extra("b", json!(2))
.extra("a", json!(1));
let json1 = serde_json::to_string(&options1).unwrap();
let json2 = serde_json::to_string(&options2).unwrap();
assert_eq!(
json1, json2,
"Serialization of extra options should be deterministic"
);
}
/// Verify the new style/rendering options round-trip correctly through serde.
#[test]
fn test_style_options_serialization() {
let opts = MilsymbolOptions::new()
.fill(false)
.frame(true)
.icon(false)
.stroke_width(6.0)
.fill_opacity(0.5)
.mono_color("#ff0000")
.fontfamily("Helvetica")
.info_size(30.0)
.info_outline_width(2.0)
.info_outline_color("rgb(0,0,0)")
.padding(10.0)
.square(true)
.simple_status_modifier(true)
.hq_staff_length(150.0)
.standard("APP6")
.info_fields(false)
.stack(1.0);
let json = serde_json::to_value(&opts).unwrap();
assert_eq!(json["fill"], false);
assert_eq!(json["frame"], true);
assert_eq!(json["icon"], false);
assert_eq!(json["strokeWidth"], 6.0);
assert_eq!(json["fillOpacity"], 0.5);
assert_eq!(json["monoColor"], "#ff0000");
assert_eq!(json["fontfamily"], "Helvetica");
assert_eq!(json["infoSize"], 30.0);
assert_eq!(json["infoOutlineWidth"], 2.0);
assert_eq!(json["infoOutlineColor"], "rgb(0,0,0)");
assert_eq!(json["padding"], 10.0);
assert_eq!(json["square"], true);
assert_eq!(json["simpleStatusModifier"], true);
assert_eq!(json["hqStaffLength"], 150.0);
assert_eq!(json["standard"], "APP6");
assert_eq!(json["infoFields"], false);
assert_eq!(json["stack"], 1.0);
}
/// Verify that unset Option fields are absent from JSON (JS defaults must not be overridden).
#[test]
fn test_unset_options_absent_from_json() {
let opts = MilsymbolOptions::new().size(100.0);
let json = serde_json::to_value(&opts).unwrap();
assert!(json.get("fill").is_none(), "unset 'fill' must not appear in JSON");
assert!(json.get("frame").is_none(), "unset 'frame' must not appear in JSON");
assert!(json.get("colorMode").is_none(), "unset 'colorMode' must not appear in JSON");
assert!(json.get("monoColor").is_none(), "unset 'monoColor' must not appear in JSON");
assert!(json.get("strokeWidth").is_none(), "unset 'strokeWidth' must not appear in JSON");
assert_eq!(json["size"], 100.0);
}
/// Verify that color_mode_str produces a JSON string, and color_mode_obj a JSON object.
#[test]
fn test_color_mode_variants() {
// String form
let opts_str = MilsymbolOptions::new().color_mode_str("Dark");
let json_str = serde_json::to_value(&opts_str).unwrap();
assert_eq!(json_str["colorMode"], "Dark");
// Object form via a typed ColorMode
let mode = crate::types::ColorMode::new(
"#800080", "#00ff00", "#ff0000", "#ffff00", "#ffffff", "#ff4500",
)
.unwrap();
let opts_obj = MilsymbolOptions::new().color_mode_obj(&mode);
let json_obj = serde_json::to_value(&opts_obj).unwrap();
assert!(
json_obj["colorMode"].is_object(),
"color_mode_obj should produce a JSON object, got: {:?}",
json_obj["colorMode"]
);
assert_eq!(json_obj["colorMode"]["Friend"], "#00ff00");
}
}