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