ts-visio 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ Built using specific schema-level abstractions to handle the complex internal st
12
12
 
13
13
  ## Features
14
14
 
15
- - **Read VSDX**: Open and parse `.vsdx` files (zippped XML).
15
+ - **Read VSDX**: Open and parse `.vsdx` files (zipped XML).
16
16
  - **Strict Typing**: Interact with `VisioPage`, `VisioShape`, and `VisioConnect` objects.
17
17
  - **ShapeSheet Access**: Read `Cells`, `Rows`, and `Sections` directly.
18
18
  - **Connections**: Analyze connectivity between shapes.
@@ -20,6 +20,11 @@ Built using specific schema-level abstractions to handle the complex internal st
20
20
  - **Modify Content**: Update text content of shapes.
21
21
  - **Create Shapes**: Add new rectangular shapes with text to pages.
22
22
  - **Connect Shapes**: Create dynamic connectors between shapes.
23
+ - **Text Styling**: Font size, font family, bold, color, horizontal/vertical alignment.
24
+ - **Shape Transformations**: Rotate, flip (X/Y), and resize shapes via a fluent API.
25
+ - **Deletion**: Remove shapes and pages cleanly (including orphaned connectors and relationships).
26
+ - **Lookup API**: Find shapes by ID, predicate, or look up pages by name.
27
+ - **Read-Back API**: Read custom properties, hyperlinks, and layer assignments from existing shapes.
23
28
 
24
29
  ## Installation
25
30
 
@@ -63,9 +68,13 @@ const shape = await page.addShape({
63
68
  y: 1,
64
69
  width: 3,
65
70
  height: 1,
66
- fillColor: "#ff0000", // Option hexadecimal fill color
71
+ fillColor: "#ff0000", // Hex fill color
67
72
  fontColor: "#ffffff",
68
- bold: true
73
+ bold: true,
74
+ fontSize: 14, // Points
75
+ fontFamily: "Segoe UI",
76
+ horzAlign: "center", // "left" | "center" | "right" | "justify"
77
+ verticalAlign: "middle" // "top" | "middle" | "bottom"
69
78
  });
70
79
 
71
80
  // Modify text
@@ -345,6 +354,70 @@ await lane1.addMember(startShape);
345
354
  await lane2.addMember(serverShape);
