Skip to content

Commit a4293d9

Browse files
authored
Merge pull request #257 from ducflair/dev
Dev
2 parents 4d8d8db + 0c41b00 commit a4293d9

12 files changed

Lines changed: 243 additions & 181 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ The `duc` 2D CAD file format is a cornerstone of our advanced design system, con
3535
This documentation aims to provide a comprehensive guide to the format's structure, capabilities, and integration with various applications.
3636

3737
## Tools
38-
[Documentation](https://duc.ducflair.com) | [Playground](https://scopture.com)
38+
[Documentation](https://duc.ducflair.com) | [Playground](https://scopture.com/w/hollow)
3939

4040

4141
## Market Insights

packages/ducjs/src/types/elements/index.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,28 @@ export const DUC_ELEMENT_SELECTABLE_FIELDS = [
281281
"startBinding",
282282
"endBinding",
283283
"svgPath",
284-
] as const;
284+
"isPlot",
285+
"isCollapsed",
286+
] as const satisfies readonly DucElementSelectableField[];
287+
288+
type DucElementSelectableFieldCoverageExclusion = "strokeColor" | "backgroundColor";
289+
type _MissingSelectableFields = Exclude<
290+
DucElementSelectableField,
291+
typeof DUC_ELEMENT_SELECTABLE_FIELDS[number] | DucElementSelectableFieldCoverageExclusion
292+
>;
293+
type _AssertAllFieldsCovered = _MissingSelectableFields extends never ? true : { missing: _MissingSelectableFields };
294+
const _fieldCoverage: _AssertAllFieldsCovered = true;
295+
296+
export const DUC_ELEMENT_COMMUNICATION_DENY_FIELDS = [
297+
"seed",
298+
"versionNonce",
299+
"thumbnail",
300+
"svgPath",
301+
"originalText",
302+
] as const satisfies readonly DucElementSelectableField[];
303+
304+
export type DucElementCommunicationDenyField = typeof DUC_ELEMENT_COMMUNICATION_DENY_FIELDS[number];
305+
export const DUC_ELEMENT_COMMUNICATION_DENY_FIELD_SET: ReadonlySet<string> = new Set(DUC_ELEMENT_COMMUNICATION_DENY_FIELDS);
285306

286307
export const DUC_ELEMENT_TYPE_VALUES = [
287308
"rectangle",
@@ -299,7 +320,10 @@ export const DUC_ELEMENT_TYPE_VALUES = [
299320
"pdf",
300321
"doc",
301322
"plot",
302-
] as const;
323+
] as const satisfies readonly Exclude<DucElement["type"], "selection">[];
324+
325+
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"> };
326+
const _typeCoverage: _AssertAllTypesCovered = true;
303327

304328
export type NonDeleted<TElement extends DucElement> = TElement & {
305329
isDeleted: boolean;

packages/ducpy/src/ducpy/builders/state_builders.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -446,9 +446,9 @@ def create_local_state_from_base(base: BaseStateParams, **kwargs) -> DucLocalSta
446446
current_item_stroke=kwargs.get('current_item_stroke', None),
447447
current_item_background=kwargs.get('current_item_background', None),
448448
current_item_opacity=kwargs.get('current_item_opacity', 1.0),
449-
current_item_font_family=kwargs.get('current_item_font_family', "Virgil"),
449+
current_item_font_family=kwargs.get('current_item_font_family', "Roboto Mono"),
450450
current_item_font_size=kwargs.get('current_item_font_size', 20.0),
451-
current_item_text_align=kwargs.get('current_item_text_align', None),
451+
current_item_text_align=kwargs.get('current_item_text_align', TEXT_ALIGN.LEFT),
452452
current_item_roundness=kwargs.get('current_item_roundness', 0.0),
453453
current_item_start_line_head=kwargs.get('current_item_start_line_head', None),
454454
current_item_end_line_head=kwargs.get('current_item_end_line_head', None),
@@ -457,7 +457,7 @@ def create_local_state_from_base(base: BaseStateParams, **kwargs) -> DucLocalSta
457457
objects_snap_mode_enabled=kwargs.get('objects_snap_mode_enabled', True),
458458
grid_mode_enabled=kwargs.get('grid_mode_enabled', True),
459459
outline_mode_enabled=kwargs.get('outline_mode_enabled', False),
460-
manual_save_mode=kwargs.get('manual_save_mode', None),
460+
manual_save_mode=kwargs.get('manual_save_mode', False),
461461
decimal_places=kwargs.get('decimal_places', 2),
462462
)
463463

packages/ducpy/src/ducpy/serialize.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import ducpy_native
1818
from ducpy.utils.convert import (deep_snake_to_camel, snake_to_camel,
19-
to_serializable)
19+
to_serializable, _flatten_dict)
2020

