diff --git a/packages/ducpy/docs/conf.py b/packages/ducpy/docs/conf.py
index 1d9f49a..016ff78 100644
--- a/packages/ducpy/docs/conf.py
+++ b/packages/ducpy/docs/conf.py
@@ -26,6 +26,7 @@
html_theme = "furo"
html_static_path = ['_static']
+html_extra_path = ['extra']
html_baseurl = "https://ducflair.github.io/duc/reference/python/"
extensions.append('autoapi.extension')
diff --git a/packages/ducpy/docs/examples.rst b/packages/ducpy/docs/examples.rst
index 6cb104e..870cb19 100644
--- a/packages/ducpy/docs/examples.rst
+++ b/packages/ducpy/docs/examples.rst
@@ -49,3 +49,25 @@ anything beyond what the high-level builders expose.
.. literalinclude:: ../src/examples/sql_builder_demo.py
:language: python
:linenos:
+
+----
+
+Serialization
+-------------
+
+Demonstrates how to serialize builder-created elements directly to a `.duc` file using `duc.serialize_duc`.
+
+.. literalinclude:: ../src/examples/serialization_demo.py
+ :language: python
+ :linenos:
+
+----
+
+Parsing
+-------
+
+Demonstrates how to parse a `.duc` file or raw binary bytes using `duc.parse_duc`, allowing attribute-style access to the document's content.
+
+.. literalinclude:: ../src/examples/parsing_demo.py
+ :language: python
+ :linenos:
diff --git a/packages/ducpy/docs/extra/context7.json b/packages/ducpy/docs/extra/context7.json
new file mode 100644
index 0000000..c9e96d2
--- /dev/null
+++ b/packages/ducpy/docs/extra/context7.json
@@ -0,0 +1,4 @@
+{
+ "url": "https://context7.com/websites/ducflair_github_io_duc",
+ "public_key": "pk_rW7pJ4T4uki1BAr4guCs1"
+}
\ No newline at end of file
diff --git a/packages/ducpy/docs/index.rst b/packages/ducpy/docs/index.rst
index ef0278e..017b064 100644
--- a/packages/ducpy/docs/index.rst
+++ b/packages/ducpy/docs/index.rst
@@ -16,6 +16,29 @@ ducpy
+Overview
+--------
+
+**Builders API (High-level)**
+ The easy way to build, manage ``.duc`` files.
+ Construct elements, apply styles, manage layers, build blocks,
+ and handle document state with the
+ :doc:`builders ` module.
+
+**SQL Builder (Low-level)**
+ A ``.duc`` file is a zlib-compressed SQLite database. Use the
+ :doc:`sql_builder `
+ for direct schema access, bulk queries, and low-level manipulation.
+
+**Search**
+ Query/search elements and files programmatically via the
+ :doc:`search ` API.
+
+**File I/O**
+ Read and write ``.duc`` files using the
+ :doc:`parse ` and
+ :doc:`serialize ` modules.
+
.. toctree::
:maxdepth: 2
:caption: Contents:
diff --git a/packages/ducpy/src/ducpy/__init__.py b/packages/ducpy/src/ducpy/__init__.py
index 8b24000..abcb04d 100644
--- a/packages/ducpy/src/ducpy/__init__.py
+++ b/packages/ducpy/src/ducpy/__init__.py
@@ -1,12 +1,25 @@
-"""Main module for duc_py package.
+"""Python library for the DUC 2D CAD file format.
-Import usage examples:
- import ducpy as duc
-
- duc.classes.SomeClass
- duc.parse_duc(source)
- duc.serialize_duc(name=..., elements=...)
- duc.utils.some_utility
+Usage::
+ ``import ducpy as duc``
+
+Builders API (High-level):
+ The easy way to build, manage ``.duc`` files.
+ Construct elements, apply styles, manage layers, build blocks,
+ and handle document state with the ``duc.builders`` module.
+
+SQL Builder (Low-level):
+ A ``.duc`` file is a zlib-compressed SQLite database. Use
+ ``duc.builders.sql_builder`` for direct schema access, bulk
+ queries, and low-level manipulation.
+
+Search:
+ Query/search elements and files programmatically via the
+ ``duc.search`` API.
+
+File I/O:
+ Read and write ``.duc`` files using the ``duc.parse``
+ and ``duc.serialize`` modules.
"""
from .builders import *
diff --git a/packages/ducpy/src/ducpy/parse.py b/packages/ducpy/src/ducpy/parse.py
index 7342389..a6a24f8 100644
--- a/packages/ducpy/src/ducpy/parse.py
+++ b/packages/ducpy/src/ducpy/parse.py
@@ -57,16 +57,29 @@ def _read_bytes(source: Union[bytes, bytearray, BinaryIO, str]) -> bytes:
def parse_duc(source: Union[bytes, bytearray, BinaryIO, str]) -> DucData:
"""Parse a ``.duc`` file into a :class:`DucData` dict.
+ This function reads a raw `.duc` binary blob or file path and parses it using
+ the Rust native extension. It returns a specialized dictionary (`DucData`)
+ that allows attribute-style access to the parsed properties (e.g. `data.elements[0].id`),
+ using `snake_case` keys instead of the internal `camelCase` format.
+
Parameters
----------
source : bytes | file | str
- Raw bytes, an open binary file, or a path to a ``.duc`` file.
+ Raw bytes, an open binary file, or a string path to a ``.duc`` file.
Returns
-------
DucData
- Attribute-accessible dict matching the ExportedDataState schema with
- snake_case keys.
+ An attribute-accessible dictionary matching the internal `ExportedDataState`
+ schema with snake_case keys. Common keys include `elements`, `global_state`,
+ `local_state`, and `version_graph`.
+
+ Examples
+ --------
+ >>> data = duc.parse_duc("path/to/file.duc")
+ >>> data = duc.parse_duc(binary_data)
+ >>> print(f"Found {len(data.elements)} elements")
+ >>> print(f"First element type: {data.elements[0].type}")
"""
buf = _read_bytes(source)
raw = ducpy_native.parse_duc(buf)
diff --git a/packages/ducpy/src/ducpy/serialize.py b/packages/ducpy/src/ducpy/serialize.py
index 18f3a17..c6908f3 100644
--- a/packages/ducpy/src/ducpy/serialize.py
+++ b/packages/ducpy/src/ducpy/serialize.py
@@ -216,9 +216,48 @@ def serialize_duc(
layers: Optional[list] = None,
external_files: Optional[list] = None,
) -> bytes:
- """Serialize elements to ``.duc`` format.
- Element instances and state dataclasses are automatically converted to the
- camelCase dicts expected by the Rust native module.
+ """Serialize elements and document state to raw ``.duc`` binary format.
+
+ This function accepts lists of elements created via the `ducpy.builders` API
+ (e.g., `ElementBuilder`) and serializes them into the compressed format
+ expected by `.duc` files. Element instances and state dataclasses are
+ automatically converted to the camelCase dicts expected by the Rust native module.
+
+ Parameters
+ ----------
+ name : str
+ The document name or identifier (used to populate the `source` field).
+ thumbnail : Optional[bytes], default=None
+ Raw PNG bytes representing a thumbnail of the document.
+ dictionary : Optional[list], default=None
+ List of Key-Value string pairs for dictionary entries.
+ elements : Optional[list], default=None
+ A list of elements (e.g., created via `ElementBuilder`) to include.
+ duc_local_state : Any, default=None
+ A `DucLocalState` object representing viewport state (pan, zoom, etc).
+ duc_global_state : Any, default=None
+ A `DucGlobalState` object representing document-wide settings.
+ version_graph : Any, default=None
+ Version history metadata of the document.
+ blocks : Optional[list], default=None
+ List of block definitions.
+ block_instances : Optional[list], default=None
+ List of block instances.
+ block_collections : Optional[list], default=None
+ List of block collections (libraries).
+ groups : Optional[list], default=None
+ List of element groups.
+ regions : Optional[list], default=None
+ List of boolean regions.
+ layers : Optional[list], default=None
+ List of document layers.
+ external_files : Optional[list], default=None
+ List of external files (e.g., embedded images or PDFs).
+
+ Returns
+ -------
+ bytes
+ The raw `.duc` binary data, ready to be written to a file.
"""
thumb = bytes(thumbnail) if thumbnail is not None else None
diff --git a/packages/ducpy/src/examples/parsing_demo.py b/packages/ducpy/src/examples/parsing_demo.py
new file mode 100644
index 0000000..f93bb77
--- /dev/null
+++ b/packages/ducpy/src/examples/parsing_demo.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+"""
+Example demonstrating how to parse a .duc file using the parsing API.
+
+This demo shows how to read a `.duc` binary blob or file path and access
+the parsed data using attribute-style access via DucData.
+"""
+
+import os
+import tempfile
+import ducpy as duc
+
+def main():
+ print("Parsing Demo")
+ print("=" * 30)
+
+ # First, let's create a temporary .duc file to parse
+ from ducpy.builders.style_builders import create_fill_and_stroke_style, create_solid_content
+ elements = [
+ duc.ElementBuilder()
+ .at_position(10, 20)
+ .with_size(100, 50)
+ .with_label("Parsed Rectangle")
+ .with_styles(create_fill_and_stroke_style(
+ fill_content=create_solid_content("#FF6B6B"),
+ stroke_content=create_solid_content("#2C3E50"),
+ stroke_width=2.0
+ ))
+ .build_rectangle()
+ .build()
+ ]
+ duc_bytes = duc.serialize_duc(name="parsing_example", elements=elements)
+
+ with tempfile.NamedTemporaryFile(suffix=".duc", delete=False) as tmp:
+ tmp.write(duc_bytes)
+ tmp_path = tmp.name
+
+ print("1. Parsing a .duc file from a file path...")
+
+ # You can pass a string path directly to parse_duc
+ parsed_data = duc.parse_duc(tmp_path)
+
+ print(f" Document Source: {parsed_data.source}")
+ print(f" Parsed {len(parsed_data.elements)} elements.")
+
+ print("\n2. Accessing element attributes (snake_case)...")
+
+ # Element properties are accessible via dot-notation with snake_case keys
+ # because parse_duc returns a DucData object.
+ first_element = parsed_data.elements[0]
+ print(f" Element ID: {first_element.id}")
+ print(f" Element Type: {first_element.type}")
+ print(f" Element Label: {first_element.label}")
+ print(f" Element Position: (X: {first_element.x}, Y: {first_element.y})")
+
+ print("\n3. Parsing directly from raw bytes...")
+
+ # You can also pass raw bytes directly to parse_duc
+ parsed_from_bytes = duc.parse_duc(duc_bytes)
+ print(f" Parsed successfully from bytes. Found {len(parsed_from_bytes.elements)} elements.")
+
+ # Clean up the temporary file
+ os.unlink(tmp_path)
+
+ print("\n✅ Parsing demo complete!")
+
+if __name__ == "__main__":
+ main()
diff --git a/packages/ducpy/src/examples/serialization_demo.py b/packages/ducpy/src/examples/serialization_demo.py
new file mode 100644
index 0000000..226c3b1
--- /dev/null
+++ b/packages/ducpy/src/examples/serialization_demo.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+"""
+Example demonstrating how to serialize elements created by the Builder API into a .duc file.
+
+This demo shows the correct pattern for taking in-memory python elements
+and writing them to a raw `.duc` binary blob.
+"""
+
+import os
+import ducpy as duc
+from ducpy.builders.style_builders import create_fill_and_stroke_style, create_solid_content
+
+def main():
+ print("Serialization Demo")
+ print("=" * 30)
+
+ print("1. Creating elements via Builder API...")
+ elements = []
+
+ # Create some basic elements
+ rect = (duc.ElementBuilder()
+ .at_position(0, 0)
+ .with_size(100, 50)
+ .with_label("Sample Rectangle")
+ .with_styles(create_fill_and_stroke_style(
+ fill_content=create_solid_content("#FF6B6B"),
+ stroke_content=create_solid_content("#2C3E50"),
+ stroke_width=2.0
+ ))
+ .build_rectangle()
+ .build())
+ elements.append(rect)
+
+ ellipse = (duc.ElementBuilder()
+ .at_position(120, 0)
+ .with_size(60, 40)
+ .with_label("Sample Ellipse")
+ .with_styles(create_fill_and_stroke_style(
+ fill_content=create_solid_content("#4ECDC4"),
+ stroke_content=create_solid_content("#34495E"),
+ stroke_width=1.5
+ ))
+ .build_ellipse()
+ .build())
+ elements.append(ellipse)
+
+ print(f" Created {len(elements)} elements.")
+
+ print("2. Serializing to .duc format...")
+ # NOTE: The serialize_duc function takes keyword arguments for elements,
+ # blocks, global state, etc. and bridges to the Rust native backend.
+ duc_bytes = duc.serialize_duc(
+ name="serialization_example",
+ elements=elements
+ )
+
+ print(f" Successfully serialized {len(duc_bytes)} bytes.")
+
+ # You would typically write this to a file
+ # output_file = "example.duc"
+ # with open(output_file, "wb") as f:
+ # f.write(duc_bytes)
+ # print(f"3. Saved to {output_file}")
+
+ print("\n✅ Serialization demo complete!")
+
+if __name__ == "__main__":
+ main()
diff --git a/packages/ducpy/src/tests/src/test_empire_state_floor_plan.py b/packages/ducpy/src/tests/src/test_empire_state_floor_plan.py
new file mode 100644
index 0000000..157e49a
--- /dev/null
+++ b/packages/ducpy/src/tests/src/test_empire_state_floor_plan.py
@@ -0,0 +1,89 @@
+import os
+import ducpy as duc
+import ducpy.serialize
+from ducpy.builders.style_builders import create_fill_and_stroke_style, create_simple_styles, create_solid_content
+from ducpy.classes.DataStateClass import ExportedDataState
+
+def test_empire_state_floor_plan(test_output_dir):
+ elements = []
+
+ def create_wall(x1, y1, x2, y2, label=""):
+ return (duc.ElementBuilder()
+ .with_label(label)
+ .with_styles(create_simple_styles(
+ strokes=[duc.create_stroke(create_solid_content("#000000"), width=2.0)]
+ ))
+ .build_linear_element()
+ .with_points([(x1, y1), (x2, y2)])
+ .build())
+
+ def create_rect(x, y, w, h, fill_color="#CCCCCC", stroke_color="#000000", label=""):
+ return (duc.ElementBuilder()
+ .at_position(x, y)
+ .with_size(w, h)
+ .with_label(label)
+ .with_styles(create_fill_and_stroke_style(
+ fill_content=create_solid_content(fill_color),
+ stroke_content=create_solid_content(stroke_color),
+ stroke_width=1.5
+ ))
+ .build_rectangle()
+ .build())
+
+ floor_outline_points = [
+ (-40, -50), (-40, -97), (40, -97), (40, -50),
+ (50, -40), (100, -40), (100, 40), (50, 40),
+ (40, 50), (40, 97), (-40, 97), (-40, 50),
+ (-50, 40), (-100, 40), (-100, -40), (-50, -40),
+ (-40, -50),
+ ]
+
+ for i in range(len(floor_outline_points) - 1):
+ p1 = floor_outline_points[i]
+ p2 = floor_outline_points[i + 1]
+ elements.append(create_wall(p1[0], p1[1], p2[0], p2[1]))
+
+ elements.append(create_rect(-35, -25, 30, 50, "#E0E0E0", "#000000", "West Elevator Core"))
+ elements.append(create_rect(5, -25, 30, 50, "#E0E0E0", "#000000", "East Elevator Core"))
+ elements.append(create_rect(-15, -45, 30, 15, "#D0D0D0", "#000000", "North Elevator Bank"))
+ elements.append(create_rect(-15, 30, 30, 15, "#D0D0D0", "#000000", "South Elevator Bank"))
+
+ for x, y, w, h, label in [(-40, -30, 5, 10, "Stair NW"), (-40, 20, 5, 10, "Stair SW"), (35, -30, 5, 10, "Stair NE"), (35, 20, 5, 10, "Stair SE")]:
+ elements.append(create_rect(x, y, w, h, "#C0C0C0", "#000000", label))
+
+ column_size = 3
+ for x in [-30, -15, 0, 15, 30]:
+ elements.append(create_rect(x - column_size/2, -70 - column_size/2, column_size, column_size, "#808080", "#000000"))
+ elements.append(create_rect(x - column_size/2, 70 - column_size/2, column_size, column_size, "#808080", "#000000"))
+ for y in [-30, -15, 0, 15, 30]:
+ elements.append(create_rect(-70 - column_size/2, y - column_size/2, column_size, column_size, "#808080", "#000000"))
+ elements.append(create_rect(70 - column_size/2, y - column_size/2, column_size, column_size, "#808080", "#000000"))
+
+ for x1, y1, x2, y2, label in [(-50, -25, -5, -25, "Corridor N"), (-50, 25, -5, 25, "Corridor S"), (5, -25, 50, -25, "Corridor N"), (5, 25, 50, 25, "Corridor S"), (-35, -50, -35, -30, "Wall W1"), (-35, 30, -35, 50, "Wall W2"), (35, -50, 35, -30, "Wall E1"), (35, 30, 35, 50, "Wall E2")]:
+ elements.append(create_wall(x1, y1, x2, y2, label))
+
+ title_text = (duc.ElementBuilder()
+ .at_position(-50, 110)
+ .with_size(100, 15)
+ .with_label("Title")
+ .with_styles(create_simple_styles(opacity=1.0))
+ .build_text_element()
+ .with_text("EMPIRE STATE BUILDING - TYPICAL FLOOR PLAN (6th-20th Floors)")
+ .build())
+ elements.append(title_text)
+
+ global_state = (duc.StateBuilder().build_global_state().with_main_scope("ft").build())
+ local_state = (duc.StateBuilder().build_local_state().build())
+
+ serialized_bytes = ducpy.serialize.serialize_duc(
+ name="empire_state_floor_plan",
+ elements=elements
+ )
+
+ output_file_path = os.path.join(test_output_dir, "test_empire_state_floor_plan.duc")
+ with open(output_file_path, "wb") as f:
+ f.write(serialized_bytes)
+
+ assert os.path.exists(output_file_path)
+ assert os.path.getsize(output_file_path) > 0
+ print(f"Empire State floor plan created successfully at {output_file_path}")
diff --git a/packages/ducpy/src/tests/src/test_examples.py b/packages/ducpy/src/tests/src/test_examples.py
index 1eda21c..59603f7 100644
--- a/packages/ducpy/src/tests/src/test_examples.py
+++ b/packages/ducpy/src/tests/src/test_examples.py
@@ -18,6 +18,8 @@
import external_files_demo
import mutation_demo
import sql_builder_demo
+import serialization_demo
+import parsing_demo
class TestElementCreationDemo:
@@ -97,6 +99,40 @@ def test_sql_builder_demo_runs_successfully(self):
assert "All DucSQL demos completed successfully!" in output_text
+class TestSerializationDemo:
+ """Test the serialization demo."""
+
+ def test_serialization_demo_runs_successfully(self):
+ """Test that the serialization demo runs without errors."""
+ output = StringIO()
+ with redirect_stdout(output):
+ serialization_demo.main()
+
+ output_text = output.getvalue()
+ assert "Serialization Demo" in output_text
+ assert "Creating elements via Builder API" in output_text
+ assert "Serializing to .duc format" in output_text
+ assert "Successfully serialized" in output_text
+ assert "Serialization demo complete" in output_text
+
+
+class TestParsingDemo:
+ """Test the parsing demo."""
+
+ def test_parsing_demo_runs_successfully(self):
+ """Test that the parsing demo runs without errors."""
+ output = StringIO()
+ with redirect_stdout(output):
+ parsing_demo.main()
+
+ output_text = output.getvalue()
+ assert "Parsing Demo" in output_text
+ assert "Parsing a .duc file from a file path" in output_text
+ assert "Accessing element attributes" in output_text
+ assert "Parsing directly from raw bytes" in output_text
+ assert "Parsing demo complete" in output_text
+
+
class TestStyleBuilders:
"""Test that style builders produce valid style objects."""
diff --git a/packages/ducpy/src/tests/src/test_flywheel.py b/packages/ducpy/src/tests/src/test_flywheel.py
new file mode 100644
index 0000000..e204803
--- /dev/null
+++ b/packages/ducpy/src/tests/src/test_flywheel.py
@@ -0,0 +1,78 @@
+import math
+import os
+import ducpy as duc
+from ducpy.builders.style_builders import create_fill_and_stroke_style, create_simple_styles, create_solid_content, create_text_style
+
+def test_create_flywheel(test_output_dir):
+ center_x, center_y = 400, 300
+ outer_radius, rim_thickness = 200, 30
+ inner_radius = outer_radius - rim_thickness
+ hub_radius, spoke_count = 40, 6
+
+ elements = []
+
+ outer_rim = (duc.ElementBuilder()
+ .at_position(center_x - outer_radius, center_y - outer_radius)
+ .with_size(outer_radius * 2, outer_radius * 2)
+ .with_label("Outer Rim")
+ .with_styles(create_fill_and_stroke_style(fill_content=create_solid_content("#4A90D9"), stroke_content=create_solid_content("#1E3A5F"), stroke_width=2.0))
+ .build_ellipse().build())
+ elements.append(outer_rim)
+
+ inner_bg = (duc.ElementBuilder()
+ .at_position(center_x - inner_radius, center_y - inner_radius)
+ .with_size(inner_radius * 2, inner_radius * 2)
+ .with_label("Inner Background")
+ .with_styles(create_fill_and_stroke_style(fill_content=create_solid_content("#E8F0F8"), stroke_content=create_solid_content("#1E3A5F"), stroke_width=1.0))
+ .build_ellipse().build())
+ elements.append(inner_bg)
+
+ hub = (duc.ElementBuilder()
+ .at_position(center_x - hub_radius, center_y - hub_radius)
+ .with_size(hub_radius * 2, hub_radius * 2)
+ .with_label("Hub")
+ .with_styles(create_fill_and_stroke_style(fill_content=create_solid_content("#2C5F8A"), stroke_content=create_solid_content("#1E3A5F"), stroke_width=2.0))
+ .build_ellipse().build())
+ elements.append(hub)
+
+ center_hole = (duc.ElementBuilder()
+ .at_position(center_x - 15, center_y - 15).with_size(30, 30)
+ .with_label("Center Hole")
+ .with_styles(create_fill_and_stroke_style(fill_content=create_solid_content("#1A3A52"), stroke_content=create_solid_content("#1E3A5F"), stroke_width=1.0))
+ .build_ellipse().build())
+ elements.append(center_hole)
+
+ for i in range(spoke_count):
+ a = (2 * math.pi * i) / spoke_count
+ spoke = (duc.ElementBuilder()
+ .with_label(f"Spoke {i}")
+ .with_styles(create_simple_styles(strokes=[duc.create_stroke(create_solid_content("#5BA3E6"), width=20.0)]))
+ .build_linear_element()
+ .with_points([(center_x + hub_radius * math.cos(a), center_y + hub_radius * math.sin(a)), (center_x + (inner_radius - 5) * math.cos(a), center_y + (inner_radius - 5) * math.sin(a))])
+ .build())
+ elements.append(spoke)
+
+ for i in range(4):
+ a = (2 * math.pi * i) / 4 + math.pi / 4
+ bx, by = center_x + 25 * math.cos(a), center_y + 25 * math.sin(a)
+ bolt = (duc.ElementBuilder()
+ .at_position(bx - 5, by - 5).with_size(10, 10).with_label(f"Bolt {i}")
+ .with_styles(create_fill_and_stroke_style(fill_content=create_solid_content("#1A3A52"), stroke_content=create_solid_content("#1E3A5F"), stroke_width=1.0))
+ .build_ellipse().build())
+ elements.append(bolt)
+
+ label = (duc.ElementBuilder()
+ .at_position(center_x - 50, center_y + outer_radius + 40).with_size(200, 30)
+ .with_label("Flywheel Label").with_styles(create_simple_styles(opacity=1.0))
+ .build_text_element().with_text("Flywheel").build())
+ elements.append(label)
+
+ duc_bytes = duc.serialize_duc(name="flywheel", elements=elements)
+
+ output_file_path = os.path.join(test_output_dir, "test_flywheel.duc")
+ with open(output_file_path, "wb") as f:
+ f.write(duc_bytes)
+
+ assert os.path.exists(output_file_path)
+ assert os.path.getsize(output_file_path) > 0
+ print(f"Flywheel created successfully at {output_file_path}")