ts-visio 1.5.0 → 1.7.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
@@ -25,6 +25,8 @@ Built using specific schema-level abstractions to handle the complex internal st
25
25
  - **Read-Back API**: Read custom properties, hyperlinks, and layer assignments from existing shapes.
26
26
  - **Page Size & Orientation**: Set canvas dimensions with named sizes (`Letter`, `A4`, …) or raw inches; rotate between portrait and landscape.
27
27
  - **Document Metadata**: Read and write document properties (title, author, description, keywords, company, dates) via `doc.getMetadata()` / `doc.setMetadata()`.
28
+ - **Named Connection Points**: Define specific ports on shapes (`Top`, `Right`, etc.) and connect to them precisely using `fromPort`/`toPort` on any connector API.
29
+ - **StyleSheets**: Create document-level named styles with fill, line, and text properties via `doc.createStyle()` and apply them to shapes at creation time (`styleId`) or post-creation (`shape.applyStyle()`).
28
30
 
29
31
  Feature gaps are being tracked in [FEATURES.md](./FEATURES.md).
30
32
 
@@ -558,6 +560,112 @@ await shape.setStyle({
558
560
 
559
561
  ---
560
562
 
563
+ #### 28. Named Connection Points
564
+ Define specific ports on shapes and connect to them precisely instead of relying on edge-intersection.
565
+
566
+ ```typescript
567
+ import { StandardConnectionPoints } from 'ts-visio';
568
+
569
+ // 1. Add connection points at shape-creation time
570
+ const nodeA = await page.addShape({
571
+ text: 'A', x: 2, y: 3, width: 2, height: 1,
572
+ connectionPoints: StandardConnectionPoints.cardinal, // Top, Right, Bottom, Left
573
+ });
574
+
575
+ const nodeB = await page.addShape({
576
+ text: 'B', x: 6, y: 3, width: 2, height: 1,
577
+ connectionPoints: StandardConnectionPoints.cardinal,
578
+ });
579
+
580
+ // 2. Connect using named ports (Right of A → Left of B)
581
+ await page.connectShapes(nodeA, nodeB, undefined, undefined, undefined,
582
+ { name: 'Right' }, // fromPort
583
+ { name: 'Left' }, // toPort
584
+ );
585
+
586
+ // 3. Fluent Shape API
587
+ await nodeA.connectTo(nodeB, undefined, undefined, undefined,
588
+ { name: 'Right' }, { name: 'Left' });
589
+
590
+ // 4. Add a point to an existing shape by index
591
+ const ix = nodeA.addConnectionPoint({
592
+ name: 'Center',
593
+ xFraction: 0.5, yFraction: 0.5,
594
+ type: 'both',
595
+ });
596
+
597
+ // 5. Connect by zero-based index instead of name
598
+ await page.connectShapes(nodeA, nodeB, undefined, undefined, undefined,
599
+ { index: 1 }, // Right (IX=1 in cardinal preset)
600
+ { index: 3 }, // Left (IX=3 in cardinal preset)
601
+ );
602
+
603
+ // 6. 'center' target (default behaviour) works alongside named ports
604
+ await page.connectShapes(nodeA, nodeB, undefined, undefined, undefined,
605
+ 'center', { name: 'Left' });
606
+ ```
607
+
608
+ `StandardConnectionPoints.cardinal` — 4 points: `Top`, `Right`, `Bottom`, `Left`.
609
+ `StandardConnectionPoints.full` — 8 points: cardinal + `TopLeft`, `TopRight`, `BottomRight`, `BottomLeft`.
610
+ Unknown port names fall back gracefully to edge-intersection routing without throwing.
611
+
612
+ ---
613
+
614
+ #### 29. StyleSheets (Document-Level Styles)
615
+ Define reusable named styles at the document level and apply them to shapes so they inherit line, fill, and text properties without repeating the same values on every shape.
616
+
617
+ ```typescript
618
+ // 1. Create a document-level style
619
+ const headerStyle = doc.createStyle('Header', {
620
+ fillColor: '#4472C4', // Blue fill
621
+ lineColor: '#2F5597', // Dark-blue border
622
+ lineWeight: 1.5, // 1.5 pt stroke
623
+ fontColor: '#FFFFFF', // White text
624
+ fontSize: 14, // 14 pt
625
+ bold: true,
626
+ fontFamily: 'Calibri',
627
+ horzAlign: 'center',
628
+ verticalAlign: 'middle',
629
+ });
630
+
631
+ const bodyStyle = doc.createStyle('Body', {
632
+ fillColor: '#DEEAF1',
633
+ lineColor: '#2F5597',
634
+ fontSize: 11,
635
+ horzAlign: 'left',
636
+ });
637
+
638
+ // 2. Apply at shape-creation time
639
+ const title = await page.addShape({
640
+ text: 'System Architecture',
641
+ x: 1, y: 8, width: 8, height: 1,
642
+ styleId: headerStyle.id, // sets LineStyle, FillStyle, TextStyle
643
+ });
644
+
645
+ // Fine-grained: apply only the fill from one style, line from another
646
+ const hybrid = await page.addShape({
647
+ text: 'Hybrid',
648
+ x: 1, y: 6, width: 3, height: 1,
649
+ fillStyleId: headerStyle.id,
650
+ lineStyleId: bodyStyle.id,
651
+ });
652
+
653
+ // 3. Apply (or change) style post-creation
654
+ const box = await page.addShape({ text: 'Server', x: 4, y: 4, width: 2, height: 1 });
655
+ box.applyStyle(bodyStyle.id); // all three categories
656
+ box.applyStyle(headerStyle.id, 'fill'); // fill only — leaves line & text unchanged
657
+ box.applyStyle(headerStyle.id, 'text'); // text only
658
+
659
+ // 4. List all styles in the document
660
+ const styles = doc.getStyles();
661
+ // [ { id: 0, name: 'No Style' }, { id: 1, name: 'Normal' }, { id: 2, name: 'Header' }, … ]
662
+ ```
663
+
664
+ `StyleProps` supports: `fillColor`, `lineColor`, `lineWeight` (pt), `linePattern`, `fontColor`, `fontSize` (pt), `bold`, `italic`, `underline`, `strikethrough`, `fontFamily`, `horzAlign`, `verticalAlign`, `spaceBefore`, `spaceAfter`, `lineSpacing`, `textMarginTop/Bottom/Left/Right` (in).
665
+ Local shape properties always override inherited stylesheet values.
666
+
667
+ ---
668
+
561
669
  ## Examples
562
670
 
563
671
  Check out the [examples](./examples) directory for complete scripts.
package/dist/Page.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { VisioPage, ConnectorStyle, PageOrientation, PageSizeName } from './types/VisioTypes';
1
+ import { VisioPage, ConnectorStyle, PageOrientation, PageSizeName, ConnectionTarget } from './types/VisioTypes';
2
2
  import { VisioPackage } from './VisioPackage';
3
3
  import { ShapeModifier } from './ShapeModifier';
4
4
  import { NewShapeProps } from './types/VisioTypes';
@@ -50,7 +50,7 @@ export declare class Page {
50
50
  */
51
51
  findShapes(predicate: (shape: Shape) => boolean): Shape[];
52
52
  addShape(props: NewShapeProps, parentId?: string): Promise<Shape>;
53
- connectShapes(fromShape: Shape, toShape: Shape, beginArrow?: string, endArrow?: string, style?: ConnectorStyle): Promise<void>;
53
+ connectShapes(fromShape: Shape, toShape: Shape, beginArrow?: string, endArrow?: string, style?: ConnectorStyle, fromPort?: ConnectionTarget, toPort?: ConnectionTarget): Promise<void>;
54
54
  addImage(data: Buffer, name: string, x: number, y: number, width: number, height: number): Promise<Shape>;
55
55
  addContainer(props: NewShapeProps): Promise<Shape>;
56
56
  addList(props: NewShapeProps, direction?: 'vertical' | 'horizontal'): Promise<Shape>;
package/dist/Page.js CHANGED
@@ -124,8 +124,8 @@ class Page {
124
124
  });
125
125
  return new Shape_1.Shape(internalStub, this.id, this.pkg, this.modifier);
126
126
  }
127
- async connectShapes(fromShape, toShape, beginArrow, endArrow, style) {
128
- await this.modifier.addConnector(this.id, fromShape.id, toShape.id, beginArrow, endArrow, style);
127
+ async connectShapes(fromShape, toShape, beginArrow, endArrow, style, fromPort, toPort) {
128
+ await this.modifier.addConnector(this.id, fromShape.id, toShape.id, beginArrow, endArrow, style, fromPort, toPort);
129
129
  }
130
130
  async addImage(data, name, x, y, width, height) {
131
131
  // 1. Upload Media
package/dist/Shape.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { VisioShape, ConnectorStyle } from './types/VisioTypes';
1
+ import { VisioShape, ConnectorStyle, ConnectionTarget, ConnectionPointDef } from './types/VisioTypes';
2
2
  import { VisioPackage } from './VisioPackage';
3
3
  import { ShapeModifier, ShapeStyle } from './ShapeModifier';
4
4
  import { VisioPropType } from './types/VisioTypes';
@@ -30,7 +30,21 @@ export declare class Shape {
30
30
  get x(): number;
31
31
  get y(): number;
32
32
  delete(): Promise<void>;
33
- connectTo(targetShape: Shape, beginArrow?: string, endArrow?: string, style?: ConnectorStyle): Promise<this>;
33
+ connectTo(targetShape: Shape, beginArrow?: string, endArrow?: string, style?: ConnectorStyle, fromPort?: ConnectionTarget, toPort?: ConnectionTarget): Promise<this>;
34
+ /**
35
+ * Add a connection point to this shape.
36
+ * Returns the zero-based index (IX) of the newly added point.
37
+ */
38
+ addConnectionPoint(point: ConnectionPointDef): number;
39
+ /**
40
+ * Apply a document-level stylesheet to this shape.
41
+ * Create styles via `doc.createStyle()` and pass the returned `id`.
42
+ *
43
+ * @param styleId The stylesheet ID to apply.
44
+ * @param which `'all'` (default) applies to line, fill, and text;
45
+ * `'line'`, `'fill'`, or `'text'` applies to only that category.
46
+ */
47
+ applyStyle(styleId: number, which?: 'all' | 'line' | 'fill' | 'text'): this;
34
48
  setStyle(style: ShapeStyle): Promise<this>;
35
49
  placeRightOf(targetShape: Shape, options?: {
36
50
  gap: number;
package/dist/Shape.js CHANGED
@@ -42,8 +42,27 @@ class Shape {
42
42
  async delete() {
43
43
  await this.modifier.deleteShape(this.pageId, this.id);
44
44
  }
45
- async connectTo(targetShape, beginArrow, endArrow, style) {
46
- await this.modifier.addConnector(this.pageId, this.id, targetShape.id, beginArrow, endArrow, style);
45
+ async connectTo(targetShape, beginArrow, endArrow, style, fromPort, toPort) {
46
+ await this.modifier.addConnector(this.pageId, this.id, targetShape.id, beginArrow, endArrow, style, fromPort, toPort);
47
+ return this;
48
+ }
49
+ /**
50
+ * Add a connection point to this shape.
51
+ * Returns the zero-based index (IX) of the newly added point.
52
+ */
53
+ addConnectionPoint(point) {
54
+ return this.modifier.addConnectionPoint(this.pageId, this.id, point);
55
+ }
56
+ /**
57
+ * Apply a document-level stylesheet to this shape.
58
+ * Create styles via `doc.createStyle()` and pass the returned `id`.
59
+ *
60
+ * @param styleId The stylesheet ID to apply.
61
+ * @param which `'all'` (default) applies to line, fill, and text;
62
+ * `'line'`, `'fill'`, or `'text'` applies to only that category.
63
+ */
64
+ applyStyle(styleId, which = 'all') {
65
+ this.modifier.applyStyle(this.pageId, this.id, styleId, which);
47
66
  return this;
48
67
  }
49
68
  async setStyle(style) {
@@ -1,6 +1,6 @@
1
1
  import { VisioPackage } from './VisioPackage';
2
2
  import { HorzAlign, VertAlign } from './utils/StyleHelpers';
3
- import { NewShapeProps, ConnectorStyle } from './types/VisioTypes';
3
+ import { NewShapeProps, ConnectorStyle, ConnectionTarget, ConnectionPointDef } from './types/VisioTypes';
4
4
  import type { ShapeData, ShapeHyperlink } from './Shape';
5
5
  export declare class ShapeModifier {
6
6
  private pkg;
@@ -31,7 +31,19 @@ export declare class ShapeModifier {
31
31
  private saveParsed;
32
32
  private performSave;
33
33
  flush(): void;
34
- addConnector(pageId: string, fromShapeId: string, toShapeId: string, beginArrow?: string, endArrow?: string, style?: ConnectorStyle): Promise<string>;
34
+ addConnector(pageId: string, fromShapeId: string, toShapeId: string, beginArrow?: string, endArrow?: string, style?: ConnectorStyle, fromPort?: ConnectionTarget, toPort?: ConnectionTarget): Promise<string>;
35
+ /**
36
+ * Add a single connection point to an existing shape.
37
+ * Returns the zero-based IX (row index) of the newly added point.
38
+ */
39
+ addConnectionPoint(pageId: string, shapeId: string, point: ConnectionPointDef): number;
40
+ /**
41
+ * Apply a document-level stylesheet to an existing shape by setting its
42
+ * `LineStyle`, `FillStyle`, and/or `TextStyle` attributes.
43
+ *
44
+ * @param which `'all'` (default) sets all three; `'line'`, `'fill'`, or `'text'` sets only one.
45
+ */
46
+ applyStyle(pageId: string, shapeId: string, styleId: number, which?: 'all' | 'line' | 'fill' | 'text'): void;
35
47
  addShape(pageId: string, props: NewShapeProps, parentId?: string): Promise<string>;
36
48
  deleteShape(pageId: string, shapeId: string): Promise<void>;
37
49
  private removeShapeFromTree;
@@ -5,6 +5,7 @@ const RelsManager_1 = require("./core/RelsManager");
5
5
  const StyleHelpers_1 = require("./utils/StyleHelpers");
6
6
  const VisioConstants_1 = require("./core/VisioConstants");
7
7
  const VisioTypes_1 = require("./types/VisioTypes");
8
+ const ConnectionPointBuilder_1 = require("./shapes/ConnectionPointBuilder");
8
9
  const ForeignShapeBuilder_1 = require("./shapes/ForeignShapeBuilder");
9
10
  const ShapeBuilder_1 = require("./shapes/ShapeBuilder");
10
11
  const ConnectorBuilder_1 = require("./shapes/ConnectorBuilder");
@@ -184,7 +185,7 @@ class ShapeModifier {
184
185
  }
185
186
  this.dirtyPages.clear();
186
187
  }
187
- async addConnector(pageId, fromShapeId, toShapeId, beginArrow, endArrow, style) {
188
+ async addConnector(pageId, fromShapeId, toShapeId, beginArrow, endArrow, style, fromPort, toPort) {
188
189
  const parsed = this.getParsed(pageId);
189
190
  // Ensure Shapes collection exists
190
191
  if (!parsed.PageContents.Shapes) {
@@ -204,15 +205,66 @@ class ShapeModifier {
204
205
  return '0';
205
206
  return val;
206
207
  };
207
- const layout = ConnectorBuilder_1.ConnectorBuilder.calculateConnectorLayout(fromShapeId, toShapeId, shapeHierarchy);
208
+ const layout = ConnectorBuilder_1.ConnectorBuilder.calculateConnectorLayout(fromShapeId, toShapeId, shapeHierarchy, fromPort, toPort);
208
209
  const connectorShape = ConnectorBuilder_1.ConnectorBuilder.createConnectorShapeObject(newId, layout, validateArrow(beginArrow), validateArrow(endArrow), style);
209
210
  const topLevelShapes = parsed.PageContents.Shapes.Shape;
210
211
  topLevelShapes.push(connectorShape);
211
212
  this.getShapeMap(parsed).set(newId, connectorShape);
212
- ConnectorBuilder_1.ConnectorBuilder.addConnectorToConnects(parsed, newId, fromShapeId, toShapeId);
213
+ ConnectorBuilder_1.ConnectorBuilder.addConnectorToConnects(parsed, newId, fromShapeId, toShapeId, shapeHierarchy, fromPort, toPort);
213
214
  this.saveParsed(pageId, parsed);
214
215
  return newId;
215
216
  }
217
+ /**
218
+ * Add a single connection point to an existing shape.
219
+ * Returns the zero-based IX (row index) of the newly added point.
220
+ */
221
+ addConnectionPoint(pageId, shapeId, point) {
222
+ const parsed = this.getParsed(pageId);
223
+ const shapeMap = this.getShapeMap(parsed);
224
+ const shape = shapeMap.get(shapeId);
225
+ if (!shape)
226
+ throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
227
+ // Ensure Section array exists
228
+ if (!shape.Section)
229
+ shape.Section = [];
230
+ if (!Array.isArray(shape.Section))
231
+ shape.Section = [shape.Section];
232
+ // Find or create Connection section
233
+ let connSection = shape.Section.find((s) => s['@_N'] === 'Connection');
234
+ if (!connSection) {
235
+ connSection = { '@_N': 'Connection', Row: [] };
236
+ shape.Section.push(connSection);
237
+ }
238
+ if (!connSection.Row)
239
+ connSection.Row = [];
240
+ if (!Array.isArray(connSection.Row))
241
+ connSection.Row = [connSection.Row];
242
+ const ix = connSection.Row.length;
243
+ connSection.Row.push(ConnectionPointBuilder_1.ConnectionPointBuilder.buildRow(point, ix));
244
+ this.saveParsed(pageId, parsed);
245
+ return ix;
246
+ }
247
+ /**
248
+ * Apply a document-level stylesheet to an existing shape by setting its
249
+ * `LineStyle`, `FillStyle`, and/or `TextStyle` attributes.
250
+ *
251
+ * @param which `'all'` (default) sets all three; `'line'`, `'fill'`, or `'text'` sets only one.
252
+ */
253
+ applyStyle(pageId, shapeId, styleId, which = 'all') {
254
+ const parsed = this.getParsed(pageId);
255
+ const shapeMap = this.getShapeMap(parsed);
256
+ const shape = shapeMap.get(shapeId);
257
+ if (!shape)
258
+ throw new Error(`Shape ${shapeId} not found on page ${pageId}`);
259
+ const sid = styleId.toString();
260
+ if (which === 'all' || which === 'line')
261
+ shape['@_LineStyle'] = sid;
262
+ if (which === 'all' || which === 'fill')
263
+ shape['@_FillStyle'] = sid;
264
+ if (which === 'all' || which === 'text')
265
+ shape['@_TextStyle'] = sid;
266
+ this.saveParsed(pageId, parsed);
267
+ }
216
268
  async addShape(pageId, props, parentId) {
217
269
  const parsed = this.getParsed(pageId);
218
270
  // Ensure Shapes container exists
@@ -1,11 +1,12 @@
1
1
  import { Page } from './Page';
2
- import { DocumentMetadata } from './types/VisioTypes';
2
+ import { DocumentMetadata, StyleProps, StyleRecord } from './types/VisioTypes';
3
3
  export declare class VisioDocument {
4
4
  private pkg;
5
5
  private pageManager;
6
6
  private _pageCache;
7
7
  private mediaManager;
8
8
  private metadataManager;
9
+ private styleSheetManager;
9
10
  private constructor();
10
11
  static create(): Promise<VisioDocument>;
11
12
  static load(pathOrBuffer: string | Buffer | ArrayBuffer | Uint8Array): Promise<VisioDocument>;
@@ -42,5 +43,18 @@ export declare class VisioDocument {
42
43
  * doc.setMetadata({ title: 'My Diagram', author: 'Alice', company: 'ACME' });
43
44
  */
44
45
  setMetadata(props: Partial<DocumentMetadata>): void;
46
+ /**
47
+ * Create a named document-level stylesheet and return its record.
48
+ * The returned `id` can be passed to `addShape({ styleId })` or `shape.applyStyle()`.
49
+ *
50
+ * @example
51
+ * const s = doc.createStyle('Header', { fillColor: '#4472C4', fontColor: '#ffffff', bold: true });
52
+ * const shape = await page.addShape({ text: 'Title', x: 1, y: 1, width: 3, height: 1, styleId: s.id });
53
+ */
54
+ createStyle(name: string, props?: StyleProps): StyleRecord;
55
+ /**
56
+ * Return all stylesheets defined in the document (including built-in styles).
57
+ */
58
+ getStyles(): StyleRecord[];
45
59
  save(filename?: string): Promise<Buffer>;
46
60
  }
@@ -39,6 +39,7 @@ const PageManager_1 = require("./core/PageManager");
39
39
  const Page_1 = require("./Page");
40
40
  const MediaManager_1 = require("./core/MediaManager");
41
41
  const MetadataManager_1 = require("./core/MetadataManager");
42
+ const StyleSheetManager_1 = require("./core/StyleSheetManager");
42
43
  class VisioDocument {
43
44
  constructor(pkg) {
44
45
  this.pkg = pkg;
@@ -46,6 +47,7 @@ class VisioDocument {
46
47
  this.pageManager = new PageManager_1.PageManager(pkg);
47
48
  this.mediaManager = new MediaManager_1.MediaManager(pkg);
48
49
  this.metadataManager = new MetadataManager_1.MetadataManager(pkg);
50
+ this.styleSheetManager = new StyleSheetManager_1.StyleSheetManager(pkg);
49
51
  }
50
52
  static async create() {
51
53
  const pkg = await VisioPackage_1.VisioPackage.create();
@@ -150,6 +152,23 @@ class VisioDocument {
150
152
  setMetadata(props) {
151
153
  this.metadataManager.setMetadata(props);
152
154
  }
155
+ /**
156
+ * Create a named document-level stylesheet and return its record.
157
+ * The returned `id` can be passed to `addShape({ styleId })` or `shape.applyStyle()`.
158
+ *
159
+ * @example
160
+ * const s = doc.createStyle('Header', { fillColor: '#4472C4', fontColor: '#ffffff', bold: true });
161
+ * const shape = await page.addShape({ text: 'Title', x: 1, y: 1, width: 3, height: 1, styleId: s.id });
162
+ */
163
+ createStyle(name, props = {}) {
164
+ return this.styleSheetManager.createStyle(name, props);
165
+ }
166
+ /**
167
+ * Return all stylesheets defined in the document (including built-in styles).
168
+ */
169
+ getStyles() {
170
+ return this.styleSheetManager.getStyles();
171
+ }
153
172
  async save(filename) {
154
173
  return this.pkg.save(filename);
155
174
  }
@@ -0,0 +1,37 @@
1
+ import { VisioPackage } from '../VisioPackage';
2
+ import { StyleProps, StyleRecord } from '../types/VisioTypes';
3
+ /**
4
+ * Manages document-level stylesheets stored in `visio/document.xml`.
5
+ *
6
+ * Stylesheets define reusable sets of line, fill, and text properties that
7
+ * shapes inherit via the `LineStyle`, `FillStyle`, and `TextStyle` attributes.
8
+ *
9
+ * The minimal document template ships with:
10
+ * - ID 0 "No Style" — the inheritance root (all shapes fall back here)
11
+ * - ID 1 "Normal" — empty style inheriting everything from ID 0
12
+ *
13
+ * User-created styles receive IDs starting at 2.
14
+ */
15
+ export declare class StyleSheetManager {
16
+ private pkg;
17
+ private parser;
18
+ private builder;
19
+ constructor(pkg: VisioPackage);
20
+ /**
21
+ * Create a new named document-level stylesheet and return its record.
22
+ * The returned `id` can be passed to `addShape({ styleId })` or `shape.applyStyle()`.
23
+ */
24
+ createStyle(name: string, props?: StyleProps): StyleRecord;
25
+ /**
26
+ * Return all stylesheets defined in the document (including the built-in ones).
27
+ */
28
+ getStyles(): StyleRecord[];
29
+ private getParsedDoc;
30
+ private saveParsedDoc;
31
+ /** Ensure `<StyleSheets>` contains at least Style 0 and Style 1. */
32
+ private ensureStyleSheets;
33
+ private nextId;
34
+ /** Normalise a fast-xml-parser value that may be undefined, a single object, or an array. */
35
+ private normalizeArray;
36
+ private buildStyleSheetXml;
37
+ }
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StyleSheetManager = void 0;
4
+ const StyleHelpers_1 = require("../utils/StyleHelpers");
5
+ const XmlHelper_1 = require("../utils/XmlHelper");
6
+ const HORZ_ALIGN = {
7
+ left: '0', center: '1', right: '2', justify: '3',
8
+ };
9
+ const VERT_ALIGN = {
10
+ top: '0', middle: '1', bottom: '2',
11
+ };
12
+ /**
13
+ * Manages document-level stylesheets stored in `visio/document.xml`.
14
+ *
15
+ * Stylesheets define reusable sets of line, fill, and text properties that
16
+ * shapes inherit via the `LineStyle`, `FillStyle`, and `TextStyle` attributes.
17
+ *
18
+ * The minimal document template ships with:
19
+ * - ID 0 "No Style" — the inheritance root (all shapes fall back here)
20
+ * - ID 1 "Normal" — empty style inheriting everything from ID 0
21
+ *
22
+ * User-created styles receive IDs starting at 2.
23
+ */
24
+ class StyleSheetManager {
25
+ constructor(pkg) {
26
+ this.pkg = pkg;
27
+ this.parser = (0, XmlHelper_1.createXmlParser)();
28
+ this.builder = (0, XmlHelper_1.createXmlBuilder)();
29
+ }
30
+ // ── Public API ────────────────────────────────────────────────────────────
31
+ /**
32
+ * Create a new named document-level stylesheet and return its record.
33
+ * The returned `id` can be passed to `addShape({ styleId })` or `shape.applyStyle()`.
34
+ */
35
+ createStyle(name, props = {}) {
36
+ const parsed = this.getParsedDoc();
37
+ const doc = parsed['VisioDocument'];
38
+ this.ensureStyleSheets(doc);
39
+ const styleSheets = doc['StyleSheets'];
40
+ const existing = this.normalizeArray(styleSheets['StyleSheet']);
41
+ const newId = this.nextId(existing);
42
+ existing.push(this.buildStyleSheetXml(newId, name, props));
43
+ styleSheets['StyleSheet'] = existing;
44
+ this.saveParsedDoc(parsed);
45
+ return { id: newId, name };
46
+ }
47
+ /**
48
+ * Return all stylesheets defined in the document (including the built-in ones).
49
+ */
50
+ getStyles() {
51
+ const parsed = this.getParsedDoc();
52
+ const doc = parsed['VisioDocument'];
53
+ const styleSheets = doc?.['StyleSheets'];
54
+ if (!styleSheets)
55
+ return [];
56
+ return this.normalizeArray(styleSheets['StyleSheet']).map((s) => ({
57
+ id: parseInt(s['@_ID'], 10),
58
+ name: s['@_Name'] ?? s['@_NameU'] ?? `Style ${s['@_ID']}`,
59
+ }));
60
+ }
61
+ // ── Private helpers ───────────────────────────────────────────────────────
62
+ getParsedDoc() {
63
+ const xml = this.pkg.getFileText('visio/document.xml');
64
+ return this.parser.parse(xml);
65
+ }
66
+ saveParsedDoc(parsed) {
67
+ this.pkg.updateFile('visio/document.xml', (0, XmlHelper_1.buildXml)(this.builder, parsed));
68
+ }
69
+ /** Ensure `<StyleSheets>` contains at least Style 0 and Style 1. */
70
+ ensureStyleSheets(doc) {
71
+ if (!doc['StyleSheets']) {
72
+ doc['StyleSheets'] = {};
73
+ }
74
+ const ss = doc['StyleSheets'];
75
+ const existing = this.normalizeArray(ss['StyleSheet']);
76
+ const hasStyle0 = existing.some((s) => s['@_ID'] === '0');
77
+ if (!hasStyle0) {
78
+ existing.unshift({
79
+ '@_ID': '0',
80
+ '@_Name': 'No Style',
81
+ '@_NameU': 'No Style',
82
+ '@_IsCustomName': '0',
83
+ '@_IsCustomNameU': '0',
84
+ Cell: [
85
+ { '@_N': 'EnableLineProps', '@_V': '1' },
86
+ { '@_N': 'EnableFillProps', '@_V': '1' },
87
+ { '@_N': 'EnableTextProps', '@_V': '1' },
88
+ { '@_N': 'HideForApply', '@_V': '0' },
89
+ ],
90
+ });
91
+ }
92
+ const hasStyle1 = existing.some((s) => s['@_ID'] === '1');
93
+ if (!hasStyle1) {
94
+ existing.push({
95
+ '@_ID': '1',
96
+ '@_Name': 'Normal',
97
+ '@_NameU': 'Normal',
98
+ '@_IsCustomName': '0',
99
+ '@_IsCustomNameU': '0',
100
+ '@_LineStyle': '0',
101
+ '@_FillStyle': '0',
102
+ '@_TextStyle': '0',
103
+ });
104
+ }
105
+ ss['StyleSheet'] = existing;
106
+ }
107
+ nextId(existing) {
108
+ if (existing.length === 0)
109
+ return 2;
110
+ const max = existing.reduce((m, s) => {
111
+ const id = parseInt(s['@_ID'], 10);
112
+ return isNaN(id) ? m : Math.max(m, id);
113
+ }, 0);
114
+ return Math.max(max + 1, 2); // user styles always start at ≥ 2
115
+ }
116
+ /** Normalise a fast-xml-parser value that may be undefined, a single object, or an array. */
117
+ normalizeArray(val) {
118
+ if (!val)
119
+ return [];
120
+ return Array.isArray(val) ? val : [val];
121
+ }
122
+ buildStyleSheetXml(id, name, props) {
123
+ const sheet = {
124
+ '@_ID': id.toString(),
125
+ '@_Name': name,
126
+ '@_NameU': name,
127
+ '@_IsCustomName': '1',
128
+ '@_IsCustomNameU': '1',
129
+ '@_LineStyle': (props.parentLineStyleId ?? 0).toString(),
130
+ '@_FillStyle': (props.parentFillStyleId ?? 0).toString(),
131
+ '@_TextStyle': (props.parentTextStyleId ?? 0).toString(),
132
+ };
133
+ const cells = [];
134
+ const sections = [];
135
+ // ── Fill ────────────────────────────────────────────────────────────
136
+ if (props.fillColor !== undefined) {
137
+ cells.push({ '@_N': 'FillForegnd', '@_V': props.fillColor, '@_F': (0, StyleHelpers_1.hexToRgb)(props.fillColor) });
138
+ cells.push({ '@_N': 'FillPattern', '@_V': '1' });
139
+ }
140
+ // ── Line ────────────────────────────────────────────────────────────
141
+ if (props.lineColor !== undefined) {
142
+ cells.push({ '@_N': 'LineColor', '@_V': props.lineColor, '@_F': (0, StyleHelpers_1.hexToRgb)(props.lineColor) });
143
+ }
144
+ if (props.lineWeight !== undefined) {
145
+ cells.push({ '@_N': 'LineWeight', '@_V': (props.lineWeight / 72).toString(), '@_U': 'PT' });
146
+ }
147
+ if (props.linePattern !== undefined) {
148
+ cells.push({ '@_N': 'LinePattern', '@_V': props.linePattern.toString() });
149
+ }
150
+ // ── TextBlock ───────────────────────────────────────────────────────
151
+ if (props.verticalAlign !== undefined) {
152
+ cells.push({ '@_N': 'VerticalAlign', '@_V': VERT_ALIGN[props.verticalAlign] });
153
+ }
154
+ if (props.textMarginTop !== undefined)
155
+ cells.push({ '@_N': 'TopMargin', '@_V': props.textMarginTop.toString(), '@_U': 'IN' });
156
+ if (props.textMarginBottom !== undefined)
157
+ cells.push({ '@_N': 'BottomMargin', '@_V': props.textMarginBottom.toString(), '@_U': 'IN' });
158
+ if (props.textMarginLeft !== undefined)
159
+ cells.push({ '@_N': 'LeftMargin', '@_V': props.textMarginLeft.toString(), '@_U': 'IN' });
160
+ if (props.textMarginRight !== undefined)
161
+ cells.push({ '@_N': 'RightMargin', '@_V': props.textMarginRight.toString(), '@_U': 'IN' });
162
+ // ── Character section (no @_T on rows — stylesheet convention) ─────
163
+ const charCells = [];
164
+ const colorVal = props.fontColor ?? '#000000';
165
+ if (props.fontColor !== undefined) {
166
+ charCells.push({ '@_N': 'Color', '@_V': colorVal, '@_F': (0, StyleHelpers_1.hexToRgb)(colorVal) });
167
+ }
168
+ let styleVal = 0;
169
+ if (props.bold)
170
+ styleVal |= 1;
171
+ if (props.italic)
172
+ styleVal |= 2;
173
+ if (props.underline)
174
+ styleVal |= 4;
175
+ if (props.strikethrough)
176
+ styleVal |= 8;
177
+ if (styleVal > 0 || props.bold !== undefined || props.italic !== undefined
178
+ || props.underline !== undefined || props.strikethrough !== undefined) {
179
+ charCells.push({ '@_N': 'Style', '@_V': styleVal.toString() });
180
+ }
181
+ if (props.fontSize !== undefined) {
182
+ charCells.push({ '@_N': 'Size', '@_V': (props.fontSize / 72).toString(), '@_U': 'PT' });
183
+ }
184
+ if (props.fontFamily !== undefined) {
185
+ charCells.push({ '@_N': 'Font', '@_V': '0', '@_F': `FONT("${props.fontFamily}")` });
186
+ }
187
+ if (charCells.length > 0) {
188
+ sections.push({
189
+ '@_N': 'Character',
190
+ Row: { '@_IX': '0', Cell: charCells },
191
+ });
192
+ }
193
+ // ── Paragraph section ───────────────────────────────────────────────
194
+ const paraCells = [];
195
+ if (props.horzAlign !== undefined)
196
+ paraCells.push({ '@_N': 'HorzAlign', '@_V': HORZ_ALIGN[props.horzAlign] });
197
+ if (props.spaceBefore !== undefined)
198
+ paraCells.push({ '@_N': 'SpBefore', '@_V': (props.spaceBefore / 72).toString(), '@_U': 'PT' });
199
+ if (props.spaceAfter !== undefined)
200
+ paraCells.push({ '@_N': 'SpAfter', '@_V': (props.spaceAfter / 72).toString(), '@_U': 'PT' });
201
+ if (props.lineSpacing !== undefined)
202
+ paraCells.push({ '@_N': 'SpLine', '@_V': (-props.lineSpacing).toString() });
203
+ if (paraCells.length > 0) {
204
+ sections.push({
205
+ '@_N': 'Paragraph',
206
+ Row: { '@_IX': '0', Cell: paraCells },
207
+ });
208
+ }
209
+ if (cells.length > 0)
210
+ sheet.Cell = cells;
211
+ if (sections.length > 0)
212
+ sheet.Section = sections;
213
+ return sheet;
214
+ }
215
+ }
216
+ exports.StyleSheetManager = StyleSheetManager;
@@ -0,0 +1,40 @@
1
+ import { ConnectionPointDef, ConnectionTarget } from '../types/VisioTypes';
2
+ /**
3
+ * Builds and resolves Visio Connection section XML structures.
4
+ *
5
+ * Connection points in Visio are stored as rows in a `<Section N="Connection">` element.
6
+ * Each row has an IX attribute (0-based) and an optional N (name) attribute.
7
+ * X and Y cells store fractions via formula: `Width*{xFraction}` / `Height*{yFraction}`.
8
+ *
9
+ * When connecting to a named point, the Connect element uses:
10
+ * ToPart = 100 + IX (instead of 3 for shape centre)
11
+ * ToCell = "Connections.X{IX+1}" (1-based)
12
+ */
13
+ export declare class ConnectionPointBuilder {
14
+ /** Build the raw XML object for a Connection section from a list of point definitions. */
15
+ static buildConnectionSection(points: ConnectionPointDef[]): any;
16
+ /** Build a single Connection row XML object. */
17
+ static buildRow(point: ConnectionPointDef, ix: number): any;
18
+ /**
19
+ * Resolve a `ConnectionTarget` against a raw shape XML object.
20
+ *
21
+ * Returns the `ToPart` / `ToCell` strings for the Connect element, plus the
22
+ * fractional X/Y position of the connection point (undefined when targeting
23
+ * the shape centre, which falls back to edge-intersection logic).
24
+ */
25
+ static resolveTarget(target: ConnectionTarget, shape: any): {
26
+ toPart: string;
27
+ toCell: string;
28
+ xFraction?: number;
29
+ yFraction?: number;
30
+ };
31
+ private static getConnectionSection;
32
+ private static getRows;
33
+ private static findIxByName;
34
+ private static getPointByIx;
35
+ /**
36
+ * Extract the numeric multiplier from a formula of the form `Width*0.5` or `Height*1`.
37
+ * Returns `undefined` if the formula does not match that pattern.
38
+ */
39
+ private static parseFraction;
40
+ }
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectionPointBuilder = void 0;
4
+ const TYPE_VALUES = {
5
+ inward: '0',
6
+ outward: '1',
7
+ both: '2',
8
+ };
9
+ /**
10
+ * Builds and resolves Visio Connection section XML structures.
11
+ *
12
+ * Connection points in Visio are stored as rows in a `<Section N="Connection">` element.
13
+ * Each row has an IX attribute (0-based) and an optional N (name) attribute.
14
+ * X and Y cells store fractions via formula: `Width*{xFraction}` / `Height*{yFraction}`.
15
+ *
16
+ * When connecting to a named point, the Connect element uses:
17
+ * ToPart = 100 + IX (instead of 3 for shape centre)
18
+ * ToCell = "Connections.X{IX+1}" (1-based)
19
+ */
20
+ class ConnectionPointBuilder {
21
+ /** Build the raw XML object for a Connection section from a list of point definitions. */
22
+ static buildConnectionSection(points) {
23
+ return {
24
+ '@_N': 'Connection',
25
+ Row: points.map((pt, ix) => this.buildRow(pt, ix)),
26
+ };
27
+ }
28
+ /** Build a single Connection row XML object. */
29
+ static buildRow(point, ix) {
30
+ const row = {
31
+ '@_IX': ix.toString(),
32
+ Cell: [
33
+ { '@_N': 'X', '@_V': '0', '@_F': `Width*${point.xFraction}` },
34
+ { '@_N': 'Y', '@_V': '0', '@_F': `Height*${point.yFraction}` },
35
+ { '@_N': 'DirX', '@_V': point.direction ? point.direction.x.toString() : '0' },
36
+ { '@_N': 'DirY', '@_V': point.direction ? point.direction.y.toString() : '0' },
37
+ { '@_N': 'Type', '@_V': point.type ? TYPE_VALUES[point.type] : '0' },
38
+ { '@_N': 'AutoGen', '@_V': '0' },
39
+ ],
40
+ };
41
+ if (point.name)
42
+ row['@_N'] = point.name;
43
+ if (point.prompt)
44
+ row.Cell.push({ '@_N': 'Prompt', '@_V': point.prompt });
45
+ return row;
46
+ }
47
+ /**
48
+ * Resolve a `ConnectionTarget` against a raw shape XML object.
49
+ *
50
+ * Returns the `ToPart` / `ToCell` strings for the Connect element, plus the
51
+ * fractional X/Y position of the connection point (undefined when targeting
52
+ * the shape centre, which falls back to edge-intersection logic).
53
+ */
54
+ static resolveTarget(target, shape) {
55
+ if (target === 'center') {
56
+ return { toPart: '3', toCell: 'PinX' };
57
+ }
58
+ if ('index' in target) {
59
+ const ix = target.index;
60
+ const pt = this.getPointByIx(shape, ix);
61
+ return {
62
+ toPart: (100 + ix).toString(),
63
+ toCell: `Connections.X${ix + 1}`,
64
+ xFraction: pt?.xFraction,
65
+ yFraction: pt?.yFraction,
66
+ };
67
+ }
68
+ // name lookup
69
+ const ix = this.findIxByName(shape, target.name);
70
+ if (ix === -1) {
71
+ // Named point not found — fall back to centre
72
+ return { toPart: '3', toCell: 'PinX' };
73
+ }
74
+ const pt = this.getPointByIx(shape, ix);
75
+ return {
76
+ toPart: (100 + ix).toString(),
77
+ toCell: `Connections.X${ix + 1}`,
78
+ xFraction: pt?.xFraction,
79
+ yFraction: pt?.yFraction,
80
+ };
81
+ }
82
+ // ── private helpers ────────────────────────────────────────────────────────
83
+ static getConnectionSection(shape) {
84
+ const sections = shape?.Section;
85
+ if (!sections)
86
+ return null;
87
+ const arr = Array.isArray(sections) ? sections : [sections];
88
+ return arr.find((s) => s['@_N'] === 'Connection') ?? null;
89
+ }
90
+ static getRows(section) {
91
+ if (!section?.Row)
92
+ return [];
93
+ return Array.isArray(section.Row) ? section.Row : [section.Row];
94
+ }
95
+ static findIxByName(shape, name) {
96
+ const section = this.getConnectionSection(shape);
97
+ if (!section)
98
+ return -1;
99
+ const row = this.getRows(section).find((r) => r['@_N'] === name);
100
+ if (!row)
101
+ return -1;
102
+ return parseInt(row['@_IX'], 10);
103
+ }
104
+ static getPointByIx(shape, ix) {
105
+ const section = this.getConnectionSection(shape);
106
+ if (!section)
107
+ return undefined;
108
+ const row = this.getRows(section).find((r) => parseInt(r['@_IX'], 10) === ix);
109
+ if (!row)
110
+ return undefined;
111
+ const cells = Array.isArray(row.Cell) ? row.Cell : row.Cell ? [row.Cell] : [];
112
+ const xCell = cells.find((c) => c['@_N'] === 'X');
113
+ const yCell = cells.find((c) => c['@_N'] === 'Y');
114
+ // Prefer formula parsing (works for both freshly-created and loaded shapes).
115
+ // Fall back to @_V if no formula is present.
116
+ const xFraction = this.parseFraction(xCell?.['@_F']) ?? parseFloat(xCell?.['@_V'] ?? '0');
117
+ const yFraction = this.parseFraction(yCell?.['@_F']) ?? parseFloat(yCell?.['@_V'] ?? '0');
118
+ return { xFraction, yFraction };
119
+ }
120
+ /**
121
+ * Extract the numeric multiplier from a formula of the form `Width*0.5` or `Height*1`.
122
+ * Returns `undefined` if the formula does not match that pattern.
123
+ */
124
+ static parseFraction(formula) {
125
+ if (!formula)
126
+ return undefined;
127
+ const m = formula.match(/^(?:Width|Height)\*([0-9.]+)$/);
128
+ return m ? parseFloat(m[1]) : undefined;
129
+ }
130
+ }
131
+ exports.ConnectionPointBuilder = ConnectionPointBuilder;
@@ -1,4 +1,4 @@
1
- import { ConnectorStyle } from '../types/VisioTypes';
1
+ import { ConnectorStyle, ConnectionTarget } from '../types/VisioTypes';
2
2
  export declare class ConnectorBuilder {
3
3
  private static getCellVal;
4
4
  private static getAbsolutePos;
@@ -10,7 +10,7 @@ export declare class ConnectorBuilder {
10
10
  static calculateConnectorLayout(fromShapeId: string, toShapeId: string, shapeHierarchy: Map<string, {
11
11
  shape: any;
12
12
  parent: any;
13
- }>): {
13
+ }>, fromPort?: ConnectionTarget, toPort?: ConnectionTarget): {
14
14
  beginX: number;
15
15
  beginY: number;
16
16
  endX: number;
@@ -18,6 +18,12 @@ export declare class ConnectorBuilder {
18
18
  width: number;
19
19
  angle: number;
20
20
  };
21
+ /**
22
+ * Resolve a ConnectionTarget to an absolute page position using the shape hierarchy.
23
+ * Returns null when the target is 'center' or the named/indexed point is not found,
24
+ * signalling the caller to fall back to edge-intersection logic.
25
+ */
26
+ private static resolveConnectionPointPos;
21
27
  static createConnectorShapeObject(id: string, layout: any, beginArrow?: string, endArrow?: string, style?: ConnectorStyle): {
22
28
  '@_ID': string;
23
29
  '@_NameU': string;
@@ -34,5 +40,8 @@ export declare class ConnectorBuilder {
34
40
  })[];
35
41
  Section: import("../utils/StyleHelpers").VisioSection[];
36
42
  };
37
- static addConnectorToConnects(parsed: any, connectorId: string, fromShapeId: string, toShapeId: string): void;
43
+ static addConnectorToConnects(parsed: any, connectorId: string, fromShapeId: string, toShapeId: string, shapeHierarchy?: Map<string, {
44
+ shape: any;
45
+ parent: any;
46
+ }>, fromPort?: ConnectionTarget, toPort?: ConnectionTarget): void;
38
47
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectorBuilder = void 0;
4
4
  const StyleHelpers_1 = require("../utils/StyleHelpers");
5
+ const ConnectionPointBuilder_1 = require("./ConnectionPointBuilder");
5
6
  const ROUTING_VALUES = {
6
7
  straight: '2',
7
8
  orthogonal: '1',
@@ -65,7 +66,7 @@ class ConnectorBuilder {
65
66
  mapHierarchy(topShapes, null);
66
67
  return shapeHierarchy;
67
68
  }
68
- static calculateConnectorLayout(fromShapeId, toShapeId, shapeHierarchy) {
69
+ static calculateConnectorLayout(fromShapeId, toShapeId, shapeHierarchy, fromPort, toPort) {
69
70
  let beginX = 0, beginY = 0, endX = 0, endY = 0;
70
71
  let sourceGeom = null;
71
72
  let targetGeom = null;
@@ -88,12 +89,32 @@ class ConnectorBuilder {
88
89
  endY = abs.y;
89
90
  }
90
91
  if (sourceGeom && targetGeom) {
91
- const startNode = this.getEdgePoint(sourceGeom.x, sourceGeom.y, sourceGeom.w, sourceGeom.h, targetGeom.x, targetGeom.y);
92
- const endNode = this.getEdgePoint(targetGeom.x, targetGeom.y, targetGeom.w, targetGeom.h, sourceGeom.x, sourceGeom.y);
93
- beginX = startNode.x;
94
- beginY = startNode.y;
95
- endX = endNode.x;
96
- endY = endNode.y;
92
+ // Compute begin (from-side) endpoint
93
+ const beginPt = fromPort && fromPort !== 'center'
94
+ ? this.resolveConnectionPointPos(fromShapeId, fromPort, shapeHierarchy, sourceGeom)
95
+ : null;
96
+ if (beginPt) {
97
+ beginX = beginPt.x;
98
+ beginY = beginPt.y;
99
+ }
100
+ else {
101
+ const sp = this.getEdgePoint(sourceGeom.x, sourceGeom.y, sourceGeom.w, sourceGeom.h, targetGeom.x, targetGeom.y);
102
+ beginX = sp.x;
103
+ beginY = sp.y;
104
+ }
105
+ // Compute end (to-side) endpoint
106
+ const endPt = toPort && toPort !== 'center'
107
+ ? this.resolveConnectionPointPos(toShapeId, toPort, shapeHierarchy, targetGeom)
108
+ : null;
109
+ if (endPt) {
110
+ endX = endPt.x;
111
+ endY = endPt.y;
112
+ }
113
+ else {
114
+ const ep = this.getEdgePoint(targetGeom.x, targetGeom.y, targetGeom.w, targetGeom.h, sourceGeom.x, sourceGeom.y);
115
+ endX = ep.x;
116
+ endY = ep.y;
117
+ }
97
118
  }
98
119
  const dx = endX - beginX;
99
120
  const dy = endY - beginY;
@@ -101,6 +122,26 @@ class ConnectorBuilder {
101
122
  const angle = Math.atan2(dy, dx);
102
123
  return { beginX, beginY, endX, endY, width, angle };
103
124
  }
125
+ /**
126
+ * Resolve a ConnectionTarget to an absolute page position using the shape hierarchy.
127
+ * Returns null when the target is 'center' or the named/indexed point is not found,
128
+ * signalling the caller to fall back to edge-intersection logic.
129
+ */
130
+ static resolveConnectionPointPos(shapeId, target, shapeHierarchy, geom) {
131
+ const entry = shapeHierarchy.get(shapeId);
132
+ if (!entry)
133
+ return null;
134
+ const resolved = ConnectionPointBuilder_1.ConnectionPointBuilder.resolveTarget(target, entry.shape);
135
+ if (resolved.xFraction === undefined || resolved.yFraction === undefined)
136
+ return null;
137
+ const locPinX = parseFloat(this.getCellVal(entry.shape, 'LocPinX'));
138
+ const locPinY = parseFloat(this.getCellVal(entry.shape, 'LocPinY'));
139
+ const abs = this.getAbsolutePos(shapeId, shapeHierarchy);
140
+ return {
141
+ x: (abs.x - locPinX) + geom.w * resolved.xFraction,
142
+ y: (abs.y - locPinY) + geom.h * resolved.yFraction,
143
+ };
144
+ }
104
145
  static createConnectorShapeObject(id, layout, beginArrow, endArrow, style) {
105
146
  const { beginX, beginY, endX, endY, width, angle } = layout;
106
147
  const routeStyle = style?.routing ? (ROUTING_VALUES[style.routing] ?? '1') : '1';
@@ -151,7 +192,7 @@ class ConnectorBuilder {
151
192
  ]
152
193
  };
153
194
  }
154
- static addConnectorToConnects(parsed, connectorId, fromShapeId, toShapeId) {
195
+ static addConnectorToConnects(parsed, connectorId, fromShapeId, toShapeId, shapeHierarchy, fromPort, toPort) {
155
196
  if (!parsed.PageContents.Connects) {
156
197
  parsed.PageContents.Connects = { Connect: [] };
157
198
  }
@@ -161,21 +202,43 @@ class ConnectorBuilder {
161
202
  connectCollection = connectCollection ? [connectCollection] : [];
162
203
  parsed.PageContents.Connects.Connect = connectCollection;
163
204
  }
205
+ // Resolve from-side ToPart/ToCell
206
+ let fromToCell = 'PinX';
207
+ let fromToPart = '3';
208
+ if (fromPort && fromPort !== 'center' && shapeHierarchy) {
209
+ const fromEntry = shapeHierarchy.get(fromShapeId);
210
+ if (fromEntry) {
211
+ const r = ConnectionPointBuilder_1.ConnectionPointBuilder.resolveTarget(fromPort, fromEntry.shape);
212
+ fromToCell = r.toCell;
213
+ fromToPart = r.toPart;
214
+ }
215
+ }
216
+ // Resolve to-side ToPart/ToCell
217
+ let toToCell = 'PinX';
218
+ let toToPart = '3';
219
+ if (toPort && toPort !== 'center' && shapeHierarchy) {
220
+ const toEntry = shapeHierarchy.get(toShapeId);
221
+ if (toEntry) {
222
+ const r = ConnectionPointBuilder_1.ConnectionPointBuilder.resolveTarget(toPort, toEntry.shape);
223
+ toToCell = r.toCell;
224
+ toToPart = r.toPart;
225
+ }
226
+ }
164
227
  connectCollection.push({
165
228
  '@_FromSheet': connectorId,
166
229
  '@_FromCell': 'BeginX',
167
230
  '@_FromPart': '9',
168
231
  '@_ToSheet': fromShapeId,
169
- '@_ToCell': 'PinX',
170
- '@_ToPart': '3'
232
+ '@_ToCell': fromToCell,
233
+ '@_ToPart': fromToPart,
171
234
  });
172
235
  connectCollection.push({
173
236
  '@_FromSheet': connectorId,
174
237
  '@_FromCell': 'EndX',
175
238
  '@_FromPart': '12',
176
239
  '@_ToSheet': toShapeId,
177
- '@_ToCell': 'PinX',
178
- '@_ToPart': '3'
240
+ '@_ToCell': toToCell,
241
+ '@_ToPart': toToPart,
179
242
  });
180
243
  }
181
244
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ShapeBuilder = void 0;
4
4
  const StyleHelpers_1 = require("../utils/StyleHelpers");
5
5
  const GeometryBuilder_1 = require("./GeometryBuilder");
6
+ const ConnectionPointBuilder_1 = require("./ConnectionPointBuilder");
6
7
  class ShapeBuilder {
7
8
  static createStandardShape(id, props) {
8
9
  // Validate dimensions
@@ -28,6 +29,20 @@ class ShapeBuilder {
28
29
  if (props.masterId) {
29
30
  shape['@_Master'] = props.masterId;
30
31
  }
32
+ // Apply document-level stylesheet references
33
+ if (props.styleId !== undefined) {
34
+ shape['@_LineStyle'] = props.styleId.toString();
35
+ shape['@_FillStyle'] = props.styleId.toString();
36
+ shape['@_TextStyle'] = props.styleId.toString();
37
+ }
38
+ else {
39
+ if (props.lineStyleId !== undefined)
40
+ shape['@_LineStyle'] = props.lineStyleId.toString();
41
+ if (props.fillStyleId !== undefined)
42
+ shape['@_FillStyle'] = props.fillStyleId.toString();
43
+ if (props.textStyleId !== undefined)
44
+ shape['@_TextStyle'] = props.textStyleId.toString();
45
+ }
31
46
  // Add Styles
32
47
  if (props.fillColor) {
33
48
  shape.Section.push((0, StyleHelpers_1.createFillSection)(props.fillColor));
@@ -82,6 +97,10 @@ class ShapeBuilder {
82
97
  if (props.verticalAlign !== undefined) {
83
98
  shape.Cell.push({ '@_N': 'VerticalAlign', '@_V': (0, StyleHelpers_1.vertAlignValue)(props.verticalAlign) });
84
99
  }
100
+ // Connection points
101
+ if (props.connectionPoints && props.connectionPoints.length > 0) {
102
+ shape.Section.push(ConnectionPointBuilder_1.ConnectionPointBuilder.buildConnectionSection(props.connectionPoints));
103
+ }
85
104
  // Add Geometry
86
105
  // Only if NOT a Group AND NOT a Master Instance
87
106
  if (props.type !== 'Group' && !props.masterId) {
@@ -27,7 +27,15 @@ exports.DOCUMENT_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
27
27
  <ColorEntry IX="1" RGB="#FFFFFF"/>
28
28
  </Colors>
29
29
  <FaceNames/>
30
- <StyleSheets/>
30
+ <StyleSheets>
31
+ <StyleSheet ID="0" Name="No Style" NameU="No Style" IsCustomName="0" IsCustomNameU="0">
32
+ <Cell N="EnableLineProps" V="1"/>
33
+ <Cell N="EnableFillProps" V="1"/>
34
+ <Cell N="EnableTextProps" V="1"/>
35
+ <Cell N="HideForApply" V="0"/>
36
+ </StyleSheet>
37
+ <StyleSheet ID="1" Name="Normal" NameU="Normal" IsCustomName="0" IsCustomNameU="0" LineStyle="0" FillStyle="0" TextStyle="0"/>
38
+ </StyleSheets>
31
39
  </VisioDocument>`;
32
40
  exports.DOCUMENT_RELS_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
33
41
  <Relationships xmlns="${VisioConstants_1.XML_NAMESPACES.RELATIONSHIPS}">
@@ -146,6 +146,107 @@ export interface ConnectorStyle {
146
146
  }
147
147
  /** Non-rectangular geometry variants supported by ShapeBuilder. */
148
148
  export type ShapeGeometry = 'rectangle' | 'ellipse' | 'diamond' | 'rounded-rectangle' | 'triangle' | 'parallelogram';
149
+ /** A reference to a document-level stylesheet, returned by `doc.createStyle()`. */
150
+ export interface StyleRecord {
151
+ /** Zero-based integer ID used as `LineStyle` / `FillStyle` / `TextStyle` on shapes. */
152
+ id: number;
153
+ /** Human-readable name given to the style. */
154
+ name: string;
155
+ }
156
+ /**
157
+ * Visual properties for a document-level stylesheet created via `doc.createStyle()`.
158
+ * All fields are optional — omitted properties are inherited from the parent style (default: Style 0 "No Style").
159
+ */
160
+ export interface StyleProps {
161
+ /** Parent style for line property inheritance. Defaults to 0 ("No Style"). */
162
+ parentLineStyleId?: number;
163
+ /** Parent style for fill property inheritance. Defaults to 0 ("No Style"). */
164
+ parentFillStyleId?: number;
165
+ /** Parent style for text property inheritance. Defaults to 0 ("No Style"). */
166
+ parentTextStyleId?: number;
167
+ /** Stroke colour as a CSS hex string (e.g. `'#cc0000'`). */
168
+ lineColor?: string;
169
+ /** Stroke weight in **points**. Stored internally as inches (pt / 72). */
170
+ lineWeight?: number;
171
+ /** Line pattern. 0 = none, 1 = solid, 2 = dash, 3 = dot, 4 = dash-dot. */
172
+ linePattern?: number;
173
+ /** Background fill colour as a CSS hex string. */
174
+ fillColor?: string;
175
+ /** Text colour as a CSS hex string. */
176
+ fontColor?: string;
177
+ /** Font size in **points**. */
178
+ fontSize?: number;
179
+ /** Bold text. */
180
+ bold?: boolean;
181
+ /** Italic text. */
182
+ italic?: boolean;
183
+ /** Underline text. */
184
+ underline?: boolean;
185
+ /** Strikethrough text. */
186
+ strikethrough?: boolean;
187
+ /** Font family name (e.g. `'Calibri'`). */
188
+ fontFamily?: string;
189
+ /** Horizontal text alignment within the paragraph. */
190
+ horzAlign?: 'left' | 'center' | 'right' | 'justify';
191
+ /** Space before each paragraph in **points**. */
192
+ spaceBefore?: number;
193
+ /** Space after each paragraph in **points**. */
194
+ spaceAfter?: number;
195
+ /** Line-height multiplier (1.0 = single, 1.5 = 1.5×, 2.0 = double). */
196
+ lineSpacing?: number;
197
+ /** Vertical text alignment within the shape. */
198
+ verticalAlign?: 'top' | 'middle' | 'bottom';
199
+ /** Top text margin in inches. */
200
+ textMarginTop?: number;
201
+ /** Bottom text margin in inches. */
202
+ textMarginBottom?: number;
203
+ /** Left text margin in inches. */
204
+ textMarginLeft?: number;
205
+ /** Right text margin in inches. */
206
+ textMarginRight?: number;
207
+ }
208
+ /** Whether a connection point accepts incoming glue, outgoing glue, or both. */
209
+ export type ConnectionPointType = 'inward' | 'outward' | 'both';
210
+ /**
211
+ * Definition of a single connection point on a shape.
212
+ * Positions are expressed as fractions of the shape's width/height
213
+ * (0.0 = left/bottom, 1.0 = right/top in Visio's coordinate system).
214
+ */
215
+ export interface ConnectionPointDef {
216
+ /** Optional display name (e.g. `'Top'`, `'Right'`). Referenced when connecting by name. */
217
+ name?: string;
218
+ /** Horizontal position as a fraction of shape width. 0 = left edge, 1 = right edge. */
219
+ xFraction: number;
220
+ /** Vertical position as a fraction of shape height. 0 = bottom edge, 1 = top edge. */
221
+ yFraction: number;
222
+ /** Glue direction vector. Defaults to `{ x: 0, y: 0 }` (no preferred direction). */
223
+ direction?: {
224
+ x: number;
225
+ y: number;
226
+ };
227
+ /** Connection point type. Defaults to `'inward'`. */
228
+ type?: ConnectionPointType;
229
+ /** Optional tooltip shown in the Visio UI. */
230
+ prompt?: string;
231
+ }
232
+ /**
233
+ * Specifies which connection point to use when attaching a connector endpoint.
234
+ * - `'center'` → shape centre (default behaviour, `ToPart=3`)
235
+ * - `{ name }` → named connection point (e.g. `{ name: 'Top' }`)
236
+ * - `{ index }` → connection point by zero-based row index
237
+ */
238
+ export type ConnectionTarget = 'center' | {
239
+ name: string;
240
+ } | {
241
+ index: number;
242
+ };
243
+ /** Ready-made connection-point presets. */
244
+ export declare const StandardConnectionPoints: {
245
+ /** Four cardinal points: Top, Right, Bottom, Left. */
246
+ cardinal: ConnectionPointDef[];
247
+ /** Eight points: four cardinal + four corners. */
248
+ full: ConnectionPointDef[];
249
+ };
149
250
  export interface NewShapeProps {
150
251
  text: string;
151
252
  x: number;
@@ -192,4 +293,22 @@ export interface NewShapeProps {
192
293
  textMarginLeft?: number;
193
294
  /** Right text margin in inches. */
194
295
  textMarginRight?: number;
296
+ /**
297
+ * Connection points to add to the shape.
298
+ * Use `StandardConnectionPoints.cardinal` for the four cardinal points,
299
+ * or `StandardConnectionPoints.full` for eight points.
300
+ */
301
+ connectionPoints?: ConnectionPointDef[];
302
+ /**
303
+ * Apply a document-level stylesheet to this shape (sets `LineStyle`, `FillStyle`,
304
+ * and `TextStyle` all to the same ID). Create styles via `doc.createStyle()`.
305
+ * Takes precedence over `lineStyleId`, `fillStyleId`, and `textStyleId`.
306
+ */
307
+ styleId?: number;
308
+ /** Apply a stylesheet only for line properties (`LineStyle` attribute). */
309
+ lineStyleId?: number;
310
+ /** Apply a stylesheet only for fill properties (`FillStyle` attribute). */
311
+ fillStyleId?: number;
312
+ /** Apply a stylesheet only for text properties (`TextStyle` attribute). */
313
+ textStyleId?: number;
195
314
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PageSizes = exports.VisioPropType = void 0;
3
+ exports.StandardConnectionPoints = exports.PageSizes = exports.VisioPropType = void 0;
4
4
  var VisioPropType;
5
5
  (function (VisioPropType) {
6
6
  VisioPropType[VisioPropType["String"] = 0] = "String";
@@ -21,3 +21,22 @@ exports.PageSizes = {
21
21
  A4: { width: 8.268, height: 11.693 },
22
22
  A5: { width: 5.827, height: 8.268 },
23
23
  };
24
+ /** Ready-made connection-point presets. */
25
+ exports.StandardConnectionPoints = {
26
+ cardinal: [
27
+ { name: 'Top', xFraction: 0.5, yFraction: 1.0, direction: { x: 0, y: 1 } },
28
+ { name: 'Right', xFraction: 1.0, yFraction: 0.5, direction: { x: 1, y: 0 } },
29
+ { name: 'Bottom', xFraction: 0.5, yFraction: 0.0, direction: { x: 0, y: -1 } },
30
+ { name: 'Left', xFraction: 0.0, yFraction: 0.5, direction: { x: -1, y: 0 } },
31
+ ],
32
+ full: [
33
+ { name: 'Top', xFraction: 0.5, yFraction: 1.0, direction: { x: 0, y: 1 } },
34
+ { name: 'Right', xFraction: 1.0, yFraction: 0.5, direction: { x: 1, y: 0 } },
35
+ { name: 'Bottom', xFraction: 0.5, yFraction: 0.0, direction: { x: 0, y: -1 } },
36
+ { name: 'Left', xFraction: 0.0, yFraction: 0.5, direction: { x: -1, y: 0 } },
37
+ { name: 'TopLeft', xFraction: 0.0, yFraction: 1.0, direction: { x: -1, y: 1 } },
38
+ { name: 'TopRight', xFraction: 1.0, yFraction: 1.0, direction: { x: 1, y: 1 } },
39
+ { name: 'BottomRight', xFraction: 1.0, yFraction: 0.0, direction: { x: 1, y: -1 } },
40
+ { name: 'BottomLeft', xFraction: 0.0, yFraction: 0.0, direction: { x: -1, y: -1 } },
41
+ ],
42
+ };
@@ -4,6 +4,7 @@ export interface VisioSection {
4
4
  Row?: any[];
5
5
  Cell?: any[];
6
6
  }
7
+ export declare const hexToRgb: (hex: string) => string;
7
8
  export declare function createFillSection(hexColor: string): VisioSection;
8
9
  export declare const ArrowHeads: {
9
10
  None: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ArrowHeads = void 0;
3
+ exports.ArrowHeads = exports.hexToRgb = void 0;
4
4
  exports.createFillSection = createFillSection;
5
5
  exports.horzAlignValue = horzAlignValue;
6
6
  exports.vertAlignValue = vertAlignValue;
@@ -15,10 +15,11 @@ const hexToRgb = (hex) => {
15
15
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
16
16
  return result ? `RGB(${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)})` : 'RGB(0,0,0)';
17
17
  };
18
+ exports.hexToRgb = hexToRgb;
18
19
  function createFillSection(hexColor) {
19
20
  // Visio uses FillForegnd for the main background color.
20
21
  // Ideally we should sanitize hexColor to be #RRGGBB.
21
- const rgbFormula = hexToRgb(hexColor);
22
+ const rgbFormula = (0, exports.hexToRgb)(hexColor);
22
23
  return {
23
24
  '@_N': 'Fill',
24
25
  Cell: [
@@ -68,7 +69,7 @@ function createCharacterSection(props) {
68
69
  styleVal |= 8;
69
70
  const colorVal = props.color || '#000000';
70
71
  const cells = [
71
- { '@_N': 'Color', '@_V': colorVal, '@_F': hexToRgb(colorVal) },
72
+ { '@_N': 'Color', '@_V': colorVal, '@_F': (0, exports.hexToRgb)(colorVal) },
72
73
  { '@_N': 'Style', '@_V': styleVal.toString() },
73
74
  ];
74
75
  if (props.fontSize !== undefined) {
@@ -144,7 +145,7 @@ function createLineSection(props) {
144
145
  ];
145
146
  // Add RGB Formula for custom colors
146
147
  if (props.color) {
147
- cells[0]['@_F'] = hexToRgb(props.color);
148
+ cells[0]['@_F'] = (0, exports.hexToRgb)(props.color);
148
149
  }
149
150
  return {
150
151
  '@_N': 'Line',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-visio",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {