@richardmcquiston01/unofficial-xcs-writer 0.7.0 → 0.8.1

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 ADDED
@@ -0,0 +1,86 @@
1
+ # API Reference
2
+
3
+ ## `assertXcsFormat(buffer: ArrayBuffer): void`
4
+
5
+ Validates that `buffer` is a parseable XCS JSON file containing a `canvas` array. Throws a descriptive error if the file is invalid or unrecognized.
6
+
7
+ ## `readXcsFile(buffer: ArrayBuffer): string`
8
+
9
+ Decodes the buffer and returns the raw JSON string. Useful for inspection or pass-through scenarios.
10
+
11
+ ## `extractXcsTokens(buffer: ArrayBuffer): string[]`
12
+
13
+ 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.
14
+
15
+ ## `renderXcsFile(buffer, variables, values, fonts?): Uint8Array`
16
+
17
+ 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.
18
+
19
+ 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.
20
+
21
+ Pass `fonts` to supply real font file bytes (TTF/OTF/WOFF) keyed by a display's `style.fontFamily`:
22
+
23
+ ```ts
24
+ const arialBuffer = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
25
+ renderXcsFile(buffer, variables, values, { Arial: arialBuffer });
26
+ ```
27
+
28
+ 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.
29
+
30
+ **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.
31
+
32
+ ## `XcsVariable`
33
+
34
+ ```ts
35
+ interface XcsVariable {
36
+ token: string; // token name without braces, e.g. "LastName"
37
+ defaultValue?: string;
38
+ }
39
+ ```
40
+
41
+ ## `.xs` (v2 workspace) format
42
+
43
+ ## `assertXsFormat(buffer: ArrayBuffer): void`
44
+
45
+ Validates that `buffer` is a ZIP archive with a `.format` entry equal to `"v2"` and a `project.json` entry. Throws a descriptive error otherwise.
46
+
47
+ ## `extractXsTokens(buffer: ArrayBuffer): string[]`
48
+
49
+ Same as `extractXcsTokens`, but scans every `canvases/<canvasId>/displays-<chunkIndex>.json` chunk across the archive instead of a single `canvas` array.
50
+
51
+ ## `renderXsFile(buffer, variables, values, fonts?): Uint8Array`
52
+
53
+ 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.
54
+
55
+ ## `buildXsArchive(file: XCSFile): Uint8Array`
56
+
57
+ 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.
58
+
59
+ ## `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
60
+
61
+ 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.
62
+
63
+ ## Building (`src/builder.ts`, `src/layout.ts`, `src/machines.ts`)
64
+
65
+ `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:
66
+
67
+ | Method | Adds |
68
+ |--------|------|
69
+ | `.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. |
70
+ | `.addPath(pathData, x, y, width, height, options?)` | A `PATH` display from SVG path data. |
71
+ | `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
72
+ | `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
73
+
74
+ `.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.
75
+
76
+ 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`).
77
+
78
+ Text layout — build the `layout` option for `.addText()` with one of:
79
+
80
+ | Function | Layout |
81
+ |----------|--------|
82
+ | `layoutGlyphText(font, text, emSizeMm, xMm, yMm, letterSpacingMm?)` | Single-line, straight, anchored so the ink box's top-left lands at `(xMm, yMm)`. |
83
+ | `layoutMultilineGlyphText(font, text, emSizeMm, letterSpacingMm, lineHeightMult, align)` | Multi-line (`\n`-separated), straight, each line aligned and vertically stacked. |
84
+ | `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. |
85
+
86
+ 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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,43 @@ 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
+ ## [Unreleased]
9
+
10
+ ## [0.8.1] - 2026-09-21
11
+
12
+ ### Changed
13
+
14
+ - **README restructure**: removed the "Releasing" subsection (internal maintainer process,
15
+ not user-facing); moved the full "API" section into a new `API.md` (added to `package.json`'s
16
+ `files` so it ships with the npm package), with the README's "API" section now just a link to
17
+ it; added a disclaimer blockquote under the title stating this is an unofficial,
18
+ community-built library not affiliated with xTool; removed the hyperlink on "xTool Creative
19
+ Space" in the intro paragraph (kept as plain text); moved "Buy Me a Coffee" up to sit directly
20
+ above the API section instead of at the very end.
21
+ - **Copyright section**: added a note that xTool, xTool Studio, and xTool Creative Space are
22
+ registered trademarks of Makeblock Co., Ltd., and that this project isn't affiliated with,
23
+ endorsed by, or sponsored by Makeblock or xTool.
24
+
25
+ ## [0.8.0] - 2026-09-21
26
+
27
+ ### Added
28
+
29
+ - **Machine catalog** (`src/machines.ts`): `XCSGeneratorOptions.deviceId`/`createXCS`'s argument
30
+ now accepts a `XTOOL_MACHINES` key -- currently 13 machines verified from real exports in this
31
+ repo (`P2S`, `F2 Ultra UV`, `S1`, `P3`, `M2`, `F2`, `F2 Ultra (Single)`, `F2 Ultra`, `M1 Ultra`,
32
+ `F1`, `F1 Lite`, `F1 Ultra`, `MetalFab`) -- and resolves that machine's real `extId`/`extName`/
33
+ `deviceCode` (`.xs` only)/default power, instead of reusing whatever string you pass for all of
34
+ them. A `MachineProfile` object can also be supplied directly for a machine not yet in the
35
+ catalog. Existing raw device id strings still behave exactly as before (unaffected, since they
36
+ simply don't match a catalog key).
37
+
38
+ ### Changed
39
+
40
+ - `createXCS()`/`new XCSGenerator()` with no `deviceId` (defaulting to `"P2S"`) now embeds the
41
+ real `deviceCode: "ZY013"` in `.toXsBytes()` output, instead of the previous placeholder
42
+ (`file.device.id`, i.e. `"P2S"`). `.xcs` output (`.toBytes()`) is unaffected -- it has no
43
+ `deviceCode` field.
44
+
8
45
  ## [0.7.0] - 2026-09-21
9
46
 
10
47
  ### Added
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # unofficial-xcs-writer
2
2
 
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.
3
+ > **Disclaimer:** This is an unofficial, community-built library. It is not affiliated with, endorsed by, or sponsored by xTool in any way.
4
+
5
+ A framework-agnostic TypeScript library for reading, writing, building, and applying variable substitution to `.xcs` and `.xs` files produced by xTool Creative Space.
4
6
 
5
7
  ## What is an XCS / XS file?
6
8
 
@@ -113,92 +115,38 @@ and undocumented beyond real exports — see `xs_samples/*.xs`'s `profiles.json`
113
115
  option behave as before: a generated `.xs` file's power/speed may need to be set manually in
114
116
  xTool Studio before cutting/engraving.
115
117
 
116
- ## API
117
-
118
- ### `assertXcsFormat(buffer: ArrayBuffer): void`
119
-
120
- Validates that `buffer` is a parseable XCS JSON file containing a `canvas` array. Throws a descriptive error if the file is invalid or unrecognized.
121
-
122
- ### `readXcsFile(buffer: ArrayBuffer): string`
123
-
124
- Decodes the buffer and returns the raw JSON string. Useful for inspection or pass-through scenarios.
125
-
126
- ### `extractXcsTokens(buffer: ArrayBuffer): string[]`
127
-
128
- 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.
129
-
130
- ### `renderXcsFile(buffer, variables, values, fonts?): Uint8Array`
131
-
132
- 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.
133
-
134
- 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.
135
-
136
- Pass `fonts` to supply real font file bytes (TTF/OTF/WOFF) keyed by a display's `style.fontFamily`:
118
+ `createXCS('P2S')`'s argument (and `XCSGeneratorOptions.deviceId`) accepts a known machine name
119
+ from `XTOOL_MACHINES` — currently 13 machines verified from real exports in this repo (`"P2S"`,
120
+ `"F2 Ultra UV"`, `"S1"`, `"P3"`, `"M2"`, `"F2"`, `"F2 Ultra (Single)"`, `"F2 Ultra"`,
121
+ `"M1 Ultra"`, `"F1"`, `"F1 Lite"`, `"F1 Ultra"`, `"MetalFab"`) — and resolves the real
122
+ `extId`/`extName`/`deviceCode` (`.xs` only)/default power for that machine, instead of reusing
123
+ whatever string you pass for all of them. Export the same design for multiple machines by
124
+ building it once and calling `.toXsBytes()`/`.toBytes()` once per `createXCS(machineName)`:
137
125
 
138
126
  ```ts
139
- const arialBuffer = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
140
- renderXcsFile(buffer, variables, values, { Arial: arialBuffer });
141
- ```
142
-
143
- 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.
144
-
145
- **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.
146
-
147
- ### `XcsVariable`
127
+ import { createXCS, XTOOL_MACHINES } from '@richardmcquiston01/unofficial-xcs-writer';
148
128
 
149
- ```ts
150
- interface XcsVariable {
151
- token: string; // token name without braces, e.g. "LastName"
152
- defaultValue?: string;
129
+ for (const machine of Object.keys(XTOOL_MACHINES)) {
130
+ const bytes = createXCS(machine).addPath('M0 0L10 0L10 10L0 10Z', 0, 0, 10, 10).toXsBytes();
131
+ // write `bytes` to e.g. `design-${machine}.xs`
153
132
  }
154
133
  ```
155
134
 
156
- ### `.xs` (v2 workspace) format
157
-
158
- ### `assertXsFormat(buffer: ArrayBuffer): void`
159
-
160
- Validates that `buffer` is a ZIP archive with a `.format` entry equal to `"v2"` and a `project.json` entry. Throws a descriptive error otherwise.
161
-
162
- ### `extractXsTokens(buffer: ArrayBuffer): string[]`
163
-
164
- Same as `extractXcsTokens`, but scans every `canvases/<canvasId>/displays-<chunkIndex>.json` chunk across the archive instead of a single `canvas` array.
135
+ A device id not in the catalog still works exactly as before (no `.xs` `deviceCode`, `extId`/
136
+ `extName`/`device.id` all set to that string, default power `55`); you can also pass a
137
+ `MachineProfile` object directly for a machine not yet in the catalog.
165
138
 
166
- ### `renderXsFile(buffer, variables, values, fonts?): Uint8Array`
167
-
168
- 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.
169
-
170
- ### `buildXsArchive(file: XCSFile): Uint8Array`
171
-
172
- 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.
173
-
174
- ### `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
175
-
176
- 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.
177
-
178
- ### Building (`src/builder.ts`, `src/layout.ts`)
179
-
180
- `createXCS(deviceId?)` / `new XCSGenerator(options?)` — starts a new project (one canvas, one default layer). Chainable methods:
181
-
182
- | Method | Adds |
183
- |--------|------|
184
- | `.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. |
185
- | `.addPath(pathData, x, y, width, height, options?)` | A `PATH` display from SVG path data. |
186
- | `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
187
- | `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
139
+ ## Buy Me a Coffee
188
140
 
189
- `.addText()`/`.addPath()`/`.addBitmap()` all also accept a `processing: { processingType, values }` option — see "Building a project from scratch" above; it only affects `.toXsBytes()` output.
141
+ 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.
190
142
 
191
- 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`).
143
+ [**Donate via Stripe**](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800), or scan:
192
144
 
193
- Text layout — build the `layout` option for `.addText()` with one of:
145
+ [![Donate via Stripe](./donate.svg)](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800)
194
146
 
195
- | Function | Layout |
196
- |----------|--------|
197
- | `layoutGlyphText(font, text, emSizeMm, xMm, yMm, letterSpacingMm?)` | Single-line, straight, anchored so the ink box's top-left lands at `(xMm, yMm)`. |
198
- | `layoutMultilineGlyphText(font, text, emSizeMm, letterSpacingMm, lineHeightMult, align)` | Multi-line (`\n`-separated), straight, each line aligned and vertically stacked. |
199
- | `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. |
147
+ ## API
200
148
 
201
- 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.
149
+ See [API.md](./API.md) for the full API reference — function signatures, options, types, and important behavior notes (glyph regeneration, curved-text limitations, machine catalog usage, etc.).
202
150
 
203
151
  ## Development
204
152
 
@@ -218,14 +166,6 @@ Output files after `pnpm build`:
218
166
  | `dist/index.cjs` | CommonJS |
219
167
  | `dist/index.d.ts` | TypeScript declarations |
220
168
 
221
- ### Releasing
222
-
223
- Publishing to npm is automated. Bump the version in `package.json`, add a
224
- `CHANGELOG.md` entry, then create a GitHub release tagged `vX.Y.Z` — the
225
- [publish workflow](.github/workflows/publish.yml) checks the tag against
226
- `package.json`, rebuilds, runs the tests, and publishes with provenance.
227
- It needs an `NPM_TOKEN` repository secret with publish rights.
228
-
229
169
  ## Notes
230
170
 
231
171
  - 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.)
@@ -238,10 +178,8 @@ It needs an `NPM_TOKEN` repository secret with publish rights.
238
178
 
239
179
  [MIT](LICENSE). The bundled Arimo fallback font is separately licensed under Apache-2.0 (`third_party/arimo/LICENSE`).
240
180
 
241
- ## Buy Me a Coffee
181
+ ## Copyright
242
182
 
243
- 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.
183
+ (c)2026 Richard McQuiston
244
184
 
245
- [**Donate via Stripe**](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800), or scan:
246
-
247
- [![Donate via Stripe](./donate.svg)](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800)
185
+ xTool, xTool Studio, and xTool Creative Space are registered trademarks of Makeblock Co., Ltd. This project is not affiliated with, endorsed by, or sponsored by Makeblock Co., Ltd. or xTool.
package/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  XCSGenerator: () => XCSGenerator,
34
+ XTOOL_MACHINES: () => XTOOL_MACHINES,
34
35
  assertXcsFormat: () => assertXcsFormat,
35
36
  assertXsFormat: () => assertXsFormat,
36
37
  buildXsArchive: () => buildXsArchive,
@@ -416,7 +417,7 @@ function coverResourceEntries(coverDataUrl) {
416
417
  })
417
418
  };
418
419
  }
419
- function buildXsArchive(file, processingBindings = []) {
420
+ function buildXsArchive(file, processingBindings = [], deviceCode = file.device.id) {
420
421
  const canvas = file.canvas[0];
421
422
  if (!canvas) {
422
423
  throw new Error("Cannot build a .xs archive from a project with no canvas.");
@@ -478,7 +479,7 @@ function buildXsArchive(file, processingBindings = []) {
478
479
  }),
479
480
  [`devices/device-${file.device.id}.json`]: encodeJson({
480
481
  id: file.device.id,
481
- deviceCode: file.device.id,
482
+ deviceCode,
482
483
  extId: file.extId,
483
484
  extName: file.extName,
484
485
  power: [file.device.power],
@@ -766,6 +767,101 @@ function layoutCurvedGlyphText(font, text, emSizeMm, curveDeg, boxWidthMm, boxHe
766
767
  };
767
768
  }
768
769
 
770
+ // src/machines.ts
771
+ var XTOOL_MACHINES = {
772
+ P2S: {
773
+ name: "P2S",
774
+ extId: "P2S",
775
+ extName: "P2S",
776
+ deviceCode: "ZY013",
777
+ defaultPower: 55
778
+ },
779
+ "F2 Ultra UV": {
780
+ name: "F2 Ultra UV",
781
+ extId: "GS009-CLASS-4",
782
+ extName: "F2 Ultra UV",
783
+ deviceCode: "GS009-CLASS-4",
784
+ defaultPower: 5
785
+ },
786
+ S1: {
787
+ name: "S1",
788
+ extId: "S1",
789
+ extName: "S1",
790
+ deviceCode: "MD2",
791
+ defaultPower: 40
792
+ },
793
+ P3: {
794
+ name: "P3",
795
+ extId: "P3",
796
+ extName: "P3",
797
+ deviceCode: "ZY015",
798
+ defaultPower: 80
799
+ },
800
+ M2: {
801
+ name: "M2",
802
+ extId: "JS002",
803
+ extName: "M2",
804
+ deviceCode: "JS002",
805
+ defaultPower: 10
806
+ },
807
+ F2: {
808
+ name: "F2",
809
+ extId: "GS006",
810
+ extName: "F2",
811
+ deviceCode: "GS006",
812
+ defaultPower: 5
813
+ },
814
+ "F2 Ultra (Single)": {
815
+ name: "F2 Ultra (Single)",
816
+ extId: "GS007-CLASS-4",
817
+ extName: "F2 Ultra (Single)",
818
+ deviceCode: "GS007-CLASS-4",
819
+ defaultPower: 60
820
+ },
821
+ "F2 Ultra": {
822
+ name: "F2 Ultra",
823
+ extId: "GS004-CLASS-4",
824
+ extName: "F2 Ultra",
825
+ deviceCode: "GS004-CLASS-4",
826
+ defaultPower: 60
827
+ },
828
+ "M1 Ultra": {
829
+ name: "M1 Ultra",
830
+ extId: "M1Ultra",
831
+ extName: "M1 Ultra",
832
+ deviceCode: "ZH009",
833
+ defaultPower: 20
834
+ },
835
+ F1: {
836
+ name: "F1",
837
+ extId: "F1",
838
+ extName: "F1",
839
+ deviceCode: "MF1",
840
+ defaultPower: 10
841
+ },
842
+ "F1 Lite": {
843
+ name: "F1 Lite",
844
+ extId: "GS005",
845
+ extName: "F1 Lite",
846
+ deviceCode: "GS005",
847
+ defaultPower: 10
848
+ },
849
+ "F1 Ultra": {
850
+ name: "F1 Ultra",
851
+ extId: "F1Ultra",
852
+ extName: "F1 Ultra",
853
+ deviceCode: "GS002",
854
+ defaultPower: 20
855
+ },
856
+ MetalFab: {
857
+ name: "MetalFab",
858
+ extId: "HJ003",
859
+ extName: "MetalFab CNC Cutter",
860
+ deviceCode: "HJ003",
861
+ defaultPower: 1200
862
+ }
863
+ };
864
+
769
865
  // src/builder.ts
770
866
  var XCSGenerator = class {
771
867
  canvasId;
@@ -773,12 +869,20 @@ var XCSGenerator = class {
773
869
  displays = [];
774
870
  layers = /* @__PURE__ */ new Map();
