pub trait MeshBoolean: Backend {
// Required method
fn boolean(
&self,
subject: &TriMesh,
tool: &TriMesh,
operation: BooleanOperator,
options: &ExecutionOptions,
) -> Result<BooleanOutcome, GeomError>;
// Provided methods
fn scratch_requirement(&self) -> ScratchRequirement { ... }
fn determinism(&self) -> Determinism { ... }
fn cancellation_granularity(&self) -> CancellationGranularity { ... }
fn solid_requirements(&self) -> SolidRequirements { ... }
fn subtract_many(
&self,
subject: &TriMesh,
tools: &[TriMesh],
options: &ExecutionOptions,
) -> Result<BooleanOutcome, GeomError> { ... }
fn union_many(
&self,
solids: &[TriMesh],
options: &ExecutionOptions,
) -> Result<BooleanOutcome, GeomError> { ... }
}Expand description
Mesh boolean provider.
Implementing this trait is the capability declaration. Providers that do not implement mesh booleans must not implement this trait.
Required Methods§
Sourcefn boolean(
&self,
subject: &TriMesh,
tool: &TriMesh,
operation: BooleanOperator,
options: &ExecutionOptions,
) -> Result<BooleanOutcome, GeomError>
fn boolean( &self, subject: &TriMesh, tool: &TriMesh, operation: BooleanOperator, options: &ExecutionOptions, ) -> Result<BooleanOutcome, GeomError>
Apply one regularized set operation.
Operands are pre-validated by the registry. Returns a
BooleanOutcome: the mesh plus what was done to produce it. An empty
result mesh is a legitimate value, not an error.
Provided Methods§
Sourcefn scratch_requirement(&self) -> ScratchRequirement
fn scratch_requirement(&self) -> ScratchRequirement
Scratch this provider needs beyond its inputs and result.
Callers budget against this before dispatch. Defaults to
ScratchRequirement::Unbounded so an unaudited provider is treated as
unbudgetable rather than silently assumed cheap.
The elements are the input triangles of every operand together, which
is what axiolid-dispatch charges. A provider that runs on a thread
pool declares what each worker costs as well
(ScratchRequirement::Affine), since that cost does not shrink with
the input (#226).
For Self::subtract_many and Self::union_many it bounds each
boolean of the batch on that step’s own operands. The batch as a
whole has no bound in its input triangles: a union’s result can have
more triangles than its operands, so later steps run on larger
intermediates, and a batch that runs booleans concurrently has
several live at once. A provider whose batch holds intermediates or
runs concurrently charges each step when its operands exist, and
refuses with GeomError::BudgetExceeded before the first that does
not fit (#259). The registry’s check before a batch, in the input
triangles, is then a necessary condition and not a sufficient one.
Sourcefn determinism(&self) -> Determinism
fn determinism(&self) -> Determinism
Reproducibility this provider guarantees for its results.
Defaults to Determinism::BestEffort: the weakest level, so a
provider that has not audited its own reproducibility cannot silently
satisfy a stronger request. Overstating this is the dangerous
direction — Plan::admit refuses a step whose guarantee is weaker
than the caller asked for, and that refusal is only sound if the
declared level is honest.
A provider whose output depends on thread scheduling, hash seeding, or
any other run-to-run variation must not claim Determinism::Bitwise.
Sourcefn cancellation_granularity(&self) -> CancellationGranularity
fn cancellation_granularity(&self) -> CancellationGranularity
How finely this provider polls a cancellation token.
Defaults to CancellationGranularity::None: a provider that has not
declared otherwise is assumed not to poll. Claiming responsiveness a
provider does not have is worse than admitting none.
Sourcefn solid_requirements(&self) -> SolidRequirements
fn solid_requirements(&self) -> SolidRequirements
Admissibility this provider requires of its operands.
Advisory only: the registry validates at the contract level before dispatch. A provider declaring a lower level does not thereby get to accept looser input, and one declaring a higher level is rejected by the conformance suite for narrowing the contract.
Sourcefn subtract_many(
&self,
subject: &TriMesh,
tools: &[TriMesh],
options: &ExecutionOptions,
) -> Result<BooleanOutcome, GeomError>
fn subtract_many( &self, subject: &TriMesh, tools: &[TriMesh], options: &ExecutionOptions, ) -> Result<BooleanOutcome, GeomError>
Subtract many tools in one batch so implementations can union or schedule cutters efficiently. The default is correct but deliberately simple.
The default polls cancellation between tools, which is why the default granularity for an overriding provider must be declared honestly.
Sourcefn union_many(
&self,
solids: &[TriMesh],
options: &ExecutionOptions,
) -> Result<BooleanOutcome, GeomError>
fn union_many( &self, solids: &[TriMesh], options: &ExecutionOptions, ) -> Result<BooleanOutcome, GeomError>
Union many solids in one batch so implementations can choose a reduction order.
The default folds left, which is correct but makes step i pay for
an accumulator holding i operands – quadratic total work. A
provider that can do better should override this; see
BoolmeshBoolean::union_tree for a balanced reduction that measures
28.9x faster on a 512-sphere grid.
An empty slice yields an empty solid: the union of nothing is nothing, which is a legitimate answer rather than an error.
The default polls cancellation between operands, which is why the declared granularity for an overriding provider must stay honest.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".
Implementations on Foreign Types§
Source§impl MeshBoolean for BoolmeshBoolean
impl MeshBoolean for BoolmeshBoolean
Source§fn determinism(&self) -> Determinism
fn determinism(&self) -> Determinism
Honest about the general path, which is not byte-reproducible.
Upstream boolmesh dedups vertices through a randomly-seeded
HashMap, so its output ordering varies between processes even
single-threaded. That is the same root cause documented in
Self::subtract_boxes_analytic, which avoids it with a BTreeMap. The general
path therefore cannot claim Determinism::Bitwise at any thread
count, and enabling parallel does not make it weaker than it
already is.
Determinism::Topological is the honest ceiling: the result’s
connectivity is stable, its vertex ordering is not. Callers needing
byte reproducibility use Self::subtract_boxes_analytic,
whose ordered vertex identity makes it reproducible across processes.
Source§fn scratch_requirement(&self) -> ScratchRequirement
fn scratch_requirement(&self) -> ScratchRequirement
Measured, not guessed: ScratchRequirement::Affine over the input
triangles of all operands and the worker threads the call runs on.
boolmesh builds a Morton collider and intersection tables sized by the
combined input. It exposes no bound, so this was measured directly with
a counting global allocator (src/bin/scratch_probe/) across all four
operations at 24 to 1,536 input triangles, after one discarded warmup
call so the first operation measured is not charged for process
startup (#110). With rayon each boolean runs in a pool started inside
the measured window, with the join halves forced onto other workers
(#226; the module docs of scratch.rs say how). Worst peak in bytes
over the four operations and ten runs with parallel, six on an idle
20-core machine and four at load average 20 to 43:
triangles 1 worker 4 workers 16 workers 64 workers
24 59752 65656 160136 471736
96 108232 112152 193504 460672
384 348792 357000 422472 680384
1536 1354692 1362900 1411548 1702180Three terms, each with its own evidence:
- Per triangle, 1,536 bytes. The slope is linear and flat at about
880 bytes per triangle from 384 up to 24,576 triangles (one worker,
checked once at the two larger sizes), so this is 1.7x it.
SymmetricDifferenceis the worst operation because it is composed from three passes and holds intermediates alive. - Base, 64 KiB: fixed setup that dominates small inputs. 24 triangles peak at 59,752 bytes on one worker, 22 KiB above the per-triangle term, so the base is 2.9x that excess.
- Per worker, 16 KiB with
parallel. A rayon worker’s own bookkeeping (deque, registry slot, thread-locals) is 4.8 KB at most from 1 to 512 threads, and it is charged to whichever boolean runs while the worker starts, which on a loaded machine can be any of them. Work stolen onto it adds its scratch alongside the rest. The measured excess over one worker is at most 6.7 KB per extra worker, idle or loaded, so this is 2.4x it. Withparallel-batchalone the batch’s pool workers pay the same bookkeeping. Without either feature nothing runs off the calling thread and the term is zero.
§The result is charged in the solve (#308)
This counts input triangles only, so it is the admission bound on
the operands, not a bound on the call. Operands that cross many
times have a far larger result: two perpendicular combs of 32 teeth,
1,048 triangles, cross 17,716 times, and their union peaked at 14.7
MB, 8.8x this declaration (52.9 MB, 31x, for the symmetric
difference). With a memory budget the solve therefore charges the
result once its size is known: after the intersection pass,
boolean45 counts the result’s half-edges exactly and refuses with
BudgetExceeded before allocating the result when this declaration
plus 448 bytes per half-edge beyond three per input triangle does not
fit. Ordinary results stay within the three and are charged nothing
more; the intersection pass stops as soon as its crossings alone
could not fit. A budget that admits this declaration can still be
refused, so, as for batches, the check before dispatch is a
necessary condition, not a sufficient one. A composed
SymmetricDifference charges each step the union and intersection
it holds. src/budget.rs gives the measurements.
§Batches
The bound covers each boolean of MeshBoolean::subtract_many and
MeshBoolean::union_many on that step’s own operands, not the batch
in its input triangles: intermediates can outgrow the inputs, and
with parallel-batch stolen work can put every pair of a level in
flight at once (#259). With a memory budget the batch charges each
step when its operands exist (its own copies and intermediates, plus
this bound for every boolean that can be live) and refuses with
BudgetExceeded before the first step that does not fit, so the
check before dispatch is a necessary condition for a batch, not a
sufficient one. Each boolean then gets the budget the batch does not
hold (split evenly between the pairs of a parallel-batch level)
and charges its result against it (#308). tests/scratch_bound.rs
holds the peak of both batches under forced schedules to the
smallest budget they admit.
The old PerElement { bytes_per_element: 4096 } was 2x the large-input
slope but no bound at all for small inputs on a pool: 24 triangles
on 16 workers peak at 125,688 to 160,136 bytes against its 98,304
(#226).
tests/scratch_bound.rs measures the same workloads on 1 to 64
workers and fails if any peak exceeds this. A declared bound that is
occasionally too low is worse than Unbounded, because it makes a
budget look enforced when it is not.
The worker count is only known where the call runs, so with a memory
budget every boolean re-checks this bound against the pool it is in
and refuses with BudgetExceeded when that pool is wider than the
budget check before dispatch assumed. The re-check runs in every
build, so a budget given to the provider directly, without dispatch,
is enforced too.
Source§fn cancellation_granularity(&self) -> CancellationGranularity
fn cancellation_granularity(&self) -> CancellationGranularity
boolmesh takes no cancellation handle, so nothing can interrupt a
single boolean once it starts. Declared honestly: the batch override
polls between groups, which is the only real poll point available.
Source§fn subtract_many(
&self,
subject: &TriMesh,
tools: &[TriMesh],
options: &ExecutionOptions,
) -> Result<BooleanOutcome, GeomError>
fn subtract_many( &self, subject: &TriMesh, tools: &[TriMesh], options: &ExecutionOptions, ) -> Result<BooleanOutcome, GeomError>
Group mutually disjoint cutters and remove each group with one boolean, instead of one boolean per cutter.
Rests on (S \ A) \ B == S \ (A union B) and on a concatenation of
disjoint solids being their union. Bounding-box grouping over-separates
but never wrongly fuses, so the result is identical to the sequential
default – gated by volume equality in tests/batch.rs.
fn union_many( &self, solids: &[TriMesh], options: &ExecutionOptions, ) -> Result<BooleanOutcome, GeomError>
fn boolean( &self, subject: &TriMesh, tool: &TriMesh, operation: BooleanOperator, options: &ExecutionOptions, ) -> Result<BooleanOutcome, GeomError>
Source§impl MeshBoolean for ScalarBoolean
impl MeshBoolean for ScalarBoolean
Source§fn scratch_requirement(&self) -> ScratchRequirement
fn scratch_requirement(&self) -> ScratchRequirement
Exact, so no filter escalation and no scratch beyond the output.
Source§fn cancellation_granularity(&self) -> CancellationGranularity
fn cancellation_granularity(&self) -> CancellationGranularity
Checked per triangle pair, which is the inner loop of the O(n·m) scan.