This commit is contained in:
Andrej730
2025-11-24 10:59:03 +05:00
parent 97a501036b
commit 497ebc55e7
7 changed files with 191 additions and 158 deletions
+163 -128
View File
@@ -52,6 +52,7 @@ ARC_LINE_WIDTH = 0.015
PRECISION_MODE_MULTIPLIER = 0.1
class SnapManager:
"""Manages snap point visualization and mesh snapping."""
@@ -64,16 +65,14 @@ class SnapManager:
"""Set snap point and register draw handler if needed."""
self._snap_point = point
if self._draw_handler is None and point is not None:
self._draw_handler = bpy.types.SpaceView3D.draw_handler_add(
self._draw, (), 'WINDOW', 'POST_VIEW'
)
self._draw_handler = bpy.types.SpaceView3D.draw_handler_add(self._draw, (), "WINDOW", "POST_VIEW")
self._redraw_viewport()
def clear(self) -> None:
"""Clear snap point and unregister handler."""
self._snap_point = None
if self._draw_handler is not None:
bpy.types.SpaceView3D.draw_handler_remove(self._draw_handler, 'WINDOW')
bpy.types.SpaceView3D.draw_handler_remove(self._draw_handler, "WINDOW")
self._draw_handler = None
self._redraw_viewport()
@@ -83,17 +82,15 @@ class SnapManager:
return
if self._shader is None:
self._shader = gpu.shader.from_builtin('UNIFORM_COLOR')
self._shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self._shader.bind()
self._shader.uniform_float("color", SNAP_CIRCLE_COLOR)
gpu.state.line_width_set(SNAP_LINE_WIDTH)
for plane in ('XY', 'XZ', 'YZ'):
vertices = generate_circle_vertices(
self._snap_point, SNAP_CIRCLE_RADIUS, SNAP_CIRCLE_SEGMENTS, plane
)
batch = batch_for_shader(self._shader, 'LINE_STRIP', {"pos": vertices})
for plane in ("XY", "XZ", "YZ"):
vertices = generate_circle_vertices(self._snap_point, SNAP_CIRCLE_RADIUS, SNAP_CIRCLE_SEGMENTS, plane)
batch = batch_for_shader(self._shader, "LINE_STRIP", {"pos": vertices})
batch.draw(self._shader)
gpu.state.line_width_set(1.0)
@@ -102,15 +99,12 @@ class SnapManager:
def _redraw_viewport() -> None:
"""Force 3D viewport redraw."""
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
if area.type == "VIEW_3D":
area.tag_redraw()
@staticmethod
def snap_to_mesh(
location: Vector,
context: bpy.types.Context,
axis_vector: Vector,
active_obj: bpy.types.Object
location: Vector, context: bpy.types.Context, axis_vector: Vector, active_obj: bpy.types.Object
) -> Vector:
"""Snap a location to the nearest mesh element if snapping is enabled.
@@ -131,9 +125,7 @@ class SnapManager:
snap_elements = tool_settings.snap_elements_base
mesh_objects = [
obj
for obj in context.visible_objects
if obj.type == "MESH" and obj != active_obj and obj.visible_get()
obj for obj in context.visible_objects if obj.type == "MESH" and obj != active_obj and obj.visible_get()
]
if not mesh_objects:
@@ -168,14 +160,14 @@ class SnapManager:
world_vertices = [obj.matrix_world @ v.co for v in mesh.vertices]
if 'VERTEX' in snap_elements:
if "VERTEX" in snap_elements:
for v_co in world_vertices:
dist = (v_co - location).length
if dist < closest_distance:
closest_distance = dist
closest_point = v_co
if 'EDGE' in snap_elements:
if "EDGE" in snap_elements:
for edge in mesh.edges:
v1 = world_vertices[edge.vertices[0]]
v2 = world_vertices[edge.vertices[1]]
@@ -192,10 +184,8 @@ class SnapManager:
closest_distance = dist
closest_point = closest_on_edge
if 'FACE' in snap_elements:
bvh = BVHTree.FromPolygons(
world_vertices, [p.vertices for p in mesh.polygons]
)
if "FACE" in snap_elements:
bvh = BVHTree.FromPolygons(world_vertices, [p.vertices for p in mesh.polygons])
nearest_loc, normal, index, dist = bvh.find_nearest(location)
@@ -223,19 +213,13 @@ def clear_snap_point() -> None:
def snap_to_mesh(
location: Vector,
context: bpy.types.Context,
axis_vector: Vector,
active_obj: bpy.types.Object
location: Vector, context: bpy.types.Context, axis_vector: Vector, active_obj: bpy.types.Object
) -> Vector:
return _snap_manager.snap_to_mesh(location, context, axis_vector, active_obj)
def generate_circle_vertices(
center: Tuple[float, float, float],
radius: float,
segments: int,
plane: str = 'XY'
center: Tuple[float, float, float], radius: float, segments: int, plane: str = "XY"
) -> List[Tuple[float, float, float]]:
"""Generate circle vertices in specified plane.
@@ -254,9 +238,9 @@ def generate_circle_vertices(
cos_a = radius * math.cos(angle)
sin_a = radius * math.sin(angle)
if plane == 'XY':
if plane == "XY":
vertices.append((center[0] + cos_a, center[1] + sin_a, center[2]))
elif plane == 'XZ':
elif plane == "XZ":
vertices.append((center[0] + cos_a, center[1], center[2] + sin_a))
else:
vertices.append((center[0], center[1] + cos_a, center[2] + sin_a))
@@ -265,10 +249,7 @@ def generate_circle_vertices(
def create_quarter_circle_arc(
radius: float = 1.0,
segments: int = ARC_SEGMENTS,
direction: str = "LEFT",
line_width: float = ARC_LINE_WIDTH
radius: float = 1.0, segments: int = ARC_SEGMENTS, direction: str = "LEFT", line_width: float = ARC_LINE_WIDTH
) -> Tuple[Tuple[float, float, float], ...]:
"""Create a quarter circle arc with cross-section thickness for visibility from all angles.
@@ -307,27 +288,35 @@ def create_quarter_circle_arc(
if length > 0:
px, py = -dy / length * half_width, dx / length * half_width
arc_triangles.extend([
(x1 + px, y1 + py, 0.0),
(x1 - px, y1 - py, 0.0),
(x2 + px, y2 + py, 0.0),
])
arc_triangles.extend([
(x2 + px, y2 + py, 0.0),
(x1 - px, y1 - py, 0.0),
(x2 - px, y2 - py, 0.0),
])
arc_triangles.extend(
[
(x1 + px, y1 + py, 0.0),
(x1 - px, y1 - py, 0.0),
(x2 + px, y2 + py, 0.0),
]
)
arc_triangles.extend(
[
(x2 + px, y2 + py, 0.0),
(x1 - px, y1 - py, 0.0),
(x2 - px, y2 - py, 0.0),
]
)
arc_triangles.extend([
(x1, y1, -half_width),
(x2, y2, -half_width),
(x1, y1, +half_width),
])
arc_triangles.extend([
(x1, y1, +half_width),
(x2, y2, -half_width),
(x2, y2, +half_width),
])
arc_triangles.extend(
[
(x1, y1, -half_width),
(x2, y2, -half_width),
(x1, y1, +half_width),
]
)
arc_triangles.extend(
[
(x1, y1, +half_width),
(x2, y2, -half_width),
(x2, y2, +half_width),
]
)
return tuple(arc_triangles)
@@ -387,8 +376,10 @@ class GizmoMovable(bpy.types.Gizmo):
axis_direction = self.get_axis_direction()
result = intersect_line_line(
view_origin, view_origin + view_direction * 1000,
self.start_location, self.start_location + axis_direction * 1000
view_origin,
view_origin + view_direction * 1000,
self.start_location,
self.start_location + axis_direction * 1000,
)
current_3d = result[1] if result else self.start_location
@@ -414,13 +405,7 @@ class GizmoMovable(bpy.types.Gizmo):
return {"RUNNING_MODAL"}
def _update_header(
self,
context: bpy.types.Context,
value: float,
is_snapping: bool,
is_precision: bool
) -> None:
def _update_header(self, context: bpy.types.Context, value: float, is_snapping: bool, is_precision: bool) -> None:
header_text = f"Value: {value:.3f}m"
hints = []
if is_snapping:
@@ -446,25 +431,57 @@ class GizmoLock(bpy.types.Gizmo):
)
tris_closed = (
(0.16803650558, 0.18791499734, 0.0), (-0.07805634290, 0.18791499734, 0.0), (0.16803650558, 0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0), (-0.07805634290, 0.18791499734, 0.0), (-0.07805634290, 0.44701099396, 0.0),
(0.20165449381, 0.13603900373, 0.0), (-0.11167433113, 0.13603900373, 0.0), (0.20165449381, -0.44701099396, 0.0),
(0.20165449381, -0.44701099396, 0.0), (-0.11167433113, 0.13603900373, 0.0), (-0.11167433113, -0.44701099396, 0.0),
(-0.07805634290, 0.18791499734, 0.0), (-0.39353451133, 0.18791499734, 0.0), (-0.07805634290, 0.44701099396, 0.0),
(-0.07805634290, 0.44701099396, 0.0), (-0.39353451133, 0.18791499734, 0.0), (-0.39353451133, 0.30746498704, 0.0),
(-0.44701099396, 0.18791499734, 0.0), (-0.44701099396, -0.04477182776, 0.0), (-0.39353451133, 0.18791499734, 0.0),
(-0.39353451133, 0.18791499734, 0.0), (-0.44701099396, -0.04477182776, 0.0), (-0.39353451133, -0.04477182776, 0.0),
(0.16803650558, 0.18791499734, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(0.20165449381, 0.13603900373, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(-0.11167433113, -0.44701099396, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.39353451133, 0.30746498704, 0.0),
(-0.44701099396, 0.18791499734, 0.0),
(-0.44701099396, -0.04477182776, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.39353451133, 0.18791499734, 0.0),
(-0.44701099396, -0.04477182776, 0.0),
(-0.39353451133, -0.04477182776, 0.0),
)
tris_open = (
(0.16803650558, 0.18791499734, 0.0), (-0.07805634290, 0.18791499734, 0.0), (0.16803650558, 0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0), (-0.07805634290, 0.18791499734, 0.0), (-0.07805634290, 0.44701099396, 0.0),
(0.20165449381, 0.13603900373, 0.0), (-0.11167433113, 0.13603900373, 0.0), (0.20165449381, -0.44701099396, 0.0),
(0.20165449381, -0.44701099396, 0.0), (-0.11167433113, 0.13603900373, 0.0), (-0.11167433113, -0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0), (-0.07805634290, 0.44701099396, 0.0), (0.16803650558, 0.70610702038, 0.0),
(0.16803650558, 0.70610702038, 0.0), (-0.07805634290, 0.44701099396, 0.0), (-0.07805634290, 0.58656096458, 0.0),
(-0.11167433113, 0.44701099396, 0.0), (-0.11167433113, 0.73432201147, 0.0), (-0.07805634290, 0.44701099396, 0.0),
(-0.07805634290, 0.44701099396, 0.0), (-0.11167433113, 0.73432201147, 0.0), (-0.07805634290, 0.73432201147, 0.0),
(0.16803650558, 0.18791499734, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(-0.07805634290, 0.18791499734, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(0.20165449381, 0.13603900373, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(0.20165449381, -0.44701099396, 0.0),
(-0.11167433113, 0.13603900373, 0.0),
(-0.11167433113, -0.44701099396, 0.0),
(0.16803650558, 0.44701099396, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(0.16803650558, 0.70610702038, 0.0),
(0.16803650558, 0.70610702038, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.07805634290, 0.58656096458, 0.0),
(-0.11167433113, 0.44701099396, 0.0),
(-0.11167433113, 0.73432201147, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.07805634290, 0.44701099396, 0.0),
(-0.11167433113, 0.73432201147, 0.0),
(-0.07805634290, 0.73432201147, 0.0),
)
def get_custom_shape(self, context: bpy.types.Context):
@@ -656,40 +673,52 @@ class GizmoArrow(GizmoMovable):
"""Generate arrow geometry along +X axis."""
triangles = []
triangles.extend([
(0, -ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
(0, +ARROW_WIDTH, 0),
])
triangles.extend([
(0, +ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, +ARROW_WIDTH, 0),
])
triangles.extend(
[
(0, -ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
(0, +ARROW_WIDTH, 0),
]
)
triangles.extend(
[
(0, +ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
(ARROW_SHAFT_LENGTH, +ARROW_WIDTH, 0),
]
)
triangles.extend([
(0, 0, -ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
(0, 0, +ARROW_WIDTH),
])
triangles.extend([
(0, 0, +ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, +ARROW_WIDTH),
])
triangles.extend(
[
(0, 0, -ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
(0, 0, +ARROW_WIDTH),
]
)
triangles.extend(
[
(0, 0, +ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
(ARROW_SHAFT_LENGTH, 0, +ARROW_WIDTH),
]
)
head_width = ARROW_WIDTH * ARROW_HEAD_WIDTH_MULTIPLIER
triangles.extend([
(ARROW_SHAFT_LENGTH, -head_width, 0),
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, +head_width, 0),
])
triangles.extend(
[
(ARROW_SHAFT_LENGTH, -head_width, 0),
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, +head_width, 0),
]
)
triangles.extend([
(ARROW_SHAFT_LENGTH, 0, -head_width),
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, 0, +head_width),
])
triangles.extend(
[
(ARROW_SHAFT_LENGTH, 0, -head_width),
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, 0, +head_width),
]
)
for i in range(ARROW_CIRCLE_SEGMENTS):
angle1 = (2 * math.pi * i) / ARROW_CIRCLE_SEGMENTS
@@ -700,11 +729,13 @@ class GizmoArrow(GizmoMovable):
y2 = head_width * math.cos(angle2)
z2 = head_width * math.sin(angle2)
triangles.extend([
(ARROW_SHAFT_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, y1, z1),
(ARROW_SHAFT_LENGTH, y2, z2),
])
triangles.extend(
[
(ARROW_SHAFT_LENGTH, 0, 0),
(ARROW_SHAFT_LENGTH, y1, z1),
(ARROW_SHAFT_LENGTH, y2, z2),
]
)
return tuple(triangles)
@@ -746,17 +777,21 @@ class GizmoCone(GizmoMovable):
y2 = CONE_RADIUS * math.cos(angle2)
z2 = CONE_RADIUS * math.sin(angle2)
triangles.extend([
(cone_tip_x, 0, 0),
(0, y1, z1),
(0, y2, z2),
])
triangles.extend(
[
(cone_tip_x, 0, 0),
(0, y1, z1),
(0, y2, z2),
]
)
triangles.extend([
(0, 0, 0),
(0, y2, z2),
(0, y1, z1),
])
triangles.extend(
[
(0, 0, 0),
(0, y2, z2),
(0, y1, z1),
]
)
return tuple(triangles)
@@ -130,7 +130,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
# Skip spatial elements - they should not be deleted even if empty
if aggregate.is_a("IfcSpatialElement"):
continue
related_objects = ifcopenshell.util.element.get_parts(aggregate)
if len(related_objects) == 0:
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
@@ -745,10 +745,10 @@ class DimensionDecorator(BaseDecorator):
if not show_description_only:
length = (v1 - v0).length
text = self.format_value(
context,
length,
context,
length,
suppress_zero_inches=dimension_data["suppress_zero_inches"],
custom_unit=dimension_data["custom_unit"]
custom_unit=dimension_data["custom_unit"],
)
if isinstance(self, DiameterDecorator):
text = "D" + text
@@ -1203,11 +1203,11 @@ class PlanLevelDecorator(BaseDecorator):
def get_text():
abs_z = verts[0].z
z = helper.get_relative_z(obj, element, abs_z)
# Z is in meters (Blender world space), convert to project units (feet)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
z = z / unit_scale # Convert meters to feet
rl = self.format_value(context, z, in_unit_length=True)
text = "{}{}".format("" if z < 0 else "+", rl)
return text
@@ -1284,7 +1284,7 @@ class SectionLevelDecorator(BaseDecorator):
# Z is in meters (Blender world space), convert to project units (feet)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
z = z / unit_scale # Convert meters to feet
rl = self.format_value(context, z, in_unit_length=True)
text = "RL {}{}".format("" if z < 0 else "+", rl)
return text
@@ -1300,6 +1300,7 @@ class SectionLevelDecorator(BaseDecorator):
)
break # support only 1 label
class BreakDecorator(BaseDecorator):
"""Decorator for breakline objects
- first edge of a mesh with zigzag thingy in the middle
+16 -18
View File
@@ -121,11 +121,12 @@ class BoundingBox:
retVal = True
return retVal
def find_best_precision(fractional_inches, max_precision=256, base_tolerance=0.0001):
"""
Find the simplest fraction denominator that accurately represents the value.
Uses a sliding tolerance - smaller denominators get more tolerance.
:param fractional_inches: The fractional part of inches (0.0 to 1.0)
:param max_precision: Maximum denominator to try (256 for imperial)
:param base_tolerance: Base tolerance in inches (very strict)
@@ -133,29 +134,29 @@ def find_best_precision(fractional_inches, max_precision=256, base_tolerance=0.0
"""
if abs(fractional_inches) < 0.00001:
return 1 # No fraction needed
# Try denominators from coarsest to finest: 2, 4, 8, 16, 32, 64, 128, 256
for exp in range(1, 9): # 2^1 to 2^8
denom = 2 ** exp
denom = 2**exp
if denom > max_precision:
break
# Find nearest numerator for this denominator
numer = round(fractional_inches * denom)
# Skip if numerator is 0
if numer == 0:
continue
# Check if this fraction is close enough
fraction_value = numer / denom
error = abs(fraction_value - fractional_inches)
# Very strict tolerance - only simplify if it's a near-perfect match
# This keeps 3/256 as 3/256, but allows floating point errors to be cleaned up
if error < base_tolerance:
return denom
# If no simpler fraction works, use max precision
return max_precision
@@ -233,11 +234,10 @@ def format_distance(
if isArea:
toInches = 1550
inPerFoot = 144
decInches = value * toInches
decFeet = decInches / 12
if not precision:
precision = 256
elif precision == "1":
@@ -258,10 +258,10 @@ def format_distance(
else:
feet = 0
# Separate Fractional Inches
# Separate Fractional Inches
decInches = abs(decInches) # ignore the sign for inches
inches = math.floor(decInches) # remove decimal
# Clean up floating point errors
fractional_inches = decInches - inches
tolerance = 0.01 # About 1/100 of an inch
@@ -280,11 +280,11 @@ def format_distance(
else:
frac = round(base * fractional_inches)
# Set proper numerator and denominator
if frac != base:
# Simplify using GCD
from math import gcd
divisor = gcd(int(frac), int(base))
frac = int(frac / divisor)
base = int(base / divisor)
@@ -312,14 +312,14 @@ def format_distance(
tx_dist += str(feet) + "'"
if not feet and not add_inches:
tx_dist += str(feet) + "'"
# Add "0' - " when we have inches but no feet
# But only add " - " separator if we actually have inches to show
if not feet and add_inches:
tx_dist += "0' - "
elif feet and add_inches:
tx_dist += " - "
if not feet and value < 0:
tx_dist += "-"
if add_inches:
@@ -341,13 +341,11 @@ def format_distance(
# Only add inch symbol if we actually added inch content
if inches > 0 or frac > 0 or feet == 0:
tx_dist += '"'
if precision == "12" and unit_system == "IMPERIAL":
tx_dist = str(round(decFeet)) + "'"
if tx_dist == '"':
tx_dist = "0' - 0\""
tx_dist = "0' - 0\""
else:
fmt = "%1.3f"
sq_feet = round(value * toInches / inPerFoot, 4)
@@ -731,6 +731,7 @@ class GizmoDoorEdition(bpy.types.GizmoGroup):
return cls.COLOR_GREEN
else:
return cls.COLOR_BLUE
gizmo_props = [
{"attr_name": "overall_height", "axis": (0, 0, 1)},
{"attr_name": "overall_width", "axis": (1, 0, 0)},
+1 -3
View File
@@ -777,9 +777,7 @@ def draw_stair_properties(layout: bpy.types.UILayout, props: bpy.types.PropertyG
if prop_name == "custom_first_last_tread_run":
# Draw the lock toggle
row_lock = layout.row(align=True)
lock_text = (
"Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
)
lock_text = "Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
row_lock.prop(
props,
"custom_tread_lock",
+2 -2
View File
@@ -1625,11 +1625,11 @@ class Drawing(bonsai.core.tool.Drawing):
xmin, xmax, ymin, ymax = helper.ortho_view_frame(camera.data)[:4]
rl = ifcopenshell.util.placement.get_local_placement(storey.ObjectPlacement)[2][3]
# Convert RL from project units (feet) to meters for Blender world space
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
rl_meters = rl * unit_scale
y = (camera.matrix_world.inverted() @ Vector((0.0, 0.0, rl_meters))).y
if y < ymin or y > ymax:
return