Skip to main content

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;
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;
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
574use crate::core::{Positioned, ElementSized, Dimension};
575
576/// Shape definition
577#[derive(Clone, Debug)]
578pub struct Shape {
579    pub shape_type: ShapeType,
580    pub x: u32,      // Position X in EMU
581    pub y: u32,      // Position Y in EMU
582    pub width: u32,  // Width in EMU
583    pub height: u32, // Height in EMU
584    pub fill: Option<ShapeFill>,
585    pub gradient: Option<GradientFill>,
586    pub line: Option<ShapeLine>,
587    pub text: Option<String>,
588    /// Optional fixed shape ID for connector anchoring
589    pub id: Option<u32>,
590    /// Rotation in degrees (0-360)
591    pub rotation: Option<i32>,
592    /// Optional hyperlink
593    pub hyperlink: Option<crate::generator::hyperlinks::Hyperlink>,
594}
595
596impl Shape {
597    /// Create a new shape
598    pub fn new(shape_type: ShapeType, x: u32, y: u32, width: u32, height: u32) -> Self {
599        Shape {
600            shape_type,
601            x,
602            y,
603            width,
604            height,
605            fill: None,
606            gradient: None,
607            line: None,
608            text: None,
609            id: None,
610            rotation: None,
611            hyperlink: None,
612        }
613    }
614
615    /// Set shape ID for connector anchoring
616    pub fn with_id(mut self, id: u32) -> Self {
617        self.id = Some(id);
618        self
619    }
620
621    /// Set shape rotation in degrees
622    pub fn with_rotation(mut self, degrees: i32) -> Self {
623        self.rotation = Some(degrees);
624        self
625    }
626
627    /// Set shape hyperlink
628    pub fn with_hyperlink(mut self, hyperlink: crate::generator::hyperlinks::Hyperlink) -> Self {
629        self.hyperlink = Some(hyperlink);
630        self
631    }
632
633    /// Set shape fill (solid color)
634    pub fn with_fill(mut self, fill: ShapeFill) -> Self {
635        self.fill = Some(fill);
636        self.gradient = None; // Clear gradient if setting solid fill
637        self
638    }
639    
640    /// Set gradient fill
641    pub fn with_gradient(mut self, gradient: GradientFill) -> Self {
642        self.gradient = Some(gradient);
643        self.fill = None; // Clear solid fill if setting gradient
644        self
645    }
646
647    /// Set shape line
648    pub fn with_line(mut self, line: ShapeLine) -> Self {
649        self.line = Some(line);
650        self
651    }
652
653    /// Set shape text
654    pub fn with_text(mut self, text: &str) -> Self {
655        self.text = Some(text.to_string());
656        self
657    }
658
659    /// Create a shape using flexible Dimension units for position and size.
660    ///
661    /// ```
662    /// use ppt_rs::core::Dimension;
663    /// use ppt_rs::generator::shapes::{Shape, ShapeType};
664    ///
665    /// // Position at 10% from left, 20% from top; size = 80% width, 2 inches height
666    /// let shape = Shape::from_dimensions(
667    ///     ShapeType::Rectangle,
668    ///     Dimension::Ratio(0.1), Dimension::Ratio(0.2),
669    ///     Dimension::Ratio(0.8), Dimension::Inches(2.0),
670    /// );
671    /// ```
672    pub fn from_dimensions(shape_type: ShapeType, x: Dimension, y: Dimension, width: Dimension, height: Dimension) -> Self {
673        Shape::new(shape_type, x.to_emu_x(), y.to_emu_y(), width.to_emu_x(), height.to_emu_y())
674    }
675
676    /// Set position using flexible Dimension units (fluent).
677    ///
678    /// ```
679    /// use ppt_rs::core::Dimension;
680    /// use ppt_rs::generator::shapes::{Shape, ShapeType};
681    ///
682    /// let shape = Shape::new(ShapeType::Rectangle, 0, 0, 914400, 914400)
683    ///     .at(Dimension::Ratio(0.5), Dimension::Ratio(0.5));
684    /// ```
685    pub fn at(mut self, x: Dimension, y: Dimension) -> Self {
686        self.x = x.to_emu_x();
687        self.y = y.to_emu_y();
688        self
689    }
690
691    /// Set size using flexible Dimension units (fluent).
692    ///
693    /// ```
694    /// use ppt_rs::core::Dimension;
695    /// use ppt_rs::generator::shapes::{Shape, ShapeType};
696    ///
697    /// let shape = Shape::new(ShapeType::Rectangle, 0, 0, 0, 0)
698    ///     .with_dimensions(Dimension::Inches(3.0), Dimension::Cm(5.0));
699    /// ```
700    pub fn with_dimensions(mut self, width: Dimension, height: Dimension) -> Self {
701        self.width = width.to_emu_x();
702        self.height = height.to_emu_y();
703        self
704    }
705}
706
707impl Positioned for Shape {
708    fn x(&self) -> u32 { self.x }
709    fn y(&self) -> u32 { self.y }
710    fn set_position(&mut self, x: u32, y: u32) {
711        self.x = x;
712        self.y = y;
713    }
714}
715
716impl ElementSized for Shape {
717    fn width(&self) -> u32 { self.width }
718    fn height(&self) -> u32 { self.height }
719    fn set_size(&mut self, width: u32, height: u32) {
720        self.width = width;
721        self.height = height;
722    }
723}
724
725/// Convert EMU (English Metric Units) to inches
726pub fn emu_to_inches(emu: u32) -> f64 {
727    emu as f64 / 914400.0
728}
729
730/// Convert inches to EMU
731pub fn inches_to_emu(inches: f64) -> u32 {
732    (inches * 914400.0) as u32
733}
734
735/// Convert centimeters to EMU
736pub fn cm_to_emu(cm: f64) -> u32 {
737    (cm * 360000.0) as u32
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743
744    #[test]
745    fn test_shape_type_names() {
746        assert_eq!(ShapeType::Rectangle.preset_name(), "rect");
747        assert_eq!(ShapeType::Circle.preset_name(), "ellipse");
748        assert_eq!(ShapeType::RightArrow.preset_name(), "rightArrow");
749        assert_eq!(ShapeType::Star5.preset_name(), "star5");
750        assert_eq!(ShapeType::Heart.preset_name(), "heart");
751    }
752
753    #[test]
754    fn test_shape_fill_builder() {
755        let fill = ShapeFill::new("FF0000").transparency(50);
756        assert_eq!(fill.color, "FF0000");
757        assert_eq!(fill.transparency, Some(50000));
758    }
759
760    #[test]
761    fn test_shape_builder() {
762        let shape = Shape::new(ShapeType::Rectangle, 0, 0, 1000000, 500000)
763            .with_fill(ShapeFill::new("0000FF"))
764            .with_line(ShapeLine::new("000000", 25400))
765            .with_text("Hello");
766
767        assert_eq!(shape.x, 0);
768        assert_eq!(shape.width, 1000000);
769        assert_eq!(shape.text, Some("Hello".to_string()));
770    }
771
772    #[test]
773    fn test_emu_conversions() {
774        let emu = inches_to_emu(1.0);
775        assert_eq!(emu, 914400);
776        assert!((emu_to_inches(emu) - 1.0).abs() < 0.001);
777    }
778
779    #[test]
780    fn test_cm_to_emu() {
781        let emu = cm_to_emu(2.54); // 1 inch
782        assert_eq!(emu, 914400);
783    }
784
785    #[test]
786    fn test_shape_from_dimensions_ratio() {
787        let shape = Shape::from_dimensions(
788            ShapeType::Rectangle,
789            Dimension::Ratio(0.1), Dimension::Ratio(0.2),
790            Dimension::Ratio(0.8), Dimension::Ratio(0.6),
791        );
792        // 10% of 9144000 = 914400
793        assert_eq!(shape.x, 914400);
794        // 20% of 6858000 = 1371600
795        assert_eq!(shape.y, 1371600);
796        // 80% of 9144000 = 7315200
797        assert_eq!(shape.width, 7315200);
798        // 60% of 6858000 = 4114800
799        assert_eq!(shape.height, 4114800);
800    }
801
802    #[test]
803    fn test_shape_from_dimensions_mixed() {
804        let shape = Shape::from_dimensions(
805            ShapeType::Ellipse,
806            Dimension::Inches(1.0), Dimension::Cm(2.54),
807            Dimension::Pt(72.0), Dimension::Ratio(0.5),
808        );
809        assert_eq!(shape.x, 914400);   // 1 inch
810        assert_eq!(shape.y, 914400);   // 2.54 cm = 1 inch
811        assert_eq!(shape.width, 914400); // 72pt = 1 inch
812        assert_eq!(shape.height, 6858000 / 2); // 50% of slide height
813    }
814
815    #[test]
816    fn test_shape_at_fluent() {
817        let shape = Shape::new(ShapeType::Rectangle, 0, 0, 1000000, 500000)
818            .at(Dimension::Ratio(0.5), Dimension::Inches(2.0));
819        assert_eq!(shape.x, 9144000 / 2);
820        assert_eq!(shape.y, 914400 * 2);
821        assert_eq!(shape.width, 1000000); // unchanged
822    }
823
824    #[test]
825    fn test_shape_with_dimensions_fluent() {
826        let shape = Shape::new(ShapeType::Rectangle, 100, 200, 0, 0)
827            .with_dimensions(Dimension::Ratio(0.9), Dimension::Cm(5.0));
828        assert_eq!(shape.x, 100); // unchanged
829        assert_eq!(shape.y, 200); // unchanged
830        assert_eq!(shape.width, (9144000.0 * 0.9) as u32);
831        assert_eq!(shape.height, (5.0 * 360000.0) as u32);
832    }
833
834    #[test]
835    fn test_shape_chained_at_and_dimensions() {
836        let shape = Shape::new(ShapeType::Star5, 0, 0, 0, 0)
837            .at(Dimension::percent(10.0), Dimension::percent(20.0))
838            .with_dimensions(Dimension::percent(80.0), Dimension::percent(60.0))
839            .with_fill(ShapeFill::new("FF0000"));
840        assert_eq!(shape.x, 914400);
841        assert_eq!(shape.y, 1371600);
842        assert_eq!(shape.width, 7315200);
843        assert_eq!(shape.height, 4114800);
844        assert!(shape.fill.is_some());
845    }
846}