2121
logger = logging.getLogger(__name__)
2222

@@ -104,22 +104,6 @@ def _read_schema_version_fallback() -> str:
104104
"DucModelElement": "model",
105105
}
106106

107-
# Keys in the asdict() output that should be flattened (merged into the parent),
108-
# mirroring Rust's #[serde(flatten)] on base / styles / linear_base / stack_element_base.
109-
_FLATTEN_KEYS = frozenset({"base", "styles", "linear_base", "stack_element_base"})
110-
111-
112-
def _flatten_dict(d: dict) -> dict:
113-
"""Recursively flatten nested dicts whose key is in ``_FLATTEN_KEYS``."""
114-
result: dict = {}
115-
for k, v in d.items():
116-
if k in _FLATTEN_KEYS and isinstance(v, dict):
117-
result.update(_flatten_dict(v))
118-
else:
119-
result[k] = v
120-
return result
121-
122-
123107
def _element_to_camel(wrapper_or_element: Any) -> dict:
124108
"""Convert an element (or ElementWrapper) to the camelCase dict Rust expects."""
125109
el = wrapper_or_element

packages/ducpy/src/ducpy/utils/convert.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,19 @@ def deep_snake_to_camel(obj: Any) -> Any:
6565

6666

6767
def _flatten_dict(d: dict) -> dict:
68-
"""Recursively flatten keys that Rust serde #[serde(flatten)] would flatten."""
68+
"""Recursively flatten keys that Rust serde #[serde(flatten)] would flatten.
69+
70+
Walks the entire dict so that nested flatten keys (e.g. ``stack_element_base``
71+
-> ``stack_base`` -> ``styles``) are all merged into the appropriate level.
72+
"""
6973
result: dict = {}
7074
for k, v in d.items():
71-
if k in _FLATTEN_KEYS and isinstance(v, dict):
72-
result.update(_flatten_dict(v))
75+
if isinstance(v, dict):
76+
v = _flatten_dict(v)
77+
if k in _FLATTEN_KEYS:
78+
result.update(v)
79+
else:
80+
result[k] = v
7381
else:
7482
result[k] = v
7583
return result

packages/ducpy/src/examples/element_creation_demo.py

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
def demo_basic_elements():
1414
"""Demo basic elements using the builders API."""
1515
print("=== Basic Elements Demo ===")
16-
17-
# Create basic shapes with styles
16+
1817
rect = (duc.ElementBuilder()
1918
.at_position(0, 0)
2019
.with_size(100, 50)
@@ -27,7 +26,7 @@ def demo_basic_elements():
2726
))
2827
.build_rectangle()
2928
.build())
30-
29+
3130
ellipse = (duc.ElementBuilder()
3231
.at_position(120, 0)
3332
.with_size(60, 40)
@@ -39,7 +38,7 @@ def demo_basic_elements():
3938
))
4039
.build_ellipse()
4140
.build())
42-
41+
4342
poly = (duc.ElementBuilder()
4443
.at_position(200, 0)
4544
.with_size(50, 50)
@@ -53,22 +52,21 @@ def demo_basic_elements():
5352
.build_polygon()
5453
.with_sides(6)
5554
.build())
56-
55+
5756
print(f"Rectangle ID: {rect.element.base.id}")
58-
print(f"Ellipse ID: {ellipse.element.base.id}")
57+
print(f"Ellipse ID: {ellipse.element.base.id}")
5958
print(f"Polygon sides: {poly.element.sides}")
60-
59+
6160
# Demonstrate mutation with random versioning
62-
original_version = rect.element.base.version
6361
duc.mutate_element(rect, x=10, label="Moved Rectangle")
64-
print(f"Version changed: {original_version} -> {rect.element.base.version}")
62+
63+
return [rect, ellipse, poly]
6564

