@richardmcquiston01/unofficial-xcs-writer 0.3.1 → 0.5.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/CHANGELOG.md CHANGED
@@ -5,6 +5,60 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.0] - 2026-09-20
9
+
10
+ ### Added
11
+
12
+ - **`.xs` (v2 workspace) format support** (`src/xs.ts`): `assertXsFormat`, `extractXsTokens`,
13
+ and `renderXsFile` read and apply `{{token}}` substitution to xTool Studio v1.7+'s new `.xs`
14
+ format — a ZIP archive of chunked per-canvas display files, unlike `.xcs`'s single plain-JSON
15
+ document. `.xcs` is now legacy as of xTool Studio v1.7 (still readable, no longer savable from
16
+ the app). The `TEXT` display schema itself (`text`/`style`/`fontData`/`charJSONs`) is
17
+ unchanged from `.xcs`, so token extraction and glyph regeneration share their implementation
18
+ with `renderXcsFile`/`extractXcsTokens` via a new `src/substitution.ts` module; only the
19
+ container format differs. Archives are read/written with [fflate](https://github.com/101arrowz/fflate)
20
+ (new runtime dependency); every entry other than the modified displays chunks round-trips
21
+ byte-for-byte unchanged, including large `PATH` displays' deduplicated `vectors/<bucketType>/`
22
+ vector store. `.xs` *generation* (building a new project from scratch) isn't implemented yet
23
+ — see `CLAUDE.md`'s "Open Issues".
24
+
25
+ ## [0.4.1] - 2026-09-19
26
+
27
+ ### Changed
28
+
29
+ - Version bump only — 0.4.0 itself was never published to npm (only consumed
30
+ ahead of release via a git dependency from `maker-toolkit`'s `apps/desktop`,
31
+ which pointed at this repo's `dist` branch as a workaround). No code changes
32
+ from 0.4.0; this is the first published release carrying the generation API
33
+ and curved-text layout described below.
34
+
35
+ ## [0.4.0] - 2026-09-19
36
+
37
+ ### Added
38
+
39
+ - **Generation API** (`src/builder.ts`): `XCSGenerator`/`createXCS` build a `.xcs` project from
40
+ scratch — canvas, layers, and `TEXT`/`PATH`/`BITMAP` displays — ported from `maker-toolkit`'s
41
+ previously-duplicated hand-rolled generator. Chainable `.addText()`/`.addPath()`/
42
+ `.addBitmap()`/`.addLayer()`, then `.generate()`/`.toJSON()`/`.toBytes()`.
43
+ - **Curved text layout** (`src/layout.ts`): `layoutCurvedGlyphText` lays a single line of text
44
+ out along a circular arc, baking each character's rotated position and glyph shape directly
45
+ (same approach `renderXcsFile` already uses for straight substituted text — xTool Studio only
46
+ trusts stored glyph shapes). Uses the same arc-geometry convention (chord width, sweep angle,
47
+ up/down sign, align-driven start offset) as SVG `<textPath>`/canvas curved-text editors, so a
48
+ document renders consistently across export formats. Not yet visually verified against real
49
+ xTool Studio output — see `CLAUDE.md`'s "Open Issues".
50
+ - `layoutGlyphText`/`layoutMultilineGlyphText`/`translateGlyphLayout`/`fontSizePoints`
51
+ (`src/layout.ts`): the higher-level, real-world-scaled/positioned layout helpers built on
52
+ `glyphs.ts`'s `layoutText`, also ported from `maker-toolkit` (previously duplicated
53
+ consumer-side). `effectiveCurveDeg` clamps/normalizes a curve angle (±355°, treats <1° as
54
+ straight).
55
+
56
+ ### Changed
57
+
58
+ - README's substitution "Known limitation" note narrowed to `renderXcsFile` specifically — it
59
+ still can't decode an existing curved display's `style.curveX`/`curveY`, but the new building
60
+ API doesn't have that problem (the caller supplies the angle directly).
61
+
8
62
  ## [0.3.1] - 2026-08-17
9
63
 
10
64
  ### Changed
package/README.md CHANGED
@@ -1,14 +1,19 @@
1
1
  # unofficial-xcs-writer
2
2
 
3
- A framework-agnostic TypeScript library for reading, writing, and applying variable substitution to `.xcs` files produced by [xTool Creative Space](https://www.xtool.com/pages/software). Not affiliated with xTool.
3
+ A framework-agnostic TypeScript library for reading, writing, building, and applying variable substitution to `.xcs` and `.xs` files produced by [xTool Creative Space](https://www.xtool.com/pages/software). Not affiliated with xTool.
4
4
 
5
- ## What is an XCS file?
5
+ ## What is an XCS / XS file?
6
6
 
7
- An `.xcs` file is a plain UTF-8 JSON document exported by xTool Creative Space. It describes a laser project — canvas dimensions, design objects (shapes, text, images), layer assignments, and device parameters. This library lets you:
7
+ An `.xcs` file is a plain UTF-8 JSON document exported by xTool Creative Space. It describes a laser project — canvas dimensions, design objects (shapes, text, images), layer assignments, and device parameters.
8
8
 
9
- - **Read** an XCS file and inspect its contents
9
+ Starting with xTool Studio v1.7, new projects are saved as `.xs` instead — a ZIP archive of several JSON files (project metadata, per-canvas display chunks, processing profiles, device bindings, and binary resources as real files rather than inline base64). `.xcs` is now legacy: v1.7+ can still open `.xcs` files but can no longer save back to that format. See `src/xs.ts` for the full format writeup.
10
+
11
+ This library lets you, for both formats:
12
+
13
+ - **Read** a file and inspect its contents
10
14
  - **Extract** `{{token}}` template placeholders from text objects
11
15
  - **Render** a filled-in copy by substituting values for those placeholders
16
+ - **Build** a new `.xcs` project from scratch — text (straight, multi-line, or curved), paths, and embedded images (`.xs` generation isn't supported yet — see `CLAUDE.md`'s "Open Issues")
12
17
 
13
18
  ## Installation
14
19
 
@@ -45,6 +50,53 @@ const values = { FirstName: 'Jane', LastName: 'Smith' };
45
50
  const output: Uint8Array = renderXcsFile(buffer, variables, values);
46
51
  ```
47
52
 
53
+ ### Working with `.xs` files
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
69
+
70
+ ```ts
71
+ import {
72
+ createXCS,
73
+ loadDefaultFont,
74
+ layoutGlyphText,
75
+ layoutCurvedGlyphText,
76
+ } from '@richardmcquiston01/unofficial-xcs-writer';
77
+
78
+ const font = loadDefaultFont();
79
+
80
+ const project = createXCS('P2S')
81
+ // Straight text needs a real glyph layout so xTool Studio renders
82
+ // actual outlines instead of placeholder blocks.
83
+ .addText('Hello', 10, 10, {
84
+ fontFamily: 'Arial',
85
+ fontSize: 24,
86
+ layout: layoutGlyphText(font, 'Hello', 8, 10, 10),
87
+ })
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
+ .addText('Arch Text', 0, 40, {
92
+ fontFamily: 'Arial',
93
+ fontSize: 18,
94
+ layout: layoutCurvedGlyphText(font, 'Arch Text', 6, 90, 60, 20, 'center'),
95
+ })
96
+ .addPath('M0 0L10 0L10 10L0 10Z', 0, 60, 10, 10)
97
+ .toBytes(); // Uint8Array, ready to write to a .xcs file
98
+ ```
99
+
48
100
  ## API
49
101
 
50
102
  ### `assertXcsFormat(buffer: ArrayBuffer): void`
@@ -74,7 +126,7 @@ renderXcsFile(buffer, variables, values, { Arial: arialBuffer });
74
126
 
75
127
  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.
76
128
 
77
- **Known limitation:** only straight horizontal-baseline layout is reproduced. xTool Studio's `style.curveX`/`curveY` curved-text layout formula is undocumented and isn't replicated — substituted text on a curved TEXT display gets correctly-shaped glyphs laid out on a straight line instead of the original curve.
129
+ **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.
78
130
 
79
131
  ### `XcsVariable`
80
132
 
@@ -85,9 +137,46 @@ interface XcsVariable {
85
137
  }
86
138
  ```
87
139
 
140
+ ### `.xs` (v2 workspace) format
141
+
142
+ ### `assertXsFormat(buffer: ArrayBuffer): void`
143
+
144
+ Validates that `buffer` is a ZIP archive with a `.format` entry equal to `"v2"` and a `project.json` entry. Throws a descriptive error otherwise.
145
+
146
+ ### `extractXsTokens(buffer: ArrayBuffer): string[]`
147
+
148
+ Same as `extractXcsTokens`, but scans every `canvases/<canvasId>/displays-<chunkIndex>.json` chunk across the archive instead of a single `canvas` array.
149
+
150
+ ### `renderXsFile(buffer, variables, values, fonts?): Uint8Array`
151
+
152
+ 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.
153
+
88
154
  ### `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
89
155
 
90
- Lower-level glyph-extraction primitives `renderXcsFile` is built on, exported for direct use (e.g. building a `TEXT` display from scratch). See `src/glyphs.ts` for the exact xTool JSON conventions these were reverse-engineered against.
156
+ 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.
157
+
158
+ ### Building (`src/builder.ts`, `src/layout.ts`)
159
+
160
+ `createXCS(deviceId?)` / `new XCSGenerator(options?)` — starts a new project (one canvas, one default layer). Chainable methods:
161
+
162
+ | Method | Adds |
163
+ |--------|------|
164
+ | `.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. |
165
+ | `.addPath(pathData, x, y, width, height, options?)` | A `PATH` display from SVG path data. |
166
+ | `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
167
+ | `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
168
+
169
+ Then `.generate()` (the plain `XCSFile` object), `.toJSON()` (string), or `.toBytes()` (`Uint8Array`, matching `renderXcsFile`'s output type).
170
+
171
+ Text layout — build the `layout` option for `.addText()` with one of:
172
+
173
+ | Function | Layout |
174
+ |----------|--------|
175
+ | `layoutGlyphText(font, text, emSizeMm, xMm, yMm, letterSpacingMm?)` | Single-line, straight, anchored so the ink box's top-left lands at `(xMm, yMm)`. |
176
+ | `layoutMultilineGlyphText(font, text, emSizeMm, letterSpacingMm, lineHeightMult, align)` | Multi-line (`\n`-separated), straight, each line aligned and vertically stacked. |
177
+ | `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. |
178
+
179
+ 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.
91
180
 
92
181
  ## Development
93
182
 
@@ -119,7 +208,8 @@ It needs an `NPM_TOKEN` repository secret with publish rights.
119
208
 
120
209
  - All inputs and outputs use plain browser-compatible types (`ArrayBuffer`, `Uint8Array`, `string`) — no Node.js APIs, no DOM, no framework required. (`atob`/`crypto.randomUUID` are used for the bundled font and glyph IDs; both are available in browsers and Node ≥ 19.)
121
210
  - Only `TEXT` display objects are modified during substitution. Geometry, bitmaps, device configuration, and all other fields pass through unchanged.
122
- - This library targets the current xTool Creative Space JSON format. Older versions of the software may export a different structure.
211
+ - This library targets the current xTool Creative Space JSON/`.xs` formats. Older versions of the software may export a different structure.
212
+ - `.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.
123
213
  - 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`).
124
214
 
125
215
  ## License