Skip to main content

hwpforge_foundation/enums/
shape.rs

1//! Shape geometry enums: flip, arc, arrow, and curve-segment types.
2
3use crate::error::FoundationError;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7// ---------------------------------------------------------------------------
8// Flip
9// ---------------------------------------------------------------------------
10
11/// Flip/mirror state for drawing shapes.
12///
13/// Controls horizontal and/or vertical mirroring of a shape.
14///
15/// # Examples
16///
17/// ```
18/// use hwpforge_foundation::Flip;
19///
20/// assert_eq!(Flip::default(), Flip::None);
21/// assert_eq!(Flip::Horizontal.to_string(), "Horizontal");
22/// ```
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
24#[non_exhaustive]
25#[repr(u8)]
26pub enum Flip {
27    /// No flip (default).
28    #[default]
29    None = 0,
30    /// Mirrored horizontally.
31    Horizontal = 1,
32    /// Mirrored vertically.
33    Vertical = 2,
34    /// Mirrored both horizontally and vertically.
35    Both = 3,
36}
37
38impl fmt::Display for Flip {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            Self::None => f.write_str("None"),
42            Self::Horizontal => f.write_str("Horizontal"),
43            Self::Vertical => f.write_str("Vertical"),
44            Self::Both => f.write_str("Both"),
45        }
46    }
47}
48
49impl std::str::FromStr for Flip {
50    type Err = FoundationError;
51
52    fn from_str(s: &str) -> Result<Self, Self::Err> {
53        match s {
54            "None" | "NONE" | "none" => Ok(Self::None),
55            "Horizontal" | "HORIZONTAL" | "horizontal" => Ok(Self::Horizontal),
56            "Vertical" | "VERTICAL" | "vertical" => Ok(Self::Vertical),
57            "Both" | "BOTH" | "both" => Ok(Self::Both),
58            _ => Err(FoundationError::ParseError {
59                type_name: "Flip".to_string(),
60                value: s.to_string(),
61                valid_values: "None, Horizontal, Vertical, Both".to_string(),
62            }),
63        }
64    }
65}
66
67impl TryFrom<u8> for Flip {
68    type Error = FoundationError;
69
70    fn try_from(value: u8) -> Result<Self, Self::Error> {
71        match value {
72            0 => Ok(Self::None),
73            1 => Ok(Self::Horizontal),
74            2 => Ok(Self::Vertical),
75            3 => Ok(Self::Both),
76            _ => Err(FoundationError::ParseError {
77                type_name: "Flip".to_string(),
78                value: value.to_string(),
79                valid_values: "0 (None), 1 (Horizontal), 2 (Vertical), 3 (Both)".to_string(),
80            }),
81        }
82    }
83}
84
85impl schemars::JsonSchema for Flip {
86    fn schema_name() -> std::borrow::Cow<'static, str> {
87        std::borrow::Cow::Borrowed("Flip")
88    }
89
90    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
91        gen.subschema_for::<String>()
92    }
93}
94
95// ---------------------------------------------------------------------------
96// ArcType
97// ---------------------------------------------------------------------------
98
99/// Arc drawing type for ellipse-based arc shapes.
100///
101/// # Examples
102///
103/// ```
104/// use hwpforge_foundation::ArcType;
105///
106/// assert_eq!(ArcType::default(), ArcType::Normal);
107/// ```
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
109#[non_exhaustive]
110#[repr(u8)]
111pub enum ArcType {
112    /// Open arc (just the curved edge).
113    #[default]
114    Normal = 0,
115    /// Pie/sector (arc + two radii closing to center).
116    Pie = 1,
117    /// Chord (arc + straight line closing endpoints).
118    Chord = 2,
119}
120
121impl fmt::Display for ArcType {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        match self {
124            Self::Normal => f.write_str("NORMAL"),
125            Self::Pie => f.write_str("PIE"),
126            Self::Chord => f.write_str("CHORD"),
127        }
128    }
129}
130
131impl std::str::FromStr for ArcType {
132    type Err = FoundationError;
133
134    fn from_str(s: &str) -> Result<Self, Self::Err> {
135        match s {
136            "NORMAL" | "Normal" | "normal" => Ok(Self::Normal),
137            "PIE" | "Pie" | "pie" => Ok(Self::Pie),
138            "CHORD" | "Chord" | "chord" => Ok(Self::Chord),
139            _ => Err(FoundationError::ParseError {
140                type_name: "ArcType".to_string(),
141                value: s.to_string(),
142                valid_values: "NORMAL, PIE, CHORD".to_string(),
143            }),
144        }
145    }
146}
147
148impl TryFrom<u8> for ArcType {
149    type Error = FoundationError;
150
151    fn try_from(value: u8) -> Result<Self, Self::Error> {
152        match value {
153            0 => Ok(Self::Normal),
154            1 => Ok(Self::Pie),
155            2 => Ok(Self::Chord),
156            _ => Err(FoundationError::ParseError {
157                type_name: "ArcType".to_string(),
158                value: value.to_string(),
159                valid_values: "0 (Normal), 1 (Pie), 2 (Chord)".to_string(),
160            }),
161        }
162    }
163}
164
165impl schemars::JsonSchema for ArcType {
166    fn schema_name() -> std::borrow::Cow<'static, str> {
167        std::borrow::Cow::Borrowed("ArcType")
168    }
169
170    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
171        gen.subschema_for::<String>()
172    }
173}
174
175// ---------------------------------------------------------------------------
176// ArrowType
177// ---------------------------------------------------------------------------
178
179/// Arrowhead shape for line endpoints.
180///
181/// # Examples
182///
183/// ```
184/// use hwpforge_foundation::ArrowType;
185///
186/// assert_eq!(ArrowType::default(), ArrowType::None);
187/// ```
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
189#[non_exhaustive]
190#[repr(u8)]
191pub enum ArrowType {
192    /// No arrowhead (default).
193    #[default]
194    None = 0,
195    /// Standard filled arrowhead.
196    Normal = 1,
197    /// Arrow-shaped arrowhead.
198    Arrow = 2,
199    /// Concave arrowhead.
200    Concave = 3,
201    /// Diamond arrowhead.
202    Diamond = 4,
203    /// Oval/circle arrowhead.
204    Oval = 5,
205    /// Open (unfilled) arrowhead.
206    Open = 6,
207}
208
209impl fmt::Display for ArrowType {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        // KS X 6101 ArrowType values.
212        // Diamond/Oval/Open default to FILLED_ variants here;
213        // the encoder resolves FILLED_ vs EMPTY_ based on ArrowStyle.filled.
214        match self {
215            Self::None => f.write_str("NORMAL"),
216            Self::Normal => f.write_str("ARROW"),
217            Self::Arrow => f.write_str("SPEAR"),
218            Self::Concave => f.write_str("CONCAVE_ARROW"),
219            Self::Diamond => f.write_str("FILLED_DIAMOND"),
220            Self::Oval => f.write_str("FILLED_CIRCLE"),
221            Self::Open => f.write_str("EMPTY_BOX"),
222        }
223    }
224}
225
226impl std::str::FromStr for ArrowType {
227    type Err = FoundationError;
228
229    fn from_str(s: &str) -> Result<Self, Self::Err> {
230        // KS X 6101 ArrowType values (primary) + legacy aliases for backward compat.
231        match s {
232            "NORMAL" => Ok(Self::None),
233            "ARROW" => Ok(Self::Normal),
234            "SPEAR" => Ok(Self::Arrow),
235            "CONCAVE_ARROW" => Ok(Self::Concave),
236            "FILLED_DIAMOND" | "EMPTY_DIAMOND" => Ok(Self::Diamond),
237            "FILLED_CIRCLE" | "EMPTY_CIRCLE" => Ok(Self::Oval),
238            "FILLED_BOX" | "EMPTY_BOX" => Ok(Self::Open),
239            _ => Err(FoundationError::ParseError {
240                type_name: "ArrowType".to_string(),
241                value: s.to_string(),
242                valid_values: "NORMAL, ARROW, SPEAR, CONCAVE_ARROW, FILLED_DIAMOND, EMPTY_DIAMOND, FILLED_CIRCLE, EMPTY_CIRCLE, FILLED_BOX, EMPTY_BOX"
243                    .to_string(),
244            }),
245        }
246    }
247}
248
249impl TryFrom<u8> for ArrowType {
250    type Error = FoundationError;
251
252    fn try_from(value: u8) -> Result<Self, Self::Error> {
253        match value {
254            0 => Ok(Self::None),
255            1 => Ok(Self::Normal),
256            2 => Ok(Self::Arrow),
257            3 => Ok(Self::Concave),
258            4 => Ok(Self::Diamond),
259            5 => Ok(Self::Oval),
260            6 => Ok(Self::Open),
261            _ => Err(FoundationError::ParseError {
262                type_name: "ArrowType".to_string(),
263                value: value.to_string(),
264                valid_values:
265                    "0 (None), 1 (Normal), 2 (Arrow), 3 (Concave), 4 (Diamond), 5 (Oval), 6 (Open)"
266                        .to_string(),
267            }),
268        }
269    }
270}
271
272impl schemars::JsonSchema for ArrowType {
273    fn schema_name() -> std::borrow::Cow<'static, str> {
274        std::borrow::Cow::Borrowed("ArrowType")
275    }
276
277    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
278        gen.subschema_for::<String>()
279    }
280}
281
282// ---------------------------------------------------------------------------
283// ArrowSize
284// ---------------------------------------------------------------------------
285
286/// Arrowhead size for line endpoints.
287///
288/// Encoded as `{HEAD}_{TAIL}` string in HWPX (e.g. `"MEDIUM_MEDIUM"`).
289///
290/// # Examples
291///
292/// ```
293/// use hwpforge_foundation::ArrowSize;
294///
295/// assert_eq!(ArrowSize::default(), ArrowSize::Medium);
296/// ```
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
298#[non_exhaustive]
299#[repr(u8)]
300pub enum ArrowSize {
301    /// Small arrowhead.
302    Small = 0,
303    /// Medium arrowhead (default).
304    #[default]
305    Medium = 1,
306    /// Large arrowhead.
307    Large = 2,
308}
309
310impl fmt::Display for ArrowSize {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        match self {
313            Self::Small => f.write_str("SMALL_SMALL"),
314            Self::Medium => f.write_str("MEDIUM_MEDIUM"),
315            Self::Large => f.write_str("LARGE_LARGE"),
316        }
317    }
318}
319
320impl std::str::FromStr for ArrowSize {
321    type Err = FoundationError;
322
323    fn from_str(s: &str) -> Result<Self, Self::Err> {
324        match s {
325            "SMALL_SMALL" | "Small" | "small" => Ok(Self::Small),
326            "MEDIUM_MEDIUM" | "Medium" | "medium" => Ok(Self::Medium),
327            "LARGE_LARGE" | "Large" | "large" => Ok(Self::Large),
328            _ => Err(FoundationError::ParseError {
329                type_name: "ArrowSize".to_string(),
330                value: s.to_string(),
331                valid_values: "SMALL_SMALL, MEDIUM_MEDIUM, LARGE_LARGE".to_string(),
332            }),
333        }
334    }
335}
336
337impl TryFrom<u8> for ArrowSize {
338    type Error = FoundationError;
339
340    fn try_from(value: u8) -> Result<Self, Self::Error> {
341        match value {
342            0 => Ok(Self::Small),
343            1 => Ok(Self::Medium),
344            2 => Ok(Self::Large),
345            _ => Err(FoundationError::ParseError {
346                type_name: "ArrowSize".to_string(),
347                value: value.to_string(),
348                valid_values: "0 (Small), 1 (Medium), 2 (Large)".to_string(),
349            }),
350        }
351    }
352}
353
354impl schemars::JsonSchema for ArrowSize {
355    fn schema_name() -> std::borrow::Cow<'static, str> {
356        std::borrow::Cow::Borrowed("ArrowSize")
357    }
358
359    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
360        gen.subschema_for::<String>()
361    }
362}
363
364// ---------------------------------------------------------------------------
365// CurveSegmentType
366// ---------------------------------------------------------------------------
367
368/// Segment type within a curve path.
369///
370/// # Examples
371///
372/// ```
373/// use hwpforge_foundation::CurveSegmentType;
374///
375/// assert_eq!(CurveSegmentType::default(), CurveSegmentType::Line);
376/// ```
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
378#[non_exhaustive]
379#[repr(u8)]
380pub enum CurveSegmentType {
381    /// Straight line segment (default).
382    #[default]
383    Line = 0,
384    /// Cubic bezier curve segment.
385    Curve = 1,
386}
387
388impl fmt::Display for CurveSegmentType {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        match self {
391            Self::Line => f.write_str("LINE"),
392            Self::Curve => f.write_str("CURVE"),
393        }
394    }
395}
396
397impl std::str::FromStr for CurveSegmentType {
398    type Err = FoundationError;
399
400    fn from_str(s: &str) -> Result<Self, Self::Err> {
401        match s {
402            "LINE" | "Line" | "line" => Ok(Self::Line),
403            "CURVE" | "Curve" | "curve" => Ok(Self::Curve),
404            _ => Err(FoundationError::ParseError {
405                type_name: "CurveSegmentType".to_string(),
406                value: s.to_string(),
407                valid_values: "LINE, CURVE".to_string(),
408            }),
409        }
410    }
411}
412
413impl TryFrom<u8> for CurveSegmentType {
414    type Error = FoundationError;
415
416    fn try_from(value: u8) -> Result<Self, Self::Error> {
417        match value {
418            0 => Ok(Self::Line),
419            1 => Ok(Self::Curve),
420            _ => Err(FoundationError::ParseError {
421                type_name: "CurveSegmentType".to_string(),
422                value: value.to_string(),
423                valid_values: "0 (Line), 1 (Curve)".to_string(),
424            }),
425        }
426    }
427}
428
429impl schemars::JsonSchema for CurveSegmentType {
430    fn schema_name() -> std::borrow::Cow<'static, str> {
431        std::borrow::Cow::Borrowed("CurveSegmentType")
432    }
433
434    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
435        gen.subschema_for::<String>()
436    }
437}
438
439// ---------------------------------------------------------------------------
440// VerticalAlign
441// ---------------------------------------------------------------------------
442
443/// Vertical alignment of text inside a drawing shape (글상자/타원/다각형).
444///
445/// Maps to the HWPX `<hp:drawText><hp:subList vertAlign="...">` attribute and
446/// the HWP5 문단 리스트 헤더 (`HWPTAG_LIST_HEADER`) 속성 bits 5–6 (표 65):
447/// `0 = top`, `1 = center`, `2 = bottom`. The default is [`VerticalAlign::Top`],
448/// matching 한컴's default rendering (text pinned to the shape's top edge).
449///
450/// This is a neutral enum shared by shape text. Table cells keep their own
451/// [`crate`]-external `TableVerticalAlign` for now; unifying the two is a
452/// follow-up concern (see ADR-008).
453///
454/// `Display`/`FromStr` use the HWPX wire tokens (`TOP`/`CENTER`/`BOTTOM`).
455///
456/// # Examples
457///
458/// ```
459/// use hwpforge_foundation::VerticalAlign;
460/// use std::str::FromStr;
461///
462/// assert_eq!(VerticalAlign::default(), VerticalAlign::Top);
463/// assert_eq!(VerticalAlign::Center.to_string(), "CENTER");
464/// assert_eq!(VerticalAlign::from_str("BOTTOM").unwrap(), VerticalAlign::Bottom);
465/// ```
466#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
467#[non_exhaustive]
468#[repr(u8)]
469pub enum VerticalAlign {
470    /// Align text to the top edge of the shape (default).
471    #[default]
472    Top = 0,
473    /// Center text vertically within the shape.
474    Center = 1,
475    /// Align text to the bottom edge of the shape.
476    Bottom = 2,
477}
478
479impl fmt::Display for VerticalAlign {
480    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
481        match self {
482            Self::Top => f.write_str("TOP"),
483            Self::Center => f.write_str("CENTER"),
484            Self::Bottom => f.write_str("BOTTOM"),
485        }
486    }
487}
488
489impl std::str::FromStr for VerticalAlign {
490    type Err = FoundationError;
491
492    fn from_str(s: &str) -> Result<Self, Self::Err> {
493        match s {
494            "Top" | "top" | "TOP" => Ok(Self::Top),
495            "Center" | "center" | "CENTER" => Ok(Self::Center),
496            "Bottom" | "bottom" | "BOTTOM" => Ok(Self::Bottom),
497            _ => Err(FoundationError::ParseError {
498                type_name: "VerticalAlign".to_string(),
499                value: s.to_string(),
500                valid_values: "TOP, CENTER, BOTTOM".to_string(),
501            }),
502        }
503    }
504}
505
506impl TryFrom<u8> for VerticalAlign {
507    type Error = FoundationError;
508
509    fn try_from(value: u8) -> Result<Self, Self::Error> {
510        match value {
511            0 => Ok(Self::Top),
512            1 => Ok(Self::Center),
513            2 => Ok(Self::Bottom),
514            _ => Err(FoundationError::ParseError {
515                type_name: "VerticalAlign".to_string(),
516                value: value.to_string(),
517                valid_values: "0 (Top), 1 (Center), 2 (Bottom)".to_string(),
518            }),
519        }
520    }
521}
522
523impl schemars::JsonSchema for VerticalAlign {
524    fn schema_name() -> std::borrow::Cow<'static, str> {
525        std::borrow::Cow::Borrowed("VerticalAlign")
526    }
527
528    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
529        gen.subschema_for::<String>()
530    }
531}
532
533#[cfg(test)]
534mod vertical_align_tests {
535    use super::VerticalAlign;
536    use std::str::FromStr;
537
538    #[test]
539    fn default_is_top() {
540        assert_eq!(VerticalAlign::default(), VerticalAlign::Top);
541    }
542
543    #[test]
544    fn display_uses_hwpx_tokens() {
545        assert_eq!(VerticalAlign::Top.to_string(), "TOP");
546        assert_eq!(VerticalAlign::Center.to_string(), "CENTER");
547        assert_eq!(VerticalAlign::Bottom.to_string(), "BOTTOM");
548    }
549
550    #[test]
551    fn from_str_accepts_hwpx_and_pascal_and_lower() {
552        assert_eq!(VerticalAlign::from_str("TOP").unwrap(), VerticalAlign::Top);
553        assert_eq!(VerticalAlign::from_str("Center").unwrap(), VerticalAlign::Center);
554        assert_eq!(VerticalAlign::from_str("bottom").unwrap(), VerticalAlign::Bottom);
555    }
556
557    #[test]
558    fn from_str_round_trips_display() {
559        for v in [VerticalAlign::Top, VerticalAlign::Center, VerticalAlign::Bottom] {
560            assert_eq!(VerticalAlign::from_str(&v.to_string()).unwrap(), v);
561        }
562    }
563
564    #[test]
565    fn from_str_rejects_invalid() {
566        assert!(VerticalAlign::from_str("").is_err());
567        assert!(VerticalAlign::from_str("MIDDLE").is_err());
568        assert!(VerticalAlign::from_str("baseline").is_err());
569    }
570
571    #[test]
572    fn try_from_u8_boundaries() {
573        assert_eq!(VerticalAlign::try_from(0u8).unwrap(), VerticalAlign::Top);
574        assert_eq!(VerticalAlign::try_from(1u8).unwrap(), VerticalAlign::Center);
575        assert_eq!(VerticalAlign::try_from(2u8).unwrap(), VerticalAlign::Bottom);
576        assert!(VerticalAlign::try_from(3u8).is_err());
577        assert!(VerticalAlign::try_from(u8::MAX).is_err());
578    }
579
580    #[test]
581    fn is_one_byte() {
582        assert_eq!(std::mem::size_of::<VerticalAlign>(), 1);
583    }
584}