Skip to content
Draft
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
its layer; generated content like the Material Test Grid restricts
its step to the workpiece it creates, and that selection is now
visible instead of hidden
- Variable Text: a `{serial}` variable for text templates, with a
project counter that advances after every finished job and consecutive
numbers for array copies (Tools > Variable Text) (#527)
- Variable Text: merge a CSV file into text templates with
`{csv["Name"]}`, `{csv[0]}` and `{row}`, one row per copy or per job
(#528)

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,14 @@ def get_geometry(
resolved_text_cache.update(clone._resolved_text_cache)
return geo, fills

def get_template_sources(self) -> list[str]:
"""The raw contents of all text boxes, including templates."""
return [
e.content
for e in self.registry.entities
if isinstance(e, TextBoxEntity) and e.content
]

def _clone_for_geometry(self) -> "Sketch":
"""
Lightweight clone for geometry generation.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from typing import cast

from raygeo.geo.shape.text import FontConfig
from sketcher.core import Sketch
from sketcher.core.entities.text_box import TextBoxEntity

from rayforge.core.doc import Doc
from rayforge.core.variable_text import SerialSettings
from rayforge.core.workpiece import WorkPiece


def _sketch_with_text(content: str) -> tuple[Sketch, TextBoxEntity]:
sketch = Sketch(name="plate")
origin = sketch.add_point(0, 0)
width_pt = sketch.add_point(10, 0)
height_pt = sketch.add_point(0, 10)
box_id = sketch.registry.add_text_box(
origin, width_pt, height_pt, content, FontConfig()
)
return sketch, cast(TextBoxEntity, sketch.registry.get_entity(box_id))


def _resolved(wp: WorkPiece, box: TextBoxEntity) -> str:
entry = wp._resolved_text_cache[box.id]
return entry[1]


def _place(doc: Doc, sketch: Sketch, offset: int = 0) -> WorkPiece:
doc.add_asset(sketch)
wp = WorkPiece.from_geometry_provider(sketch)
wp.variable_offset = offset
doc.active_layer.add_child(wp)
return wp


def test_template_sources_lists_text_box_contents():
sketch, _box = _sketch_with_text("No. {serial:03d}")
assert sketch.get_template_sources() == ["No. {serial:03d}"]


def test_serial_is_resolved_in_document():
doc = Doc()
doc.variable_text.serial = SerialSettings(current=7)
sketch, box = _sketch_with_text("No. {serial:03d}")
wp = _place(doc, sketch)
assert _resolved(wp, box) == "No. 007"


def test_copies_show_consecutive_numbers():
doc = Doc()
doc.variable_text.serial = SerialSettings(current=41)
sketch, box = _sketch_with_text("{serial}")
first = _place(doc, sketch)
second = first.duplicate()
assert isinstance(second, WorkPiece)
second.variable_offset = 1
doc.active_layer.add_child(second)
assert _resolved(first, box) == "41"
assert _resolved(second, box) == "42"


def test_counter_change_updates_text():
doc = Doc()
sketch, box = _sketch_with_text("{serial:04d}")
wp = _place(doc, sketch)
doc.variable_text.serial = SerialSettings(current=12)
assert _resolved(wp, box) == "0012"


def test_saved_text_is_kept_on_load():
doc = Doc()
doc.variable_text.serial = SerialSettings(current=5)
sketch, box = _sketch_with_text("{serial}-{uuid8()}")
wp = _place(doc, sketch)
before = _resolved(wp, box)
restored = Doc.from_dict(doc.to_dict())
restored_wp = restored.all_workpieces[0]
assert restored_wp.boundaries is not None
assert _resolved(restored_wp, box) == before
61 changes: 61 additions & 0 deletions rayforge/core/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .item import DocItem
from .layer import Layer
from .source_asset import SourceAsset
from .variable_text import VariableText
from .workpiece import WorkPiece

if TYPE_CHECKING:
Expand Down Expand Up @@ -45,6 +46,11 @@ def __init__(self):
self.assets: dict[str, IAsset] = {}
self.asset_order: list[str] = []

# Variables for text templates (serial number, ...)
self._variable_text = VariableText()
self._variable_text.changed.connect(self._on_variable_text_changed)
self.descendant_added.connect(self._on_item_added_for_templates)

# A new document starts with three empty workpiece layers
for i in range(3):
workpiece_layer = Layer(_("Layer {}").format(i + 1))
Expand Down Expand Up @@ -90,6 +96,7 @@ def _deserialize_item(item_data: dict) -> DocItem:

doc = cls()
doc.uid = data.get("uid", doc.uid)
doc.variable_text = VariableText.from_dict(data.get("variable_text"))

# Clear the default layer created by __init__
doc.set_children([])
Expand Down Expand Up @@ -173,8 +180,62 @@ def to_dict(self) -> dict:
"active_layer_index": self._active_layer_index,
"children": [child.to_dict() for child in self.children],
"assets": [asset.to_dict() for asset in self.get_all_assets()],
"variable_text": self._variable_text.to_dict(),
}

@property
def variable_text(self) -> VariableText:
"""The variables offered to text templates."""
return self._variable_text

@variable_text.setter
def variable_text(self, value: VariableText) -> None:
self._variable_text.changed.disconnect(self._on_variable_text_changed)
self._variable_text = value
value.changed.connect(self._on_variable_text_changed)
self._on_variable_text_changed(value)

def _template_workpieces(
self, names: set[str] | None = None
) -> list[WorkPiece]:
"""Workpieces whose templates read a document variable, or one
of *names* if given."""
if names is None:
names = self._variable_text.names
return [
wp
for wp in self.all_workpieces
if wp.references_template_names(names)
]

def variable_text_copies(self, names: set[str] | None = None) -> int:
"""
How many consecutive variable values one job uses: the highest
variable offset of a template workpiece plus one, or 0 if no
workpiece uses a document variable (or one of *names*).
"""
offsets = [
wp.variable_offset for wp in self._template_workpieces(names)
]
if not offsets:
return 0
return max(offsets) + 1

def _on_variable_text_changed(self, sender: VariableText) -> None:
for wp in self._template_workpieces():
wp.refresh_template_values()

def _on_item_added_for_templates(
self, sender: DocItem, *, origin: DocItem, parent_of_origin: DocItem
) -> None:
if isinstance(origin, WorkPiece):
added = [origin]
else:
added = origin.get_descendants(of_type=WorkPiece)
for wp in added:
if wp.references_template_names(self._variable_text.names):
wp.sync_template_values()

def add_asset(
self, asset: IAsset, index: int | None = None, silent: bool = False
):
Expand Down
Loading
Loading