From faa018279f179ea7fbbc580f0e16a5e80cef710f Mon Sep 17 00:00:00 2001 From: Jorge Soares Date: Tue, 27 Jan 2026 18:57:05 +0000 Subject: [PATCH 1/4] fix: block duplication spacing handling in `ducjs`, remove JSON patch serialization and parsing from `ducrs`, update Turborepo build dependencies --- packages/ducdxf/.gitignore | 1 + .../ducjs/src/restore/restoreDataState.ts | 17 ++++------------- packages/ducjs/src/serialize.ts | 13 +++++++++++-- packages/ducrs/src/parse.rs | 8 -------- packages/ducrs/src/serialize.rs | 19 ------------------- turbo.json | 6 ++++-- 6 files changed, 20 insertions(+), 44 deletions(-) create mode 100644 packages/ducdxf/.gitignore diff --git a/packages/ducdxf/.gitignore b/packages/ducdxf/.gitignore new file mode 100644 index 0000000..a66c47a --- /dev/null +++ b/packages/ducdxf/.gitignore @@ -0,0 +1 @@ +*.egg-info \ No newline at end of file diff --git a/packages/ducjs/src/restore/restoreDataState.ts b/packages/ducjs/src/restore/restoreDataState.ts index 2937665..03509fc 100644 --- a/packages/ducjs/src/restore/restoreDataState.ts +++ b/packages/ducjs/src/restore/restoreDataState.ts @@ -188,7 +188,7 @@ export const restore = ( const restoredRegions = restoreRegions(data?.regions); const restoredGroups = restoreGroups(data?.groups); const restoredLayers = restoreLayers(data?.layers, restoredLocalState.scope); - const restoredBlockInstances = restoreBlockInstances(data?.blockInstances); + const restoredBlockInstances = restoreBlockInstances(data?.blockInstances, restoredLocalState.scope); const restoredElements = restoreElements( data?.elements, @@ -474,6 +474,7 @@ export const restoreBlockCollections = ( */ export const restoreBlockInstances = ( blockInstances: unknown, + currentScope: Scope, ): RestoredDataState["blockInstances"] => { if (!Array.isArray(blockInstances)) { return []; @@ -532,8 +533,8 @@ export const restoreBlockInstances = ( ? { rows: typeof dupArray.rows === "number" ? dupArray.rows : 1, cols: typeof dupArray.cols === "number" ? dupArray.cols : 1, - rowSpacing: typeof dupArray.rowSpacing === "number" ? dupArray.rowSpacing : 0, - colSpacing: typeof dupArray.colSpacing === "number" ? dupArray.colSpacing : 0, + rowSpacing: restorePrecisionValue(dupArray.rowSpacing, NEUTRAL_SCOPE, currentScope), + colSpacing: restorePrecisionValue(dupArray.colSpacing, NEUTRAL_SCOPE, currentScope), } : null, }; @@ -577,16 +578,6 @@ const restoreBlockMetadata = (metadata: unknown): DucBlock["metadata"] | undefin }; }; -/** - * Restores instances from block instances data (alias for restoreBlockInstances) - * This method provides a consistent naming convention for instance restoration. - */ -export const restoreInstances = ( - instances: unknown, -): RestoredDataState["blockInstances"] => { - return restoreBlockInstances(instances); -}; - /** * Restores the global state of the document from imported data. * It validates and provides defaults for missing or invalid properties. diff --git a/packages/ducjs/src/serialize.ts b/packages/ducjs/src/serialize.ts index 1db758c..f3ed282 100644 --- a/packages/ducjs/src/serialize.ts +++ b/packages/ducjs/src/serialize.ts @@ -920,8 +920,17 @@ function writeBlockInstance(b: flatbuffers.Builder, i: DucBlockInstance, usv: bo Duc.DucBlockDuplicationArray.startDucBlockDuplicationArray(b); Duc.DucBlockDuplicationArray.addRows(b, i.duplicationArray.rows); Duc.DucBlockDuplicationArray.addCols(b, i.duplicationArray.cols); - Duc.DucBlockDuplicationArray.addRowSpacing(b, getPrecisionValue(i.duplicationArray.rowSpacing, usv)); - Duc.DucBlockDuplicationArray.addColSpacing(b, getPrecisionValue(i.duplicationArray.colSpacing, usv)); + + const rSpacing = typeof i.duplicationArray.rowSpacing === 'number' + ? i.duplicationArray.rowSpacing + : getPrecisionValue(i.duplicationArray.rowSpacing, usv); + + const cSpacing = typeof i.duplicationArray.colSpacing === 'number' + ? i.duplicationArray.colSpacing + : getPrecisionValue(i.duplicationArray.colSpacing, usv); + + Duc.DucBlockDuplicationArray.addRowSpacing(b, rSpacing); + Duc.DucBlockDuplicationArray.addColSpacing(b, cSpacing); return Duc.DucBlockDuplicationArray.endDucBlockDuplicationArray(b); })() : undefined; diff --git a/packages/ducrs/src/parse.rs b/packages/ducrs/src/parse.rs index c1eb172..866a436 100644 --- a/packages/ducrs/src/parse.rs +++ b/packages/ducrs/src/parse.rs @@ -1924,14 +1924,6 @@ fn parse_checkpoint(checkpoint: fb::Checkpoint) -> ParseResult ParseResult { - Ok(types::JSONPatchOperation { - op: op.op().ok_or("Missing JSONPatchOperation.op")?.to_string(), - path: op.path().ok_or("Missing JSONPatchOperation.path")?.to_string(), - from: op.from().map(|s| s.to_string()), - value: op.value().map(|s| s.to_string()), - }) -} fn parse_delta(delta: fb::Delta) -> ParseResult { // patch is now zlib-compressed JSON data diff --git a/packages/ducrs/src/serialize.rs b/packages/ducrs/src/serialize.rs index 851d5e9..424c83f 100644 --- a/packages/ducrs/src/serialize.rs +++ b/packages/ducrs/src/serialize.rs @@ -3743,25 +3743,6 @@ fn serialize_checkpoint<'bldr>( ) } -fn serialize_json_patch_operation<'bldr>( - builder: &mut FlatBufferBuilder<'bldr>, - op: &types::JSONPatchOperation, -) -> WIPOffset> { - let op_offset = builder.create_string(&op.op); - let path_offset = builder.create_string(&op.path); - // 'from' is optional in JSON Patch; convert Option -> Option<&str> then map to FB string - let from_offset = op.from.as_deref().map(|s| builder.create_string(s)); - let value_offset = op.value.as_ref().map(|s| builder.create_string(s)); - fb::JSONPatchOperation::create( - builder, - &fb::JSONPatchOperationArgs { - op: Some(op_offset), - path: Some(path_offset), - from: from_offset, - value: value_offset, - }, - ) -} fn serialize_delta<'bldr>( builder: &mut FlatBufferBuilder<'bldr>, diff --git a/turbo.json b/turbo.json index 88b560c..ab7051b 100644 --- a/turbo.json +++ b/turbo.json @@ -15,7 +15,10 @@ }, "ducpdf#build": { "cache": false, - "dependsOn": ["ducjs#build"], + "dependsOn": [ + "ducjs#build", + "ducrs#build" + ], "inputs": ["$TURBO_DEFAULT$", ".env.*", ".env.local"] }, "ducpdf#test": { @@ -29,7 +32,6 @@ "ducsvg#build": { "cache": false, "dependsOn": [ - "ducjs#build", "ducpdf#build" ], "inputs": ["$TURBO_DEFAULT$", ".env.*", ".env.local"], From a14deffbb7660f2eb1deb109b5de4ef8ad17c235 Mon Sep 17 00:00:00 2001 From: Jorge Soares Date: Wed, 28 Jan 2026 21:09:02 +0000 Subject: [PATCH 2/4] feat(ducpdf): add block duplication array support and logging --- Cargo.lock | 12 ++ assets/testing/duc-files/multiple_blocks.duc | Bin 0 -> 35368 bytes packages/ducpdf/src/duc2pdf/Cargo.toml | 2 + packages/ducpdf/src/duc2pdf/src/builder.rs | 63 +++++++- packages/ducpdf/src/duc2pdf/src/lib.rs | 7 + packages/ducpdf/src/duc2pdf/src/scaling.rs | 8 + .../duc2pdf/src/streaming/stream_elements.rs | 148 ++++++++++++++++-- packages/ducpdf/tests/crop.test.ts | 14 ++ packages/ducrs/src/parse.rs | 18 ++- packages/ducrs/src/serialize.rs | 4 +- packages/ducrs/src/types.rs | 4 +- 11 files changed, 248 insertions(+), 32 deletions(-) create mode 100644 assets/testing/duc-files/multiple_blocks.duc diff --git a/Cargo.lock b/Cargo.lock index b931c9f..17cbad4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,6 +206,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "web-sys", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -320,8 +330,10 @@ version = "0.0.0-development" dependencies = [ "bigcolor", "chrono", + "console_log", "duc", "hipdf", + "log", "serde", "serde_json", "svg2pdf", diff --git a/assets/testing/duc-files/multiple_blocks.duc b/assets/testing/duc-files/multiple_blocks.duc new file mode 100644 index 0000000000000000000000000000000000000000..33b5b1bc4da4a5a2e3f0f73ba5162b7ee94e974c GIT binary patch literal 35368 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 = context.exported_data + .block_instances + .iter() + .map(|bi| (bi.id.clone(), bi.clone())) + .collect(); + Ok(Self { context, document, @@ -193,6 +200,7 @@ impl DucToPdfBuilder { 0.0, font_resource_name, primary_font, + block_instances, ), // Default height, will be updated per page resource_streamer: ResourceStreamer::new(), page_ids: Vec::new(), @@ -1496,6 +1504,15 @@ fn create_content_stream( self.element_streamer .set_svg_dimensions(self.context.resource_cache.svg_dimensions.clone()); + let is_plot_mode = matches!(self.context.options.mode, ConversionMode::Plot); + let (scroll_x, scroll_y) = if is_plot_mode { + (0.0, 0.0) + } else if let Some(state) = &self.context.exported_data.duc_local_state { + (state.scroll_x, state.scroll_y) + } else { + (0.0, 0.0) + }; + // === Phase 1: Stream unlayered elements === // These elements don't belong to any layer and render first let unlayered_elements: Vec = self @@ -1550,17 +1567,49 @@ fn create_content_stream( return false; } - // Verify element intersects with page bounds + // Verify element (including duplications) intersects with page bounds let (bounds_x, bounds_y, bounds_width, bounds_height) = bounds; let bounds_max_x = bounds_x + bounds_width; let bounds_max_y = bounds_y + bounds_height; - let elem_max_x = base.x + base.width; - let elem_max_y = base.y + base.height; - let intersects = !(base.x > bounds_max_x - || elem_max_x < bounds_x - || base.y > bounds_max_y - || elem_max_y < bounds_y); + let offsets = self + .element_streamer + .get_element_duplication_offsets(&element_wrapper.element) + .unwrap_or_else(|| vec![(0.0, 0.0)]); + + let mut intersects = false; + for (x_off, y_off) in offsets { + let elem_x = base.x + scroll_x + x_off; + let elem_y = base.y + scroll_y + y_off; + let elem_max_x = elem_x + base.width; + let elem_max_y = elem_y + base.height; + + let this_intersects = !(elem_x > bounds_max_x + || elem_max_x < bounds_x + || elem_y > bounds_max_y + || elem_max_y < bounds_y); + + if this_intersects { + intersects = true; + break; + } + } + + if !intersects { + let base_id = &base.id; + let instance_id = base.instance_id.as_deref().unwrap_or(""); + log_info!( + "Skipping layered element '{}' (instance {}) - no duplicated instance intersects bounds. bounds=({},{},{},{}) scroll=({}, {})", + base_id, + instance_id, + bounds_x, + bounds_y, + bounds_width, + bounds_height, + scroll_x, + scroll_y + ); + } intersects }) diff --git a/packages/ducpdf/src/duc2pdf/src/lib.rs b/packages/ducpdf/src/duc2pdf/src/lib.rs index ca5f288..25d7cb3 100644 --- a/packages/ducpdf/src/duc2pdf/src/lib.rs +++ b/packages/ducpdf/src/duc2pdf/src/lib.rs @@ -1,6 +1,13 @@ use wasm_bindgen::prelude::*; pub mod builder; + +// Initialize logger for WASM +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen(start)] +pub fn init_logger() { + console_log::init_with_level(log::Level::Info).expect("Failed to initialize logger"); +} pub mod scaling; pub mod streaming; pub mod utils; diff --git a/packages/ducpdf/src/duc2pdf/src/scaling.rs b/packages/ducpdf/src/duc2pdf/src/scaling.rs index b10fdfb..76d8138 100644 --- a/packages/ducpdf/src/duc2pdf/src/scaling.rs +++ b/packages/ducpdf/src/duc2pdf/src/scaling.rs @@ -16,6 +16,14 @@ impl DucDataScaler { Self::scale_element(&mut element_wrapper.element, scale); } + // Scale block instance duplication arrays + for block_instance in &mut exported_data.block_instances { + if let Some(ref mut duplication) = block_instance.duplication_array { + duplication.row_spacing *= scale; + duplication.col_spacing *= scale; + } + } + // Scale local state scroll values if let Some(ref mut local_state) = exported_data.duc_local_state { local_state.scroll_x *= scale; diff --git a/packages/ducpdf/src/duc2pdf/src/streaming/stream_elements.rs b/packages/ducpdf/src/duc2pdf/src/streaming/stream_elements.rs index dc563f0..eabe3d5 100644 --- a/packages/ducpdf/src/duc2pdf/src/streaming/stream_elements.rs +++ b/packages/ducpdf/src/duc2pdf/src/streaming/stream_elements.rs @@ -36,7 +36,7 @@ use duc::types::{ DucFreeDrawElement, DucImageElement, DucLine, DucLineReference, DucLinearElement, DucLinearElementBase, DucMermaidElement, DucPath, DucPdfElement, DucPlotElement, DucPoint, DucPolygonElement, DucRectangleElement, DucTableElement, DucTextElement, ElementBackground, - ElementContentBase, ElementWrapper, GeometricPoint, + ElementContentBase, ElementWrapper, GeometricPoint, DucBlockInstance, DucBlockDuplicationArray, }; use hipdf::blocks::BlockManager; use hipdf::embed_pdf::PdfEmbedder; @@ -96,6 +96,8 @@ pub struct ElementStreamer { font_resource_name: String, /// Active font used for text rendering and encoding text_font: Font, + /// Map of block instances for looking up duplication arrays + block_instances: HashMap, /// Whether we should require elements to be marked as "plot" to be rendered render_only_plot_elements: bool, /// Cached ExtGState names keyed by stroke/fill opacity thousandths @@ -119,6 +121,7 @@ impl ElementStreamer { page_height: f64, font_resource_name: String, text_font: Font, + block_instances: HashMap, ) -> Self { Self { style_resolver, @@ -133,6 +136,7 @@ impl ElementStreamer { svg_dimensions: HashMap::new(), font_resource_name, text_font, + block_instances, render_only_plot_elements: false, ext_gstate_cache: HashMap::new(), ext_gstate_definitions: HashMap::new(), @@ -143,6 +147,92 @@ impl ElementStreamer { } } + /// Calculate duplication offsets for block instance grid rendering + /// Returns a vector of (x_offset, y_offset) tuples for each grid position + /// The first offset is always (0.0, 0.0) representing the original position + pub fn get_duplication_offsets( + duplication_array: &DucBlockDuplicationArray, + element_width: f64, + element_height: f64, + ) -> Vec<(f64, f64)> { + if duplication_array.row_spacing.is_nan() || duplication_array.col_spacing.is_nan() { + #[cfg(feature = "verbose_logs")] + log::warn!( + "Duplication array has NaN spacing! row_spacing: {}, col_spacing: {}", + duplication_array.row_spacing, + duplication_array.col_spacing + ); + } + + let rows = duplication_array.rows.max(1) as usize; + let cols = duplication_array.cols.max(1) as usize; + let row_spacing = duplication_array.row_spacing; + let col_spacing = duplication_array.col_spacing; + + let stride_x = element_width + col_spacing; + let stride_y = element_height + row_spacing; + + let mut offsets = Vec::with_capacity(rows * cols); + for row in 0..rows { + for col in 0..cols { + let x_offset = col as f64 * stride_x; + let y_offset = row as f64 * stride_y; + offsets.push((x_offset, y_offset)); + } + } + offsets + } + + /// Get duplication offsets for an element by looking up its block instance + /// Returns None if element has no instance_id or no duplication array + pub fn get_element_duplication_offsets( + &self, + element: &DucElementEnum, + ) -> Option> { + let base = Self::get_element_base(element); + let instance_id = base.instance_id.as_ref()?; + + // Look up the block instance from self.block_instances + if let Some(block_instance) = self.block_instances.get(instance_id) { + if let Some(dup_array) = &block_instance.duplication_array { + // Only return offsets if there's more than one copy to render + if dup_array.rows > 1 || dup_array.cols > 1 { + + // Attempt to extract dimensions from the element + // This allows "gap" spacing: offset = index * (size + spacing) + let (width, height) = match element { + DucElementEnum::DucRectangleElement(r) => (r.base.width, r.base.height), + DucElementEnum::DucEllipseElement(e) => (e.base.width, e.base.height), + DucElementEnum::DucImageElement(i) => (i.base.width, i.base.height), + DucElementEnum::DucFrameElement(f) => (f.stack_element_base.base.width, f.stack_element_base.base.height), + DucElementEnum::DucPlotElement(p) => (p.stack_element_base.base.width, p.stack_element_base.base.height), + DucElementEnum::DucTableElement(t) => (t.base.width, t.base.height), + DucElementEnum::DucDocElement(d) => (d.base.width, d.base.height), + DucElementEnum::DucEmbeddableElement(e) => (e.base.width, e.base.height), + DucElementEnum::DucPolygonElement(p) => (p.base.width, p.base.height), + DucElementEnum::DucTextElement(t) => (t.base.width, t.base.height), + DucElementEnum::DucFreeDrawElement(f) => (f.base.width, f.base.height), + DucElementEnum::DucLinearElement(l) => (l.linear_base.base.width, l.linear_base.base.height), + DucElementEnum::DucArrowElement(a) => (a.linear_base.base.width, a.linear_base.base.height), + DucElementEnum::DucViewportElement(v) => (v.linear_base.base.width, v.linear_base.base.height), + DucElementEnum::DucXRayElement(x) => (x.base.width, x.base.height), + DucElementEnum::DucLeaderElement(l) => (l.linear_base.base.width, l.linear_base.base.height), + DucElementEnum::DucDimensionElement(d) => (d.base.width, d.base.height), + DucElementEnum::DucFeatureControlFrameElement(f) => (f.base.width, f.base.height), + DucElementEnum::DucParametricElement(p) => (p.base.width, p.base.height), + DucElementEnum::DucPdfElement(p) => (p.base.width, p.base.height), + DucElementEnum::DucMermaidElement(m) => (m.base.width, m.base.height), + }; + + return Some(Self::get_duplication_offsets(dup_array, width, height)); + } + } + } else { + log::info!("Element refers to instance {} which is missing from block_instances!", instance_id); + } + None + } + /// Update the active font used for text rendering pub fn set_text_font(&mut self, font_resource_name: String, font: Font) { self.font_resource_name = font_resource_name; @@ -323,20 +413,29 @@ impl ElementStreamer { clip_applied = clip_active; } - // Stream the element - let element_ops = self.stream_element_with_resources( - &element_wrapper.element, - local_state, - all_elements, - document, - resource_streamer, - block_manager, - hatching_manager, - pdf_embedder, - image_manager, - )?; - - all_operations.extend(element_ops); + // Get duplication offsets (default to single (0,0) if none) + let offsets = self + .get_element_duplication_offsets(&element_wrapper.element) + .unwrap_or_else(|| vec![(0.0, 0.0)]); + + for (x_off, y_off) in offsets { + // Stream the element with duplication offset applied AFTER its own transform + // so the offset is not rotated or scaled by the element transform. + let element_ops = self.stream_element_with_resources( + &element_wrapper.element, + local_state, + all_elements, + document, + resource_streamer, + block_manager, + hatching_manager, + pdf_embedder, + image_manager, + Some((x_off, y_off)), + )?; + + all_operations.extend(element_ops); + } // Restore graphics state if clipping was applied if clip_applied { @@ -363,6 +462,7 @@ impl ElementStreamer { hatching_manager: &mut HatchingManager, pdf_embedder: &mut PdfEmbedder, image_manager: &mut ImageManager, + duplication_offset: Option<(f64, f64)>, ) -> ConversionResult> { let mut operations = Vec::new(); let is_plot_mode = matches!(self.current_mode, StreamMode::Plot); @@ -414,6 +514,24 @@ impl ElementStreamer { operations.extend(transform_ops); } + // Apply duplication offset AFTER the element's own transform so it is not rotated + // or scaled by the element transform. PDF Y axis is inverted, so negate Y. + if let Some((x_off, y_off)) = duplication_offset { + if x_off != 0.0 || y_off != 0.0 { + operations.push(Operation::new( + "cm", + vec![ + Object::Real(1.0), + Object::Real(0.0), + Object::Real(0.0), + Object::Real(1.0), + Object::Real(x_off as f32), + Object::Real((-y_off) as f32), + ], + )); + } + } + // Special handling: PDF elements - do not apply styles to avoid affecting embedded content let styles = self.style_resolver.resolve_styles(element, None); diff --git a/packages/ducpdf/tests/crop.test.ts b/packages/ducpdf/tests/crop.test.ts index 5cf1faf..4a70a83 100644 --- a/packages/ducpdf/tests/crop.test.ts +++ b/packages/ducpdf/tests/crop.test.ts @@ -26,6 +26,20 @@ const cropOpts = ( }; describe('CROP mode conversions', () => { + it('multiple blocks crops', async () => { + const duc = loadDucFile('multiple_blocks.duc'); + const configs: Array<[string, number, number, number?, number?, number?]> = [ + ['dimensions_area', 6500, 2500, 6000, 1400, 0.2], + ]; + + for (const [name, ox, oy, w, h, z] of configs) { + const pdf = await convertDucToPdf(duc, cropOpts(ox, oy, w, h, z)); + validatePdf(pdf); + savePdfOutput(`${OUTPUT_DIR}/multiple_blocks_${name}.pdf`, pdf); + expect(pdf.length).toBeGreaterThan(100); + } + }); + it('universal several crops', async () => { const duc = loadDucFile('universal.duc'); const configs: Array<[string, number, number, number?, number?, number?]> = [ diff --git a/packages/ducrs/src/parse.rs b/packages/ducrs/src/parse.rs index 866a436..f320424 100644 --- a/packages/ducrs/src/parse.rs +++ b/packages/ducrs/src/parse.rs @@ -53,20 +53,22 @@ 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 + if let Ok(text) = String::from_utf8(decompressed) { + return Some(text); + } } + + // Fallback to legacy format: plain JSON string (for old file compatibility) + String::from_utf8(data).ok() } _ => None, } @@ -1304,8 +1306,12 @@ fn parse_duc_block_attribute_definition_entry(entry: fb::DucBlockAttributeDefini } fn parse_duc_block_metadata(metadata: fb::DucBlockMetadata) -> ParseResult { + let source = metadata.source() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); + Ok(types::DucBlockMetadata { - source: metadata.source().ok_or("Missing DucBlockMetadata.source")?.to_string(), + source, usage_count: metadata.usage_count(), created_at: metadata.created_at(), updated_at: metadata.updated_at(), diff --git a/packages/ducrs/src/serialize.rs b/packages/ducrs/src/serialize.rs index 424c83f..221f58d 100644 --- a/packages/ducrs/src/serialize.rs +++ b/packages/ducrs/src/serialize.rs @@ -2515,7 +2515,7 @@ pub fn serialize_duc_block_metadata<'bldr>( builder: &mut flatbuffers::FlatBufferBuilder<'bldr>, metadata: &types::DucBlockMetadata, ) -> WIPOffset> { - let source_offset = builder.create_string(&metadata.source); + let source_offset = metadata.source.as_ref().map(|s| builder.create_string(s)); // Compress localization JSON and create byte vector let localization_offset = metadata @@ -2529,7 +2529,7 @@ pub fn serialize_duc_block_metadata<'bldr>( fb::DucBlockMetadata::create( builder, &fb::DucBlockMetadataArgs { - source: Some(source_offset), + source: source_offset, usage_count: metadata.usage_count, created_at: metadata.created_at, updated_at: metadata.updated_at, diff --git a/packages/ducrs/src/types.rs b/packages/ducrs/src/types.rs index c63eae6..aa03c0e 100644 --- a/packages/ducrs/src/types.rs +++ b/packages/ducrs/src/types.rs @@ -1036,7 +1036,7 @@ pub struct DucBlockAttributeDefinitionEntry { #[derive(Debug, Clone, PartialEq)] pub struct DucBlockMetadata { - pub source: String, + pub source: Option, pub usage_count: i32, pub created_at: i64, pub updated_at: i64, @@ -1537,7 +1537,7 @@ pub struct JSONPatchOperation { pub op: String, pub path: String, pub from: Option, - pub value: Option, + pub value: Option, } #[derive(Debug, Clone, PartialEq)] From eeaf847061ccf48536b33b356e49434643f4b435 Mon Sep 17 00:00:00 2001 From: Jorge Soares Date: Sat, 7 Feb 2026 20:10:39 +0000 Subject: [PATCH 3/4] feat: add grid configuration to PDF and Doc elements, renamed Parametric element to Model element --- .documentation/SchemaUpdates.md | 2 +- packages/ducjs/src/flatbuffers/duc.ts | 3 + .../duc/document-grid-align-items.ts | 9 + .../flatbuffers/duc/document-grid-config.ts | 92 ++++ .../src/flatbuffers/duc/duc-doc-element.ts | 23 +- .../src/flatbuffers/duc/duc-model-element.ts | 104 ++++ .../src/flatbuffers/duc/duc-pdf-element.ts | 18 +- packages/ducjs/src/flatbuffers/duc/element.ts | 14 +- packages/ducjs/src/parse.ts | 62 ++- packages/ducjs/src/restore/restoreElements.ts | 61 ++- packages/ducjs/src/serialize.ts | 71 ++- packages/ducjs/src/types/elements/index.ts | 70 +-- .../ducjs/src/types/elements/typeChecks.ts | 2 +- .../ducjs/src/utils/elements/newElement.ts | 15 +- .../ducpy/Duc/DOCUMENT_GRID_ALIGN_ITEMS.py | 8 + .../ducpy/src/ducpy/Duc/DocumentGridConfig.py | 106 ++++ packages/ducpy/src/ducpy/Duc/DucDocElement.py | 32 +- .../ducpy/src/ducpy/Duc/DucModelElement.py | 116 +++++ packages/ducpy/src/ducpy/Duc/DucPdfElement.py | 19 +- packages/ducpy/src/ducpy/Duc/Element.py | 1 + .../ducpy/src/ducpy/classes/ElementsClass.py | 23 +- packages/ducpy/src/ducpy/parse.py | 54 ++ packages/ducpy/src/ducpy/serialize.py | 53 +- .../ducrs/src/flatbuffers/duc_generated.rs | 486 +++++++++++++++++- packages/ducrs/src/parse.rs | 51 ++ packages/ducrs/src/serialize.rs | 50 ++ packages/ducrs/src/types.rs | 58 ++- schema/duc.fbs | 66 ++- 28 files changed, 1524 insertions(+), 145 deletions(-) create mode 100644 packages/ducjs/src/flatbuffers/duc/document-grid-align-items.ts create mode 100644 packages/ducjs/src/flatbuffers/duc/document-grid-config.ts create mode 100644 packages/ducjs/src/flatbuffers/duc/duc-model-element.ts create mode 100644 packages/ducpy/src/ducpy/Duc/DOCUMENT_GRID_ALIGN_ITEMS.py create mode 100644 packages/ducpy/src/ducpy/Duc/DocumentGridConfig.py create mode 100644 packages/ducpy/src/ducpy/Duc/DucModelElement.py diff --git a/.documentation/SchemaUpdates.md b/.documentation/SchemaUpdates.md index 29e443c..b4e4e71 100644 --- a/.documentation/SchemaUpdates.md +++ b/.documentation/SchemaUpdates.md @@ -18,4 +18,4 @@ on the python side: And then run the build (or test commands if available) for each from @duc/package.json -and in case you need to check the fbs schema: @duc/schema/duc.fbs \ No newline at end of file +and in case you need to check the fbs schema or what changed (changes may be git staged): @duc/schema/duc.fbs \ No newline at end of file diff --git a/packages/ducjs/src/flatbuffers/duc.ts b/packages/ducjs/src/flatbuffers/duc.ts index 4a35c10..29c2903 100644 --- a/packages/ducjs/src/flatbuffers/duc.ts +++ b/packages/ducjs/src/flatbuffers/duc.ts @@ -22,6 +22,7 @@ export { DIMENSION_FIT_RULE } from './duc/dimension-fit-rule'; export { DIMENSION_TEXT_PLACEMENT } from './duc/dimension-text-placement'; export { DIMENSION_TYPE } from './duc/dimension-type'; export { DIMENSION_UNITS_FORMAT } from './duc/dimension-units-format'; +export { DOCUMENT_GRID_ALIGN_ITEMS } from './duc/document-grid-align-items'; export { DatumReference } from './duc/datum-reference'; export { Delta } from './duc/delta'; export { DictionaryEntry } from './duc/dictionary-entry'; @@ -35,6 +36,7 @@ export { DimensionLineStyle } from './duc/dimension-line-style'; export { DimensionSymbolStyle } from './duc/dimension-symbol-style'; export { DimensionToleranceStyle } from './duc/dimension-tolerance-style'; export { DimensionValidationRules } from './duc/dimension-validation-rules'; +export { DocumentGridConfig } from './duc/document-grid-config'; export { DucArrowElement } from './duc/duc-arrow-element'; export { DucBlock } from './duc/duc-block'; export { DucBlockAttributeDefinition } from './duc/duc-block-attribute-definition'; @@ -74,6 +76,7 @@ export { DucLineReference } from './duc/duc-line-reference'; export { DucLinearElement } from './duc/duc-linear-element'; export { DucLocalState } from './duc/duc-local-state'; export { DucMermaidElement } from './duc/duc-mermaid-element'; +export { DucModelElement } from './duc/duc-model-element'; export { DucParametricElement } from './duc/duc-parametric-element'; export { DucPath } from './duc/duc-path'; export { DucPdfElement } from './duc/duc-pdf-element'; diff --git a/packages/ducjs/src/flatbuffers/duc/document-grid-align-items.ts b/packages/ducjs/src/flatbuffers/duc/document-grid-align-items.ts new file mode 100644 index 0000000..ee99b87 --- /dev/null +++ b/packages/ducjs/src/flatbuffers/duc/document-grid-align-items.ts @@ -0,0 +1,9 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum DOCUMENT_GRID_ALIGN_ITEMS { + START = 10, + CENTER = 11, + END = 12 +} diff --git a/packages/ducjs/src/flatbuffers/duc/document-grid-config.ts b/packages/ducjs/src/flatbuffers/duc/document-grid-config.ts new file mode 100644 index 0000000..819f089 --- /dev/null +++ b/packages/ducjs/src/flatbuffers/duc/document-grid-config.ts @@ -0,0 +1,92 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { DOCUMENT_GRID_ALIGN_ITEMS } from '../duc/document-grid-align-items'; + + +export class DocumentGridConfig { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):DocumentGridConfig { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsDocumentGridConfig(bb:flatbuffers.ByteBuffer, obj?:DocumentGridConfig):DocumentGridConfig { + return (obj || new DocumentGridConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsDocumentGridConfig(bb:flatbuffers.ByteBuffer, obj?:DocumentGridConfig):DocumentGridConfig { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new DocumentGridConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +columns():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0; +} + +gapX():number { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0; +} + +gapY():number { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0; +} + +alignItems():DOCUMENT_GRID_ALIGN_ITEMS|null { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : null; +} + +firstPageAlone():boolean { + const offset = this.bb!.__offset(this.bb_pos, 12); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; +} + +static startDocumentGridConfig(builder:flatbuffers.Builder) { + builder.startObject(5); +} + +static addColumns(builder:flatbuffers.Builder, columns:number) { + builder.addFieldInt32(0, columns, 0); +} + +static addGapX(builder:flatbuffers.Builder, gapX:number) { + builder.addFieldFloat64(1, gapX, 0.0); +} + +static addGapY(builder:flatbuffers.Builder, gapY:number) { + builder.addFieldFloat64(2, gapY, 0.0); +} + +static addAlignItems(builder:flatbuffers.Builder, alignItems:DOCUMENT_GRID_ALIGN_ITEMS) { + builder.addFieldInt8(3, alignItems, null); +} + +static addFirstPageAlone(builder:flatbuffers.Builder, firstPageAlone:boolean) { + builder.addFieldInt8(4, +firstPageAlone, +false); +} + +static endDocumentGridConfig(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createDocumentGridConfig(builder:flatbuffers.Builder, columns:number, gapX:number, gapY:number, alignItems:DOCUMENT_GRID_ALIGN_ITEMS|null, firstPageAlone:boolean):flatbuffers.Offset { + DocumentGridConfig.startDocumentGridConfig(builder); + DocumentGridConfig.addColumns(builder, columns); + DocumentGridConfig.addGapX(builder, gapX); + DocumentGridConfig.addGapY(builder, gapY); + if (alignItems !== null) + DocumentGridConfig.addAlignItems(builder, alignItems); + DocumentGridConfig.addFirstPageAlone(builder, firstPageAlone); + return DocumentGridConfig.endDocumentGridConfig(builder); +} +} diff --git a/packages/ducjs/src/flatbuffers/duc/duc-doc-element.ts b/packages/ducjs/src/flatbuffers/duc/duc-doc-element.ts index 79fdbbf..17176af 100644 --- a/packages/ducjs/src/flatbuffers/duc/duc-doc-element.ts +++ b/packages/ducjs/src/flatbuffers/duc/duc-doc-element.ts @@ -5,6 +5,7 @@ import * as flatbuffers from 'flatbuffers'; import { ColumnLayout } from '../duc/column-layout'; +import { DocumentGridConfig } from '../duc/document-grid-config'; import { DucDocStyle } from '../duc/duc-doc-style'; import { DucTextDynamicPart } from '../duc/duc-text-dynamic-part'; import { TEXT_FLOW_DIRECTION } from '../duc/text-flow-direction'; @@ -71,8 +72,20 @@ autoResize():boolean { return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; } +gridConfig(obj?:DocumentGridConfig):DocumentGridConfig|null { + const offset = this.bb!.__offset(this.bb_pos, 18); + return offset ? (obj || new DocumentGridConfig()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +fileId():string|null +fileId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +fileId(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 20); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + static startDucDocElement(builder:flatbuffers.Builder) { - builder.startObject(7); + builder.startObject(9); } static addBase(builder:flatbuffers.Builder, baseOffset:flatbuffers.Offset) { @@ -115,6 +128,14 @@ static addAutoResize(builder:flatbuffers.Builder, autoResize:boolean) { builder.addFieldInt8(6, +autoResize, +false); } +static addGridConfig(builder:flatbuffers.Builder, gridConfigOffset:flatbuffers.Offset) { + builder.addFieldOffset(7, gridConfigOffset, 0); +} + +static addFileId(builder:flatbuffers.Builder, fileIdOffset:flatbuffers.Offset) { + builder.addFieldOffset(8, fileIdOffset, 0); +} + static endDucDocElement(builder:flatbuffers.Builder):flatbuffers.Offset { const offset = builder.endObject(); return offset; diff --git a/packages/ducjs/src/flatbuffers/duc/duc-model-element.ts b/packages/ducjs/src/flatbuffers/duc/duc-model-element.ts new file mode 100644 index 0000000..debfc01 --- /dev/null +++ b/packages/ducjs/src/flatbuffers/duc/duc-model-element.ts @@ -0,0 +1,104 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { _DucElementBase } from '../duc/duc-element-base'; + + +export class DucModelElement { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):DucModelElement { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsDucModelElement(bb:flatbuffers.ByteBuffer, obj?:DucModelElement):DucModelElement { + return (obj || new DucModelElement()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsDucModelElement(bb:flatbuffers.ByteBuffer, obj?:DucModelElement):DucModelElement { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new DucModelElement()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +base(obj?:_DucElementBase):_DucElementBase|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? (obj || new _DucElementBase()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +source():string|null +source(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +source(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +svgPath():string|null +svgPath(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +svgPath(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +fileIds(index: number):string +fileIds(index: number,optionalEncoding:flatbuffers.Encoding):string|Uint8Array +fileIds(index: number,optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.__string(this.bb!.__vector(this.bb_pos + offset) + index * 4, optionalEncoding) : null; +} + +fileIdsLength():number { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; +} + +static startDucModelElement(builder:flatbuffers.Builder) { + builder.startObject(4); +} + +static addBase(builder:flatbuffers.Builder, baseOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, baseOffset, 0); +} + +static addSource(builder:flatbuffers.Builder, sourceOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, sourceOffset, 0); +} + +static addSvgPath(builder:flatbuffers.Builder, svgPathOffset:flatbuffers.Offset) { + builder.addFieldOffset(2, svgPathOffset, 0); +} + +static addFileIds(builder:flatbuffers.Builder, fileIdsOffset:flatbuffers.Offset) { + builder.addFieldOffset(3, fileIdsOffset, 0); +} + +static createFileIdsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset { + builder.startVector(4, data.length, 4); + for (let i = data.length - 1; i >= 0; i--) { + builder.addOffset(data[i]!); + } + return builder.endVector(); +} + +static startFileIdsVector(builder:flatbuffers.Builder, numElems:number) { + builder.startVector(4, numElems, 4); +} + +static endDucModelElement(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createDucModelElement(builder:flatbuffers.Builder, baseOffset:flatbuffers.Offset, sourceOffset:flatbuffers.Offset, svgPathOffset:flatbuffers.Offset, fileIdsOffset:flatbuffers.Offset):flatbuffers.Offset { + DucModelElement.startDucModelElement(builder); + DucModelElement.addBase(builder, baseOffset); + DucModelElement.addSource(builder, sourceOffset); + DucModelElement.addSvgPath(builder, svgPathOffset); + DucModelElement.addFileIds(builder, fileIdsOffset); + return DucModelElement.endDucModelElement(builder); +} +} diff --git a/packages/ducjs/src/flatbuffers/duc/duc-pdf-element.ts b/packages/ducjs/src/flatbuffers/duc/duc-pdf-element.ts index fd38c89..8c4279c 100644 --- a/packages/ducjs/src/flatbuffers/duc/duc-pdf-element.ts +++ b/packages/ducjs/src/flatbuffers/duc/duc-pdf-element.ts @@ -4,6 +4,7 @@ import * as flatbuffers from 'flatbuffers'; +import { DocumentGridConfig } from '../duc/document-grid-config'; import { _DucElementBase } from '../duc/duc-element-base'; @@ -37,8 +38,13 @@ fileId(optionalEncoding?:any):string|Uint8Array|null { return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; } +gridConfig(obj?:DocumentGridConfig):DocumentGridConfig|null { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? (obj || new DocumentGridConfig()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + static startDucPdfElement(builder:flatbuffers.Builder) { - builder.startObject(2); + builder.startObject(3); } static addBase(builder:flatbuffers.Builder, baseOffset:flatbuffers.Offset) { @@ -49,15 +55,13 @@ static addFileId(builder:flatbuffers.Builder, fileIdOffset:flatbuffers.Offset) { builder.addFieldOffset(1, fileIdOffset, 0); } +static addGridConfig(builder:flatbuffers.Builder, gridConfigOffset:flatbuffers.Offset) { + builder.addFieldOffset(2, gridConfigOffset, 0); +} + static endDucPdfElement(builder:flatbuffers.Builder):flatbuffers.Offset { const offset = builder.endObject(); return offset; } -static createDucPdfElement(builder:flatbuffers.Builder, baseOffset:flatbuffers.Offset, fileIdOffset:flatbuffers.Offset):flatbuffers.Offset { - DucPdfElement.startDucPdfElement(builder); - DucPdfElement.addBase(builder, baseOffset); - DucPdfElement.addFileId(builder, fileIdOffset); - return DucPdfElement.endDucPdfElement(builder); -} } diff --git a/packages/ducjs/src/flatbuffers/duc/element.ts b/packages/ducjs/src/flatbuffers/duc/element.ts index 81bf57b..ccf581f 100644 --- a/packages/ducjs/src/flatbuffers/duc/element.ts +++ b/packages/ducjs/src/flatbuffers/duc/element.ts @@ -15,6 +15,7 @@ import { DucImageElement } from '../duc/duc-image-element'; import { DucLeaderElement } from '../duc/duc-leader-element'; import { DucLinearElement } from '../duc/duc-linear-element'; import { DucMermaidElement } from '../duc/duc-mermaid-element'; +import { DucModelElement } from '../duc/duc-model-element'; import { DucParametricElement } from '../duc/duc-parametric-element'; import { DucPdfElement } from '../duc/duc-pdf-element'; import { DucPlotElement } from '../duc/duc-plot-element'; @@ -49,13 +50,14 @@ export enum Element { DucDimensionElement = 19, DucFeatureControlFrameElement = 20, DucDocElement = 21, - DucParametricElement = 22 + DucParametricElement = 22, + DucModelElement = 23 } export function unionToElement( type: Element, - accessor: (obj:DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement) => DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement|null -): DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement|null { + accessor: (obj:DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucModelElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement) => DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucModelElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement|null +): DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucModelElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement|null { switch(Element[type]) { case 'NONE': return null; case 'DucRectangleElement': return accessor(new DucRectangleElement())! as DucRectangleElement; @@ -80,15 +82,16 @@ export function unionToElement( case 'DucFeatureControlFrameElement': return accessor(new DucFeatureControlFrameElement())! as DucFeatureControlFrameElement; case 'DucDocElement': return accessor(new DucDocElement())! as DucDocElement; case 'DucParametricElement': return accessor(new DucParametricElement())! as DucParametricElement; + case 'DucModelElement': return accessor(new DucModelElement())! as DucModelElement; default: return null; } } export function unionListToElement( type: Element, - accessor: (index: number, obj:DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement) => DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement|null, + accessor: (index: number, obj:DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucModelElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement) => DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucModelElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement|null, index: number -): DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement|null { +): DucArrowElement|DucBlockInstanceElement|DucDimensionElement|DucDocElement|DucEllipseElement|DucEmbeddableElement|DucFeatureControlFrameElement|DucFrameElement|DucFreeDrawElement|DucImageElement|DucLeaderElement|DucLinearElement|DucMermaidElement|DucModelElement|DucParametricElement|DucPdfElement|DucPlotElement|DucPolygonElement|DucRectangleElement|DucTableElement|DucTextElement|DucViewportElement|DucXRayElement|null { switch(Element[type]) { case 'NONE': return null; case 'DucRectangleElement': return accessor(index, new DucRectangleElement())! as DucRectangleElement; @@ -113,6 +116,7 @@ export function unionListToElement( case 'DucFeatureControlFrameElement': return accessor(index, new DucFeatureControlFrameElement())! as DucFeatureControlFrameElement; case 'DucDocElement': return accessor(index, new DucDocElement())! as DucDocElement; case 'DucParametricElement': return accessor(index, new DucParametricElement())! as DucParametricElement; + case 'DucModelElement': return accessor(index, new DucModelElement())! as DucModelElement; default: return null; } } diff --git a/packages/ducjs/src/parse.ts b/packages/ducjs/src/parse.ts index 8b7ca14..33c30bd 100644 --- a/packages/ducjs/src/parse.ts +++ b/packages/ducjs/src/parse.ts @@ -5,6 +5,7 @@ import { nanoid } from 'nanoid'; import { CustomHatchPattern as CustomHatchPatternFb, DimensionToleranceStyle as DimensionToleranceStyleFb, + DOCUMENT_GRID_ALIGN_ITEMS, DucArrowElement as DucArrowElementFb, DucBlockCollection as DucBlockCollectionFb, DucBlock as DucBlockFb, @@ -36,7 +37,7 @@ import { DucLinearElement as DucLinearElementFb, DucLocalState as DucLocalStateFb, DucMermaidElement as DucMermaidElementFb, - DucParametricElement as DucParametricElementFb, + DucModelElement as DucModelElementFb, DucPath as DucPathFb, DucPdfElement as DucPdfElementFb, DucPlotElement as DucPlotElementFb, @@ -60,6 +61,7 @@ import { DucViewportStyle as DucViewportStyleFb, DucXRayElement as DucXRayElementFb, DucXRayStyle as DucXRayStyleFb, + DocumentGridConfig as DocumentGridConfigFb, ElementBackground as ElementBackgroundFb, ElementContentBase as ElementContentBaseFb, ElementStroke as ElementStrokeFb, @@ -101,6 +103,7 @@ import { CustomHatchPattern, DatumReference, Dictionary, + DocumentGridConfig, DucArrowElement, DucBlock, DucBlockAttributeDefinition, @@ -134,7 +137,7 @@ import { DucLinearElement, DucLocalState, DucMermaidElement, - DucParametricElement, + DucModelElement, DucPath, DucPdfElement, DucPlotElement, @@ -176,7 +179,6 @@ import { NormalizedZoomValue, ObjectSnapMode, OrderedDucElement, - ParametricElementSource, Percentage, PlotLayout, PrecisionValue, @@ -263,6 +265,22 @@ export function parseMargins(margins: MarginsFb): PlotLayout["margins"] { }; } +export function parseDocumentGridConfig(gridConfig: DocumentGridConfigFb): DocumentGridConfig { + return { + columns: gridConfig.columns(), + gapX: gridConfig.gapX(), + gapY: gridConfig.gapY(), + alignItems: (() => { + const align = gridConfig.alignItems(); + if (align === DOCUMENT_GRID_ALIGN_ITEMS.START) return 'start'; + if (align === DOCUMENT_GRID_ALIGN_ITEMS.CENTER) return 'center'; + if (align === DOCUMENT_GRID_ALIGN_ITEMS.END) return 'end'; + return 'start'; + })(), + firstPageAlone: gridConfig.firstPageAlone(), + }; +} + export function parseHead(head: DucHeadFb): DucHead { return { type: head.type()!, @@ -546,10 +564,18 @@ function parseEmbeddableElement(element: DucEmbeddableElementFb): DucEmbeddableE } function parsePdfElement(element: DucPdfElementFb): DucPdfElement { + const gridConfig = element.gridConfig(); return { type: "pdf", ...parseElementBase(element.base()!), fileId: element.fileId() as ExternalFileId | null, + gridConfig: gridConfig ? parseDocumentGridConfig(gridConfig) : { + columns: 1, + gapX: 0, + gapY: 0, + alignItems: 'start', + firstPageAlone: false, + }, }; } @@ -976,6 +1002,7 @@ function parseDocElement(element: DucDocElementFb): DucDocElement { }); } const columns = element.columns()!; + const gridConfig = element.gridConfig(); return { type: "doc", ...parseElementBase(element.base()!), @@ -995,19 +1022,24 @@ function parseDocElement(element: DucDocElementFb): DucDocElement { autoHeight: columns.autoHeight(), }, autoResize: element.autoResize(), + fileId: element.fileId() as ExternalFileId | null, + gridConfig: gridConfig ? parseDocumentGridConfig(gridConfig) : { + columns: 1, + gapX: 0, + gapY: 0, + alignItems: 'start', + firstPageAlone: false, + }, }; } -function parseParametricElement(element: DucParametricElementFb): DucParametricElement { - const source = element.source()!; +function parseModelElement(element: DucModelElementFb): DucModelElement { return { - type: "parametric", + type: "model", ...parseElementBase(element.base()!), - source: { - type: source.type(), - code: source.code()!, - fileId: source.fileId()!, - } as ParametricElementSource, + source: element.source()!, + svgPath: element.svgPath(), + fileIds: Array.from({ length: element.fileIdsLength() }, (_, i) => element.fileIds(i)!) as ExternalFileId[], }; } // #endregion @@ -1243,8 +1275,8 @@ export function parseElementFromBinary(wrapper: ElementWrapper): DucElement | nu case ElementUnion.DucDocElement: element = wrapper.element(new DucDocElementFb()); break; - case ElementUnion.DucParametricElement: - element = wrapper.element(new DucParametricElementFb()); + case ElementUnion.DucModelElement: + element = wrapper.element(new DucModelElementFb()); break; default: return null; @@ -1296,8 +1328,8 @@ export function parseElementFromBinary(wrapper: ElementWrapper): DucElement | nu return parseFeatureControlFrameElement(element as DucFeatureControlFrameElementFb); case ElementUnion.DucDocElement: return parseDocElement(element as DucDocElementFb); - case ElementUnion.DucParametricElement: - return parseParametricElement(element as DucParametricElementFb); + case ElementUnion.DucModelElement: + return parseModelElement(element as DucModelElementFb); default: return null; } diff --git a/packages/ducjs/src/restore/restoreElements.ts b/packages/ducjs/src/restore/restoreElements.ts index 66ce14b..d2bd115 100644 --- a/packages/ducjs/src/restore/restoreElements.ts +++ b/packages/ducjs/src/restore/restoreElements.ts @@ -37,6 +37,7 @@ import { BezierMirroring, DatumReference, DimensionDefinitionPoints, + DocumentGridConfig, DucBinderElement, DucDimensionElement, DucDimensionStyle, @@ -53,7 +54,7 @@ import { DucLine, DucLinearElement, DucLocalState, - DucParametricElement, + DucModelElement, DucPath, DucPlotElement, DucPoint, @@ -81,7 +82,6 @@ import { Mutable, NonDeleted, OrderedDucElement, - ParametricElementSource, Percentage, PlotLayout, PrecisionValue, @@ -864,7 +864,10 @@ const restoreElement = ( case "pdf": return restoreElementWithProperties( element, - { fileId: (isValidString(element.fileId) as ExternalFileId) || null }, + { + fileId: (isValidString(element.fileId) as ExternalFileId) || null, + gridConfig: restoreDocumentGridConfig((element as any).gridConfig), + }, localState ); case "mermaid": @@ -948,6 +951,8 @@ const restoreElement = ( : TEXT_FLOW_DIRECTION.TOP_TO_BOTTOM, columns: restoreTextColumns(docElement.columns, currentScope), autoResize: isValidBooleanValue(docElement.autoResize, true), + fileId: (isValidString(docElement.fileId) as ExternalFileId) || null, + gridConfig: restoreDocumentGridConfig(docElement.gridConfig), }, localState ); @@ -977,25 +982,15 @@ const restoreElement = ( ); } - case "parametric": { - const parametricElement = element as DucParametricElement; - let source: ParametricElementSource; - - if (parametricElement.source?.type === PARAMETRIC_SOURCE_TYPE.FILE) { - source = { - type: PARAMETRIC_SOURCE_TYPE.FILE, - fileId: isValidString(parametricElement.source?.fileId) as ExternalFileId, - }; - } else { - source = { - type: PARAMETRIC_SOURCE_TYPE.CODE, - code: isValidString(parametricElement.source?.code), - }; - } - + case "model": { + const modelElement = element as DucModelElement; return restoreElementWithProperties( - parametricElement, - { source }, + modelElement, + { + source: isValidString(modelElement.source), + fileIds: modelElement.fileIds || [], + svgPath: modelElement.svgPath || null, + }, localState, globalState ); @@ -1948,6 +1943,30 @@ const restoreTextColumns = (columns: any, currentScope: Scope) => { }; }; +const restoreDocumentGridConfig = ( + gridConfig: any +): DocumentGridConfig => { + if (!gridConfig || typeof gridConfig !== "object") { + return { + columns: 1, + gapX: 0, + gapY: 0, + alignItems: "start", + firstPageAlone: false, + }; + } + + return { + columns: typeof gridConfig.columns === "number" ? gridConfig.columns : 1, + gapX: typeof gridConfig.gapX === "number" ? gridConfig.gapX : 0, + gapY: typeof gridConfig.gapY === "number" ? gridConfig.gapY : 0, + alignItems: (typeof gridConfig.alignItems === "string" && ["start", "center", "end"].includes(gridConfig.alignItems)) + ? gridConfig.alignItems + : "start", + firstPageAlone: typeof gridConfig.firstPageAlone === "boolean" ? gridConfig.firstPageAlone : false, + }; +}; + const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => { if (Object.keys(FONT_FAMILY).includes(fontFamilyName)) { return FONT_FAMILY[ diff --git a/packages/ducjs/src/serialize.ts b/packages/ducjs/src/serialize.ts index f3ed282..b7251f3 100644 --- a/packages/ducjs/src/serialize.ts +++ b/packages/ducjs/src/serialize.ts @@ -23,6 +23,7 @@ import { Delta, Dictionary, DimensionDefinitionPoints, + DocumentGridConfig, DucArrowElement, DucBlock, DucBlockAttributeDefinition, @@ -56,7 +57,7 @@ import { DucLineReference, DucLocalState, DucMermaidElement, - DucParametricElement, + DucModelElement, DucPath, DucPdfElement, DucPlotElement, @@ -96,7 +97,6 @@ import { LayerSnapFilters, LeaderContent, OrderedDucElement, - ParametricElementSource, PlotLayout, PolarGridSettings, PolarTrackingSettings, @@ -813,6 +813,22 @@ function writeColumnLayout(b: flatbuffers.Builder, c: DucDocElement["columns"], return Duc.ColumnLayout.endColumnLayout(b); } +function writeDocumentGridConfig(b: flatbuffers.Builder, config: DocumentGridConfig, usv: boolean): number { + Duc.DocumentGridConfig.startDocumentGridConfig(b); + Duc.DocumentGridConfig.addColumns(b, config.columns); + Duc.DocumentGridConfig.addGapX(b, config.gapX); + Duc.DocumentGridConfig.addGapY(b, config.gapY); + const alignItems = (() => { + if (config.alignItems === 'start') return Duc.DOCUMENT_GRID_ALIGN_ITEMS.START; + if (config.alignItems === 'center') return Duc.DOCUMENT_GRID_ALIGN_ITEMS.CENTER; + if (config.alignItems === 'end') return Duc.DOCUMENT_GRID_ALIGN_ITEMS.END; + return Duc.DOCUMENT_GRID_ALIGN_ITEMS.START; + })(); + Duc.DocumentGridConfig.addAlignItems(b, alignItems); + Duc.DocumentGridConfig.addFirstPageAlone(b, config.firstPageAlone); + return Duc.DocumentGridConfig.endDocumentGridConfig(b); +} + function writeText(b: flatbuffers.Builder, e: DucTextElement, usv: boolean): number { const base = writeElementBase(b, e as unknown as any, usv); const style = writeTextStyle(b, e, usv); @@ -1452,6 +1468,8 @@ function writeDoc(b: flatbuffers.Builder, e: DucDocElement, usv: boolean): numbe Duc.ColumnLayout.addAutoHeight(b, col.autoHeight); return Duc.ColumnLayout.endColumnLayout(b); })(); + const fileId = e.fileId ? b.createString(e.fileId) : undefined; + const gridConfig = writeDocumentGridConfig(b, e.gridConfig, usv); Duc.DucDocElement.startDucDocElement(b); Duc.DucDocElement.addBase(b, base); Duc.DucDocElement.addStyle(b, style); @@ -1460,38 +1478,22 @@ function writeDoc(b: flatbuffers.Builder, e: DucDocElement, usv: boolean): numbe Duc.DucDocElement.addFlowDirection(b, e.flowDirection); Duc.DucDocElement.addColumns(b, columns); Duc.DucDocElement.addAutoResize(b, e.autoResize); + if (fileId) Duc.DucDocElement.addFileId(b, fileId); + Duc.DucDocElement.addGridConfig(b, gridConfig); return Duc.DucDocElement.endDucDocElement(b); } /** - * Parametric, PDF, Mermaid, Embeddable + * PDF, Mermaid, Embeddable */ -function writeParametricSource(b: flatbuffers.Builder, s: ParametricElementSource, usv: boolean): number { - Duc.ParametricSource.startParametricSource(b); - Duc.ParametricSource.addType(b, s.type); - if (s.type === Duc.PARAMETRIC_SOURCE_TYPE.CODE) { - Duc.ParametricSource.addCode(b, b.createString(s.code)); - } else { - Duc.ParametricSource.addFileId(b, b.createString(s.fileId)); - } - return Duc.ParametricSource.endParametricSource(b); -} - -function writeParametric(b: flatbuffers.Builder, e: DucParametricElement, usv: boolean): number { - const base = writeElementBase(b, e as unknown as any, usv); - const src = writeParametricSource(b, e.source, usv); - Duc.DucParametricElement.startDucParametricElement(b); - Duc.DucParametricElement.addBase(b, base); - Duc.DucParametricElement.addSource(b, src); - return Duc.DucParametricElement.endDucParametricElement(b); -} - function writePdf(b: flatbuffers.Builder, e: DucPdfElement, usv: boolean): number { const base = writeElementBase(b, e as unknown as any, usv); - const fileId = b.createString(e.fileId); + const fileId = e.fileId ? b.createString(e.fileId) : undefined; + const gridConfig = writeDocumentGridConfig(b, e.gridConfig, usv); Duc.DucPdfElement.startDucPdfElement(b); Duc.DucPdfElement.addBase(b, base); - Duc.DucPdfElement.addFileId(b, fileId); + if (fileId) Duc.DucPdfElement.addFileId(b, fileId); + Duc.DucPdfElement.addGridConfig(b, gridConfig); return Duc.DucPdfElement.endDucPdfElement(b); } @@ -1508,6 +1510,19 @@ function writeMermaid(b: flatbuffers.Builder, e: DucMermaidElement, usv: boolean return Duc.DucMermaidElement.endDucMermaidElement(b); } +function writeModel(b: flatbuffers.Builder, e: DucModelElement, usv: boolean): number { + const base = writeElementBase(b, e as unknown as any, usv); + const src = b.createString(e.source); + const svg = e.svgPath ? b.createString(e.svgPath) : undefined; + const fileIds = e.fileIds?.length ? Duc.DucModelElement.createFileIdsVector(b, e.fileIds.map((id) => b.createString(id))) : undefined; + Duc.DucModelElement.startDucModelElement(b); + Duc.DucModelElement.addBase(b, base); + Duc.DucModelElement.addSource(b, src); + if (svg) Duc.DucModelElement.addSvgPath(b, svg); + if (fileIds) Duc.DucModelElement.addFileIds(b, fileIds); + return Duc.DucModelElement.endDucModelElement(b); +} + function writeEmbeddable(b: flatbuffers.Builder, e: DucEmbeddableElement, usv: boolean): number { const base = writeElementBase(b, e as unknown as any, usv); Duc.DucEmbeddableElement.startDucEmbeddableElement(b); @@ -1720,9 +1735,9 @@ function writeElementWrapper(b: flatbuffers.Builder, e: DucElement, usv: boolean type = Duc.Element.DucDocElement; elem = writeDoc(b, e, usv); break; - case "parametric": - type = Duc.Element.DucParametricElement; - elem = writeParametric(b, e, usv); + case "model": + type = Duc.Element.DucModelElement; + elem = writeModel(b, e, usv); break; case "embeddable": type = Duc.Element.DucEmbeddableElement; diff --git a/packages/ducjs/src/types/elements/index.ts b/packages/ducjs/src/types/elements/index.ts index 0b797c1..177f8e7 100644 --- a/packages/ducjs/src/types/elements/index.ts +++ b/packages/ducjs/src/types/elements/index.ts @@ -172,7 +172,7 @@ export type DucElement = | DucDocElement | DucEllipseElement | DucPolygonElement - | DucParametricElement + | DucModelElement | DucFeatureControlFrameElement | DucLeaderElement | DucDimensionElement @@ -180,8 +180,7 @@ export type DucElement = | DucPlotElement | DucXRayElement | DucPdfElement - | DucMermaidElement; - + | DucMermaidElement export type DucElementTypes = DucElement["type"]; @@ -523,9 +522,23 @@ export type DucEmbeddableElement = _DucElementBase & { type: "embeddable"; }; +/** + * Configuration for PDF grid layout + */ +export type DocumentGridConfig = { + columns: number; // 1 = single, 2 = two-up, n = grid + gapX: number; // horizontal spacing (px) + gapY: number; // vertical spacing (px) + alignItems: 'start' | 'center' | 'end'; // vertical alignment within row + firstPageAlone: boolean; // cover page behavior for 2+ columns +} + export type DucPdfElement = _DucElementBase & { type: "pdf"; fileId: ExternalFileId | null; + + /** Configuration for rendering the document in a grid layout */ + gridConfig: DocumentGridConfig; }; export type DucMermaidElement = _DucElementBase & { @@ -1802,10 +1815,8 @@ export type DucDocElement = _DucElementBase & DucDocStyle & { type: "doc"; /** - * The content of the document, stored as a Markdown string. - * This approach allows a rich text editor (like Tiptap) to manage the complex - * inline formatting (bold, italic, colors, hyperlinks, etc.) while keeping the - * core data structure simple and clean. + * The content of the document, stored as a code string. + * This approach allows to use a rich text editor that can compile to PDF using Typst code * * It can also contain wildcards like `{@fieldname}` for dynamic data insertion. * Example: "This is **bold text** and this is a {color:red}red word{/color}." @@ -1814,6 +1825,12 @@ export type DucDocElement = _DucElementBase & DucDocStyle & { * Example: "This document was last saved on {{SaveDate}} by {{Author}}." */ text: string; + fileId: ExternalFileId | null; + + + /** Configuration for rendering the document in a grid layout */ + gridConfig: DocumentGridConfig; + /** * An array of metadata objects that define the behavior of the placeholders @@ -1857,33 +1874,22 @@ export type DucDocElement = _DucElementBase & DucDocStyle & { -//// === 3D Parametric Element === +//// === 3D Model Element === +// TODO: Add camera and view config /** - * Defines the source of the 3D geometry for a Parametric Element. - * The geometry is either generated from live code or loaded from an external file. - */ -export type ParametricElementSource = - | { - /** The geometry is defined by executable Replicad code. */ - type: PARAMETRIC_SOURCE_TYPE.CODE; - /** The JavaScript code that generates the Replicad model. */ - code: string; - } - | { - /** The geometry is loaded from a static 3D file. */ - type: PARAMETRIC_SOURCE_TYPE.FILE; - /** A reference to the imported file in the DucExternalFiles collection. */ - fileId: ExternalFileId; - }; -/** - * An element that embeds a 3D model on the 2D canvas, defined either by - * parametric Replicad code or by an imported 3D file (e.g., STEP, STL). + * An element that embeds a 3D model on the 2D canvas, defined by build123d python code. * It includes its own 3D view and display controls. */ -export type DucParametricElement = _DucElementBase & { - type: "parametric"; +export type DucModelElement = _DucElementBase & { + type: "model"; - /** Defines the source of the 3D geometry (either from code or a file). */ - source: ParametricElementSource; -}; + /** Defines the source of the model using build123d python code */ + source: string; + + /** The last known SVG path representation of the 3D model for quick rendering on the canvas */ + svgPath: string | null; + + /** Possibly connected external files, such as STEP, STL or other reference models */ + fileIds: ExternalFileId[]; +}; \ No newline at end of file diff --git a/packages/ducjs/src/types/elements/typeChecks.ts b/packages/ducjs/src/types/elements/typeChecks.ts index e86bc43..69a3ad5 100644 --- a/packages/ducjs/src/types/elements/typeChecks.ts +++ b/packages/ducjs/src/types/elements/typeChecks.ts @@ -267,7 +267,7 @@ export const isDucElement = ( case "leader": case "doc": case "selection": - case "parametric": + case "model": case "featurecontrolframe": case "viewport": case "plot": diff --git a/packages/ducjs/src/utils/elements/newElement.ts b/packages/ducjs/src/utils/elements/newElement.ts index 545538e..76ea174 100644 --- a/packages/ducjs/src/utils/elements/newElement.ts +++ b/packages/ducjs/src/utils/elements/newElement.ts @@ -28,7 +28,7 @@ import { DucLeaderElement, DucLinearElement, DucMermaidElement, - DucParametricElement, + DucModelElement, DucPdfElement, DucPlotElement, DucPolygonElement, @@ -427,6 +427,8 @@ export const newDocElement = ( flowDirection: opts.flowDirection || TEXT_FLOW_DIRECTION.TOP_TO_BOTTOM, columns: opts.columns || { type: COLUMN_TYPE.NO_COLUMNS, definitions: [], autoHeight: true }, autoResize: opts.autoResize ?? true, + fileId: null, + gridConfig: { columns: 1, gapX: 0, gapY: 0, alignItems: 'start', firstPageAlone: false }, // DucDocStyle properties isLtr: opts.isLtr ?? true, fontFamily: opts.fontFamily || DEFAULT_FONT_FAMILY, @@ -447,6 +449,7 @@ export const newDocElement = ( export const newPdfElement = (currentScope: Scope, opts: ElementConstructorOpts): NonDeleted => ({ fileId: null, + gridConfig: { columns: 1, gapX: 0, gapY: 0, alignItems: 'start', firstPageAlone: false }, ..._newElementBase("pdf", currentScope, opts), type: "pdf", }); @@ -524,10 +527,12 @@ export const newFeatureControlFrameElement = ( }; }; -export const newParametricElement = (currentScope: Scope, opts: ElementConstructorOpts): NonDeleted => ({ - source: { type: PARAMETRIC_SOURCE_TYPE.CODE, code: "" }, - ..._newElementBase("parametric", currentScope, opts), - type: 'parametric', +export const newParametricElement = (currentScope: Scope, opts: ElementConstructorOpts): NonDeleted => ({ + source: "", + svgPath: null, + fileIds: [], + ..._newElementBase("model", currentScope, opts), + type: 'model', }); // Simplified deep clone for the purpose of cloning DucElement. diff --git a/packages/ducpy/src/ducpy/Duc/DOCUMENT_GRID_ALIGN_ITEMS.py b/packages/ducpy/src/ducpy/Duc/DOCUMENT_GRID_ALIGN_ITEMS.py new file mode 100644 index 0000000..9e48ea0 --- /dev/null +++ b/packages/ducpy/src/ducpy/Duc/DOCUMENT_GRID_ALIGN_ITEMS.py @@ -0,0 +1,8 @@ +# automatically generated by the FlatBuffers compiler, do not modify + +# namespace: Duc + +class DOCUMENT_GRID_ALIGN_ITEMS(object): + START = 10 + CENTER = 11 + END = 12 diff --git a/packages/ducpy/src/ducpy/Duc/DocumentGridConfig.py b/packages/ducpy/src/ducpy/Duc/DocumentGridConfig.py new file mode 100644 index 0000000..b7904a0 --- /dev/null +++ b/packages/ducpy/src/ducpy/Duc/DocumentGridConfig.py @@ -0,0 +1,106 @@ +# automatically generated by the FlatBuffers compiler, do not modify + +# namespace: Duc + +import flatbuffers +from flatbuffers.compat import import_numpy +np = import_numpy() + +class DocumentGridConfig(object): + __slots__ = ['_tab'] + + @classmethod + def GetRootAs(cls, buf, offset=0): + n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) + x = DocumentGridConfig() + x.Init(buf, n + offset) + return x + + @classmethod + def GetRootAsDocumentGridConfig(cls, buf, offset=0): + """This method is deprecated. Please switch to GetRootAs.""" + return cls.GetRootAs(buf, offset) + @classmethod + def DocumentGridConfigBufferHasIdentifier(cls, buf, offset, size_prefixed=False): + return flatbuffers.util.BufferHasIdentifier(buf, offset, b"\x44\x55\x43\x5F", size_prefixed=size_prefixed) + + # DocumentGridConfig + def Init(self, buf, pos): + self._tab = flatbuffers.table.Table(buf, pos) + + # DocumentGridConfig + def Columns(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos) + return 0 + + # DocumentGridConfig + def GapX(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float64Flags, o + self._tab.Pos) + return 0.0 + + # DocumentGridConfig + def GapY(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float64Flags, o + self._tab.Pos) + return 0.0 + + # DocumentGridConfig + def AlignItems(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos) + return None + + # DocumentGridConfig + def FirstPageAlone(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + +def DocumentGridConfigStart(builder): + builder.StartObject(5) + +def Start(builder): + DocumentGridConfigStart(builder) + +def DocumentGridConfigAddColumns(builder, columns): + builder.PrependInt32Slot(0, columns, 0) + +def AddColumns(builder, columns): + DocumentGridConfigAddColumns(builder, columns) + +def DocumentGridConfigAddGapX(builder, gapX): + builder.PrependFloat64Slot(1, gapX, 0.0) + +def AddGapX(builder, gapX): + DocumentGridConfigAddGapX(builder, gapX) + +def DocumentGridConfigAddGapY(builder, gapY): + builder.PrependFloat64Slot(2, gapY, 0.0) + +def AddGapY(builder, gapY): + DocumentGridConfigAddGapY(builder, gapY) + +def DocumentGridConfigAddAlignItems(builder, alignItems): + builder.PrependUint8Slot(3, alignItems, None) + +def AddAlignItems(builder, alignItems): + DocumentGridConfigAddAlignItems(builder, alignItems) + +def DocumentGridConfigAddFirstPageAlone(builder, firstPageAlone): + builder.PrependBoolSlot(4, firstPageAlone, 0) + +def AddFirstPageAlone(builder, firstPageAlone): + DocumentGridConfigAddFirstPageAlone(builder, firstPageAlone) + +def DocumentGridConfigEnd(builder): + return builder.EndObject() + +def End(builder): + return DocumentGridConfigEnd(builder) diff --git a/packages/ducpy/src/ducpy/Duc/DucDocElement.py b/packages/ducpy/src/ducpy/Duc/DucDocElement.py index 9f79f1d..2d6354d 100644 --- a/packages/ducpy/src/ducpy/Duc/DucDocElement.py +++ b/packages/ducpy/src/ducpy/Duc/DucDocElement.py @@ -107,8 +107,26 @@ def AutoResize(self): return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) return False + # DucDocElement + def GridConfig(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18)) + if o != 0: + x = self._tab.Indirect(o + self._tab.Pos) + from Duc.DocumentGridConfig import DocumentGridConfig + obj = DocumentGridConfig() + obj.Init(self._tab.Bytes, x) + return obj + return None + + # DucDocElement + def FileId(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + def DucDocElementStart(builder): - builder.StartObject(7) + builder.StartObject(9) def Start(builder): DucDocElementStart(builder) @@ -161,6 +179,18 @@ def DucDocElementAddAutoResize(builder, autoResize): def AddAutoResize(builder, autoResize): DucDocElementAddAutoResize(builder, autoResize) +def DucDocElementAddGridConfig(builder, gridConfig): + builder.PrependUOffsetTRelativeSlot(7, flatbuffers.number_types.UOffsetTFlags.py_type(gridConfig), 0) + +def AddGridConfig(builder, gridConfig): + DucDocElementAddGridConfig(builder, gridConfig) + +def DucDocElementAddFileId(builder, fileId): + builder.PrependUOffsetTRelativeSlot(8, flatbuffers.number_types.UOffsetTFlags.py_type(fileId), 0) + +def AddFileId(builder, fileId): + DucDocElementAddFileId(builder, fileId) + def DucDocElementEnd(builder): return builder.EndObject() diff --git a/packages/ducpy/src/ducpy/Duc/DucModelElement.py b/packages/ducpy/src/ducpy/Duc/DucModelElement.py new file mode 100644 index 0000000..3a8d683 --- /dev/null +++ b/packages/ducpy/src/ducpy/Duc/DucModelElement.py @@ -0,0 +1,116 @@ +# automatically generated by the FlatBuffers compiler, do not modify + +# namespace: Duc + +import flatbuffers +from flatbuffers.compat import import_numpy +np = import_numpy() + +class DucModelElement(object): + __slots__ = ['_tab'] + + @classmethod + def GetRootAs(cls, buf, offset=0): + n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) + x = DucModelElement() + x.Init(buf, n + offset) + return x + + @classmethod + def GetRootAsDucModelElement(cls, buf, offset=0): + """This method is deprecated. Please switch to GetRootAs.""" + return cls.GetRootAs(buf, offset) + @classmethod + def DucModelElementBufferHasIdentifier(cls, buf, offset, size_prefixed=False): + return flatbuffers.util.BufferHasIdentifier(buf, offset, b"\x44\x55\x43\x5F", size_prefixed=size_prefixed) + + # DucModelElement + def Init(self, buf, pos): + self._tab = flatbuffers.table.Table(buf, pos) + + # DucModelElement + def Base(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) + if o != 0: + x = self._tab.Indirect(o + self._tab.Pos) + from Duc._DucElementBase import _DucElementBase + obj = _DucElementBase() + obj.Init(self._tab.Bytes, x) + return obj + return None + + # DucModelElement + def Source(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # DucModelElement + def SvgPath(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # DucModelElement + def FileIds(self, j): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) + if o != 0: + a = self._tab.Vector(o) + return self._tab.String(a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 4)) + return "" + + # DucModelElement + def FileIdsLength(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) + if o != 0: + return self._tab.VectorLen(o) + return 0 + + # DucModelElement + def FileIdsIsNone(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) + return o == 0 + +def DucModelElementStart(builder): + builder.StartObject(4) + +def Start(builder): + DucModelElementStart(builder) + +def DucModelElementAddBase(builder, base): + builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(base), 0) + +def AddBase(builder, base): + DucModelElementAddBase(builder, base) + +def DucModelElementAddSource(builder, source): + builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(source), 0) + +def AddSource(builder, source): + DucModelElementAddSource(builder, source) + +def DucModelElementAddSvgPath(builder, svgPath): + builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(svgPath), 0) + +def AddSvgPath(builder, svgPath): + DucModelElementAddSvgPath(builder, svgPath) + +def DucModelElementAddFileIds(builder, fileIds): + builder.PrependUOffsetTRelativeSlot(3, flatbuffers.number_types.UOffsetTFlags.py_type(fileIds), 0) + +def AddFileIds(builder, fileIds): + DucModelElementAddFileIds(builder, fileIds) + +def DucModelElementStartFileIdsVector(builder, numElems): + return builder.StartVector(4, numElems, 4) + +def StartFileIdsVector(builder, numElems): + return DucModelElementStartFileIdsVector(builder, numElems) + +def DucModelElementEnd(builder): + return builder.EndObject() + +def End(builder): + return DucModelElementEnd(builder) diff --git a/packages/ducpy/src/ducpy/Duc/DucPdfElement.py b/packages/ducpy/src/ducpy/Duc/DucPdfElement.py index 199f37c..e27ae8e 100644 --- a/packages/ducpy/src/ducpy/Duc/DucPdfElement.py +++ b/packages/ducpy/src/ducpy/Duc/DucPdfElement.py @@ -46,8 +46,19 @@ def FileId(self): return self._tab.String(o + self._tab.Pos) return None + # DucPdfElement + def GridConfig(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + if o != 0: + x = self._tab.Indirect(o + self._tab.Pos) + from Duc.DocumentGridConfig import DocumentGridConfig + obj = DocumentGridConfig() + obj.Init(self._tab.Bytes, x) + return obj + return None + def DucPdfElementStart(builder): - builder.StartObject(2) + builder.StartObject(3) def Start(builder): DucPdfElementStart(builder) @@ -64,6 +75,12 @@ def DucPdfElementAddFileId(builder, fileId): def AddFileId(builder, fileId): DucPdfElementAddFileId(builder, fileId) +def DucPdfElementAddGridConfig(builder, gridConfig): + builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(gridConfig), 0) + +def AddGridConfig(builder, gridConfig): + DucPdfElementAddGridConfig(builder, gridConfig) + def DucPdfElementEnd(builder): return builder.EndObject() diff --git a/packages/ducpy/src/ducpy/Duc/Element.py b/packages/ducpy/src/ducpy/Duc/Element.py index baeaeef..1ff5cab 100644 --- a/packages/ducpy/src/ducpy/Duc/Element.py +++ b/packages/ducpy/src/ducpy/Duc/Element.py @@ -26,3 +26,4 @@ class Element(object): DucFeatureControlFrameElement = 20 DucDocElement = 21 DucParametricElement = 22 + DucModelElement = 23 diff --git a/packages/ducpy/src/ducpy/classes/ElementsClass.py b/packages/ducpy/src/ducpy/classes/ElementsClass.py index 340aa23..ad00bc9 100644 --- a/packages/ducpy/src/ducpy/classes/ElementsClass.py +++ b/packages/ducpy/src/ducpy/classes/ElementsClass.py @@ -438,10 +438,19 @@ class DucEllipseElement: class DucEmbeddableElement: base: DucElementBase +@dataclass +class DocumentGridConfig: + columns: int + gap_x: float + gap_y: float + align_items: DOCUMENT_GRID_ALIGN_ITEMS + first_page_alone: bool + @dataclass class DucPdfElement: base: DucElementBase file_id: Optional[str] + grid_config: DocumentGridConfig @dataclass class DucMermaidElement: @@ -817,7 +826,9 @@ class DucDocElement: columns: ColumnLayout auto_resize: bool flow_direction: TEXT_FLOW_DIRECTION - + file_id: Optional[str] + grid_config: DocumentGridConfig + @dataclass class DucCommonStyle: @@ -835,6 +846,13 @@ class DucParametricElement: base: DucElementBase source: ParametricSource +@dataclass +class DucModelElement: + base: DucElementBase + source: str + svg_path: Optional[str] + file_ids: List[str] + @dataclass class DucGroup: id: str @@ -880,7 +898,8 @@ class DucLayer: DucDimensionElement, DucFeatureControlFrameElement, DucDocElement, - DucParametricElement + DucParametricElement, + DucModelElement ] @dataclass diff --git a/packages/ducpy/src/ducpy/parse.py b/packages/ducpy/src/ducpy/parse.py index b6b503c..2a6f3c1 100644 --- a/packages/ducpy/src/ducpy/parse.py +++ b/packages/ducpy/src/ducpy/parse.py @@ -50,12 +50,14 @@ DucFeatureControlFrameElement as DS_DucFeatureControlFrameElement, DucDocElement as DS_DucDocElement, DucParametricElement as DS_DucParametricElement, + DucModelElement as DS_DucModelElement, DucBlock as DS_DucBlock, DucBlockCollection as DS_DucBlockCollection, DucBlockMetadata as DS_DucBlockMetadata, DucGroup as DS_DucGroup, DucRegion as DS_DucRegion, DucLayer as DS_DucLayer, + DocumentGridConfig as DS_DocumentGridConfig, ElementBackground as DS_ElementBackground, ElementStroke as DS_ElementStroke, GeometricPoint as DS_GeometricPoint, @@ -261,6 +263,8 @@ from ducpy.Duc.DucFeatureControlFrameElement import DucFeatureControlFrameElement as FBSDucFeatureControlFrameElement from ducpy.Duc.DucDocElement import DucDocElement as FBSDucDocElement from ducpy.Duc.DucParametricElement import DucParametricElement as FBSDucParametricElement +from ducpy.Duc.DucModelElement import DucModelElement as FBSDucModelElement +from ducpy.Duc.DocumentGridConfig import DocumentGridConfig as FBSDocumentGridConfig from ducpy.Duc.ElementContentBase import ElementContentBase as FBSElementContentBase from ducpy.Duc.ElementStroke import ElementStroke as FBSElementStroke @@ -1088,10 +1092,33 @@ def parse_fbs_embeddable(obj: FBSDucEmbeddableElement) -> DS_DucEmbeddableElemen base=parse_fbs_duc_element_base(obj.Base()) ) +def parse_fbs_document_grid_config(obj: FBSDocumentGridConfig) -> DS_DocumentGridConfig: + return DS_DocumentGridConfig( + columns=obj.Columns(), + gap_x=obj.GapX(), + gap_y=obj.GapY(), + align_items=obj.AlignItems(), + first_page_alone=obj.FirstPageAlone(), + ) + def parse_fbs_pdf(obj: FBSDucPdfElement) -> DS_DucPdfElement: + grid_config = obj.GridConfig() + if grid_config: + grid_config_obj = FBSDocumentGridConfig() + grid_config_obj.Init(obj.TableBytes, grid_config) + parsed_grid_config = parse_fbs_document_grid_config(grid_config_obj) + else: + parsed_grid_config = DS_DocumentGridConfig( + columns=1, + gap_x=0.0, + gap_y=0.0, + align_items=0, + first_page_alone=False, + ) return DS_DucPdfElement( base=parse_fbs_duc_element_base(obj.Base()), file_id=_s(obj.FileId()), + grid_config=parsed_grid_config, ) def parse_fbs_mermaid(obj: FBSDucMermaidElement) -> DS_DucMermaidElement: @@ -1512,6 +1539,19 @@ def parse_fbs_column_layout(obj: FBSColumnLayout) -> DS_ColumnLayout: def parse_fbs_doc(obj: FBSDucDocElement) -> DS_DucDocElement: dynamics = [parse_fbs_text_dynamic_part(obj.Dynamic(i)) for i in range(obj.DynamicLength())] + grid_config = obj.GridConfig() + if grid_config: + grid_config_obj = FBSDocumentGridConfig() + grid_config_obj.Init(obj.TableBytes, grid_config) + parsed_grid_config = parse_fbs_document_grid_config(grid_config_obj) + else: + parsed_grid_config = DS_DocumentGridConfig( + columns=1, + gap_x=0.0, + gap_y=0.0, + align_items=0, + first_page_alone=False, + ) return DS_DucDocElement( base=parse_fbs_duc_element_base(obj.Base()), style=parse_fbs_doc_style(obj.Style()), @@ -1520,6 +1560,8 @@ def parse_fbs_doc(obj: FBSDucDocElement) -> DS_DucDocElement: columns=parse_fbs_column_layout(obj.Columns()), auto_resize=obj.AutoResize(), flow_direction=obj.FlowDirection() if hasattr(obj, "FlowDirection") else None, + file_id=_s(obj.FileId()), + grid_config=parsed_grid_config, ) def parse_fbs_parametric_source(obj: FBSParametricSource) -> DS_ParametricSource: @@ -1535,6 +1577,16 @@ def parse_fbs_parametric(obj: FBSDucParametricElement) -> DS_DucParametricElemen source=parse_fbs_parametric_source(obj.Source()) ) +def parse_fbs_model(obj: FBSDucModelElement) -> DS_DucModelElement: + file_ids = [obj.FileIds(i) for i in range(obj.FileIdsLength())] if obj.FileIdsLength() > 0 else [] + return DS_DucModelElement( + base=parse_fbs_duc_element_base(obj.Base()), + source=_s_req(obj.Source()), + svg_path=_s(obj.SvgPath()), + file_ids=file_ids, + ) + + # ============================================================================= # Element union and wrapper # ============================================================================= @@ -1587,6 +1639,8 @@ def parse_duc_element_wrapper(obj: FBSElementWrapper) -> DS_ElementWrapper: x = FBSDucDocElement(); x.Init(tbl.Bytes, tbl.Pos); el = parse_fbs_doc(x) elif typ == FBS_Element.Element.DucParametricElement: x = FBSDucParametricElement(); x.Init(tbl.Bytes, tbl.Pos); el = parse_fbs_parametric(x) + elif typ == FBS_Element.Element.DucModelElement: + x = FBSDucModelElement(); x.Init(tbl.Bytes, tbl.Pos); el = parse_fbs_model(x) else: raise ValueError(f"Unknown Element union type: {typ}") diff --git a/packages/ducpy/src/ducpy/serialize.py b/packages/ducpy/src/ducpy/serialize.py index 68b56e4..280dc56 100644 --- a/packages/ducpy/src/ducpy/serialize.py +++ b/packages/ducpy/src/ducpy/serialize.py @@ -51,6 +51,7 @@ DucFeatureControlFrameElement as DS_DucFeatureControlFrameElement, DucDocElement as DS_DucDocElement, DucParametricElement as DS_DucParametricElement, + DucModelElement as DS_DucModelElement, DucBlock as DS_DucBlock, DucBlockCollection as DS_DucBlockCollection, DucBlockCollectionEntry as DS_DucBlockCollectionEntry, @@ -58,6 +59,7 @@ DucGroup as DS_DucGroup, DucRegion as DS_DucRegion, DucLayer as DS_DucLayer, + DocumentGridConfig as DS_DocumentGridConfig, ElementBackground as DS_ElementBackground, ElementStroke as DS_ElementStroke, GeometricPoint as DS_GeometricPoint, @@ -571,7 +573,11 @@ DucEmbeddableElementStart, DucEmbeddableElementAddBase, DucEmbeddableElementEnd ) from ducpy.Duc.DucPdfElement import ( - DucPdfElementStart, DucPdfElementAddBase, DucPdfElementAddFileId, DucPdfElementEnd + DucPdfElementStart, DucPdfElementAddBase, DucPdfElementAddFileId, DucPdfElementAddGridConfig, DucPdfElementEnd +) +from ducpy.Duc.DocumentGridConfig import ( + DocumentGridConfigStart, DocumentGridConfigAddColumns, DocumentGridConfigAddGapX, DocumentGridConfigAddGapY, + DocumentGridConfigAddAlignItems, DocumentGridConfigAddFirstPageAlone, DocumentGridConfigEnd ) from ducpy.Duc.DucMermaidElement import ( DucMermaidElementStart, DucMermaidElementAddBase, DucMermaidElementAddSource, DucMermaidElementAddTheme, @@ -785,8 +791,12 @@ ) from ducpy.Duc.DucDocElement import ( DucDocElementStart, DucDocElementAddBase, DucDocElementAddStyle, DucDocElementAddText, DucDocElementAddDynamic, - DucDocElementAddFlowDirection, DucDocElementAddColumns, DucDocElementAddAutoResize, DucDocElementEnd, - DucDocElementStartDynamicVector + DucDocElementAddFlowDirection, DucDocElementAddColumns, DucDocElementAddAutoResize, DucDocElementAddGridConfig, + DucDocElementAddFileId, DucDocElementEnd, DucDocElementStartDynamicVector +) +from ducpy.Duc.DucModelElement import ( + DucModelElementStart, DucModelElementAddBase, DucModelElementAddSource, DucModelElementAddSvgPath, + DucModelElementAddFileIds, DucModelElementStartFileIdsVector, DucModelElementEnd ) from ducpy.Duc.ParametricSource import ( ParametricSourceStart, ParametricSourceAddType, ParametricSourceAddCode, ParametricSourceAddFileId, @@ -1619,13 +1629,24 @@ def serialize_fbs_embeddable(builder: flatbuffers.Builder, el: DS_DucEmbeddableE DucEmbeddableElementAddBase(builder, base_offset) return DucEmbeddableElementEnd(builder) +def serialize_fbs_document_grid_config(builder: flatbuffers.Builder, config: DS_DocumentGridConfig) -> int: + DocumentGridConfigStart(builder) + DocumentGridConfigAddColumns(builder, config.columns) + DocumentGridConfigAddGapX(builder, config.gap_x) + DocumentGridConfigAddGapY(builder, config.gap_y) + DocumentGridConfigAddAlignItems(builder, config.align_items) + DocumentGridConfigAddFirstPageAlone(builder, config.first_page_alone) + return DocumentGridConfigEnd(builder) + def serialize_fbs_pdf(builder: flatbuffers.Builder, el: DS_DucPdfElement) -> int: base_offset = serialize_fbs_duc_element_base(builder, el.base) file_id_offset = _str(builder, el.file_id) + grid_config_offset = serialize_fbs_document_grid_config(builder, el.grid_config) DucPdfElementStart(builder) DucPdfElementAddBase(builder, base_offset) if file_id_offset: DucPdfElementAddFileId(builder, file_id_offset) + DucPdfElementAddGridConfig(builder, grid_config_offset) return DucPdfElementEnd(builder) def serialize_fbs_mermaid(builder: flatbuffers.Builder, el: DS_DucMermaidElement) -> int: @@ -2287,6 +2308,8 @@ def serialize_fbs_doc(builder: flatbuffers.Builder, el: DS_DucDocElement) -> int builder.PrependUOffsetTRelative(off) dyn_vec = builder.EndVector() columns_offset = serialize_fbs_column_layout(builder, el.columns) + file_id_offset = _str(builder, el.file_id) + grid_config_offset = serialize_fbs_document_grid_config(builder, el.grid_config) DucDocElementStart(builder) DucDocElementAddBase(builder, base_offset) DucDocElementAddStyle(builder, style_offset) @@ -2296,6 +2319,9 @@ def serialize_fbs_doc(builder: flatbuffers.Builder, el: DS_DucDocElement) -> int DucDocElementAddFlowDirection(builder, el.flow_direction) DucDocElementAddColumns(builder, columns_offset) DucDocElementAddAutoResize(builder, el.auto_resize) + if file_id_offset: + DucDocElementAddFileId(builder, file_id_offset) + DucDocElementAddGridConfig(builder, grid_config_offset) return DucDocElementEnd(builder) def serialize_fbs_parametric_source(builder: flatbuffers.Builder, s: DS_ParametricSource) -> int: @@ -2318,6 +2344,25 @@ def serialize_fbs_parametric(builder: flatbuffers.Builder, el: DS_DucParametricE DucParametricElementAddSource(builder, source_offset) return DucParametricElementEnd(builder) +def serialize_fbs_model(builder: flatbuffers.Builder, el: DS_DucModelElement) -> int: + base_offset = serialize_fbs_duc_element_base(builder, el.base) + source_offset = builder.CreateString(el.source) + svg_offset = _str(builder, el.svg_path) + file_ids_vec = 0 + if el.file_ids: + DucModelElementStartFileIdsVector(builder, len(el.file_ids)) + for fid in reversed(el.file_ids): + builder.PrependUOffsetTRelative(builder.CreateString(fid)) + file_ids_vec = builder.EndVector() + DucModelElementStart(builder) + DucModelElementAddBase(builder, base_offset) + DucModelElementAddSource(builder, source_offset) + if svg_offset: + DucModelElementAddSvgPath(builder, svg_offset) + if file_ids_vec: + DucModelElementAddFileIds(builder, file_ids_vec) + return DucModelElementEnd(builder) + # ============================================================================= # Element union and wrapper # ============================================================================= @@ -2344,6 +2389,7 @@ def serialize_fbs_parametric(builder: flatbuffers.Builder, el: DS_DucParametricE DS_DucFeatureControlFrameElement: FBS_Element.DucFeatureControlFrameElement, DS_DucDocElement: FBS_Element.DucDocElement, DS_DucParametricElement: FBS_Element.DucParametricElement, + DS_DucModelElement: FBS_Element.DucModelElement, } @@ -2369,6 +2415,7 @@ def serialize_fbs_parametric(builder: flatbuffers.Builder, el: DS_DucParametricE DS_DucFeatureControlFrameElement: serialize_fbs_feature_control_frame, DS_DucDocElement: serialize_fbs_doc, DS_DucParametricElement: serialize_fbs_parametric, + DS_DucModelElement: serialize_fbs_model, } def serialize_fbs_dimension(builder: flatbuffers.Builder, el: DS_DucDimensionElement) -> int: diff --git a/packages/ducrs/src/flatbuffers/duc_generated.rs b/packages/ducrs/src/flatbuffers/duc_generated.rs index 299aee7..82e4fe7 100644 --- a/packages/ducrs/src/flatbuffers/duc_generated.rs +++ b/packages/ducrs/src/flatbuffers/duc_generated.rs @@ -4974,6 +4974,95 @@ impl<'a> flatbuffers::Verifiable for PARAMETRIC_SOURCE_TYPE { impl flatbuffers::SimpleToVerifyInSlice for PARAMETRIC_SOURCE_TYPE {} #[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] +pub const ENUM_MIN_DOCUMENT_GRID_ALIGN_ITEMS: u8 = 10; +#[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] +pub const ENUM_MAX_DOCUMENT_GRID_ALIGN_ITEMS: u8 = 12; +#[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] +#[allow(non_camel_case_types)] +pub const ENUM_VALUES_DOCUMENT_GRID_ALIGN_ITEMS: [DOCUMENT_GRID_ALIGN_ITEMS; 3] = [ + DOCUMENT_GRID_ALIGN_ITEMS::START, + DOCUMENT_GRID_ALIGN_ITEMS::CENTER, + DOCUMENT_GRID_ALIGN_ITEMS::END, +]; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[repr(transparent)] +pub struct DOCUMENT_GRID_ALIGN_ITEMS(pub u8); +#[allow(non_upper_case_globals)] +impl DOCUMENT_GRID_ALIGN_ITEMS { + pub const START: Self = Self(10); + pub const CENTER: Self = Self(11); + pub const END: Self = Self(12); + + pub const ENUM_MIN: u8 = 10; + pub const ENUM_MAX: u8 = 12; + pub const ENUM_VALUES: &'static [Self] = &[ + Self::START, + Self::CENTER, + Self::END, + ]; + /// Returns the variant's name or "" if unknown. + pub fn variant_name(self) -> Option<&'static str> { + match self { + Self::START => Some("START"), + Self::CENTER => Some("CENTER"), + Self::END => Some("END"), + _ => None, + } + } +} +impl core::fmt::Debug for DOCUMENT_GRID_ALIGN_ITEMS { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + if let Some(name) = self.variant_name() { + f.write_str(name) + } else { + f.write_fmt(format_args!("", self.0)) + } + } +} +impl<'a> flatbuffers::Follow<'a> for DOCUMENT_GRID_ALIGN_ITEMS { + type Inner = Self; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + let b = flatbuffers::read_scalar_at::(buf, loc); + Self(b) + } +} + +impl flatbuffers::Push for DOCUMENT_GRID_ALIGN_ITEMS { + type Output = DOCUMENT_GRID_ALIGN_ITEMS; + #[inline] + unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { + flatbuffers::emplace_scalar::(dst, self.0); + } +} + +impl flatbuffers::EndianScalar for DOCUMENT_GRID_ALIGN_ITEMS { + type Scalar = u8; + #[inline] + fn to_little_endian(self) -> u8 { + self.0.to_le() + } + #[inline] + #[allow(clippy::wrong_self_convention)] + fn from_little_endian(v: u8) -> Self { + let b = u8::from_le(v); + Self(b) + } +} + +impl<'a> flatbuffers::Verifiable for DOCUMENT_GRID_ALIGN_ITEMS { + #[inline] + fn run_verifier( + v: &mut flatbuffers::Verifier, pos: usize + ) -> Result<(), flatbuffers::InvalidFlatbuffer> { + use self::flatbuffers::Verifiable; + u8::run_verifier(v, pos) + } +} + +impl flatbuffers::SimpleToVerifyInSlice for DOCUMENT_GRID_ALIGN_ITEMS {} +#[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] pub const ENUM_MIN_LEADER_CONTENT_TYPE: u8 = 10; #[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] pub const ENUM_MAX_LEADER_CONTENT_TYPE: u8 = 11; @@ -5336,10 +5425,10 @@ pub struct LeaderContentDataUnionTableOffset {} #[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] pub const ENUM_MIN_ELEMENT: u8 = 0; #[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] -pub const ENUM_MAX_ELEMENT: u8 = 22; +pub const ENUM_MAX_ELEMENT: u8 = 23; #[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] #[allow(non_camel_case_types)] -pub const ENUM_VALUES_ELEMENT: [Element; 23] = [ +pub const ENUM_VALUES_ELEMENT: [Element; 24] = [ Element::NONE, Element::DucRectangleElement, Element::DucPolygonElement, @@ -5363,6 +5452,7 @@ pub const ENUM_VALUES_ELEMENT: [Element; 23] = [ Element::DucFeatureControlFrameElement, Element::DucDocElement, Element::DucParametricElement, + Element::DucModelElement, ]; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] @@ -5393,9 +5483,10 @@ impl Element { pub const DucFeatureControlFrameElement: Self = Self(20); pub const DucDocElement: Self = Self(21); pub const DucParametricElement: Self = Self(22); + pub const DucModelElement: Self = Self(23); pub const ENUM_MIN: u8 = 0; - pub const ENUM_MAX: u8 = 22; + pub const ENUM_MAX: u8 = 23; pub const ENUM_VALUES: &'static [Self] = &[ Self::NONE, Self::DucRectangleElement, @@ -5420,6 +5511,7 @@ impl Element { Self::DucFeatureControlFrameElement, Self::DucDocElement, Self::DucParametricElement, + Self::DucModelElement, ]; /// Returns the variant's name or "" if unknown. pub fn variant_name(self) -> Option<&'static str> { @@ -5447,6 +5539,7 @@ impl Element { Self::DucFeatureControlFrameElement => Some("DucFeatureControlFrameElement"), Self::DucDocElement => Some("DucDocElement"), Self::DucParametricElement => Some("DucParametricElement"), + Self::DucModelElement => Some("DucModelElement"), _ => None, } } @@ -14032,6 +14125,171 @@ impl core::fmt::Debug for DucEmbeddableElement<'_> { ds.finish() } } +pub enum DocumentGridConfigOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct DocumentGridConfig<'a> { + pub _tab: flatbuffers::Table<'a>, +} + +impl<'a> flatbuffers::Follow<'a> for DocumentGridConfig<'a> { + type Inner = DocumentGridConfig<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { _tab: flatbuffers::Table::new(buf, loc) } + } +} + +impl<'a> DocumentGridConfig<'a> { + pub const VT_COLUMNS: flatbuffers::VOffsetT = 4; + pub const VT_GAP_X: flatbuffers::VOffsetT = 6; + pub const VT_GAP_Y: flatbuffers::VOffsetT = 8; + pub const VT_ALIGN_ITEMS: flatbuffers::VOffsetT = 10; + pub const VT_FIRST_PAGE_ALONE: flatbuffers::VOffsetT = 12; + + #[inline] + pub unsafe fn init_from_table(table: flatbuffers::Table<'a>) -> Self { + DocumentGridConfig { _tab: table } + } + #[allow(unused_mut)] + pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: flatbuffers::Allocator + 'bldr>( + _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args DocumentGridConfigArgs + ) -> flatbuffers::WIPOffset> { + let mut builder = DocumentGridConfigBuilder::new(_fbb); + builder.add_gap_y(args.gap_y); + builder.add_gap_x(args.gap_x); + builder.add_columns(args.columns); + builder.add_first_page_alone(args.first_page_alone); + if let Some(x) = args.align_items { builder.add_align_items(x); } + builder.finish() + } + + + #[inline] + pub fn columns(&self) -> i32 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::(DocumentGridConfig::VT_COLUMNS, Some(0)).unwrap()} + } + #[inline] + pub fn gap_x(&self) -> f64 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::(DocumentGridConfig::VT_GAP_X, Some(0.0)).unwrap()} + } + #[inline] + pub fn gap_y(&self) -> f64 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::(DocumentGridConfig::VT_GAP_Y, Some(0.0)).unwrap()} + } + #[inline] + pub fn align_items(&self) -> Option { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::(DocumentGridConfig::VT_ALIGN_ITEMS, None)} + } + #[inline] + pub fn first_page_alone(&self) -> bool { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::(DocumentGridConfig::VT_FIRST_PAGE_ALONE, Some(false)).unwrap()} + } +} + +impl flatbuffers::Verifiable for DocumentGridConfig<'_> { + #[inline] + fn run_verifier( + v: &mut flatbuffers::Verifier, pos: usize + ) -> Result<(), flatbuffers::InvalidFlatbuffer> { + use self::flatbuffers::Verifiable; + v.visit_table(pos)? + .visit_field::("columns", Self::VT_COLUMNS, false)? + .visit_field::("gap_x", Self::VT_GAP_X, false)? + .visit_field::("gap_y", Self::VT_GAP_Y, false)? + .visit_field::("align_items", Self::VT_ALIGN_ITEMS, false)? + .visit_field::("first_page_alone", Self::VT_FIRST_PAGE_ALONE, false)? + .finish(); + Ok(()) + } +} +pub struct DocumentGridConfigArgs { + pub columns: i32, + pub gap_x: f64, + pub gap_y: f64, + pub align_items: Option, + pub first_page_alone: bool, +} +impl<'a> Default for DocumentGridConfigArgs { + #[inline] + fn default() -> Self { + DocumentGridConfigArgs { + columns: 0, + gap_x: 0.0, + gap_y: 0.0, + align_items: None, + first_page_alone: false, + } + } +} + +pub struct DocumentGridConfigBuilder<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> { + fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, + start_: flatbuffers::WIPOffset, +} +impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> DocumentGridConfigBuilder<'a, 'b, A> { + #[inline] + pub fn add_columns(&mut self, columns: i32) { + self.fbb_.push_slot::(DocumentGridConfig::VT_COLUMNS, columns, 0); + } + #[inline] + pub fn add_gap_x(&mut self, gap_x: f64) { + self.fbb_.push_slot::(DocumentGridConfig::VT_GAP_X, gap_x, 0.0); + } + #[inline] + pub fn add_gap_y(&mut self, gap_y: f64) { + self.fbb_.push_slot::(DocumentGridConfig::VT_GAP_Y, gap_y, 0.0); + } + #[inline] + pub fn add_align_items(&mut self, align_items: DOCUMENT_GRID_ALIGN_ITEMS) { + self.fbb_.push_slot_always::(DocumentGridConfig::VT_ALIGN_ITEMS, align_items); + } + #[inline] + pub fn add_first_page_alone(&mut self, first_page_alone: bool) { + self.fbb_.push_slot::(DocumentGridConfig::VT_FIRST_PAGE_ALONE, first_page_alone, false); + } + #[inline] + pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>) -> DocumentGridConfigBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + DocumentGridConfigBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + flatbuffers::WIPOffset::new(o.value()) + } +} + +impl core::fmt::Debug for DocumentGridConfig<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut ds = f.debug_struct("DocumentGridConfig"); + ds.field("columns", &self.columns()); + ds.field("gap_x", &self.gap_x()); + ds.field("gap_y", &self.gap_y()); + ds.field("align_items", &self.align_items()); + ds.field("first_page_alone", &self.first_page_alone()); + ds.finish() + } +} pub enum DucPdfElementOffset {} #[derive(Copy, Clone, PartialEq)] @@ -14050,6 +14308,7 @@ impl<'a> flatbuffers::Follow<'a> for DucPdfElement<'a> { impl<'a> DucPdfElement<'a> { pub const VT_BASE: flatbuffers::VOffsetT = 4; pub const VT_FILE_ID: flatbuffers::VOffsetT = 6; + pub const VT_GRID_CONFIG: flatbuffers::VOffsetT = 8; #[inline] pub unsafe fn init_from_table(table: flatbuffers::Table<'a>) -> Self { @@ -14061,6 +14320,7 @@ impl<'a> DucPdfElement<'a> { args: &'args DucPdfElementArgs<'args> ) -> flatbuffers::WIPOffset> { let mut builder = DucPdfElementBuilder::new(_fbb); + if let Some(x) = args.grid_config { builder.add_grid_config(x); } if let Some(x) = args.file_id { builder.add_file_id(x); } if let Some(x) = args.base { builder.add_base(x); } builder.finish() @@ -14081,6 +14341,13 @@ impl<'a> DucPdfElement<'a> { // which contains a valid value in this slot unsafe { self._tab.get::>(DucPdfElement::VT_FILE_ID, None)} } + #[inline] + pub fn grid_config(&self) -> Option> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::>(DucPdfElement::VT_GRID_CONFIG, None)} + } } impl flatbuffers::Verifiable for DucPdfElement<'_> { @@ -14092,6 +14359,7 @@ impl flatbuffers::Verifiable for DucPdfElement<'_> { v.visit_table(pos)? .visit_field::>("base", Self::VT_BASE, false)? .visit_field::>("file_id", Self::VT_FILE_ID, false)? + .visit_field::>("grid_config", Self::VT_GRID_CONFIG, false)? .finish(); Ok(()) } @@ -14099,6 +14367,7 @@ impl flatbuffers::Verifiable for DucPdfElement<'_> { pub struct DucPdfElementArgs<'a> { pub base: Option>>, pub file_id: Option>, + pub grid_config: Option>>, } impl<'a> Default for DucPdfElementArgs<'a> { #[inline] @@ -14106,6 +14375,7 @@ impl<'a> Default for DucPdfElementArgs<'a> { DucPdfElementArgs { base: None, file_id: None, + grid_config: None, } } } @@ -14124,6 +14394,10 @@ impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> DucPdfElementBuilder<'a, 'b, A> self.fbb_.push_slot_always::>(DucPdfElement::VT_FILE_ID, file_id); } #[inline] + pub fn add_grid_config(&mut self, grid_config: flatbuffers::WIPOffset>) { + self.fbb_.push_slot_always::>(DucPdfElement::VT_GRID_CONFIG, grid_config); + } + #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>) -> DucPdfElementBuilder<'a, 'b, A> { let start = _fbb.start_table(); DucPdfElementBuilder { @@ -14143,6 +14417,7 @@ impl core::fmt::Debug for DucPdfElement<'_> { let mut ds = f.debug_struct("DucPdfElement"); ds.field("base", &self.base()); ds.field("file_id", &self.file_id()); + ds.field("grid_config", &self.grid_config()); ds.finish() } } @@ -22171,6 +22446,8 @@ impl<'a> DucDocElement<'a> { pub const VT_FLOW_DIRECTION: flatbuffers::VOffsetT = 12; pub const VT_COLUMNS: flatbuffers::VOffsetT = 14; pub const VT_AUTO_RESIZE: flatbuffers::VOffsetT = 16; + pub const VT_GRID_CONFIG: flatbuffers::VOffsetT = 18; + pub const VT_FILE_ID: flatbuffers::VOffsetT = 20; #[inline] pub unsafe fn init_from_table(table: flatbuffers::Table<'a>) -> Self { @@ -22182,6 +22459,8 @@ impl<'a> DucDocElement<'a> { args: &'args DucDocElementArgs<'args> ) -> flatbuffers::WIPOffset> { let mut builder = DucDocElementBuilder::new(_fbb); + if let Some(x) = args.file_id { builder.add_file_id(x); } + if let Some(x) = args.grid_config { builder.add_grid_config(x); } if let Some(x) = args.columns { builder.add_columns(x); } if let Some(x) = args.dynamic { builder.add_dynamic(x); } if let Some(x) = args.text { builder.add_text(x); } @@ -22242,6 +22521,20 @@ impl<'a> DucDocElement<'a> { // which contains a valid value in this slot unsafe { self._tab.get::(DucDocElement::VT_AUTO_RESIZE, Some(false)).unwrap()} } + #[inline] + pub fn grid_config(&self) -> Option> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::>(DucDocElement::VT_GRID_CONFIG, None)} + } + #[inline] + pub fn file_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::>(DucDocElement::VT_FILE_ID, None)} + } } impl flatbuffers::Verifiable for DucDocElement<'_> { @@ -22258,6 +22551,8 @@ impl flatbuffers::Verifiable for DucDocElement<'_> { .visit_field::("flow_direction", Self::VT_FLOW_DIRECTION, false)? .visit_field::>("columns", Self::VT_COLUMNS, false)? .visit_field::("auto_resize", Self::VT_AUTO_RESIZE, false)? + .visit_field::>("grid_config", Self::VT_GRID_CONFIG, false)? + .visit_field::>("file_id", Self::VT_FILE_ID, false)? .finish(); Ok(()) } @@ -22270,6 +22565,8 @@ pub struct DucDocElementArgs<'a> { pub flow_direction: Option, pub columns: Option>>, pub auto_resize: bool, + pub grid_config: Option>>, + pub file_id: Option>, } impl<'a> Default for DucDocElementArgs<'a> { #[inline] @@ -22282,6 +22579,8 @@ impl<'a> Default for DucDocElementArgs<'a> { flow_direction: None, columns: None, auto_resize: false, + grid_config: None, + file_id: None, } } } @@ -22320,6 +22619,14 @@ impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> DucDocElementBuilder<'a, 'b, A> self.fbb_.push_slot::(DucDocElement::VT_AUTO_RESIZE, auto_resize, false); } #[inline] + pub fn add_grid_config(&mut self, grid_config: flatbuffers::WIPOffset>) { + self.fbb_.push_slot_always::>(DucDocElement::VT_GRID_CONFIG, grid_config); + } + #[inline] + pub fn add_file_id(&mut self, file_id: flatbuffers::WIPOffset<&'b str>) { + self.fbb_.push_slot_always::>(DucDocElement::VT_FILE_ID, file_id); + } + #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>) -> DucDocElementBuilder<'a, 'b, A> { let start = _fbb.start_table(); DucDocElementBuilder { @@ -22344,6 +22651,8 @@ impl core::fmt::Debug for DucDocElement<'_> { ds.field("flow_direction", &self.flow_direction()); ds.field("columns", &self.columns()); ds.field("auto_resize", &self.auto_resize()); + ds.field("grid_config", &self.grid_config()); + ds.field("file_id", &self.file_id()); ds.finish() } } @@ -22592,6 +22901,154 @@ impl core::fmt::Debug for DucParametricElement<'_> { ds.finish() } } +pub enum DucModelElementOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct DucModelElement<'a> { + pub _tab: flatbuffers::Table<'a>, +} + +impl<'a> flatbuffers::Follow<'a> for DucModelElement<'a> { + type Inner = DucModelElement<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { _tab: flatbuffers::Table::new(buf, loc) } + } +} + +impl<'a> DucModelElement<'a> { + pub const VT_BASE: flatbuffers::VOffsetT = 4; + pub const VT_SOURCE: flatbuffers::VOffsetT = 6; + pub const VT_SVG_PATH: flatbuffers::VOffsetT = 8; + pub const VT_FILE_IDS: flatbuffers::VOffsetT = 10; + + #[inline] + pub unsafe fn init_from_table(table: flatbuffers::Table<'a>) -> Self { + DucModelElement { _tab: table } + } + #[allow(unused_mut)] + pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: flatbuffers::Allocator + 'bldr>( + _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args DucModelElementArgs<'args> + ) -> flatbuffers::WIPOffset> { + let mut builder = DucModelElementBuilder::new(_fbb); + if let Some(x) = args.file_ids { builder.add_file_ids(x); } + if let Some(x) = args.svg_path { builder.add_svg_path(x); } + if let Some(x) = args.source { builder.add_source(x); } + if let Some(x) = args.base { builder.add_base(x); } + builder.finish() + } + + + #[inline] + pub fn base(&self) -> Option<_DucElementBase<'a>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::>(DucModelElement::VT_BASE, None)} + } + #[inline] + pub fn source(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::>(DucModelElement::VT_SOURCE, None)} + } + #[inline] + pub fn svg_path(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::>(DucModelElement::VT_SVG_PATH, None)} + } + #[inline] + pub fn file_ids(&self) -> Option>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::>>>(DucModelElement::VT_FILE_IDS, None)} + } +} + +impl flatbuffers::Verifiable for DucModelElement<'_> { + #[inline] + fn run_verifier( + v: &mut flatbuffers::Verifier, pos: usize + ) -> Result<(), flatbuffers::InvalidFlatbuffer> { + use self::flatbuffers::Verifiable; + v.visit_table(pos)? + .visit_field::>("base", Self::VT_BASE, false)? + .visit_field::>("source", Self::VT_SOURCE, false)? + .visit_field::>("svg_path", Self::VT_SVG_PATH, false)? + .visit_field::>>>("file_ids", Self::VT_FILE_IDS, false)? + .finish(); + Ok(()) + } +} +pub struct DucModelElementArgs<'a> { + pub base: Option>>, + pub source: Option>, + pub svg_path: Option>, + pub file_ids: Option>>>, +} +impl<'a> Default for DucModelElementArgs<'a> { + #[inline] + fn default() -> Self { + DucModelElementArgs { + base: None, + source: None, + svg_path: None, + file_ids: None, + } + } +} + +pub struct DucModelElementBuilder<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> { + fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, + start_: flatbuffers::WIPOffset, +} +impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> DucModelElementBuilder<'a, 'b, A> { + #[inline] + pub fn add_base(&mut self, base: flatbuffers::WIPOffset<_DucElementBase<'b >>) { + self.fbb_.push_slot_always::>(DucModelElement::VT_BASE, base); + } + #[inline] + pub fn add_source(&mut self, source: flatbuffers::WIPOffset<&'b str>) { + self.fbb_.push_slot_always::>(DucModelElement::VT_SOURCE, source); + } + #[inline] + pub fn add_svg_path(&mut self, svg_path: flatbuffers::WIPOffset<&'b str>) { + self.fbb_.push_slot_always::>(DucModelElement::VT_SVG_PATH, svg_path); + } + #[inline] + pub fn add_file_ids(&mut self, file_ids: flatbuffers::WIPOffset>>) { + self.fbb_.push_slot_always::>(DucModelElement::VT_FILE_IDS, file_ids); + } + #[inline] + pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>) -> DucModelElementBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + DucModelElementBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + flatbuffers::WIPOffset::new(o.value()) + } +} + +impl core::fmt::Debug for DucModelElement<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut ds = f.debug_struct("DucModelElement"); + ds.field("base", &self.base()); + ds.field("source", &self.source()); + ds.field("svg_path", &self.svg_path()); + ds.field("file_ids", &self.file_ids()); + ds.finish() + } +} pub enum ElementWrapperOffset {} #[derive(Copy, Clone, PartialEq)] @@ -22971,6 +23428,21 @@ impl<'a> ElementWrapper<'a> { } } + #[inline] + #[allow(non_snake_case)] + pub fn element_as_duc_model_element(&self) -> Option> { + if self.element_type() == Element::DucModelElement { + self.element().map(|t| { + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + unsafe { DucModelElement::init_from_table(t) } + }) + } else { + None + } + } + } impl flatbuffers::Verifiable for ElementWrapper<'_> { @@ -23004,6 +23476,7 @@ impl flatbuffers::Verifiable for ElementWrapper<'_> { Element::DucFeatureControlFrameElement => v.verify_union_variant::>("Element::DucFeatureControlFrameElement", pos), Element::DucDocElement => v.verify_union_variant::>("Element::DucDocElement", pos), Element::DucParametricElement => v.verify_union_variant::>("Element::DucParametricElement", pos), + Element::DucModelElement => v.verify_union_variant::>("Element::DucModelElement", pos), _ => Ok(()), } })? @@ -23212,6 +23685,13 @@ impl core::fmt::Debug for ElementWrapper<'_> { ds.field("element", &"InvalidFlatbuffer: Union discriminant does not match value.") } }, + Element::DucModelElement => { + if let Some(x) = self.element_as_duc_model_element() { + ds.field("element", &x) + } else { + ds.field("element", &"InvalidFlatbuffer: Union discriminant does not match value.") + } + }, _ => { let x: Option<()> = None; ds.field("element", &x) diff --git a/packages/ducrs/src/parse.rs b/packages/ducrs/src/parse.rs index f320424..c1b1cfd 100644 --- a/packages/ducrs/src/parse.rs +++ b/packages/ducrs/src/parse.rs @@ -681,9 +681,26 @@ fn parse_duc_embeddable_element(el: fb::DucEmbeddableElement) -> ParseResult ParseResult { + let grid_config = el.grid_config(); Ok(types::DucPdfElement { base: parse_duc_element_base(el.base().ok_or("Missing DucPdfElement.base")?)?, file_id: el.file_id().map(|s| s.to_string()), + grid_config: match grid_config { + Some(gc) => types::DocumentGridConfig { + columns: gc.columns(), + gap_x: gc.gap_x(), + gap_y: gc.gap_y(), + align_items: gc.align_items().map(|a| a.into()).unwrap_or(types::DocumentGridAlignItems::Start), + first_page_alone: gc.first_page_alone(), + }, + None => types::DocumentGridConfig { + columns: 1, + gap_x: 0.0, + gap_y: 0.0, + align_items: types::DocumentGridAlignItems::Start, + first_page_alone: false, + }, + }, }) } @@ -1163,6 +1180,7 @@ fn parse_duc_doc_element(el: fb::DucDocElement) -> ParseResult>>()) .transpose()? .unwrap_or_default(); + let grid_config = el.grid_config(); Ok(types::DucDocElement { base: parse_duc_element_base(el.base().ok_or("Missing DucDocElement.base")?)?, style: parse_duc_doc_style(el.style().ok_or("Missing DucDocElement.style")?)?, @@ -1171,6 +1189,23 @@ fn parse_duc_doc_element(el: fb::DucDocElement) -> ParseResult types::DocumentGridConfig { + columns: gc.columns(), + gap_x: gc.gap_x(), + gap_y: gc.gap_y(), + align_items: gc.align_items().map(|a| a.into()).unwrap_or(types::DocumentGridAlignItems::Start), + first_page_alone: gc.first_page_alone(), + }, + None => types::DocumentGridConfig { + columns: 1, + gap_x: 0.0, + gap_y: 0.0, + align_items: types::DocumentGridAlignItems::Start, + first_page_alone: false, + }, + }, }) } @@ -1189,6 +1224,18 @@ fn parse_duc_parametric_element(el: fb::DucParametricElement) -> ParseResult ParseResult { + let file_ids = el.file_ids() + .map(|v| v.iter().map(|s| s.to_string()).collect::>()) + .unwrap_or_default(); + Ok(types::DucModelElement { + base: parse_duc_element_base(el.base().ok_or("Missing DucModelElement.base")?)?, + source: el.source().ok_or("Missing DucModelElement.source")?.to_string(), + svg_path: el.svg_path().map(|s| s.to_string()), + file_ids, + }) +} + // ============================================================================= // ELEMENT UNION & WRAPPER // ============================================================================= @@ -1279,6 +1326,10 @@ fn parse_element_wrapper(wrapper: fb::ElementWrapper) -> ParseResult { + let el = wrapper.element_as_duc_model_element().ok_or("Mismatched element type")?; + types::DucElementEnum::DucModelElement(parse_duc_model_element(el)?) + }, _ => return Err("Unknown element type in wrapper"), }; Ok(types::ElementWrapper { element: element_enum }) diff --git a/packages/ducrs/src/serialize.rs b/packages/ducrs/src/serialize.rs index 221f58d..2a404ab 100644 --- a/packages/ducrs/src/serialize.rs +++ b/packages/ducrs/src/serialize.rs @@ -1184,11 +1184,13 @@ fn serialize_duc_pdf_element<'bldr>( ) -> WIPOffset> { let base_offset = serialize_duc_element_base(builder, &element.base); let file_id_offset = element.file_id.as_deref().map(|s| builder.create_string(s)); + let grid_config_offset = serialize_document_grid_config(builder, &element.grid_config); fb::DucPdfElement::create( builder, &fb::DucPdfElementArgs { base: Some(base_offset), file_id: file_id_offset, + grid_config: Some(grid_config_offset), }, ) } @@ -2307,6 +2309,22 @@ fn serialize_column_layout<'bldr>( ) } +fn serialize_document_grid_config<'bldr>( + builder: &mut FlatBufferBuilder<'bldr>, + config: &types::DocumentGridConfig, +) -> WIPOffset> { + fb::DocumentGridConfig::create( + builder, + &fb::DocumentGridConfigArgs { + columns: config.columns, + gap_x: config.gap_x, + gap_y: config.gap_y, + align_items: Some(config.align_items.into()), + first_page_alone: config.first_page_alone, + }, + ) +} + fn serialize_duc_doc_element<'bldr>( builder: &mut FlatBufferBuilder<'bldr>, element: &types::DucDocElement, @@ -2323,6 +2341,8 @@ fn serialize_duc_doc_element<'bldr>( flatbuffers::Vector<'bldr, flatbuffers::ForwardsUOffset>>, > = builder.create_vector(&dynamic_offsets); let columns_offset = serialize_column_layout(builder, &element.columns); + let file_id_offset = element.file_id.as_deref().map(|s| builder.create_string(s)); + let grid_config_offset = serialize_document_grid_config(builder, &element.grid_config); fb::DucDocElement::create( builder, @@ -2334,6 +2354,8 @@ fn serialize_duc_doc_element<'bldr>( flow_direction: Some(element.flow_direction), columns: Some(columns_offset), auto_resize: element.auto_resize, + file_id: file_id_offset, + grid_config: Some(grid_config_offset), }, ) } @@ -2369,6 +2391,30 @@ fn serialize_duc_parametric_element<'bldr>( ) } +fn serialize_duc_model_element<'bldr>( + builder: &mut FlatBufferBuilder<'bldr>, + element: &types::DucModelElement, +) -> WIPOffset> { + let base_offset = serialize_duc_element_base(builder, &element.base); + let source_offset = builder.create_string(&element.source); + let svg_path_offset = element.svg_path.as_deref().map(|s| builder.create_string(s)); + let file_id_offsets: Vec<_> = element.file_ids.iter().map(|s| builder.create_string(s)).collect(); + let file_ids_vec = if !file_id_offsets.is_empty() { + Some(builder.create_vector(&file_id_offsets)) + } else { + None + }; + fb::DucModelElement::create( + builder, + &fb::DucModelElementArgs { + base: Some(base_offset), + source: Some(source_offset), + svg_path: svg_path_offset, + file_ids: file_ids_vec, + }, + ) +} + // ============================================================================= // ELEMENT UNION & WRAPPER // ============================================================================= @@ -2462,6 +2508,10 @@ fn serialize_element_wrapper<'bldr>( fb::Element::DucParametricElement, serialize_duc_parametric_element(builder, e).as_union_value(), ), + types::DucElementEnum::DucModelElement(e) => ( + fb::Element::DucModelElement, + serialize_duc_model_element(builder, e).as_union_value(), + ), }; fb::ElementWrapper::create( diff --git a/packages/ducrs/src/types.rs b/packages/ducrs/src/types.rs index aa03c0e..2ca6add 100644 --- a/packages/ducrs/src/types.rs +++ b/packages/ducrs/src/types.rs @@ -1,7 +1,8 @@ use crate::generated::duc::{ ANGULAR_UNITS_FORMAT, AXIS, BEZIER_MIRRORING, BLENDING, BLOCK_ATTACHMENT, BOOLEAN_OPERATION, COLUMN_TYPE, DATUM_BRACKET_STYLE, DECIMAL_SEPARATOR, DIMENSION_FIT_RULE, - DIMENSION_TEXT_PLACEMENT, DIMENSION_TYPE, DIMENSION_UNITS_FORMAT, ELEMENT_CONTENT_PREFERENCE, + DIMENSION_TEXT_PLACEMENT, DIMENSION_TYPE, DIMENSION_UNITS_FORMAT, DOCUMENT_GRID_ALIGN_ITEMS, + ELEMENT_CONTENT_PREFERENCE, FEATURE_MODIFIER, GDT_SYMBOL, GRID_DISPLAY_TYPE, GRID_TYPE, HATCH_STYLE, IMAGE_STATUS, LEADER_CONTENT_TYPE, LINE_HEAD, LINE_SPACING_TYPE, MARK_ELLIPSE_CENTER, MATERIAL_CONDITION, OBJECT_SNAP_MODE, PARAMETRIC_SOURCE_TYPE, PRUNING_LEVEL, SNAP_MARKER_SHAPE, SNAP_MODE, @@ -33,6 +34,7 @@ pub enum ElementType { FeatureControlFrame, Doc, Parametric, + Model, Embeddable, Pdf, Mermaid, @@ -59,6 +61,7 @@ impl ElementType { ElementType::FeatureControlFrame => "featurecontrolframe", ElementType::Doc => "doc", ElementType::Parametric => "parametric", + ElementType::Model => "model", ElementType::Embeddable => "embeddable", ElementType::Pdf => "pdf", ElementType::Mermaid => "mermaid", @@ -87,6 +90,7 @@ pub enum DucElementVariant { FeatureControlFrame(DucFeatureControlFrameElement), Doc(DucDocElement), Parametric(DucParametricElement), + Model(DucModelElement), Embeddable(DucEmbeddableElement), Pdf(DucPdfElement), Mermaid(DucMermaidElement), @@ -113,6 +117,7 @@ impl DucElementVariant { DucElementVariant::FeatureControlFrame(elem) => &elem.base, DucElementVariant::Doc(elem) => &elem.base, DucElementVariant::Parametric(elem) => &elem.base, + DucElementVariant::Model(elem) => &elem.base, DucElementVariant::Embeddable(elem) => &elem.base, DucElementVariant::Pdf(elem) => &elem.base, DucElementVariant::Mermaid(elem) => &elem.base, @@ -587,10 +592,50 @@ pub struct DucEmbeddableElement { pub base: DucElementBase, } +/// Configuration for PDF grid layout +#[derive(Debug, Clone, PartialEq)] +pub struct DocumentGridConfig { + pub columns: i32, + pub gap_x: f64, + pub gap_y: f64, + pub align_items: DocumentGridAlignItems, + pub first_page_alone: bool, +} + +/// Vertical alignment for document grid layout +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DocumentGridAlignItems { + Start, + Center, + End, +} + +impl From for DocumentGridAlignItems { + fn from(value: DOCUMENT_GRID_ALIGN_ITEMS) -> Self { + match value { + DOCUMENT_GRID_ALIGN_ITEMS::START => DocumentGridAlignItems::Start, + DOCUMENT_GRID_ALIGN_ITEMS::CENTER => DocumentGridAlignItems::Center, + DOCUMENT_GRID_ALIGN_ITEMS::END => DocumentGridAlignItems::End, + _ => DocumentGridAlignItems::Start, + } + } +} + +impl From for DOCUMENT_GRID_ALIGN_ITEMS { + fn from(value: DocumentGridAlignItems) -> Self { + match value { + DocumentGridAlignItems::Start => DOCUMENT_GRID_ALIGN_ITEMS::START, + DocumentGridAlignItems::Center => DOCUMENT_GRID_ALIGN_ITEMS::CENTER, + DocumentGridAlignItems::End => DOCUMENT_GRID_ALIGN_ITEMS::END, + } + } +} + #[derive(Debug, Clone, PartialEq)] pub struct DucPdfElement { pub base: DucElementBase, pub file_id: Option, + pub grid_config: DocumentGridConfig, } #[derive(Debug, Clone, PartialEq)] @@ -964,6 +1009,8 @@ pub struct DucDocElement { pub flow_direction: TEXT_FLOW_DIRECTION, pub columns: ColumnLayout, pub auto_resize: bool, + pub file_id: Option, + pub grid_config: DocumentGridConfig, } #[derive(Debug, Clone, PartialEq)] @@ -973,6 +1020,14 @@ pub struct ParametricSource { pub file_id: Option, } +#[derive(Debug, Clone, PartialEq)] +pub struct DucModelElement { + pub base: DucElementBase, + pub source: String, + pub svg_path: Option, + pub file_ids: Vec, +} + #[derive(Debug, Clone, PartialEq)] pub struct DucParametricElement { pub base: DucElementBase, @@ -1004,6 +1059,7 @@ pub enum DucElementEnum { DucFeatureControlFrameElement(DucFeatureControlFrameElement), DucDocElement(DucDocElement), DucParametricElement(DucParametricElement), + DucModelElement(DucModelElement), } #[derive(Debug, Clone, PartialEq)] diff --git a/schema/duc.fbs b/schema/duc.fbs index a24861c..a8c562c 100644 --- a/schema/duc.fbs +++ b/schema/duc.fbs @@ -12,6 +12,7 @@ * - **DONT CHANGE FIELD TYPES**: Modifying a field's type can lead to incompatibilities. If necessary, add a new field with the desired type and depreciate the old one. * - **BE CAREFUL WITH DEFAULT VALUES**: Changing default values can cause inconsistencies when reading older data. It's best to set appropriate defaults initially and avoid altering them. * - **STRING or TABLE CANNOT FALLBACK TO NULL**: these fields cannot be null. They already have a default null value. + * - **DEFINE JSON FIELDS AS UBYTE ARRAY**: On our architecture of the fbs file, we always store JSON as a zlib compressed binary ([ubyte]) to save space and improve performance.** */ namespace Duc; @@ -702,13 +703,18 @@ enum PRUNING_LEVEL:ubyte { AGGRESSIVE = 30 } +// deprecated enum PARAMETRIC_SOURCE_TYPE:ubyte { - /** Parametric element defined by executable code. */ CODE = 10, - /** Parametric element defined by a file. */ FILE = 11 } +enum DOCUMENT_GRID_ALIGN_ITEMS:ubyte { + START = 10, + CENTER = 11, + END = 12 +} + enum LEADER_CONTENT_TYPE:ubyte { /** Leader content is text. */ TEXT = 10, @@ -1436,9 +1442,24 @@ table DucEmbeddableElement { base: _DucElementBase; } +table DocumentGridConfig { + /** 1 = single, 2 = two-up, n = grid */ + columns: int; + /** horizontal spacing (px) */ + gap_x: double; + /** vertical spacing (px) */ + gap_y: double; + /** vertical alignment within row */ + align_items: DOCUMENT_GRID_ALIGN_ITEMS = null; + /** cover page behavior for 2+ columns */ + first_page_alone: bool; +} + table DucPdfElement { base: _DucElementBase; - file_id: string; // ExternalFileId + file_id: string; + /** Configuration for rendering the document in a grid layout */ + grid_config: DocumentGridConfig; } table DucMermaidElement { @@ -2067,10 +2088,8 @@ table DucDocElement { base: _DucElementBase; style: DucDocStyle; /** - * The content of the document, stored as a Markdown string. - * This approach allows a rich text editor to manage the complex - * inline formatting (bold, italic, colors, hyperlinks, etc.) while keeping the - * core data structure simple and clean. + * The content of the document, stored as a code string. + * This approach allows to use a rich text editor that can compile to PDF using Typst code * * It can also contain wildcards like `{@fieldname}` for dynamic data insertion. * Example: "This is **bold text** and this is a {color:red}red word{/color}." @@ -2078,7 +2097,7 @@ table DucDocElement { * It can also contain zero or more placeholders in the format `{{tag}}`. * Example: "This document was last saved on {{SaveDate}} by {{Author}}." */ - text: string; // Markdown + text: string; /** * An array of metadata objects that define the behavior of the placeholders * found in the `text` property. If this is empty, the text is treated @@ -2095,25 +2114,35 @@ table DucDocElement { * - `false`: Text wraps or is clipped to fit the element's fixed bounds. */ auto_resize: bool; + /** Configuration for rendering the document in a grid layout */ + grid_config: DocumentGridConfig; + /** External file reference */ + file_id: string; } +// deprecated table ParametricSource { - /** - * The geometry is defined by executable code. - * The geometry is loaded from a static 3D file. - */ type: PARAMETRIC_SOURCE_TYPE = null; - code: string; // Used if type is CODE - /** A reference to the imported file in the DucExternalFiles collection. */ - file_id: string; // Used if type is FILE + code: string; + file_id: string; } +// deprecated table DucParametricElement { base: _DucElementBase; - /** Defines the source of the 3D geometry (either from code or a file). */ source: ParametricSource; } +table DucModelElement { + base: _DucElementBase; + /** Defines the source of the model using build123d python code */ + source: string; + /** The last known SVG path representation of the 3D model for quick rendering on the canvas */ + svg_path: string; + /** Possibly connected external files, such as STEP, STL or other reference models */ + file_ids: [string]; +} + /** * ============================================================================= * ELEMENT UNION & WRAPPER @@ -2127,7 +2156,7 @@ union Element { DucEllipseElement, DucEmbeddableElement, DucPdfElement, - DucMermaidElement, + DucMermaidElement, // deprecated DucTableElement, DucImageElement, DucTextElement, @@ -2143,7 +2172,8 @@ union Element { DucDimensionElement, DucFeatureControlFrameElement, DucDocElement, - DucParametricElement + DucParametricElement, // deprecated + DucModelElement } /** A wrapper to hold an element from the union. */ From 2c7af18ec284a7b6be558880ba02393e3ace6fc2 Mon Sep 17 00:00:00 2001 From: Jorge Soares Date: Mon, 9 Feb 2026 22:32:06 +0000 Subject: [PATCH 4/4] fix: Add DucModelElement support and remove BlockManager from PDF generation. --- packages/ducjs/src/types/index.ts | 23 ++++++----- packages/ducpdf/src/duc2pdf/src/builder.rs | 12 +++--- packages/ducpdf/src/duc2pdf/src/lib.rs | 1 + packages/ducpdf/src/duc2pdf/src/scaling.rs | 3 ++ .../src/duc2pdf/src/streaming/pdf_linear.rs | 4 +- .../duc2pdf/src/streaming/stream_elements.rs | 28 +++++++------- .../duc2pdf/src/streaming/stream_resources.rs | 38 ++++--------------- .../src/duc2pdf/src/utils/style_resolver.rs | 17 --------- 8 files changed, 44 insertions(+), 82 deletions(-) diff --git a/packages/ducjs/src/types/index.ts b/packages/ducjs/src/types/index.ts index 6cb08a2..694a4c8 100644 --- a/packages/ducjs/src/types/index.ts +++ b/packages/ducjs/src/types/index.ts @@ -1,11 +1,18 @@ export * from "./elements"; export * from "./geometryTypes"; -export * from "./utility-types"; export * from "./typeChecks"; +export * from "./utility-types"; import { OBJECT_SNAP_MODE, PRUNING_LEVEL } from "../flatbuffers/duc"; import { SupportedMeasures } from "../technical/scopes"; import { Standard } from "../technical/standards"; +import type { + GRID_DISPLAY_TYPE, + GRID_TYPE, + SNAP_MARKER_SHAPE, + SNAP_MODE, + SNAP_OVERRIDE_BEHAVIOR +} from "../utils/constants"; import { DucBindableElement, DucBlock, @@ -35,16 +42,6 @@ import { ScaleFactor, } from "./geometryTypes"; import { MakeBrand, MarkOptional, MaybePromise, ValueOf } from "./utility-types"; -import type { - GRID_DISPLAY_TYPE, - GRID_TYPE, - IMAGE_MIME_TYPES, - MIME_TYPES, - SUPPORTED_DATA_TYPES, - SNAP_MARKER_SHAPE, - SNAP_MODE, - SNAP_OVERRIDE_BEHAVIOR, -} from "../utils/constants"; /** * Root data structure for the stored data state @@ -192,7 +189,9 @@ export type ToolType = | "ruler" | "lasso" | "laser" - | "table"; + | "table" + | "doc" + | "pdf"; export type ElementOrToolType = DucElementType | ToolType | "custom"; diff --git a/packages/ducpdf/src/duc2pdf/src/builder.rs b/packages/ducpdf/src/duc2pdf/src/builder.rs index 4e76ab0..33a367f 100644 --- a/packages/ducpdf/src/duc2pdf/src/builder.rs +++ b/packages/ducpdf/src/duc2pdf/src/builder.rs @@ -14,7 +14,7 @@ use bigcolor::BigColor; use duc::types::{ DucBlock, DucElementEnum, DucExternalFileEntry, ElementWrapper, ExportedDataState, Standard, }; -use hipdf::blocks::BlockManager; + use hipdf::embed_pdf::PdfEmbedder; use hipdf::fonts::{Font, FontManager, StandardFont}; use hipdf::hatching::HatchingManager; @@ -22,7 +22,7 @@ use hipdf::images::{Image, ImageManager}; use hipdf::lopdf::content::{Content, Operation}; use hipdf::lopdf::{Dictionary, Document, Object, Stream}; use hipdf::ocg::OCGManager; -use web_sys::console; + use std::collections::{HashMap, HashSet}; // Logging utilities that work in both WASM and native environments @@ -77,7 +77,7 @@ pub struct DucToPdfBuilder { context: ConversionContext, document: Document, ocg_manager: OCGManager, - block_manager: BlockManager, + hatching_manager: HatchingManager, pdf_embedder: PdfEmbedder, image_manager: ImageManager, @@ -190,7 +190,7 @@ impl DucToPdfBuilder { context, document, ocg_manager: OCGManager::new(), - block_manager: BlockManager::new(), + hatching_manager: HatchingManager::new(), pdf_embedder: PdfEmbedder::new(), image_manager: ImageManager::new(), @@ -307,6 +307,7 @@ impl DucToPdfBuilder { DucElementEnum::DucFeatureControlFrameElement(elem) => &elem.base, DucElementEnum::DucDocElement(elem) => &elem.base, DucElementEnum::DucParametricElement(elem) => &elem.base, + DucElementEnum::DucModelElement(elem) => &elem.base, } } @@ -1428,7 +1429,6 @@ fn create_content_stream( bounds, self.context.exported_data.duc_local_state.as_ref(), &mut self.resource_streamer, - &mut self.block_manager, &mut self.hatching_manager, &mut self.pdf_embedder, &mut self.image_manager, @@ -1535,7 +1535,6 @@ fn create_content_stream( bounds, self.context.exported_data.duc_local_state.as_ref(), &mut self.resource_streamer, - &mut self.block_manager, &mut self.hatching_manager, &mut self.pdf_embedder, &mut self.image_manager, @@ -1638,7 +1637,6 @@ fn create_content_stream( bounds, self.context.exported_data.duc_local_state.as_ref(), &mut self.resource_streamer, - &mut self.block_manager, &mut self.hatching_manager, &mut self.pdf_embedder, &mut self.image_manager, diff --git a/packages/ducpdf/src/duc2pdf/src/lib.rs b/packages/ducpdf/src/duc2pdf/src/lib.rs index 25d7cb3..cf9e54f 100644 --- a/packages/ducpdf/src/duc2pdf/src/lib.rs +++ b/packages/ducpdf/src/duc2pdf/src/lib.rs @@ -166,6 +166,7 @@ pub fn calculate_bounding_box(data: &duc::types::ExportedDataState) -> (f64, f64 duc::types::DucElementEnum::DucFeatureControlFrameElement(elem) => &elem.base, duc::types::DucElementEnum::DucDocElement(elem) => &elem.base, duc::types::DucElementEnum::DucParametricElement(elem) => &elem.base, + duc::types::DucElementEnum::DucModelElement(elem) => &elem.base, }; // Assume all coordinates are already in millimeters diff --git a/packages/ducpdf/src/duc2pdf/src/scaling.rs b/packages/ducpdf/src/duc2pdf/src/scaling.rs index 76d8138..f917a64 100644 --- a/packages/ducpdf/src/duc2pdf/src/scaling.rs +++ b/packages/ducpdf/src/duc2pdf/src/scaling.rs @@ -105,6 +105,9 @@ impl DucDataScaler { types::DucElementEnum::DucMermaidElement(mermaid) => { Self::scale_element_base(&mut mermaid.base, scale); } + types::DucElementEnum::DucModelElement(model) => { + Self::scale_element_base(&mut model.base, scale); + } } } diff --git a/packages/ducpdf/src/duc2pdf/src/streaming/pdf_linear.rs b/packages/ducpdf/src/duc2pdf/src/streaming/pdf_linear.rs index d69ecbb..5efa50b 100644 --- a/packages/ducpdf/src/duc2pdf/src/streaming/pdf_linear.rs +++ b/packages/ducpdf/src/duc2pdf/src/streaming/pdf_linear.rs @@ -325,7 +325,7 @@ impl PdfLinearRenderer { ) -> ConversionResult> { let mut ops = Vec::new(); - let (p0_x, p0_y) = Self::transform_point_to_pdf(start_point); + let (p3_x, p3_y) = Self::transform_point_to_pdf(end_point); match (start_handle, end_handle) { @@ -362,7 +362,7 @@ impl PdfLinearRenderer { // Quadratic curve (single handle) - convert to cubic _ if start_handle.is_some() || end_handle.is_some() => { if let Some(h) = start_handle.as_ref().or(end_handle.as_ref()) { - let (c_x, c_y) = Self::transform_handle_to_pdf(h); + // Convert quadratic to cubic using original coordinates, then transform the result. // cp1 = p0 + 2/3 * (c - p0) diff --git a/packages/ducpdf/src/duc2pdf/src/streaming/stream_elements.rs b/packages/ducpdf/src/duc2pdf/src/streaming/stream_elements.rs index eabe3d5..3084c7b 100644 --- a/packages/ducpdf/src/duc2pdf/src/streaming/stream_elements.rs +++ b/packages/ducpdf/src/duc2pdf/src/streaming/stream_elements.rs @@ -26,7 +26,7 @@ use crate::scaling::DucDataScaler; use crate::streaming::pdf_linear::PdfLinearRenderer; use crate::streaming::stream_resources::ResourceStreamer; -use crate::utils::freedraw_bounds::{calculate_freedraw_bbox, FreeDrawBounds}; +use crate::utils::freedraw_bounds::FreeDrawBounds; use crate::utils::style_resolver::{ResolvedStyles, StyleResolver}; use crate::{ConversionError, ConversionResult}; use bigcolor::BigColor; @@ -38,7 +38,7 @@ use duc::types::{ DucPolygonElement, DucRectangleElement, DucTableElement, DucTextElement, ElementBackground, ElementContentBase, ElementWrapper, GeometricPoint, DucBlockInstance, DucBlockDuplicationArray, }; -use hipdf::blocks::BlockManager; + use hipdf::embed_pdf::PdfEmbedder; use hipdf::fonts::Font; use hipdf::hatching::HatchingManager; @@ -156,7 +156,7 @@ impl ElementStreamer { element_height: f64, ) -> Vec<(f64, f64)> { if duplication_array.row_spacing.is_nan() || duplication_array.col_spacing.is_nan() { - #[cfg(feature = "verbose_logs")] + log::warn!( "Duplication array has NaN spacing! row_spacing: {}, col_spacing: {}", duplication_array.row_spacing, @@ -222,6 +222,7 @@ impl ElementStreamer { DucElementEnum::DucParametricElement(p) => (p.base.width, p.base.height), DucElementEnum::DucPdfElement(p) => (p.base.width, p.base.height), DucElementEnum::DucMermaidElement(m) => (m.base.width, m.base.height), + DucElementEnum::DucModelElement(m) => (m.base.width, m.base.height), }; return Some(Self::get_duplication_offsets(dup_array, width, height)); @@ -320,7 +321,7 @@ impl ElementStreamer { bounds: (f64, f64, f64, f64), local_state: Option<&duc::types::DucLocalState>, resource_streamer: &mut ResourceStreamer, - block_manager: &mut BlockManager, + hatching_manager: &mut HatchingManager, pdf_embedder: &mut PdfEmbedder, image_manager: &mut ImageManager, @@ -329,14 +330,9 @@ impl ElementStreamer { ) -> ConversionResult> { let mut all_operations = Vec::new(); let is_plot_mode = matches!(self.current_mode, StreamMode::Plot); - let (bounds_x, bounds_y, bounds_width, bounds_height) = bounds; - let bounds_max_x = bounds_x + bounds_width; - let bounds_max_y = bounds_y + bounds_height; - let (scroll_x, scroll_y) = if let Some(state) = local_state { - (state.scroll_x, state.scroll_y) - } else { - (0.0, 0.0) - }; + let (_bounds_x, _bounds_y, _bounds_width, _bounds_height) = bounds; + + // Filter and sort elements by z-index and visibility criteria let mut filtered_elements: Vec<_> = elements @@ -427,7 +423,6 @@ impl ElementStreamer { all_elements, document, resource_streamer, - block_manager, hatching_manager, pdf_embedder, image_manager, @@ -458,7 +453,7 @@ impl ElementStreamer { all_elements: &[ElementWrapper], document: &mut Document, resource_streamer: &mut ResourceStreamer, - block_manager: &mut BlockManager, + hatching_manager: &mut HatchingManager, pdf_embedder: &mut PdfEmbedder, image_manager: &mut ImageManager, @@ -597,6 +592,9 @@ impl ElementStreamer { DucElementEnum::DucParametricElement(_) => { vec![Operation::new("% DucParametricElement - WIP", vec![])] } + DucElementEnum::DucModelElement(_) => { + vec![Operation::new("% DucModelElement - WIP", vec![])] + } }; operations.extend(element_ops); @@ -630,6 +628,7 @@ impl ElementStreamer { DucElementEnum::DucFeatureControlFrameElement(elem) => &elem.base, DucElementEnum::DucDocElement(elem) => &elem.base, DucElementEnum::DucParametricElement(elem) => &elem.base, + DucElementEnum::DucModelElement(elem) => &elem.base, } } @@ -1478,6 +1477,7 @@ impl ElementStreamer { fill_from_stroke: false, apply_stroke_properties: false, }, + &DucElementEnum::DucModelElement(_) => todo!(), } } diff --git a/packages/ducpdf/src/duc2pdf/src/streaming/stream_resources.rs b/packages/ducpdf/src/duc2pdf/src/streaming/stream_resources.rs index 9691768..b8e344c 100644 --- a/packages/ducpdf/src/duc2pdf/src/streaming/stream_resources.rs +++ b/packages/ducpdf/src/duc2pdf/src/streaming/stream_resources.rs @@ -56,8 +56,7 @@ pub struct ResourceStreamer { resource_cache: HashMap, /// PDF document reference for embedding document: Option, - /// SVG to PDF converter - svg_converter: SvgToPdfConverter, + /// PDF embedder pdf_embedder: Option, /// Block manager for reusable content @@ -74,7 +73,7 @@ impl ResourceStreamer { Self { resource_cache: HashMap::new(), document: None, - svg_converter: SvgToPdfConverter, + pdf_embedder: None, block_manager: None, hatching_manager: None, @@ -153,7 +152,7 @@ impl ResourceStreamer { let xobject_id = SvgToPdfConverter::convert_file_entry_to_xobject(document, file_entry)?; // Get SVG dimensions from the converted XObject - let (width, height) = self.get_svg_dimensions(file_entry); + let (width, height) = (100.0, 100.0); Ok(ResourceInfo { id: file_entry.key.clone(), @@ -207,9 +206,7 @@ impl ResourceStreamer { // Load PDF data let pdf_data = &file_entry.value.data; - let pdf_doc = Document::load_from(std::io::Cursor::new(pdf_data)).map_err(|e| { - ConversionError::ResourceLoadError(format!("Failed to load PDF: {}", e)) - })?; + // Embed PDF using hipdf let embed_id = format!("pdf_{}", file_entry.key); @@ -220,7 +217,7 @@ impl ResourceStreamer { })?; // Get PDF dimensions from first page - let (width, height) = self.get_pdf_dimensions(&pdf_doc)?; + let (width, height) = (100.0, 100.0); Ok(ResourceInfo { id: file_entry.key.clone(), @@ -285,30 +282,11 @@ impl ResourceStreamer { Ok(object_id) } - /// Get SVG dimensions - fn get_svg_dimensions(&self, _file_entry: &DucExternalFileEntry) -> (f64, f64) { - // Parse SVG to get dimensions - // For now, return default dimensions - (100.0, 100.0) - } - /// Get image dimensions from raw data - fn get_image_dimensions( - &self, - _image_data: &[u8], - _resource_type: &ResourceType, - ) -> ConversionResult<(f64, f64)> { - // Parse image to get dimensions - // For now, return default dimensions - Ok((100.0, 100.0)) - } - /// Get PDF dimensions from first page - fn get_pdf_dimensions(&self, _pdf_doc: &Document) -> ConversionResult<(f64, f64)> { - // Parse PDF to get page dimensions - // For now, return default dimensions - Ok((100.0, 100.0)) - } + + + /// Stream SVG resource as PDF operations pub fn stream_svg_resource( diff --git a/packages/ducpdf/src/duc2pdf/src/utils/style_resolver.rs b/packages/ducpdf/src/duc2pdf/src/utils/style_resolver.rs index 8ad640c..028a093 100644 --- a/packages/ducpdf/src/duc2pdf/src/utils/style_resolver.rs +++ b/packages/ducpdf/src/duc2pdf/src/utils/style_resolver.rs @@ -199,24 +199,7 @@ impl ResolvedStyles { } } - /// Apply element styles base (helper method) - fn apply_element_styles_base(&mut self, styles: &DucElementStylesBase) { - // Apply opacity and roundness - self.opacity = styles.opacity; - self.roundness = styles.roundness; - - // Merge backgrounds (later ones override earlier ones) - for bg in &styles.background { - self.background - .push(ResolvedBackground::from_element_background(bg)); - } - // Merge strokes - for stroke in &styles.stroke { - self.stroke - .push(ResolvedStroke::from_element_stroke(stroke)); - } - } /// Resolve dynamic fields in text elements with comprehensive field support pub fn resolve_dynamic_fields(&self, text: &str, element: &DucElementEnum) -> String {