Skip to content
Merged

Dev #257

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The `duc` 2D CAD file format is a cornerstone of our advanced design system, con
This documentation aims to provide a comprehensive guide to the format's structure, capabilities, and integration with various applications.

## Tools
[Documentation](https://duc.ducflair.com) | [Playground](https://scopture.com)
[Documentation](https://duc.ducflair.com) | [Playground](https://scopture.com/w/hollow)


## Market Insights
Expand Down
28 changes: 26 additions & 2 deletions packages/ducjs/src/types/elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,28 @@ export const DUC_ELEMENT_SELECTABLE_FIELDS = [
"startBinding",
"endBinding",
"svgPath",
] as const;
"isPlot",
"isCollapsed",
] as const satisfies readonly DucElementSelectableField[];

type DucElementSelectableFieldCoverageExclusion = "strokeColor" | "backgroundColor";
type _MissingSelectableFields = Exclude<
DucElementSelectableField,
typeof DUC_ELEMENT_SELECTABLE_FIELDS[number] | DucElementSelectableFieldCoverageExclusion
>;
type _AssertAllFieldsCovered = _MissingSelectableFields extends never ? true : { missing: _MissingSelectableFields };
const _fieldCoverage: _AssertAllFieldsCovered = true;

export const DUC_ELEMENT_COMMUNICATION_DENY_FIELDS = [
"seed",
"versionNonce",
"thumbnail",
"svgPath",
"originalText",
] as const satisfies readonly DucElementSelectableField[];

export type DucElementCommunicationDenyField = typeof DUC_ELEMENT_COMMUNICATION_DENY_FIELDS[number];
export const DUC_ELEMENT_COMMUNICATION_DENY_FIELD_SET: ReadonlySet<string> = new Set(DUC_ELEMENT_COMMUNICATION_DENY_FIELDS);

export const DUC_ELEMENT_TYPE_VALUES = [
"rectangle",
Expand All @@ -299,7 +320,10 @@ export const DUC_ELEMENT_TYPE_VALUES = [
"pdf",
"doc",
"plot",
] as const;
] as const satisfies readonly Exclude<DucElement["type"], "selection">[];

type _AssertAllTypesCovered = Exclude<DucElement["type"], typeof DUC_ELEMENT_TYPE_VALUES[number] | "selection"> extends never ? true : { missing: Exclude<DucElement["type"], typeof DUC_ELEMENT_TYPE_VALUES[number] | "selection"> };
const _typeCoverage: _AssertAllTypesCovered = true;

export type NonDeleted<TElement extends DucElement> = TElement & {
isDeleted: boolean;
Expand Down
6 changes: 3 additions & 3 deletions packages/ducpy/src/ducpy/builders/state_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,9 @@ def create_local_state_from_base(base: BaseStateParams, **kwargs) -> DucLocalSta
current_item_stroke=kwargs.get('current_item_stroke', None),
current_item_background=kwargs.get('current_item_background', None),
current_item_opacity=kwargs.get('current_item_opacity', 1.0),
current_item_font_family=kwargs.get('current_item_font_family', "Virgil"),
current_item_font_family=kwargs.get('current_item_font_family', "Roboto Mono"),
current_item_font_size=kwargs.get('current_item_font_size', 20.0),
current_item_text_align=kwargs.get('current_item_text_align', None),
current_item_text_align=kwargs.get('current_item_text_align', TEXT_ALIGN.LEFT),
current_item_roundness=kwargs.get('current_item_roundness', 0.0),
current_item_start_line_head=kwargs.get('current_item_start_line_head', None),
current_item_end_line_head=kwargs.get('current_item_end_line_head', None),
Expand All @@ -457,7 +457,7 @@ def create_local_state_from_base(base: BaseStateParams, **kwargs) -> DucLocalSta
objects_snap_mode_enabled=kwargs.get('objects_snap_mode_enabled', True),
grid_mode_enabled=kwargs.get('grid_mode_enabled', True),
outline_mode_enabled=kwargs.get('outline_mode_enabled', False),
manual_save_mode=kwargs.get('manual_save_mode', None),
manual_save_mode=kwargs.get('manual_save_mode', False),
decimal_places=kwargs.get('decimal_places', 2),
)

Expand Down
18 changes: 1 addition & 17 deletions packages/ducpy/src/ducpy/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import ducpy_native
from ducpy.utils.convert import (deep_snake_to_camel, snake_to_camel,
to_serializable)
to_serializable, _flatten_dict)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -104,22 +104,6 @@ def _read_schema_version_fallback() -> str:
"DucModelElement": "model",
}

# Keys in the asdict() output that should be flattened (merged into the parent),
# mirroring Rust's #[serde(flatten)] on base / styles / linear_base / stack_element_base.
_FLATTEN_KEYS = frozenset({"base", "styles", "linear_base", "stack_element_base"})


