ducjs 2.3.0 → 2.4.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.
@@ -11,12 +11,14 @@ export declare class DocumentGridConfig {
11
11
  gapY(): number;
12
12
  alignItems(): DOCUMENT_GRID_ALIGN_ITEMS | null;
13
13
  firstPageAlone(): boolean;
14
+ scale(): number;
14
15
  static startDocumentGridConfig(builder: flatbuffers.Builder): void;
15
16
  static addColumns(builder: flatbuffers.Builder, columns: number): void;
16
17
  static addGapX(builder: flatbuffers.Builder, gapX: number): void;
17
18
  static addGapY(builder: flatbuffers.Builder, gapY: number): void;
18
19
  static addAlignItems(builder: flatbuffers.Builder, alignItems: DOCUMENT_GRID_ALIGN_ITEMS): void;
19
20
  static addFirstPageAlone(builder: flatbuffers.Builder, firstPageAlone: boolean): void;
21
+ static addScale(builder: flatbuffers.Builder, scale: number): void;
20
22
  static endDocumentGridConfig(builder: flatbuffers.Builder): flatbuffers.Offset;
21
- static createDocumentGridConfig(builder: flatbuffers.Builder, columns: number, gapX: number, gapY: number, alignItems: DOCUMENT_GRID_ALIGN_ITEMS | null, firstPageAlone: boolean): flatbuffers.Offset;
23
+ static createDocumentGridConfig(builder: flatbuffers.Builder, columns: number, gapX: number, gapY: number, alignItems: DOCUMENT_GRID_ALIGN_ITEMS | null, firstPageAlone: boolean, scale: number): flatbuffers.Offset;
22
24
  }
@@ -38,8 +38,12 @@ export class DocumentGridConfig {
38
38
  const offset = this.bb.__offset(this.bb_pos, 12);
39
39
  return offset ? !!this.bb.readInt8(this.bb_pos + offset) : false;
40
40
  }
41
+ scale() {
42
+ const offset = this.bb.__offset(this.bb_pos, 14);
43
+ return offset ? this.bb.readFloat64(this.bb_pos + offset) : 0.0;
44
+ }
41
45
  static startDocumentGridConfig(builder) {
42
- builder.startObject(5);
46
+ builder.startObject(6);
43
47
  }
