@richardmcquiston01/unofficial-xcs-writer 0.5.0 → 0.6.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 +14 -0
- package/README.md +12 -2
- package/dist/index.cjs +132 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -11
- package/dist/index.d.ts +43 -11
- package/dist/index.js +131 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.6.0] - 2026-09-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`.xs` generation**: `XCSGenerator.toXsBytes()` (`src/xs.ts`'s new `buildXsArchive`) exports a
|
|
13
|
+
built project as a `.xs` v2 archive instead of `.xcs` -- the same chainable builder
|
|
14
|
+
(`.addText()`/`.addPath()`/`.addBitmap()`/`.addLayer()`), same display objects, different
|
|
15
|
+
container. `project.json`/`profiles.json`/`devices/`/`canvases/` metadata is synthesized
|
|
16
|
+
fresh, always as a single canvas in a single `displays-0.json` chunk, with the cover image
|
|
17
|
+
written as a real `resources/project-cover.png` file instead of inline base64. Processing
|
|
18
|
+
profiles and device bindings are left empty (no `addProfile`-style API exists yet) -- a
|
|
19
|
+
generated `.xs` file's power/speed settings may need to be configured manually in xTool
|
|
20
|
+
Studio before cutting/engraving.
|
|
21
|
+
|
|
8
22
|
## [0.5.0] - 2026-09-20
|
|
9
23
|
|
|
10
24
|
### 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
|
|
|
@@ -97,6 +97,12 @@ const project = createXCS('P2S')
|
|
|
97
97
|
.toBytes(); // Uint8Array, ready to write to a .xcs file
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
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.
|
|
105
|
+
|
|
100
106
|
## API
|
|
101
107
|
|
|
102
108
|
### `assertXcsFormat(buffer: ArrayBuffer): void`
|
|
@@ -151,6 +157,10 @@ Same as `extractXcsTokens`, but scans every `canvases/<canvasId>/displays-<chunk
|
|
|
151
157
|
|
|
152
158
|
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
159
|
|
|
160
|
+
### `buildXsArchive(file: XCSFile): Uint8Array`
|
|
161
|
+
|
|
162
|
+
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.
|
|
163
|
+
|
|
154
164
|
### `loadFont(buffer: ArrayBuffer)` / `loadDefaultFont()` / `layoutText(font, text, originX, originY)`
|
|
155
165
|
|
|
156
166
|
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 +176,7 @@ Lower-level glyph-extraction primitives everything else in this library is built
|
|
|
166
176
|
| `.addBitmap(pngBase64, x, y, widthMm, heightMm, originWidthPx, originHeightPx, options?)` | An embedded PNG, physically sized in mm. |
|
|
167
177
|
| `.addLayer(color, name, order)` | A named, colored layer (one `#00befe` "Cyan" layer exists by default). |
|
|
168
178
|
|
|
169
|
-
Then `.generate()` (the plain `XCSFile` object), `.toJSON()` (string),
|
|
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).
|
|
170
180
|
|
|
171
181
|
Text layout — build the `layout` option for `.addText()` with one of:
|
|
172
182
|
|
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,126 @@ 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 decodeDataUrl(dataUrl) {
|
|
366
|
+
const match = /^data:([^;]+);base64,(.*)$/.exec(dataUrl);
|
|
367
|
+
if (!match) return null;
|
|
368
|
+
const [, mimeType, base64] = match;
|
|
369
|
+
const binary = atob(base64);
|
|
370
|
+
const bytes = new Uint8Array(binary.length);
|
|
371
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
372
|
+
return { bytes, mimeType };
|
|
373
|
+
}
|
|
374
|
+
function coverResourceEntries(coverDataUrl) {
|
|
375
|
+
const cover = decodeDataUrl(coverDataUrl);
|
|
376
|
+
if (!cover) return {};
|
|
377
|
+
return {
|
|
378
|
+
"resources/project-cover.png": cover.bytes,
|
|
379
|
+
"resources/project-cover.png.meta.json": encodeJson({
|
|
380
|
+
ref: "resources/project-cover.png",
|
|
381
|
+
metadata: {
|
|
382
|
+
kind: "image",
|
|
383
|
+
source: { type: "workspace", value: "project-cover.png" },
|
|
384
|
+
mimeType: cover.mimeType
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function buildXsArchive(file) {
|
|
390
|
+
const canvas = file.canvas[0];
|
|
391
|
+
if (!canvas) {
|
|
392
|
+
throw new Error("Cannot build a .xs archive from a project with no canvas.");
|
|
393
|
+
}
|
|
394
|
+
return (0, import_fflate.zipSync)({
|
|
395
|
+
".format": new TextEncoder().encode("v2"),
|
|
396
|
+
"meta/persistence-meta.json": encodeJson({
|
|
397
|
+
schemaVersion: "2.0.0",
|
|
398
|
+
protocol: "xcs-workspace-v2"
|
|
399
|
+
}),
|
|
400
|
+
"project.json": encodeJson({
|
|
401
|
+
__v2__: true,
|
|
402
|
+
version: "2.0.0",
|
|
403
|
+
schemaMeta: { schemaVersion: "2.1.0", format: "directory" },
|
|
404
|
+
projectId: file.projectTraceID,
|
|
405
|
+
projectTraceID: file.projectTraceID,
|
|
406
|
+
projectName: "Untitled",
|
|
407
|
+
activeCanvasId: canvas.id,
|
|
408
|
+
activeDeviceId: file.device.id,
|
|
409
|
+
versionInfo: {
|
|
410
|
+
source: "web",
|
|
411
|
+
appVersion: "",
|
|
412
|
+
savedAt: file.modify,
|
|
413
|
+
ua: file.ua,
|
|
414
|
+
minRequiredVersion: file.minRequiredVersion,
|
|
415
|
+
appMinRequiredVersion: file.appMinRequiredVersion,
|
|
416
|
+
webMinRequiredVersion: file.webMinRequiredVersion
|
|
417
|
+
},
|
|
418
|
+
created: file.created,
|
|
419
|
+
modify: file.modify,
|
|
420
|
+
modules: { canvases: [canvas.id], devices: [file.device.id] },
|
|
421
|
+
cover: "resources/project-cover.png",
|
|
422
|
+
customProjectData: { projectTraceID: file.projectTraceID }
|
|
423
|
+
}),
|
|
424
|
+
"profiles.json": encodeJson({ profiles: {} }),
|
|
425
|
+
[`canvases/${canvas.id}.json`]: encodeJson({
|
|
426
|
+
id: canvas.id,
|
|
427
|
+
title: canvas.title,
|
|
428
|
+
hidden: false,
|
|
429
|
+
layerData: canvas.layerData,
|
|
430
|
+
groupData: canvas.groupData,
|
|
431
|
+
extendInfo: {
|
|
432
|
+
version: canvas.extendInfo.version,
|
|
433
|
+
minCanvasVersion: canvas.extendInfo.minCanvasVersion,
|
|
434
|
+
displayProcessConfigMap: {},
|
|
435
|
+
rulerPluginData: { rulerGuide: [] },
|
|
436
|
+
type: "2d"
|
|
437
|
+
},
|
|
438
|
+
chunkLayout: { displayCount: canvas.displays.length, chunkCount: 1, chunkIndexes: [0] }
|
|
439
|
+
}),
|
|
440
|
+
[`canvases/${canvas.id}/displays-0.json`]: encodeJson({
|
|
441
|
+
canvasId: canvas.id,
|
|
442
|
+
chunkIndex: 0,
|
|
443
|
+
displays: canvas.displays
|
|
444
|
+
}),
|
|
445
|
+
[`devices/device-${file.device.id}.json`]: encodeJson({
|
|
446
|
+
id: file.device.id,
|
|
447
|
+
deviceCode: file.device.id,
|
|
448
|
+
extId: file.extId,
|
|
449
|
+
extName: file.extName,
|
|
450
|
+
power: [file.device.power],
|
|
451
|
+
processing: {
|
|
452
|
+
[canvas.id]: {
|
|
453
|
+
id: canvas.id,
|
|
454
|
+
activeMode: "LASER_PLANE",
|
|
455
|
+
modes: {
|
|
456
|
+
LASER_PLANE: {
|
|
457
|
+
ignoredDisplayIds: [],
|
|
458
|
+
data: {
|
|
459
|
+
material: 0,
|
|
460
|
+
thickness: null,
|
|
461
|
+
perimeter: null,
|
|
462
|
+
diameter: null,
|
|
463
|
+
isProcessByLayer: false,
|
|
464
|
+
pathPlanning: "auto",
|
|
465
|
+
fillPlanning: "separate",
|
|
466
|
+
scanDirection: "topToBottom",
|
|
467
|
+
enableOddEvenKerf: true,
|
|
468
|
+
focalLen: null,
|
|
469
|
+
focalLength: null
|
|
470
|
+
},
|
|
471
|
+
profileRefs: [],
|
|
472
|
+
patches: {},
|
|
473
|
+
bindings: []
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}),
|
|
479
|
+
...coverResourceEntries(file.cover)
|
|
480
|
+
});
|
|
481
|
+
}
|
|
361
482
|
|
|
362
483
|
// src/layout.ts
|
|
363
484
|
var LAYOUT_MM_PER_EM = 25.4;
|
|
@@ -790,6 +911,16 @@ var XCSGenerator = class {
|
|
|
790
911
|
toBytes() {
|
|
791
912
|
return new TextEncoder().encode(this.toJSON());
|
|
792
913
|
}
|
|
914
|
+
/**
|
|
915
|
+
* 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).
|
|
918
|
+
* Display objects themselves carry over unchanged; only the
|
|
919
|
+
* container differs from `toBytes()`.
|
|
920
|
+
*/
|
|
921
|
+
toXsBytes() {
|
|
922
|
+
return buildXsArchive(this.generate());
|
|
923
|
+
}
|
|
793
924
|
createCanvas() {
|
|
794
925
|
return {
|
|
795
926
|
id: this.canvasId,
|
|
@@ -1025,6 +1156,7 @@ function createXCS(deviceId = "P2S") {
|
|
|
1025
1156
|
XCSGenerator,
|
|
1026
1157
|
assertXcsFormat,
|
|
1027
1158
|
assertXsFormat,
|
|
1159
|
+
buildXsArchive,
|
|
1028
1160
|
createXCS,
|
|
1029
1161
|
effectiveCurveDeg,
|
|
1030
1162
|
extractXcsTokens,
|