@richardmcquiston01/unofficial-xcs-writer 0.8.0 → 0.8.2
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/API.md +118 -0
- package/CHANGELOG.md +58 -0
- package/README.md +105 -182
- package/package.json +3 -2
package/API.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
[Back to README](./README.md)
|
|
4
|
+
|
|
5
|
+
| [`assertXcsFormat(buffer: ArrayBuffer): void`](#assertxcsformatbuffer-arraybuffer-void) | [`readXcsFile(buffer: ArrayBuffer): string`](#readxcsfilebuffer-arraybuffer-string) | [`extractXcsTokens(buffer: ArrayBuffer): string[]`](#extractxcstokensbuffer-arraybuffer-string) | [`renderXcsFile(buffer, variables, values, fonts?): Uint8Array`](#renderxcsfilebuffer-variables-values-fonts-uint8array) | [`XcsVariable`](#xcsvariable) | [`.xs` (v2 workspace) format](#xs-v2-workspace-format) | [`assertXsFormat(buffer: ArrayBuffer): void`](#assertxsformatbuffer-arraybuffer-void) | [`extractXsTokens(buffer: ArrayBuffer): string[]`](#extractxstokensbuffer-arraybuffer-string) | [`renderXsFile(buffer, variables, values, fonts?): Uint8Array`](#renderxsfilebuffer-variables-values-fonts-uint8array) | [`buildXsArchive(file: XCSFile): Uint8Array`](#buildxsarchivefile-xcsfile-uint8array) | [`loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`](#loadfontbuffer-arraybuffer--loaddefaultfont--layouttextfont-text-originx-originy) | [Building (`src/builder.ts`, `src/layout.ts`, `src/machines.ts`)](#building-srcbuilderts-srclayoutts-srcmachinests) |
|
|
6
|
+
|
|
7
|
+
## `assertXcsFormat(buffer: ArrayBuffer): void`
|
|
8
|
+
|
|
9
|
+
Validates that `buffer` is a parseable XCS JSON file containing a `canvas` array. Throws a descriptive error if the file is invalid or unrecognized.
|
|
10
|
+
|
|
11
|
+
[Back to Top](#api-reference)
|
|
12
|
+
|
|
13
|
+
## `readXcsFile(buffer: ArrayBuffer): string`
|
|
14
|
+
|
|
15
|
+
Decodes the buffer and returns the raw JSON string. Useful for inspection or pass-through scenarios.
|
|
16
|
+
|
|
17
|
+
[Back to Top](#api-reference)
|
|
18
|
+
|
|
19
|
+
## `extractXcsTokens(buffer: ArrayBuffer): string[]`
|
|
20
|
+
|
|
21
|
+
Scans all `TEXT` display objects across all canvases and returns the unique token names found (without braces). For example, `{{LastName}}` in the file → `"LastName"` in the returned array.
|
|
22
|
+
|
|
23
|
+
[Back to Top](#api-reference)
|
|
24
|
+
|
|
25
|
+
## `renderXcsFile(buffer, variables, values, fonts?): Uint8Array`
|
|
26
|
+
|
|
27
|
+
Applies substitution to every `TEXT` display object and returns the modified file encoded as a UTF-8 `Uint8Array`. Tokens with no matching entry in `values` fall back to `variable.defaultValue`, or an empty string if no default is set.
|
|
28
|
+
|
|
29
|
+
Whenever a display's text actually changes, its glyph outline data (`charJSONs` / `fontData.glyphData`) is regenerated from a real font so xTool Studio renders the new text correctly — xTool Studio only trusts a TEXT display's stored glyph shapes; it does not re-shape text on its own unless you manually reselect the font inside the app. Displays whose text doesn't change are left completely untouched.
|
|
30
|
+
|
|
31
|
+
Pass `fonts` to supply real font file bytes (TTF/OTF/WOFF) keyed by a display's `style.fontFamily`:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const arialBuffer = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
|
|
35
|
+
renderXcsFile(buffer, variables, values, { Arial: arialBuffer });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Any `fontFamily` not present in `fonts` falls back to a bundled default (Arimo, Apache-2.0, metric-compatible with Arial) — so the fix works out of the box with no font files required, just with shapes drawn from Arimo rather than the exact requested typeface.
|
|
39
|
+
|
|
40
|
+
**Known limitation:** `renderXcsFile` only reproduces straight horizontal-baseline layout when substituting into an *existing* TEXT display. xTool Studio's `style.curveX`/`curveY` curved-text formula is undocumented and can't be reverse-decoded from a file alone, so a curved display's substituted text gets correctly-shaped glyphs laid out straight instead of on the original curve. This doesn't apply to the *building* API below — there, you supply the curve angle yourself (see `layoutCurvedGlyphText`), so there's nothing to decode.
|
|
41
|
+
|
|
42
|
+
[Back to Top](#api-reference)
|
|
43
|
+
|
|
44
|
+
## `XcsVariable`
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
interface XcsVariable {
|
|
48
|
+
token: string; // token name without braces, e.g. "LastName"
|
|
49
|
+
defaultValue?: string;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
[Back to Top](#api-reference)
|
|
54
|
+
|
|
55
|
+
## `.xs` (v2 workspace) format
|
|
56
|
+
|
|
57
|
+
[Back to Top](#api-reference)
|
|
58
|
+
|
|
59
|
+
## `assertXsFormat(buffer: ArrayBuffer): void`
|
|
60
|
+
|
|
61
|
+
Validates that `buffer` is a ZIP archive with a `.format` entry equal to `"v2"` and a `project.json` entry. Throws a descriptive error otherwise.
|
|
62
|
+
|
|
63
|
+
[Back to Top](#api-reference)
|
|
64
|
+
|
|
65
|
+
## `extractXsTokens(buffer: ArrayBuffer): string[]`
|
|
66
|
+
|
|
67
|
+
Same as `extractXcsTokens`, but scans every `canvases/<canvasId>/displays-<chunkIndex>.json` chunk across the archive instead of a single `canvas` array.
|
|
68
|
+
|
|
69
|
+
[Back to Top](#api-reference)
|
|
70
|
+
|
|
71
|
+
## `renderXsFile(buffer, variables, values, fonts?): Uint8Array`
|
|
72
|
+
|
|
73
|
+
Same substitution and glyph-regeneration behavior as `renderXcsFile` (see above — including the same `fonts` parameter and curved-text limitation), applied to every displays chunk. Returns a re-zipped `.xs` archive; every entry other than the modified displays chunks is passed through byte-for-byte unchanged.
|
|
74
|
+
|
|
75
|
+
[Back to Top](#api-reference)
|
|
76
|
+
|
|
77
|
+
## `buildXsArchive(file: XCSFile): Uint8Array`
|
|
78
|
+
|
|
79
|
+
Packages an already-`generate()`d project (see Building, below) into a `.xs` v2 ZIP archive. You won't normally call this directly — use `XCSGenerator.toXsBytes()` instead, which calls it for you.
|
|
80
|
+
|
|
81
|
+
[Back to Top](#api-reference)
|
|
82
|
+
|
|
83
|
+
## `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
|
|
84
|
+
|
|
85
|
+
Lower-level glyph-extraction primitives everything else in this library is built on, exported for direct use. See `src/glyphs.ts` for the exact xTool JSON conventions these were reverse-engineered against.
|
|
86
|
+
|
|
87
|
+
[Back to Top](#api-reference)
|
|
88
|
+
|
|
89
|
+
## Building (`src/builder.ts`, `src/layout.ts`, `src/machines.ts`)
|
|
90
|
+
|
|
91
|
+
`createXCS(deviceId?)` / `new XCSGenerator(options?)` — starts a new project (one canvas, one default layer). `deviceId` (and `options.deviceId`) accepts a `XTOOL_MACHINES` key, a `MachineProfile` object, or a raw device id string — see "Building a project from scratch" in the [README](./README.md). Chainable methods:
|
|
92
|
+
|
|
93
|
+
| Method | Adds |
|
|
94
|
+
|--------|------|
|
|
95
|
+
| `.addText(text, x, y, options?)` | A `TEXT` display. Pass `options.layout` (see below) for real glyph outlines — without it, text renders as placeholder blocks in xTool Studio. |
|
|
96
|
+
| `.addPath(pathData, x, y, width, height, options?)` | A `PATH` display from SVG path data. |
|
|
97
|
+
| `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
|
|
98
|
+
| `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
|
|
99
|
+
|
|
100
|
+
`.addText()`/`.addPath()`/`.addBitmap()` all also accept a `processing: { processingType, values }` option — see "Building a project from scratch" in the [README](./README.md); it only affects `.toXsBytes()` output.
|
|
101
|
+
|
|
102
|
+
Then `.generate()` (the plain `XCSFile` object), `.toJSON()` (string), `.toBytes()` (`Uint8Array`, matching `renderXcsFile`'s output type, as `.xcs`), or `.toXsBytes()` (`Uint8Array`, matching `renderXsFile`'s output type, as `.xs`).
|
|
103
|
+
|
|
104
|
+
Text layout — build the `layout` option for `.addText()` with one of:
|
|
105
|
+
|
|
106
|
+
| Function | Layout |
|
|
107
|
+
|----------|--------|
|
|
108
|
+
| `layoutGlyphText(font, text, emSizeMm, xMm, yMm, letterSpacingMm?)` | Single-line, straight, anchored so the ink box's top-left lands at `(xMm, yMm)`. |
|
|
109
|
+
| `layoutMultilineGlyphText(font, text, emSizeMm, letterSpacingMm, lineHeightMult, align)` | Multi-line (`\n`-separated), straight, each line aligned and vertically stacked. |
|
|
110
|
+
| `layoutCurvedGlyphText(font, text, emSizeMm, curveDeg, boxWidthMm, boxHeightMm, align?, letterSpacingMm?)` | Single-line, curved along an arc spanning `curveDeg` degrees across a `boxWidthMm`-wide box (positive arches the apex upward, negative bows it downward — same convention as SVG `<textPath>`/canvas curved-text editors). Multi-line curved text isn't supported (matches typical curved-text editors, which also flatten multi-line curved text to one line). **Not yet visually verified against real xTool Studio rendering** (there's no way to check outside the application itself) — the geometry is self-consistent and unit-tested, but treat curved output as best-effort until you've spot-checked it. |
|
|
111
|
+
|
|
112
|
+
All three return `GlyphTextLayout | null` (`null` for empty/whitespace-only text). `translateGlyphLayout(layout, dxMm, dyMm)` cheaply shifts an already-built layout without relaying out the font.
|
|
113
|
+
|
|
114
|
+
[Back to Top](#api-reference)
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
[Back to README](./README.md)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
[Back to README](./README.md)
|
|
4
|
+
|
|
5
|
+
| [Unreleased](#unreleased) | [0.8.2 - 2026-10-13](#082---2026-10-13) | [0.8.1 - 2026-09-21](#081---2026-09-21) | [0.8.0 - 2026-09-21](#080---2026-09-21) | [0.7.0 - 2026-09-21](#070---2026-09-21) | [0.6.0 - 2026-09-21](#060---2026-09-21) | [0.5.0 - 2026-09-20](#050---2026-09-20) | [0.4.1 - 2026-09-19](#041---2026-09-19) | [0.4.0 - 2026-09-19](#040---2026-09-19) | [0.3.1 - 2026-08-17](#031---2026-08-17) | [0.3.0 - 2026-07-14](#030---2026-07-14) | [0.2.0 - 2026-07-14](#020---2026-07-14) | [0.1.0 - 2026-05-19](#010---2026-05-19) |
|
|
6
|
+
|
|
3
7
|
All notable changes to this project will be documented in this file.
|
|
4
8
|
|
|
5
9
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
@@ -7,6 +11,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
11
|
|
|
8
12
|
## [Unreleased]
|
|
9
13
|
|
|
14
|
+
[Back to Top](#changelog)
|
|
15
|
+
|
|
16
|
+
## [0.8.2] - 2026-10-13
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Documentation only**: no library code changed. Rewrote `README.md` to be shorter, with an
|
|
21
|
+
author block, a Table of Contents, Back to Top links, a Supported Machines table, and the
|
|
22
|
+
License section corrected to MIT. Added a Back to README link, a menu row, and Back to Top
|
|
23
|
+
links to `API.md` and `CHANGELOG.md`.
|
|
24
|
+
|
|
25
|
+
[Back to Top](#changelog)
|
|
26
|
+
|
|
27
|
+
## [0.8.1] - 2026-09-21
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **README restructure**: removed the "Releasing" subsection (internal maintainer process,
|
|
32
|
+
not user-facing); moved the full "API" section into a new `API.md` (added to `package.json`'s
|
|
33
|
+
`files` so it ships with the npm package), with the README's "API" section now just a link to
|
|
34
|
+
it; added a disclaimer blockquote under the title stating this is an unofficial,
|
|
35
|
+
community-built library not affiliated with xTool; removed the hyperlink on "xTool Creative
|
|
36
|
+
Space" in the intro paragraph (kept as plain text); moved "Buy Me a Coffee" up to sit directly
|
|
37
|
+
above the API section instead of at the very end.
|
|
38
|
+
- **Copyright section**: added a note that xTool, xTool Studio, and xTool Creative Space are
|
|
39
|
+
registered trademarks of Makeblock Co., Ltd., and that this project isn't affiliated with,
|
|
40
|
+
endorsed by, or sponsored by Makeblock or xTool.
|
|
41
|
+
|
|
42
|
+
[Back to Top](#changelog)
|
|
43
|
+
|
|
10
44
|
## [0.8.0] - 2026-09-21
|
|
11
45
|
|
|
12
46
|
### Added
|
|
@@ -27,6 +61,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
27
61
|
(`file.device.id`, i.e. `"P2S"`). `.xcs` output (`.toBytes()`) is unaffected -- it has no
|
|
28
62
|
`deviceCode` field.
|
|
29
63
|
|
|
64
|
+
[Back to Top](#changelog)
|
|
65
|
+
|
|
30
66
|
## [0.7.0] - 2026-09-21
|
|
31
67
|
|
|
32
68
|
### Added
|
|
@@ -41,6 +77,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
41
77
|
is structurally different and much less understood, so it isn't implemented (see `CLAUDE.md`'s
|
|
42
78
|
Open Issues).
|
|
43
79
|
|
|
80
|
+
[Back to Top](#changelog)
|
|
81
|
+
|
|
44
82
|
## [0.6.0] - 2026-09-21
|
|
45
83
|
|
|
46
84
|
### Added
|
|
@@ -55,6 +93,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
55
93
|
generated `.xs` file's power/speed settings may need to be configured manually in xTool
|
|
56
94
|
Studio before cutting/engraving.
|
|
57
95
|
|
|
96
|
+
[Back to Top](#changelog)
|
|
97
|
+
|
|
58
98
|
## [0.5.0] - 2026-09-20
|
|
59
99
|
|
|
60
100
|
### Added
|
|
@@ -72,6 +112,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
72
112
|
vector store. `.xs` *generation* (building a new project from scratch) isn't implemented yet
|
|
73
113
|
— see `CLAUDE.md`'s "Open Issues".
|
|
74
114
|
|
|
115
|
+
[Back to Top](#changelog)
|
|
116
|
+
|
|
75
117
|
## [0.4.1] - 2026-09-19
|
|
76
118
|
|
|
77
119
|
### Changed
|
|
@@ -82,6 +124,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
82
124
|
from 0.4.0; this is the first published release carrying the generation API
|
|
83
125
|
and curved-text layout described below.
|
|
84
126
|
|
|
127
|
+
[Back to Top](#changelog)
|
|
128
|
+
|
|
85
129
|
## [0.4.0] - 2026-09-19
|
|
86
130
|
|
|
87
131
|
### Added
|
|
@@ -109,6 +153,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
109
153
|
still can't decode an existing curved display's `style.curveX`/`curveY`, but the new building
|
|
110
154
|
API doesn't have that problem (the caller supplies the angle directly).
|
|
111
155
|
|
|
156
|
+
[Back to Top](#changelog)
|
|
157
|
+
|
|
112
158
|
## [0.3.1] - 2026-08-17
|
|
113
159
|
|
|
114
160
|
### Changed
|
|
@@ -119,6 +165,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
119
165
|
- `donate.svg` is included in the published tarball so the README's QR image
|
|
120
166
|
resolves for consumers reading it from the package.
|
|
121
167
|
|
|
168
|
+
[Back to Top](#changelog)
|
|
169
|
+
|
|
122
170
|
## [0.3.0] - 2026-07-14
|
|
123
171
|
|
|
124
172
|
### Added
|
|
@@ -134,6 +182,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
134
182
|
0.2.0 tarball on npm was packed from a stale `dist/` and does not actually
|
|
135
183
|
contain the glyph regeneration code its README describes — use 0.3.0 instead.
|
|
136
184
|
|
|
185
|
+
[Back to Top](#changelog)
|
|
186
|
+
|
|
137
187
|
## [0.2.0] - 2026-07-14
|
|
138
188
|
|
|
139
189
|
> **Warning:** the npm tarball for this version was built from a stale `dist/`
|
|
@@ -159,6 +209,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
159
209
|
substituted text on a curved TEXT display gets correctly-shaped glyphs laid
|
|
160
210
|
out on a straight baseline.
|
|
161
211
|
|
|
212
|
+
[Back to Top](#changelog)
|
|
213
|
+
|
|
162
214
|
## [0.1.0] - 2026-05-19
|
|
163
215
|
|
|
164
216
|
### Added
|
|
@@ -168,3 +220,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
168
220
|
- Core API: `assertXcsFormat`, `readXcsFile`, `extractXcsTokens`,
|
|
169
221
|
`renderXcsFile`, and the `XcsVariable` type.
|
|
170
222
|
- Dual ESM + CJS build with TypeScript declarations via tsup.
|
|
223
|
+
|
|
224
|
+
[Back to Top](#changelog)
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
[Back to README](./README.md)
|
package/README.md
CHANGED
|
@@ -1,21 +1,43 @@
|
|
|
1
1
|
# unofficial-xcs-writer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
- Author: [Richard McQuiston](mailto:richard.mcquiston@gmail.com)
|
|
4
|
+
- Website: https://richardmcquiston.com/
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
> **Disclaimer:** This is an unofficial, community-built library. It is not affiliated with, endorsed by, or sponsored by xTool or Makeblock.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Table of Contents
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
| [Overview](#overview) | [Features](#features) | [Getting Started](#getting-started) | [Supported Machines](#supported-machines) | [Known Limitations](#known-limitations) | [More Information](#more-information) | [Development](#development) | [Buy Me a Coffee](#buy-me-a-coffee) | [License](#license) | [Copyright](#copyright) |
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
## Overview
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
- **Extract** `{{token}}` template placeholders from text objects
|
|
15
|
-
- **Render** a filled-in copy by substituting values for those placeholders
|
|
16
|
-
- **Build** a new project from scratch — text (straight, multi-line, or curved), paths, and embedded images — and export it as either `.xcs` or `.xs`
|
|
14
|
+
This is a TypeScript library for xTool project files. It reads, builds, and fills in templates for `.xcs` and `.xs` files.
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
An `.xcs` file is plain JSON. An `.xs` file is a ZIP archive of JSON files. xTool Studio 1.7 and newer saves `.xs` by default. It can still open `.xcs` files but can no longer save them.
|
|
17
|
+
|
|
18
|
+
The library uses no framework, no DOM, and no Node.js-only features. It works in browsers and in Node.js.
|
|
19
|
+
|
|
20
|
+
[Back to Top](#table-of-contents)
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- Find `{{token}}` placeholders in text objects.
|
|
25
|
+
- Fill in the placeholders with your own values. The library draws new glyph outlines for the changed text.
|
|
26
|
+
- Build a project from scratch. You can add straight, multi-line, or curved text, paths, and images.
|
|
27
|
+
- Save the project as `.xcs` or `.xs`.
|
|
28
|
+
- Pick a target machine from a list of 13 xTool devices.
|
|
29
|
+
- Set power and speed for each object in `.xs` output.
|
|
30
|
+
|
|
31
|
+
[Back to Top](#table-of-contents)
|
|
32
|
+
|
|
33
|
+
## Getting Started
|
|
34
|
+
|
|
35
|
+
### Prerequisites
|
|
36
|
+
|
|
37
|
+
- Node.js 19 or newer, or a current web browser.
|
|
38
|
+
- A package manager such as npm or pnpm.
|
|
39
|
+
|
|
40
|
+
### Installation
|
|
19
41
|
|
|
20
42
|
```bash
|
|
21
43
|
npm install @richardmcquiston01/unofficial-xcs-writer
|
|
@@ -23,49 +45,46 @@ npm install @richardmcquiston01/unofficial-xcs-writer
|
|
|
23
45
|
pnpm add @richardmcquiston01/unofficial-xcs-writer
|
|
24
46
|
```
|
|
25
47
|
|
|
26
|
-
|
|
48
|
+
### Usage
|
|
49
|
+
|
|
50
|
+
To fill in a template, follow these steps:
|
|
51
|
+
|
|
52
|
+
1. Load the file as an `ArrayBuffer`.
|
|
53
|
+
2. Check the format with `assertXcsFormat`. It throws an error if the file is not valid.
|
|
54
|
+
3. List the placeholders with `extractXcsTokens`.
|
|
55
|
+
4. Call `renderXcsFile` with your values.
|
|
56
|
+
|
|
57
|
+
For `.xs` files, use `assertXsFormat`, `extractXsTokens`, and `renderXsFile`. They work the same way.
|
|
58
|
+
|
|
59
|
+
To build a project, call `createXCS` with a machine name. Add objects with `.addText()`, `.addPath()`, and `.addBitmap()`. Finish with `.toBytes()` for `.xcs` or `.toXsBytes()` for `.xs`.
|
|
60
|
+
|
|
61
|
+
### Examples
|
|
62
|
+
|
|
63
|
+
Fill in a template:
|
|
27
64
|
|
|
28
65
|
```ts
|
|
29
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
assertXcsFormat,
|
|
68
|
+
extractXcsTokens,
|
|
69
|
+
renderXcsFile,
|
|
70
|
+
} from '@richardmcquiston01/unofficial-xcs-writer';
|
|
30
71
|
import type { XcsVariable } from '@richardmcquiston01/unofficial-xcs-writer';
|
|
31
72
|
|
|
32
|
-
|
|
33
|
-
const buffer = await fetch('/templates/name-tag.xcs').then(r => r.arrayBuffer());
|
|
73
|
+
const buffer: ArrayBuffer = await fetch('/templates/name-tag.xcs').then((r) => r.arrayBuffer());
|
|
34
74
|
|
|
35
|
-
|
|
36
|
-
|
|
75
|
+
assertXcsFormat(buffer);
|
|
76
|
+
const tokens: string[] = extractXcsTokens(buffer); // ['FirstName', 'LastName']
|
|
37
77
|
|
|
38
|
-
// Discover which {{token}} placeholders are in the file
|
|
39
|
-
const tokens = extractXcsTokens(buffer);
|
|
40
|
-
// e.g. ['FirstName', 'LastName']
|
|
41
|
-
|
|
42
|
-
// Define variables and supply values
|
|
43
78
|
const variables: XcsVariable[] = [
|
|
44
79
|
{ token: 'FirstName', defaultValue: 'Guest' },
|
|
45
80
|
{ token: 'LastName' },
|
|
46
81
|
];
|
|
47
|
-
const values = { FirstName: 'Jane', LastName: 'Smith' };
|
|
82
|
+
const values: Record<string, string> = { FirstName: 'Jane', LastName: 'Smith' };
|
|
48
83
|
|
|
49
|
-
// Produce a filled-in .xcs file ready for download
|
|
50
84
|
const output: Uint8Array = renderXcsFile(buffer, variables, values);
|
|
51
85
|
```
|
|
52
86
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Same shape, different functions — `.xs` is a ZIP archive, so there's no `readXsFile` equivalent to `readXcsFile` (there's no single JSON string to return), but token extraction and substitution work the same way:
|
|
56
|
-
|
|
57
|
-
```ts
|
|
58
|
-
import { assertXsFormat, extractXsTokens, renderXsFile } from '@richardmcquiston01/unofficial-xcs-writer';
|
|
59
|
-
|
|
60
|
-
const buffer = await fetch('/templates/name-tag.xs').then(r => r.arrayBuffer());
|
|
61
|
-
|
|
62
|
-
assertXsFormat(buffer); // throws if invalid
|
|
63
|
-
const tokens = extractXsTokens(buffer); // e.g. ['FirstName', 'LastName']
|
|
64
|
-
|
|
65
|
-
const output: Uint8Array = renderXsFile(buffer, variables, values);
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Building a project from scratch
|
|
87
|
+
Build a project from scratch:
|
|
69
88
|
|
|
70
89
|
```ts
|
|
71
90
|
import {
|
|
@@ -77,192 +96,96 @@ import {
|
|
|
77
96
|
|
|
78
97
|
const font = loadDefaultFont();
|
|
79
98
|
|
|
80
|
-
const
|
|
81
|
-
// Straight text needs a real glyph layout so xTool Studio renders
|
|
82
|
-
// actual outlines instead of placeholder blocks.
|
|
99
|
+
const bytes: Uint8Array = createXCS('P2S')
|
|
83
100
|
.addText('Hello', 10, 10, {
|
|
84
101
|
fontFamily: 'Arial',
|
|
85
102
|
fontSize: 24,
|
|
86
103
|
layout: layoutGlyphText(font, 'Hello', 8, 10, 10),
|
|
87
104
|
})
|
|
88
|
-
// Curved text: same idea, via layoutCurvedGlyphText -- the curve
|
|
89
|
-
// is baked into each character's own glyph shape and position, the
|
|
90
|
-
// way substituted text already bakes straight shapes.
|
|
91
105
|
.addText('Arch Text', 0, 40, {
|
|
92
106
|
fontFamily: 'Arial',
|
|
93
107
|
fontSize: 18,
|
|
94
108
|
layout: layoutCurvedGlyphText(font, 'Arch Text', 6, 90, 60, 20, 'center'),
|
|
95
109
|
})
|
|
96
110
|
.addPath('M0 0L10 0L10 10L0 10Z', 0, 60, 10, 10, {
|
|
97
|
-
// Only affects .toXsBytes() output -- see below.
|
|
98
111
|
processing: { processingType: 'VECTOR_CUTTING', values: { power: 80, speed: 10 } },
|
|
99
112
|
})
|
|
100
|
-
.
|
|
113
|
+
.toXsBytes();
|
|
101
114
|
```
|
|
102
115
|
|
|
103
|
-
|
|
104
|
-
archive — same chainable builder, same display objects, different container.
|
|
105
|
-
|
|
106
|
-
Pass `processing: { processingType, values }` to `.addText()`/`.addPath()`/`.addBitmap()`'s
|
|
107
|
-
options to set that display's power/speed/etc. in the exported `.xs` file's `profiles.json` +
|
|
108
|
-
device bindings — `.xcs` output (`.toBytes()`) ignores this option; its own per-display
|
|
109
|
-
processing format is different and not supported yet. Displays given identical settings share
|
|
110
|
-
one profile/binding automatically. `processingType`/`values`' exact shape is xTool Studio's own
|
|
111
|
-
and undocumented beyond real exports — see `xs_samples/*.xs`'s `profiles.json` for examples
|
|
112
|
-
(`VECTOR_ENGRAVING`, `VECTOR_CUTTING`, `FILL_VECTOR_ENGRAVING`). Displays with no `processing`
|
|
113
|
-
option behave as before: a generated `.xs` file's power/speed may need to be set manually in
|
|
114
|
-
xTool Studio before cutting/engraving.
|
|
115
|
-
|
|
116
|
-
`createXCS('P2S')`'s argument (and `XCSGeneratorOptions.deviceId`) accepts a known machine name
|
|
117
|
-
from `XTOOL_MACHINES` — currently 13 machines verified from real exports in this repo (`"P2S"`,
|
|
118
|
-
`"F2 Ultra UV"`, `"S1"`, `"P3"`, `"M2"`, `"F2"`, `"F2 Ultra (Single)"`, `"F2 Ultra"`,
|
|
119
|
-
`"M1 Ultra"`, `"F1"`, `"F1 Lite"`, `"F1 Ultra"`, `"MetalFab"`) — and resolves the real
|
|
120
|
-
`extId`/`extName`/`deviceCode` (`.xs` only)/default power for that machine, instead of reusing
|
|
121
|
-
whatever string you pass for all of them. Export the same design for multiple machines by
|
|
122
|
-
building it once and calling `.toXsBytes()`/`.toBytes()` once per `createXCS(machineName)`:
|
|
116
|
+
Keep these points in mind:
|
|
123
117
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for (const machine of Object.keys(XTOOL_MACHINES)) {
|
|
128
|
-
const bytes = createXCS(machine).addPath('M0 0L10 0L10 10L0 10Z', 0, 0, 10, 10).toXsBytes();
|
|
129
|
-
// write `bytes` to e.g. `design-${machine}.xs`
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
A device id not in the catalog still works exactly as before (no `.xs` `deviceCode`, `extId`/
|
|
134
|
-
`extName`/`device.id` all set to that string, default power `55`); you can also pass a
|
|
135
|
-
`MachineProfile` object directly for a machine not yet in the catalog.
|
|
118
|
+
- Text needs a glyph `layout`. Without one, xTool Studio shows placeholder blocks instead of real letters.
|
|
119
|
+
- The `processing` option only changes `.xs` output. Objects with the same settings share one profile.
|
|
120
|
+
- The library does not check the processing values. Confirm power and speed in xTool Studio before you cut.
|
|
136
121
|
|
|
137
|
-
|
|
122
|
+
[Back to Top](#table-of-contents)
|
|
138
123
|
|
|
139
|
-
|
|
124
|
+
## Supported Machines
|
|
140
125
|
|
|
141
|
-
|
|
126
|
+
Pass one of these names to `createXCS`. The library then uses the real device ids and default power for that machine.
|
|
142
127
|
|
|
143
|
-
|
|
128
|
+
| | | | |
|
|
129
|
+
|---|---|---|---|
|
|
130
|
+
| P2S | P3 | S1 | M2 |
|
|
131
|
+
| M1 Ultra | F1 | F1 Lite | F1 Ultra |
|
|
132
|
+
| F2 | F2 Ultra | F2 Ultra (Single) | F2 Ultra UV |
|
|
133
|
+
| MetalFab | | | |
|
|
144
134
|
|
|
145
|
-
|
|
135
|
+
You can also pass a `MachineProfile` object or a plain device id string. A plain string still works, but it gets default values. To export one design for every machine, loop over `Object.keys(XTOOL_MACHINES)`.
|
|
146
136
|
|
|
147
|
-
|
|
137
|
+
[Back to Top](#table-of-contents)
|
|
148
138
|
|
|
149
|
-
|
|
139
|
+
## Known Limitations
|
|
150
140
|
|
|
151
|
-
|
|
141
|
+
- New text on an already curved text object is laid out straight.
|
|
142
|
+
- Curved text built from scratch has not been checked against real xTool Studio output.
|
|
143
|
+
- Multi-line curved text is not supported.
|
|
144
|
+
- `.xcs` output has no processing-profile support.
|
|
152
145
|
|
|
153
|
-
|
|
146
|
+
[Back to Top](#table-of-contents)
|
|
154
147
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
Pass `fonts` to supply real font file bytes (TTF/OTF/WOFF) keyed by a display's `style.fontFamily`:
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
const arialBuffer = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
|
|
161
|
-
renderXcsFile(buffer, variables, values, { Arial: arialBuffer });
|
|
162
|
-
```
|
|
148
|
+
## More Information
|
|
163
149
|
|
|
164
|
-
|
|
150
|
+
- [API.md](./API.md) lists every function, option, and type.
|
|
151
|
+
- [CHANGELOG.md](./CHANGELOG.md) lists changes for each release.
|
|
165
152
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
### `XcsVariable`
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
interface XcsVariable {
|
|
172
|
-
token: string; // token name without braces, e.g. "LastName"
|
|
173
|
-
defaultValue?: string;
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### `.xs` (v2 workspace) format
|
|
178
|
-
|
|
179
|
-
### `assertXsFormat(buffer: ArrayBuffer): void`
|
|
180
|
-
|
|
181
|
-
Validates that `buffer` is a ZIP archive with a `.format` entry equal to `"v2"` and a `project.json` entry. Throws a descriptive error otherwise.
|
|
182
|
-
|
|
183
|
-
### `extractXsTokens(buffer: ArrayBuffer): string[]`
|
|
184
|
-
|
|
185
|
-
Same as `extractXcsTokens`, but scans every `canvases/<canvasId>/displays-<chunkIndex>.json` chunk across the archive instead of a single `canvas` array.
|
|
186
|
-
|
|
187
|
-
### `renderXsFile(buffer, variables, values, fonts?): Uint8Array`
|
|
188
|
-
|
|
189
|
-
Same substitution and glyph-regeneration behavior as `renderXcsFile` (see above — including the same `fonts` parameter and curved-text limitation), applied to every displays chunk. Returns a re-zipped `.xs` archive; every entry other than the modified displays chunks is passed through byte-for-byte unchanged.
|
|
190
|
-
|
|
191
|
-
### `buildXsArchive(file: XCSFile): Uint8Array`
|
|
192
|
-
|
|
193
|
-
Packages an already-`generate()`d project (see Building, below) into a `.xs` v2 ZIP archive. You won't normally call this directly — use `XCSGenerator.toXsBytes()` instead, which calls it for you.
|
|
194
|
-
|
|
195
|
-
### `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
|
|
196
|
-
|
|
197
|
-
Lower-level glyph-extraction primitives everything else in this library is built on, exported for direct use. See `src/glyphs.ts` for the exact xTool JSON conventions these were reverse-engineered against.
|
|
198
|
-
|
|
199
|
-
### Building (`src/builder.ts`, `src/layout.ts`, `src/machines.ts`)
|
|
200
|
-
|
|
201
|
-
`createXCS(deviceId?)` / `new XCSGenerator(options?)` — starts a new project (one canvas, one default layer). `deviceId` (and `options.deviceId`) accepts a `XTOOL_MACHINES` key, a `MachineProfile` object, or a raw device id string — see "Building a project from scratch" above. Chainable methods:
|
|
202
|
-
|
|
203
|
-
| Method | Adds |
|
|
204
|
-
|--------|------|
|
|
205
|
-
| `.addText(text, x, y, options?)` | A `TEXT` display. Pass `options.layout` (see below) for real glyph outlines — without it, text renders as placeholder blocks in xTool Studio. |
|
|
206
|
-
| `.addPath(pathData, x, y, width, height, options?)` | A `PATH` display from SVG path data. |
|
|
207
|
-
| `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
|
|
208
|
-
| `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
|
|
209
|
-
|
|
210
|
-
`.addText()`/`.addPath()`/`.addBitmap()` all also accept a `processing: { processingType, values }` option — see "Building a project from scratch" above; it only affects `.toXsBytes()` output.
|
|
211
|
-
|
|
212
|
-
Then `.generate()` (the plain `XCSFile` object), `.toJSON()` (string), `.toBytes()` (`Uint8Array`, matching `renderXcsFile`'s output type, as `.xcs`), or `.toXsBytes()` (`Uint8Array`, matching `renderXsFile`'s output type, as `.xs`).
|
|
213
|
-
|
|
214
|
-
Text layout — build the `layout` option for `.addText()` with one of:
|
|
215
|
-
|
|
216
|
-
| Function | Layout |
|
|
217
|
-
|----------|--------|
|
|
218
|
-
| `layoutGlyphText(font, text, emSizeMm, xMm, yMm, letterSpacingMm?)` | Single-line, straight, anchored so the ink box's top-left lands at `(xMm, yMm)`. |
|
|
219
|
-
| `layoutMultilineGlyphText(font, text, emSizeMm, letterSpacingMm, lineHeightMult, align)` | Multi-line (`\n`-separated), straight, each line aligned and vertically stacked. |
|
|
220
|
-
| `layoutCurvedGlyphText(font, text, emSizeMm, curveDeg, boxWidthMm, boxHeightMm, align?, letterSpacingMm?)` | Single-line, curved along an arc spanning `curveDeg` degrees across a `boxWidthMm`-wide box (positive arches the apex upward, negative bows it downward — same convention as SVG `<textPath>`/canvas curved-text editors). Multi-line curved text isn't supported (matches typical curved-text editors, which also flatten multi-line curved text to one line). **Not yet visually verified against real xTool Studio rendering** (there's no way to check outside the application itself) — the geometry is self-consistent and unit-tested, but treat curved output as best-effort until you've spot-checked it. |
|
|
221
|
-
|
|
222
|
-
All three return `GlyphTextLayout | null` (`null` for empty/whitespace-only text). `translateGlyphLayout(layout, dxMm, dyMm)` cheaply shifts an already-built layout without relaying out the font.
|
|
153
|
+
[Back to Top](#table-of-contents)
|
|
223
154
|
|
|
224
155
|
## Development
|
|
225
156
|
|
|
226
157
|
```bash
|
|
227
|
-
pnpm install
|
|
228
|
-
pnpm build
|
|
229
|
-
pnpm dev
|
|
230
|
-
pnpm test
|
|
231
|
-
|
|
158
|
+
pnpm install # install dependencies
|
|
159
|
+
pnpm build # build to dist/ (ESM, CJS, and type files)
|
|
160
|
+
pnpm dev # rebuild on every save
|
|
161
|
+
pnpm test # run the tests
|
|
162
|
+
pnpm typecheck # check types without building
|
|
232
163
|
```
|
|
233
164
|
|
|
234
|
-
|
|
165
|
+
The library uses [opentype.js](https://github.com/opentypejs/opentype.js) to read glyph outlines and [fflate](https://github.com/101arrowz/fflate) to read and write ZIP archives. The built-in fallback font is [Arimo](https://fonts.google.com/specimen/Arimo) version 5.2.8, under the Apache 2 license. Its license text is in `third_party/arimo/LICENSE`.
|
|
235
166
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
| `dist/index.cjs` | CommonJS |
|
|
240
|
-
| `dist/index.d.ts` | TypeScript declarations |
|
|
167
|
+
[Back to Top](#table-of-contents)
|
|
168
|
+
|
|
169
|
+
## Buy Me a Coffee
|
|
241
170
|
|
|
242
|
-
|
|
171
|
+
If this app, code, or repository has helped you or someone you know, please consider donating. I appreciate any help to offset the costs of development and/or AI Credits.
|
|
243
172
|
|
|
244
|
-
|
|
245
|
-
`CHANGELOG.md` entry, then create a GitHub release tagged `vX.Y.Z` — the
|
|
246
|
-
[publish workflow](.github/workflows/publish.yml) checks the tag against
|
|
247
|
-
`package.json`, rebuilds, runs the tests, and publishes with provenance.
|
|
248
|
-
It needs an `NPM_TOKEN` repository secret with publish rights.
|
|
173
|
+
[**Donate via Stripe**](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800), or scan:
|
|
249
174
|
|
|
250
|
-
|
|
175
|
+
[](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800)
|
|
251
176
|
|
|
252
|
-
|
|
253
|
-
- Only `TEXT` display objects are modified during substitution. Geometry, bitmaps, device configuration, and all other fields pass through unchanged.
|
|
254
|
-
- This library targets the current xTool Creative Space JSON/`.xs` formats. Older versions of the software may export a different structure.
|
|
255
|
-
- `.xs` archives are read and re-written with [fflate](https://github.com/101arrowz/fflate); large `PATH` displays' vector data may be deduplicated into a separate `vectors/<bucketType>/` store rather than inlined — this library never touches `PATH` displays, so that structure round-trips untouched.
|
|
256
|
-
- Glyph outline extraction is powered by [opentype.js](https://github.com/opentypejs/opentype.js). The bundled fallback font is [Arimo](https://fonts.google.com/specimen/Arimo) v5.2.8 via [`@fontsource/arimo`](https://www.npmjs.com/package/@fontsource/arimo) (Apache License 2.0, see `third_party/arimo/LICENSE`).
|
|
177
|
+
[Back to Top](#table-of-contents)
|
|
257
178
|
|
|
258
179
|
## License
|
|
259
180
|
|
|
260
|
-
[
|
|
181
|
+
MIT. See [LICENSE](./LICENSE). The bundled Arimo font uses the Apache 2 license.
|
|
261
182
|
|
|
262
|
-
|
|
183
|
+
[Back to Top](#table-of-contents)
|
|
263
184
|
|
|
264
|
-
|
|
185
|
+
## Copyright
|
|
265
186
|
|
|
266
|
-
|
|
187
|
+
(c)2026 Richard McQuiston. All rights reserved.
|
|
267
188
|
|
|
268
|
-
|
|
189
|
+
xTool, xTool Studio, and xTool Creative Space are trademarks of Makeblock Co., Ltd. This project is not affiliated with, endorsed by, or sponsored by Makeblock Co., Ltd. or xTool.
|
|
190
|
+
|
|
191
|
+
[Back to Top](#table-of-contents)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@richardmcquiston01/unofficial-xcs-writer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Read, write, build, and apply variable substitution to xTool Creative Space .xcs and .xs files, with real glyph outline extraction for text",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Richard McQuiston <richard.mcquiston01@gmail.com>",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"dist",
|
|
34
34
|
"third_party",
|
|
35
35
|
"donate.svg",
|
|
36
|
-
"CHANGELOG.md"
|
|
36
|
+
"CHANGELOG.md",
|
|
37
|
+
"API.md"
|
|
37
38
|
],
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/node": "^26.6.2",
|