346
355
  ```
347
356
 
357
+ #### 19. Shape Transformations
358
+ Rotate, flip, and resize shapes using a fluent API.
359
+
360
+ ```typescript
361
+ const shape = await page.addShape({ text: "Widget", x: 3, y: 3, width: 2, height: 1 });
362
+
363
+ // Rotate 45 degrees (clockwise)
364
+ await shape.rotate(45);
365
+ console.log(shape.angle); // 45
366
+
367
+ // Mirror horizontally or vertically
368
+ await shape.flipX();
369
+ await shape.flipY(false); // un-flip
370
+
371
+ // Resize (keeps the pin point centred)
372
+ await shape.resize(4, 2);
373
+ console.log(shape.width, shape.height); // 4, 2
374
+
375
+ // Chainable
376
+ await shape.rotate(90).then(s => s.resize(3, 1));
377
+ ```
378
+
379
+ #### 20. Deleting Shapes and Pages
380
+ Remove shapes or entire pages. Orphaned connectors and relationships are cleaned up automatically.
381
+
382
+ ```typescript
383
+ // Delete a shape
384
+ await shape.delete();
385
+
386
+ // Delete a page (removes page file, rels, and all back-page references)
387
+ await doc.deletePage(page2);
388
+ ```
389
+
390
+ #### 21. Lookup API
391
+ Find shapes and pages without iterating manually.
392
+
393
+ ```typescript
394
+ // Find a shape by its numeric ID (searches nested groups too)
395
+ const target = await page.getShapeById("42");
396
+
397
+ // Find all shapes matching a predicate
398
+ const servers = await page.findShapes(s => s.text.startsWith("Server"));
399
+
400
+ // Look up a page by name (exact, case-sensitive)
401
+ const detailPage = doc.getPage("Architecture Diagram");
402
+ ```
403
+
404
+ #### 22. Reading Shape Data Back
405
+ Retrieve custom properties, hyperlinks, and layer assignments that were previously written.
406
+
407
+ ```typescript
408
+ // Custom properties (shape data)
409
+ const props = shape.getProperties();
410
+ console.log(props["IP"].value); // "192.168.1.10"
411
+ console.log(props["Port"].type); // VisioPropType.Number
412
+
413
+ // Hyperlinks
414
+ const links = shape.getHyperlinks();
415
+ // [ { address: "https://example.com", description: "Docs", newWindow: false } ]
416
+
417
+ // Layer indices
418
+ const indices = shape.getLayerIndices(); // e.g. [0, 2]
419
+ ```
420
+
348
421
  ## Examples
349
422
 
350
423
  Check out the [examples](./examples) directory for complete scripts.
package/dist/Layer.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { VisioPackage } from './VisioPackage';
2
+ import { ShapeModifier } from './ShapeModifier';
2
3
  export declare class Layer {
3
4
  name: string;
4
5
  index: number;
5
6
  private pageId?;
6
7
  private pkg?;
7
- constructor(name: string, index: number, pageId?: string | undefined, pkg?: VisioPackage | undefined);
8
+ private modifier;
9
+ constructor(name: string, index: number, pageId?: string | undefined, pkg?: VisioPackage | undefined, modifier?: ShapeModifier);
8
10
  setVisible(visible: boolean): Promise<this>;
9
11
  setLocked(locked: boolean): Promise<this>;
10
12
  hide(): Promise<this>;
package/dist/Layer.js CHANGED
@@ -3,26 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Layer = void 0;
4
4
  const ShapeModifier_1 = require("./ShapeModifier");
5
5
  class Layer {
6
- constructor(name, index, pageId, pkg) {
6
+ constructor(name, index, pageId, pkg, modifier) {
7
7
  this.name = name;
8
8
  this.index = index;
9
9
  this.pageId = pageId;
10
10
  this.pkg = pkg;
11
+ this.modifier = modifier ?? (pkg ? new ShapeModifier_1.ShapeModifier(pkg) : null);
11
12
  }
12
13
  async setVisible(visible) {
13
- if (!this.pageId || !this.pkg) {
14
+ if (!this.pageId || !this.modifier) {
14
15
  throw new Error('Layer was not created with page context. Cannot update properties.');
15
16
  }
16
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
17
- await modifier.updateLayerProperty(this.pageId, this.index, 'Visible', visible ? '1' : '0');
17
+ await this.modifier.updateLayerProperty(this.pageId, this.index, 'Visible', visible ? '1' : '0');
18
18
  return this;
19
19
  }
20
20
  async setLocked(locked) {
21
- if (!this.pageId || !this.pkg) {
21
+ if (!this.pageId || !this.modifier) {
22
22
  throw new Error('Layer was not created with page context. Cannot update properties.');
23
23
  }
24
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
25
- await modifier.updateLayerProperty(this.pageId, this.index, 'Lock', locked ? '1' : '0');
24
+ await this.modifier.updateLayerProperty(this.pageId, this.index, 'Lock', locked ? '1' : '0');
26
25
  return this;
27
26
  }
28
27
  async hide() {
package/dist/Page.d.ts CHANGED
@@ -12,10 +12,22 @@ export declare class Page {
12
12
  private media;
13
13
  private rels;
14
14
  private modifier;
15
+ /** Resolved OPC part path for this page's XML file. */
16
+ private pagePath;
15
17
  constructor(internalPage: VisioPage, pkg: VisioPackage, media?: MediaManager, rels?: RelsManager, modifier?: ShapeModifier);
16
18
  get id(): string;
17
19
  get name(): string;
18
20
  getShapes(): Shape[];
21
+ /**
22
+ * Find a shape by its ID anywhere on the page, including shapes nested inside groups.
23
+ * Returns undefined if no shape with that ID exists.
24
+ */
25
+ getShapeById(id: string): Shape | undefined;
26
+ /**
27
+ * Return all shapes on the page (including nested group children) that satisfy
28
+ * the predicate. Equivalent to getAllShapes().filter(predicate).
29
+ */
30
+ findShapes(predicate: (shape: Shape) => boolean): Shape[];
19
31
  addShape(props: NewShapeProps, parentId?: string): Promise<Shape>;
20
32
  connectShapes(fromShape: Shape, toShape: Shape, beginArrow?: string, endArrow?: string): Promise<void>;
21
33
  addImage(data: Buffer, name: string, x: number, y: number, width: number, height: number): Promise<Shape>;
package/dist/Page.js CHANGED
@@ -12,9 +12,13 @@ class Page {
12
12
  constructor(internalPage, pkg, media, rels, modifier) {
13
13
  this.internalPage = internalPage;
14
14
  this.pkg = pkg;
15
+ // Prefer the relationship-resolved path over the ID-derived fallback so
16
+ // that loaded files with non-sequential page filenames work correctly.
17
+ this.pagePath = internalPage.xmlPath ?? `visio/pages/page${internalPage.ID}.xml`;
15
18
  this.media = media || new MediaManager_1.MediaManager(pkg);
16
19
  this.rels = rels || new RelsManager_1.RelsManager(pkg);
17
20
  this.modifier = modifier || new ShapeModifier_1.ShapeModifier(pkg);
21
+ this.modifier.registerPage(internalPage.ID, this.pagePath);
18
22
  }
19
23
  get id() {
20
24
  return this.internalPage.ID;
@@ -24,18 +28,37 @@ class Page {
24
28
  }
25
29
  getShapes() {
26
30
  const reader = new ShapeReader_1.ShapeReader(this.pkg);
27
- // Assuming standard path mapping for now
28
- const pagePath = `visio/pages/page${this.id}.xml`;
29
31
  try {
30
- const internalShapes = reader.readShapes(pagePath);
31
- return internalShapes.map(s => new Shape_1.Shape(s, this.id, this.pkg));
32
+ const internalShapes = reader.readShapes(this.pagePath);
33
+ return internalShapes.map(s => new Shape_1.Shape(s, this.id, this.pkg, this.modifier));
32
34
  }
33
35
  catch (e) {
34
- // If page file doesn't exist or is empty, return empty array
35
36
  console.warn(`Could not read shapes for page ${this.id}:`, e);
36
37
  return [];
37
38
  }
38
39
  }
40
+ /**
41
+ * Find a shape by its ID anywhere on the page, including shapes nested inside groups.
42
+ * Returns undefined if no shape with that ID exists.
43
+ */
44
+ getShapeById(id) {
45
+ const reader = new ShapeReader_1.ShapeReader(this.pkg);
46
+ const internal = reader.readShapeById(this.pagePath, id);
47
+ if (!internal)
48
+ return undefined;
49
+ return new Shape_1.Shape(internal, this.id, this.pkg, this.modifier);
50
+ }
51
+ /**
52
+ * Return all shapes on the page (including nested group children) that satisfy
53
+ * the predicate. Equivalent to getAllShapes().filter(predicate).
54
+ */
55
+ findShapes(predicate) {
56
+ const reader = new ShapeReader_1.ShapeReader(this.pkg);
57
+ const all = reader.readAllShapes(this.pagePath);
58
+ return all
59
+ .map(s => new Shape_1.Shape(s, this.id, this.pkg, this.modifier))
60
+ .filter(predicate);
61
+ }
39
62
  async addShape(props, parentId) {
40
63
  const newId = await this.modifier.addShape(this.id, props, parentId);
41
64
  // Return a fresh Shape object representing the new shape
@@ -53,7 +76,7 @@ class Page {
53
76
  'LocPinY': props.height / 2
54
77
  }
55
78
  });
56
- return new Shape_1.Shape(internalStub, this.id, this.pkg);
79
+ return new Shape_1.Shape(internalStub, this.id, this.pkg, this.modifier);
57
80
  }
58
81
  async connectShapes(fromShape, toShape, beginArrow, endArrow) {
59
82
  await this.modifier.addConnector(this.id, fromShape.id, toShape.id, beginArrow, endArrow);
@@ -61,8 +84,8 @@ class Page {
61
84
  async addImage(data, name, x, y, width, height) {
62
85
  // 1. Upload Media
63
86
  const mediaPath = this.media.addMedia(name, data);
64
- // 2. Link Page to Media
65
- const rId = await this.rels.addPageImageRel(this.id, mediaPath);
87
+ // 2. Link Page to Media (use resolved path so loaded files work correctly)
88
+ const rId = await this.rels.addImageRelationship(this.pagePath, mediaPath);
66
89
  // 3. Create Shape
67
90
  const newId = await this.modifier.addShape(this.id, {
68
91
  text: '',
@@ -80,7 +103,7 @@ class Page {
80
103
  'PinY': y
81
104
  }
82
105
  });
83
- return new Shape_1.Shape(internalStub, this.id, this.pkg);
106
+ return new Shape_1.Shape(internalStub, this.id, this.pkg, this.modifier);
84
107
  }
85
108
  async addContainer(props) {
86
109
  const newId = await this.modifier.addContainer(this.id, props);
@@ -94,7 +117,7 @@ class Page {
94
117
  'PinY': props.y
95
118
  }
96
119
  });
97
- return new Shape_1.Shape(internalStub, this.id, this.pkg);
120
+ return new Shape_1.Shape(internalStub, this.id, this.pkg, this.modifier);
98
121
  }
99
122
  async addList(props, direction = 'vertical') {
100
123
  const newId = await this.modifier.addList(this.id, props, direction);
@@ -108,7 +131,7 @@ class Page {
108
131
  'PinY': props.y
109
132
  }
110
133
  });
111
- return new Shape_1.Shape(internalStub, this.id, this.pkg);
134
+ return new Shape_1.Shape(internalStub, this.id, this.pkg, this.modifier);
112
135
  }
113
136
  /**
114
137
  * Creates a Swimlane Pool (which is technically a Vertical List of Containers).
@@ -179,7 +202,7 @@ class Page {
179
202
  }
180
203
  async addLayer(name, options) {
181
204
  const info = await this.modifier.addLayer(this.id, name, options);
182
- return new Layer_1.Layer(info.name, info.index, this.id, this.pkg);
205
+ return new Layer_1.Layer(info.name, info.index, this.id, this.pkg, this.modifier);
183
206
  }
184
207
  }
185
208
  exports.Page = Page;
package/dist/Shape.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { VisioShape } from './types/VisioTypes';
2
2
  import { VisioPackage } from './VisioPackage';
3
- import { ShapeStyle } from './ShapeModifier';
3
+ import { ShapeModifier, ShapeStyle } from './ShapeModifier';
4
4
  import { VisioPropType } from './types/VisioTypes';
5
5
  import { Layer } from './Layer';
6
6
  export interface ShapeData {
@@ -9,11 +9,18 @@ export interface ShapeData {
9
9
  hidden?: boolean;
10
10
  type?: VisioPropType;
11
11
  }
12
+ export interface ShapeHyperlink {
13
+ address?: string;
14
+ subAddress?: string;
15
+ description?: string;
16
+ newWindow: boolean;
17
+ }
12
18
  export declare class Shape {
13
19
  private internalShape;
14
20
  private pageId;
15
21
  private pkg;
16
- constructor(internalShape: VisioShape, pageId: string, pkg: VisioPackage);
22
+ private modifier;
23
+ constructor(internalShape: VisioShape, pageId: string, pkg: VisioPackage, modifier?: ShapeModifier);
17
24
  get id(): string;
18
25
  get name(): string;
19
26
  get text(): string;
@@ -22,6 +29,7 @@ export declare class Shape {
22
29
  get height(): number;
23
30
  get x(): number;
24
31
  get y(): number;
32
+ delete(): Promise<void>;
25
33
  connectTo(targetShape: Shape, beginArrow?: string, endArrow?: string): Promise<this>;
26
34
  setStyle(style: ShapeStyle): Promise<this>;
27
35
  placeRightOf(targetShape: Shape, options?: {
@@ -36,6 +44,41 @@ export declare class Shape {
36
44
  }): this;
37
45
  setPropertyValue(name: string, value: string | number | boolean | Date): this;
38
46
  addData(key: string, data: ShapeData): this;
47
+ /**
48
+ * Read back all custom property (shape data) entries written to this shape.
49
+ * Returns a map of property key → ShapeData. Values are coerced to the
50
+ * declared Visio type (Number, Boolean, Date, or String).
51
+ */
52
+ getProperties(): Record<string, ShapeData>;
53
+ /**
54
+ * Read back all hyperlinks attached to this shape.
55
+ */
56
+ getHyperlinks(): ShapeHyperlink[];
57
+ /**
58
+ * Read back the layer indices this shape is assigned to.
59
+ * Returns an empty array if the shape has no layer assignment.
60
+ */
61
+ getLayerIndices(): number[];
62
+ /** Current rotation angle in degrees (0 if no Angle cell is set). */
63
+ get angle(): number;
64
+ /**
65
+ * Rotate the shape to an absolute angle (degrees, clockwise).
66
+ * Replaces any existing rotation.
67
+ */
68
+ rotate(degrees: number): Promise<this>;
69
+ /**
70
+ * Resize the shape to the given width and height (in inches).
71
+ * Updates LocPinX/LocPinY to keep the centre-pin at width/2, height/2.
72
+ */
73
+ resize(width: number, height: number): Promise<this>;
74
+ /**
75
+ * Flip the shape horizontally. Pass `false` to un-flip.
76
+ */
77
+ flipX(enabled?: boolean): Promise<this>;
78
+ /**
79
+ * Flip the shape vertically. Pass `false` to un-flip.
80
+ */
81
+ flipY(enabled?: boolean): Promise<this>;
39
82
  addMember(memberShape: Shape): Promise<this>;
40
83
  addListItem(item: Shape): Promise<this>;
41
84
  resizeToFit(padding?: number): Promise<this>;
@@ -65,4 +108,6 @@ export declare class Shape {
65
108
  * Alias for assignLayer. Adds this shape to a layer.
66
109
  */
67
110
  addToLayer(layer: Layer | number): Promise<this>;
111
+ private setLocalCoord;
112
+ private setLocalRawCell;
68
113
  }
package/dist/Shape.js CHANGED
@@ -3,11 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Shape = void 0;
4
4
  const ShapeModifier_1 = require("./ShapeModifier");
5
5
  const VisioTypes_1 = require("./types/VisioTypes");
6
+ /** Round a coordinate to 10 decimal places to prevent float-to-string-to-float precision drift. */
7
+ function fmtCoord(n) {
8
+ return parseFloat(n.toFixed(10)).toString();
9
+ }
6
10
  class Shape {
7
- constructor(internalShape, pageId, pkg) {
11
+ constructor(internalShape, pageId, pkg, modifier) {
8
12
  this.internalShape = internalShape;
9
13
  this.pageId = pageId;
10
14
  this.pkg = pkg;
15
+ this.modifier = modifier ?? new ShapeModifier_1.ShapeModifier(pkg);
11
16
  }
12
17
  get id() {
13
18
  return this.internalShape.ID;
@@ -19,9 +24,7 @@ class Shape {
19
24
  return this.internalShape.Text || '';
20
25
  }
21
26
  async setText(newText) {
22
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
23
- await modifier.updateShapeText(this.pageId, this.id, newText);
24
- // Update local state to reflect change
27
+ await this.modifier.updateShapeText(this.pageId, this.id, newText);
25
28
  this.internalShape.Text = newText;
26
29
  }
27
30
  get width() {
@@ -36,65 +39,47 @@ class Shape {
36
39
  get y() {
37
40
  return this.internalShape.Cells['PinY'] ? Number(this.internalShape.Cells['PinY'].V) : 0;
38
41
  }
42
+ async delete() {
43
+ await this.modifier.deleteShape(this.pageId, this.id);
44
+ }
39
45
  async connectTo(targetShape, beginArrow, endArrow) {
40
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
41
- await modifier.addConnector(this.pageId, this.id, targetShape.id, beginArrow, endArrow);
46
+ await this.modifier.addConnector(this.pageId, this.id, targetShape.id, beginArrow, endArrow);
42
47
  return this;
43
48
  }
44
49
  async setStyle(style) {
45
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
46
- await modifier.updateShapeStyle(this.pageId, this.id, style);
47
- // Minimal local state update to reflect changes if necessary
48
- // For now, valid XML is the priority.
50
+ await this.modifier.updateShapeStyle(this.pageId, this.id, style);
49
51
  return this;
50
52
  }
51
53
  async placeRightOf(targetShape, options = { gap: 1 }) {
52
- const newX = targetShape.x + targetShape.width + options.gap;
53
- const newY = targetShape.y; // Keep same Y (per instructions, aligns center-to-center if PinY is center)
54
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
55
- await modifier.updateShapePosition(this.pageId, this.id, newX, newY);
56
- // Update local state is crucial for chaining successive placements
57
- if (this.internalShape.Cells['PinX'])
58
- this.internalShape.Cells['PinX'].V = newX.toString();
59
- else
60
- this.internalShape.Cells['PinX'] = { V: newX.toString(), N: 'PinX' };
61
- if (this.internalShape.Cells['PinY'])
62
- this.internalShape.Cells['PinY'].V = newY.toString();
63
- else
64
- this.internalShape.Cells['PinY'] = { V: newY.toString(), N: 'PinY' };
54
+ // PinX is the shape centre, so right edge of target = target.x + target.width/2;
55
+ // left edge of this = newX - this.width/2. Set left edge = right edge of target + gap.
56
+ const newX = targetShape.x + (targetShape.width / 2) + options.gap + (this.width / 2);
57
+ const newY = targetShape.y; // Align centres vertically
58
+ await this.modifier.updateShapePosition(this.pageId, this.id, newX, newY);
59
+ // Update local state — rounded to avoid float precision drift in chained placements
60
+ this.setLocalCoord('PinX', newX);
61
+ this.setLocalCoord('PinY', newY);
65
62
  return this;
66
63
  }
67
64
  async placeBelow(targetShape, options = { gap: 1 }) {
68
- const newX = targetShape.x; // Align Centers
69
- // Target Bottom = target.y - target.height / 2
70
- // My Top = Target Bottom - gap
71
- // My Center = My Top - my.height / 2
72
- // My Center = target.y - target.height/2 - gap - my.height/2
65
+ const newX = targetShape.x; // Align centres horizontally
66
+ // Target bottom edge = target.y - target.height/2
67
+ // This centre = target bottom - gap - this.height/2
73
68
  const newY = targetShape.y - (targetShape.height + this.height) / 2 - options.gap;
74
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
75
- await modifier.updateShapePosition(this.pageId, this.id, newX, newY);
76
- if (this.internalShape.Cells['PinX'])
77
- this.internalShape.Cells['PinX'].V = newX.toString();
78
- else
79
- this.internalShape.Cells['PinX'] = { V: newX.toString(), N: 'PinX' };
80
- if (this.internalShape.Cells['PinY'])
81
- this.internalShape.Cells['PinY'].V = newY.toString();
82
- else
83
- this.internalShape.Cells['PinY'] = { V: newY.toString(), N: 'PinY' };
69
+ await this.modifier.updateShapePosition(this.pageId, this.id, newX, newY);
70
+ this.setLocalCoord('PinX', newX);
71
+ this.setLocalCoord('PinY', newY);
84
72
  return this;
85
73
  }
86
74
  addPropertyDefinition(name, type, options = {}) {
87
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
88
- modifier.addPropertyDefinition(this.pageId, this.id, name, type, options);
75
+ this.modifier.addPropertyDefinition(this.pageId, this.id, name, type, options);
89
76
  return this;
90
77
  }
91
78
  setPropertyValue(name, value) {
92
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
93
- modifier.setPropertyValue(this.pageId, this.id, name, value);
79
+ this.modifier.setPropertyValue(this.pageId, this.id, name, value);
94
80
  return this;
95
81
  }
96
82
  addData(key, data) {
97
- // Auto-detect type if not provided
98
83
  let type = data.type;
99
84
  if (type === undefined) {
100
85
  if (data.value instanceof Date) {
@@ -110,59 +95,103 @@ class Shape {
110
95
  type = VisioTypes_1.VisioPropType.String;
111
96
  }
112
97
  }
113
- // 1. Define Property
114
- this.addPropertyDefinition(key, type, {
115
- label: data.label,
116
- invisible: data.hidden
117
- });
118
- // 2. Set Value
98
+ this.addPropertyDefinition(key, type, { label: data.label, invisible: data.hidden });
119
99
  this.setPropertyValue(key, data.value);
120
100
  return this;
121
101
  }
102
+ /**
103
+ * Read back all custom property (shape data) entries written to this shape.
104
+ * Returns a map of property key → ShapeData. Values are coerced to the
105
+ * declared Visio type (Number, Boolean, Date, or String).
106
+ */
107
+ getProperties() {
108
+ return this.modifier.getShapeProperties(this.pageId, this.id);
109
+ }
110
+ /**
111
+ * Read back all hyperlinks attached to this shape.
112
+ */
113
+ getHyperlinks() {
114
+ return this.modifier.getShapeHyperlinks(this.pageId, this.id);
115
+ }
116
+ /**
117
+ * Read back the layer indices this shape is assigned to.
118
+ * Returns an empty array if the shape has no layer assignment.
119
+ */
120
+ getLayerIndices() {
121
+ return this.modifier.getShapeLayerIndices(this.pageId, this.id);
122
+ }
123
+ /** Current rotation angle in degrees (0 if no Angle cell is set). */
124
+ get angle() {
125
+ const cell = this.internalShape.Cells['Angle'];
126
+ return cell ? (parseFloat(cell.V) * 180) / Math.PI : 0;
127
+ }
128
+ /**
129
+ * Rotate the shape to an absolute angle (degrees, clockwise).
130
+ * Replaces any existing rotation.
131
+ */
132
+ async rotate(degrees) {
133
+ await this.modifier.rotateShape(this.pageId, this.id, degrees);
134
+ const radians = (degrees * Math.PI) / 180;
135
+ this.setLocalCoord('Angle', radians);
136
+ return this;
137
+ }
138
+ /**
139
+ * Resize the shape to the given width and height (in inches).
140
+ * Updates LocPinX/LocPinY to keep the centre-pin at width/2, height/2.
141
+ */
142
+ async resize(width, height) {
143
+ if (width <= 0 || height <= 0)
144
+ throw new Error('Shape dimensions must be positive');
145
+ await this.modifier.resizeShape(this.pageId, this.id, width, height);
146
+ this.setLocalCoord('Width', width);
147
+ this.setLocalCoord('Height', height);
148
+ this.setLocalCoord('LocPinX', width / 2);
149
+ this.setLocalCoord('LocPinY', height / 2);
150
+ return this;
151
+ }
152
+ /**
153
+ * Flip the shape horizontally. Pass `false` to un-flip.
154
+ */
155
+ async flipX(enabled = true) {
156
+ this.modifier.setShapeFlip(this.pageId, this.id, 'x', enabled);
157
+ this.setLocalRawCell('FlipX', enabled ? '1' : '0');
158
+ return this;
159
+ }
160
+ /**
161
+ * Flip the shape vertically. Pass `false` to un-flip.
162
+ */
163
+ async flipY(enabled = true) {
164
+ this.modifier.setShapeFlip(this.pageId, this.id, 'y', enabled);
165
+ this.setLocalRawCell('FlipY', enabled ? '1' : '0');
166
+ return this;
167
+ }
122
168
  async addMember(memberShape) {
123
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
124
- // Type="Container" is the standard for Container relationships
125
- await modifier.addRelationship(this.pageId, this.id, memberShape.id, 'Container');
169
+ await this.modifier.addRelationship(this.pageId, this.id, memberShape.id, 'Container');
126
170
  return this;
127
171
  }
128
172
  async addListItem(item) {
129
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
130
- await modifier.addListItem(this.pageId, this.id, item.id);
131
- // Refresh local state after modifer updates (resizeToFit called internally)
173
+ await this.modifier.addListItem(this.pageId, this.id, item.id);
132
174
  await this.refreshLocalState();
133
175
  return this;
134
176
  }
135
177
  async resizeToFit(padding = 0.25) {
136
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
137
- await modifier.resizeContainerToFit(this.pageId, this.id, padding);
178
+ await this.modifier.resizeContainerToFit(this.pageId, this.id, padding);
138
179
  await this.refreshLocalState();
139
180
  return this;
140
181
  }
141
182
  async refreshLocalState() {
142
- // Reloads internal Cells from modifier's fresh XML
143
- // This is a bit expensive but ensures consistency
144
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
145
- const geo = modifier.getShapeGeometry(this.pageId, this.id);
146
- const update = (n, v) => {
147
- if (this.internalShape.Cells[n])
148
- this.internalShape.Cells[n].V = v;
149
- else
150
- this.internalShape.Cells[n] = { V: v, N: n };
151
- };
152
- update('PinX', geo.x.toString());
153
- update('PinY', geo.y.toString());
154
- update('Width', geo.width.toString());
155
- update('Height', geo.height.toString());
183
+ const geo = this.modifier.getShapeGeometry(this.pageId, this.id);
184
+ this.setLocalCoord('PinX', geo.x);
185
+ this.setLocalCoord('PinY', geo.y);
186
+ this.setLocalCoord('Width', geo.width);
187
+ this.setLocalCoord('Height', geo.height);
156
188
  }
157
189
  async addHyperlink(address, description) {
158
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
159
- await modifier.addHyperlink(this.pageId, this.id, { address, description });
190
+ await this.modifier.addHyperlink(this.pageId, this.id, { address, description });
160
191
  return this;
161
192
  }
162
193
  async linkToPage(targetPage, description) {
163
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
164
- // Internal links use SubAddress='PageName' and empty Address
165
- await modifier.addHyperlink(this.pageId, this.id, {
194
+ await this.modifier.addHyperlink(this.pageId, this.id, {
166
195
  address: '',
167
196
  subAddress: targetPage.name,
168
197
  description
@@ -189,8 +218,7 @@ class Shape {
189
218
  }
190
219
  async assignLayer(layer) {
191
220
  const index = typeof layer === 'number' ? layer : layer.index;
192
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
193
- await modifier.assignLayer(this.pageId, this.id, index);
221
+ await this.modifier.assignLayer(this.pageId, this.id, index);
194
222
  return this;
195
223
  }
196
224
  /**
@@ -199,5 +227,18 @@ class Shape {
199
227
  async addToLayer(layer) {
200
228
  return this.assignLayer(layer);
201
229
  }
230
+ setLocalCoord(name, value) {
231
+ const v = fmtCoord(value);
232
+ if (this.internalShape.Cells[name])
233
+ this.internalShape.Cells[name].V = v;
234
+ else
235
+ this.internalShape.Cells[name] = { V: v, N: name };
236
+ }
237
+ setLocalRawCell(name, value) {
238
+ if (this.internalShape.Cells[name])
239
+ this.internalShape.Cells[name].V = value;
240
+ else
241
+ this.internalShape.Cells[name] = { V: value, N: name };
242
+ }
202
243
  }
203
244
  exports.Shape = Shape;