44
48
  static addColumns(builder, columns) {
45
49
  builder.addFieldInt32(0, columns, 0);
@@ -56,11 +60,14 @@ export class DocumentGridConfig {
56
60
  static addFirstPageAlone(builder, firstPageAlone) {
57
61
  builder.addFieldInt8(4, +firstPageAlone, +false);
58
62
  }
63
+ static addScale(builder, scale) {
64
+ builder.addFieldFloat64(5, scale, 0.0);
65
+ }
59
66
  static endDocumentGridConfig(builder) {
60
67
  const offset = builder.endObject();
61
68
  return offset;
62
69
  }
63
- static createDocumentGridConfig(builder, columns, gapX, gapY, alignItems, firstPageAlone) {
70
+ static createDocumentGridConfig(builder, columns, gapX, gapY, alignItems, firstPageAlone, scale) {
64
71
  DocumentGridConfig.startDocumentGridConfig(builder);
65
72
  DocumentGridConfig.addColumns(builder, columns);
66
73
  DocumentGridConfig.addGapX(builder, gapX);
@@ -68,6 +75,7 @@ export class DocumentGridConfig {
68
75
  if (alignItems !== null)
69
76
  DocumentGridConfig.addAlignItems(builder, alignItems);
70
77
  DocumentGridConfig.addFirstPageAlone(builder, firstPageAlone);
78
+ DocumentGridConfig.addScale(builder, scale);
71
79
  return DocumentGridConfig.endDocumentGridConfig(builder);
72
80
  }
73
81
  }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export * as DucBin from "./flatbuffers/duc";
2
- export * from "./types";
3
- export * from "./utils";
4
- export * from "./serialize";
2
+ export * from "./lazy-files";
5
3
  export * from "./parse";
6
4
  export * from "./restore";
5
+ export * from "./serialize";
7
6
  export * from "./technical";
7
+ export * from "./types";
8
+ export * from "./utils";
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  export * as DucBin from "./flatbuffers/duc";
2
- export * from "./types";
3
- export * from "./utils";
4
- export * from "./serialize";
2
+ export * from "./lazy-files";
5
3
  export * from "./parse";
6
4
  export * from "./restore";
5
+ export * from "./serialize";
7
6
  export * from "./technical";
7
+ export * from "./types";
8
+ export * from "./utils";
@@ -0,0 +1,84 @@
1
+ /**
2
+ * LazyExternalFileStore — Zero-copy, on-demand access to external file data from a FlatBuffer.
3
+ *
4
+ * Instead of eagerly parsing and copying every external file's binary data into JS memory,
5
+ * this store keeps a reference to the original FlatBuffer Uint8Array and reads file bytes
6
+ * only when explicitly requested. FlatBuffer `dataArray()` returns a zero-copy view
7
+ * (a Uint8Array pointing into the original buffer), so no allocation occurs until the
8
+ * consumer actually needs the data.
9
+ *
10
+ * Memory lifecycle:
11
+ * 1. On parse: only metadata (~200 bytes per file) enters JS heap.
12
+ * 2. On demand: `getFileData(fileId)` reads the zero-copy slice from the buffer.
13
+ * 3. The caller (renderer/worker) uses the data, then lets it GC naturally.
14
+ * 4. If the store is released, the buffer reference is dropped, freeing everything.
15
+ *
16
+ * This is the key to supporting 1000s of external files without RAM bloat.
17
+ */
18
+ import type { DucExternalFileData, DucExternalFileMetadata, DucExternalFiles } from "./types";
19
+ export type ExternalFileMetadataMap = Record<string, DucExternalFileMetadata>;
20
+ export declare class LazyExternalFileStore {
21
+ private _buffer;
22
+ private _byteBuffer;
23
+ private _dataState;
24
+ /** Map from file id → lazy entry */
25
+ private _entries;
26
+ /** Map from element key → file id (the external_files vector uses element id as key) */
27
+ private _keyToFileId;
28
+ /**
29
+ * Files that were added at runtime (e.g. user uploading a new image).
30
+ * These aren't in the original FlatBuffer so we hold their data directly.
31
+ */
32
+ private _runtimeFiles;
33
+ constructor(buffer: Uint8Array);
34
+ private _indexMetadata;
35
+ /** Total number of external files */
36
+ get size(): number;
37
+ /** Whether a file with the given id exists */
38
+ has(fileId: string): boolean;
39
+ /** Get metadata only (no binary data copied) — ~200 bytes per file */
40
+ getMetadata(fileId: string): DucExternalFileMetadata | null;
41
+ /** Get all metadata entries (for UI listing, etc.) */
42
+ getAllMetadata(): ExternalFileMetadataMap;
43
+ /**
44
+ * Get full file data (metadata + binary bytes) ON DEMAND.
45
+ *
46
+ * For files from the original FlatBuffer, this returns a zero-copy Uint8Array
47
+ * view into the original buffer — no allocation for the file bytes themselves.
48
+ * The view is valid as long as this store hasn't been released.
49
+ *
50
+ * For runtime-added files, returns the data directly.
51
+ */
52
+ getFileData(fileId: string): DucExternalFileData | null;
53
+ /**
54
+ * Get a detached copy of the file data (allocates new ArrayBuffer).
55
+ * Use this when you need to transfer data to a worker or keep it beyond store lifetime.
56
+ */
57
+ getFileDataCopy(fileId: string): DucExternalFileData | null;
58
+ /**
59
+ * Add a file at runtime (user upload, paste, etc.).
60
+ * These files are held in memory since they aren't in the FlatBuffer.
61
+ */
62
+ addRuntimeFile(fileData: DucExternalFileData): void;
63
+ /** Remove a runtime-added file */
64
+ removeRuntimeFile(fileId: string): void;
65
+ /**
66
+ * Export all files as a standard DucExternalFiles record.
67
+ * This COPIES all file data eagerly — use only for serialization.
68
+ */
69
+ toExternalFiles(): DucExternalFiles;
70
+ /**
71
+ * Merge runtime files from the given DucExternalFiles map.
72
+ * Only adds files not already present in the store.
73
+ */
74
+ mergeFiles(files: DucExternalFiles): void;
75
+ /** Estimated RAM usage for metadata only (not counting the backing buffer) */
76
+ get estimatedMetadataBytes(): number;
77
+ /**
78
+ * Release the FlatBuffer reference. After this, only runtime-added files remain accessible.
79
+ * Call this when switching documents or when the store is no longer needed.
80
+ */
81
+ release(): void;
82
+ /** Whether the store has been released */
83
+ get isReleased(): boolean;
84
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * LazyExternalFileStore — Zero-copy, on-demand access to external file data from a FlatBuffer.
3
+ *
4
+ * Instead of eagerly parsing and copying every external file's binary data into JS memory,
5
+ * this store keeps a reference to the original FlatBuffer Uint8Array and reads file bytes
6
+ * only when explicitly requested. FlatBuffer `dataArray()` returns a zero-copy view
7
+ * (a Uint8Array pointing into the original buffer), so no allocation occurs until the
8
+ * consumer actually needs the data.
9
+ *
10
+ * Memory lifecycle:
11
+ * 1. On parse: only metadata (~200 bytes per file) enters JS heap.
12
+ * 2. On demand: `getFileData(fileId)` reads the zero-copy slice from the buffer.
13
+ * 3. The caller (renderer/worker) uses the data, then lets it GC naturally.
14
+ * 4. If the store is released, the buffer reference is dropped, freeing everything.
15
+ *
16
+ * This is the key to supporting 1000s of external files without RAM bloat.
17
+ */
18
+ var __rest = (this && this.__rest) || function (s, e) {
19
+ var t = {};
20
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
21
+ t[p] = s[p];
22
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
23
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
24
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
25
+ t[p[i]] = s[p[i]];
26
+ }
27
+ return t;
28
+ };
29
+ import * as flatbuffers from "flatbuffers";
30
+ import { ExportedDataState as ExportedDataStateFb } from "./flatbuffers/duc";
31
+ export class LazyExternalFileStore {
32
+ constructor(buffer) {
33
+ /** Map from file id → lazy entry */
34
+ this._entries = new Map();
35
+ /** Map from element key → file id (the external_files vector uses element id as key) */
36
+ this._keyToFileId = new Map();
37
+ /**
38
+ * Files that were added at runtime (e.g. user uploading a new image).
39
+ * These aren't in the original FlatBuffer so we hold their data directly.
40
+ */
41
+ this._runtimeFiles = new Map();
42
+ this._buffer = buffer;
43
+ this._byteBuffer = new flatbuffers.ByteBuffer(buffer);
44
+ this._dataState = ExportedDataStateFb.getRootAsExportedDataState(this._byteBuffer);
45
+ this._indexMetadata();
46
+ console.info(`[LazyExternalFileStore] indexed ${this._entries.size} files from ${buffer.byteLength} byte buffer, ids: [${[...this._entries.keys()].map(k => k.slice(0, 12)).join(', ')}]`);
47
+ }
48
+ _indexMetadata() {
49
+ if (!this._dataState)
50
+ return;
51
+ const count = this._dataState.externalFilesLength();
52
+ for (let i = 0; i < count; i++) {
53
+ const entry = this._dataState.externalFiles(i);
54
+ if (!entry)
55
+ continue;
56
+ const key = entry.key();
57
+ const fileData = entry.value();
58
+ if (!key || !fileData)
59
+ continue;
60
+ const id = fileData.id();
61
+ if (!id)
62
+ continue;
63
+ const metadata = {
64
+ id,
65
+ mimeType: fileData.mimeType() || "application/octet-stream",
66
+ created: Number(fileData.created()),
67
+ lastRetrieved: Number(fileData.lastRetrieved()) || undefined,
68
+ };
69
+ const lazyEntry = { metadata, vectorIndex: i };
70
+ this._entries.set(id, lazyEntry);
71
+ this._keyToFileId.set(key, id);
72
+ }
73
+ }
74
+ /** Total number of external files */
75
+ get size() {
76
+ return this._entries.size + this._runtimeFiles.size;
77
+ }
78
+ /** Whether a file with the given id exists */
79
+ has(fileId) {
80
+ return this._entries.has(fileId) || this._runtimeFiles.has(fileId);
81
+ }
82
+ /** Get metadata only (no binary data copied) — ~200 bytes per file */
83
+ getMetadata(fileId) {
84
+ var _a, _b;
85
+ const runtime = this._runtimeFiles.get(fileId);
86
+ if (runtime) {
87
+ const { data: _ } = runtime, meta = __rest(runtime, ["data"]);
88
+ return meta;
89
+ }
90
+ return (_b = (_a = this._entries.get(fileId)) === null || _a === void 0 ? void 0 : _a.metadata) !== null && _b !== void 0 ? _b : null;
91
+ }
92
+ /** Get all metadata entries (for UI listing, etc.) */
93
+ getAllMetadata() {
94
+ const result = {};
95
+ for (const [id, entry] of this._entries) {
96
+ result[id] = entry.metadata;
97
+ }
98
+ for (const [id, file] of this._runtimeFiles) {
99
+ const { data: _ } = file, meta = __rest(file, ["data"]);
100
+ result[id] = meta;
101
+ }
102
+ return result;
103
+ }
104
+ /**
105
+ * Get full file data (metadata + binary bytes) ON DEMAND.
106
+ *
107
+ * For files from the original FlatBuffer, this returns a zero-copy Uint8Array
108
+ * view into the original buffer — no allocation for the file bytes themselves.
109
+ * The view is valid as long as this store hasn't been released.
110
+ *
111
+ * For runtime-added files, returns the data directly.
112
+ */
113
+ getFileData(fileId) {
114
+ const runtime = this._runtimeFiles.get(fileId);
115
+ if (runtime)
116
+ return runtime;
117
+ const entry = this._entries.get(fileId);
118
+ if (!entry || !this._dataState)
119
+ return null;
120
+ const fbEntry = this._dataState.externalFiles(entry.vectorIndex);
121
+ if (!fbEntry)
122
+ return null;
123
+ const fileData = fbEntry.value();
124
+ if (!fileData)
125
+ return null;
126
+ const data = fileData.dataArray();
127
+ if (!data)
128
+ return null;
129
+ return Object.assign(Object.assign({}, entry.metadata), { data });
130
+ }
131
+ /**
132
+ * Get a detached copy of the file data (allocates new ArrayBuffer).
133
+ * Use this when you need to transfer data to a worker or keep it beyond store lifetime.
134
+ */
135
+ getFileDataCopy(fileId) {
136
+ const fileDataRef = this.getFileData(fileId);
137
+ if (!fileDataRef)
138
+ return null;
139
+ return Object.assign(Object.assign({}, fileDataRef), { data: new Uint8Array(fileDataRef.data) });
140
+ }
141
+ /**
142
+ * Add a file at runtime (user upload, paste, etc.).
143
+ * These files are held in memory since they aren't in the FlatBuffer.
144
+ */
145
+ addRuntimeFile(fileData) {
146
+ this._runtimeFiles.set(fileData.id, fileData);
147
+ }
148
+ /** Remove a runtime-added file */
149
+ removeRuntimeFile(fileId) {
150
+ this._runtimeFiles.delete(fileId);
151
+ }
152
+ /**
153
+ * Export all files as a standard DucExternalFiles record.
154
+ * This COPIES all file data eagerly — use only for serialization.
155
+ */
156
+ toExternalFiles() {
157
+ const result = {};
158
+ for (const [key, fileId] of this._keyToFileId) {
159
+ const fileData = this.getFileData(fileId);
160
+ if (fileData) {
161
+ result[key] = fileData;
162
+ }
163
+ }
164
+ for (const [id, file] of this._runtimeFiles) {
165
+ result[id] = file;
166
+ }
167
+ return result;
168
+ }
169
+ /**
170
+ * Merge runtime files from the given DucExternalFiles map.
171
+ * Only adds files not already present in the store.
172
+ */
173
+ mergeFiles(files) {
174
+ for (const [_key, fileData] of Object.entries(files)) {
175
+ if (!this.has(fileData.id)) {
176
+ this.addRuntimeFile(fileData);
177
+ }
178
+ }
179
+ }
180
+ /** Estimated RAM usage for metadata only (not counting the backing buffer) */
181
+ get estimatedMetadataBytes() {
182
+ var _a, _b;
183
+ let bytes = 0;
184
+ for (const [, entry] of this._entries) {
185
+ bytes += 200 + entry.metadata.id.length * 2 + entry.metadata.mimeType.length * 2;
186
+ }
187
+ for (const [, file] of this._runtimeFiles) {
188
+ bytes += 200 + ((_b = (_a = file.data) === null || _a === void 0 ? void 0 : _a.byteLength) !== null && _b !== void 0 ? _b : 0);
189
+ }
190
+ return bytes;
191
+ }
192
+ /**
193
+ * Release the FlatBuffer reference. After this, only runtime-added files remain accessible.
194
+ * Call this when switching documents or when the store is no longer needed.
195
+ */
196
+ release() {
197
+ this._buffer = null;
198
+ this._byteBuffer = null;
199
+ this._dataState = null;
200
+ this._entries.clear();
201
+ this._keyToFileId.clear();
202
+ }
203
+ /** Whether the store has been released */
204
+ get isReleased() {
205
+ return this._buffer === null;
206
+ }
207
+ }
package/dist/parse.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { FileSystemHandle } from 'browser-fs-access';
2
- import { CustomHatchPattern as CustomHatchPatternFb, DimensionToleranceStyle as DimensionToleranceStyleFb, DucBlock as DucBlockFb, DucDimensionStyle as DucDimensionStyleFb, DucDocStyle as DucDocStyleFb, DucExternalFileEntry, DucFeatureControlFrameStyle as DucFeatureControlFrameStyleFb, DucGlobalState as DucGlobalStateFb, DucGroup as DucGroupFb, DucHatchStyle as DucHatchStyleFb, DucHead as DucHeadFb, DucImageFilter as DucImageFilterFb, DucLayer as DucLayerFb, DucLeaderStyle as DucLeaderStyleFb, DucLine as DucLineFb, DucLineReference as DucLineReferenceFb, DucLocalState as DucLocalStateFb, DucPath as DucPathFb, DucPlotStyle as DucPlotStyleFb, DucPointBinding as DucPointBindingFb, DucPoint as DucPointFb, DucRegion as DucRegionFb, DucStackLikeStyles as DucStackLikeStylesFb, DucTableCellStyle as DucTableCellStyleFb, DucTableStyle as DucTableStyleFb, DucTextStyle as DucTextStyleFb, DucViewportStyle as DucViewportStyleFb, DucXRayStyle as DucXRayStyleFb, DocumentGridConfig as DocumentGridConfigFb, ElementBackground as ElementBackgroundFb, ElementContentBase as ElementContentBaseFb, ElementStroke as ElementStrokeFb, ElementWrapper, ExportedDataState as ExportedDataStateFb, GeometricPoint as GeometricPointFb, HatchPatternLine as HatchPatternLineFb, Margins as MarginsFb, PrimaryUnits as PrimaryUnitsFb, Standard as StandardFb, StrokeSides as StrokeSidesFb, StrokeStyle as StrokeStyleFb, TilingProperties as TilingPropertiesFb, VersionGraph as VersionGraphFb, _DucElementBase as _DucElementBaseFb, _DucElementStylesBase as _DucElementStylesBaseFb, _DucLinearElementBase as _DucLinearElementBaseFb, _DucStackBase as _DucStackBaseFb, _DucStackElementBase as _DucStackElementBaseFb } from "./flatbuffers/duc";
2
+ import { CustomHatchPattern as CustomHatchPatternFb, DimensionToleranceStyle as DimensionToleranceStyleFb, DocumentGridConfig as DocumentGridConfigFb, DucBlock as DucBlockFb, DucDimensionStyle as DucDimensionStyleFb, DucDocStyle as DucDocStyleFb, DucExternalFileEntry, DucFeatureControlFrameStyle as DucFeatureControlFrameStyleFb, DucGlobalState as DucGlobalStateFb, DucGroup as DucGroupFb, DucHatchStyle as DucHatchStyleFb, DucHead as DucHeadFb, DucImageFilter as DucImageFilterFb, DucLayer as DucLayerFb, DucLeaderStyle as DucLeaderStyleFb, DucLine as DucLineFb, DucLineReference as DucLineReferenceFb, DucLocalState as DucLocalStateFb, DucPath as DucPathFb, DucPlotStyle as DucPlotStyleFb, DucPointBinding as DucPointBindingFb, DucPoint as DucPointFb, DucRegion as DucRegionFb, DucStackLikeStyles as DucStackLikeStylesFb, DucTableCellStyle as DucTableCellStyleFb, DucTableStyle as DucTableStyleFb, DucTextStyle as DucTextStyleFb, DucViewportStyle as DucViewportStyleFb, DucXRayStyle as DucXRayStyleFb, ElementBackground as ElementBackgroundFb, ElementContentBase as ElementContentBaseFb, ElementStroke as ElementStrokeFb, ElementWrapper, ExportedDataState as ExportedDataStateFb, GeometricPoint as GeometricPointFb, HatchPatternLine as HatchPatternLineFb, Margins as MarginsFb, PrimaryUnits as PrimaryUnitsFb, Standard as StandardFb, StrokeSides as StrokeSidesFb, StrokeStyle as StrokeStyleFb, TilingProperties as TilingPropertiesFb, VersionGraph as VersionGraphFb, _DucElementBase as _DucElementBaseFb, _DucElementStylesBase as _DucElementStylesBaseFb, _DucLinearElementBase as _DucLinearElementBaseFb, _DucStackBase as _DucStackBaseFb, _DucStackElementBase as _DucStackElementBaseFb } from "./flatbuffers/duc";
3
3
  import { RestoreConfig, RestoredDataState } from "./restore";
4
4
  import { Standard, StandardUnits } from "./technical";
5
- import { CustomHatchPattern, Dictionary, DocumentGridConfig, DucBlock, DucDimensionStyle, DucDocStyle, DucElement, DucExternalFiles, DucFeatureControlFrameStyle, DucGlobalState, DucGroup, DucHatchStyle, DucHead, DucImageFilter, DucLayer, DucLeaderStyle, DucLine, DucLineReference, DucLocalState, DucPath, DucPlotStyle, DucPoint, DucPointBinding, DucRegion, DucStackLikeStyles, DucTableCellStyle, DucTableStyle, DucTextStyle, DucViewportStyle, DucXRayStyle, ElementBackground, ElementContentBase, ElementStroke, GeometricPoint, HatchPatternLine, PlotLayout, StrokeSides, StrokeStyle, TilingProperties, VersionGraph, _DucElementBase, _DucElementStylesBase, _DucLinearElementBase, _DucStackBase, _DucStackElementBase } from "./types";
5
+ import { CustomHatchPattern, Dictionary, DocumentGridConfig, DucBlock, DucDimensionStyle, DucDocStyle, DucElement, DucExternalFileMetadata, DucExternalFiles, DucFeatureControlFrameStyle, DucGlobalState, DucGroup, DucHatchStyle, DucHead, DucImageFilter, DucLayer, DucLeaderStyle, DucLine, DucLineReference, DucLocalState, DucPath, DucPlotStyle, DucPoint, DucPointBinding, DucRegion, DucStackLikeStyles, DucTableCellStyle, DucTableStyle, DucTextStyle, DucViewportStyle, DucXRayStyle, ElementBackground, ElementContentBase, ElementStroke, GeometricPoint, HatchPatternLine, PlotLayout, StrokeSides, StrokeStyle, TilingProperties, VersionGraph, _DucElementBase, _DucElementStylesBase, _DucLinearElementBase, _DucStackBase, _DucStackElementBase } from "./types";
6
6
  export declare function parseGeometricPoint(point: GeometricPointFb): GeometricPoint;
7
7
  export declare function parsePoint(point: DucPointFb): DucPoint;
8
8
  export declare function parseMargins(margins: MarginsFb): PlotLayout["margins"];
@@ -43,6 +43,14 @@ export declare function parseElementFromBinary(wrapper: ElementWrapper): DucElem
43
43
  export declare function parseBlockFromBinary(block: DucBlockFb): DucBlock;
44
44
  export declare function parseDictionaryFromBinary(data: ExportedDataStateFb): Dictionary;
45
45
  export declare function parseExternalFilesFromBinary(entry: DucExternalFileEntry): DucExternalFiles;
46
+ /**
47
+ * Parse only metadata (no binary data) from an external file entry.
48
+ * Used by the lazy file store to avoid copying file bytes into JS memory.
49
+ */
50
+ export declare function parseExternalFileMetadataFromBinary(entry: DucExternalFileEntry): {
51
+ key: string;
52
+ metadata: DucExternalFileMetadata;
53
+ } | null;
46
54
  export declare function parseGlobalStateFromBinary(state: DucGlobalStateFb): DucGlobalState;
47
55
  export declare function parseGroupFromBinary(group: DucGroupFb): DucGroup;
48
56
  export declare function parseLayerFromBinary(layer: DucLayerFb): DucLayer;
@@ -53,3 +61,27 @@ export declare function parseStandardFromBinary(standard: StandardFb): Standard;
53
61
  export declare function parseThumbnailFromBinary(data: ExportedDataStateFb): Uint8Array | undefined;
54
62
  export declare function parseVersionGraphFromBinary(graph: VersionGraphFb | null): VersionGraph | null;
55
63
  export declare const parseDuc: (blob: Blob | File, fileHandle?: FileSystemHandle | null, restoreConfig?: RestoreConfig) => Promise<RestoredDataState>;
64
+ import { LazyExternalFileStore } from "./lazy-files";
65
+ export type LazyRestoredDataState = Omit<RestoredDataState, 'files'> & {
66
+ /** Lazy file store: only metadata is in memory, file bytes are read on-demand from the buffer */
67
+ lazyFileStore: LazyExternalFileStore;
68
+ /** Legacy `files` field — always empty. Use lazyFileStore instead. */
69
+ files: DucExternalFiles;
70
+ };
71
+ /**
72
+ * Parse a .duc binary with lazy external file loading.
73
+ *
74
+ * This is identical to `parseDuc` except:
75
+ * - External file BYTES are NOT copied into JS memory.
76
+ * - Only file metadata (~200 bytes per file) is parsed.
77
+ * - A `LazyExternalFileStore` is returned for on-demand data access.
78
+ * - The store holds a reference to the original Uint8Array buffer.
79
+ *
80
+ * Memory comparison for 500 PDFs averaging 5MB each:
81
+ * - parseDuc: files field holds 2.5GB of ArrayBuffer data in JS heap
82
+ * - parseDucLazy: ~100KB of metadata + the original buffer (referenced, not copied)
83
+ *
84
+ * @param buffer - The raw .duc file bytes (from storage/IndexedDB/filesystem)
85
+ * @param restoreConfig - Optional restore configuration
86
+ */
87
+ export declare const parseDucLazy: (buffer: Uint8Array, restoreConfig?: RestoreConfig) => Promise<LazyRestoredDataState>;
package/dist/parse.js CHANGED
@@ -86,6 +86,7 @@ export function parseDocumentGridConfig(gridConfig) {
86
86
  return 'start';
87
87
  })(),
88
88
  firstPageAlone: gridConfig.firstPageAlone(),
89
+ scale: gridConfig.scale(),
89
90
  };
90
91
  }
