@richardmcquiston01/unofficial-xcs-writer 0.5.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 +28 -0
- package/README.md +25 -3
- package/dist/index.cjs +186 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -11
- package/dist/index.d.ts +77 -11
- package/dist/index.js +185 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ 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
|
+
|
|
22
|
+
## [0.6.0] - 2026-09-21
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`.xs` generation**: `XCSGenerator.toXsBytes()` (`src/xs.ts`'s new `buildXsArchive`) exports a
|
|
27
|
+
built project as a `.xs` v2 archive instead of `.xcs` -- the same chainable builder
|
|
28
|
+
(`.addText()`/`.addPath()`/`.addBitmap()`/`.addLayer()`), same display objects, different
|
|
29
|
+
container. `project.json`/`profiles.json`/`devices/`/`canvases/` metadata is synthesized
|
|
30
|
+
fresh, always as a single canvas in a single `displays-0.json` chunk, with the cover image
|
|
31
|
+
written as a real `resources/project-cover.png` file instead of inline base64. Processing
|
|
32
|
+
profiles and device bindings are left empty (no `addProfile`-style API exists yet) -- a
|
|
33
|
+
generated `.xs` file's power/speed settings may need to be configured manually in xTool
|
|
34
|
+
Studio before cutting/engraving.
|
|
35
|
+
|
|
8
36
|
## [0.5.0] - 2026-09-20
|
|
9
37
|
|
|
10
38
|
### Added
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ This library lets you, for both formats:
|
|
|
13
13
|
- **Read** a file and inspect its contents
|
|
14
14
|
- **Extract** `{{token}}` template placeholders from text objects
|
|
15
15
|
- **Render** a filled-in copy by substituting values for those placeholders
|
|
16
|
-
- **Build** a new
|
|
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`
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
@@ -93,10 +93,26 @@ 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
|
|
|
103
|
+
Call `.toXsBytes()` instead of `.toBytes()` to export the same project as a `.xs` (v2 workspace)
|
|
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
|
+
|
|
100
116
|
## API
|
|
101
117
|
|
|
102
118
|
### `assertXcsFormat(buffer: ArrayBuffer): void`
|
|
@@ -151,6 +167,10 @@ Same as `extractXcsTokens`, but scans every `canvases/<canvasId>/displays-<chunk
|
|
|
151
167
|
|
|
152
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.
|
|
153
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
|
+
|
|
154
174
|
### `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
|
|
155
175
|
|
|
156
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.
|
|
@@ -166,7 +186,9 @@ Lower-level glyph-extraction primitives everything else in this library is built
|
|
|
166
186
|
| `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
|
|
167
187
|
| `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
|
|
168
188
|
|
|
169
|
-
|
|
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`).
|
|
170
192
|
|
|
171
193
|
Text layout — build the `layout` option for `.addText()` with one of:
|
|
172
194
|
|
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(index_exports, {
|
|
|
33
33
|
XCSGenerator: () => XCSGenerator,
|
|
34
34
|
assertXcsFormat: () => assertXcsFormat,
|
|
35
35
|
assertXsFormat: () => assertXsFormat,
|
|
36
|
+
buildXsArchive: () => buildXsArchive,
|
|
36
37
|
createXCS: () => createXCS,
|
|
37
38
|
effectiveCurveDeg: () => effectiveCurveDeg,
|
|
38
39
|
extractXcsTokens: () => extractXcsTokens,
|
|
@@ -358,6 +359,160 @@ function renderXsFile(buffer, variables, values, fonts) {
|
|
|
358
359
|
}
|
|
359
360
|
return (0, import_fflate.zipSync)(entries);
|
|
360
361
|
}
|
|
362
|
+
function encodeJson(value) {
|
|
363
|
+
return new TextEncoder().encode(JSON.stringify(value));
|
|
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
|
+
}
|
|
395
|
+
function decodeDataUrl(dataUrl) {
|
|
396
|
+
const match = /^data:([^;]+);base64,(.*)$/.exec(dataUrl);
|
|
397
|
+
if (!match) return null;
|
|
398
|
+
const [, mimeType, base64] = match;
|
|
399
|
+
const binary = atob(base64);
|
|
400
|
+
const bytes = new Uint8Array(binary.length);
|
|
401
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
402
|
+
return { bytes, mimeType };
|
|
403
|
+
}
|
|
404
|
+
function coverResourceEntries(coverDataUrl) {
|
|
405
|
+
const cover = decodeDataUrl(coverDataUrl);
|
|
406
|
+
if (!cover) return {};
|
|
407
|
+
return {
|
|
408
|
+
"resources/project-cover.png": cover.bytes,
|
|
409
|
+
"resources/project-cover.png.meta.json": encodeJson({
|
|
410
|
+
ref: "resources/project-cover.png",
|
|
411
|
+
metadata: {
|
|
412
|
+
kind: "image",
|
|
413
|
+
source: { type: "workspace", value: "project-cover.png" },
|
|
414
|
+
mimeType: cover.mimeType
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function buildXsArchive(file, processingBindings = []) {
|
|
420
|
+
const canvas = file.canvas[0];
|
|
421
|
+
if (!canvas) {
|
|
422
|
+
throw new Error("Cannot build a .xs archive from a project with no canvas.");
|
|
423
|
+
}
|
|
424
|
+
const { profiles, profileRefs, deviceBindings } = buildProcessing(
|
|
425
|
+
canvas.id,
|
|
426
|
+
processingBindings
|
|
427
|
+
);
|
|
428
|
+
return (0, import_fflate.zipSync)({
|
|
429
|
+
".format": new TextEncoder().encode("v2"),
|
|
430
|
+
"meta/persistence-meta.json": encodeJson({
|
|
431
|
+
schemaVersion: "2.0.0",
|
|
432
|
+
protocol: "xcs-workspace-v2"
|
|
433
|
+
}),
|
|
434
|
+
"project.json": encodeJson({
|
|
435
|
+
__v2__: true,
|
|
436
|
+
version: "2.0.0",
|
|
437
|
+
schemaMeta: { schemaVersion: "2.1.0", format: "directory" },
|
|
438
|
+
projectId: file.projectTraceID,
|
|
439
|
+
projectTraceID: file.projectTraceID,
|
|
440
|
+
projectName: "Untitled",
|
|
441
|
+
activeCanvasId: canvas.id,
|
|
442
|
+
activeDeviceId: file.device.id,
|
|
443
|
+
versionInfo: {
|
|
444
|
+
source: "web",
|
|
445
|
+
appVersion: "",
|
|
446
|
+
savedAt: file.modify,
|
|
447
|
+
ua: file.ua,
|
|
448
|
+
minRequiredVersion: file.minRequiredVersion,
|
|
449
|
+
appMinRequiredVersion: file.appMinRequiredVersion,
|
|
450
|
+
webMinRequiredVersion: file.webMinRequiredVersion
|
|
451
|
+
},
|
|
452
|
+
created: file.created,
|
|
453
|
+
modify: file.modify,
|
|
454
|
+
modules: { canvases: [canvas.id], devices: [file.device.id] },
|
|
455
|
+
cover: "resources/project-cover.png",
|
|
456
|
+
customProjectData: { projectTraceID: file.projectTraceID }
|
|
457
|
+
}),
|
|
458
|
+
"profiles.json": encodeJson({ profiles }),
|
|
459
|
+
[`canvases/${canvas.id}.json`]: encodeJson({
|
|
460
|
+
id: canvas.id,
|
|
461
|
+
title: canvas.title,
|
|
462
|
+
hidden: false,
|
|
463
|
+
layerData: canvas.layerData,
|
|
464
|
+
groupData: canvas.groupData,
|
|
465
|
+
extendInfo: {
|
|
466
|
+
version: canvas.extendInfo.version,
|
|
467
|
+
minCanvasVersion: canvas.extendInfo.minCanvasVersion,
|
|
468
|
+
displayProcessConfigMap: {},
|
|
469
|
+
rulerPluginData: { rulerGuide: [] },
|
|
470
|
+
type: "2d"
|
|
471
|
+
},
|
|
472
|
+
chunkLayout: { displayCount: canvas.displays.length, chunkCount: 1, chunkIndexes: [0] }
|
|
473
|
+
}),
|
|
474
|
+
[`canvases/${canvas.id}/displays-0.json`]: encodeJson({
|
|
475
|
+
canvasId: canvas.id,
|
|
476
|
+
chunkIndex: 0,
|
|
477
|
+
displays: canvas.displays
|
|
478
|
+
}),
|
|
479
|
+
[`devices/device-${file.device.id}.json`]: encodeJson({
|
|
480
|
+
id: file.device.id,
|
|
481
|
+
deviceCode: file.device.id,
|
|
482
|
+
extId: file.extId,
|
|
483
|
+
extName: file.extName,
|
|
484
|
+
power: [file.device.power],
|
|
485
|
+
processing: {
|
|
486
|
+
[canvas.id]: {
|
|
487
|
+
id: canvas.id,
|
|
488
|
+
activeMode: "LASER_PLANE",
|
|
489
|
+
modes: {
|
|
490
|
+
LASER_PLANE: {
|
|
491
|
+
ignoredDisplayIds: [],
|
|
492
|
+
data: {
|
|
493
|
+
material: 0,
|
|
494
|
+
thickness: null,
|
|
495
|
+
perimeter: null,
|
|
496
|
+
diameter: null,
|
|
497
|
+
isProcessByLayer: false,
|
|
498
|
+
pathPlanning: "auto",
|
|
499
|
+
fillPlanning: "separate",
|
|
500
|
+
scanDirection: "topToBottom",
|
|
501
|
+
enableOddEvenKerf: true,
|
|
502
|
+
focalLen: null,
|
|
503
|
+
focalLength: null
|
|
504
|
+
},
|
|
505
|
+
profileRefs,
|
|
506
|
+
patches: {},
|
|
507
|
+
bindings: deviceBindings
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}),
|
|
513
|
+
...coverResourceEntries(file.cover)
|
|
514
|
+
});
|
|
515
|
+
}
|
|
361
516
|
|
|
362
517
|
// src/layout.ts
|
|
363
518
|
var LAYOUT_MM_PER_EM = 25.4;
|
|
@@ -618,6 +773,8 @@ var XCSGenerator = class {
|
|
|
618
773
|
displays = [];
|
|
619
774
|
layers = /* @__PURE__ */ new Map();
|
|
620
775
|
options;
|
|
776
|
+
/** Keyed by `JSON.stringify([processingType, values])` to merge displays sharing identical settings into one profile. */
|
|
777
|
+
processingBindings = /* @__PURE__ */ new Map();
|
|
621
778
|
constructor(options = {}) {
|
|
622
779
|
this.options = {
|
|
623
780
|
deviceId: options.deviceId || "P2S",
|
|
@@ -639,6 +796,7 @@ var XCSGenerator = class {
|
|
|
639
796
|
addText(text, x, y, options = {}) {
|
|
640
797
|
const textObj = this.createTextObject(text, x, y, options);
|
|
641
798
|
this.displays.push(textObj);
|
|
799
|
+
this.registerProcessing(textObj.id, options.processing);
|
|
642
800
|
return this;
|
|
643
801
|
}
|
|
644
802
|
/**
|
|
@@ -647,6 +805,7 @@ var XCSGenerator = class {
|
|
|
647
805
|
addPath(pathData, x, y, width, height, options = {}) {
|
|
648
806
|
const pathObj = this.createPathObject(pathData, x, y, width, height, options);
|
|
649
807
|
this.displays.push(pathObj);
|
|
808
|
+
this.registerProcessing(pathObj.id, options.processing);
|
|
650
809
|
return this;
|
|
651
810
|
}
|
|
652
811
|
/**
|
|
@@ -730,6 +889,7 @@ var XCSGenerator = class {
|
|
|
730
889
|
opacity: 1
|
|
731
890
|
};
|
|
732
891
|
this.displays.push(bitmap);
|
|
892
|
+
this.registerProcessing(bitmap.id, options.processing);
|
|
733
893
|
return this;
|
|
734
894
|
}
|
|
735
895
|
/**
|
|
@@ -790,6 +950,31 @@ var XCSGenerator = class {
|
|
|
790
950
|
toBytes() {
|
|
791
951
|
return new TextEncoder().encode(this.toJSON());
|
|
792
952
|
}
|
|
953
|
+
/**
|
|
954
|
+
* 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).
|
|
957
|
+
* Display objects themselves carry over unchanged; only the
|
|
958
|
+
* container differs from `toBytes()`.
|
|
959
|
+
*/
|
|
960
|
+
toXsBytes() {
|
|
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
|
+
});
|
|
977
|
+
}
|
|
793
978
|
createCanvas() {
|
|
794
979
|
return {
|
|
795
980
|
id: this.canvasId,
|
|
@@ -1025,6 +1210,7 @@ function createXCS(deviceId = "P2S") {
|
|
|
1025
1210
|
XCSGenerator,
|
|
1026
1211
|
assertXcsFormat,
|
|
1027
1212
|
assertXsFormat,
|
|
1213
|
+
buildXsArchive,
|
|
1028
1214
|
createXCS,
|
|
1029
1215
|
effectiveCurveDeg,
|
|
1030
1216
|
extractXcsTokens,
|