@richardmcquiston01/unofficial-xcs-writer 0.6.0 → 0.8.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,42 @@ 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.0] - 2026-09-21
11
+
12
+ ### Added
13
+
14
+ - **Machine catalog** (`src/machines.ts`): `XCSGeneratorOptions.deviceId`/`createXCS`'s argument
15
+ now accepts a `XTOOL_MACHINES` key -- currently 13 machines verified from real exports in this
16
+ repo (`P2S`, `F2 Ultra UV`, `S1`, `P3`, `M2`, `F2`, `F2 Ultra (Single)`, `F2 Ultra`, `M1 Ultra`,
17
+ `F1`, `F1 Lite`, `F1 Ultra`, `MetalFab`) -- and resolves that machine's real `extId`/`extName`/
18
+ `deviceCode` (`.xs` only)/default power, instead of reusing whatever string you pass for all of
19
+ them. A `MachineProfile` object can also be supplied directly for a machine not yet in the
20
+ catalog. Existing raw device id strings still behave exactly as before (unaffected, since they
21
+ simply don't match a catalog key).
22
+
23
+ ### Changed
24
+
25
+ - `createXCS()`/`new XCSGenerator()` with no `deviceId` (defaulting to `"P2S"`) now embeds the
26
+ real `deviceCode: "ZY013"` in `.toXsBytes()` output, instead of the previous placeholder
27
+ (`file.device.id`, i.e. `"P2S"`). `.xcs` output (`.toBytes()`) is unaffected -- it has no
28
+ `deviceCode` field.
29
+
30
+ ## [0.7.0] - 2026-09-21
31
+
32
+ ### Added
33
+
34
+ - **`.xs` processing-profile support**: `.addText()`/`.addPath()`/`.addBitmap()` accept a new
35
+ `processing: { processingType, values }` option (`src/builder.ts`), which `.toXsBytes()`
36
+ (`src/xs.ts`'s `buildXsArchive`, now taking an optional `XsProcessingBinding[]`) turns into a
37
+ real `profiles.json` entry plus a matching `LASER_PLANE`-mode binding in
38
+ `devices/device-<id>.json`. Displays given identical `processingType`+`values` are merged into
39
+ one shared profile/binding, matching how real xTool Studio exports dedupe identical settings.
40
+ `.toBytes()` (`.xcs` output) ignores this option -- `.xcs`'s own per-display processing format
41
+ is structurally different and much less understood, so it isn't implemented (see `CLAUDE.md`'s
42
+ Open Issues).
43
+
8
44
  ## [0.6.0] - 2026-09-21
9
45
 
10
46
  ### Added
package/README.md CHANGED
@@ -93,15 +93,46 @@ const project = createXCS('P2S')
93
93
  fontSize: 18,
94
94
  layout: layoutCurvedGlyphText(font, 'Arch Text', 6, 90, 60, 20, 'center'),
95
95
  })
96
- .addPath('M0 0L10 0L10 10L0 10Z', 0, 60, 10, 10)
96
+ .addPath('M0 0L10 0L10 10L0 10Z', 0, 60, 10, 10, {
97
+ // Only affects .toXsBytes() output -- see below.
98
+ processing: { processingType: 'VECTOR_CUTTING', values: { power: 80, speed: 10 } },
99
+ })
97
100
  .toBytes(); // Uint8Array, ready to write to a .xcs file
98
101
  ```
99
102
 
100
103
  Call `.toXsBytes()` instead of `.toBytes()` to export the same project as a `.xs` (v2 workspace)
101
- archive — same chainable builder, same display objects, different container. Processing
102
- profiles/device bindings aren't populated (there's no `addProfile`-style API yet), so a
103
- generated `.xs` file's power/speed may need to be set manually in xTool Studio before
104
- cutting/engraving.
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)`:
123
+
124
+ ```ts
125
+ import { createXCS, XTOOL_MACHINES } from '@richardmcquiston01/unofficial-xcs-writer';
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.
105
136
 
106
137
  ## API
107
138
 
@@ -165,9 +196,9 @@ Packages an already-`generate()`d project (see Building, below) into a `.xs` v2
165
196
 
166
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.
167
198
 
168
- ### Building (`src/builder.ts`, `src/layout.ts`)
199
+ ### Building (`src/builder.ts`, `src/layout.ts`, `src/machines.ts`)
169
200
 
170
- `createXCS(deviceId?)` / `new XCSGenerator(options?)` — starts a new project (one canvas, one default layer). Chainable methods:
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:
171
202
 
172
203
  | Method | Adds |
173
204
  |--------|------|
@@ -176,7 +207,9 @@ Lower-level glyph-extraction primitives everything else in this library is built
176
207
  | `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
