@richardmcquiston01/unofficial-xcs-writer 0.6.0 → 0.7.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,20 @@ 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.7.0] - 2026-09-21
9
+
10
+ ### Added
11
+
12
+ - **`.xs` processing-profile support**: `.addText()`/`.addPath()`/`.addBitmap()` accept a new
13
+ `processing: { processingType, values }` option (`src/builder.ts`), which `.toXsBytes()`
14
+ (`src/xs.ts`'s `buildXsArchive`, now taking an optional `XsProcessingBinding[]`) turns into a
15
+ real `profiles.json` entry plus a matching `LASER_PLANE`-mode binding in
16
+ `devices/device-<id>.json`. Displays given identical `processingType`+`values` are merged into
17
+ one shared profile/binding, matching how real xTool Studio exports dedupe identical settings.
18
+ `.toBytes()` (`.xcs` output) ignores this option -- `.xcs`'s own per-display processing format
19
+ is structurally different and much less understood, so it isn't implemented (see `CLAUDE.md`'s
20
+ Open Issues).
21
+
8
22
  ## [0.6.0] - 2026-09-21
9
23
 
10
24
  ### Added
package/README.md CHANGED
@@ -93,15 +93,25 @@ 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.
105
115
 
106
116
  ## API
107
117
 
@@ -176,7 +186,9 @@ Lower-level glyph-extraction primitives everything else in this library is built
176
186
  | `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
177
187
  | `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
178
188
 
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).
189
+ `.addText()`/`.addPath()`/`.addBitmap()` all also accept a `processing: { processingType, values }` option — see "Building a project from scratch" above; it only affects `.toXsBytes()` output.
190
+
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`).
180
192
 
181
193
  Text layout — build the `layout` option for `.addText()` with one of:
182
194
 
package/dist/index.cjs CHANGED
@@ -362,6 +362,36 @@ function renderXsFile(buffer, variables, values, fonts) {
362
362
  function encodeJson(value) {
363
363
  return new TextEncoder().encode(JSON.stringify(value));
364
364
  }
365
+ function generateXsId(prefix) {
366
+ const uuid = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
367
+ const r = Math.random() * 16 | 0;
368
+ const v = c === "x" ? r : r & 3 | 8;
369
+ return v.toString(16);
370
+ });
371
+ return `${prefix}:${uuid}`;
372
+ }
373
+ function buildProcessing(canvasId, bindings) {
374
+ const profiles = {};
375
+ const profileRefs = [];
376
+ const deviceBindings = [];
377
+ for (const binding of bindings) {
378
+ const profileId = generateXsId("profile");
379
+ profiles[profileId] = {
380
+ id: profileId,
381
+ processingType: binding.processingType,
382
+ values: binding.values
383
+ };
384
+ profileRefs.push(profileId);
385
+ deviceBindings.push({
386
+ bindingId: generateXsId("binding"),
387
+ canvasId,
388
+ mode: "LASER_PLANE",
389
+ baseProfileId: profileId,
390
+ displayIds: binding.displayIds
391
+ });
392
+ }
393
+ return { profiles, profileRefs, deviceBindings };
394
+ }
365
395
  function decodeDataUrl(dataUrl) {
366
396
  const match = /^data:([^;]+);base64,(.*)$/.exec(dataUrl);
367
397
  if (!match) return null;
@@ -386,11 +416,15 @@ function coverResourceEntries(coverDataUrl) {
386
416
  })
387
417
  };
388
418
  }
389
- function buildXsArchive(file) {
419
+ function buildXsArchive(file, processingBindings = []) {
390
420
  const canvas = file.canvas[0];
391
421
  if (!canvas) {
392
422
  throw new Error("Cannot build a .xs archive from a project with no canvas.");
393
423
  }
424
+ const { profiles, profileRefs, deviceBindings } = buildProcessing(
425
+ canvas.id,
426
+ processingBindings
427
+ );
394
428
  return (0, import_fflate.zipSync)({
395
429
  ".format": new TextEncoder().encode("v2"),
396
430
  "meta/persistence-meta.json": encodeJson({
@@ -421,7 +455,7 @@ function buildXsArchive(file) {
421
455
  cover: "resources/project-cover.png",
422
456
  customProjectData: { projectTraceID: file.projectTraceID }
423
457
  }),
424
- "profiles.json": encodeJson({ profiles: {} }),
458
+ "profiles.json": encodeJson({ profiles }),
425
459
  [`canvases/${canvas.id}.json`]: encodeJson({
426
460
  id: canvas.id,
427
461
  title: canvas.title,
@@ -468,9 +502,9 @@ function buildXsArchive(file) {
468
502
  focalLen: null,
469
503
  focalLength: null
470
504
  },
471
- profileRefs: [],
505
+ profileRefs,
472
506
  patches: {},
473
- bindings: []
507
+ bindings: deviceBindings
474
508
  }
475
509
  }
476
510
  }
@@ -739,6 +773,8 @@ var XCSGenerator = class {
739
773
  displays = [];
740
774
  layers = /* @__PURE__ */ new Map();
741
775
  options;
776
+ /** Keyed by `JSON.stringify([processingType, values])` to merge displays sharing identical settings into one profile. */
777
+ processingBindings = /* @__PURE__ */ new Map();
742
778
  constructor(options = {}) {
743
779
  this.options = {
744
780
  deviceId: options.deviceId || "P2S",
@@ -760,6 +796,7 @@ var XCSGenerator = class {
760
796
  addText(text, x, y, options = {}) {
761
797
  const textObj = this.createTextObject(text, x, y, options);
762
798
  this.displays.push(textObj);
799
+ this.registerProcessing(textObj.id, options.processing);
763
800
  return this;
764
801
  }
765
802
  /**
@@ -768,6 +805,7 @@ var XCSGenerator = class {
768
805
  addPath(pathData, x, y, width, height, options = {}) {
769
806
  const pathObj = this.createPathObject(pathData, x, y, width, height, options);
770
807
  this.displays.push(pathObj);
808
+ this.registerProcessing(pathObj.id, options.processing);
771
809
  return this;
772
810
  }
773
811
  /**
@@ -851,6 +889,7 @@ var XCSGenerator = class {
851
889
  opacity: 1
852
890
  };
853
891
  this.displays.push(bitmap);
892
+ this.registerProcessing(bitmap.id, options.processing);
854
893
  return this;
855
894
  }
856
895
  /**
@@ -919,7 +958,22 @@ var XCSGenerator = class {
919
958
  * container differs from `toBytes()`.
920
959
  */
921
960
  toXsBytes() {
922
- return buildXsArchive(this.generate());
961
+ return buildXsArchive(this.generate(), Array.from(this.processingBindings.values()));
962
+ }
963
+ /** Merges a display into an existing profile when another display already shares its exact `processing` settings, else starts a new one. */
964
+ registerProcessing(displayId, processing) {
965
+ if (!processing) return;
966
+ const key = JSON.stringify([processing.processingType, processing.values]);
967
+ const existing = this.processingBindings.get(key);
968
+ if (existing) {
969
+ existing.displayIds.push(displayId);
970
+ return;
971
+ }
972
+ this.processingBindings.set(key, {
973
+ processingType: processing.processingType,
974
+ values: processing.values,
975
+ displayIds: [displayId]
976
+ });
923
977
  }
924
978
  createCanvas() {
925
979
  return {