natural-pdf 0.1.15__py3-none-any.whl → 0.1.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- natural_pdf/__init__.py +31 -0
- natural_pdf/analyzers/layout/gemini.py +137 -162
- natural_pdf/analyzers/layout/layout_manager.py +9 -5
- natural_pdf/analyzers/layout/layout_options.py +77 -7
- natural_pdf/analyzers/layout/paddle.py +318 -165
- natural_pdf/analyzers/layout/table_structure_utils.py +78 -0
- natural_pdf/analyzers/shape_detection_mixin.py +770 -405
- natural_pdf/classification/mixin.py +2 -8
- natural_pdf/collections/pdf_collection.py +25 -30
- natural_pdf/core/highlighting_service.py +47 -32
- natural_pdf/core/page.py +119 -76
- natural_pdf/core/pdf.py +19 -22
- natural_pdf/describe/__init__.py +21 -0
- natural_pdf/describe/base.py +457 -0
- natural_pdf/describe/elements.py +411 -0
- natural_pdf/describe/mixin.py +84 -0
- natural_pdf/describe/summary.py +186 -0
- natural_pdf/elements/base.py +11 -10
- natural_pdf/elements/collections.py +116 -51
- natural_pdf/elements/region.py +204 -127
- natural_pdf/exporters/paddleocr.py +38 -13
- natural_pdf/flows/__init__.py +3 -3
- natural_pdf/flows/collections.py +303 -132
- natural_pdf/flows/element.py +277 -132
- natural_pdf/flows/flow.py +33 -16
- natural_pdf/flows/region.py +142 -79
- natural_pdf/ocr/engine_doctr.py +37 -4
- natural_pdf/ocr/engine_easyocr.py +23 -3
- natural_pdf/ocr/engine_paddle.py +281 -30
- natural_pdf/ocr/engine_surya.py +8 -3
- natural_pdf/ocr/ocr_manager.py +75 -76
- natural_pdf/ocr/ocr_options.py +52 -87
- natural_pdf/search/__init__.py +25 -12
- natural_pdf/search/lancedb_search_service.py +91 -54
- natural_pdf/search/numpy_search_service.py +86 -65
- natural_pdf/search/searchable_mixin.py +2 -2
- natural_pdf/selectors/parser.py +125 -81
- natural_pdf/widgets/__init__.py +1 -1
- natural_pdf/widgets/viewer.py +205 -449
- {natural_pdf-0.1.15.dist-info → natural_pdf-0.1.17.dist-info}/METADATA +27 -45
- {natural_pdf-0.1.15.dist-info → natural_pdf-0.1.17.dist-info}/RECORD +44 -38
- {natural_pdf-0.1.15.dist-info → natural_pdf-0.1.17.dist-info}/WHEEL +0 -0
- {natural_pdf-0.1.15.dist-info → natural_pdf-0.1.17.dist-info}/licenses/LICENSE +0 -0
- {natural_pdf-0.1.15.dist-info → natural_pdf-0.1.17.dist-info}/top_level.txt +0 -0
natural_pdf/flows/element.py
CHANGED
@@ -2,9 +2,10 @@ import logging
|
|
2
2
|
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, cast
|
3
3
|
|
4
4
|
if TYPE_CHECKING:
|
5
|
+
from natural_pdf.core.page import Page as PhysicalPage # For type checking physical_object.page
|
5
6
|
from natural_pdf.elements.base import Element as PhysicalElement
|
6
7
|
from natural_pdf.elements.region import Region as PhysicalRegion
|
7
|
-
|
8
|
+
|
8
9
|
from .flow import Flow
|
9
10
|
from .region import FlowRegion
|
10
11
|
|
@@ -27,14 +28,14 @@ class FlowElement:
|
|
27
28
|
natural_pdf.elements.region.Region object.
|
28
29
|
flow: The Flow instance this element is part of.
|
29
30
|
"""
|
30
|
-
if not (hasattr(physical_object,
|
31
|
+
if not (hasattr(physical_object, "bbox") and hasattr(physical_object, "page")):
|
31
32
|
raise TypeError(
|
32
33
|
f"physical_object must be a valid PDF element-like object with 'bbox' and 'page' attributes. Got {type(physical_object)}"
|
33
34
|
)
|
34
35
|
self.physical_object: Union["PhysicalElement", "PhysicalRegion"] = physical_object
|
35
36
|
self.flow: "Flow" = flow
|
36
37
|
|
37
|
-
# --- Properties to delegate to the physical_object ---
|
38
|
+
# --- Properties to delegate to the physical_object ---
|
38
39
|
@property
|
39
40
|
def bbox(self) -> Tuple[float, float, float, float]:
|
40
41
|
return self.physical_object.bbox
|
@@ -65,33 +66,37 @@ class FlowElement:
|
|
65
66
|
|
66
67
|
@property
|
67
68
|
def text(self) -> Optional[str]:
|
68
|
-
return getattr(self.physical_object,
|
69
|
-
|
69
|
+
return getattr(self.physical_object, "text", None)
|
70
|
+
|
70
71
|
@property
|
71
72
|
def page(self) -> Optional["PhysicalPage"]:
|
72
73
|
"""Returns the physical page of the underlying element."""
|
73
|
-
return getattr(self.physical_object,
|
74
|
+
return getattr(self.physical_object, "page", None)
|
74
75
|
|
75
76
|
def _flow_direction(
|
76
77
|
self,
|
77
78
|
direction: str, # "above", "below", "left", "right"
|
78
79
|
size: Optional[float] = None,
|
79
|
-
cross_size_ratio: Optional[float] = None,
|
80
|
-
cross_size_absolute: Optional[float] = None,
|
81
|
-
cross_alignment: str = "center",
|
80
|
+
cross_size_ratio: Optional[float] = None, # Default to None for full flow width
|
81
|
+
cross_size_absolute: Optional[float] = None,
|
82
|
+
cross_alignment: str = "center", # "start", "center", "end"
|
82
83
|
until: Optional[str] = None,
|
83
84
|
include_endpoint: bool = True,
|
84
85
|
**kwargs,
|
85
86
|
) -> "FlowRegion":
|
86
|
-
from .region import FlowRegion # Runtime import for return if not stringized, but stringizing is safer
|
87
87
|
# Ensure correct import for creating new PhysicalRegion instances if needed
|
88
|
-
from natural_pdf.elements.region import Region as PhysicalRegion_Class
|
88
|
+
from natural_pdf.elements.region import Region as PhysicalRegion_Class # Runtime import
|
89
89
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
remaining_size = float(size) if size is not None else float('inf')
|
90
|
+
from .region import ( # Runtime import for return if not stringized, but stringizing is safer
|
91
|
+
FlowRegion,
|
92
|
+
)
|
94
93
|
|
94
|
+
collected_constituent_regions: List[PhysicalRegion_Class] = (
|
95
|
+
[]
|
96
|
+
) # PhysicalRegion_Class is runtime
|
97
|
+
boundary_element_hit: Optional["PhysicalElement"] = None # Stringized
|
98
|
+
# Ensure remaining_size is float, even if size is int.
|
99
|
+
remaining_size = float(size) if size is not None else float("inf")
|
95
100
|
|
96
101
|
# 1. Identify Starting Segment and its index
|
97
102
|
start_segment_index = -1
|
@@ -107,276 +112,416 @@ class FlowElement:
|
|
107
112
|
break
|
108
113
|
obj_bbox = self.physical_object.bbox
|
109
114
|
seg_bbox = segment_in_flow.bbox
|
110
|
-
if not (
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
+
if not (
|
116
|
+
obj_bbox[2] < seg_bbox[0]
|
117
|
+
or obj_bbox[0] > seg_bbox[2]
|
118
|
+
or obj_bbox[3] < seg_bbox[1]
|
119
|
+
or obj_bbox[1] > seg_bbox[3]
|
120
|
+
):
|
121
|
+
if start_segment_index == -1:
|
122
|
+
start_segment_index = i
|
123
|
+
|
115
124
|
if start_segment_index == -1:
|
116
|
-
page_num_str =
|
125
|
+
page_num_str = (
|
126
|
+
str(self.physical_object.page.page_number) if self.physical_object.page else "N/A"
|
127
|
+
)
|
117
128
|
logger.warning(
|
118
129
|
f"FlowElement's physical object {self.physical_object.bbox} on page {page_num_str} "
|
119
130
|
f"not found within any flow segment. Cannot perform directional operation '{direction}'."
|
120
131
|
)
|
121
132
|
# Need FlowRegion for the return type, ensure it's available or stringized
|
122
|
-
from .region import FlowRegion as RuntimeFlowRegion
|
133
|
+
from .region import FlowRegion as RuntimeFlowRegion
|
134
|
+
|
123
135
|
return RuntimeFlowRegion(
|
124
136
|
flow=self.flow,
|
125
137
|
constituent_regions=[],
|
126
138
|
source_flow_element=self,
|
127
|
-
boundary_element_found=None
|
139
|
+
boundary_element_found=None,
|
128
140
|
)
|
129
141
|
|
130
142
|
is_primary_vertical = self.flow.arrangement == "vertical"
|
131
143
|
segment_iterator: range
|
132
144
|
|
133
145
|
if direction == "below":
|
134
|
-
if not is_primary_vertical:
|
135
|
-
|
146
|
+
if not is_primary_vertical:
|
147
|
+
raise NotImplementedError("'below' is for vertical flows.")
|
148
|
+
is_forward = True
|
136
149
|
segment_iterator = range(start_segment_index, len(self.flow.segments))
|
137
150
|
elif direction == "above":
|
138
|
-
if not is_primary_vertical:
|
151
|
+
if not is_primary_vertical:
|
152
|
+
raise NotImplementedError("'above' is for vertical flows.")
|
139
153
|
is_forward = False
|
140
154
|
segment_iterator = range(start_segment_index, -1, -1)
|
141
155
|
elif direction == "right":
|
142
|
-
if is_primary_vertical:
|
156
|
+
if is_primary_vertical:
|
157
|
+
raise NotImplementedError("'right' is for horizontal flows.")
|
143
158
|
is_forward = True
|
144
159
|
segment_iterator = range(start_segment_index, len(self.flow.segments))
|
145
160
|
elif direction == "left":
|
146
|
-
if is_primary_vertical:
|
161
|
+
if is_primary_vertical:
|
162
|
+
raise NotImplementedError("'left' is for horizontal flows.")
|
147
163
|
is_forward = False
|
148
164
|
segment_iterator = range(start_segment_index, -1, -1)
|
149
165
|
else:
|
150
|
-
raise ValueError(
|
166
|
+
raise ValueError(
|
167
|
+
f"Internal error: Invalid direction '{direction}' for _flow_direction."
|
168
|
+
)
|
151
169
|
|
152
170
|
for current_segment_idx in segment_iterator:
|
153
|
-
if remaining_size <= 0 and size is not None:
|
154
|
-
|
171
|
+
if remaining_size <= 0 and size is not None:
|
172
|
+
break
|
173
|
+
if boundary_element_hit:
|
174
|
+
break
|
155
175
|
|
156
176
|
current_segment: PhysicalRegion_Class = self.flow.segments[current_segment_idx]
|
157
177
|
segment_contribution: Optional[PhysicalRegion_Class] = None
|
158
|
-
|
159
|
-
op_source: Union["PhysicalElement", PhysicalRegion_Class]
|
178
|
+
|
179
|
+
op_source: Union["PhysicalElement", PhysicalRegion_Class] # Stringized PhysicalElement
|
160
180
|
op_direction_params: dict = {
|
161
|
-
"direction": direction,
|
181
|
+
"direction": direction,
|
182
|
+
"until": until,
|
183
|
+
"include_endpoint": include_endpoint,
|
184
|
+
**kwargs,
|
162
185
|
}
|
163
|
-
|
186
|
+
|
164
187
|
# --- Cross-size logic: Default to "full" if no specific ratio or absolute is given ---
|
165
188
|
cross_size_for_op: Union[str, float]
|
166
189
|
if cross_size_absolute is not None:
|
167
190
|
cross_size_for_op = cross_size_absolute
|
168
|
-
elif cross_size_ratio is not None:
|
169
|
-
base_cross_dim =
|
191
|
+
elif cross_size_ratio is not None: # User explicitly provided a ratio
|
192
|
+
base_cross_dim = (
|
193
|
+
self.physical_object.width
|
194
|
+
if is_primary_vertical
|
195
|
+
else self.physical_object.height
|
196
|
+
)
|
170
197
|
cross_size_for_op = base_cross_dim * cross_size_ratio
|
171
|
-
else:
|
198
|
+
else: # Default case: neither absolute nor ratio provided, so use "full"
|
172
199
|
cross_size_for_op = "full"
|
173
200
|
op_direction_params["cross_size"] = cross_size_for_op
|
174
201
|
|
175
202
|
if current_segment_idx == start_segment_index:
|
176
|
-
op_source = self.physical_object
|
203
|
+
op_source = self.physical_object
|
177
204
|
op_direction_params["size"] = remaining_size if size is not None else None
|
178
|
-
op_direction_params["include_source"] = False
|
205
|
+
op_direction_params["include_source"] = False
|
179
206
|
|
180
207
|
source_for_op_call = op_source
|
181
208
|
if not isinstance(source_for_op_call, PhysicalRegion_Class):
|
182
|
-
if hasattr(source_for_op_call,
|
209
|
+
if hasattr(source_for_op_call, "to_region"):
|
183
210
|
source_for_op_call = source_for_op_call.to_region()
|
184
|
-
else:
|
185
|
-
logger.error(
|
211
|
+
else:
|
212
|
+
logger.error(
|
213
|
+
f"FlowElement: Cannot convert op_source {type(op_source)} to region."
|
214
|
+
)
|
186
215
|
continue
|
187
|
-
|
216
|
+
|
188
217
|
# 1. Perform directional operation *without* 'until' initially to get basic shape.
|
189
218
|
initial_op_params = {
|
190
219
|
"direction": direction,
|
191
220
|
"size": remaining_size if size is not None else None,
|
192
221
|
"cross_size": cross_size_for_op,
|
193
|
-
"cross_alignment": cross_alignment,
|
222
|
+
"cross_alignment": cross_alignment, # Pass alignment
|
194
223
|
"include_source": False,
|
195
224
|
# Pass other relevant kwargs if Region._direction uses them (e.g. strict_type)
|
196
|
-
**{k: v for k, v in kwargs.items() if k in [
|
225
|
+
**{k: v for k, v in kwargs.items() if k in ["strict_type", "first_match_only"]},
|
197
226
|
}
|
198
227
|
initial_region_from_op = source_for_op_call._direction(**initial_op_params)
|
199
228
|
|
200
229
|
# 2. Clip this initial region to the current flow segment's boundaries.
|
201
230
|
clipped_search_area = current_segment.clip(initial_region_from_op)
|
202
|
-
segment_contribution = clipped_search_area
|
231
|
+
segment_contribution = clipped_search_area # Default contribution
|
203
232
|
|
204
233
|
# 3. If 'until' is specified, search for it *within* the clipped_search_area.
|
205
|
-
if
|
234
|
+
if (
|
235
|
+
until
|
236
|
+
and clipped_search_area
|
237
|
+
and clipped_search_area.width > 0
|
238
|
+
and clipped_search_area.height > 0
|
239
|
+
):
|
206
240
|
# kwargs for find_all are the general kwargs passed to _flow_direction
|
207
241
|
until_matches = clipped_search_area.find_all(until, **kwargs)
|
208
|
-
|
242
|
+
|
209
243
|
if until_matches:
|
210
244
|
potential_hit: Optional["PhysicalElement"] = None
|
211
|
-
if direction == "below":
|
212
|
-
|
213
|
-
elif direction == "
|
214
|
-
|
215
|
-
|
245
|
+
if direction == "below":
|
246
|
+
potential_hit = until_matches.sort(key=lambda m: m.top).first
|
247
|
+
elif direction == "above":
|
248
|
+
potential_hit = until_matches.sort(
|
249
|
+
key=lambda m: m.bottom, reverse=True
|
250
|
+
).first
|
251
|
+
elif direction == "right":
|
252
|
+
potential_hit = until_matches.sort(key=lambda m: m.x0).first
|
253
|
+
elif direction == "left":
|
254
|
+
potential_hit = until_matches.sort(
|
255
|
+
key=lambda m: m.x1, reverse=True
|
256
|
+
).first
|
257
|
+
|
216
258
|
if potential_hit:
|
217
|
-
boundary_element_hit = potential_hit
|
259
|
+
boundary_element_hit = potential_hit # Set the overall boundary flag
|
218
260
|
# Adjust segment_contribution to stop at this boundary_element_hit.
|
219
261
|
if is_primary_vertical:
|
220
262
|
if direction == "below":
|
221
|
-
edge =
|
263
|
+
edge = (
|
264
|
+
boundary_element_hit.bottom
|
265
|
+
if include_endpoint
|
266
|
+
else (boundary_element_hit.top - 1)
|
267
|
+
)
|
222
268
|
else: # direction == "above"
|
223
|
-
edge =
|
269
|
+
edge = (
|
270
|
+
boundary_element_hit.top
|
271
|
+
if include_endpoint
|
272
|
+
else (boundary_element_hit.bottom + 1)
|
273
|
+
)
|
224
274
|
segment_contribution = segment_contribution.clip(
|
225
275
|
bottom=edge if direction == "below" else None,
|
226
|
-
top=edge if direction == "above" else None
|
276
|
+
top=edge if direction == "above" else None,
|
227
277
|
)
|
228
|
-
else:
|
278
|
+
else:
|
229
279
|
if direction == "right":
|
230
|
-
edge =
|
280
|
+
edge = (
|
281
|
+
boundary_element_hit.x1
|
282
|
+
if include_endpoint
|
283
|
+
else (boundary_element_hit.x0 - 1)
|
284
|
+
)
|
231
285
|
else: # direction == "left"
|
232
|
-
edge =
|
286
|
+
edge = (
|
287
|
+
boundary_element_hit.x0
|
288
|
+
if include_endpoint
|
289
|
+
else (boundary_element_hit.x1 + 1)
|
290
|
+
)
|
233
291
|
segment_contribution = segment_contribution.clip(
|
234
292
|
right=edge if direction == "right" else None,
|
235
|
-
left=edge if direction == "left" else None
|
293
|
+
left=edge if direction == "left" else None,
|
236
294
|
)
|
237
|
-
else:
|
295
|
+
else:
|
238
296
|
candidate_region_in_segment = current_segment
|
239
297
|
if until and not boundary_element_hit:
|
240
298
|
until_matches = candidate_region_in_segment.find_all(until, **kwargs)
|
241
299
|
if until_matches:
|
242
300
|
potential_hit = None
|
243
|
-
if direction == "below":
|
244
|
-
|
245
|
-
elif direction == "
|
246
|
-
|
247
|
-
|
301
|
+
if direction == "below":
|
302
|
+
potential_hit = until_matches.sort(key=lambda m: m.top).first
|
303
|
+
elif direction == "above":
|
304
|
+
potential_hit = until_matches.sort(
|
305
|
+
key=lambda m: m.bottom, reverse=True
|
306
|
+
).first
|
307
|
+
elif direction == "right":
|
308
|
+
potential_hit = until_matches.sort(key=lambda m: m.x0).first
|
309
|
+
elif direction == "left":
|
310
|
+
potential_hit = until_matches.sort(
|
311
|
+
key=lambda m: m.x1, reverse=True
|
312
|
+
).first
|
313
|
+
|
248
314
|
if potential_hit:
|
249
315
|
boundary_element_hit = potential_hit
|
250
316
|
if is_primary_vertical:
|
251
317
|
if direction == "below":
|
252
|
-
edge =
|
318
|
+
edge = (
|
319
|
+
boundary_element_hit.bottom
|
320
|
+
if include_endpoint
|
321
|
+
else (boundary_element_hit.top - 1)
|
322
|
+
)
|
253
323
|
else: # direction == "above"
|
254
|
-
edge =
|
255
|
-
|
256
|
-
|
324
|
+
edge = (
|
325
|
+
boundary_element_hit.top
|
326
|
+
if include_endpoint
|
327
|
+
else (boundary_element_hit.bottom + 1)
|
328
|
+
)
|
329
|
+
candidate_region_in_segment = candidate_region_in_segment.clip(
|
330
|
+
bottom=edge if direction == "below" else None,
|
331
|
+
top=edge if direction == "above" else None,
|
332
|
+
)
|
333
|
+
else:
|
257
334
|
if direction == "right":
|
258
|
-
edge =
|
335
|
+
edge = (
|
336
|
+
boundary_element_hit.x1
|
337
|
+
if include_endpoint
|
338
|
+
else (boundary_element_hit.x0 - 1)
|
339
|
+
)
|
259
340
|
else: # direction == "left"
|
260
|
-
edge =
|
261
|
-
|
341
|
+
edge = (
|
342
|
+
boundary_element_hit.x0
|
343
|
+
if include_endpoint
|
344
|
+
else (boundary_element_hit.x1 + 1)
|
345
|
+
)
|
346
|
+
candidate_region_in_segment = candidate_region_in_segment.clip(
|
347
|
+
right=edge if direction == "right" else None,
|
348
|
+
left=edge if direction == "left" else None,
|
349
|
+
)
|
262
350
|
segment_contribution = candidate_region_in_segment
|
263
351
|
|
264
|
-
if
|
352
|
+
if (
|
353
|
+
segment_contribution
|
354
|
+
and segment_contribution.width > 0
|
355
|
+
and segment_contribution.height > 0
|
356
|
+
and size is not None
|
357
|
+
):
|
265
358
|
current_part_consumed_size = 0.0
|
266
|
-
if is_primary_vertical:
|
359
|
+
if is_primary_vertical:
|
267
360
|
current_part_consumed_size = segment_contribution.height
|
268
361
|
if current_part_consumed_size > remaining_size:
|
269
|
-
new_edge = (
|
270
|
-
|
271
|
-
|
272
|
-
|
362
|
+
new_edge = (
|
363
|
+
(segment_contribution.top + remaining_size)
|
364
|
+
if is_forward
|
365
|
+
else (segment_contribution.bottom - remaining_size)
|
366
|
+
)
|
367
|
+
segment_contribution = segment_contribution.clip(
|
368
|
+
bottom=new_edge if is_forward else None,
|
369
|
+
top=new_edge if not is_forward else None,
|
370
|
+
)
|
371
|
+
current_part_consumed_size = remaining_size
|
372
|
+
else:
|
273
373
|
current_part_consumed_size = segment_contribution.width
|
274
374
|
if current_part_consumed_size > remaining_size:
|
275
|
-
new_edge = (
|
276
|
-
|
277
|
-
|
375
|
+
new_edge = (
|
376
|
+
(segment_contribution.x0 + remaining_size)
|
377
|
+
if is_forward
|
378
|
+
else (segment_contribution.x1 - remaining_size)
|
379
|
+
)
|
380
|
+
segment_contribution = segment_contribution.clip(
|
381
|
+
right=new_edge if is_forward else None,
|
382
|
+
left=new_edge if not is_forward else None,
|
383
|
+
)
|
384
|
+
current_part_consumed_size = remaining_size
|
278
385
|
remaining_size -= current_part_consumed_size
|
279
|
-
|
280
|
-
if
|
386
|
+
|
387
|
+
if (
|
388
|
+
segment_contribution
|
389
|
+
and segment_contribution.width > 0
|
390
|
+
and segment_contribution.height > 0
|
391
|
+
):
|
281
392
|
collected_constituent_regions.append(segment_contribution)
|
282
393
|
|
283
394
|
# If boundary was hit in this segment, and we are not on the start segment (where we might still collect part of it)
|
284
395
|
# or if we are on the start segment AND the contribution became zero (e.g. until was immediate)
|
285
|
-
if boundary_element_hit and (
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
396
|
+
if boundary_element_hit and (
|
397
|
+
current_segment_idx != start_segment_index
|
398
|
+
or not segment_contribution
|
399
|
+
or (segment_contribution.width <= 0 or segment_contribution.height <= 0)
|
400
|
+
):
|
401
|
+
break # Stop iterating through more segments
|
402
|
+
|
403
|
+
is_logically_last_segment = (
|
404
|
+
is_forward and current_segment_idx == len(self.flow.segments) - 1
|
405
|
+
) or (not is_forward and current_segment_idx == 0)
|
290
406
|
if not is_logically_last_segment and self.flow.segment_gap > 0 and size is not None:
|
291
|
-
if remaining_size > 0
|
292
|
-
|
293
|
-
|
294
|
-
from .region import FlowRegion as RuntimeFlowRegion
|
407
|
+
if remaining_size > 0:
|
408
|
+
remaining_size -= self.flow.segment_gap
|
409
|
+
|
410
|
+
from .region import FlowRegion as RuntimeFlowRegion # Ensure it's available for return
|
411
|
+
|
295
412
|
return RuntimeFlowRegion(
|
296
413
|
flow=self.flow,
|
297
414
|
constituent_regions=collected_constituent_regions,
|
298
415
|
source_flow_element=self,
|
299
|
-
boundary_element_found=boundary_element_hit
|
416
|
+
boundary_element_found=boundary_element_hit,
|
300
417
|
)
|
301
418
|
|
302
|
-
# --- Public Directional Methods ---
|
419
|
+
# --- Public Directional Methods ---
|
303
420
|
# These will largely mirror DirectionalMixin but call _flow_direction.
|
304
421
|
|
305
422
|
def above(
|
306
423
|
self,
|
307
|
-
height: Optional[float] = None,
|
308
|
-
width_ratio: Optional[float] = None,
|
309
|
-
width_absolute: Optional[float] = None,
|
310
|
-
width_alignment: str = "center",
|
424
|
+
height: Optional[float] = None,
|
425
|
+
width_ratio: Optional[float] = None,
|
426
|
+
width_absolute: Optional[float] = None,
|
427
|
+
width_alignment: str = "center",
|
311
428
|
until: Optional[str] = None,
|
312
429
|
include_endpoint: bool = True,
|
313
430
|
**kwargs,
|
314
|
-
) -> "FlowRegion":
|
431
|
+
) -> "FlowRegion": # Stringized
|
315
432
|
if self.flow.arrangement == "vertical":
|
316
433
|
return self._flow_direction(
|
317
|
-
direction="above",
|
318
|
-
|
319
|
-
|
434
|
+
direction="above",
|
435
|
+
size=height,
|
436
|
+
cross_size_ratio=width_ratio,
|
437
|
+
cross_size_absolute=width_absolute,
|
438
|
+
cross_alignment=width_alignment,
|
439
|
+
until=until,
|
440
|
+
include_endpoint=include_endpoint,
|
441
|
+
**kwargs,
|
442
|
+
)
|
443
|
+
else:
|
444
|
+
raise NotImplementedError(
|
445
|
+
"'above' in a horizontal flow is ambiguous with current 1D flow logic and not yet implemented."
|
320
446
|
)
|
321
|
-
else:
|
322
|
-
raise NotImplementedError("'above' in a horizontal flow is ambiguous with current 1D flow logic and not yet implemented.")
|
323
447
|
|
324
448
|
def below(
|
325
449
|
self,
|
326
|
-
height: Optional[float] = None,
|
450
|
+
height: Optional[float] = None,
|
327
451
|
width_ratio: Optional[float] = None,
|
328
452
|
width_absolute: Optional[float] = None,
|
329
453
|
width_alignment: str = "center",
|
330
454
|
until: Optional[str] = None,
|
331
455
|
include_endpoint: bool = True,
|
332
456
|
**kwargs,
|
333
|
-
) -> "FlowRegion":
|
457
|
+
) -> "FlowRegion": # Stringized
|
334
458
|
if self.flow.arrangement == "vertical":
|
335
459
|
return self._flow_direction(
|
336
|
-
direction="below",
|
337
|
-
|
338
|
-
|
460
|
+
direction="below",
|
461
|
+
size=height,
|
462
|
+
cross_size_ratio=width_ratio,
|
463
|
+
cross_size_absolute=width_absolute,
|
464
|
+
cross_alignment=width_alignment,
|
465
|
+
until=until,
|
466
|
+
include_endpoint=include_endpoint,
|
467
|
+
**kwargs,
|
468
|
+
)
|
469
|
+
else:
|
470
|
+
raise NotImplementedError(
|
471
|
+
"'below' in a horizontal flow is ambiguous with current 1D flow logic and not yet implemented."
|
339
472
|
)
|
340
|
-
else:
|
341
|
-
raise NotImplementedError("'below' in a horizontal flow is ambiguous with current 1D flow logic and not yet implemented.")
|
342
473
|
|
343
474
|
def left(
|
344
475
|
self,
|
345
|
-
width: Optional[float] = None,
|
346
|
-
height_ratio: Optional[float] = None,
|
347
|
-
height_absolute: Optional[float] = None,
|
348
|
-
height_alignment: str = "center",
|
476
|
+
width: Optional[float] = None,
|
477
|
+
height_ratio: Optional[float] = None,
|
478
|
+
height_absolute: Optional[float] = None,
|
479
|
+
height_alignment: str = "center",
|
349
480
|
until: Optional[str] = None,
|
350
481
|
include_endpoint: bool = True,
|
351
482
|
**kwargs,
|
352
|
-
) -> "FlowRegion":
|
483
|
+
) -> "FlowRegion": # Stringized
|
353
484
|
if self.flow.arrangement == "horizontal":
|
354
485
|
return self._flow_direction(
|
355
|
-
direction="left",
|
356
|
-
|
357
|
-
|
486
|
+
direction="left",
|
487
|
+
size=width,
|
488
|
+
cross_size_ratio=height_ratio,
|
489
|
+
cross_size_absolute=height_absolute,
|
490
|
+
cross_alignment=height_alignment,
|
491
|
+
until=until,
|
492
|
+
include_endpoint=include_endpoint,
|
493
|
+
**kwargs,
|
494
|
+
)
|
495
|
+
else:
|
496
|
+
raise NotImplementedError(
|
497
|
+
"'left' in a vertical flow is ambiguous with current 1D flow logic and not yet implemented."
|
358
498
|
)
|
359
|
-
else:
|
360
|
-
raise NotImplementedError("'left' in a vertical flow is ambiguous with current 1D flow logic and not yet implemented.")
|
361
499
|
|
362
500
|
def right(
|
363
501
|
self,
|
364
|
-
width: Optional[float] = None,
|
502
|
+
width: Optional[float] = None,
|
365
503
|
height_ratio: Optional[float] = None,
|
366
504
|
height_absolute: Optional[float] = None,
|
367
505
|
height_alignment: str = "center",
|
368
506
|
until: Optional[str] = None,
|
369
507
|
include_endpoint: bool = True,
|
370
508
|
**kwargs,
|
371
|
-
) -> "FlowRegion":
|
509
|
+
) -> "FlowRegion": # Stringized
|
372
510
|
if self.flow.arrangement == "horizontal":
|
373
511
|
return self._flow_direction(
|
374
|
-
direction="right",
|
375
|
-
|
376
|
-
|
512
|
+
direction="right",
|
513
|
+
size=width,
|
514
|
+
cross_size_ratio=height_ratio,
|
515
|
+
cross_size_absolute=height_absolute,
|
516
|
+
cross_alignment=height_alignment,
|
517
|
+
until=until,
|
518
|
+
include_endpoint=include_endpoint,
|
519
|
+
**kwargs,
|
520
|
+
)
|
521
|
+
else:
|
522
|
+
raise NotImplementedError(
|
523
|
+
"'right' in a vertical flow is ambiguous with current 1D flow logic and not yet implemented."
|
377
524
|
)
|
378
|
-
else:
|
379
|
-
raise NotImplementedError("'right' in a vertical flow is ambiguous with current 1D flow logic and not yet implemented.")
|
380
525
|
|
381
526
|
def __repr__(self) -> str:
|
382
|
-
return f"<FlowElement for {self.physical_object.__class__.__name__} {self.bbox} in {self.flow}>"
|
527
|
+
return f"<FlowElement for {self.physical_object.__class__.__name__} {self.bbox} in {self.flow}>"
|