ppt_rs/generator/
shapes.rs

1//! Shape creation support for PPTX generation
2//!
3//! Provides shape types, fills, lines, and builders for creating shapes in slides.
4
5/// Shape types available in PPTX
6#[derive(Clone, Debug, Copy, PartialEq)]
7pub enum ShapeType {
8    // Basic shapes
9    Rectangle,
10    RoundedRectangle,
11    Ellipse,
12    Circle, // Alias for Ellipse
13    Triangle,
14    RightTriangle,
15    Diamond,
16    Pentagon,
17    Hexagon,
18    Octagon,
19    
20    // Arrows
21    RightArrow,
22    LeftArrow,
23    UpArrow,
24    DownArrow,
25    LeftRightArrow,
26    UpDownArrow,
27    BentArrow,
28    UTurnArrow,
29    
30    // Stars and banners
31    Star4,
32    Star5,
33    Star6,
34    Star8,
35    Ribbon,
36    Wave,
37    
38    // Callouts
39    WedgeRectCallout,
40    WedgeEllipseCallout,
41    CloudCallout,
42    
43    // Flow chart
44    FlowChartProcess,
45    FlowChartDecision,
46    FlowChartTerminator,
47    FlowChartDocument,
48    FlowChartPredefinedProcess,
49    FlowChartInternalStorage,
50    FlowChartData,
51    FlowChartInputOutput,
52    FlowChartManualInput,
53    FlowChartManualOperation,
54    FlowChartConnector,
55    FlowChartOffPageConnector,
56    FlowChartPunchedCard,
57    FlowChartPunchedTape,
58    FlowChartSummingJunction,
59    FlowChartOr,
60    FlowChartCollate,
61    FlowChartSort,
62    FlowChartExtract,
63    FlowChartMerge,
64    FlowChartOnlineStorage,
65    FlowChartDelay,
66    FlowChartMagneticTape,
67    FlowChartMagneticDisk,
68    FlowChartMagneticDrum,
69    FlowChartDisplay,
70    FlowChartPreparation,
71    
72    // More arrows
73    CurvedRightArrow,
74    CurvedLeftArrow,
75    CurvedUpArrow,
76    CurvedDownArrow,
77    CurvedLeftRightArrow,
78    CurvedUpDownArrow,
79    StripedRightArrow,
80    NotchedRightArrow,
81    PentagonArrow,
82    ChevronArrow,
83    RightArrowCallout,
84    LeftArrowCallout,
85    UpArrowCallout,
86    DownArrowCallout,
87    LeftRightArrowCallout,
88    UpDownArrowCallout,
89    QuadArrow,
90    LeftRightUpArrow,
91    CircularArrow,
92    
93    // More geometric shapes
94    Parallelogram,
95    Trapezoid,
96    NonIsoscelesTrapezoid,
97    IsoscelesTrapezoid,
98    Cube,
99    Can,
100    Cone,
101    Cylinder,
102    Bevel,
103    Donut,
104    NoSmoking,
105    BlockArc,
106    FoldedCorner,
107    SmileyFace,
108    Arc,
109    Chord,
110    Pie,
111    Teardrop,
112    Plaque,
113    MusicNote,
114    PictureFrame,
115    
116    // More decorative
117    Star10,
118    Star12,
119    Star16,
120    Star24,
121    Star32,
122    Seal,
123    Seal4,
124    Seal8,
125    Seal16,
126    Seal32,
127    ActionButtonBlank,
128    ActionButtonHome,
129    ActionButtonHelp,
130    ActionButtonInformation,
131    ActionButtonForwardNext,
132    ActionButtonBackPrevious,
133    ActionButtonBeginning,
134    ActionButtonEnd,
135    ActionButtonReturn,
136    ActionButtonDocument,
137    ActionButtonSound,
138    ActionButtonMovie,
139    
140    // Other (original shapes)
141    Heart,
142    Lightning,
143    Sun,
144    Moon,
145    Cloud,
146    Brace,
147    Bracket,
148    Plus,
149    Minus,
150}
151
152impl ShapeType {
153    /// Get the preset geometry name for the shape (OOXML preset name)
154    pub fn preset_name(&self) -> &'static str {
155        match self {
156            ShapeType::Rectangle => "rect",
157            ShapeType::RoundedRectangle => "roundRect",
158            ShapeType::Ellipse | ShapeType::Circle => "ellipse",
159            ShapeType::Triangle => "triangle",
160            ShapeType::RightTriangle => "rtTriangle",
161            ShapeType::Diamond => "diamond",
162            ShapeType::Pentagon => "pentagon",
163            ShapeType::Hexagon => "hexagon",
164            ShapeType::Octagon => "octagon",
165            
166            ShapeType::RightArrow => "rightArrow",
167            ShapeType::LeftArrow => "leftArrow",
168            ShapeType::UpArrow => "upArrow",
169            ShapeType::DownArrow => "downArrow",
170            ShapeType::LeftRightArrow => "leftRightArrow",
171            ShapeType::UpDownArrow => "upDownArrow",
172            ShapeType::BentArrow => "bentArrow",
173            ShapeType::UTurnArrow => "uturnArrow",
174            
175            ShapeType::Star4 => "star4",
176            ShapeType::Star5 => "star5",
177            ShapeType::Star6 => "star6",
178            ShapeType::Star8 => "star8",
179            ShapeType::Ribbon => "ribbon2",
180            ShapeType::Wave => "wave",
181            
182            ShapeType::WedgeRectCallout => "wedgeRectCallout",
183            ShapeType::WedgeEllipseCallout => "wedgeEllipseCallout",
184            ShapeType::CloudCallout => "cloudCallout",
185            
186            ShapeType::FlowChartProcess => "flowChartProcess",
187            ShapeType::FlowChartDecision => "flowChartDecision",
188            ShapeType::FlowChartTerminator => "flowChartTerminator",
189            ShapeType::FlowChartDocument => "flowChartDocument",
190            ShapeType::FlowChartPredefinedProcess => "flowChartPredefinedProcess",
191            ShapeType::FlowChartInternalStorage => "flowChartInternalStorage",
192            ShapeType::FlowChartData => "flowChartData",
193            ShapeType::FlowChartInputOutput => "flowChartInputOutput",
194            ShapeType::FlowChartManualInput => "flowChartManualInput",
195            ShapeType::FlowChartManualOperation => "flowChartManualOperation",
196            ShapeType::FlowChartConnector => "flowChartConnector",
197            ShapeType::FlowChartOffPageConnector => "flowChartOffPageConnector",
198            ShapeType::FlowChartPunchedCard => "flowChartPunchedCard",
199            ShapeType::FlowChartPunchedTape => "flowChartPunchedTape",
200            ShapeType::FlowChartSummingJunction => "flowChartSummingJunction",
201            ShapeType::FlowChartOr => "flowChartOr",
202            ShapeType::FlowChartCollate => "flowChartCollate",
203            ShapeType::FlowChartSort => "flowChartSort",
204            ShapeType::FlowChartExtract => "flowChartExtract",
205            ShapeType::FlowChartMerge => "flowChartMerge",
206            ShapeType::FlowChartOnlineStorage => "flowChartOnlineStorage",
207            ShapeType::FlowChartDelay => "flowChartDelay",
208            ShapeType::FlowChartMagneticTape => "flowChartMagneticTape",
209            ShapeType::FlowChartMagneticDisk => "flowChartMagneticDisk",
210            ShapeType::FlowChartMagneticDrum => "flowChartMagneticDrum",
211            ShapeType::FlowChartDisplay => "flowChartDisplay",
212            ShapeType::FlowChartPreparation => "flowChartPreparation",
213            
214            ShapeType::CurvedRightArrow => "curvedRightArrow",
215            ShapeType::CurvedLeftArrow => "curvedLeftArrow",
216            ShapeType::CurvedUpArrow => "curvedUpArrow",
217            ShapeType::CurvedDownArrow => "curvedDownArrow",
218            ShapeType::CurvedLeftRightArrow => "curvedLeftRightArrow",
219            ShapeType::CurvedUpDownArrow => "curvedUpDownArrow",
220            ShapeType::StripedRightArrow => "stripedRightArrow",
221            ShapeType::NotchedRightArrow => "notchedRightArrow",
222            ShapeType::PentagonArrow => "pentArrow",
223            ShapeType::ChevronArrow => "chevron",
224            ShapeType::RightArrowCallout => "rightArrowCallout",
225            ShapeType::LeftArrowCallout => "leftArrowCallout",
226            ShapeType::UpArrowCallout => "upArrowCallout",
227            ShapeType::DownArrowCallout => "downArrowCallout",
228            ShapeType::LeftRightArrowCallout => "leftRightArrowCallout",
229            ShapeType::UpDownArrowCallout => "upDownArrowCallout",
230            ShapeType::QuadArrow => "quadArrow",
231            ShapeType::LeftRightUpArrow => "leftRightUpArrow",
232            ShapeType::CircularArrow => "circularArrow",
233            
234            ShapeType::Parallelogram => "parallelogram",
235            ShapeType::Trapezoid => "trapezoid",
236            ShapeType::NonIsoscelesTrapezoid => "nonIsoscelesTrapezoid",
237            ShapeType::IsoscelesTrapezoid => "isoTrapezoid",
238            ShapeType::Cube => "cube",
239            ShapeType::Can => "can",
240            ShapeType::Cone => "cone",
241            ShapeType::Cylinder => "cylinder",
242            ShapeType::Bevel => "bevel",
243            ShapeType::Donut => "donut",
244            ShapeType::NoSmoking => "noSmoking",
245            ShapeType::BlockArc => "blockArc",
246            ShapeType::FoldedCorner => "foldedCorner",
247            ShapeType::SmileyFace => "smileyFace",
248            ShapeType::Arc => "arc",
249            ShapeType::Chord => "chord",
250            ShapeType::Pie => "pie",
251            ShapeType::Teardrop => "teardrop",
252            ShapeType::Plaque => "plaque",
253            ShapeType::MusicNote => "musicNote",
254            ShapeType::PictureFrame => "frame",
255            
256            ShapeType::Star10 => "star10",
257            ShapeType::Star12 => "star12",
258            ShapeType::Star16 => "star16",
259            ShapeType::Star24 => "star24",
260            ShapeType::Star32 => "star32",
261            ShapeType::Seal => "seal",
262            ShapeType::Seal4 => "seal4",
263            ShapeType::Seal8 => "seal8",
264            ShapeType::Seal16 => "seal16",
265            ShapeType::Seal32 => "seal32",
266            ShapeType::ActionButtonBlank => "actionButtonBlank",
267            ShapeType::ActionButtonHome => "actionButtonHome",
268            ShapeType::ActionButtonHelp => "actionButtonHelp",
269            ShapeType::ActionButtonInformation => "actionButtonInformation",
270            ShapeType::ActionButtonForwardNext => "actionButtonForwardNext",
271            ShapeType::ActionButtonBackPrevious => "actionButtonBackPrevious",
272            ShapeType::ActionButtonBeginning => "actionButtonBeginning",
273            ShapeType::ActionButtonEnd => "actionButtonEnd",
274            ShapeType::ActionButtonReturn => "actionButtonReturn",
275            ShapeType::ActionButtonDocument => "actionButtonDocument",
276            ShapeType::ActionButtonSound => "actionButtonSound",
277            ShapeType::ActionButtonMovie => "actionButtonMovie",
278            
279            ShapeType::Heart => "heart",
280            ShapeType::Lightning => "lightningBolt",
281            ShapeType::Sun => "sun",
282            ShapeType::Moon => "moon",
283            ShapeType::Cloud => "cloud",
284            ShapeType::Brace => "leftBrace",
285            ShapeType::Bracket => "leftBracket",
286            ShapeType::Plus => "mathPlus",
287            ShapeType::Minus => "mathMinus",
288        }
289    }
290
291    /// Get a user-friendly name for the shape
292    pub fn display_name(&self) -> &'static str {
293        match self {
294            ShapeType::Rectangle => "Rectangle",
295            ShapeType::RoundedRectangle => "Rounded Rectangle",
296            ShapeType::Ellipse => "Ellipse",
297            ShapeType::Circle => "Circle",
298            ShapeType::Triangle => "Triangle",
299            ShapeType::RightTriangle => "Right Triangle",
300            ShapeType::Diamond => "Diamond",
301            ShapeType::Pentagon => "Pentagon",
302            ShapeType::Hexagon => "Hexagon",
303            ShapeType::Octagon => "Octagon",
304            ShapeType::RightArrow => "Right Arrow",
305            ShapeType::LeftArrow => "Left Arrow",
306            ShapeType::UpArrow => "Up Arrow",
307            ShapeType::DownArrow => "Down Arrow",
308            ShapeType::LeftRightArrow => "Left-Right Arrow",
309            ShapeType::UpDownArrow => "Up-Down Arrow",
310            ShapeType::BentArrow => "Bent Arrow",
311            ShapeType::UTurnArrow => "U-Turn Arrow",
312            ShapeType::Star4 => "4-Point Star",
313            ShapeType::Star5 => "5-Point Star",
314            ShapeType::Star6 => "6-Point Star",
315            ShapeType::Star8 => "8-Point Star",
316            ShapeType::Ribbon => "Ribbon",
317            ShapeType::Wave => "Wave",
318            ShapeType::WedgeRectCallout => "Rectangle Callout",
319            ShapeType::WedgeEllipseCallout => "Oval Callout",
320            ShapeType::CloudCallout => "Cloud Callout",
321            ShapeType::FlowChartProcess => "Process",
322            ShapeType::FlowChartDecision => "Decision",
323            ShapeType::FlowChartTerminator => "Terminator",
324            ShapeType::FlowChartDocument => "Document",
325            ShapeType::FlowChartPredefinedProcess => "Predefined Process",
326            ShapeType::FlowChartInternalStorage => "Internal Storage",
327            ShapeType::FlowChartData => "Data",
328            ShapeType::FlowChartInputOutput => "Input/Output",
329            ShapeType::FlowChartManualInput => "Manual Input",
330            ShapeType::FlowChartManualOperation => "Manual Operation",
331            ShapeType::FlowChartConnector => "Connector",
332            ShapeType::FlowChartOffPageConnector => "Off-page Connector",
333            ShapeType::FlowChartPunchedCard => "Punched Card",
334            ShapeType::FlowChartPunchedTape => "Punched Tape",
335            ShapeType::FlowChartSummingJunction => "Summing Junction",
336            ShapeType::FlowChartOr => "Or",
337            ShapeType::FlowChartCollate => "Collate",
338            ShapeType::FlowChartSort => "Sort",
339            ShapeType::FlowChartExtract => "Extract",
340            ShapeType::FlowChartMerge => "Merge",
341            ShapeType::FlowChartOnlineStorage => "Online Storage",
342            ShapeType::FlowChartDelay => "Delay",
343            ShapeType::FlowChartMagneticTape => "Magnetic Tape",
344            ShapeType::FlowChartMagneticDisk => "Magnetic Disk",
345            ShapeType::FlowChartMagneticDrum => "Magnetic Drum",
346            ShapeType::FlowChartDisplay => "Display",
347            ShapeType::FlowChartPreparation => "Preparation",
348            
349            ShapeType::CurvedRightArrow => "Curved Right Arrow",
350            ShapeType::CurvedLeftArrow => "Curved Left Arrow",
351            ShapeType::CurvedUpArrow => "Curved Up Arrow",
352            ShapeType::CurvedDownArrow => "Curved Down Arrow",
353            ShapeType::CurvedLeftRightArrow => "Curved Left-Right Arrow",
354            ShapeType::CurvedUpDownArrow => "Curved Up-Down Arrow",
355            ShapeType::StripedRightArrow => "Striped Right Arrow",
356            ShapeType::NotchedRightArrow => "Notched Right Arrow",
357            ShapeType::PentagonArrow => "Pentagon Arrow",
358            ShapeType::ChevronArrow => "Chevron Arrow",
359            ShapeType::RightArrowCallout => "Right Arrow Callout",
360            ShapeType::LeftArrowCallout => "Left Arrow Callout",
361            ShapeType::UpArrowCallout => "Up Arrow Callout",
362            ShapeType::DownArrowCallout => "Down Arrow Callout",
363            ShapeType::LeftRightArrowCallout => "Left-Right Arrow Callout",
364            ShapeType::UpDownArrowCallout => "Up-Down Arrow Callout",
365            ShapeType::QuadArrow => "Quad Arrow",
366            ShapeType::LeftRightUpArrow => "Left-Right-Up Arrow",
367            ShapeType::CircularArrow => "Circular Arrow",
368            
369            ShapeType::Parallelogram => "Parallelogram",
370            ShapeType::Trapezoid => "Trapezoid",
371            ShapeType::NonIsoscelesTrapezoid => "Non-Isosceles Trapezoid",
372            ShapeType::IsoscelesTrapezoid => "Isosceles Trapezoid",
373            ShapeType::Cube => "Cube",
374            ShapeType::Can => "Can",
375            ShapeType::Cone => "Cone",
376            ShapeType::Cylinder => "Cylinder",
377            ShapeType::Bevel => "Bevel",
378            ShapeType::Donut => "Donut",
379            ShapeType::NoSmoking => "No Smoking",
380            ShapeType::BlockArc => "Block Arc",
381            ShapeType::FoldedCorner => "Folded Corner",
382            ShapeType::SmileyFace => "Smiley Face",
383            ShapeType::Arc => "Arc",
384            ShapeType::Chord => "Chord",
385            ShapeType::Pie => "Pie",
386            ShapeType::Teardrop => "Teardrop",
387            ShapeType::Plaque => "Plaque",
388            ShapeType::MusicNote => "Music Note",
389            ShapeType::PictureFrame => "Picture Frame",
390            
391            ShapeType::Star10 => "10-Point Star",
392            ShapeType::Star12 => "12-Point Star",
393            ShapeType::Star16 => "16-Point Star",
394            ShapeType::Star24 => "24-Point Star",
395            ShapeType::Star32 => "32-Point Star",
396            ShapeType::Seal => "Seal",
397            ShapeType::Seal4 => "4-Point Seal",
398            ShapeType::Seal8 => "8-Point Seal",
399            ShapeType::Seal16 => "16-Point Seal",
400            ShapeType::Seal32 => "32-Point Seal",
401            ShapeType::ActionButtonBlank => "Action Button (Blank)",
402            ShapeType::ActionButtonHome => "Action Button (Home)",
403            ShapeType::ActionButtonHelp => "Action Button (Help)",
404            ShapeType::ActionButtonInformation => "Action Button (Information)",
405            ShapeType::ActionButtonForwardNext => "Action Button (Forward/Next)",
406            ShapeType::ActionButtonBackPrevious => "Action Button (Back/Previous)",
407            ShapeType::ActionButtonBeginning => "Action Button (Beginning)",
408            ShapeType::ActionButtonEnd => "Action Button (End)",
409            ShapeType::ActionButtonReturn => "Action Button (Return)",
410            ShapeType::ActionButtonDocument => "Action Button (Document)",
411            ShapeType::ActionButtonSound => "Action Button (Sound)",
412            ShapeType::ActionButtonMovie => "Action Button (Movie)",
413            
414            ShapeType::Heart => "Heart",
415            ShapeType::Lightning => "Lightning Bolt",
416            ShapeType::Sun => "Sun",
417            ShapeType::Moon => "Moon",
418            ShapeType::Cloud => "Cloud",
419            ShapeType::Brace => "Brace",
420            ShapeType::Bracket => "Bracket",
421            ShapeType::Plus => "Plus",
422            ShapeType::Minus => "Minus",
423        }
424    }
425}
426
427/// Gradient direction for linear gradients
428#[derive(Clone, Debug, Copy, PartialEq)]
429pub enum GradientDirection {
430    /// Left to right (0 degrees)
431    Horizontal,
432    /// Top to bottom (90 degrees)
433    Vertical,
434    /// Top-left to bottom-right (45 degrees)
435    DiagonalDown,
436    /// Bottom-left to top-right (315 degrees)
437    DiagonalUp,
438    /// Custom angle in degrees (0-360)
439    Angle(u32),
440}
441
442impl GradientDirection {
443    /// Get angle in 60000ths of a degree (OOXML format)
444    pub fn to_angle(&self) -> u32 {
445        match self {
446            GradientDirection::Horizontal => 0,
447            GradientDirection::Vertical => 5400000,      // 90 * 60000
448            GradientDirection::DiagonalDown => 2700000,  // 45 * 60000
449            GradientDirection::DiagonalUp => 18900000,   // 315 * 60000
450            GradientDirection::Angle(deg) => deg * 60000,
451        }
452    }
453}
454
455/// A gradient stop (color at a position)
456#[derive(Clone, Debug)]
457pub struct GradientStop {
458    pub color: String,
459    pub position: u32,  // 0-100000 (percentage * 1000)
460    pub transparency: Option<u32>,
461}
462
463impl GradientStop {
464    /// Create a gradient stop at a position (0-100%)
465    pub fn new(color: &str, position_percent: u32) -> Self {
466        GradientStop {
467            color: color.trim_start_matches('#').to_uppercase(),
468            position: position_percent.min(100) * 1000,
469            transparency: None,
470        }
471    }
472    
473    /// Set transparency (0-100 percent)
474    pub fn with_transparency(mut self, percent: u32) -> Self {
475        let alpha = ((100 - percent.min(100)) * 1000) as u32;
476        self.transparency = Some(alpha);
477        self
478    }
479}
480
481/// Gradient fill definition
482#[derive(Clone, Debug)]
483pub struct GradientFill {
484    pub stops: Vec<GradientStop>,
485    pub direction: GradientDirection,
486}
487
488impl GradientFill {
489    /// Create a simple two-color gradient
490    pub fn linear(start_color: &str, end_color: &str, direction: GradientDirection) -> Self {
491        GradientFill {
492            stops: vec![
493                GradientStop::new(start_color, 0),
494                GradientStop::new(end_color, 100),
495            ],
496            direction,
497        }
498    }
499    
500    /// Create a three-color gradient
501    pub fn three_color(start: &str, middle: &str, end: &str, direction: GradientDirection) -> Self {
502        GradientFill {
503            stops: vec![
504                GradientStop::new(start, 0),
505                GradientStop::new(middle, 50),
506                GradientStop::new(end, 100),
507            ],
508            direction,
509        }
510    }
511    
512    /// Add a custom stop
513    pub fn with_stop(mut self, stop: GradientStop) -> Self {
514        self.stops.push(stop);
515        self.stops.sort_by_key(|s| s.position);
516        self
517    }
518}
519
520/// Shape fill type - solid color or gradient
521#[derive(Clone, Debug)]
522pub enum FillType {
523    Solid(ShapeFill),
524    Gradient(GradientFill),
525    NoFill,
526}
527
528/// Shape fill/color properties
529#[derive(Clone, Debug)]
530pub struct ShapeFill {
531    pub color: String, // RGB hex color (e.g., "FF0000")
532    pub transparency: Option<u32>, // 0-100000 (100000 = fully transparent)
533}
534
535impl ShapeFill {
536    /// Create new shape fill with color
537    pub fn new(color: &str) -> Self {
538        ShapeFill {
539            color: color.trim_start_matches('#').to_uppercase(),
540            transparency: None,
541        }
542    }
543
544    /// Set transparency (0-100 percent)
545    pub fn with_transparency(mut self, percent: u32) -> Self {
546        let alpha = ((100 - percent.min(100)) * 1000) as u32;
547        self.transparency = Some(alpha);
548        self
549    }
550    
551    /// Set transparency (0-100 percent) - builder style (deprecated, use with_transparency)
552    pub fn transparency(self, percent: u32) -> Self {
553        self.with_transparency(percent)
554    }
555}
556
557/// Shape line/border properties
558#[derive(Clone, Debug)]
559pub struct ShapeLine {
560    pub color: String,
561    pub width: u32, // in EMU (English Metric Units)
562}
563
564impl ShapeLine {
565    /// Create new shape line with color and width
566    pub fn new(color: &str, width: u32) -> Self {
567        ShapeLine {
568            color: color.trim_start_matches('#').to_uppercase(),
569            width,
570        }
571    }
572}
573
574/// Shape definition
575#[derive(Clone, Debug)]
576pub struct Shape {
577    pub shape_type: ShapeType,
578    pub x: u32,      // Position X in EMU
579    pub y: u32,      // Position Y in EMU
580    pub width: u32,  // Width in EMU
581    pub height: u32, // Height in EMU
582    pub fill: Option<ShapeFill>,
583    pub gradient: Option<GradientFill>,
584    pub line: Option<ShapeLine>,
585    pub text: Option<String>,
586    /// Optional fixed shape ID for connector anchoring
587    pub id: Option<u32>,
588    /// Rotation in degrees (0-360)
589    pub rotation: Option<i32>,
590    /// Optional hyperlink
591    pub hyperlink: Option<crate::generator::hyperlinks::Hyperlink>,
592}
593
594impl Shape {
595    /// Create a new shape
596    pub fn new(shape_type: ShapeType, x: u32, y: u32, width: u32, height: u32) -> Self {
597        Shape {
598            shape_type,
599            x,
600            y,
601            width,
602            height,
603            fill: None,
604            gradient: None,
605            line: None,
606            text: None,
607            id: None,
608            rotation: None,
609            hyperlink: None,
610        }
611    }
612
613    /// Set shape ID for connector anchoring
614    pub fn with_id(mut self, id: u32) -> Self {
615        self.id = Some(id);
616        self
617    }
618
619    /// Set shape rotation in degrees
620    pub fn with_rotation(mut self, degrees: i32) -> Self {
621        self.rotation = Some(degrees);
622        self
623    }
624
625    /// Set shape hyperlink
626    pub fn with_hyperlink(mut self, hyperlink: crate::generator::hyperlinks::Hyperlink) -> Self {
627        self.hyperlink = Some(hyperlink);
628        self
629    }
630
631    /// Set shape fill (solid color)
632    pub fn with_fill(mut self, fill: ShapeFill) -> Self {
633        self.fill = Some(fill);
634        self.gradient = None; // Clear gradient if setting solid fill
635        self
636    }
637    
638    /// Set gradient fill
639    pub fn with_gradient(mut self, gradient: GradientFill) -> Self {
640        self.gradient = Some(gradient);
641        self.fill = None; // Clear solid fill if setting gradient
642        self
643    }
644
645    /// Set shape line
646    pub fn with_line(mut self, line: ShapeLine) -> Self {
647        self.line = Some(line);
648        self
649    }
650
651    /// Set shape text
652    pub fn with_text(mut self, text: &str) -> Self {
653        self.text = Some(text.to_string());
654        self
655    }
656}
657
658/// Convert EMU (English Metric Units) to inches
659pub fn emu_to_inches(emu: u32) -> f64 {
660    emu as f64 / 914400.0
661}
662
663/// Convert inches to EMU
664pub fn inches_to_emu(inches: f64) -> u32 {
665    (inches * 914400.0) as u32
666}
667
668/// Convert centimeters to EMU
669pub fn cm_to_emu(cm: f64) -> u32 {
670    (cm * 360000.0) as u32
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676
677    #[test]
678    fn test_shape_type_names() {
679        assert_eq!(ShapeType::Rectangle.preset_name(), "rect");
680        assert_eq!(ShapeType::Circle.preset_name(), "ellipse");
681        assert_eq!(ShapeType::RightArrow.preset_name(), "rightArrow");
682        assert_eq!(ShapeType::Star5.preset_name(), "star5");
683        assert_eq!(ShapeType::Heart.preset_name(), "heart");
684    }
685
686    #[test]
687    fn test_shape_fill_builder() {
688        let fill = ShapeFill::new("FF0000").transparency(50);
689        assert_eq!(fill.color, "FF0000");
690        assert_eq!(fill.transparency, Some(50000));
691    }
692
693    #[test]
694    fn test_shape_builder() {
695        let shape = Shape::new(ShapeType::Rectangle, 0, 0, 1000000, 500000)
696            .with_fill(ShapeFill::new("0000FF"))
697            .with_line(ShapeLine::new("000000", 25400))
698            .with_text("Hello");
699
700        assert_eq!(shape.x, 0);
701        assert_eq!(shape.width, 1000000);
702        assert_eq!(shape.text, Some("Hello".to_string()));
703    }
704
705    #[test]
706    fn test_emu_conversions() {
707        let emu = inches_to_emu(1.0);
708        assert_eq!(emu, 914400);
709        assert!((emu_to_inches(emu) - 1.0).abs() < 0.001);
710    }
711
712    #[test]
713    fn test_cm_to_emu() {
714        let emu = cm_to_emu(2.54); // 1 inch
715        assert_eq!(emu, 914400);
716    }
717}