775
871
  options;
872
+ /** From `XTOOL_MACHINES`/a supplied `MachineProfile`; `deviceId` itself when the machine isn't known. */
873
+ extName;
874
+ /** `.xs`-only; unset when the machine's real `deviceCode` isn't verified (see `src/machines.ts`). */
875
+ deviceCode;
776
876
  /** Keyed by `JSON.stringify([processingType, values])` to merge displays sharing identical settings into one profile. */
777
877
  processingBindings = /* @__PURE__ */ new Map();
778
878
  constructor(options = {}) {
879
+ const machine = this.resolveMachine(options.deviceId);
880
+ const deviceId = machine?.extId ?? (typeof options.deviceId === "string" ? options.deviceId : "P2S");
881
+ this.extName = machine?.extName ?? deviceId;
882
+ this.deviceCode = machine?.deviceCode;
779
883
  this.options = {
780
- deviceId: options.deviceId || "P2S",
781
- devicePower: options.devicePower || 55,
884
+ deviceId,
885
+ devicePower: options.devicePower || machine?.defaultPower || 55,
782
886
  canvasWidth: options.canvasWidth || 400,
783
887
  canvasHeight: options.canvasHeight || 400
784
888
  };
@@ -786,6 +890,11 @@ var XCSGenerator = class {
786
890
  this.canvas = this.createCanvas();
787
891
  this.addLayer("#00befe", "{Cyan}", 1);
788
892
  }
893
+ /** Looks `deviceId` up in `XTOOL_MACHINES` by name, passes a supplied `MachineProfile` through, or returns `undefined` for a raw/unknown device id (including the "no option given" default of `"P2S"`). */
894
+ resolveMachine(deviceId) {
895
+ if (typeof deviceId === "object") return deviceId;
896
+ return XTOOL_MACHINES[deviceId ?? "P2S"];
897
+ }
789
898
  /**
790
899
  * Add a text object to the canvas. Pass `layout` (built via
791
900
  * `layoutGlyphText`/`layoutMultilineGlyphText`/`layoutCurvedGlyphText`
@@ -914,7 +1023,7 @@ var XCSGenerator = class {
914
1023
  canvasId: this.canvasId,
915
1024
  canvas: [this.canvas],
916
1025
  extId: this.options.deviceId,
917
- extName: this.options.deviceId,
1026
+ extName: this.extName,
918
1027
  device: {
919
1028
  id: this.options.deviceId,
920
1029
  power: this.options.devicePower,
@@ -952,13 +1061,19 @@ var XCSGenerator = class {
952
1061
  }
953
1062
  /**
954
1063
  * Export as a `.xs` (v2 workspace) ZIP archive instead of `.xcs` --
955
- * see `buildXsArchive` in `src/xs.ts` for the container format and
956
- * its limitations (no processing profiles/device bindings yet).
1064
+ * see `buildXsArchive` in `src/xs.ts` for the container format.
957
1065
  * Display objects themselves carry over unchanged; only the
958
- * container differs from `toBytes()`.
1066
+ * container differs from `toBytes()`. `deviceCode` comes from
1067
+ * `XTOOL_MACHINES`/a supplied `MachineProfile` (see
1068
+ * `XCSGeneratorOptions.deviceId`) when known, else `buildXsArchive`
1069
+ * falls back to the device id itself, as before.
959
1070
  */
960
1071
  toXsBytes() {
961
- return buildXsArchive(this.generate(), Array.from(this.processingBindings.values()));
1072
+ return buildXsArchive(
1073
+ this.generate(),
1074
+ Array.from(this.processingBindings.values()),
1075
+ this.deviceCode
1076
+ );
962
1077
  }
963
1078
  /** Merges a display into an existing profile when another display already shares its exact `processing` settings, else starts a new one. */
964
1079
  registerProcessing(displayId, processing) {
@@ -1208,6 +1323,7 @@ function createXCS(deviceId = "P2S") {
1208
1323
  // Annotate the CommonJS export names for ESM import in node:
1209
1324
  0 && (module.exports = {
1210
1325
  XCSGenerator,
1326
+ XTOOL_MACHINES,
1211
1327
  assertXcsFormat,
1212
1328
  assertXsFormat,
1213
1329
  buildXsArchive,