def _flatten_dict(d: dict) -> dict:
"""Recursively flatten nested dicts whose key is in ``_FLATTEN_KEYS``."""
result: dict = {}
for k, v in d.items():
if k in _FLATTEN_KEYS and isinstance(v, dict):
result.update(_flatten_dict(v))
else:
result[k] = v
return result


def _element_to_camel(wrapper_or_element: Any) -> dict:
"""Convert an element (or ElementWrapper) to the camelCase dict Rust expects."""
el = wrapper_or_element
Expand Down
14 changes: 11 additions & 3 deletions packages/ducpy/src/ducpy/utils/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,19 @@ def deep_snake_to_camel(obj: Any) -> Any:


def _flatten_dict(d: dict) -> dict:
"""Recursively flatten keys that Rust serde #[serde(flatten)] would flatten."""
"""Recursively flatten keys that Rust serde #[serde(flatten)] would flatten.

Walks the entire dict so that nested flatten keys (e.g. ``stack_element_base``
-> ``stack_base`` -> ``styles``) are all merged into the appropriate level.
"""
result: dict = {}
for k, v in d.items():
if k in _FLATTEN_KEYS and isinstance(v, dict):
result.update(_flatten_dict(v))
if isinstance(v, dict):
v = _flatten_dict(v)
if k in _FLATTEN_KEYS:
result.update(v)
else:
result[k] = v
else:
result[k] = v
return result
Expand Down
78 changes: 38 additions & 40 deletions packages/ducpy/src/examples/element_creation_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
def demo_basic_elements():
"""Demo basic elements using the builders API."""
print("=== Basic Elements Demo ===")

# Create basic shapes with styles

rect = (duc.ElementBuilder()
.at_position(0, 0)
.with_size(100, 50)
Expand All @@ -27,7 +26,7 @@ def demo_basic_elements():
))
.build_rectangle()
.build())

ellipse = (duc.ElementBuilder()
.at_position(120, 0)
.with_size(60, 40)
Expand All @@ -39,7 +38,7 @@ def demo_basic_elements():
))
.build_ellipse()
.build())

poly = (duc.ElementBuilder()
.at_position(200, 0)
.with_size(50, 50)
Expand All @@ -53,22 +52,21 @@ def demo_basic_elements():
.build_polygon()
.with_sides(6)
.build())

print(f"Rectangle ID: {rect.element.base.id}")
print(f"Ellipse ID: {ellipse.element.base.id}")
print(f"Ellipse ID: {ellipse.element.base.id}")
print(f"Polygon sides: {poly.element.sides}")

# Demonstrate mutation with random versioning
original_version = rect.element.base.version
duc.mutate_element(rect, x=10, label="Moved Rectangle")
print(f"Version changed: {original_version} -> {rect.element.base.version}")

return [rect, ellipse, poly]


def demo_linear_elements():
"""Demo linear and arrow elements with styles."""
print("\n=== Linear Elements Demo ===")

# Create a styled line

line_points = [(0, 0), (50, 25), (100, 0)]
line = (duc.ElementBuilder()
.with_label("Sample Line")
Expand All @@ -79,8 +77,7 @@ def demo_linear_elements():
.with_points(line_points)
.build())
print(f"Line has {len(line.element.linear_base.points)} points")

# Create a styled arrow

arrow_points = [(0, 50), (75, 100)]
arrow = (duc.ElementBuilder()
.with_label("Sample Arrow")
Expand All @@ -92,11 +89,13 @@ def demo_linear_elements():
.build())
print(f"Arrow element type: {type(arrow.element).__name__}")

return [line, arrow]


def demo_text_elements():
"""Demo text elements with styles and document formatting."""
print("\n=== Text Elements Demo ===")

text = (duc.ElementBuilder()
.at_position(0, 100)
.with_size(150, 25)
Expand All @@ -106,14 +105,14 @@ def demo_text_elements():
.with_text("Hello, DucPy!")
.build())
print(f"Text content: '{text.element.text}'")
print(f"Text uses random versioning: {text.element.base.version > 0}")

return [text]


def demo_stack_elements():
"""Demo new stack-based elements with styles."""
print("\n=== Stack Elements Demo ===")

# Create a styled frame

frame = (duc.ElementBuilder()
.at_position(0, 150)
.with_size(200, 100)
Expand All @@ -127,8 +126,7 @@ def demo_stack_elements():
.build_frame_element()
.build())
print(f"Frame stack label: {frame.element.stack_element_base.stack_base.label}")

# Create a styled plot with margins

