// Copyright (c) 2026 BigSaltyMan. BSL-1.0; see LICENSE.
///|
/// Inclusive coordinate limit for geometry, not Point64 storage.
pub const COORDINATE_LIMIT : Int64 = 1_000_000_000L
///|
pub(all) enum Stage {
ValidateInputs
Cross
Orientation
Bounds
Area2
PointOnSegment
PointInPolygon
NormalizePath
NormalizePaths
EnginePrepare
ScanEvents
ActiveEdges
ScanBand
ScanTop
Intersections
HorizontalEvents
OutputTopology
BooleanOp
PolyTree
OffsetParameters
OffsetGenerate
OffsetCleanup
StrokeParameters
StrokeGenerate
StrokeCleanup
DecimalParameters
DecimalQuantize
DecimalOutput
SimplifyParameters
SimplifyEvaluate
SimplifyReport
SimplifyOutput
MinkowskiParameters
MinkowskiGenerate
MinkowskiCleanup
} derive(Eq, Debug)
///|
pub(all) enum Axis {
X
Y
} derive(Eq, Debug)
///|
pub(all) enum ResourceKind {
InputPaths
InputVertices
InternalNodes
ScanWork
} derive(Eq, Debug)
///|
/// Indices are zero-based in the original input, before normalization.
pub(all) struct ErrorLocation {
stage : Stage
path_index : Int?
vertex_index : Int?
} derive(Eq, Debug)
///|
/// Errors are returned as values; messages are English.
pub(all) enum ClipError {
InvalidInput(ErrorLocation, String)
CoordinateOutOfRange(ErrorLocation, Axis, Int64)
ExecutionFailure(ErrorLocation, String)
NumericFailure(ErrorLocation, String)
ResourceLimitExceeded(ErrorLocation, ResourceKind, Int)
QuantizationFailure(ErrorLocation, Axis?, ErrorLocation?, String)
DecimalCoordinateOutOfRange(ErrorLocation, Axis, Double, Int)
} derive(Eq, Debug)
///|
pub fn ClipError::message(self : ClipError) -> String {
match self {
QuantizationFailure(_, _, _, message)
| InvalidInput(_, message)
| ExecutionFailure(_, message)
| NumericFailure(_, message) => message
CoordinateOutOfRange(_, axis, value) => {
let name = match axis {
X => "x"
Y => "y"
}
"Coordinate \{name}=\{value} is outside [-1000000000, 1000000000]"
}
DecimalCoordinateOutOfRange(_, axis, value, digits) => {
let name = if axis == X { "x" } else { "y" }
"Decimal coordinate \{name}=\{value} is outside the supported range at precision \{digits}"
}
ResourceLimitExceeded(_, resource, limit) => {
let name = match resource {
InputPaths => "input paths"
InputVertices => "input vertices"
InternalNodes => "internal nodes"
ScanWork => "scan work"
}
"Resource limit exceeded for \{name} (limit \{limit})"
}
}
}
///|
pub(all) struct Limits {
max_input_paths : Int
max_input_vertices : Int
} derive(Eq, Debug)
///|
pub impl Default for Limits with fn default() {
{ max_input_paths: 10_000, max_input_vertices: 100_000, }
}
///|
fn location(stage : Stage, path : Int?, vertex : Int?) -> ErrorLocation {
{ stage, path_index: path, vertex_index: vertex, }
}
///|
fn check_limits(limits : Limits, stage : Stage, paths : Int) -> ClipError? {
let loc = location(stage, None, None)
if limits.max_input_paths < 0 || limits.max_input_vertices < 0 {
return Some(InvalidInput(loc, "Input limits must be non-negative"))
}
if paths > limits.max_input_paths {
return Some(
ResourceLimitExceeded(
location(stage, Some(limits.max_input_paths), None),
InputPaths,
limits.max_input_paths,
),
)
}
None
}
///|
fn check_point(point : Point64, loc : ErrorLocation) -> ClipError? {
// Compare before any subtraction or abs: even Int64::MIN_VALUE is safe here.
if point.x < -COORDINATE_LIMIT || point.x > COORDINATE_LIMIT {
return Some(CoordinateOutOfRange(loc, X, point.x))
}
if point.y < -COORDINATE_LIMIT || point.y > COORDINATE_LIMIT {
return Some(CoordinateOutOfRange(loc, Y, point.y))
}
None
}
///|
/// Preflight completes before allocating output arrays. Subtract from a remaining
/// allowance instead of summing counts, so count arithmetic cannot overflow Int.
fn check_paths(paths : Paths64, limits : Limits, stage : Stage) -> ClipError? {
if check_limits(limits, stage, paths.length()) is Some(e) {
return Some(e)
}
let mut remaining = limits.max_input_vertices
for i = 0; i < paths.length(); i = i + 1 {
let count = paths[i].length()
if count > remaining {
return Some(
ResourceLimitExceeded(
location(stage, Some(i), Some(remaining)),
InputVertices,
limits.max_input_vertices,
),
)
}
remaining = remaining - count
}
for i = 0; i < paths.length(); i = i + 1 {
for j = 0; j < paths[i].length(); j = j + 1 {
if check_point(paths[i][j], location(stage, Some(i), Some(j))) is Some(e) {
return Some(e)
}
}
}
None
}
///|
/// A single path consumes one path slot, even if empty. A query point, when
/// present, consumes one further vertex slot and has no path/vertex index.
fn check_path(
path : Path64,
limits : Limits,
stage : Stage,
query : Point64?,
) -> ClipError? {
if check_limits(limits, stage, 1) is Some(e) {
return Some(e)
}
if path.length() > limits.max_input_vertices {
return Some(
ResourceLimitExceeded(
location(stage, Some(0), Some(limits.max_input_vertices)),
InputVertices,
limits.max_input_vertices,
),
)
}
if query is Some(_) && path.length() == limits.max_input_vertices {
return Some(
ResourceLimitExceeded(
location(stage, None, None),
InputVertices,
limits.max_input_vertices,
),
)
}
for j = 0; j < path.length(); j = j + 1 {
if check_point(path[j], location(stage, Some(0), Some(j))) is Some(e) {
return Some(e)
}
}
if query is Some(point) {
return check_point(point, location(stage, None, None))
}
None
}
///|
/// Fixed-arity predicates treat their three arguments as one logical path.
fn check_three(
a : Point64,
b : Point64,
c : Point64,
limits : Limits,
stage : Stage,
) -> ClipError? {
if check_limits(limits, stage, 1) is Some(e) {
return Some(e)
}
if limits.max_input_vertices < 3 {
return Some(
ResourceLimitExceeded(
location(stage, Some(0), Some(limits.max_input_vertices)),
InputVertices,
limits.max_input_vertices,
),
)
}
if check_point(a, location(stage, Some(0), Some(0))) is Some(e) {
return Some(e)
}
if check_point(b, location(stage, Some(0), Some(1))) is Some(e) {
return Some(e)
}
check_point(c, location(stage, Some(0), Some(2)))
}
///|
/// Validate raw inputs without copying or normalizing them.
pub fn validate_paths(
paths : Paths64,
limits? : Limits = Default::default(),
) -> Result[Unit, ClipError] {
match check_paths(paths, limits, ValidateInputs) {
Some(e) => Err(e)
None => Ok(())
}
}