6665

6766
def demo_linear_elements():
6867
"""Demo linear and arrow elements with styles."""
6968
print("\n=== Linear Elements Demo ===")
70-
71-
# Create a styled line
69+
7270
line_points = [(0, 0), (50, 25), (100, 0)]
7371
line = (duc.ElementBuilder()
7472
.with_label("Sample Line")
@@ -79,8 +77,7 @@ def demo_linear_elements():
7977
.with_points(line_points)
8078
.build())
8179
print(f"Line has {len(line.element.linear_base.points)} points")
82-
83-
# Create a styled arrow
80+
8481
arrow_points = [(0, 50), (75, 100)]
8582
arrow = (duc.ElementBuilder()
8683
.with_label("Sample Arrow")
@@ -92,11 +89,13 @@ def demo_linear_elements():
9289
.build())
9390
print(f"Arrow element type: {type(arrow.element).__name__}")
9491

92+
return [line, arrow]
93+
9594

9695
def demo_text_elements():
9796
"""Demo text elements with styles and document formatting."""
9897
print("\n=== Text Elements Demo ===")
99-
98+
10099
text = (duc.ElementBuilder()
101100
.at_position(0, 100)
102101
.with_size(150, 25)
@@ -106,14 +105,14 @@ def demo_text_elements():
106105
.with_text("Hello, DucPy!")
107106
.build())
108107
print(f"Text content: '{text.element.text}'")
109-
print(f"Text uses random versioning: {text.element.base.version > 0}")
108+
109+
return [text]
110110

111111

112112
def demo_stack_elements():
113113
"""Demo new stack-based elements with styles."""
114114
print("\n=== Stack Elements Demo ===")
115-
116-
# Create a styled frame
115+
117116
frame = (duc.ElementBuilder()
118117
.at_position(0, 150)
119118
.with_size(200, 100)
@@ -127,8 +126,7 @@ def demo_stack_elements():
127126
.build_frame_element()
128127
.build())
129128
print(f"Frame stack label: {frame.element.stack_element_base.stack_base.label}")
130-
131-
# Create a styled plot with margins
129+
132130
plot = (duc.ElementBuilder()
133131
.at_position(220, 150)
134132
.with_size(180, 120)
@@ -141,48 +139,48 @@ def demo_stack_elements():
141139
.build_plot_element()
142140
.with_margins(duc.Margins(top=5, right=5, bottom=5, left=5))
143141
.build())
144-
print(f"Plot is marked as plot: {plot.element.stack_element_base.stack_base.is_plot}")
145-
print(f"Plot margins: {plot.element.layout.margins.top}mm")
146-
147142

143+
return [frame, plot]
148144

149145

150146
def demo_custom_stack_base():
151147
"""Demo custom stack base creation."""
152148
print("\n=== Custom Stack Base Demo ===")
153-
154-
# Use it in a frame
149+
155150
custom_frame = (duc.ElementBuilder()
156151
.at_position(50, 280)
157152
.with_size(150, 80)
158-
.with_label("Custom Container") # Moved label to ElementBuilder
153+
.with_label("Custom Container")
159154
.build_frame_element()
160155
.with_stack_base(duc.StateBuilder().build_stack_base()
161156
.with_is_collapsed(False)
162157
.with_styles(duc.DucStackLikeStyles(opacity=0.8))
163158
.build())
164159
.build())
165-
166-
print(f"Custom stack opacity: {custom_frame.element.stack_element_base.stack_base.styles.opacity}")
160+
161+
return [custom_frame]
167162

