ts-visio 1.0.1 → 1.1.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
@@ -321,6 +321,30 @@ await annotations.show(); // Show again
321
321
  await wireframe.setLocked(true);
322
322
  ```
323
323
 
324
+ #### 18. Cross-Functional Flowcharts (Swimlanes)
325
+ Create structured Swimlane diagrams involving Pools and Lanes.
326
+
327
+ ```typescript
328
+ // 1. Create a Pool (Vertical List)
329
+ const pool = await page.addSwimlanePool({
330
+ text: "User Registration",
331
+ x: 5, y: 5, width: 10, height: 6
332
+ });
333
+
334
+ // 2. Create Lanes (Containers)
335
+ const lane1 = await page.addSwimlaneLane({ text: "Client", width: 10, height: 2 });
336
+ const lane2 = await page.addSwimlaneLane({ text: "Server", width: 10, height: 2 });
337
+
338
+ // 3. Add Lanes to Pool (Order matters)
339
+ await pool.addListItem(lane1);
340
+ await pool.addListItem(lane2);
341
+
342
+ // 4. Group Shapes into Lanes
343
+ // This binds their movement so they stay inside the lane
344
+ await lane1.addMember(startShape);
345
+ await lane2.addMember(serverShape);
346
+ ```
347
+
324
348
  ## Examples
325
349
 
326
350
  Check out the [examples](./examples) directory for complete scripts.
@@ -332,6 +356,7 @@ Check out the [examples](./examples) directory for complete scripts.
332
356
  - **[Hyperlinks Demo](./examples/hyperlinks_demo.ts)**: Demonstrates Internal and External navigation.
333
357
  - **[Layers Demo](./examples/layers_demo.ts)**: Shows how to create layers and toggle visibility.
334
358
  - **[Image Embedding Demo](./examples/images_demo.ts)**: Demonstrates how to embed PNG or JPEG images into a diagram.
359
+ - **[Swimlane Demo](./examples/swimlane_demo.ts)**: Demonstrates creating Cross-Functional Flowcharts with Pools and Lanes.
335
360
 
336
361
  ## Development
337
362
 
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,6 +12,8 @@ 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;
@@ -21,6 +23,16 @@ export declare class Page {
21
23
  addImage(data: Buffer, name: string, x: number, y: number, width: number, height: number): Promise<Shape>;
22
24
  addContainer(props: NewShapeProps): Promise<Shape>;
23
25
  addList(props: NewShapeProps, direction?: 'vertical' | 'horizontal'): Promise<Shape>;
26
+ /**
27
+ * Creates a Swimlane Pool (which is technically a Vertical List of Containers).
28
+ * @param props Visual properties
29
+ */
30
+ addSwimlanePool(props: NewShapeProps): Promise<Shape>;
31
+ /**
32
+ * Creates a Swimlane Lane (which is technically a Container).
33
+ * @param props Visual properties
34
+ */
35
+ addSwimlaneLane(props: NewShapeProps): Promise<Shape>;
24
36
  addTable(x: number, y: number, title: string, columns: string[]): Promise<Shape>;
25
37
  addLayer(name: string, options?: {
26
38
  visible?: boolean;
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,11 +28,9 @@ 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
36
  // If page file doesn't exist or is empty, return empty array
@@ -53,7 +55,7 @@ class Page {
53
55
  'LocPinY': props.height / 2
54
56
  }
55
57
  });
56
- return new Shape_1.Shape(internalStub, this.id, this.pkg);
58
+ return new Shape_1.Shape(internalStub, this.id, this.pkg, this.modifier);
57
59
  }
58
60
  async connectShapes(fromShape, toShape, beginArrow, endArrow) {
59
61
  await this.modifier.addConnector(this.id, fromShape.id, toShape.id, beginArrow, endArrow);
@@ -61,8 +63,8 @@ class Page {
61
63
  async addImage(data, name, x, y, width, height) {
62
64
  // 1. Upload Media
63
65
  const mediaPath = this.media.addMedia(name, data);
64
- // 2. Link Page to Media
65
- const rId = await this.rels.addPageImageRel(this.id, mediaPath);
66
+ // 2. Link Page to Media (use resolved path so loaded files work correctly)
67
+ const rId = await this.rels.addImageRelationship(this.pagePath, mediaPath);
66
68
  // 3. Create Shape
67
69
  const newId = await this.modifier.addShape(this.id, {
68
70
  text: '',
@@ -80,7 +82,7 @@ class Page {
80
82
  'PinY': y
81
83
  }
82
84
  });
83
- return new Shape_1.Shape(internalStub, this.id, this.pkg);
85
+ return new Shape_1.Shape(internalStub, this.id, this.pkg, this.modifier);
84
86
  }
85
87
  async addContainer(props) {
86
88
  const newId = await this.modifier.addContainer(this.id, props);
@@ -94,7 +96,7 @@ class Page {
94
96
  'PinY': props.y
95
97
  }
96
98
  });
97
- return new Shape_1.Shape(internalStub, this.id, this.pkg);
99
+ return new Shape_1.Shape(internalStub, this.id, this.pkg, this.modifier);
98
100
  }
99
101
  async addList(props, direction = 'vertical') {
100
102
  const newId = await this.modifier.addList(this.id, props, direction);
@@ -108,7 +110,23 @@ class Page {
108
110
  'PinY': props.y
109
111
  }
110
112
  });
111
- return new Shape_1.Shape(internalStub, this.id, this.pkg);
113
+ return new Shape_1.Shape(internalStub, this.id, this.pkg, this.modifier);
114
+ }
115
+ /**
116
+ * Creates a Swimlane Pool (which is technically a Vertical List of Containers).
117
+ * @param props Visual properties
118
+ */
119
+ async addSwimlanePool(props) {
120
+ // A Pool is just a vertical list
121
+ return this.addList(props, 'vertical');
122
+ }
123
+ /**
124
+ * Creates a Swimlane Lane (which is technically a Container).
125
+ * @param props Visual properties
126
+ */
127
+ async addSwimlaneLane(props) {
128
+ // A Lane is just a container
129
+ return this.addContainer(props);
112
130
  }
113
131
  async addTable(x, y, title, columns) {
114
132
  // ... (previous implementation)
@@ -163,7 +181,7 @@ class Page {
163
181
  }
164
182
  async addLayer(name, options) {
165
183
  const info = await this.modifier.addLayer(this.id, name, options);
166
- return new Layer_1.Layer(info.name, info.index, this.id, this.pkg);
184
+ return new Layer_1.Layer(info.name, info.index, this.id, this.pkg, this.modifier);
167
185
  }
168
186
  }
169
187
  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 {
@@ -13,7 +13,8 @@ export declare class Shape {
13
13
  private internalShape;
14
14
  private pageId;
15
15
  private pkg;
16
- constructor(internalShape: VisioShape, pageId: string, pkg: VisioPackage);
16
+ private modifier;
17
+ constructor(internalShape: VisioShape, pageId: string, pkg: VisioPackage, modifier?: ShapeModifier);
17
18
  get id(): string;
18
19
  get name(): string;
19
20
  get text(): string;
@@ -65,4 +66,5 @@ export declare class Shape {
65
66
  * Alias for assignLayer. Adds this shape to a layer.
66
67
  */
67
68
  addToLayer(layer: Layer | number): Promise<this>;
69
+ private setLocalCoord;
68
70
  }
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() {
@@ -37,64 +40,43 @@ class Shape {
37
40
  return this.internalShape.Cells['PinY'] ? Number(this.internalShape.Cells['PinY'].V) : 0;
38
41
  }
39
42
  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);
43
+ await this.modifier.addConnector(this.pageId, this.id, targetShape.id, beginArrow, endArrow);
42
44
  return this;
43
45
  }
44
46
  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.
47
+ await this.modifier.updateShapeStyle(this.pageId, this.id, style);
49
48
  return this;
50
49
  }
51
50
  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' };
51
+ // PinX is the shape centre, so right edge of target = target.x + target.width/2;
52
+ // left edge of this = newX - this.width/2. Set left edge = right edge of target + gap.
53
+ const newX = targetShape.x + (targetShape.width / 2) + options.gap + (this.width / 2);
54
+ const newY = targetShape.y; // Align centres vertically
55
+ await this.modifier.updateShapePosition(this.pageId, this.id, newX, newY);
56
+ // Update local state — rounded to avoid float precision drift in chained placements
57
+ this.setLocalCoord('PinX', newX);
58
+ this.setLocalCoord('PinY', newY);
65
59
  return this;
66
60
  }
67
61
  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
62
+ const newX = targetShape.x; // Align centres horizontally
63
+ // Target bottom edge = target.y - target.height/2
64
+ // This centre = target bottom - gap - this.height/2
73
65
  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' };
66
+ await this.modifier.updateShapePosition(this.pageId, this.id, newX, newY);
67
+ this.setLocalCoord('PinX', newX);
68
+ this.setLocalCoord('PinY', newY);
84
69
  return this;
85
70
  }
86
71
  addPropertyDefinition(name, type, options = {}) {
87
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
88
- modifier.addPropertyDefinition(this.pageId, this.id, name, type, options);
72
+ this.modifier.addPropertyDefinition(this.pageId, this.id, name, type, options);
89
73
  return this;
90
74
  }
91
75
  setPropertyValue(name, value) {
92
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
93
- modifier.setPropertyValue(this.pageId, this.id, name, value);
76
+ this.modifier.setPropertyValue(this.pageId, this.id, name, value);
94
77
  return this;
95
78
  }
96
79
  addData(key, data) {
97
- // Auto-detect type if not provided
98
80
  let type = data.type;
99
81
  if (type === undefined) {
100
82
  if (data.value instanceof Date) {
@@ -110,59 +92,37 @@ class Shape {
110
92
  type = VisioTypes_1.VisioPropType.String;
111
93
  }
112
94
  }
113
- // 1. Define Property
114
- this.addPropertyDefinition(key, type, {
115
- label: data.label,
116
- invisible: data.hidden
117
- });
118
- // 2. Set Value
95
+ this.addPropertyDefinition(key, type, { label: data.label, invisible: data.hidden });
119
96
  this.setPropertyValue(key, data.value);
120
97
  return this;
121
98
  }
122
99
  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');
100
+ await this.modifier.addRelationship(this.pageId, this.id, memberShape.id, 'Container');
126
101
  return this;
127
102
  }
128
103
  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)
104
+ await this.modifier.addListItem(this.pageId, this.id, item.id);
132
105
  await this.refreshLocalState();
133
106
  return this;
134
107
  }
135
108
  async resizeToFit(padding = 0.25) {
136
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
137
- await modifier.resizeContainerToFit(this.pageId, this.id, padding);
109
+ await this.modifier.resizeContainerToFit(this.pageId, this.id, padding);
138
110
  await this.refreshLocalState();
139
111
  return this;
140
112
  }
141
113
  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());
114
+ const geo = this.modifier.getShapeGeometry(this.pageId, this.id);
115
+ this.setLocalCoord('PinX', geo.x);
116
+ this.setLocalCoord('PinY', geo.y);
117
+ this.setLocalCoord('Width', geo.width);
118
+ this.setLocalCoord('Height', geo.height);
156
119
  }
157
120
  async addHyperlink(address, description) {
158
- const modifier = new ShapeModifier_1.ShapeModifier(this.pkg);
159
- await modifier.addHyperlink(this.pageId, this.id, { address, description });
121
+ await this.modifier.addHyperlink(this.pageId, this.id, { address, description });
160
122
  return this;
161
123
  }
162
124
  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, {
125
+ await this.modifier.addHyperlink(this.pageId, this.id, {
166
126
  address: '',
167
127
  subAddress: targetPage.name,
168
128
  description
@@ -189,8 +149,7 @@ class Shape {
189
149
  }
190
150
  async assignLayer(layer) {
191
151
  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);
152
+ await this.modifier.assignLayer(this.pageId, this.id, index);
194
153
  return this;
195
154
  }
196
155
  /**
@@ -199,5 +158,12 @@ class Shape {
199
158
  async addToLayer(layer) {
200
159
  return this.assignLayer(layer);
201
160
  }
161
+ setLocalCoord(name, value) {
162
+ const v = fmtCoord(value);
163
+ if (this.internalShape.Cells[name])
164
+ this.internalShape.Cells[name].V = v;
165
+ else
166
+ this.internalShape.Cells[name] = { V: v, N: name };
167
+ }
202
168
  }
203
169
  exports.Shape = Shape;
@@ -9,9 +9,18 @@ export declare class ShapeModifier {
9
9
  private relsManager;
10
10
  private pageCache;
11
11
  private dirtyPages;
12
+ private shapeCache;
13
+ private pagePathRegistry;
12
14
  autoSave: boolean;
15
+ /**
16
+ * Register the resolved OPC part path for a page ID.
17
+ * Must be called before any operation on a loaded file to ensure the
18
+ * correct file is targeted rather than the ID-derived fallback name.
19
+ */
20
+ registerPage(pageId: string, xmlPath: string): void;
13
21
  constructor(pkg: VisioPackage);
14
22
  private getPagePath;
23
+ private getShapeMap;
15
24
  private getAllShapes;
16
25
  private getNextId;
17
26
  private ensurePageSheet;