From 06bf2bb32d6529e8a44cff7685ef640d340683e3 Mon Sep 17 00:00:00 2001 From: Jorge Soares Date: Sun, 4 Jan 2026 22:31:32 +0000 Subject: [PATCH 1/2] fix: Update `custom_data` and `localization` fields to byte vectors across FlatBuffers schema and language bindings, add `fflate` dependency, and remove memory bank documentation. --- .gitignore | 5 +- Cargo.lock | 1 + bun.lock | 7 +- memory-bank/activeContext.md | 27 ------- memory-bank/productContext.md | 22 ------ memory-bank/progress.md | 26 ------- memory-bank/projectbrief.md | 21 ------ memory-bank/systemPatterns.md | 27 ------- memory-bank/techContext.md | 34 --------- packages/ducjs/package.json | 9 ++- packages/ducjs/src/flatbuffers/duc/delta.ts | 38 +++++++--- .../src/flatbuffers/duc/duc-block-metadata.ts | 34 +++++++-- .../src/flatbuffers/duc/duc-element-base.ts | 44 ++++++++--- packages/ducjs/src/parse.ts | 47 ++++++------ packages/ducjs/src/serialize.ts | 36 ++++----- packages/ducjs/src/types/index.ts | 3 +- packages/ducpy/src/ducpy/Duc/Delta.py | 43 +++++++---- .../ducpy/src/ducpy/Duc/DucBlockMetadata.py | 38 ++++++++-- .../ducpy/src/ducpy/Duc/_DucElementBase.py | 54 ++++++++++---- packages/ducpy/src/ducpy/parse.py | 29 +++++++- packages/ducpy/src/ducpy/serialize.py | 46 ++++++++++-- packages/ducrs/Cargo.toml | 7 +- .../ducrs/src/flatbuffers/duc_generated.rs | 73 ++++++++++++------- packages/ducrs/src/parse.rs | 39 +++++++++- packages/ducrs/src/serialize.rs | 42 ++++++++--- packages/ducrs/src/types.rs | 2 +- schema/duc.fbs | 17 +++-- 27 files changed, 437 insertions(+), 334 deletions(-) delete mode 100644 memory-bank/activeContext.md delete mode 100644 memory-bank/productContext.md delete mode 100644 memory-bank/progress.md delete mode 100644 memory-bank/projectbrief.md delete mode 100644 memory-bank/systemPatterns.md delete mode 100644 memory-bank/techContext.md diff --git a/.gitignore b/.gitignore index da66df42..d20ddbbc 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,7 @@ sst.pyi # flatc build_logs -.claude \ No newline at end of file +.claude + + +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fe2ea735..b931c9f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,6 +306,7 @@ dependencies = [ "base64 0.21.7", "env_logger", "flatbuffers", + "flate2", "lazy_static", "log", "percent-encoding", diff --git a/bun.lock b/bun.lock index 38f59eb5..81cfaae5 100644 --- a/bun.lock +++ b/bun.lock @@ -63,6 +63,7 @@ "dependencies": { "@braintree/sanitize-url": "6.0.2", "browser-fs-access": "0.35.0", + "fflate": "^0.8.2", "flatbuffers": "^24.12.23", "nanoid": "5.1.5", "perfect-freehand": "1.2.2", @@ -530,7 +531,7 @@ "@types/acorn": ["@types/acorn@4.0.6", "", { "dependencies": { "@types/estree": "*" } }, ""], - "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], @@ -654,7 +655,7 @@ "buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="], - "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -888,6 +889,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, ""], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, ""], diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md deleted file mode 100644 index 475befe0..00000000 --- a/memory-bank/activeContext.md +++ /dev/null @@ -1,27 +0,0 @@ -# Active Context - -## Current Work Focus -- Establishing and documenting the Memory Bank for project context and continuity. -- Ensuring all core documentation files reflect the current state and goals of the duc project. - -## Recent Changes -- Initialized the Memory Bank directory and all core markdown files. -- Populated projectbrief.md and productContext.md with repo-specific content. - -## Next Steps -- Complete detailed documentation for systemPatterns.md, techContext.md, and progress.md. -- Keep Memory Bank files updated as the project evolves and new features are added. - -## Active Decisions & Considerations -- Use Flatbuffers as the serialization backbone for the duc format. -- Maintain language bindings for TypeScript, Python, and Rust. -- Prioritize extensibility and performance in all design decisions. - -## Important Patterns & Preferences -- Strong typing and schema-driven development. -- Modular, cross-language architecture. -- Open-source, community-driven approach. - -## Learnings & Project Insights -- Clear documentation and context tracking are essential for cross-language, multi-package projects. -- Automated and cloud-based workflows are increasingly important in CAD/PLM environments. diff --git a/memory-bank/productContext.md b/memory-bank/productContext.md deleted file mode 100644 index 391daf87..00000000 --- a/memory-bank/productContext.md +++ /dev/null @@ -1,22 +0,0 @@ -# Product Context - -## Why This Project Exists -The `duc` project was created to address the lack of a modern, open, and extensible 2D CAD file format suitable for integration with cloud-based, automated, and cross-platform design workflows. Existing formats are often proprietary, limited in extensibility, or poorly documented. - -## Problems It Solves -- Lack of an open, well-documented 2D CAD file format for professional use. -- Difficulty integrating CAD data across different languages and platforms. -- Inefficient or legacy serialization formats that hinder performance and interoperability. -- Barriers to automation and cloud-based workflows in CAD/PLM environments. - -## How It Should Work -- Developers use the schema (Flatbuffers) and language bindings (TypeScript, Python, Rust) to read, write, and manipulate duc files. -- The format supports advanced CAD features, strong typing, and efficient serialization. -- Integration with web-based tools, documentation, and playgrounds for rapid prototyping and learning. -- Designed for extensibility and easy adoption in both open-source and commercial projects. - -## User Experience Goals -- Fast, reliable, and precise CAD data manipulation. -- Seamless integration with modern development environments and CI/CD pipelines. -- Clear, accessible documentation and onboarding. -- Consistent experience across all supported languages and platforms. diff --git a/memory-bank/progress.md b/memory-bank/progress.md deleted file mode 100644 index 91ebedcc..00000000 --- a/memory-bank/progress.md +++ /dev/null @@ -1,26 +0,0 @@ -# Progress - -## What Works -- Core Flatbuffers schema (schema/duc.fbs) is defined and maintained. -- TypeScript (ducjs), Python (ducpy), and Rust (ducrs) bindings are available. -- Utilities for parsing, validating, and manipulating duc files exist in all major languages. -- Web-based documentation and playground are accessible. - -## What's Left to Build -- Expand documentation and usage examples for all language bindings. -- Enhance integration guides for external platforms (e.g., Scopture). -- Improve automated tooling for schema-to-binding generation. -- Add more format converters (e.g., PDF, SVG, DXF) and sample applications. - -## Current Status -The project is stable and usable for core 2D CAD workflows, with active development on documentation, tooling, and integrations. - -## Known Issues -- Keeping all language bindings in sync with schema changes requires manual steps. -- Some advanced CAD features and converters are still under development. -- Contribution process for some packages is limited or closed. - -## Evolution of Project Decisions -- Adopted Flatbuffers for schema-driven, cross-language serialization. -- Moved to a monorepo structure for easier maintenance. -- Prioritized open-source licensing and community documentation. diff --git a/memory-bank/projectbrief.md b/memory-bank/projectbrief.md deleted file mode 100644 index 62b133e4..00000000 --- a/memory-bank/projectbrief.md +++ /dev/null @@ -1,21 +0,0 @@ -# Project Brief - -## Purpose -The `duc` project defines an open-source, cross-platform 2D CAD file format and supporting libraries. It aims to provide a modern, efficient, and extensible foundation for professional design workflows, enabling seamless integration with cloud-based and automated CAD/PLM solutions. - -## Core Requirements and Goals -- Provide a robust, open 2D CAD file format based on Flatbuffers for performance and compatibility. -- Support bindings and utilities for TypeScript/JavaScript, Python, and Rust. -- Enable precise, efficient manipulation and rendering of CAD data. -- Integrate with modern design systems and platforms (e.g., Scopture). -- Maintain strong type safety and developer ergonomics. -- Foster community adoption and contributions under the MIT license. - -## Scope -- Core file format definition (schema/duc.fbs). -- Language bindings and utilities: ducjs (TypeScript), ducpy (Python), ducrs (Rust), ducpdf, ducsvg, ducdxf. -- Web-based documentation and playground. -- Example applications and integration guides. - -## Source of Truth -This document is the foundation for all other Memory Bank files. Update as the project evolves. diff --git a/memory-bank/systemPatterns.md b/memory-bank/systemPatterns.md deleted file mode 100644 index 0020649e..00000000 --- a/memory-bank/systemPatterns.md +++ /dev/null @@ -1,27 +0,0 @@ -# System Patterns - -## System Architecture -- Modular, multi-language ecosystem centered on the `duc` 2D CAD file format. -- Core schema defined in Flatbuffers (schema/duc.fbs) for performance and cross-platform compatibility. -- Language bindings and utilities for TypeScript (ducjs), Python (ducpy), Rust (ducrs), and additional packages for PDF, SVG, and DXF support. -- Web-based documentation and playground for user onboarding and experimentation. - -## Key Technical Decisions -- Flatbuffers chosen for efficient, strongly-typed serialization and backward compatibility. -- MIT license to encourage open-source adoption and contributions. -- Maintain a single source of truth for the schema, with automated generation of language bindings. - -## Design Patterns in Use -- Schema-driven development: all data structures and APIs are derived from the Flatbuffers schema. -- Strong typing and interface-driven design in all language bindings. -- Modular package structure for maintainability and extensibility. - -## Component Relationships -- schema/duc.fbs is the foundation for all language bindings and utilities. -- ducjs, ducpy, ducrs, ducpdf, ducsvg, and ducdxf packages each provide language- or format-specific functionality, but share the same core schema. -- Web app and documentation integrate with the core libraries for demos and guides. - -## Critical Implementation Paths -- Schema updates propagate to all language bindings via automated tooling. -- Parsing, validation, and manipulation of duc files are handled by each language binding, following the schema definitions. -- Integration with external platforms (e.g., Scopture) via open APIs and extensible architecture. diff --git a/memory-bank/techContext.md b/memory-bank/techContext.md deleted file mode 100644 index 3526622c..00000000 --- a/memory-bank/techContext.md +++ /dev/null @@ -1,34 +0,0 @@ -# Technical Context - -## Technologies Used -- Flatbuffers (core schema and serialization) -- TypeScript/JavaScript (ducjs) -- Python (ducpy) -- Rust (ducrs) -- Node.js, Bun (JS runtime and tooling) -- React, Next.js (web documentation and playground) -- PDF, SVG, DXF libraries for format conversion - -## Development Setup -- Monorepo structure with multiple packages for each language and utility. -- Node.js or Bun required for JavaScript/TypeScript development. -- Python 3.x and Rust toolchains for respective bindings. -- Flatbuffers compiler required to generate language bindings from schema/duc.fbs. -- Standard package managers: npm, bun, pip, cargo. - -## Technical Constraints -- All data structures must be compatible with Flatbuffers serialization. -- Backward compatibility must be maintained when evolving the schema. -- Language bindings must stay in sync with the core schema. - -## Dependencies -- @ducflair/duc (TypeScript package) -- flatbuffers (all languages) -- Python: flatbuffers, setuptools -- Rust: flatbuffers, serde -- Additional dependencies for PDF, SVG, DXF support in respective packages - -## Tool Usage Patterns -- Flatbuffers compiler is used to generate/update bindings after schema changes. -- Each language package provides parsing, validation, and manipulation utilities. -- Web app and documentation use the TypeScript bindings for demos and guides. diff --git a/packages/ducjs/package.json b/packages/ducjs/package.json index f363198a..be5c6145 100644 --- a/packages/ducjs/package.json +++ b/packages/ducjs/package.json @@ -34,12 +34,13 @@ "undici-types": "^6.21.0" }, "dependencies": { - "flatbuffers": "^24.12.23", - "nanoid": "5.1.5", - "tinycolor2": "1.6.0", "@braintree/sanitize-url": "6.0.2", "browser-fs-access": "0.35.0", - "perfect-freehand": "1.2.2" + "fflate": "^0.8.2", + "flatbuffers": "^24.12.23", + "nanoid": "5.1.5", + "perfect-freehand": "1.2.2", + "tinycolor2": "1.6.0" }, "files": [ "dist", diff --git a/packages/ducjs/src/flatbuffers/duc/delta.ts b/packages/ducjs/src/flatbuffers/duc/delta.ts index 74383acc..5f570b35 100644 --- a/packages/ducjs/src/flatbuffers/duc/delta.ts +++ b/packages/ducjs/src/flatbuffers/duc/delta.ts @@ -4,7 +4,6 @@ import * as flatbuffers from 'flatbuffers'; -import { JSONPatchOperation } from '../duc/jsonpatch-operation'; import { VersionBase } from '../duc/version-base'; @@ -31,38 +30,52 @@ base(obj?:VersionBase):VersionBase|null { return offset ? (obj || new VersionBase()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; } -patch(index: number, obj?:JSONPatchOperation):JSONPatchOperation|null { - const offset = this.bb!.__offset(this.bb_pos, 6); - return offset ? (obj || new JSONPatchOperation()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null; +sizeBytes():bigint { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0'); +} + +patch(index: number):number|null { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.readUint8(this.bb!.__vector(this.bb_pos + offset) + index) : 0; } patchLength():number { - const offset = this.bb!.__offset(this.bb_pos, 6); + const offset = this.bb!.__offset(this.bb_pos, 10); return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; } +patchArray():Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? new Uint8Array(this.bb!.bytes().buffer, this.bb!.bytes().byteOffset + this.bb!.__vector(this.bb_pos + offset), this.bb!.__vector_len(this.bb_pos + offset)) : null; +} + static startDelta(builder:flatbuffers.Builder) { - builder.startObject(2); + builder.startObject(4); } static addBase(builder:flatbuffers.Builder, baseOffset:flatbuffers.Offset) { builder.addFieldOffset(0, baseOffset, 0); } +static addSizeBytes(builder:flatbuffers.Builder, sizeBytes:bigint) { + builder.addFieldInt64(2, sizeBytes, BigInt('0')); +} + static addPatch(builder:flatbuffers.Builder, patchOffset:flatbuffers.Offset) { - builder.addFieldOffset(1, patchOffset, 0); + builder.addFieldOffset(3, patchOffset, 0); } -static createPatchVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset { - builder.startVector(4, data.length, 4); +static createPatchVector(builder:flatbuffers.Builder, data:number[]|Uint8Array):flatbuffers.Offset { + builder.startVector(1, data.length, 1); for (let i = data.length - 1; i >= 0; i--) { - builder.addOffset(data[i]!); + builder.addInt8(data[i]!); } return builder.endVector(); } static startPatchVector(builder:flatbuffers.Builder, numElems:number) { - builder.startVector(4, numElems, 4); + builder.startVector(1, numElems, 1); } static endDelta(builder:flatbuffers.Builder):flatbuffers.Offset { @@ -70,9 +83,10 @@ static endDelta(builder:flatbuffers.Builder):flatbuffers.Offset { return offset; } -static createDelta(builder:flatbuffers.Builder, baseOffset:flatbuffers.Offset, patchOffset:flatbuffers.Offset):flatbuffers.Offset { +static createDelta(builder:flatbuffers.Builder, baseOffset:flatbuffers.Offset, sizeBytes:bigint, patchOffset:flatbuffers.Offset):flatbuffers.Offset { Delta.startDelta(builder); Delta.addBase(builder, baseOffset); + Delta.addSizeBytes(builder, sizeBytes); Delta.addPatch(builder, patchOffset); return Delta.endDelta(builder); } diff --git a/packages/ducjs/src/flatbuffers/duc/duc-block-metadata.ts b/packages/ducjs/src/flatbuffers/duc/duc-block-metadata.ts index aa218787..76fa029f 100644 --- a/packages/ducjs/src/flatbuffers/duc/duc-block-metadata.ts +++ b/packages/ducjs/src/flatbuffers/duc/duc-block-metadata.ts @@ -44,15 +44,23 @@ updatedAt():bigint { return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0'); } -localization():string|null -localization(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null -localization(optionalEncoding?:any):string|Uint8Array|null { - const offset = this.bb!.__offset(this.bb_pos, 12); - return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +localization(index: number):number|null { + const offset = this.bb!.__offset(this.bb_pos, 14); + return offset ? this.bb!.readUint8(this.bb!.__vector(this.bb_pos + offset) + index) : 0; +} + +localizationLength():number { + const offset = this.bb!.__offset(this.bb_pos, 14); + return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; +} + +localizationArray():Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 14); + return offset ? new Uint8Array(this.bb!.bytes().buffer, this.bb!.bytes().byteOffset + this.bb!.__vector(this.bb_pos + offset), this.bb!.__vector_len(this.bb_pos + offset)) : null; } static startDucBlockMetadata(builder:flatbuffers.Builder) { - builder.startObject(5); + builder.startObject(6); } static addSource(builder:flatbuffers.Builder, sourceOffset:flatbuffers.Offset) { @@ -72,7 +80,19 @@ static addUpdatedAt(builder:flatbuffers.Builder, updatedAt:bigint) { } static addLocalization(builder:flatbuffers.Builder, localizationOffset:flatbuffers.Offset) { - builder.addFieldOffset(4, localizationOffset, 0); + builder.addFieldOffset(5, localizationOffset, 0); +} + +static createLocalizationVector(builder:flatbuffers.Builder, data:number[]|Uint8Array):flatbuffers.Offset { + builder.startVector(1, data.length, 1); + for (let i = data.length - 1; i >= 0; i--) { + builder.addInt8(data[i]!); + } + return builder.endVector(); +} + +static startLocalizationVector(builder:flatbuffers.Builder, numElems:number) { + builder.startVector(1, numElems, 1); } static endDucBlockMetadata(builder:flatbuffers.Builder):flatbuffers.Offset { diff --git a/packages/ducjs/src/flatbuffers/duc/duc-element-base.ts b/packages/ducjs/src/flatbuffers/duc/duc-element-base.ts index 2d6d668f..f26c8bfa 100644 --- a/packages/ducjs/src/flatbuffers/duc/duc-element-base.ts +++ b/packages/ducjs/src/flatbuffers/duc/duc-element-base.ts @@ -196,13 +196,6 @@ locked():boolean { return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; } -customData():string|null -customData(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null -customData(optionalEncoding?:any):string|Uint8Array|null { - const offset = this.bb!.__offset(this.bb_pos, 58); - return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; -} - blockIds(index: number):string blockIds(index: number,optionalEncoding:flatbuffers.Encoding):string|Uint8Array blockIds(index: number,optionalEncoding?:any):string|Uint8Array|null { @@ -222,8 +215,23 @@ instanceId(optionalEncoding?:any):string|Uint8Array|null { return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; } +customData(index: number):number|null { + const offset = this.bb!.__offset(this.bb_pos, 64); + return offset ? this.bb!.readUint8(this.bb!.__vector(this.bb_pos + offset) + index) : 0; +} + +customDataLength():number { + const offset = this.bb!.__offset(this.bb_pos, 64); + return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; +} + +customDataArray():Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 64); + return offset ? new Uint8Array(this.bb!.bytes().buffer, this.bb!.bytes().byteOffset + this.bb!.__vector(this.bb_pos + offset), this.bb!.__vector_len(this.bb_pos + offset)) : null; +} + static start_DucElementBase(builder:flatbuffers.Builder) { - builder.startObject(30); + builder.startObject(31); } static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) { @@ -370,10 +378,6 @@ static addLocked(builder:flatbuffers.Builder, locked:boolean) { builder.addFieldInt8(26, +locked, +false); } -static addCustomData(builder:flatbuffers.Builder, customDataOffset:flatbuffers.Offset) { - builder.addFieldOffset(27, customDataOffset, 0); -} - static addBlockIds(builder:flatbuffers.Builder, blockIdsOffset:flatbuffers.Offset) { builder.addFieldOffset(28, blockIdsOffset, 0); } @@ -394,6 +398,22 @@ static addInstanceId(builder:flatbuffers.Builder, instanceIdOffset:flatbuffers.O builder.addFieldOffset(29, instanceIdOffset, 0); } +static addCustomData(builder:flatbuffers.Builder, customDataOffset:flatbuffers.Offset) { + builder.addFieldOffset(30, customDataOffset, 0); +} + +static createCustomDataVector(builder:flatbuffers.Builder, data:number[]|Uint8Array):flatbuffers.Offset { + builder.startVector(1, data.length, 1); + for (let i = data.length - 1; i >= 0; i--) { + builder.addInt8(data[i]!); + } + return builder.endVector(); +} + +static startCustomDataVector(builder:flatbuffers.Builder, numElems:number) { + builder.startVector(1, numElems, 1); +} + static end_DucElementBase(builder:flatbuffers.Builder):flatbuffers.Offset { const offset = builder.endObject(); builder.requiredField(offset, 4) // id diff --git a/packages/ducjs/src/parse.ts b/packages/ducjs/src/parse.ts index b3deb8de..fbd71e9c 100644 --- a/packages/ducjs/src/parse.ts +++ b/packages/ducjs/src/parse.ts @@ -1,4 +1,5 @@ import { FileSystemHandle } from 'browser-fs-access'; +import { decompressSync, strFromU8 } from 'fflate'; import { CustomHatchPattern as CustomHatchPatternFb, DimensionToleranceStyle as DimensionToleranceStyleFb, @@ -190,6 +191,7 @@ import { VersionGraph, ViewportScale, Zoom, + JSONPatch, _DucElementBase, _DucElementStylesBase, _DucLinearElementBase, @@ -215,7 +217,27 @@ const toZoom = (value: number): Zoom => ({ } as Zoom); // #endregion +// Helper function to parse binary JSON data (Uint8Array) to object +// The data is zlib-compressed JSON (new format) or plain JSON string (legacy format) +function parseBinaryToJson(binaryData: Uint8Array | null): Record | undefined { + if (!binaryData || binaryData.length === 0) return undefined; + // Try new format: zlib-compressed binary JSON + try { + const decompressed = decompressSync(binaryData); + const text = strFromU8(decompressed); + return JSON.parse(text); + } catch (e) { + // Fall back to legacy format: plain JSON string (for old file compatibility) + try { + const text = new TextDecoder().decode(binaryData); + return JSON.parse(text); + } catch (e2) { + console.warn('Failed to parse binary JSON (tried both compressed and legacy formats):', e2); + return undefined; + } + } +} // #region GEOMETRY & UTILITY PARSERS export function parseGeometricPoint(point: GeometricPointFb): GeometricPoint { @@ -405,7 +427,7 @@ export function parseElementBase(base: _DucElementBaseFb): _DucElementBase { zIndex: base.zIndex(), link: base.link(), locked: base.locked(), - customData: base.customData() ? JSON.parse(base.customData()!) : undefined, + customData: parseBinaryToJson(base.customDataArray()), ...parsedStyles, } as _DucElementBase; } @@ -726,17 +748,8 @@ function parseBlockInstance(instance: DucBlockInstanceFb): DucBlockInstance { function parseBlockMetadata(metadataFb: DucBlockMetadataFb | null): DucBlockMetadata | undefined { if (!metadataFb) return undefined; - // localization is a JSON string containing the localization data - let localization: BlockLocalizationMap | undefined; - const localizationStr = metadataFb.localization(); - if (localizationStr) { - try { - localization = JSON.parse(localizationStr); - } catch (e) { - // If parsing fails, leave localization undefined - console.warn('Failed to parse localization JSON:', e); - } - } + // localization is now binary JSON data (Uint8Array) + const localization = parseBinaryToJson(metadataFb.localizationArray()) as BlockLocalizationMap | undefined; return { source: metadataFb.source()!, @@ -1747,15 +1760,7 @@ export function parseVersionGraphFromBinary(graph: VersionGraphFb | null): Versi description: base.description() || undefined, isManualSave: base.isManualSave(), userId: base.userId() || undefined, - patch: Array.from({ length: d.patchLength() }, (_, j) => { - const p = d.patch(j)!; - return { - op: p.op()!, - path: p.path()!, - from: p.from() || undefined, - value: p.value() ? JSON.parse(p.value()!) : undefined, - }; - }), + patch: parseBinaryToJson(d.patchArray()) as JSONPatch, }; }), metadata: { diff --git a/packages/ducjs/src/serialize.ts b/packages/ducjs/src/serialize.ts index 6a213459..1db758c4 100644 --- a/packages/ducjs/src/serialize.ts +++ b/packages/ducjs/src/serialize.ts @@ -9,6 +9,7 @@ */ import * as flatbuffers from "flatbuffers"; import * as Duc from "./flatbuffers/duc"; +import { zlibSync, strToU8 } from "fflate"; import { _DucElementStylesBase, @@ -118,7 +119,7 @@ import { encodeFunctionString, EXPORT_DATA_TYPES } from "./utils"; /** * Basic helpers */ -const str = (b: flatbuffers.Builder, v: string | null | undefined): number | undefined => +const str = (b: flatbuffers.Builder, v: string | Uint8Array | null | undefined): number | undefined => v == null ? undefined : b.createString(v); function writeString(builder: flatbuffers.Builder, str: string | null | undefined): number | undefined { @@ -447,7 +448,7 @@ function writeElementBase(b: flatbuffers.Builder, e: _DucElementStylesBase & _Du const frameId = str(b, e.frameId ?? undefined); const bound = e.boundElements?.length ? Duc._DucElementBase.createBoundElementsVector(b, e.boundElements.map((x) => writeBoundElement(b, x, usv))) : undefined; const link = str(b, e.link ?? undefined); - const custom = e.customData != null ? str(b, JSON.stringify(e.customData)) : undefined; + const custom = e.customData != null ? Duc._DucElementBase.createCustomDataVector(b, zlibSync(strToU8(JSON.stringify(e.customData)))) : undefined; Duc._DucElementBase.start_DucElementBase(b); if (id) Duc._DucElementBase.addId(b, id); @@ -847,7 +848,7 @@ function writeBlockAttrDef(b: flatbuffers.Builder, d: DucBlockAttributeDefinitio function writeBlockMetadata(b: flatbuffers.Builder, metadata: DucBlockMetadata): number { const source = b.createString(metadata.source); - const localization = metadata.localization ? b.createString(JSON.stringify(metadata.localization)) : undefined; + const localization = metadata.localization ? Duc.DucBlockMetadata.createLocalizationVector(b, zlibSync(strToU8(JSON.stringify(metadata.localization)))) : undefined; Duc.DucBlockMetadata.startDucBlockMetadata(b); Duc.DucBlockMetadata.addSource(b, source); @@ -955,9 +956,9 @@ function writeBlockCollection(b: flatbuffers.Builder, c: DucBlockCollection): nu const metadata = c.metadata; let localizationOffset: number | undefined; if (metadata.localization) { - // localization is stored as a JSON string - const localizationStr = JSON.stringify(metadata.localization); - localizationOffset = b.createString(localizationStr); + // localization is stored as compressed binary JSON data + const localizationBin = zlibSync(strToU8(JSON.stringify(metadata.localization))); + localizationOffset = Duc.DucBlockMetadata.createLocalizationVector(b, localizationBin); } const source = b.createString(metadata.source); @@ -2306,29 +2307,20 @@ function serializeCheckpoint(b: flatbuffers.Builder, c: Checkpoint): number { return Duc.Checkpoint.endCheckpoint(b); } -function writeJsonPatch(b: flatbuffers.Builder, p: JSONPatch): number { - const ops = p.map((op) => { - const opStr = b.createString(op.op); - const pathStr = b.createString(op.path); - const fromStr = op.from !== undefined ? b.createString(op.from) : undefined; - const valueStr = op.value !== undefined ? b.createString(JSON.stringify(op.value)) : undefined; - - Duc.JSONPatchOperation.startJSONPatchOperation(b); - Duc.JSONPatchOperation.addOp(b, opStr); - Duc.JSONPatchOperation.addPath(b, pathStr); - if (fromStr) Duc.JSONPatchOperation.addFrom(b, fromStr); - if (valueStr) Duc.JSONPatchOperation.addValue(b, valueStr); - return Duc.JSONPatchOperation.endJSONPatchOperation(b); - }); - return Duc.Delta.createPatchVector(b, ops); +function writeJsonPatch(b: flatbuffers.Builder, p: JSONPatch): { offset: number; sizeBytes: number } { + // Compress the JSON patch data + const patchData = zlibSync(strToU8(JSON.stringify(p))); + const offset = Duc.Delta.createPatchVector(b, patchData); + return { offset, sizeBytes: patchData.length }; } function serializeDelta(b: flatbuffers.Builder, d: Delta): number { const base = serializeVersionBase(b, d); - const patch = writeJsonPatch(b, d.patch); + const { offset: patch, sizeBytes } = writeJsonPatch(b, d.patch); Duc.Delta.startDelta(b); Duc.Delta.addBase(b, base); Duc.Delta.addPatch(b, patch); + Duc.Delta.addSizeBytes(b, BigInt(sizeBytes)); return Duc.Delta.endDelta(b); } diff --git a/packages/ducjs/src/types/index.ts b/packages/ducjs/src/types/index.ts index 4abdfe8e..6cb08a20 100644 --- a/packages/ducjs/src/types/index.ts +++ b/packages/ducjs/src/types/index.ts @@ -34,7 +34,7 @@ import { Radian, ScaleFactor, } from "./geometryTypes"; -import { MakeBrand, MaybePromise, ValueOf } from "./utility-types"; +import { MakeBrand, MarkOptional, MaybePromise, ValueOf } from "./utility-types"; import type { GRID_DISPLAY_TYPE, GRID_TYPE, @@ -82,6 +82,7 @@ export interface ExportedDataState { } export type ExportedDataStateContent = Omit; +export type BaseExportedDataState = MarkOptional; /** * A version of the data state where all fields are optional. diff --git a/packages/ducpy/src/ducpy/Duc/Delta.py b/packages/ducpy/src/ducpy/Duc/Delta.py index c05b2b18..5620c897 100644 --- a/packages/ducpy/src/ducpy/Duc/Delta.py +++ b/packages/ducpy/src/ducpy/Duc/Delta.py @@ -39,33 +39,42 @@ def Base(self): return obj return None + # Delta + def SizeBytes(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int64Flags, o + self._tab.Pos) + return 0 + # Delta def Patch(self, j): - o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) if o != 0: - x = self._tab.Vector(o) - x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4 - x = self._tab.Indirect(x) - from Duc.JSONPatchOperation import JSONPatchOperation - obj = JSONPatchOperation() - obj.Init(self._tab.Bytes, x) - return obj - return None + a = self._tab.Vector(o) + return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1)) + return 0 + + # Delta + def PatchAsNumpy(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) + if o != 0: + return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o) + return 0 # Delta def PatchLength(self): - o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) if o != 0: return self._tab.VectorLen(o) return 0 # Delta def PatchIsNone(self): - o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) return o == 0 def DeltaStart(builder): - builder.StartObject(2) + builder.StartObject(4) def Start(builder): DeltaStart(builder) @@ -76,14 +85,20 @@ def DeltaAddBase(builder, base): def AddBase(builder, base): DeltaAddBase(builder, base) +def DeltaAddSizeBytes(builder, sizeBytes): + builder.PrependInt64Slot(2, sizeBytes, 0) + +def AddSizeBytes(builder, sizeBytes): + DeltaAddSizeBytes(builder, sizeBytes) + def DeltaAddPatch(builder, patch): - builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(patch), 0) + builder.PrependUOffsetTRelativeSlot(3, flatbuffers.number_types.UOffsetTFlags.py_type(patch), 0) def AddPatch(builder, patch): DeltaAddPatch(builder, patch) def DeltaStartPatchVector(builder, numElems): - return builder.StartVector(4, numElems, 4) + return builder.StartVector(1, numElems, 1) def StartPatchVector(builder, numElems): return DeltaStartPatchVector(builder, numElems) diff --git a/packages/ducpy/src/ducpy/Duc/DucBlockMetadata.py b/packages/ducpy/src/ducpy/Duc/DucBlockMetadata.py index 080d11a3..f4d0fef2 100644 --- a/packages/ducpy/src/ducpy/Duc/DucBlockMetadata.py +++ b/packages/ducpy/src/ducpy/Duc/DucBlockMetadata.py @@ -57,14 +57,34 @@ def UpdatedAt(self): return 0 # DucBlockMetadata - def Localization(self): - o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) + def Localization(self, j): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) if o != 0: - return self._tab.String(o + self._tab.Pos) - return None + a = self._tab.Vector(o) + return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1)) + return 0 + + # DucBlockMetadata + def LocalizationAsNumpy(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) + if o != 0: + return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o) + return 0 + + # DucBlockMetadata + def LocalizationLength(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) + if o != 0: + return self._tab.VectorLen(o) + return 0 + + # DucBlockMetadata + def LocalizationIsNone(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) + return o == 0 def DucBlockMetadataStart(builder): - builder.StartObject(5) + builder.StartObject(6) def Start(builder): DucBlockMetadataStart(builder) @@ -94,11 +114,17 @@ def AddUpdatedAt(builder, updatedAt): DucBlockMetadataAddUpdatedAt(builder, updatedAt) def DucBlockMetadataAddLocalization(builder, localization): - builder.PrependUOffsetTRelativeSlot(4, flatbuffers.number_types.UOffsetTFlags.py_type(localization), 0) + builder.PrependUOffsetTRelativeSlot(5, flatbuffers.number_types.UOffsetTFlags.py_type(localization), 0) def AddLocalization(builder, localization): DucBlockMetadataAddLocalization(builder, localization) +def DucBlockMetadataStartLocalizationVector(builder, numElems): + return builder.StartVector(1, numElems, 1) + +def StartLocalizationVector(builder, numElems): + return DucBlockMetadataStartLocalizationVector(builder, numElems) + def DucBlockMetadataEnd(builder): return builder.EndObject() diff --git a/packages/ducpy/src/ducpy/Duc/_DucElementBase.py b/packages/ducpy/src/ducpy/Duc/_DucElementBase.py index c00bbcb5..0fc67614 100644 --- a/packages/ducpy/src/ducpy/Duc/_DucElementBase.py +++ b/packages/ducpy/src/ducpy/Duc/_DucElementBase.py @@ -265,13 +265,6 @@ def Locked(self): return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) return False - # _DucElementBase - def CustomData(self): - o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(58)) - if o != 0: - return self._tab.String(o + self._tab.Pos) - return None - # _DucElementBase def BlockIds(self, j): o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(60)) @@ -299,8 +292,35 @@ def InstanceId(self): return self._tab.String(o + self._tab.Pos) return None + # _DucElementBase + def CustomData(self, j): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(64)) + if o != 0: + a = self._tab.Vector(o) + return self._tab.Get(flatbuffers.number_types.Uint8Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 1)) + return 0 + + # _DucElementBase + def CustomDataAsNumpy(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(64)) + if o != 0: + return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint8Flags, o) + return 0 + + # _DucElementBase + def CustomDataLength(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(64)) + if o != 0: + return self._tab.VectorLen(o) + return 0 + + # _DucElementBase + def CustomDataIsNone(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(64)) + return o == 0 + def _DucElementBaseStart(builder): - builder.StartObject(30) + builder.StartObject(31) def Start(builder): _DucElementBaseStart(builder) @@ -485,12 +505,6 @@ def _DucElementBaseAddLocked(builder, locked): def AddLocked(builder, locked): _DucElementBaseAddLocked(builder, locked) -def _DucElementBaseAddCustomData(builder, customData): - builder.PrependUOffsetTRelativeSlot(27, flatbuffers.number_types.UOffsetTFlags.py_type(customData), 0) - -def AddCustomData(builder, customData): - _DucElementBaseAddCustomData(builder, customData) - def _DucElementBaseAddBlockIds(builder, blockIds): builder.PrependUOffsetTRelativeSlot(28, flatbuffers.number_types.UOffsetTFlags.py_type(blockIds), 0) @@ -509,6 +523,18 @@ def _DucElementBaseAddInstanceId(builder, instanceId): def AddInstanceId(builder, instanceId): _DucElementBaseAddInstanceId(builder, instanceId) +def _DucElementBaseAddCustomData(builder, customData): + builder.PrependUOffsetTRelativeSlot(30, flatbuffers.number_types.UOffsetTFlags.py_type(customData), 0) + +def AddCustomData(builder, customData): + _DucElementBaseAddCustomData(builder, customData) + +def _DucElementBaseStartCustomDataVector(builder, numElems): + return builder.StartVector(1, numElems, 1) + +def StartCustomDataVector(builder, numElems): + return _DucElementBaseStartCustomDataVector(builder, numElems) + def _DucElementBaseEnd(builder): return builder.EndObject() diff --git a/packages/ducpy/src/ducpy/parse.py b/packages/ducpy/src/ducpy/parse.py index 04a85e78..b6b503c9 100644 --- a/packages/ducpy/src/ducpy/parse.py +++ b/packages/ducpy/src/ducpy/parse.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import gzip from typing import List, Dict, Optional, Union, Any, IO import flatbuffers @@ -495,6 +496,16 @@ def _json_or_none(s: Optional[bytes]) -> Optional[Dict[str, Any]]: except Exception: return None +def _binary_json_or_none(data: Optional[bytes]) -> Optional[Any]: + """Parse gzip-compressed binary JSON data.""" + if not data: + return None + try: + decompressed = gzip.decompress(data) + return json.loads(decompressed.decode("utf-8")) + except Exception: + return None + def _geopoint_struct_to_ds(gp) -> Optional[DS_GeometricPoint]: if gp is None: return None @@ -738,7 +749,11 @@ def parse_fbs_duc_element_base(obj: FBSDucElementBase) -> DS_DucElementBase: bound_elements = [parse_fbs_bound_element(obj.BoundElements(i)) for i in range(obj.BoundElementsLength())] except Exception: pass - custom_data = _json_or_none(obj.CustomData()) if hasattr(obj, "CustomData") else None + # custom_data is now binary compressed JSON + custom_data = None + if hasattr(obj, "CustomData") and not obj.CustomDataIsNone(): + custom_data_bytes = _read_bytes_from_numpy(obj, "CustomDataLength", "CustomDataAsNumpy", "CustomData") + custom_data = _binary_json_or_none(custom_data_bytes) return DS_DucElementBase( id=_s_req(obj.Id()) if hasattr(obj, "Id") else "", styles=styles, @@ -1616,12 +1631,18 @@ def parse_fbs_duc_block_attribute_definition_entry(obj: FBSDucBlockAttributeDefi ) def parse_fbs_duc_block_metadata(obj: FBSDucBlockMetadata) -> DS_DucBlockMetadata: + # localization is now binary compressed JSON + localization = None + if hasattr(obj, "Localization") and not obj.LocalizationIsNone(): + localization_bytes = _read_bytes_from_numpy(obj, "LocalizationLength", "LocalizationAsNumpy", "Localization") + localization = _binary_json_or_none(localization_bytes) + return DS_DucBlockMetadata( source=_s_req(obj.Source()), usage_count=obj.UsageCount(), created_at=obj.CreatedAt(), updated_at=obj.UpdatedAt(), - localization=_s(obj.Localization()), + localization=localization, ) def parse_fbs_duc_block(obj: FBSDucBlock) -> DS_DucBlock: @@ -2286,7 +2307,9 @@ def parse_fbs_json_patch_operation(obj: FBSJSONPatchOperation) -> DS_JSONPatchOp def parse_fbs_delta(obj: FBSDelta) -> DS_Delta: base = obj.Base() base_kwargs = _parse_version_base_kwargs(base) - patch = [parse_fbs_json_patch_operation(obj.Patch(i)) for i in range(obj.PatchLength())] + # patch is now binary compressed JSON data + patch_bytes = _read_bytes_from_numpy(obj, "PatchLength", "PatchAsNumpy", "Patch") + patch = _binary_json_or_none(patch_bytes) or [] return DS_Delta( type="delta", patch=patch, diff --git a/packages/ducpy/src/ducpy/serialize.py b/packages/ducpy/src/ducpy/serialize.py index cc64386a..68b56e4b 100644 --- a/packages/ducpy/src/ducpy/serialize.py +++ b/packages/ducpy/src/ducpy/serialize.py @@ -6,6 +6,7 @@ import flatbuffers import logging import json +import gzip from typing import List, Dict, Union, Any, Optional logger = logging.getLogger(__name__) @@ -1148,7 +1149,13 @@ def serialize_fbs_duc_element_base(builder: flatbuffers.Builder, base: DS_DucEle custom_data_offset = 0 if base.custom_data: - custom_data_offset = builder.CreateString(json.dumps(base.custom_data)) + # Compress JSON and create byte vector for custom_data + json_str = json.dumps(base.custom_data) + compressed = gzip.compress(json_str.encode("utf-8")) + _DucElementBaseStartCustomDataVector(builder, len(compressed)) + for i in reversed(range(len(compressed))): + builder.PrependByte(compressed[i]) + custom_data_offset = builder.EndVector(len(compressed)) instance_id_offset = 0 if base.instance_id: @@ -2669,7 +2676,7 @@ def serialize_fbs_element_wrapper(builder: flatbuffers.Builder, wrapper: DS_Elem JSONPatchOperationAddValue, JSONPatchOperationEnd ) from ducpy.Duc.Delta import ( - DeltaStart, DeltaAddBase, DeltaAddPatch, DeltaEnd, DeltaStartPatchVector + DeltaStart, DeltaAddBase, DeltaAddPatch, DeltaAddSizeBytes, DeltaEnd, DeltaStartPatchVector ) from ducpy.Duc.VersionGraphMetadata import ( VersionGraphMetadataStart, VersionGraphMetadataAddLastPruned, @@ -3364,7 +3371,16 @@ def serialize_fbs_block_attribute_definition_entry(builder: flatbuffers.Builder, def serialize_fbs_block_metadata(builder: flatbuffers.Builder, metadata: DS_DucBlockMetadata) -> int: source_offset = builder.CreateString(metadata.source) - localization_offset = _str(builder, metadata.localization) + + # Compress localization JSON and create byte vector + localization_offset = 0 + if metadata.localization: + json_str = json.dumps(metadata.localization) + compressed = gzip.compress(json_str.encode("utf-8")) + DucBlockMetadataStartLocalizationVector(builder, len(compressed)) + for i in reversed(range(len(compressed))): + builder.PrependByte(compressed[i]) + localization_offset = builder.EndVector(len(compressed)) DucBlockMetadataStart(builder) DucBlockMetadataAddSource(builder, source_offset) @@ -3645,14 +3661,28 @@ def serialize_fbs_json_patch_operation(builder: flatbuffers.Builder, op: DS_JSON def serialize_fbs_delta(builder: flatbuffers.Builder, d: DS_Delta) -> int: base_offset = serialize_fbs_version_base(builder, d) - patch_offsets = [serialize_fbs_json_patch_operation(builder, p) for p in (d.patch or [])] - DeltaStartPatchVector(builder, len(patch_offsets)) - for off in reversed(patch_offsets): - builder.PrependUOffsetTRelative(off) - patch_vec = builder.EndVector() + + # Convert JSONPatchOperation dataclasses to dicts for JSON serialization + from dataclasses import asdict, is_dataclass + patch_list = [] + for op in (d.patch or []): + if is_dataclass(op): + patch_list.append(asdict(op)) + else: + # Already a dict (from parsed data) + patch_list.append(op) + json_str = json.dumps(patch_list) + compressed = gzip.compress(json_str.encode("utf-8")) + DeltaStartPatchVector(builder, len(compressed)) + for i in reversed(range(len(compressed))): + builder.PrependByte(compressed[i]) + patch_vec = builder.EndVector(len(compressed)) + size_bytes = len(compressed) + DeltaStart(builder) DeltaAddBase(builder, base_offset) DeltaAddPatch(builder, patch_vec) + DeltaAddSizeBytes(builder, size_bytes) return DeltaEnd(builder) def serialize_fbs_version_graph_metadata(builder: flatbuffers.Builder, m: Optional[DS_VersionGraphMetadata]) -> int: diff --git a/packages/ducrs/Cargo.toml b/packages/ducrs/Cargo.toml index 980c28c8..e8be6e85 100644 --- a/packages/ducrs/Cargo.toml +++ b/packages/ducrs/Cargo.toml @@ -13,8 +13,9 @@ flatbuffers = "25.2.10" base64 = "0.21.5" percent-encoding = "2.3.1" log = "0.4.22" -serde = { version = "1.0", features = ["derive"], optional = true } -serde_json = { version = "1.0", optional = true } +flate2 = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" lazy_static = "1.4" [dev-dependencies] @@ -22,4 +23,4 @@ env_logger = "0.10" [features] default = [] -serde_support = ["serde", "serde_json"] \ No newline at end of file +# serde_support is now always enabled since serde and serde_json are required dependencies \ No newline at end of file diff --git a/packages/ducrs/src/flatbuffers/duc_generated.rs b/packages/ducrs/src/flatbuffers/duc_generated.rs index 29466d02..299aee76 100644 --- a/packages/ducrs/src/flatbuffers/duc_generated.rs +++ b/packages/ducrs/src/flatbuffers/duc_generated.rs @@ -8321,9 +8321,9 @@ impl<'a> _DucElementBase<'a> { pub const VT_Z_INDEX: flatbuffers::VOffsetT = 52; pub const VT_LINK: flatbuffers::VOffsetT = 54; pub const VT_LOCKED: flatbuffers::VOffsetT = 56; - pub const VT_CUSTOM_DATA: flatbuffers::VOffsetT = 58; pub const VT_BLOCK_IDS: flatbuffers::VOffsetT = 60; pub const VT_INSTANCE_ID: flatbuffers::VOffsetT = 62; + pub const VT_CUSTOM_DATA: flatbuffers::VOffsetT = 64; #[inline] pub unsafe fn init_from_table(table: flatbuffers::Table<'a>) -> Self { @@ -8341,9 +8341,9 @@ impl<'a> _DucElementBase<'a> { builder.add_width(args.width); builder.add_y(args.y); builder.add_x(args.x); + if let Some(x) = args.custom_data { builder.add_custom_data(x); } if let Some(x) = args.instance_id { builder.add_instance_id(x); } if let Some(x) = args.block_ids { builder.add_block_ids(x); } - if let Some(x) = args.custom_data { builder.add_custom_data(x); } if let Some(x) = args.link { builder.add_link(x); } builder.add_z_index(args.z_index); if let Some(x) = args.bound_elements { builder.add_bound_elements(x); } @@ -8569,25 +8569,25 @@ impl<'a> _DucElementBase<'a> { unsafe { self._tab.get::(_DucElementBase::VT_LOCKED, Some(false)).unwrap()} } #[inline] - pub fn custom_data(&self) -> Option<&'a str> { + pub fn block_ids(&self) -> Option>> { // Safety: // Created from valid Table for this object // which contains a valid value in this slot - unsafe { self._tab.get::>(_DucElementBase::VT_CUSTOM_DATA, None)} + unsafe { self._tab.get::>>>(_DucElementBase::VT_BLOCK_IDS, None)} } #[inline] - pub fn block_ids(&self) -> Option>> { + pub fn instance_id(&self) -> Option<&'a str> { // Safety: // Created from valid Table for this object // which contains a valid value in this slot - unsafe { self._tab.get::>>>(_DucElementBase::VT_BLOCK_IDS, None)} + unsafe { self._tab.get::>(_DucElementBase::VT_INSTANCE_ID, None)} } #[inline] - pub fn instance_id(&self) -> Option<&'a str> { + pub fn custom_data(&self) -> Option> { // Safety: // Created from valid Table for this object // which contains a valid value in this slot - unsafe { self._tab.get::>(_DucElementBase::VT_INSTANCE_ID, None)} + unsafe { self._tab.get::>>(_DucElementBase::VT_CUSTOM_DATA, None)} } } @@ -8625,9 +8625,9 @@ impl flatbuffers::Verifiable for _DucElementBase<'_> { .visit_field::("z_index", Self::VT_Z_INDEX, false)? .visit_field::>("link", Self::VT_LINK, false)? .visit_field::("locked", Self::VT_LOCKED, false)? - .visit_field::>("custom_data", Self::VT_CUSTOM_DATA, false)? .visit_field::>>>("block_ids", Self::VT_BLOCK_IDS, false)? .visit_field::>("instance_id", Self::VT_INSTANCE_ID, false)? + .visit_field::>>("custom_data", Self::VT_CUSTOM_DATA, false)? .finish(); Ok(()) } @@ -8660,9 +8660,9 @@ pub struct _DucElementBaseArgs<'a> { pub z_index: f32, pub link: Option>, pub locked: bool, - pub custom_data: Option>, pub block_ids: Option>>>, pub instance_id: Option>, + pub custom_data: Option>>, } impl<'a> Default for _DucElementBaseArgs<'a> { #[inline] @@ -8695,9 +8695,9 @@ impl<'a> Default for _DucElementBaseArgs<'a> { z_index: 0.0, link: None, locked: false, - custom_data: None, block_ids: None, instance_id: None, + custom_data: None, } } } @@ -8816,10 +8816,6 @@ impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> _DucElementBaseBuilder<'a, 'b, self.fbb_.push_slot::(_DucElementBase::VT_LOCKED, locked, false); } #[inline] - pub fn add_custom_data(&mut self, custom_data: flatbuffers::WIPOffset<&'b str>) { - self.fbb_.push_slot_always::>(_DucElementBase::VT_CUSTOM_DATA, custom_data); - } - #[inline] pub fn add_block_ids(&mut self, block_ids: flatbuffers::WIPOffset>>) { self.fbb_.push_slot_always::>(_DucElementBase::VT_BLOCK_IDS, block_ids); } @@ -8828,6 +8824,10 @@ impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> _DucElementBaseBuilder<'a, 'b, self.fbb_.push_slot_always::>(_DucElementBase::VT_INSTANCE_ID, instance_id); } #[inline] + pub fn add_custom_data(&mut self, custom_data: flatbuffers::WIPOffset>) { + self.fbb_.push_slot_always::>(_DucElementBase::VT_CUSTOM_DATA, custom_data); + } + #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>) -> _DucElementBaseBuilder<'a, 'b, A> { let start = _fbb.start_table(); _DucElementBaseBuilder { @@ -8873,9 +8873,9 @@ impl core::fmt::Debug for _DucElementBase<'_> { ds.field("z_index", &self.z_index()); ds.field("link", &self.link()); ds.field("locked", &self.locked()); - ds.field("custom_data", &self.custom_data()); ds.field("block_ids", &self.block_ids()); ds.field("instance_id", &self.instance_id()); + ds.field("custom_data", &self.custom_data()); ds.finish() } } @@ -17866,7 +17866,7 @@ impl<'a> DucBlockMetadata<'a> { pub const VT_USAGE_COUNT: flatbuffers::VOffsetT = 6; pub const VT_CREATED_AT: flatbuffers::VOffsetT = 8; pub const VT_UPDATED_AT: flatbuffers::VOffsetT = 10; - pub const VT_LOCALIZATION: flatbuffers::VOffsetT = 12; + pub const VT_LOCALIZATION: flatbuffers::VOffsetT = 14; #[inline] pub unsafe fn init_from_table(table: flatbuffers::Table<'a>) -> Self { @@ -17916,11 +17916,11 @@ impl<'a> DucBlockMetadata<'a> { unsafe { self._tab.get::(DucBlockMetadata::VT_UPDATED_AT, Some(0)).unwrap()} } #[inline] - pub fn localization(&self) -> Option<&'a str> { + pub fn localization(&self) -> Option> { // Safety: // Created from valid Table for this object // which contains a valid value in this slot - unsafe { self._tab.get::>(DucBlockMetadata::VT_LOCALIZATION, None)} + unsafe { self._tab.get::>>(DucBlockMetadata::VT_LOCALIZATION, None)} } } @@ -17935,7 +17935,7 @@ impl flatbuffers::Verifiable for DucBlockMetadata<'_> { .visit_field::("usage_count", Self::VT_USAGE_COUNT, false)? .visit_field::("created_at", Self::VT_CREATED_AT, false)? .visit_field::("updated_at", Self::VT_UPDATED_AT, false)? - .visit_field::>("localization", Self::VT_LOCALIZATION, false)? + .visit_field::>>("localization", Self::VT_LOCALIZATION, false)? .finish(); Ok(()) } @@ -17945,7 +17945,7 @@ pub struct DucBlockMetadataArgs<'a> { pub usage_count: i32, pub created_at: i64, pub updated_at: i64, - pub localization: Option>, + pub localization: Option>>, } impl<'a> Default for DucBlockMetadataArgs<'a> { #[inline] @@ -17982,7 +17982,7 @@ impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> DucBlockMetadataBuilder<'a, 'b, self.fbb_.push_slot::(DucBlockMetadata::VT_UPDATED_AT, updated_at, 0); } #[inline] - pub fn add_localization(&mut self, localization: flatbuffers::WIPOffset<&'b str>) { + pub fn add_localization(&mut self, localization: flatbuffers::WIPOffset>) { self.fbb_.push_slot_always::>(DucBlockMetadata::VT_LOCALIZATION, localization); } #[inline] @@ -31340,7 +31340,8 @@ impl<'a> flatbuffers::Follow<'a> for Delta<'a> { impl<'a> Delta<'a> { pub const VT_BASE: flatbuffers::VOffsetT = 4; - pub const VT_PATCH: flatbuffers::VOffsetT = 6; + pub const VT_SIZE_BYTES: flatbuffers::VOffsetT = 8; + pub const VT_PATCH: flatbuffers::VOffsetT = 10; #[inline] pub unsafe fn init_from_table(table: flatbuffers::Table<'a>) -> Self { @@ -31352,6 +31353,7 @@ impl<'a> Delta<'a> { args: &'args DeltaArgs<'args> ) -> flatbuffers::WIPOffset> { let mut builder = DeltaBuilder::new(_fbb); + builder.add_size_bytes(args.size_bytes); if let Some(x) = args.patch { builder.add_patch(x); } if let Some(x) = args.base { builder.add_base(x); } builder.finish() @@ -31366,11 +31368,18 @@ impl<'a> Delta<'a> { unsafe { self._tab.get::>(Delta::VT_BASE, None)} } #[inline] - pub fn patch(&self) -> Option>>> { + pub fn size_bytes(&self) -> i64 { // Safety: // Created from valid Table for this object // which contains a valid value in this slot - unsafe { self._tab.get::>>>(Delta::VT_PATCH, None)} + unsafe { self._tab.get::(Delta::VT_SIZE_BYTES, Some(0)).unwrap()} + } + #[inline] + pub fn patch(&self) -> Option> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::>>(Delta::VT_PATCH, None)} } } @@ -31382,20 +31391,23 @@ impl flatbuffers::Verifiable for Delta<'_> { use self::flatbuffers::Verifiable; v.visit_table(pos)? .visit_field::>("base", Self::VT_BASE, false)? - .visit_field::>>>("patch", Self::VT_PATCH, false)? + .visit_field::("size_bytes", Self::VT_SIZE_BYTES, false)? + .visit_field::>>("patch", Self::VT_PATCH, false)? .finish(); Ok(()) } } pub struct DeltaArgs<'a> { pub base: Option>>, - pub patch: Option>>>>, + pub size_bytes: i64, + pub patch: Option>>, } impl<'a> Default for DeltaArgs<'a> { #[inline] fn default() -> Self { DeltaArgs { base: None, + size_bytes: 0, patch: None, } } @@ -31411,7 +31423,11 @@ impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> DeltaBuilder<'a, 'b, A> { self.fbb_.push_slot_always::>(Delta::VT_BASE, base); } #[inline] - pub fn add_patch(&mut self, patch: flatbuffers::WIPOffset>>>) { + pub fn add_size_bytes(&mut self, size_bytes: i64) { + self.fbb_.push_slot::(Delta::VT_SIZE_BYTES, size_bytes, 0); + } + #[inline] + pub fn add_patch(&mut self, patch: flatbuffers::WIPOffset>) { self.fbb_.push_slot_always::>(Delta::VT_PATCH, patch); } #[inline] @@ -31433,6 +31449,7 @@ impl core::fmt::Debug for Delta<'_> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let mut ds = f.debug_struct("Delta"); ds.field("base", &self.base()); + ds.field("size_bytes", &self.size_bytes()); ds.field("patch", &self.patch()); ds.finish() } diff --git a/packages/ducrs/src/parse.rs b/packages/ducrs/src/parse.rs index 82f24307..c1eb172b 100644 --- a/packages/ducrs/src/parse.rs +++ b/packages/ducrs/src/parse.rs @@ -47,6 +47,31 @@ fn parse_vec_of_required_strings(vec: Option>) -> Option { + match vec { + Some(v) if v.len() > 0 => { + // Collect the bytes into a Vec + let data: Vec = (0..v.len()).map(|i| v.get(i)).collect(); + + // Parse zlib-compressed JSON + use flate2::read::ZlibDecoder; + use std::io::Read; + + let mut d = ZlibDecoder::new(data.as_slice()); + let mut decompressed = Vec::new(); + if d.read_to_end(&mut decompressed).is_ok() { + String::from_utf8(decompressed).ok() + } else { + None + } + } + _ => None, + } +} + // ============================================================================= // UTILITY & GEOMETRY TYPES @@ -315,7 +340,7 @@ fn parse_duc_element_base(base: fb::_DucElementBase) -> ParseResult ParseResult ParseResult ParseResult { - let patch_vec = delta.patch().ok_or("Missing Delta.patch")?; - let patch = patch_vec.iter().map(parse_json_patch_operation).collect::>()?; + // patch is now zlib-compressed JSON data + let patch_json = parse_binary_json_to_string(delta.patch()) + .ok_or("Failed to parse delta patch")?; + + // Parse the JSON string into a vector of JSONPatchOperation + let patch: Vec = serde_json::from_str(&patch_json) + .map_err(|_| "Failed to parse delta patch JSON")?; + Ok(types::Delta { base: parse_version_base(delta.base().ok_or("Missing Delta.base")?)?, patch, diff --git a/packages/ducrs/src/serialize.rs b/packages/ducrs/src/serialize.rs index 05d4ba82..851d5e9a 100644 --- a/packages/ducrs/src/serialize.rs +++ b/packages/ducrs/src/serialize.rs @@ -36,6 +36,18 @@ fn serialize_vec_of_strings<'bldr>( Some(v) } +/// Helper function to compress a string using zlib compression +/// Returns the compressed byte vector +fn compress_string(s: &str) -> Vec { + use flate2::write::ZlibEncoder; + use flate2::Compression; + use std::io::Write; + + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(s.as_bytes()).unwrap(); + encoder.finish().unwrap() +} + // ============================================================================= // UTILITY & GEOMETRY TYPES // ============================================================================= @@ -471,7 +483,12 @@ fn serialize_duc_element_base<'bldr>( Some(builder.create_vector(&bound_elements_offsets)) }; let link_offset = base.link.as_ref().map(|s| builder.create_string(s)); - let custom_data_offset = base.custom_data.as_ref().map(|s| builder.create_string(s)); + + // Compress custom_data JSON and create byte vector + let custom_data_offset = base.custom_data.as_ref().map(|s| { + let compressed = compress_string(s); + builder.create_vector::(&compressed) + }); fb::_DucElementBase::create( builder, @@ -2499,10 +2516,15 @@ pub fn serialize_duc_block_metadata<'bldr>( metadata: &types::DucBlockMetadata, ) -> WIPOffset> { let source_offset = builder.create_string(&metadata.source); - let localization_offset: Option> = metadata + + // Compress localization JSON and create byte vector + let localization_offset = metadata .localization .as_ref() - .map(|s| builder.create_string(s.as_str())); + .map(|s| { + let compressed = compress_string(s); + builder.create_vector::(&compressed) + }); fb::DucBlockMetadata::create( builder, @@ -3746,17 +3768,19 @@ fn serialize_delta<'bldr>( delta: &types::Delta, ) -> WIPOffset> { let base_offset = serialize_version_base(builder, &delta.base); - let patch_offsets: Vec<_> = delta - .patch - .iter() - .map(|p| serialize_json_patch_operation(builder, p)) - .collect(); - let patch_vec = builder.create_vector(&patch_offsets); + + // Serialize patch as compressed JSON data + let patch_json = serde_json::to_string(&delta.patch).unwrap(); + let patch_compressed = compress_string(&patch_json); + let patch_vec = builder.create_vector::(&patch_compressed); + let size_bytes = patch_compressed.len() as i64; + fb::Delta::create( builder, &fb::DeltaArgs { base: Some(base_offset), patch: Some(patch_vec), + size_bytes, }, ) } diff --git a/packages/ducrs/src/types.rs b/packages/ducrs/src/types.rs index 18e177e4..c63eae6c 100644 --- a/packages/ducrs/src/types.rs +++ b/packages/ducrs/src/types.rs @@ -1532,7 +1532,7 @@ pub struct Checkpoint { pub size_bytes: i64, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct JSONPatchOperation { pub op: String, pub path: String, diff --git a/schema/duc.fbs b/schema/duc.fbs index 22b63b00..a24861c0 100644 --- a/schema/duc.fbs +++ b/schema/duc.fbs @@ -1012,9 +1012,7 @@ table _DucElementBase { link: string; locked: bool; - /** Contains a JSON string of custom key-value data. */ - custom_data: string; - + custom_data_string: string (deprecated); /** * List of blocks this element helps *define*. * If this is populated, `instanceId` should be null. @@ -1026,6 +1024,9 @@ table _DucElementBase { * If not null, `block_ids` is empty (the relationship to the Block is via the Instance). */ instance_id: string; + + /** Contains a JSON of custom key-value data. */ + custom_data: [ubyte]; } table DucPoint { @@ -1724,7 +1725,9 @@ table DucBlockMetadata { * description?: string; * } */ - localization: string; + localization_string: string (deprecated); + localization: [ubyte]; + } @@ -2621,7 +2624,11 @@ table JSONPatchOperation { table Delta { base: VersionBase; - patch: [JSONPatchOperation]; + patch_string: [JSONPatchOperation] (deprecated); + size_bytes: long; + + /** Compressed binary data for the delta (zlib). When present, patch is ignored. */ + patch: [ubyte]; } table VersionGraphMetadata { From 8a943f964134f6e8bf1eda1426b22fd3ef242a45 Mon Sep 17 00:00:00 2001 From: Jorge Soares Date: Sat, 10 Jan 2026 16:20:48 +0000 Subject: [PATCH 2/2] refactor: made blocks source more explicit, added utils --- packages/ducjs/src/parse.ts | 16 ++++++++++------ packages/ducjs/src/restore/restoreDataState.ts | 7 ++++++- packages/ducjs/src/types/elements/index.ts | 10 ++++++---- packages/ducjs/src/utils/index.ts | 8 ++++++++ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/ducjs/src/parse.ts b/packages/ducjs/src/parse.ts index fbd71e9c..8b7ca145 100644 --- a/packages/ducjs/src/parse.ts +++ b/packages/ducjs/src/parse.ts @@ -1,11 +1,13 @@ import { FileSystemHandle } from 'browser-fs-access'; import { decompressSync, strFromU8 } from 'fflate'; +import * as flatbuffers from "flatbuffers"; +import { nanoid } from 'nanoid'; import { CustomHatchPattern as CustomHatchPatternFb, DimensionToleranceStyle as DimensionToleranceStyleFb, DucArrowElement as DucArrowElementFb, - DucBlock as DucBlockFb, DucBlockCollection as DucBlockCollectionFb, + DucBlock as DucBlockFb, DucBlockInstance as DucBlockInstanceFb, DucBlockMetadata as DucBlockMetadataFb, DucCommonStyle as DucCommonStyleFb, @@ -117,7 +119,6 @@ import { DucFeatureControlFrameElement, DucFeatureControlFrameStyle, DucFrameElement, - DucFreeDrawEasing, DucFreeDrawElement, DucGlobalState, DucGroup, @@ -169,6 +170,7 @@ import { GeometricPoint, GridSettings, HatchPatternLine, + JSONPatch, LeaderContent, LineHead, NormalizedZoomValue, @@ -191,15 +193,12 @@ import { VersionGraph, ViewportScale, Zoom, - JSONPatch, _DucElementBase, _DucElementStylesBase, _DucLinearElementBase, _DucStackBase, _DucStackElementBase } from "./types"; -import * as flatbuffers from "flatbuffers"; -import { nanoid } from 'nanoid'; // #region HELPERS & LOW-LEVEL CASTS @@ -751,8 +750,13 @@ function parseBlockMetadata(metadataFb: DucBlockMetadataFb | null): DucBlockMeta // localization is now binary JSON data (Uint8Array) const localization = parseBinaryToJson(metadataFb.localizationArray()) as BlockLocalizationMap | undefined; + const rawSource = metadataFb.source(); + const source = typeof rawSource === "string" && rawSource.trim().length + ? rawSource.trim() + : undefined; + return { - source: metadataFb.source()!, + ...(source ? { source } : {}), usageCount: metadataFb.usageCount(), createdAt: Number(metadataFb.createdAt()), updatedAt: Number(metadataFb.updatedAt()), diff --git a/packages/ducjs/src/restore/restoreDataState.ts b/packages/ducjs/src/restore/restoreDataState.ts index b97b0e86..29376655 100644 --- a/packages/ducjs/src/restore/restoreDataState.ts +++ b/packages/ducjs/src/restore/restoreDataState.ts @@ -563,8 +563,13 @@ const restoreBlockMetadata = (metadata: unknown): DucBlock["metadata"] | undefin } } + const rawSource = metadataObj.source; + const source = typeof rawSource === "string" && rawSource.trim().length + ? rawSource.trim() + : undefined; + return { - source: typeof metadataObj.source === "string" ? metadataObj.source : "", + ...(source ? { source } : {}), usageCount: typeof metadataObj.usageCount === "number" ? metadataObj.usageCount : 0, createdAt: typeof metadataObj.createdAt === "number" ? metadataObj.createdAt : Date.now(), updatedAt: typeof metadataObj.updatedAt === "number" ? metadataObj.updatedAt : Date.now(), diff --git a/packages/ducjs/src/types/elements/index.ts b/packages/ducjs/src/types/elements/index.ts index 6926b7ad..0b797c1b 100644 --- a/packages/ducjs/src/types/elements/index.ts +++ b/packages/ducjs/src/types/elements/index.ts @@ -1052,8 +1052,10 @@ export type DucBlockAttributeDefinition = { isConstant: boolean; }; -/** Indicates whether the block belongs to the project, organization or community */ -export type BlockSourceType = string | "organization" | "community"; +/** + * Indicates the source drawing of a block. + */ +export type BlockSource = string; export interface BlockLocalizationEntry { title: string; @@ -1067,8 +1069,8 @@ export interface BlockLocalizationEntry { export type BlockLocalizationMap = Record; export interface DucBlockMetadata { - /** Indicates whether the block belongs to the project, organization or community */ - source: BlockSourceType; + /** Drawing id this block originates from */ + source?: BlockSource; /** Total number of times the block was instantiated */ usageCount: number; /** Creation timestamp */ diff --git a/packages/ducjs/src/utils/index.ts b/packages/ducjs/src/utils/index.ts index e7649d00..32935e6e 100644 --- a/packages/ducjs/src/utils/index.ts +++ b/packages/ducjs/src/utils/index.ts @@ -55,6 +55,14 @@ export const arrayToMap = ( }, new Map()); }; +export function omitKeys, K extends keyof T>( + obj: T, + keys: K[] +): Omit { + const result = { ...obj }; + keys.forEach(key => delete result[key]); + return result; +} const RS_LTR_CHARS = "A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF" +