168163

169164
def main():
170165
"""Run all element creation demos."""
171166
print("DucPy Element Creation Demo")
172167
print("=" * 40)
173-
174-
demo_basic_elements()
175-
demo_linear_elements()
176-
demo_text_elements()
177-
demo_stack_elements()
178-
demo_custom_stack_base()
179-
180-
print("\n✅ All demos completed successfully!")
181-
print("The refactored code provides:")
182-
print("- Reduced code duplication")
183-
print("- Consistent random versioning")
184-
print("- New stack-based element support")
185-
print("- Improved maintainability")
168+
169+
elements = []
170+
elements.extend(demo_basic_elements())
171+
elements.extend(demo_linear_elements())
172+
elements.extend(demo_text_elements())
173+
elements.extend(demo_stack_elements())
174+
elements.extend(demo_custom_stack_base())
175+
176+
duc_bytes = duc.serialize_duc(
177+
name="element_creation_example",
178+
elements=elements,
179+
)
180+
181+
print(f"\nCreated {len(elements)} elements → serialized {len(duc_bytes)} bytes.")
182+
print("✅ Element creation demo complete!")
183+
return duc_bytes
186184

187185

188186
if __name__ == "__main__":

packages/ducpy/src/examples/external_files_demo.py

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""
44

55
import ducpy as duc
6-
from ducpy.classes.DataStateClass import ExportedDataState
76

87

98
def create_duc_with_external_files():
@@ -17,7 +16,6 @@ def create_duc_with_external_files():
1716
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"
1817
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"
1918

20-
# 1. Create External File Entries using builders
2119
image_file_entry = (duc.StateBuilder()
2220
.build_external_file()
2321
.with_key("my_image_key")
@@ -32,49 +30,27 @@ def create_duc_with_external_files():
3230
.with_data(dummy_pdf_data)
3331
.build())
3432

35-
# 2. Create Global State (minimal for this example)
3633
global_state = (duc.StateBuilder()
3734
.build_global_state()
3835
.with_main_scope("mm")
3936
.build())
4037

41-
# 3. Create Local State (minimal for this example)
4238
local_state = (duc.StateBuilder()
4339
.build_local_state()
4440
.build())
4541

46-
# 4. Assemble into an ExportedDataState (DUC object)
47-
duc_object = ExportedDataState(
48-
type="duc_example",
49-
version="1.0.0",
50-
source="external_files_demo.py",
51-
thumbnail=b"",
52-
elements=[],
53-
blocks=[],
54-
block_instances=[],
55-
block_collections=[],
56-
groups=[],
57-
regions=[],
58-
layers=[],
59-
dictionary={},
60-
duc_local_state=local_state,
61-
duc_global_state=global_state,
62-
version_graph=None, # Not focusing on versioning here
63-
files={image_file_entry.id: image_file_entry, pdf_file_entry.id: pdf_file_entry}
64-
)
42+
duc_object_files = {image_file_entry.id: image_file_entry, pdf_file_entry.id: pdf_file_entry}
6543

6644
print("DUC object with external files created successfully!")
67-
print(f"Total external files: {len(duc_object.files)}")
68-
for fid, file in duc_object.files.items():
69-
active_rev = file.revisions[file.active_revision_id]
70-
print(f"File id: {fid}, MIME type: {active_rev.mime_type}")
71-
return duc_object
45+
print(f"Total external files: {len(duc_object_files)}")
46+
return duc_object_files, global_state, local_state
47+
7248

7349
def main():
7450
"""Run the external files demo."""
7551
print("External Files Demo")
7652
print("=" * 30)
77-
sample_duc = create_duc_with_external_files()
53+
create_duc_with_external_files()
7854
print("\nExternal files demo complete!")
7955

8056

0 commit comments

Comments
 (0)