// Copyright (c) 2026 BigSaltyMan. BSL-1.0; see LICENSE.
// Decimal grid, strict input diagnostics and cumulative adapter are MoonClip code.
///|
/// Immutable decimal digits. Construction validates the supported 0..6 range.
pub struct Precision {
priv digits_value : Int
} derive(Eq, Debug)
///|
pub fn Precision::new(digits : Int) -> Result[Precision, ClipError] {
if digits < 0 || digits > 6 {
return Err(
InvalidInput(
location(DecimalParameters, None, None),
"Precision must be between 0 and 6",
),
)
}
Ok({ digits_value: digits, })
}
///|
pub fn Precision::digits(self : Precision) -> Int {
self.digits_value
}
///|
pub impl Default for Precision with fn default() {
{ digits_value: 3, }
}
///|
fn decimal_scale(precision : Precision) -> Double {
match precision.digits_value {
0 => 1.0
1 => 10.0
2 => 100.0
3 => 1000.0
4 => 10000.0
5 => 100000.0
_ => 1000000.0
}
}
///|
fn decimal_spend(
budget : CallBudget,
nodes : Int,
work : Int,
loc : ErrorLocation,
) -> Unit raise ScanAbort {
budget_spend(budget, nodes, work, loc)
}
///|
// Compare fractional parts, never floor(x+0.5): that addition can round across a tie.
fn decimal_round(value : Double) -> Int64 {
if value >= 0.0 {
let whole = @double.floor(value)
(whole + (if value - whole >= 0.5 { 1.0 } else { 0.0 })).to_int64()
} else {
let whole = @double.ceil(value)
(whole - (if whole - value >= 0.5 { 1.0 } else { 0.0 })).to_int64()
}
}
///|
fn decimal_coordinate(
value : Double,
axis : Axis,
precision : Precision,
loc : ErrorLocation,
) -> Int64 raise ScanAbort {
if value.is_nan() || value.is_inf() {
raise ScanAbort(
QuantizationFailure(loc, Some(axis), None, "Coordinate must be finite"),
)
}
let scale = decimal_scale(precision)
let bound = COORDINATE_LIMIT.to_double() / scale
if value < -bound || value > bound {
raise ScanAbort(
DecimalCoordinateOutOfRange(loc, axis, value, precision.digits_value),
)
}
let scaled = value * scale
if scaled.is_nan() || scaled.is_inf() {
raise ScanAbort(
NumericFailure(loc, "Coordinate scaling produced a non-finite value"),
)
}
if scaled < -1000000000.0 || scaled > 1000000000.0 {
raise ScanAbort(
DecimalCoordinateOutOfRange(loc, axis, value, precision.digits_value),
)
}
decimal_round(scaled)
}
///|
fn decimal_parameter(
value : Double,
precision : Precision,
) -> Double raise ScanAbort {
let scaled = value * decimal_scale(precision)
if scaled.is_nan() || scaled.is_inf() {
raise ScanAbort(
NumericFailure(
location(DecimalParameters, None, None),
"Distance scaling produced a non-finite value",
),
)
}
scaled
}
///|
fn decimal_precheck(
subject : PathsD,
clip : PathsD,
precision : Precision,
budget : CallBudget,
) -> Unit raise ScanAbort {
let limits = budget.limits
if limits.input.max_input_paths < 0 ||
limits.input.max_input_vertices < 0 ||
limits.max_nodes < 0 ||
limits.max_work < 0 {
raise ScanAbort(
InvalidInput(
location(DecimalParameters, None, None),
"Limits must be non-negative",
),
)
}
let maximum = limits.input.max_input_paths
if subject.length() > maximum || clip.length() > maximum - subject.length() {
raise ScanAbort(
ResourceLimitExceeded(
location(DecimalQuantize, Some(maximum), None),
InputPaths,
maximum,
),
)
}
let mut vertices = 0
for i = 0; i < subject.length() + clip.length(); i = i + 1 {
let path = if i < subject.length() {
subject[i]
} else {
clip[i - subject.length()]
}
let loc = location(DecimalQuantize, Some(i), None)
decimal_spend(budget, 0, 1, loc)
if path.length() > limits.input.max_input_vertices - vertices {
raise ScanAbort(
ResourceLimitExceeded(
location(
DecimalQuantize,
Some(i),
Some(limits.input.max_input_vertices - vertices),
),
InputVertices,
limits.input.max_input_vertices,
),
)
}
vertices += path.length()
}
for i = 0; i < subject.length() + clip.length(); i = i + 1 {
let path = if i < subject.length() {
subject[i]
} else {
clip[i - subject.length()]
}
for j = 0; j < path.length(); j = j + 1 {
let loc = location(DecimalQuantize, Some(i), Some(j))
decimal_spend(budget, 0, 1, loc)
ignore(decimal_coordinate(path[j].x, X, precision, loc))
ignore(decimal_coordinate(path[j].y, Y, precision, loc))
}
}
}
///|
priv struct DecimalOrigin {
point : PointD
loc : ErrorLocation
}
///|
fn decimal_closed(
path : Path64,
index : Int,
budget : CallBudget,
) -> Unit raise ScanAbort {
if path.is_empty() {
return
}
let loc = location(DecimalQuantize, Some(index), None)
let a = path[0]
let mut second : Point64? = None
for p in path {
decimal_spend(budget, 0, 1, loc)
if p != a {
second = Some(p)
break
}
}
if second is Some(b) {
for p in path {
decimal_spend(budget, 0, 1, loc)
if cross_validated(a, b, p) != 0L {
return
}
}
}
raise ScanAbort(
QuantizationFailure(
loc,
None,
None,
"Closed contour needs at least three distinct noncollinear grid points",
),
)
}
///|
fn decimal_quantize(
subject : PathsD,
clip : PathsD,
precision : Precision,
closed : Bool,
budget : CallBudget,
) -> (Paths64, Paths64) raise ScanAbort {
decimal_spend(budget, 3, 0, location(DecimalQuantize, None, None))
let left : Paths64 = []
let right : Paths64 = []
let seen : Map[(Int64, Int64), DecimalOrigin] = Map([])
for i = 0; i < subject.length() + clip.length(); i = i + 1 {
let path = if i < subject.length() {
subject[i]
} else {
clip[i - subject.length()]
}
decimal_spend(budget, 2, 0, location(DecimalQuantize, Some(i), None))
let points : Path64 = []
for j = 0; j < path.length(); j = j + 1 {
let p = path[j]
let loc = location(DecimalQuantize, Some(i), Some(j))
let x = decimal_coordinate(p.x, X, precision, loc)
let y = decimal_coordinate(p.y, Y, precision, loc)
decimal_spend(budget, 1, 1, loc)
let key = (x, y)
match seen.get(key) {
Some(origin) =>
if origin.point != p {
raise ScanAbort(
QuantizationFailure(
loc,
None,
Some(origin.loc),
"Distinct input positions merge at this grid point",
),
)
}
None => {
decimal_spend(budget, 2, 0, loc)
seen[key] = { point: p, loc, }
}
}
decimal_spend(budget, 2, 0, loc)
points.push(Point64::new(x, y))
}
if closed {
decimal_closed(points, i, budget)
}
if i < subject.length() {
left.push(points)
} else {
right.push(points)
}
}
(left, right)
}
///|
fn decimal_value(value : Int64, precision : Precision) -> Double {
if value == 0L {
0.0
} else {
value.to_double() / decimal_scale(precision)
}
}
///|
fn decimal_output(
paths : Paths64,
precision : Precision,
budget : CallBudget,
) -> PathsD raise ScanAbort {
let loc = location(DecimalOutput, None, None)
decimal_spend(budget, 1, 0, loc)
let result : PathsD = []
for path in paths {
decimal_spend(budget, 2, 0, loc)
let output : PathD = []
for p in path {
decimal_spend(budget, 2, 1, loc)
output.push(
PointD::new(
decimal_value(p.x, precision),
decimal_value(p.y, precision),
),
)
}
result.push(output)
}
result
}
///|
fn decimal_error(error : ClipError, precision : Precision) -> ClipError {
match error {
CoordinateOutOfRange(loc, axis, value) =>
DecimalCoordinateOutOfRange(
loc,
axis,
decimal_value(value, precision),
precision.digits_value,
)
_ => error
}
}
///|
priv enum DecimalOperation {
Boolean(ClipType, FillRule)
Offset(Double, FillRule, OffsetOptions)
Stroke(Double, CapType, OffsetOptions)
}
///|
fn decimal_execute(
subject : PathsD,
clip : PathsD,
operation : DecimalOperation,
precision : Precision,
limits : ScanLimits,
budget? : CallBudget,
) -> Result[PathsD, ClipError] {
let budget = match budget {
Some(value) => value
None =>
if operation is Stroke(_, _, _) {
new_call_budget(
limits,
generate_stage=StrokeGenerate,
cleanup_stage=StrokeCleanup,
)
} else {
new_call_budget(limits)
}
}
try {
// Check options in original units, then build independent scaled options.
let op = match operation {
Boolean(kind, fill) => Boolean(kind, fill)
Offset(delta, fill, options) => {
if delta.is_nan() || delta.is_inf() {
raise ScanAbort(
InvalidInput(
location(DecimalParameters, None, None),
"Offset distance must be finite",
),
)
}
Offset(
decimal_parameter(delta, precision),
fill,
decimal_scaled_options(options, precision),
)
}
Stroke(width, cap, options) => {
if width.is_nan() || width.is_inf() || width < 0.0 {
raise ScanAbort(
InvalidInput(
location(DecimalParameters, None, None),
"Stroke width must be finite and non-negative",
),
)
}
Stroke(
decimal_parameter(width, precision),
cap,
decimal_scaled_options(options, precision),
)
}
}
decimal_precheck(subject, clip, precision, budget)
let (left, right) = decimal_quantize(
subject,
clip,
precision,
!(op is Stroke(_, _, _)),
budget,
)
let evaluated = match op {
Boolean(kind, fill) =>
boolean_compute(
left,
right,
kind,
budget.limits,
if fill == EvenOdd {
Parity
} else {
SignedNonZero
},
fn(state) raise ScanAbort {
output_extract(state, preserve_collinear=false)
},
budget~,
)
Offset(delta, fill, options) =>
offset_execute(left, delta, fill, options, budget.limits, budget~)
Stroke(width, cap, options) =>
stroke_execute(left[0], width, cap, options, budget.limits, budget~)
}
match evaluated {
Err(error) => Err(decimal_error(error, precision))
Ok(paths) => Ok(decimal_output(paths, precision, budget))
}
} catch {
ScanAbort(error) => Err(decimal_error(error, precision))
}
}
///|
pub fn boolean_op_d(
subject : PathsD,
clip : PathsD,
operation : ClipType,
precision? : Precision = Default::default(),
fill_rule? : FillRule = EvenOdd,
limits? : Limits = Default::default(),
) -> Result[PathsD, ClipError] {
decimal_execute(subject, clip, Boolean(operation, fill_rule), precision, {
input: limits,
max_nodes: 500000,
max_work: 5000000,
})
}
///|
// Keep the PathsD executor unchanged. Conversion runs inside extraction so a
// DecimalOutput failure takes boolean_compute's normal scan cleanup path.
fn decimal_tree_execute(
subject : PathsD,
clip : PathsD,
operation : ClipType,
precision : Precision,
fill_rule : FillRule,
limits : ScanLimits,
budget? : CallBudget,
) -> Result[PolyTreeD, ClipError] {
let budget = match budget {
Some(value) => value
None => new_call_budget(limits)
}
try {
decimal_precheck(subject, clip, precision, budget)
let (left, right) = decimal_quantize(subject, clip, precision, true, budget)
match
boolean_compute(
left,
right,
operation,
budget.limits,
if fill_rule == EvenOdd {
Parity
} else {
SignedNonZero
},
fn(state) raise ScanAbort {
decimal_tree_output(tree_build(state), precision, budget)
},
budget~,
) {
Ok(tree) => Ok(tree)
Err(error) => Err(decimal_error(error, precision))
}
} catch {
ScanAbort(error) => Err(decimal_error(error, precision))
}
}
///|
/// Boolean hierarchy of the quantized region, with decoded Double contours.
/// Introduced in 0.2.0; not included in the published 0.1.0 package.
pub fn boolean_tree_d(
subject : PathsD,
clip : PathsD,
operation : ClipType,
precision? : Precision = Default::default(),
fill_rule? : FillRule = EvenOdd,
limits? : Limits = Default::default(),
) -> Result[PolyTreeD, ClipError] {
decimal_tree_execute(subject, clip, operation, precision, fill_rule, {
input: limits,
max_nodes: 500000,
max_work: 5000000,
})
}
///|
fn decimal_offset_tree_execute(
paths : PathsD,
delta : Double,
precision : Precision,
fill_rule : FillRule,
options : OffsetOptions,
limits : ScanLimits,
budget? : CallBudget,
) -> Result[PolyTreeD, ClipError] {
let budget = match budget {
Some(value) => value
None => new_call_budget(limits)
}
try {
if delta.is_nan() || delta.is_inf() {
raise ScanAbort(
InvalidInput(
location(DecimalParameters, None, None),
"Offset distance must be finite",
),
)
}
let scaled_delta = decimal_parameter(delta, precision)
let scaled_options = decimal_scaled_options(options, precision)
decimal_precheck(paths, [], precision, budget)
let (grid, _) = decimal_quantize(paths, [], precision, true, budget)
match
offset_compute(
grid,
scaled_delta,
fill_rule,
scaled_options,
budget.limits,
fn(state) raise ScanAbort {
decimal_tree_output(tree_build(state), precision, budget)
},
fn(_, budget) raise ScanAbort {
decimal_tree_output(tree_empty(budget), precision, budget)
},
budget~,
) {
Ok(tree) => Ok(tree)
Err(error) => Err(decimal_error(error, precision))
}
} catch {
ScanAbort(error) => Err(decimal_error(error, precision))
}
}
///|
/// Offset hierarchy on the decimal grid. Introduced in 0.2.0.
pub fn offset_tree_d(
paths : PathsD,
delta : Double,
precision? : Precision = Default::default(),
fill_rule? : FillRule = NonZero,
options? : OffsetOptions = Default::default(),
limits? : Limits = Default::default(),
) -> Result[PolyTreeD, ClipError] {
decimal_offset_tree_execute(paths, delta, precision, fill_rule, options, {
input: limits,
max_nodes: 500000,
max_work: 5000000,
})
}
///|
pub fn offset_polygons_d(
paths : PathsD,
delta : Double,
precision? : Precision = Default::default(),
fill_rule? : FillRule = NonZero,
options? : OffsetOptions = Default::default(),
limits? : Limits = Default::default(),
) -> Result[PathsD, ClipError] {
decimal_execute(paths, [], Offset(delta, fill_rule, options), precision, {
input: limits,
max_nodes: 500000,
max_work: 5000000,
})
}
///|
fn decimal_stroke_tree_execute(
path : PathD,
width : Double,
precision : Precision,
cap : CapType,
options : OffsetOptions,
limits : ScanLimits,
budget? : CallBudget,
) -> Result[PolyTreeD, ClipError] {
let budget = match budget {
Some(value) => value
None =>
new_call_budget(
limits,
generate_stage=StrokeGenerate,
cleanup_stage=StrokeCleanup,
)
}
try {
if width.is_nan() || width.is_inf() || width < 0.0 {
raise ScanAbort(
InvalidInput(
location(DecimalParameters, None, None),
"Stroke width must be finite and non-negative",
),
)
}
let scaled_width = decimal_parameter(width, precision)
let scaled_options = decimal_scaled_options(options, precision)
decimal_precheck([path], [], precision, budget)
let (grid, _) = decimal_quantize([path], [], precision, false, budget)
match
stroke_compute(
grid[0],
scaled_width,
cap,
scaled_options,
budget.limits,
fn(state) raise ScanAbort {
decimal_tree_output(tree_build(state), precision, budget)
},
fn(budget) raise ScanAbort {
decimal_tree_output(tree_empty(budget), precision, budget)
},
budget~,
) {
Ok(tree) => Ok(tree)
Err(error) => Err(decimal_error(error, precision))
}
} catch {
ScanAbort(error) => Err(decimal_error(error, precision))
}
}
///|
/// Open-route stroke hierarchy on the decimal grid. Introduced in 0.2.0.
pub fn stroke_tree_d(
path : PathD,
width : Double,
precision? : Precision = Default::default(),
cap? : CapType = Butt,
options? : OffsetOptions = Default::default(),
limits? : Limits = Default::default(),
) -> Result[PolyTreeD, ClipError] {
decimal_stroke_tree_execute(path, width, precision, cap, options, {
input: limits,
max_nodes: 500000,
max_work: 5000000,
})
}
///|
pub fn stroke_polyline_d(
path : PathD,
width : Double,
precision? : Precision = Default::default(),
cap? : CapType = Butt,
options? : OffsetOptions = Default::default(),
limits? : Limits = Default::default(),
) -> Result[PathsD, ClipError] {
decimal_execute([path], [], Stroke(width, cap, options), precision, {
input: limits,
max_nodes: 500000,
max_work: 5000000,
})
}
///|
fn decimal_scaled_options(
options : OffsetOptions,
precision : Precision,
) -> OffsetOptions raise ScanAbort {
if offset_options_error(options, DecimalParameters) is Some(error) {
raise ScanAbort(error)
}
OffsetOptions::new(
join=options.join,
miter_limit=options.miter_limit,
arc_tolerance=decimal_parameter(options.arc_tolerance, precision),
)
}
///|
// Simplification alone reports closed degeneracy, rather than applying region input rules.
fn decimal_simplify_execute(
paths : PathsD,
precision : Precision,
options : SimplifyOptions,
limits : ScanLimits,
budget? : CallBudget,
) -> Result[SimplifyResultD, ClipError] {
let budget = match budget {
Some(value) => value
None => new_call_budget(limits)
}
try {
ignore(simplify_epsilon(options.epsilon))
let scaled = decimal_parameter(options.epsilon, precision)
let square = simplify_epsilon(scaled)
decimal_precheck(paths, [], precision, budget)
// false skips only closed-region validation, never range/merge diagnostics.
let (grid, _) = decimal_quantize(paths, [], precision, false, budget)
let value = simplify_validated(
grid,
SimplifyOptions::new(epsilon=scaled, mode=options.mode),
square,
budget,
)
let decoded = decimal_output(value.stored_paths, precision, budget)
budget_spend(budget, 1, 1, location(SimplifyReport, None, None))
Ok({ stored_paths: decoded, report: value.report, })
} catch {
ScanAbort(error) => Err(decimal_error(error, precision))
}
}
///|
pub fn simplify_paths_d(
paths : PathsD,
precision? : Precision = Default::default(),
options? : SimplifyOptions = Default::default(),
limits? : Limits = Default::default(),
) -> Result[SimplifyResultD, ClipError] {
decimal_simplify_execute(paths, precision, options, {
input: limits,
max_nodes: 500000,
max_work: 5000000,
})
}