ts-visio 1.5.0 → 1.6.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 +52 -0
- package/dist/Page.d.ts +2 -2
- package/dist/Page.js +2 -2
- package/dist/Shape.d.ts +7 -2
- package/dist/Shape.js +9 -2
- package/dist/ShapeModifier.d.ts +7 -2
- package/dist/ShapeModifier.js +34 -3
- package/dist/shapes/ConnectionPointBuilder.d.ts +40 -0
- package/dist/shapes/ConnectionPointBuilder.js +131 -0
- package/dist/shapes/ConnectorBuilder.d.ts +12 -3
- package/dist/shapes/ConnectorBuilder.js +75 -12
- package/dist/shapes/ShapeBuilder.js +5 -0
- package/dist/types/VisioTypes.d.ts +48 -0
- package/dist/types/VisioTypes.js +20 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ 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.
|
|
28
29
|
|
|
29
30
|
Feature gaps are being tracked in [FEATURES.md](./FEATURES.md).
|
|
30
31
|
|
|
@@ -558,6 +559,57 @@ await shape.setStyle({
|
|
|
558
559
|
|
|
559
560
|
---
|
|
560
561
|
|
|
562
|
+
#### 28. Named Connection Points
|
|
563
|
+
Define specific ports on shapes and connect to them precisely instead of relying on edge-intersection.
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
import { StandardConnectionPoints } from 'ts-visio';
|
|
567
|
+
|
|
568
|
+
// 1. Add connection points at shape-creation time
|
|
569
|
+
const nodeA = await page.addShape({
|
|
570
|
+
text: 'A', x: 2, y: 3, width: 2, height: 1,
|
|
571
|
+
connectionPoints: StandardConnectionPoints.cardinal, // Top, Right, Bottom, Left
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const nodeB = await page.addShape({
|
|
575
|
+
text: 'B', x: 6, y: 3, width: 2, height: 1,
|
|
576
|
+
connectionPoints: StandardConnectionPoints.cardinal,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// 2. Connect using named ports (Right of A → Left of B)
|
|
580
|
+
await page.connectShapes(nodeA, nodeB, undefined, undefined, undefined,
|
|
581
|
+
{ name: 'Right' }, // fromPort
|
|
582
|
+
{ name: 'Left' }, // toPort
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
// 3. Fluent Shape API
|
|
586
|
+
await nodeA.connectTo(nodeB, undefined, undefined, undefined,
|
|
587
|
+
{ name: 'Right' }, { name: 'Left' });
|
|
588
|
+
|
|
589
|
+
// 4. Add a point to an existing shape by index
|
|
590
|
+
const ix = nodeA.addConnectionPoint({
|
|
591
|
+
name: 'Center',
|
|
592
|
+
xFraction: 0.5, yFraction: 0.5,
|
|
593
|
+
type: 'both',
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// 5. Connect by zero-based index instead of name
|
|
597
|
+
await page.connectShapes(nodeA, nodeB, undefined, undefined, undefined,
|
|
598
|
+
{ index: 1 }, // Right (IX=1 in cardinal preset)
|
|
599
|
+
{ index: 3 }, // Left (IX=3 in cardinal preset)
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
// 6. 'center' target (default behaviour) works alongside named ports
|
|
603
|
+
await page.connectShapes(nodeA, nodeB, undefined, undefined, undefined,
|
|
604
|
+
'center', { name: 'Left' });
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
`StandardConnectionPoints.cardinal` — 4 points: `Top`, `Right`, `Bottom`, `Left`.
|
|
608
|
+
`StandardConnectionPoints.full` — 8 points: cardinal + `TopLeft`, `TopRight`, `BottomRight`, `BottomLeft`.
|
|
609
|
+
Unknown port names fall back gracefully to edge-intersection routing without throwing.
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
561
613
|
## Examples
|
|
562
614
|
|
|
563
615
|
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,12 @@ 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;
|
|
34
39
|
setStyle(style: ShapeStyle): Promise<this>;
|
|
35
40
|
placeRightOf(targetShape: Shape, options?: {
|
|
36
41
|
gap: number;
|
package/dist/Shape.js
CHANGED
|
@@ -42,10 +42,17 @@ 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
47
|
return this;
|
|
48
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
|
+
}
|
|
49
56
|
async setStyle(style) {
|
|
50
57
|
await this.modifier.updateShapeStyle(this.pageId, this.id, style);
|
|
51
58
|
return this;
|
package/dist/ShapeModifier.d.ts
CHANGED
|
@@ -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,12 @@ 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;
|
|
35
40
|
addShape(pageId: string, props: NewShapeProps, parentId?: string): Promise<string>;
|
|
36
41
|
deleteShape(pageId: string, shapeId: string): Promise<void>;
|
|
37
42
|
private removeShapeFromTree;
|
package/dist/ShapeModifier.js
CHANGED
|
@@ -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,45 @@ 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
|
+
}
|
|
216
247
|
async addShape(pageId, props, parentId) {
|
|
217
248
|
const parsed = this.getParsed(pageId);
|
|
218
249
|
// Ensure Shapes container exists
|
|
@@ -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
|
|
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
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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':
|
|
170
|
-
'@_ToPart':
|
|
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':
|
|
178
|
-
'@_ToPart':
|
|
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
|
|
@@ -82,6 +83,10 @@ class ShapeBuilder {
|
|
|
82
83
|
if (props.verticalAlign !== undefined) {
|
|
83
84
|
shape.Cell.push({ '@_N': 'VerticalAlign', '@_V': (0, StyleHelpers_1.vertAlignValue)(props.verticalAlign) });
|
|
84
85
|
}
|
|
86
|
+
// Connection points
|
|
87
|
+
if (props.connectionPoints && props.connectionPoints.length > 0) {
|
|
88
|
+
shape.Section.push(ConnectionPointBuilder_1.ConnectionPointBuilder.buildConnectionSection(props.connectionPoints));
|
|
89
|
+
}
|
|
85
90
|
// Add Geometry
|
|
86
91
|
// Only if NOT a Group AND NOT a Master Instance
|
|
87
92
|
if (props.type !== 'Group' && !props.masterId) {
|
|
@@ -146,6 +146,48 @@ 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
|
+
/** Whether a connection point accepts incoming glue, outgoing glue, or both. */
|
|
150
|
+
export type ConnectionPointType = 'inward' | 'outward' | 'both';
|
|
151
|
+
/**
|
|
152
|
+
* Definition of a single connection point on a shape.
|
|
153
|
+
* Positions are expressed as fractions of the shape's width/height
|
|
154
|
+
* (0.0 = left/bottom, 1.0 = right/top in Visio's coordinate system).
|
|
155
|
+
*/
|
|
156
|
+
export interface ConnectionPointDef {
|
|
157
|
+
/** Optional display name (e.g. `'Top'`, `'Right'`). Referenced when connecting by name. */
|
|
158
|
+
name?: string;
|
|
159
|
+
/** Horizontal position as a fraction of shape width. 0 = left edge, 1 = right edge. */
|
|
160
|
+
xFraction: number;
|
|
161
|
+
/** Vertical position as a fraction of shape height. 0 = bottom edge, 1 = top edge. */
|
|
162
|
+
yFraction: number;
|
|
163
|
+
/** Glue direction vector. Defaults to `{ x: 0, y: 0 }` (no preferred direction). */
|
|
164
|
+
direction?: {
|
|
165
|
+
x: number;
|
|
166
|
+
y: number;
|
|
167
|
+
};
|
|
168
|
+
/** Connection point type. Defaults to `'inward'`. */
|
|
169
|
+
type?: ConnectionPointType;
|
|
170
|
+
/** Optional tooltip shown in the Visio UI. */
|
|
171
|
+
prompt?: string;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Specifies which connection point to use when attaching a connector endpoint.
|
|
175
|
+
* - `'center'` → shape centre (default behaviour, `ToPart=3`)
|
|
176
|
+
* - `{ name }` → named connection point (e.g. `{ name: 'Top' }`)
|
|
177
|
+
* - `{ index }` → connection point by zero-based row index
|
|
178
|
+
*/
|
|
179
|
+
export type ConnectionTarget = 'center' | {
|
|
180
|
+
name: string;
|
|
181
|
+
} | {
|
|
182
|
+
index: number;
|
|
183
|
+
};
|
|
184
|
+
/** Ready-made connection-point presets. */
|
|
185
|
+
export declare const StandardConnectionPoints: {
|
|
186
|
+
/** Four cardinal points: Top, Right, Bottom, Left. */
|
|
187
|
+
cardinal: ConnectionPointDef[];
|
|
188
|
+
/** Eight points: four cardinal + four corners. */
|
|
189
|
+
full: ConnectionPointDef[];
|
|
190
|
+
};
|
|
149
191
|
export interface NewShapeProps {
|
|
150
192
|
text: string;
|
|
151
193
|
x: number;
|
|
@@ -192,4 +234,10 @@ export interface NewShapeProps {
|
|
|
192
234
|
textMarginLeft?: number;
|
|
193
235
|
/** Right text margin in inches. */
|
|
194
236
|
textMarginRight?: number;
|
|
237
|
+
/**
|
|
238
|
+
* Connection points to add to the shape.
|
|
239
|
+
* Use `StandardConnectionPoints.cardinal` for the four cardinal points,
|
|
240
|
+
* or `StandardConnectionPoints.full` for eight points.
|
|
241
|
+
*/
|
|
242
|
+
connectionPoints?: ConnectionPointDef[];
|
|
195
243
|
}
|
package/dist/types/VisioTypes.js
CHANGED
|
@@ -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
|
+
};
|