177
208
  | `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
178
209
 
179
- 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` — see the note in "Building a project from scratch" above about unset processing profiles).
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`).
180
213
 
181
214
  Text layout — build the `layout` option for `.addText()` with one of:
182
215
 
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,
@@ -362,6 +363,36 @@ function renderXsFile(buffer, variables, values, fonts) {
362
363
  function encodeJson(value) {
363
364
  return new TextEncoder().encode(JSON.stringify(value));
364
365
  }
366
+ function generateXsId(prefix) {
367
+ const uuid = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
368
+ const r = Math.random() * 16 | 0;
369
+ const v = c === "x" ? r : r & 3 | 8;
370
+ return v.toString(16);
371
+ });
372
+ return `${prefix}:${uuid}`;
373
+ }
374
+ function buildProcessing(canvasId, bindings) {
375
+ const profiles = {};
376
+ const profileRefs = [];
377
+ const deviceBindings = [];
378
+ for (const binding of bindings) {
379
+ const profileId = generateXsId("profile");
380
+ profiles[profileId] = {
381
+ id: profileId,
382
+ processingType: binding.processingType,
383
+ values: binding.values
384
+ };
385
+ profileRefs.push(profileId);
386
+ deviceBindings.push({
387
+ bindingId: generateXsId("binding"),
388
+ canvasId,
389
+ mode: "LASER_PLANE",
390
+ baseProfileId: profileId,
391
+ displayIds: binding.displayIds
392
+ });
393
+ }
394
+ return { profiles, profileRefs, deviceBindings };
395
+ }
365
396
  function decodeDataUrl(dataUrl) {
366
397
  const match = /^data:([^;]+);base64,(.*)$/.exec(dataUrl);
367
398
  if (!match) return null;
@@ -386,11 +417,15 @@ function coverResourceEntries(coverDataUrl) {
386
417
  })
387
418
  };
388
419
  }
389
- function buildXsArchive(file) {
420
+ function buildXsArchive(file, processingBindings = [], deviceCode = file.device.id) {
390
421
  const canvas = file.canvas[0];
391
422
  if (!canvas) {
392
423
  throw new Error("Cannot build a .xs archive from a project with no canvas.");
393
424
  }
425
+ const { profiles, profileRefs, deviceBindings } = buildProcessing(
426
+ canvas.id,
427
+ processingBindings
428
+ );
394
429
  return (0, import_fflate.zipSync)({
395
430
  ".format": new TextEncoder().encode("v2"),
396
431
  "meta/persistence-meta.json": encodeJson({
@@ -421,7 +456,7 @@ function buildXsArchive(file) {
421
456
  cover: "resources/project-cover.png",
422
457
  customProjectData: { projectTraceID: file.projectTraceID }
423
458
  }),
424
- "profiles.json": encodeJson({ profiles: {} }),
459
+ "profiles.json": encodeJson({ profiles }),
425
460
  [`canvases/${canvas.id}.json`]: encodeJson({
426
461
  id: canvas.id,
427
462
  title: canvas.title,
@@ -444,7 +479,7 @@ function buildXsArchive(file) {
444
479
  }),
445
480
  [`devices/device-${file.device.id}.json`]: encodeJson({
446
481
  id: file.device.id,
447
- deviceCode: file.device.id,
482
+ deviceCode,
448
483
  extId: file.extId,
449
484
  extName: file.extName,
450
485
  power: [file.device.power],
@@ -468,9 +503,9 @@ function buildXsArchive(file) {
468
503
  focalLen: null,
469
504
  focalLength: null
470
505
  },
471
- profileRefs: [],
506
+ profileRefs,
472
507
  patches: {},
473
- bindings: []
508
+ bindings: deviceBindings
474
509
  }
475
510
  }
476
511
  }
@@ -732,6 +767,101 @@ function layoutCurvedGlyphText(font, text, emSizeMm, curveDeg, boxWidthMm, boxHe
732
767
  };
733
768
  }
734
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
+
735
865
  // src/builder.ts
736
866
  var XCSGenerator = class {
737
867
  canvasId;
@@ -739,10 +869,20 @@ var XCSGenerator = class {
739
869
  displays = [];
740
870
  layers = /* @__PURE__ */ new Map();
741
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;
876
+ /** Keyed by `JSON.stringify([processingType, values])` to merge displays sharing identical settings into one profile. */
877
+ processingBindings = /* @__PURE__ */ new Map();
742
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;
743
883
  this.options = {
744
- deviceId: options.deviceId || "P2S",
745
- devicePower: options.devicePower || 55,
884
+ deviceId,
885
+ devicePower: options.devicePower || machine?.defaultPower || 55,
746
886
  canvasWidth: options.canvasWidth || 400,
747
887
  canvasHeight: options.canvasHeight || 400
748
888
  };
@@ -750,6 +890,11 @@ var XCSGenerator = class {
750
890
  this.canvas = this.createCanvas();
751
891
  this.addLayer("#00befe", "{Cyan}", 1);
752
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
+ }
753
898
  /**
754
899
  * Add a text object to the canvas. Pass `layout` (built via
755
900
  * `layoutGlyphText`/`layoutMultilineGlyphText`/`layoutCurvedGlyphText`
@@ -760,6 +905,7 @@ var XCSGenerator = class {
760
905
  addText(text, x, y, options = {}) {
761
906
  const textObj = this.createTextObject(text, x, y, options);
762
907
  this.displays.push(textObj);
908
+ this.registerProcessing(textObj.id, options.processing);
763
909
  return this;
764
910
  }
765
911
  /**
@@ -768,6 +914,7 @@ var XCSGenerator = class {
768
914
  addPath(pathData, x, y, width, height, options = {}) {
769
915
  const pathObj = this.createPathObject(pathData, x, y, width, height, options);
770
916
  this.displays.push(pathObj);
917
+ this.registerProcessing(pathObj.id, options.processing);
771
918
  return this;
772
919
  }
773
920
  /**
@@ -851,6 +998,7 @@ var XCSGenerator = class {
851
998
  opacity: 1
852
999
  };
853
1000
  this.displays.push(bitmap);
1001
+ this.registerProcessing(bitmap.id, options.processing);
854
1002
  return this;
855
1003
  }
856
1004
  /**
@@ -875,7 +1023,7 @@ var XCSGenerator = class {
875
1023
  canvasId: this.canvasId,
876
1024
  canvas: [this.canvas],
877
1025
  extId: this.options.deviceId,
878
- extName: this.options.deviceId,
1026
+ extName: this.extName,
879
1027
  device: {
880
1028
  id: this.options.deviceId,
881
1029
  power: this.options.devicePower,
@@ -913,13 +1061,34 @@ var XCSGenerator = class {
913
1061
  }
914
1062
  /**
915
1063
  * Export as a `.xs` (v2 workspace) ZIP archive instead of `.xcs` --
916
- * see `buildXsArchive` in `src/xs.ts` for the container format and
917
- * its limitations (no processing profiles/device bindings yet).
1064
+ * see `buildXsArchive` in `src/xs.ts` for the container format.
918
1065
  * Display objects themselves carry over unchanged; only the
919
- * 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.
920
1070
  */
921
1071
  toXsBytes() {
922
- return buildXsArchive(this.generate());
1072
+ return buildXsArchive(
1073
+ this.generate(),
1074
+ Array.from(this.processingBindings.values()),
1075
+ this.deviceCode
1076
+ );
1077
+ }
1078
+ /** Merges a display into an existing profile when another display already shares its exact `processing` settings, else starts a new one. */
1079
+ registerProcessing(displayId, processing) {
1080
+ if (!processing) return;
1081
+ const key = JSON.stringify([processing.processingType, processing.values]);
1082
+ const existing = this.processingBindings.get(key);
1083
+ if (existing) {
1084
+ existing.displayIds.push(displayId);
1085
+ return;
1086
+ }
1087
+ this.processingBindings.set(key, {
1088
+ processingType: processing.processingType,
1089
+ values: processing.values,
1090
+ displayIds: [displayId]
1091
+ });
923
1092
  }
924
1093
  createCanvas() {
925
1094
  return {
@@ -1154,6 +1323,7 @@ function createXCS(deviceId = "P2S") {
1154
1323
  // Annotate the CommonJS export names for ESM import in node:
1155
1324
  0 && (module.exports = {
1156
1325
  XCSGenerator,
1326
+ XTOOL_MACHINES,
1157
1327
  assertXcsFormat,
1158
1328
  assertXsFormat,
1159
1329
  buildXsArchive,