plot = (duc.ElementBuilder()
.at_position(220, 150)
.with_size(180, 120)
Expand All @@ -141,48 +139,48 @@ def demo_stack_elements():
.build_plot_element()
.with_margins(duc.Margins(top=5, right=5, bottom=5, left=5))
.build())
print(f"Plot is marked as plot: {plot.element.stack_element_base.stack_base.is_plot}")
print(f"Plot margins: {plot.element.layout.margins.top}mm")


return [frame, plot]


def demo_custom_stack_base():
"""Demo custom stack base creation."""
print("\n=== Custom Stack Base Demo ===")

# Use it in a frame

custom_frame = (duc.ElementBuilder()
.at_position(50, 280)
.with_size(150, 80)
.with_label("Custom Container") # Moved label to ElementBuilder
.with_label("Custom Container")
.build_frame_element()
.with_stack_base(duc.StateBuilder().build_stack_base()
.with_is_collapsed(False)
.with_styles(duc.DucStackLikeStyles(opacity=0.8))
.build())
.build())
print(f"Custom stack opacity: {custom_frame.element.stack_element_base.stack_base.styles.opacity}")

return [custom_frame]


def main():
"""Run all element creation demos."""
print("DucPy Element Creation Demo")
print("=" * 40)

demo_basic_elements()
demo_linear_elements()
demo_text_elements()
demo_stack_elements()
demo_custom_stack_base()

print("\n✅ All demos completed successfully!")
print("The refactored code provides:")
print("- Reduced code duplication")
print("- Consistent random versioning")
print("- New stack-based element support")
print("- Improved maintainability")

elements = []
elements.extend(demo_basic_elements())
elements.extend(demo_linear_elements())
elements.extend(demo_text_elements())
elements.extend(demo_stack_elements())
elements.extend(demo_custom_stack_base())

duc_bytes = duc.serialize_duc(
name="element_creation_example",
elements=elements,
)

print(f"\nCreated {len(elements)} elements → serialized {len(duc_bytes)} bytes.")
print("✅ Element creation demo complete!")
return duc_bytes


if __name__ == "__main__":
Expand Down
34 changes: 5 additions & 29 deletions packages/ducpy/src/examples/external_files_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""

import ducpy as duc
from ducpy.classes.DataStateClass import ExportedDataState


def create_duc_with_external_files():
Expand All @@ -17,7 +16,6 @@ def create_duc_with_external_files():
dummy_image_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x0cIDATx\xda\xed\xc1\x01\x01\x00\x00\x00\xc2\xa0\xf7Om\x00\x00\x00\x00IEND\xaeB`\x82"
dummy_pdf_data = b"%PDF-1.4\n1 0 obj <</Type/Catalog/Pages 2 0 R>> endobj\n2 0 obj <</Type/Pages/Count 0>> endobj\nxref\n0 3\n0000000000 65535 f\n0000000009 00000 n\n0000000074 00000 n\ntrailer<</Size 3/Root 1 0 R>>startxref\n123\n%%EOF"

# 1. Create External File Entries using builders
image_file_entry = (duc.StateBuilder()
.build_external_file()
.with_key("my_image_key")
Expand All @@ -32,49 +30,27 @@ def create_duc_with_external_files():
.with_data(dummy_pdf_data)
.build())

# 2. Create Global State (minimal for this example)
global_state = (duc.StateBuilder()
.build_global_state()
.with_main_scope("mm")
.build())

# 3. Create Local State (minimal for this example)
local_state = (duc.StateBuilder()
.build_local_state()
.build())

# 4. Assemble into an ExportedDataState (DUC object)
duc_object = ExportedDataState(
type="duc_example",
version="1.0.0",
source="external_files_demo.py",
thumbnail=b"",
elements=[],
blocks=[],
block_instances=[],
block_collections=[],
groups=[],
regions=[],
layers=[],
dictionary={},
duc_local_state=local_state,
duc_global_state=global_state,
version_graph=None, # Not focusing on versioning here
files={image_file_entry.id: image_file_entry, pdf_file_entry.id: pdf_file_entry}
)
duc_object_files = {image_file_entry.id: image_file_entry, pdf_file_entry.id: pdf_file_entry}

print("DUC object with external files created successfully!")
print(f"Total external files: {len(duc_object.files)}")
for fid, file in duc_object.files.items():
active_rev = file.revisions[file.active_revision_id]
print(f"File id: {fid}, MIME type: {active_rev.mime_type}")
return duc_object
print(f"Total external files: {len(duc_object_files)}")
return duc_object_files, global_state, local_state


def main():
"""Run the external files demo."""
print("External Files Demo")
print("=" * 30)
sample_duc = create_duc_with_external_files()
create_duc_with_external_files()
print("\nExternal files demo complete!")


Expand Down
Loading
Loading