91
92
  export function parseHead(head) {
@@ -282,6 +283,7 @@ function parsePdfElement(element) {
282
283
  gapY: 0,
283
284
  alignItems: 'start',
284
285
  firstPageAlone: false,
286
+ scale: 1,
285
287
  } });
286
288
  }
287
289
  function parseMermaidElement(element) {
@@ -587,6 +589,7 @@ function parseDocElement(element) {
587
589
  gapY: 0,
588
590
  alignItems: 'start',
589
591
  firstPageAlone: false,
592
+ scale: 1,
590
593
  } });
591
594
  }
592
595
  function parseModelElement(element) {
@@ -907,6 +910,28 @@ export function parseExternalFilesFromBinary(entry) {
907
910
  }
908
911
  };
909
912
  }
913
+ /**
914
+ * Parse only metadata (no binary data) from an external file entry.
915
+ * Used by the lazy file store to avoid copying file bytes into JS memory.
916
+ */
917
+ export function parseExternalFileMetadataFromBinary(entry) {
918
+ const fileData = entry.value();
919
+ const key = entry.key();
920
+ if (!fileData || !key)
921
+ return null;
922
+ const id = fileData.id();
923
+ if (!id)
924
+ return null;
925
+ return {
926
+ key,
927
+ metadata: {
928
+ id,
929
+ mimeType: fileData.mimeType() || "application/octet-stream",
930
+ created: Number(fileData.created()),
931
+ lastRetrieved: Number(fileData.lastRetrieved()) || undefined,
932
+ },
933
+ };
934
+ }
910
935
  export function parseGlobalStateFromBinary(state) {
911
936
  return {
912
937
  name: state.name(),
@@ -1459,3 +1484,181 @@ export const parseDuc = (blob_1, ...args_1) => __awaiter(void 0, [blob_1, ...arg
1459
1484
  };
1460
1485
  });
1461
1486
  // #endregion
1487
+ // #region LAZY ROOT PARSER
1488
+ import { LazyExternalFileStore } from "./lazy-files";
1489
+ /**
1490
+ * Parse a .duc binary with lazy external file loading.
1491
+ *
1492
+ * This is identical to `parseDuc` except:
1493
+ * - External file BYTES are NOT copied into JS memory.
1494
+ * - Only file metadata (~200 bytes per file) is parsed.
1495
+ * - A `LazyExternalFileStore` is returned for on-demand data access.
1496
+ * - The store holds a reference to the original Uint8Array buffer.
1497
+ *
1498
+ * Memory comparison for 500 PDFs averaging 5MB each:
1499
+ * - parseDuc: files field holds 2.5GB of ArrayBuffer data in JS heap
1500
+ * - parseDucLazy: ~100KB of metadata + the original buffer (referenced, not copied)
1501
+ *
1502
+ * @param buffer - The raw .duc file bytes (from storage/IndexedDB/filesystem)
1503
+ * @param restoreConfig - Optional restore configuration
1504
+ */
1505
+ export const parseDucLazy = (buffer_1, ...args_1) => __awaiter(void 0, [buffer_1, ...args_1], void 0, function* (buffer, restoreConfig = {}) {
1506
+ var _a;
1507
+ if (!buffer || buffer.byteLength === 0) {
1508
+ throw new Error('Invalid DUC buffer: empty file');
1509
+ }
1510
+ const byteBuffer = new flatbuffers.ByteBuffer(buffer);
1511
+ let data;
1512
+ try {
1513
+ data = ExportedDataState.getRootAsExportedDataState(byteBuffer);
1514
+ }
1515
+ catch (e) {
1516
+ throw new Error('Invalid DUC buffer: cannot read root table');
1517
+ }
1518
+ const legacyVersion = data.versionLegacy();
1519
+ if (legacyVersion) {
1520
+ throw new Error(`Unsupported DUC version: ${legacyVersion}. Please use version ducjs@2.0.1 or lower to support this file.`);
1521
+ }
1522
+ const localState = data.ducLocalState();
1523
+ const parsedLocalState = localState && parseLocalStateFromBinary(localState);
1524
+ const globalState = data.ducGlobalState();
1525
+ const parsedGlobalState = globalState && parseGlobalStateFromBinary(globalState);
1526
+ // Parse elements
1527
+ const elements = [];
1528
+ for (let i = 0; i < data.elementsLength(); i++) {
1529
+ const e = data.elements(i);
1530
+ if (e) {
1531
+ const element = parseElementFromBinary(e);
1532
+ if (element) {
1533
+ elements.push(element);
1534
+ }
1535
+ }
1536
+ }
1537
+ // Create lazy file store — only metadata is parsed, no file bytes copied
1538
+ const lazyFileStore = new LazyExternalFileStore(buffer);
1539
+ // Parse blocks
1540
+ const blocks = [];
1541
+ for (let i = 0; i < data.blocksLength(); i++) {
1542
+ const block = data.blocks(i);
1543
+ if (block) {
1544
+ const parsedBlock = parseBlockFromBinary(block);
1545
+ if (parsedBlock) {
1546
+ blocks.push(parsedBlock);
1547
+ }
1548
+ }
1549
+ }
1550
+ // Parse block instances
1551
+ const blockInstances = [];
1552
+ for (let i = 0; i < data.blockInstancesLength(); i++) {
1553
+ const blockInstance = data.blockInstances(i);
1554
+ if (blockInstance) {
1555
+ const parsedBlockInstance = parseBlockInstance(blockInstance);
1556
+ if (parsedBlockInstance) {
1557
+ blockInstances.push(parsedBlockInstance);
1558
+ }
1559
+ }
1560
+ }
1561
+ // Parse block collections
1562
+ const blockCollections = [];
1563
+ for (let i = 0; i < data.blockCollectionsLength(); i++) {
1564
+ const blockCollection = data.blockCollections(i);
1565
+ if (blockCollection) {
1566
+ const parsedBlockCollection = parseBlockCollection(blockCollection);
1567
+ if (parsedBlockCollection) {
1568
+ blockCollections.push(parsedBlockCollection);
1569
+ }
1570
+ }
1571
+ }
1572
+ // Parse groups
1573
+ const groups = [];
1574
+ for (let i = 0; i < data.groupsLength(); i++) {
1575
+ const group = data.groups(i);
1576
+ if (group) {
1577
+ const parsedGroup = parseGroupFromBinary(group);
1578
+ if (parsedGroup) {
1579
+ groups.push(parsedGroup);
1580
+ }
1581
+ }
1582
+ }
1583
+ // Parse dictionary
1584
+ const dictionary = parseDictionaryFromBinary(data);
1585
+ // Parse thumbnail
1586
+ const thumbnail = parseThumbnailFromBinary(data);
1587
+ // Parse version graph
1588
+ const versionGraphData = data.versionGraph();
1589
+ const versionGraph = parseVersionGraphFromBinary(versionGraphData);
1590
+ // Parse regions
1591
+ const regions = [];
1592
+ for (let i = 0; i < data.regionsLength(); i++) {
1593
+ const region = data.regions(i);
1594
+ if (region) {
1595
+ const parsedRegion = parseRegionFromBinary(region);
1596
+ if (parsedRegion) {
1597
+ regions.push(parsedRegion);
1598
+ }
1599
+ }
1600
+ }
1601
+ // Parse layers
1602
+ const layers = [];
1603
+ for (let i = 0; i < data.layersLength(); i++) {
1604
+ const layer = data.layers(i);
1605
+ if (layer) {
1606
+ const parsedLayer = parseLayerFromBinary(layer);
1607
+ if (parsedLayer) {
1608
+ layers.push(parsedLayer);
1609
+ }
1610
+ }
1611
+ }
1612
+ // Parse standards
1613
+ const standards = [];
1614
+ for (let i = 0; i < data.standardsLength(); i++) {
1615
+ const standard = data.standards(i);
1616
+ if (standard) {
1617
+ const parsedStandard = parseStandardFromBinary(standard);
1618
+ if (parsedStandard) {
1619
+ standards.push(parsedStandard);
1620
+ }
1621
+ }
1622
+ }
1623
+ const exportData = {
1624
+ thumbnail,
1625
+ dictionary,
1626
+ elements: elements,
1627
+ localState: parsedLocalState,
1628
+ globalState: parsedGlobalState,
1629
+ blocks,
1630
+ blockInstances,
1631
+ blockCollections,
1632
+ groups,
1633
+ regions,
1634
+ layers,
1635
+ standards,
1636
+ files: {}, // empty — use lazyFileStore
1637
+ versionGraph: versionGraph !== null && versionGraph !== void 0 ? versionGraph : undefined,
1638
+ id: (_a = data.id()) !== null && _a !== void 0 ? _a : nanoid(),
1639
+ };
1640
+ const sanitized = restore(exportData, {
1641
+ syncInvalidIndices: (elements) => elements,
1642
+ repairBindings: true,
1643
+ refreshDimensions: false,
1644
+ }, restoreConfig);
1645
+ return {
1646
+ thumbnail: sanitized.thumbnail,
1647
+ dictionary: sanitized.dictionary,
1648
+ elements: sanitized.elements,
1649
+ localState: sanitized.localState,
1650
+ globalState: sanitized.globalState,
1651
+ files: {},
1652
+ lazyFileStore,
1653
+ blocks: sanitized.blocks,
1654
+ blockInstances: sanitized.blockInstances,
1655
+ groups: sanitized.groups,
1656
+ regions: sanitized.regions,
1657
+ layers: sanitized.layers,
1658
+ blockCollections: sanitized.blockCollections,
1659
+ standards: sanitized.standards,
1660
+ versionGraph: sanitized.versionGraph,
1661
+ id: sanitized.id,
1662
+ };
1663
+ });
1664
+ // #endregion
@@ -90,6 +90,10 @@ const restoreElement = (element, currentScope, restoredBlocks, localState, globa
90
90
  case "text": {
91
91
  let fontSize = element.fontSize;
92
92
  let fontFamily = element.fontFamily;
93
+ // Restore condition: if font family is "10", change to DEFAULT_FONT_FAMILY
94
+ if (fontFamily === "10") {
95
+ fontFamily = DEFAULT_FONT_FAMILY;
96
+ }
93
97
  if ("font" in element) {
94
98
  try {
95
99
  const fontParts = String(element.font).split(" ");
@@ -975,6 +979,7 @@ const restoreDocumentGridConfig = (gridConfig) => {
975
979
  gapY: 0,
976
980
  alignItems: "start",
977
981
  firstPageAlone: false,
982
+ scale: 1,
978
983
  };
979
984
  }
980
985
  return {
@@ -985,6 +990,7 @@ const restoreDocumentGridConfig = (gridConfig) => {
985
990
  ? gridConfig.alignItems
986
991
  : "start",
987
992
  firstPageAlone: typeof gridConfig.firstPageAlone === "boolean" ? gridConfig.firstPageAlone : false,
993
+ scale: typeof gridConfig.scale === "number" ? gridConfig.scale : 1,
988
994
  };
989
995
  };
990
996
  const getFontFamilyByName = (fontFamilyName) => {
@@ -1272,10 +1278,15 @@ const restoreFcfDatumDefinition = (def, elementScope, currentScope, restoredBloc
1272
1278
  const restoreTextStyle = (style, currentScope) => {
1273
1279
  var _a, _b, _c;
1274
1280
  const defaultLineHeight = 1.15;
1281
+ // Restore condition: if font family is "10", change to DEFAULT_FONT_FAMILY
1282
+ let fontFamily = style === null || style === void 0 ? void 0 : style.fontFamily;
1283
+ if (fontFamily === "10") {
1284
+ fontFamily = DEFAULT_FONT_FAMILY;
1285
+ }
1275
1286
  return {
1276
1287
  // Text-specific styles
1277
1288
  isLtr: isValidBoolean(style === null || style === void 0 ? void 0 : style.isLtr, true),
1278
- fontFamily: getFontFamilyByName(style === null || style === void 0 ? void 0 : style.fontFamily),
1289
+ fontFamily: getFontFamilyByName(fontFamily),
1279
1290
  bigFontFamily: isValidString(style === null || style === void 0 ? void 0 : style.bigFontFamily, "sans-serif"),
1280
1291
  textAlign: isValidTextAlignValue(style === null || style === void 0 ? void 0 : style.textAlign),
1281
1292
  verticalAlign: isValidVerticalAlignValue(style === null || style === void 0 ? void 0 : style.verticalAlign),
package/dist/serialize.js CHANGED
@@ -756,6 +756,7 @@ function writeDocumentGridConfig(b, config, usv) {
756
756
  })();
757
757
  Duc.DocumentGridConfig.addAlignItems(b, alignItems);
758
758
  Duc.DocumentGridConfig.addFirstPageAlone(b, config.firstPageAlone);
759
+ Duc.DocumentGridConfig.addScale(b, config.scale);
759
760
  return Duc.DocumentGridConfig.endDocumentGridConfig(b);
760
761
  }
761
762
  function writeText(b, e, usv) {
@@ -325,6 +325,7 @@ export type DocumentGridConfig = {
325
325
  gapY: number;
326
326
  alignItems: 'start' | 'center' | 'end';
327
327
  firstPageAlone: boolean;
328
+ scale: number;
328
329
  };
329
330
  export type DucPdfElement = _DucElementBase & {
330
331
  type: "pdf";
@@ -470,9 +471,8 @@ export type DucImageElement = _DucElementBase & {
470
471
  export type InitializedDucImageElement = MarkNonNullable<DucImageElement, "fileId">;
471
472
  export type FontFamilyKeys = keyof typeof FONT_FAMILY;
472
473
  export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
473
- export type FontString = string & {
474
- _brand: "fontString";
475
- };
474
+ /** Font family identifier — any valid CSS font-family string (Google Font name, system font, etc.) */
475
+ export type FontString = string;
476
476
  export type TextAlign = ValueOf<typeof TEXT_ALIGN>;
477
477
  export type VerticalAlign = ValueOf<typeof VERTICAL_ALIGN>;
478
478
  export type LineSpacingType = ValueOf<typeof LINE_SPACING_TYPE>;
@@ -486,7 +486,7 @@ export type DucTextStyle = {
486
486
  /**
487
487
  * The primary font family to use for the text
488
488
  */
489
- fontFamily: FontFamilyValues;
489
+ fontFamily: FontString;
490
490
  /**
491
491
  * Fallback font family for broader compatibility across all systems and languages
492
492
  * Useful for emojis, non-latin characters, etc.
@@ -1,9 +1,12 @@
1
1
  import type { ElementOrToolType } from "..";
2
2
  import type { MarkNonNullable } from "../utility-types";
3
3
  import { Bounds, LineSegment, TuplePoint } from "../geometryTypes";
4
- import type { DucArrowElement, DucBindableElement, DucElbowArrowElement, DucElement, DucElementType, DucEmbeddableElement, DucFlowchartNodeElement, DucFrameElement, DucFrameLikeElement, DucFreeDrawElement, DucImageElement, DucLinearElement, DucPlotElement, DucTableElement, DucPointBinding, DucTextContainer, DucTextElement, DucTextElementWithContainer, FixedPointBinding, InitializedDucImageElement, DucNonSelectionElement, DucEllipseElement, DucPolygonElement, NonDeleted, DucIframeLikeElement } from "./";
4
+ import type { DucArrowElement, DucBindableElement, DucDocElement, DucElbowArrowElement, DucElement, DucElementType, DucEmbeddableElement, DucFlowchartNodeElement, DucFrameElement, DucFrameLikeElement, DucFreeDrawElement, DucImageElement, DucLinearElement, DucPdfElement, DucPlotElement, DucTableElement, DucPointBinding, DucTextContainer, DucTextElement, DucTextElementWithContainer, FixedPointBinding, InitializedDucImageElement, DucNonSelectionElement, DucEllipseElement, DucPolygonElement, NonDeleted, DucIframeLikeElement } from "./";
5
5
  export declare const isInitializedImageElement: (element: DucElement | null) => element is InitializedDucImageElement;
6
6
  export declare const isImageElement: (element: DucElement | null) => element is DucImageElement;
7
+ export declare const isPdfElement: (element: DucElement | null) => element is DucPdfElement;
8
+ export type DucPdfLikeElement = DucPdfElement | DucDocElement;
9
+ export declare const isPdfLikeElement: (element: DucElement | null) => element is DucPdfLikeElement;
7
10
  export declare const isEmbeddableElement: (element: DucElement | null | undefined) => element is DucEmbeddableElement;
8
11
  export declare const isTableElement: (element: DucElement | null) => element is DucTableElement;
9
12
  export declare const isIframeLikeElement: (element: DucElement | null) => element is DucIframeLikeElement;
@@ -5,6 +5,12 @@ export const isInitializedImageElement = (element) => {
5
5
  export const isImageElement = (element) => {
6
6
  return !!element && element.type === "image";
7
7
  };
8
+ export const isPdfElement = (element) => {
9
+ return !!element && element.type === "pdf";
10
+ };
11
+ export const isPdfLikeElement = (element) => {
12
+ return !!element && (element.type === "pdf" || element.type === "doc");
13
+ };
8
14
  export const isEmbeddableElement = (element) => {
9
15
  return !!element && element.type === "embeddable";
10
16
  };
@@ -89,24 +89,28 @@ export declare const HIDE_FRAME_NAME_ZOOM_THRESHOLD = 0.18;
89
89
  export declare const DEFAULT_PROPORTIONAL_RADIUS = 0.25;
90
90
  export declare const DEFAULT_ADAPTIVE_RADIUS = 32;
91
91
  /**
92
- * // TODO: shouldn't be really `const`, likely neither have integers as values, due to value for the custom fonts, which should likely be some hash.
92
+ * Font family identifiers. Values are the actual CSS font-family names
93
+ * so they can be passed directly to Google Fonts / Canvas2D.
93
94
  *
94
- * Let's think this through and consider:
95
- * - https://developer.mozilla.org/en-US/docs/Web/CSS/generic-family
96
- * - https://drafts.csswg.org/css-fonts-4/#font-family-prop
97
- * - https://learn.microsoft.com/en-us/typography/opentype/spec/ibmfc
95
+ * For backward compatibility with old files that stored numeric IDs,
96
+ * use `LEGACY_FONT_ID_TO_NAME` to resolve them.
98
97
  */
99
98
  export declare const FONT_FAMILY: {
100
- Virgil: number;
101
- Helvetica: number;
102
- Cascadia: number;
103
- Excalifont: number;
104
- Nunito: number;
105
- "Lilita One": number;
106
- "Comic Shanns": number;
107
- "Liberation Sans": number;
108
- "Roboto Mono": number;
99
+ readonly Virgil: "Virgil";
100
+ readonly Helvetica: "Helvetica";
101
+ readonly Cascadia: "Cascadia";
102
+ readonly Excalifont: "Excalifont";
103
+ readonly Nunito: "Nunito";
104
+ readonly "Lilita One": "Lilita One";
105
+ readonly "Comic Shanns": "Comic Shanns";
106
+ readonly "Liberation Sans": "Liberation Sans";
107
+ readonly "Roboto Mono": "Roboto Mono";
109
108
  };
109
+ /**
110
+ * Reverse mapping from legacy numeric font IDs to font family names.
111
+ * Used when loading old .duc files that encoded fontFamily as a number.
112
+ */
113
+ export declare const LEGACY_FONT_ID_TO_NAME: Record<number, string>;
110
114
  export declare const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
111
115
  export declare const DEFAULT_VERSION = "{version}";
112
116
  export declare const MIN_FONT_SIZE = 1;
@@ -87,24 +87,37 @@ export const DEFAULT_PROPORTIONAL_RADIUS = 0.25;
87
87
  // Fixed radius for the ADAPTIVE_RADIUS algorithm. In pixels.
88
88
  export const DEFAULT_ADAPTIVE_RADIUS = 32;
89
89
  /**
90
- * // TODO: shouldn't be really `const`, likely neither have integers as values, due to value for the custom fonts, which should likely be some hash.
90
+ * Font family identifiers. Values are the actual CSS font-family names
91
+ * so they can be passed directly to Google Fonts / Canvas2D.
91
92
  *
92
- * Let's think this through and consider:
93
- * - https://developer.mozilla.org/en-US/docs/Web/CSS/generic-family
94
- * - https://drafts.csswg.org/css-fonts-4/#font-family-prop
95
- * - https://learn.microsoft.com/en-us/typography/opentype/spec/ibmfc
93
+ * For backward compatibility with old files that stored numeric IDs,
94
+ * use `LEGACY_FONT_ID_TO_NAME` to resolve them.
96
95
  */
97
96
  export const FONT_FAMILY = {
98
- Virgil: 1,
99
- Helvetica: 2,
100
- Cascadia: 3,
101
- // leave 4 unused as it was historically used for Assistant (which we don't use anymore) or custom font (Obsidian)
102
- Excalifont: 5,
103
- Nunito: 6,
104
- "Lilita One": 7,
105
- "Comic Shanns": 8,
106
- "Liberation Sans": 9,
107
- "Roboto Mono": 10,
97
+ Virgil: "Virgil",
98
+ Helvetica: "Helvetica",
99
+ Cascadia: "Cascadia",
100
+ Excalifont: "Excalifont",
101
+ Nunito: "Nunito",
102
+ "Lilita One": "Lilita One",
103
+ "Comic Shanns": "Comic Shanns",
104
+ "Liberation Sans": "Liberation Sans",
105
+ "Roboto Mono": "Roboto Mono",
106
+ };
107
+ /**
108
+ * Reverse mapping from legacy numeric font IDs to font family names.
109
+ * Used when loading old .duc files that encoded fontFamily as a number.
110
+ */
111
+ export const LEGACY_FONT_ID_TO_NAME = {
112
+ 1: "Virgil",
113
+ 2: "Helvetica",
114
+ 3: "Cascadia",
115
+ 5: "Excalifont",
116
+ 6: "Nunito",
117
+ 7: "Lilita One",
118
+ 8: "Comic Shanns",
119
+ 9: "Liberation Sans",
120
+ 10: "Roboto Mono",
108
121
  };
109
122
  export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
110
123
  export const DEFAULT_VERSION = "{version}";
@@ -1,2 +1,8 @@
1
1
  import { DucFreeDrawElement } from "../../types/elements";
2
2
  export declare function getFreeDrawSvgPath(element: DucFreeDrawElement): string;
3
+ /**
4
+ * Returns the raw outline polygon points from perfect-freehand.
5
+ * Each point is [x, y]. The result forms a closed polygon that
6
+ * represents the visual shape of the freedraw stroke.
7
+ */
8
+ export declare function getFreeDrawStrokePoints(element: DucFreeDrawElement): number[][];
@@ -22,16 +22,8 @@ function getSvgPathFromStroke(points) {
22
22
  .join(" ")
23
23
  .replace(TO_FIXED_PRECISION, "$1");
24
24
  }
25
- export function getFreeDrawSvgPath(element) {
26
- // If input points are empty (should they ever be?) return a dot
27
- if (element.points.length === 0) {
28
- return "";
29
- }
30
- const inputPoints = element.simulatePressure
31
- ? element.points.map(({ x, y }, i) => [x.scoped, y.scoped, element.pressures[i]])
32
- : element.points.map(({ x, y }) => [x.scoped, y.scoped]);
33
- // Consider changing the options for simulated pressure vs real pressure
34
- const options = {
25
+ function buildStrokeOptions(element) {
26
+ return {
35
27
  size: element.size.scoped,
36
28
  simulatePressure: element.simulatePressure,
37
29
  thinning: element.thinning,
@@ -40,7 +32,32 @@ export function getFreeDrawSvgPath(element) {
40
32
  easing: element.easing,
41
33
  start: element.start || undefined,
42
34
  end: element.end || undefined,
43
- last: !!element.lastCommittedPoint, // LastCommittedPoint is added on pointerup
35
+ last: !!element.lastCommittedPoint,
44
36
  };
37
+ }
38
+ function buildInputPoints(element) {
39
+ return element.simulatePressure
40
+ ? element.points.map(({ x, y }, i) => [x.scoped, y.scoped, element.pressures[i]])
41
+ : element.points.map(({ x, y }) => [x.scoped, y.scoped]);
42
+ }
43
+ export function getFreeDrawSvgPath(element) {
44
+ if (element.points.length === 0) {
45
+ return "";
46
+ }
47
+ const inputPoints = buildInputPoints(element);
48
+ const options = buildStrokeOptions(element);
45
49
  return getSvgPathFromStroke(getStroke(inputPoints, options));
46
50
  }
51
+ /**
52
+ * Returns the raw outline polygon points from perfect-freehand.
53
+ * Each point is [x, y]. The result forms a closed polygon that
54
+ * represents the visual shape of the freedraw stroke.
55
+ */
56
+ export function getFreeDrawStrokePoints(element) {
57
+ if (element.points.length === 0) {
58
+ return [];
59
+ }
60
+ const inputPoints = buildInputPoints(element);
61
+ const options = buildStrokeOptions(element);
62
+ return getStroke(inputPoints, options);
63
+ }
@@ -123,13 +123,22 @@ export const newTextElement = (currentScope, opts) => {
123
123
  const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
124
124
  const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
125
125
  const offsets = getTextElementPositionOffsets({ textAlign, verticalAlign }, metrics);
126
+ // Minimum dimensions: at least 1px wide, at least one line high (NaN-safe)
127
+ const rawMinLineHeight = fontSize.value * lineHeight;
128
+ const minLineHeight = (Number.isFinite(rawMinLineHeight) && rawMinLineHeight > 0)
129
+ ? rawMinLineHeight
130
+ : DEFAULT_FONT_SIZE * lineHeight;
131
+ const finalWidth = (Number.isFinite(metrics.width) && metrics.width > 0) ? metrics.width : 1;
132
+ const finalHeight = (Number.isFinite(metrics.height) && metrics.height > 0)
133
+ ? Math.max(metrics.height, minLineHeight)
134
+ : minLineHeight;
126
135
  const x = getPrecisionValueFromRaw(opts.x.value - offsets.x, scope, currentScope);
127
136
  const y = getPrecisionValueFromRaw(opts.y.value - offsets.y, scope, currentScope);
128
137
  return Object.assign(Object.assign({}, _newElementBase("text", currentScope, Object.assign(Object.assign({}, opts), { x, y }))), { type: "text", text,
129
138
  fontSize,
130
139
  fontFamily,
131
140
  textAlign,
132
- verticalAlign, width: getPrecisionValueFromRaw(metrics.width, scope, currentScope), height: getPrecisionValueFromRaw(metrics.height, scope, currentScope), containerId: opts.containerId || null, originalText: (_b = opts.originalText) !== null && _b !== void 0 ? _b : text, autoResize: (_c = opts.autoResize) !== null && _c !== void 0 ? _c : true, lineHeight,
141
+ verticalAlign, width: getPrecisionValueFromRaw(finalWidth, scope, currentScope), height: getPrecisionValueFromRaw(finalHeight, scope, currentScope), containerId: opts.containerId || null, originalText: (_b = opts.originalText) !== null && _b !== void 0 ? _b : text, autoResize: (_c = opts.autoResize) !== null && _c !== void 0 ? _c : true, lineHeight,
133
142
  // DucTextStyle properties
134
143
  isLtr: (_d = opts.isLtr) !== null && _d !== void 0 ? _d : true, bigFontFamily: opts.bigFontFamily || "sans-serif", lineSpacing: opts.lineSpacing || { type: LINE_SPACING_TYPE.MULTIPLE, value: lineHeight }, obliqueAngle: opts.obliqueAngle || 0, paperTextHeight: opts.paperTextHeight, widthFactor: opts.widthFactor || 1, isUpsideDown: (_e = opts.isUpsideDown) !== null && _e !== void 0 ? _e : false, isBackwards: (_f = opts.isBackwards) !== null && _f !== void 0 ? _f : false, dynamic: opts.dynamic || [] });
135
144
  };
@@ -153,11 +162,11 @@ export const newImageElement = (currentScope, opts) => {
153
162
  export const newTableElement = (currentScope, opts) => (Object.assign(Object.assign(Object.assign({}, _newElementBase("table", currentScope, opts)), getDefaultTableData(currentScope)), { type: "table" }));
154
163
  export const newDocElement = (currentScope, opts) => {
155
164
  var _a, _b, _c, _d;
156
- return (Object.assign(Object.assign({}, _newElementBase("doc", currentScope, opts)), { type: "doc", text: opts.text || "", dynamic: opts.dynamic || [], flowDirection: opts.flowDirection || TEXT_FLOW_DIRECTION.TOP_TO_BOTTOM, columns: opts.columns || { type: COLUMN_TYPE.NO_COLUMNS, definitions: [], autoHeight: true }, autoResize: (_a = opts.autoResize) !== null && _a !== void 0 ? _a : true, fileId: null, gridConfig: { columns: 1, gapX: 0, gapY: 0, alignItems: 'start', firstPageAlone: false },
165
+ return (Object.assign(Object.assign({}, _newElementBase("doc", currentScope, opts)), { type: "doc", text: opts.text || "", dynamic: opts.dynamic || [], flowDirection: opts.flowDirection || TEXT_FLOW_DIRECTION.TOP_TO_BOTTOM, columns: opts.columns || { type: COLUMN_TYPE.NO_COLUMNS, definitions: [], autoHeight: true }, autoResize: (_a = opts.autoResize) !== null && _a !== void 0 ? _a : true, fileId: null, gridConfig: { columns: 1, gapX: 0, gapY: 0, alignItems: 'start', firstPageAlone: false, scale: 1 },
157
166
  // DucDocStyle properties
158
167
  isLtr: (_b = opts.isLtr) !== null && _b !== void 0 ? _b : true, fontFamily: opts.fontFamily || DEFAULT_FONT_FAMILY, bigFontFamily: opts.bigFontFamily || "sans-serif", textAlign: opts.textAlign || DEFAULT_TEXT_ALIGN, verticalAlign: opts.verticalAlign || DEFAULT_VERTICAL_ALIGN, lineHeight: opts.lineHeight || 1.2, lineSpacing: opts.lineSpacing || { type: LINE_SPACING_TYPE.MULTIPLE, value: 1.2 }, obliqueAngle: opts.obliqueAngle || 0, fontSize: opts.fontSize || getPrecisionValueFromRaw(DEFAULT_FONT_SIZE, currentScope, currentScope), paperTextHeight: opts.paperTextHeight, widthFactor: opts.widthFactor || 1, isUpsideDown: (_c = opts.isUpsideDown) !== null && _c !== void 0 ? _c : false, isBackwards: (_d = opts.isBackwards) !== null && _d !== void 0 ? _d : false, paragraph: opts.paragraph || { firstLineIndent: getPrecisionValueFromRaw(0, currentScope, currentScope), hangingIndent: getPrecisionValueFromRaw(0, currentScope, currentScope), leftIndent: getPrecisionValueFromRaw(0, currentScope, currentScope), rightIndent: getPrecisionValueFromRaw(0, currentScope, currentScope), spaceBefore: getPrecisionValueFromRaw(0, currentScope, currentScope), spaceAfter: getPrecisionValueFromRaw(0, currentScope, currentScope), tabStops: [] }, stackFormat: opts.stackFormat || { autoStack: false, stackChars: [], properties: { upperScale: 0.7, lowerScale: 0.7, alignment: STACKED_TEXT_ALIGN.CENTER } } }));
159
168
  };
160
- export const newPdfElement = (currentScope, opts) => (Object.assign(Object.assign({ fileId: null, gridConfig: { columns: 1, gapX: 0, gapY: 0, alignItems: 'start', firstPageAlone: false } }, _newElementBase("pdf", currentScope, opts)), { type: "pdf" }));
169
+ export const newPdfElement = (currentScope, opts) => (Object.assign(Object.assign({ fileId: null, gridConfig: { columns: 1, gapX: 0, gapY: 0, alignItems: 'start', firstPageAlone: false, scale: 1 } }, _newElementBase("pdf", currentScope, opts)), { type: "pdf" }));
161
170
  export const newMermaidElement = (currentScope, opts) => (Object.assign(Object.assign({ source: "", theme: undefined, svgPath: null }, _newElementBase("mermaid", currentScope, opts)), { type: "mermaid" }));
162
171
  export const newXRayElement = (currentScope, opts) => (Object.assign(Object.assign({ origin: { x: getPrecisionValueFromRaw(0, currentScope, currentScope), y: getPrecisionValueFromRaw(0, currentScope, currentScope) }, direction: { x: getPrecisionValueFromRaw(1, currentScope, currentScope), y: getPrecisionValueFromRaw(0, currentScope, currentScope) }, startFromOrigin: false, color: '#FF00FF' }, _newElementBase("xray", currentScope, opts)), { type: "xray" }));
163
172
  export const newLeaderElement = (currentScope, opts) => {
@@ -1,9 +1,9 @@
1
+ import { SupportedMeasures } from "../../technical/scopes";
1
2
  import { DucLocalState, RawValue, Scope, ScopedValue } from "../../types";
2
3
  import { DucElement, DucElementType, DucTextContainer, DucTextElement, DucTextElementWithContainer, ElementsMap, FontFamilyValues, FontString, NonDeletedDucElement } from "../../types/elements";
3
4
  import { GeometricPoint } from "../../types/geometryTypes";
4
5
  import { ExtractSetType } from "../../types/utility-types";
5
6
  import { getBoundTextElementPosition } from "./linearElement";
6
- import { SupportedMeasures } from "../../technical/scopes";
7
7
  export declare const computeBoundTextPosition: (container: DucElement, boundTextElement: DucTextElementWithContainer, elementsMap: ElementsMap, currentScope: SupportedMeasures) => {
8
8
  x: ScopedValue;
9
9
  y: ScopedValue;
@@ -49,12 +49,12 @@ export declare const getMinTextElementWidth: (font: FontString, lineHeight: DucT
49
49
  /** retrieves text from text elements and concatenates to a single string */
50
50
  export declare const getTextFromElements: (elements: readonly DucElement[], separator?: string) => string;
51
51
  export declare const getFontFamilyString: ({ fontFamily, }: {
52
- fontFamily: FontFamilyValues;
52
+ fontFamily: FontFamilyValues | string;
53
53
  }) => string;
54
54
  /** returns fontSize+fontFamily string for assignment to DOM elements */
55
55
  export declare const getFontString: ({ fontSize, fontFamily, }: {
56
56
  fontSize: DucTextElement["fontSize"];
57
- fontFamily: FontFamilyValues;
57
+ fontFamily: FontFamilyValues | string;
58
58
  }) => FontString;
59
59
  /** computes element x/y offset based on textAlign/verticalAlign */
60
60
  export declare const getTextElementPositionOffsets: (opts: {
@@ -1,11 +1,11 @@
1
1
  import { TEXT_ALIGN, VERTICAL_ALIGN } from "../../flatbuffers/duc";
2
+ import { getPrecisionValueFromRaw, getScopedBezierPointFromDucPoint } from "../../technical/scopes";
2
3
  import { isArrowElement, isBoundToContainer, isTextElement } from "../../types/elements/typeChecks";
3
4
  import { getContainerElement, getElementAbsoluteCoords, getResizedElementAbsoluteCoords } from "../bounds";
4
- import { ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO, ARROW_LABEL_WIDTH_FRACTION, BOUND_TEXT_PADDING, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, FONT_FAMILY, WINDOWS_EMOJI_FALLBACK_FONT } from "../constants";
5
- import { getBoundTextElementPosition, getPointGlobalCoordinates, getPointsGlobalCoordinates, getSegmentMidPoint } from "./linearElement";
5
+ import { ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO, ARROW_LABEL_WIDTH_FRACTION, BOUND_TEXT_PADDING, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, LEGACY_FONT_ID_TO_NAME, WINDOWS_EMOJI_FALLBACK_FONT } from "../constants";
6
6
  import { adjustXYWithRotation } from "../math";
7
7
  import { normalizeText } from "../normalize";
8
- import { getPrecisionValueFromRaw, getScopedBezierPointFromDucPoint } from "../../technical/scopes";
8
+ import { getBoundTextElementPosition, getPointGlobalCoordinates, getPointsGlobalCoordinates, getSegmentMidPoint } from "./linearElement";
9
9
  export const computeBoundTextPosition = (container, boundTextElement, elementsMap, currentScope) => {
10
10
  if (isArrowElement(container)) {
11
11
  const coords = getBoundTextElementPosition(container, boundTextElement, elementsMap, currentScope);
@@ -54,10 +54,19 @@ export const measureText = (text, font, lineHeight, currentScope) => {
54
54
  // lines would be stripped from computation
55
55
  .map((x) => x || " ")
56
56
  .join("\n");
57
- const fontSize = getPrecisionValueFromRaw(parseFloat(font), currentScope, currentScope);
57
+ const parsedFontSize = parseFloat(font);
58
+ // Guard: if font string produced an unparseable size (NaN) or zero,
59
+ // fall back to DEFAULT_FONT_SIZE so measurements are never degenerate.
60
+ const safeFontSize = (Number.isFinite(parsedFontSize) && parsedFontSize > 0)
61
+ ? parsedFontSize
62
+ : DEFAULT_FONT_SIZE;
63
+ const fontSize = getPrecisionValueFromRaw(safeFontSize, currentScope, currentScope);
58
64
  const height = getTextHeight(text, fontSize, lineHeight);
59
65
  const width = getTextWidth(text, font);
60
- return { width, height };
66
+ // Defensive: ensure we never return 0 or NaN dimensions
67
+ const safeWidth = (Number.isFinite(width) && width > 0) ? width : 1;
68
+ const safeHeight = (Number.isFinite(height) && height > 0) ? height : (safeFontSize * lineHeight);
69
+ return { width: safeWidth, height: safeHeight };
61
70
  };
62
71
  /**
63
72
  * We calculate the line height from the font size and the unitless line height,
@@ -104,15 +113,15 @@ const getLineWidth = (text, font, forceAdvanceWidth, isTestEnv) => {
104
113
  // fallback to advance width if the actual width is zero, i.e. on text editing start
105
114
  // or when actual width does not respect whitespace chars, i.e. spaces
106
115
  // otherwise actual width should always be bigger
107
- return Math.max(actualWidth, advanceWidth);
116
+ return Math.ceil(Math.max(actualWidth, advanceWidth));
108
117
  }
109
118
  // since in test env the canvas measureText algo
110
119
  // doesn't measure text and instead just returns number of
111
120
  // characters hence we assume that each letteris 10px
112
121
  if (isTestEnv) {
113
- return advanceWidth * 10;
122
+ return Math.ceil(advanceWidth * 10);
114
123
  }
115
- return advanceWidth;
124
+ return Math.ceil(advanceWidth);
116
125
  };
117
126
  export const getTextWidth = (text, font, forceAdvanceWidth) => {
118
127
  const lines = splitIntoLines(text);
@@ -470,14 +479,20 @@ export const getTextFromElements = (elements, separator = "\n\n") => {
470
479
  return text;
471
480
  };
472
481
  export const getFontFamilyString = ({ fontFamily, }) => {
473
- // Handle both number and string fontFamily values
474
- const fontFamilyNum = typeof fontFamily === 'string' ? parseInt(fontFamily, 10) : fontFamily;
475
- for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) {
476
- if (id === fontFamilyNum) {
477
- return `${fontFamilyString}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
478
- }
479
- }
480
- return WINDOWS_EMOJI_FALLBACK_FONT;
482
+ // Handle legacy numeric font IDs from old files
483
+ if (typeof fontFamily === "number") {
484
+ const name = LEGACY_FONT_ID_TO_NAME[fontFamily];
485
+ if (name)
486
+ return `${name}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
487
+ return WINDOWS_EMOJI_FALLBACK_FONT;
488
+ }
489
+ // Handle stringified numeric IDs (e.g. "10")
490
+ const parsed = Number(fontFamily);
491
+ if (!Number.isNaN(parsed) && LEGACY_FONT_ID_TO_NAME[parsed]) {
492
+ return `${LEGACY_FONT_ID_TO_NAME[parsed]}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
493
+ }
494
+ // New path: fontFamily is already a string name
495
+ return `${fontFamily}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
481
496
  };
482
497
  /** returns fontSize+fontFamily string for assignment to DOM elements */
483
498
  export const getFontString = ({ fontSize, fontFamily, }) => {
@@ -504,6 +519,18 @@ export const refreshTextDimensions = (textElement, container, elementsMap, curre
504
519
  : textElement.width.scoped);
505
520
  }
506
521
  const dimensions = getAdjustedDimensions(textElement, elementsMap, text, currentScope);
522
+ // Defensive minimums — ensure height is always at least one line.
523
+ // Use negated >= to also catch NaN (NaN < x is always false).
524
+ const rawMinLineHeight = textElement.fontSize.value * textElement.lineHeight;
525
+ const minLineHeight = (Number.isFinite(rawMinLineHeight) && rawMinLineHeight > 0)
526
+ ? rawMinLineHeight
527
+ : DEFAULT_FONT_SIZE * textElement.lineHeight;
528
+ if (!(dimensions.height >= minLineHeight)) {
529
+ dimensions.height = minLineHeight;
530
+ }
531
+ if (!(dimensions.width > 0)) {
532
+ dimensions.width = (textElement.autoResize ? 1 : textElement.width.value);
533
+ }
507
534
  return Object.assign({ text }, dimensions);
508
535
  };
509
536
  export const splitIntoLines = (text) => {
@@ -78,7 +78,7 @@ export const getDefaultGlobalState = () => {
78
78
  return {
79
79
  name: null,
80
80
  viewBackgroundColor: typeof window !== "undefined" ? (window.matchMedia("(prefers-color-scheme: dark)").matches ? COLOR_PALETTE.night : COLOR_PALETTE.white) : COLOR_PALETTE.white,
81
- scopeExponentThreshold: 2,
81
+ scopeExponentThreshold: 3,
82
82
  mainScope: NEUTRAL_SCOPE,
83
83
  dashSpacingScale: 1,
84
84
  isDashSpacingAffectedByViewportScale: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ducjs",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "The duc 2D CAD file format is a cornerstone of our advanced design system, conceived to cater to professionals seeking precision and efficiency in their design work.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",