diff --git a/MIGRATION.md b/MIGRATION.md index cfa55af4..dc3b3cca 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -12,6 +12,30 @@ It is written for people and for coding assistants alike: each section says what 4. For a name the table does not list, look in "Partly replaced". Anything listed under "Removed" has no replacement: take that code out, or stay on the older version. 5. Finish with its "Check your result" steps. +## Unreleased + +### What changed + +- `operate` now builds the union and the intersection of two planar solids that are not both axis-aligned boxes: a cuboid turned against the other body's axes, a polyhedron with sloped faces such as a roof, or a polyhedron that is not convex. 3.0 threw an error for these (`CoverageGap`, for some pairs `UnresolvedIntersection`). The result is one closed solid, or several when the intersection falls apart into pieces. The report's `handlers` is `['boolean_planar_polyhedra']`. + +### Different behaviour + +- Where faces of both bodies lie in one plane and face the same way, the connected region they cover becomes one face. Faces of one body alone in a plane stay as they are. +- Some pairs that 3.0 already built through the generic route, for example a body inside the other that touches it along an edge, are now built by the planar handler with the same volume and number of solids. The report's `handlers` becomes `['boolean_planar_polyhedra']`. +- Two of these solids that touch on a face, one standing on the other, now unite into one solid. 3.0 threw. +- Two planar solids that touch only along an edge or at a corner give the same result as in 3.0: the union keeps both solids, and the intersection throws `EmptyResult`, as for two separate solids. +- An intersection of two planar solids that touch only on a face now throws `EmptyResult` too, because nothing is left. 3.0 threw `CoverageGap` or `UnresolvedIntersection`. +- Subtraction: pairs that built in 3.0 build the same bodies, and the report's `handlers` is still `['subtract_planar_polyhedra']`. Subtracting a body that only stands on a face of the host now returns the host. 3.0 threw `InvalidTopology`. When the host lies wholly inside the tool and touches it, on a face, along an edge or at a corner, the subtraction now throws `EmptyResult`, because nothing is left. 3.0 threw `CoverageGap` or `UnresolvedIntersection`. +- Two flat faces whose normals differ by an angle of at most 1e-11 radians are now treated as parallel. Rounding gives such faces when a polyhedron's points are turned about a slanted axis. Pairs that were refused only because of such faces now build. For example, a box wholly inside a gable roof, not touching it, both turned that way, gives the roof for the union, the box for the intersection, the roof with a cavity for the roof minus the box, and `EmptyResult` for the box minus the roof. 3.0 threw `UnresolvedIntersection` for many such turns, with `plane intersection: SingularSystem` or `intersection curve misses support`. Some pairs that are still refused now throw `CoverageGap` instead. +- When the boundaries of two solids do not cross, whether one lies inside the other is now decided from points on their surfaces, not from a point stepped inward from one face. Pairs where the inner solid lay very close to the face that was stepped from, or touched it there, now build. 3.0 threw `UnresolvedIntersection` with `mutual containment without a coincident boundary is unresolved`. Some pairs of a curved solid very close to a flat face, which 3.0 built through `prismatic_curved_boolean`, now report `['generic_non_intersecting_boolean']`, with the same volume, `coincident` false and fewer edges. +- When a body has a cavity or several separate solids, and its shells lie on different sides of the other body, the operation throws `UnresolvedIntersection` with `generic boolean containment classification is unresolved`, unless another handler builds it. 3.0 built a wrong result for some of these pairs, for example a sphere in the wall of a hollow cube that encloses the cavity. +- Subtracting a body that lies wholly inside another now puts a cavity for each of its solids into the solid that holds it. 3.0 made a cavity for only one of them, kept the others as solids turned inside out, and always put the cavity into the first solid, even when the hole lay in another. Subtracting a body that has a cavity of its own and lies wholly inside another now builds: the material left inside that cavity becomes a separate solid, and a cavity of the host or a solid of the cutter that lies inside it goes with that solid. 3.0 threw `InvalidTopology` with `closed shell has no solid membership` for a hollow ball inside a cube. Some of these pairs with a prism-shaped cutter, which 3.0 built through `prismatic_curved_boolean`, now report `['generic_non_intersecting_boolean']`, with the same volume and other faces and edges. +- A union or intersection of two planar solids that is still refused throws `CoverageGap` with `details.families` equal to `['layered planar profile extrusion', 'layered planar profile extrusion']`. 3.0 gave `['canonical aligned cuboid', 'canonical aligned cuboid']`. Code that matches the old pair must match the new one. + +### Check your result + +1. A union of a cuboid of width 2, height 1 and depth 2 with a cuboid of width 1, height 1 and depth 1, turned 30 degrees about `[0, 1, 0]` and moved by `[1, 0, 0]`, does not throw. `validity()` gives `{ valid: true, closed: true, bodyType: 'Solid' }`, `measure().volume` is within `errorBound` of 4.5, and the report's `handlers` is `['boolean_planar_polyhedra']`. The intersection of the same pair has a volume of 0.5. + ## 2.5 to 3.0 2.5 was not published. From 2.0, follow 2.0 to 2.5, then 2.5 to 3.0. diff --git a/main/opengeometry-three/tests/node/planar-booleans.test.mjs b/main/opengeometry-three/tests/node/planar-booleans.test.mjs new file mode 100644 index 00000000..8791e56d --- /dev/null +++ b/main/opengeometry-three/tests/node/planar-booleans.test.mjs @@ -0,0 +1,87 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { + OGError, Solid, OG_OPERATION_INTERSECT, OG_OPERATION_SUBTRACT, OG_OPERATION_UNION, OG_PRIMITIVE_CUBOID, + OG_PRIMITIVE_POLYHEDRON, OG_TRANSFORM_ROTATE, OG_TRANSFORM_TRANSLATE, +} from '../../../dist/index.js'; +import { boot } from './support.mjs'; + +const GABLE_FACES = [[0, 1, 2, 3], [1, 0, 4, 5], [2, 1, 5], [3, 2, 5, 4], [0, 3, 4]]; +const CROSS_FACES = [[0, 1, 2, 3], [1, 0, 4], [3, 2, 5], [0, 3, 5, 4], [2, 1, 4, 5]]; + +function gableRoof(ogId) { + const points = [[0, 0, 0], [4, 0, 0], [4, 0, 2], [0, 0, 2], [0, 1, 1], [4, 1, 1]]; + return new Solid(OG_PRIMITIVE_POLYHEDRON, { points, faces: GABLE_FACES }, { ogId }); +} + +function crossGable(ogId) { + const points = [[1.5, -0.5, -1], [2.5, -0.5, -1], [2.5, -0.5, 3], [1.5, -0.5, 3], [2, 1.5, -1], [2, 1.5, 3]]; + return new Solid(OG_PRIMITIVE_POLYHEDRON, { points, faces: CROSS_FACES }, { ogId }); +} + +function turnedCube(ogId) { + const cube = new Solid(OG_PRIMITIVE_CUBOID, { width: 1, height: 1, depth: 1 }, { ogId }); + cube.transform(OG_TRANSFORM_ROTATE, { axis: [0, 1, 0], degrees: 30, pivot: [0, 0, 0] }); + cube.transform(OG_TRANSFORM_TRANSLATE, { offset: [1, 0, 0] }); + return cube; +} + +function operated(host, operation, tool, expected) { + try { + host.operate(operation, { tools: [tool] }); + assert.deepEqual(host.validity(), { valid: true, closed: true, bodyType: 'Solid' }); + const { volume, errorBound } = host.measure(); + assert( + Math.abs(volume - expected) <= errorBound, + `${host.ogId}: ${volume} versus ${expected} with bound ${errorBound}`, + ); + return host.getReport()?.handlers; + } finally { + tool.dispose(); + host.dispose(); + } +} + +test('a gable roof and a crossing gable sunk below its eaves union into one closed solid', async () => { + await boot(); + const handlers = operated(gableRoof('union-roof'), OG_OPERATION_UNION, crossGable('union-cross'), 89 / 12); + assert.deepEqual(handlers, ['boolean_planar_polyhedra']); +}); + +test('a gable roof and a crossing gable sunk below its eaves intersect into one closed solid', async () => { + await boot(); + const handlers = operated(gableRoof('meet-roof'), OG_OPERATION_INTERSECT, crossGable('meet-cross'), 7 / 12); + assert.deepEqual(handlers, ['boolean_planar_polyhedra']); +}); + +test('a gable roof minus a crossing gable still goes through the planar polyhedra subtraction', async () => { + await boot(); + const handlers = operated(gableRoof('cut-roof'), OG_OPERATION_SUBTRACT, crossGable('cut-cross'), 41 / 12); + assert.deepEqual(handlers, ['subtract_planar_polyhedra']); +}); + +test('a cuboid and a cube turned 30 degrees about its height union and intersect', async () => { + await boot(); + const slab = (ogId) => new Solid(OG_PRIMITIVE_CUBOID, { width: 2, height: 1, depth: 2 }, { ogId }); + const union = operated(slab('turned-union-slab'), OG_OPERATION_UNION, turnedCube('turned-union-cube'), 4.5); + assert.deepEqual(union, ['boolean_planar_polyhedra']); + const meet = operated(slab('turned-meet-slab'), OG_OPERATION_INTERSECT, turnedCube('turned-meet-cube'), 0.5); + assert.deepEqual(meet, ['boolean_planar_polyhedra']); +}); + +test('a block standing on the floor wholly inside a gable roof subtracts to nothing', async () => { + await boot(); + const block = new Solid(OG_PRIMITIVE_CUBOID, { width: 0.4, height: 0.3, depth: 0.4 }, { ogId: 'inside-block' }); + block.transform(OG_TRANSFORM_TRANSLATE, { offset: [2, 0, 1] }); + const roof = gableRoof('inside-roof'); + try { + assert.throws( + () => block.operate(OG_OPERATION_SUBTRACT, { tools: [roof] }), + (error) => error instanceof OGError && error.code === 'EmptyResult', + ); + assert(Math.abs(block.measure().volume - 0.048) <= block.measure().errorBound); + } finally { + roof.dispose(); + block.dispose(); + } +}); diff --git a/main/opengeometry/src/intersection/ssi/planar.rs b/main/opengeometry/src/intersection/ssi/planar.rs index f17dadbe..5bce8e0e 100644 --- a/main/opengeometry/src/intersection/ssi/planar.rs +++ b/main/opengeometry/src/intersection/ssi/planar.rs @@ -6,6 +6,8 @@ use crate::math::{ Interval, Sign, }; +const PARALLEL_SINE: f64 = 1.0e-11; + pub(super) fn plane_plane( a: Frame3, b: Frame3, @@ -14,7 +16,7 @@ pub(super) fn plane_plane( let tangent = cross(a.z, b.z); let length = norm(tangent); let delta = sub(b.origin, a.origin); - if length == 0.0 { + if length <= PARALLEL_SINE { return Ok(if dot(a.z, delta).abs() <= accuracy.intersection { GeometryResult::Coincident } else { diff --git a/main/opengeometry/src/intersection/ssi/tests/closed_form.rs b/main/opengeometry/src/intersection/ssi/tests/closed_form.rs index 58ed3f25..b7274703 100644 --- a/main/opengeometry/src/intersection/ssi/tests/closed_form.rs +++ b/main/opengeometry/src/intersection/ssi/tests/closed_form.rs @@ -382,3 +382,28 @@ fn canonical_fast_dispatch_covers_all_fifteen_unordered_family_pairs() { } assert_eq!(count, 15); } + +#[test] +fn planes_parallel_up_to_rounding_are_apart_or_coincident() { + let intersect = |gap: f64, tilt: f64| { + let mut store = GeometryStore::new(); + store.surfaces = vec![ + SurfaceGeometry::Plane { + frame: Frame3::IDENTITY, + }, + SurfaceGeometry::Plane { + frame: Frame3::from_axis([0.0, 0.0, gap], [tilt, 0.0, 1.0], [1.0, 0.0, 0.0]) + .unwrap(), + }, + ]; + intersect_surfaces(&mut store, 0, 1, fine_accuracy()).unwrap() + }; + for tilt in [1.0e-16, 1.0e-12] { + let apart = intersect(0.1, tilt); + assert!(apart.curves.is_empty() && apart.contacts.is_empty() && !apart.coincident); + assert!(intersect(0.0, tilt).coincident); + } + let crossing = intersect(0.0, 1.0e-9); + assert_eq!(crossing.curves.len(), 1); + assert!(!crossing.coincident); +} diff --git a/main/opengeometry/src/operations/modifying/boolean/dispatch/handler_id.rs b/main/opengeometry/src/operations/modifying/boolean/dispatch/handler_id.rs index d8930d21..d98940bd 100644 --- a/main/opengeometry/src/operations/modifying/boolean/dispatch/handler_id.rs +++ b/main/opengeometry/src/operations/modifying/boolean/dispatch/handler_id.rs @@ -20,6 +20,7 @@ pub(crate) enum HandlerId { BooleanRectilinear, SubtractLayeredExtrusions, SubtractPlanarPolyhedra, + BooleanPlanarPolyhedra, BooleanSpheres, SubtractVerticalArcExtrusion, GenericTwoSidedCutterBandSubtraction, @@ -55,6 +56,7 @@ impl HandlerId { Self::BooleanRectilinear => "boolean_rectilinear", Self::SubtractLayeredExtrusions => "subtract_layered_extrusions", Self::SubtractPlanarPolyhedra => "subtract_planar_polyhedra", + Self::BooleanPlanarPolyhedra => "boolean_planar_polyhedra", Self::BooleanSpheres => "boolean_spheres", Self::SubtractVerticalArcExtrusion => "subtract_vertical_arc_extrusion", Self::GenericTwoSidedCutterBandSubtraction => { diff --git a/main/opengeometry/src/operations/modifying/boolean/dispatch/table.rs b/main/opengeometry/src/operations/modifying/boolean/dispatch/table.rs index 72270b83..78d35198 100644 --- a/main/opengeometry/src/operations/modifying/boolean/dispatch/table.rs +++ b/main/opengeometry/src/operations/modifying/boolean/dispatch/table.rs @@ -2,15 +2,14 @@ use super::handler_id::HandlerId; use super::trace::{record_handler, record_optional, Trace}; use crate::brep::{BrepEnvelope, GeometryError, SurfaceGeometry}; use crate::operations::modifying::boolean::handlers::{ - boolean_boxes, boolean_cylinders, boolean_planar_extrusions, boolean_rectilinear, - boolean_spheres, conic_box_containment, conic_containment_boolean, cylinder_box_boolean, - cylinder_conic_containment, generic_multiple_closed_loops_boolean, + boolean_boxes, boolean_cylinders, boolean_planar_extrusions, boolean_planar_polyhedra, + boolean_rectilinear, boolean_spheres, conic_box_containment, conic_containment_boolean, + cylinder_box_boolean, cylinder_conic_containment, generic_multiple_closed_loops_boolean, generic_non_intersecting_boolean, generic_single_closed_loop_boolean, generic_two_sided_cutter_band_subtraction, prismatic_curved_boolean, sphere_box_boolean, sphere_conic_containment, sphere_cylinder_boolean, sphere_torus_containment, - subtract_layered_extrusions, subtract_planar_polyhedra, subtract_vertical_arc_extrusion, - torus_box_containment, torus_conic_containment, torus_containment_boolean, - torus_cylinder_containment, + subtract_layered_extrusions, subtract_vertical_arc_extrusion, torus_box_containment, + torus_conic_containment, torus_containment_boolean, torus_cylinder_containment, }; use crate::operations::modifying::boolean::operands::all_planar; use crate::operations::modifying::boolean::types::{BooleanOp, BooleanResult}; @@ -129,10 +128,18 @@ const FAMILY_HANDLERS: &[FamilyHandler] = &[ struct BoxHandler { name: HandlerId, - subtract_only: bool, + operations: &'static [BooleanOp], call: BooleanHandler, } +const EVERY_OPERATION: &[BooleanOp] = &[ + BooleanOp::Union, + BooleanOp::Intersection, + BooleanOp::Subtraction, +]; +const SUBTRACTION: &[BooleanOp] = &[BooleanOp::Subtraction]; +const UNION_AND_INTERSECTION: &[BooleanOp] = &[BooleanOp::Union, BooleanOp::Intersection]; + fn dispatch_boxes( a: &BrepEnvelope, b: &BrepEnvelope, @@ -160,40 +167,36 @@ fn dispatch_layered( subtract_layered_extrusions(a, b, id) } -fn dispatch_polyhedra( - a: &BrepEnvelope, - b: &BrepEnvelope, - _: BooleanOp, - id: String, -) -> Result { - subtract_planar_polyhedra(a, b, id) -} - const BOX_HANDLERS: &[BoxHandler] = &[ BoxHandler { name: HandlerId::BooleanBoxes, - subtract_only: false, + operations: EVERY_OPERATION, call: dispatch_boxes, }, BoxHandler { name: HandlerId::BooleanPlanarExtrusions, - subtract_only: false, + operations: EVERY_OPERATION, call: boolean_planar_extrusions, }, BoxHandler { name: HandlerId::BooleanRectilinear, - subtract_only: false, + operations: EVERY_OPERATION, call: dispatch_rectilinear, }, BoxHandler { name: HandlerId::SubtractLayeredExtrusions, - subtract_only: true, + operations: SUBTRACTION, call: dispatch_layered, }, BoxHandler { name: HandlerId::SubtractPlanarPolyhedra, - subtract_only: true, - call: dispatch_polyhedra, + operations: SUBTRACTION, + call: boolean_planar_polyhedra, + }, + BoxHandler { + name: HandlerId::BooleanPlanarPolyhedra, + operations: UNION_AND_INTERSECTION, + call: boolean_planar_polyhedra, }, ]; @@ -213,8 +216,8 @@ pub(super) fn specialized_boolean( if pair == [OperandFamily::Planar, OperandFamily::Planar] { let mut gap = None; for entry in BOX_HANDLERS { - if entry.subtract_only && operation != BooleanOp::Subtraction { - break; + if !entry.operations.contains(&operation) { + continue; } match record_handler(trace, entry.name, || { (entry.call)(a, b, operation, id.clone()) diff --git a/main/opengeometry/src/operations/modifying/boolean/handlers/containment/core.rs b/main/opengeometry/src/operations/modifying/boolean/handlers/containment/core.rs index 1ad9e1ac..f19bfc9d 100644 --- a/main/opengeometry/src/operations/modifying/boolean/handlers/containment/core.rs +++ b/main/opengeometry/src/operations/modifying/boolean/handlers/containment/core.rs @@ -1,10 +1,12 @@ -use crate::brep::{unit, Accuracy, BrepEnvelope, FaceRole, GeometryError, Surface}; -use crate::math::{scale, sub, Point3}; +use crate::brep::{Accuracy, BrepEnvelope, FaceRole, GeometryError, SolidRegion, Surface}; +use crate::math::Point3; use crate::operations::modifying::boolean::assembly::{ append_analytic_input, finish_analytic_result, reverse_face, }; use crate::operations::modifying::boolean::types::{BooleanOp, BooleanResult}; -use crate::query::{classify_point, face_contains_uv, PointClassification}; +use crate::query::{ + classify_point, classify_point_in_shell, face_contains_uv, PointClassification, +}; pub(crate) fn analytic_containment_boolean( a: &BrepEnvelope, @@ -26,41 +28,131 @@ pub(crate) fn analytic_containment_boolean( BooleanOp::Subtraction if b_inside_a => { append_analytic_input(&mut out, a)?; let face_offset = out.topology.faces.len() as u32; + let solids = out.solids.len(); append_analytic_input(&mut out, b)?; - let cavity = out - .solids - .pop() - .ok_or_else(|| GeometryError::InvalidTopology("missing cutter solid".into()))? - .outer_shell; - out.solids[0].cavity_shells.push(cavity); for face in face_offset..out.topology.faces.len() as u32 { reverse_face(&mut out, face); out.topology.faces[face as usize].provenance.role = FaceRole::Cut; out.topology.faces[face as usize].provenance.reversed = true; } + let cutters = out.solids.split_off(solids); + let outers = out + .solids + .iter() + .map(|solid| solid.outer_shell) + .chain(cutters.iter().flat_map(|solid| solid.cavity_shells.clone())) + .collect::>(); + let holes = out + .solids + .iter() + .flat_map(|solid| solid.cavity_shells.clone()) + .chain(cutters.iter().map(|solid| solid.outer_shell)) + .collect::>(); + out.solids = nest_shells(&out, &outers, holes)?; } BooleanOp::Subtraction => {} } finish_analytic_result(out, a, b, true, operation, false) } -pub(crate) fn interior_sample(brep: &BrepEnvelope) -> Result { - let bounds = brep.bounds()?.ok_or_else(|| { - GeometryError::InvalidTopology("cannot classify an empty solid boundary".into()) +pub(crate) fn boundary_side( + brep: &BrepEnvelope, + other: &BrepEnvelope, +) -> Result { + let mut sides = Vec::new(); + for solid in &brep.solids { + for shell in std::iter::once(&solid.outer_shell).chain(&solid.cavity_shells) { + let faces = &brep.topology.shells[*shell as usize].faces; + sides.push(shell_side(brep, faces, other)?); + } + } + Ok(match sides.split_first() { + Some((first, rest)) if rest.iter().all(|side| side == first) => *first, + _ => PointClassification::Unknown, + }) +} + +fn shell_side( + brep: &BrepEnvelope, + faces: &[u32], + other: &BrepEnvelope, +) -> Result { + let side = first_sample(brep, faces, |point| { + let place = classify_point(other, point)?; + Ok(matches!( + place, + PointClassification::Inside | PointClassification::Outside + ) + .then_some(place)) })?; - let model_scale = bounds - .axes + Ok(side.unwrap_or(PointClassification::Unknown)) +} + +fn nest_shells( + brep: &BrepEnvelope, + outers: &[u32], + holes: Vec, +) -> Result, GeometryError> { + let depths = outers + .iter() + .map(|&shell| Ok(enclosing_outers(brep, outers, shell)?.len())) + .collect::, GeometryError>>()?; + let mut solids = outers .iter() - .map(|axis| axis.width()) - .fold(brep.accuracy.geometric, f64::max); - let offsets = [ - (model_scale * 1.0e-6).max(32.0 * brep.accuracy.geometric), - 16.0 * brep.accuracy.geometric, - 8.0 * brep.accuracy.geometric, - 4.0 * brep.accuracy.geometric, - ]; + .map(|&outer_shell| SolidRegion { + outer_shell, + cavity_shells: Vec::new(), + }) + .collect::>(); + for hole in holes { + let home = enclosing_outers(brep, outers, hole)? + .into_iter() + .max_by_key(|&index| depths[index]) + .ok_or_else(|| { + GeometryError::UnresolvedIntersection( + "containment cavity has no enclosing solid".into(), + ) + })?; + solids[home].cavity_shells.push(hole); + } + Ok(solids) +} + +fn enclosing_outers( + brep: &BrepEnvelope, + outers: &[u32], + shell: u32, +) -> Result, GeometryError> { + let faces = |shell: u32| &brep.topology.shells[shell as usize].faces; + let found = first_sample(brep, faces(shell), |point| { + let mut enclosing = Vec::new(); + for (index, &outer) in outers.iter().enumerate() { + if outer == shell { + continue; + } + match classify_point_in_shell(brep, faces(outer), point)? { + PointClassification::Inside => enclosing.push(index), + PointClassification::Outside => {} + _ => return Ok(None), + } + } + Ok(Some(enclosing)) + })?; + found.ok_or_else(|| { + GeometryError::UnresolvedIntersection("containment shell nesting is unresolved".into()) + }) +} + +fn first_sample( + brep: &BrepEnvelope, + faces: &[u32], + mut found: impl FnMut(Point3) -> Result, GeometryError>, +) -> Result, GeometryError> { let samples = [0.5, 0.25, 0.75, 0.125, 0.875]; - for face in &brep.topology.faces { + for face in faces + .iter() + .map(|&face| &brep.topology.faces[face as usize]) + { let surface = brep.geometry.surface(face.surface)?; for u in samples { for v in samples { @@ -71,18 +163,11 @@ pub(crate) fn interior_sample(brep: &BrepEnvelope) -> Result Result { let mut assigned = vec![false; facets.builder.brep.topology.faces.len()]; let mut outer_shells = Vec::new(); @@ -251,7 +252,7 @@ pub(super) fn finish( .cavity_shells .push(cavity_shell); } - finish_planar_result(facets.builder.brep, a, b) + finish_planar_result(facets.builder.brep, a, b, operation) } fn shell_volume(brep: &BrepEnvelope, faces: &[u32]) -> Result { @@ -344,6 +345,7 @@ fn finish_planar_result( mut out: BrepEnvelope, a: &BrepEnvelope, b: &BrepEnvelope, + operation: BooleanOp, ) -> Result { out.revision = a.revision.max(b.revision).checked_add(1).ok_or_else(|| { @@ -353,7 +355,7 @@ fn finish_planar_result( let face_mappings = analytic_face_mappings(&out, [a, b]); Ok(BooleanResult { report: BooleanReport { - operation: BooleanOp::Subtraction, + operation, quality: out.quality.clone(), contacts: Vec::new(), coincident: true, diff --git a/main/opengeometry/src/operations/modifying/boolean/handlers/planar/layered_extrusions/mod.rs b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/layered_extrusions/mod.rs index 12a4102e..c149246b 100644 --- a/main/opengeometry/src/operations/modifying/boolean/handlers/planar/layered_extrusions/mod.rs +++ b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/layered_extrusions/mod.rs @@ -10,7 +10,7 @@ use crate::math::{cross, dot, scale, sub, Point3}; use crate::operations::modifying::boolean::operands::{ all_planar, brep_face_source, full_planar_extrusion, }; -use crate::operations::modifying::boolean::types::BooleanResult; +use crate::operations::modifying::boolean::types::{BooleanOp, BooleanResult}; use crate::query::face_contains_uv; use regions::{contours, layer_boundaries, layer_regions, LayerBoundaries}; @@ -97,7 +97,7 @@ pub(crate) fn subtract_layered_extrusions( &boundaries.vertices, &[(host, FaceRole::Split)], )?; - finish(facets, host, cutter) + finish(facets, host, cutter, BooleanOp::Subtraction) } fn check_layer_alignment(host: &BrepEnvelope, frame: Frame3) -> Result<(), GeometryError> { diff --git a/main/opengeometry/src/operations/modifying/boolean/handlers/planar/mod.rs b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/mod.rs index bba4a9ff..0dc79bab 100644 --- a/main/opengeometry/src/operations/modifying/boolean/handlers/planar/mod.rs +++ b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/mod.rs @@ -3,4 +3,4 @@ mod layered_extrusions; mod polyhedra; pub(crate) use layered_extrusions::subtract_layered_extrusions; -pub(crate) use polyhedra::subtract_planar_polyhedra; +pub(crate) use polyhedra::boolean_planar_polyhedra; diff --git a/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/keep.rs b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/keep.rs new file mode 100644 index 00000000..ca20aa61 --- /dev/null +++ b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/keep.rs @@ -0,0 +1,135 @@ +use crate::brep::{FaceRole, Frame3, GeometryError}; +use crate::geom2d::{signed_area2, Pt2, RingRegion}; +use crate::math::scale; +use crate::operations::modifying::boolean::handlers::planar::facets::coverage; +use crate::operations::modifying::boolean::types::BooleanOp; +use crate::query::PointClassification; +use std::cell::Cell; + +pub(super) struct Keeper { + operation: BooleanOp, + shared: Cell, +} + +impl Keeper { + pub(super) fn new(operation: BooleanOp) -> Self { + Self { + operation, + shared: Cell::new(false), + } + } + + pub(super) fn probe_side(&self) -> f64 { + boundary_probe_side(self.operation) + } + + pub(super) fn host(&self, place: PointClassification) -> Option { + self.note(place); + host_keeps(self.operation, place) + } + + pub(super) fn cutter(&self, place: PointClassification) -> Option { + self.note(place); + cutter_keeps(self.operation, place) + } + + pub(super) fn shares_volume_or_face(&self) -> bool { + self.shared.get() + } + + fn note(&self, place: PointClassification) { + if place == PointClassification::Inside { + self.shared.set(true); + } + } +} + +fn host_keeps(operation: BooleanOp, place: PointClassification) -> Option { + match place { + PointClassification::Outside => Some(operation != BooleanOp::Intersection), + PointClassification::Inside => Some(operation == BooleanOp::Intersection), + PointClassification::Boundary | PointClassification::Unknown => None, + } +} + +fn boundary_probe_side(operation: BooleanOp) -> f64 { + if operation == BooleanOp::Union { + -1.0 + } else { + 1.0 + } +} + +fn cutter_keeps(operation: BooleanOp, place: PointClassification) -> Option { + match place { + PointClassification::Inside => Some(operation != BooleanOp::Union), + PointClassification::Outside => Some(operation == BooleanOp::Union), + PointClassification::Boundary => Some(operation == BooleanOp::Subtraction), + PointClassification::Unknown => None, + } +} + +pub(super) fn turns_cutter(operation: BooleanOp) -> bool { + operation == BooleanOp::Subtraction +} + +pub(super) fn cutter_frame(operation: BooleanOp, frame: Frame3) -> Frame3 { + if !turns_cutter(operation) { + return frame; + } + Frame3 { + origin: frame.origin, + x: frame.x, + y: scale(frame.y, -1.0), + z: scale(frame.z, -1.0), + } +} + +pub(super) fn piece_role( + operation: BooleanOp, + cutter: bool, + region: &RingRegion, + base: &[Vec], + tolerance: f64, +) -> FaceRole { + if operation == BooleanOp::Subtraction { + return if cutter { + FaceRole::Cut + } else { + FaceRole::Split + }; + } + let area = |rings: &[Vec]| { + rings + .iter() + .map(|ring| signed_area2(ring).abs()) + .sum::() + }; + let region_area = signed_area2(®ion.outer).abs() - area(®ion.holes); + let base_area = signed_area2(&base[0]).abs() - area(&base[1..]); + let perimeter = base + .iter() + .flat_map(|ring| { + ring.iter() + .zip(ring.iter().cycle().skip(1)) + .map(|(a, b)| (b.x - a.x).hypot(b.z - a.z)) + }) + .sum::(); + if base_area - region_area <= tolerance * perimeter { + FaceRole::Preserved + } else { + FaceRole::Split + } +} + +pub(super) fn open_boundary_gap(operation: BooleanOp, error: GeometryError) -> GeometryError { + match error { + GeometryError::InvalidTopology(message) + if operation != BooleanOp::Subtraction + && message == "layered planar Boolean has an open boundary" => + { + coverage() + } + other => other, + } +} diff --git a/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/merge.rs b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/merge.rs new file mode 100644 index 00000000..b2905886 --- /dev/null +++ b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/merge.rs @@ -0,0 +1,139 @@ +use super::sections::{local_ring, PlanarFace, PlanarFacet}; +use crate::brep::{Accuracy, BrepEnvelope, FaceProvenance, FaceRole, Frame3, GeometryError}; +use crate::geom2d::{boolean_oriented_regions, PlanarBooleanOp, Pt2}; +use crate::math::{dot, sub, Point3}; +use crate::operations::modifying::boolean::operands::brep_face_source; + +type Inputs<'a> = [(&'a BrepEnvelope, &'a [PlanarFace]); 2]; + +struct Merge { + members: Vec, + facet: PlanarFacet, +} + +pub(super) fn merged_coplanar( + pieces: Vec, + inputs: Inputs<'_>, + accuracy: Accuracy, +) -> Result, GeometryError> { + let mut grouped = vec![false; pieces.len()]; + let mut merges = Vec::new(); + for start in 0..pieces.len() { + if grouped[start] { + continue; + } + let frame = pieces[start].frame; + let group = (start..pieces.len()) + .filter(|&index| !grouped[index] && same_plane(frame, pieces[index].frame, accuracy)) + .collect::>(); + for &index in &group { + grouped[index] = true; + } + merges.extend(coincident_faces(&pieces, &group, inputs, accuracy)?); + } + let mut slots = pieces.into_iter().map(Some).collect::>(); + for merge in merges { + for &member in &merge.members { + slots[member] = None; + } + slots[merge.members[0]] = Some(merge.facet); + } + Ok(slots.into_iter().flatten().collect()) +} + +fn coincident_faces( + pieces: &[PlanarFacet], + group: &[usize], + inputs: Inputs<'_>, + accuracy: Accuracy, +) -> Result, GeometryError> { + let frame = pieces[group[0]].frame; + let candidates = inputs + .iter() + .enumerate() + .flat_map(|(input, &(body, faces))| { + faces + .iter() + .filter(move |face| same_plane(frame, face.frame, accuracy)) + .map(move |face| (input, body, face)) + }) + .collect::>(); + if (0..2).any(|input| candidates.iter().all(|candidate| candidate.0 != input)) { + return Ok(Vec::new()); + } + let local = |outer: &[Point3], holes: &[Vec]| { + std::iter::once(outer) + .chain(holes.iter().map(Vec::as_slice)) + .map(|ring| local_ring(ring, frame)) + .collect::>() + }; + let member_rings = group + .iter() + .map(|&index| local(&pieces[index].outer, &pieces[index].holes)) + .collect::>(); + let union = boolean_oriented_regions( + &member_rings.concat(), + &[], + PlanarBooleanOp::Union, + accuracy.intersection, + )?; + let mut merges = Vec::new(); + for region in union { + let rings = std::iter::once(region.outer) + .chain(region.holes) + .collect::>(); + let (mut sources, mut from) = (Vec::new(), [false; 2]); + for &(input, body, face) in &candidates { + if overlaps(&rings, &local(&face.polygon, &face.holes), accuracy)? { + sources.push(brep_face_source(body, face.id)); + from[input] = true; + } + } + let mut members = Vec::new(); + for (&index, member) in group.iter().zip(&member_rings) { + if overlaps(&rings, member, accuracy)? { + members.push(index); + } + } + if from.contains(&false) || members.is_empty() { + continue; + } + let lifted = lifted_rings(&rings, frame); + let facet = PlanarFacet { + key: pieces[members[0]].key.clone(), + frame, + outer: lifted[0].clone(), + holes: lifted[1..].to_vec(), + provenance: FaceProvenance { + sources, + role: FaceRole::Coincident, + reversed: false, + }, + }; + merges.push(Merge { members, facet }); + } + Ok(merges) +} + +fn lifted_rings(rings: &[Vec], frame: Frame3) -> Vec> { + rings + .iter() + .map(|ring| { + ring.iter() + .map(|point| frame.point([point.x, point.z, 0.0])) + .collect() + }) + .collect() +} + +fn overlaps(a: &[Vec], b: &[Vec], accuracy: Accuracy) -> Result { + Ok( + !boolean_oriented_regions(a, b, PlanarBooleanOp::Intersection, accuracy.intersection)? + .is_empty(), + ) +} + +fn same_plane(frame: Frame3, other: Frame3, accuracy: Accuracy) -> bool { + dot(frame.z, other.z) >= 1.0 - 1.0e-10 + && dot(frame.z, sub(other.origin, frame.origin)).abs() <= accuracy.intersection +} diff --git a/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/mod.rs b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/mod.rs index 60ab6255..c9706e98 100644 --- a/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/mod.rs +++ b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/mod.rs @@ -1,3 +1,5 @@ +mod keep; +mod merge; mod sections; use super::facets::{coverage, finish, PlanarFacets}; @@ -7,18 +9,33 @@ use crate::geom2d::{ RingRegion, }; use crate::math::{dot, scale, sub, Point3}; +use crate::operations::modifying::boolean::handlers::containment::analytic_containment_boolean; +use crate::operations::modifying::boolean::handlers::disjoint::separate_boolean; use crate::operations::modifying::boolean::operands::brep_face_source; -use crate::operations::modifying::boolean::types::BooleanResult; +use crate::operations::modifying::boolean::types::{BooleanOp, BooleanResult}; use crate::query::{Classifier, PointClassification}; +use keep::{cutter_frame, open_boundary_gap, piece_role, turns_cutter, Keeper}; +use merge::merged_coplanar; use sections::{ - convex_faces, convex_section_polygon, planar_faces, planar_section_segments, + convex_faces, convex_section_polygon, local_ring, planar_faces, planar_section_segments, ring_vertex_candidates, split_planar_ring, PlanarFace, PlanarFacet, }; use std::cell::RefCell; -pub(crate) fn subtract_planar_polyhedra( +pub(crate) fn boolean_planar_polyhedra( host: &BrepEnvelope, cutter: &BrepEnvelope, + operation: BooleanOp, + id: String, +) -> Result { + planar_polyhedra(host, cutter, operation, id) + .map_err(|error| open_boundary_gap(operation, error)) +} + +fn planar_polyhedra( + host: &BrepEnvelope, + cutter: &BrepEnvelope, + operation: BooleanOp, id: String, ) -> Result { let accuracy = Accuracy::combined(host.accuracy, cutter.accuracy); @@ -28,37 +45,75 @@ pub(crate) fn subtract_planar_polyhedra( let Some(cutter_faces) = planar_faces(cutter, accuracy.intersection)? else { return Err(coverage()); }; - let convex_cutter_faces = convex_faces(cutter, accuracy.intersection)?; + let convex_cutter_faces = if operation == BooleanOp::Subtraction { + convex_faces(cutter, accuracy.intersection)? + } else { + None + }; check_not_nested(host, cutter)?; + let keeper = Keeper::new(operation); let mut pieces = Vec::new(); for face in &host_faces { - let mut base = vec![local_ring(&face.polygon, face.frame)]; - base.extend(face.holes.iter().map(|ring| local_ring(ring, face.frame))); + let base = face_rings(face); let regions = if let Some(convex_faces) = &convex_cutter_faces { convex_host_regions(cutter, convex_faces, face, &base, accuracy)? } else { - classified_host_regions(cutter, &cutter_faces, face, &base, accuracy)? + classified_host_regions(cutter, &cutter_faces, face, &base, accuracy, &keeper)? }; for region in regions { - pieces.push(host_piece(host, face, ®ion)); + let role = piece_role(operation, false, ®ion, &base, accuracy.geometric); + pieces.push(host_piece(host, face, ®ion, role)); } } let classifier = Classifier::new(host); for face in &cutter_faces { - let regions = cutter_face_regions(host, &classifier, &host_faces, face, accuracy)?; - let reversed = Frame3 { - origin: face.frame.origin, - x: face.frame.x, - y: scale(face.frame.y, -1.0), - z: scale(face.frame.z, -1.0), - }; + let base = face_rings(face); + let regions = cutter_face_regions( + host, + &classifier, + &host_faces, + face, + &base, + accuracy, + &keeper, + )?; for region in regions { - pieces.push(cutter_piece(cutter, face, reversed, ®ion)); + let role = piece_role(operation, true, ®ion, &base, accuracy.geometric); + pieces.push(cutter_piece(cutter, face, operation, ®ion, role)); } } + if operation != BooleanOp::Subtraction && !keeper.shares_volume_or_face() { + return match operation { + BooleanOp::Intersection => separate_boolean(host, cutter, operation, id, Vec::new()), + _ => Err(coverage()), + }; + } if pieces.is_empty() { - return Err(coverage()); + return match operation { + BooleanOp::Subtraction => { + analytic_containment_boolean(host, cutter, false, operation, id) + } + _ => Err(coverage()), + }; + } + if operation != BooleanOp::Subtraction { + pieces = merged_coplanar( + pieces, + [(host, &host_faces), (cutter, &cutter_faces)], + accuracy, + )?; } + let facets = welded_facets(pieces, id, accuracy)?; + let mut result = finish(facets, host, cutter, operation)?; + result.report.coincident = false; + Ok(result) +} + +fn welded_facets( + pieces: Vec, + id: String, + accuracy: Accuracy, +) -> Result { let vertices = pieces .iter() .flat_map(|piece| { @@ -87,9 +142,14 @@ pub(crate) fn subtract_planar_polyhedra( let holes = piece.holes.iter().map(|ring| split(ring)).collect(); facets.face(&piece.key, piece.frame, outer, holes, piece.provenance)?; } - let mut result = finish(facets, host, cutter)?; - result.report.coincident = false; - Ok(result) + Ok(facets) +} + +fn face_rings(face: &PlanarFace) -> Vec> { + std::iter::once(&face.polygon) + .chain(&face.holes) + .map(|ring| local_ring(ring, face.frame)) + .collect() } fn check_not_nested(host: &BrepEnvelope, cutter: &BrepEnvelope) -> Result<(), GeometryError> { @@ -110,16 +170,6 @@ fn check_not_nested(host: &BrepEnvelope, cutter: &BrepEnvelope) -> Result<(), Ge Ok(()) } -fn local_ring(points: &[Point3], frame: Frame3) -> Vec { - points - .iter() - .map(|point| { - let local = frame.local(*point); - Pt2::new(local[0], local[1]) - }) - .collect::>() -} - fn convex_host_regions( cutter: &BrepEnvelope, convex_faces: &[PlanarFace], @@ -127,7 +177,14 @@ fn convex_host_regions( base: &[Vec], accuracy: Accuracy, ) -> Result, GeometryError> { - let section = convex_section_polygon(cutter, convex_faces, face.frame, accuracy.intersection); + let touches_from_outside = cutter.topology.vertices.iter().all(|vertex| { + dot(face.frame.z, sub(vertex.position, face.frame.origin)) >= -accuracy.intersection + }); + let section = if touches_from_outside { + Vec::new() + } else { + convex_section_polygon(cutter, convex_faces, face.frame, accuracy.intersection) + }; let cut = if section.is_empty() { Vec::new() } else { @@ -147,6 +204,7 @@ fn classified_host_regions( face: &PlanarFace, base: &[Vec], accuracy: Accuracy, + keeper: &Keeper, ) -> Result, GeometryError> { let mut edges = planar_section_segments(cutter, cutter_faces, face.frame, accuracy.intersection)?; @@ -176,15 +234,14 @@ fn classified_host_regions( position, scale( face.frame.z, - accuracy.geometric.max(accuracy.intersection) * 4.0, + keeper.probe_side() * accuracy.geometric.max(accuracy.intersection) * 4.0, ), )), other => other, }; - match classification { - Ok(PointClassification::Outside) => true, - Ok(PointClassification::Inside) => false, - Ok(PointClassification::Boundary) | Ok(PointClassification::Unknown) => { + match classification.map(|place| keeper.host(place)) { + Ok(Some(keep)) => keep, + Ok(None) => { *failure.borrow_mut() = Some(coverage()); false } @@ -216,27 +273,24 @@ fn coplanar_rings(faces: &[PlanarFace], face: &PlanarFace, accuracy: Accuracy) - .collect::>() } -fn host_piece(host: &BrepEnvelope, face: &PlanarFace, region: &RingRegion) -> PlanarFacet { +fn host_piece( + host: &BrepEnvelope, + face: &PlanarFace, + region: &RingRegion, + role: FaceRole, +) -> PlanarFacet { PlanarFacet { key: format!("planar-host-{}", face.id), frame: face.frame, - outer: region - .outer - .iter() - .map(|point| face.frame.point([point.x, point.z, 0.0])) - .collect(), + outer: lifted_ring(®ion.outer, face.frame, false), holes: region .holes .iter() - .map(|ring| { - ring.iter() - .map(|point| face.frame.point([point.x, point.z, 0.0])) - .collect() - }) + .map(|ring| lifted_ring(ring, face.frame, false)) .collect(), provenance: FaceProvenance { sources: vec![brep_face_source(host, face.id)], - role: FaceRole::Split, + role, reversed: false, }, } @@ -247,12 +301,12 @@ fn cutter_face_regions( classifier: &Classifier, host_faces: &[PlanarFace], face: &PlanarFace, + cutter_region: &[Vec], accuracy: Accuracy, + keeper: &Keeper, ) -> Result, GeometryError> { let mut edges = planar_section_segments(host, host_faces, face.frame, accuracy.intersection)?; let coplanar = coplanar_rings(host_faces, face, accuracy); - let mut cutter_region = vec![local_ring(&face.polygon, face.frame)]; - cutter_region.extend(face.holes.iter().map(|ring| local_ring(ring, face.frame))); for ring in cutter_region.iter().chain(&coplanar) { edges.extend( ring.iter() @@ -275,10 +329,12 @@ fn cutter_face_regions( { return false; } - match classifier.classify(face.frame.point([point.x, point.z, 0.0])) { - Ok(PointClassification::Inside) | Ok(PointClassification::Boundary) => true, - Ok(PointClassification::Outside) => false, - Ok(PointClassification::Unknown) => { + match classifier + .classify(face.frame.point([point.x, point.z, 0.0])) + .map(|place| keeper.cutter(place)) + { + Ok(Some(keep)) => keep, + Ok(None) => { *failure.borrow_mut() = Some(coverage()); false } @@ -297,36 +353,35 @@ fn cutter_face_regions( fn cutter_piece( cutter: &BrepEnvelope, face: &PlanarFace, - reversed: Frame3, + operation: BooleanOp, region: &RingRegion, + role: FaceRole, ) -> PlanarFacet { - let mut outer = region - .outer - .iter() - .map(|point| face.frame.point([point.x, point.z, 0.0])) - .collect::>(); - outer.reverse(); - let holes = region - .holes - .iter() - .map(|ring| { - let mut points = ring - .iter() - .map(|point| face.frame.point([point.x, point.z, 0.0])) - .collect::>(); - points.reverse(); - points - }) - .collect(); + let reversed = turns_cutter(operation); PlanarFacet { key: format!("planar-cut-{}", face.id), - frame: reversed, - outer, - holes, + frame: cutter_frame(operation, face.frame), + outer: lifted_ring(®ion.outer, face.frame, reversed), + holes: region + .holes + .iter() + .map(|ring| lifted_ring(ring, face.frame, reversed)) + .collect(), provenance: FaceProvenance { sources: vec![brep_face_source(cutter, face.id)], - role: FaceRole::Cut, - reversed: true, + role, + reversed, }, } } + +fn lifted_ring(ring: &[Pt2], frame: Frame3, reversed: bool) -> Vec { + let mut points = ring + .iter() + .map(|point| frame.point([point.x, point.z, 0.0])) + .collect::>(); + if reversed { + points.reverse(); + } + points +} diff --git a/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/sections.rs b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/sections.rs index 8f7ccea3..d5c96467 100644 --- a/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/sections.rs +++ b/main/opengeometry/src/operations/modifying/boolean/handlers/planar/polyhedra/sections.rs @@ -406,3 +406,13 @@ pub(super) fn split_planar_ring( } split } + +pub(super) fn local_ring(points: &[Point3], frame: Frame3) -> Vec { + points + .iter() + .map(|point| { + let local = frame.local(*point); + Pt2::new(local[0], local[1]) + }) + .collect::>() +} diff --git a/main/opengeometry/src/operations/modifying/boolean/tests/mod.rs b/main/opengeometry/src/operations/modifying/boolean/tests/mod.rs index ce775760..e1b7a650 100644 --- a/main/opengeometry/src/operations/modifying/boolean/tests/mod.rs +++ b/main/opengeometry/src/operations/modifying/boolean/tests/mod.rs @@ -10,6 +10,7 @@ mod generic; mod planar_batch; mod planar_extrusions; mod planar_oblique; +mod planar_polyhedra; mod shell; mod shell_necks; mod sphere_cylinder; diff --git a/main/opengeometry/src/operations/modifying/boolean/tests/planar_polyhedra.rs b/main/opengeometry/src/operations/modifying/boolean/tests/planar_polyhedra.rs new file mode 100644 index 00000000..4e3b0082 --- /dev/null +++ b/main/opengeometry/src/operations/modifying/boolean/tests/planar_polyhedra.rs @@ -0,0 +1,503 @@ +use super::support::{extrusion_span, volume}; +use crate::brep::{Accuracy, BrepEnvelope, Face, FaceRole, Frame3, GeometryError}; +use crate::math::Point3; +use crate::operations::modifying::boolean::dispatch::boolean_brep_with_handlers; +use crate::operations::modifying::boolean::types::{BooleanOp, BooleanResult}; +use crate::primitives; +use crate::query::{classify_point, PointClassification}; + +const ACCURACY: Accuracy = Accuracy { + geometric: 1.0e-9, + intersection: 1.0e-10, + tessellation: 0.01, + exchange: 1.0e-5, +}; + +fn cube() -> BrepEnvelope { + primitives::cuboid( + "cube".into(), + Frame3 { + origin: [-1.0, -1.0, -1.0], + ..Frame3::IDENTITY + }, + [2.0, 2.0, 2.0], + ACCURACY, + ) + .unwrap() +} + +fn bar_turned_about_x() -> BrepEnvelope { + let half = std::f64::consts::FRAC_1_SQRT_2; + let (y, z) = ([0.0, half, half], [0.0, -half, half]); + primitives::cuboid( + "turned-bar".into(), + Frame3 { + origin: [-1.5, 0.0, -std::f64::consts::SQRT_2], + x: [1.0, 0.0, 0.0], + y, + z, + }, + [3.0, 2.0, 2.0], + ACCURACY, + ) + .unwrap() +} + +fn bar_in_cube() -> f64 { + 16.0 * (std::f64::consts::SQRT_2 - 1.0) +} + +fn operated(a: &BrepEnvelope, b: &BrepEnvelope, operation: BooleanOp) -> (BooleanResult, f64) { + let (result, handlers) = boolean_brep_with_handlers(a, b, operation, "planar".into()).unwrap(); + let expected = if operation == BooleanOp::Subtraction { + "subtract_planar_polyhedra" + } else { + "boolean_planar_polyhedra" + }; + assert_eq!(handlers, [expected], "{operation:?}"); + assert_eq!(result.report.operation, operation); + result.brep.validate().unwrap(); + assert!(result + .brep + .topology + .shells + .iter() + .all(|shell| shell.is_closed)); + let measured = volume(&result.brep); + assert!(measured > 0.0, "{operation:?} winds inward: {measured}"); + (result, measured) +} + +fn assert_identities(a: &BrepEnvelope, b: &BrepEnvelope, overlap: f64) -> [BooleanResult; 3] { + let (va, vb) = (volume(a), volume(b)); + let (union, united) = operated(a, b, BooleanOp::Union); + let (intersection, common) = operated(a, b, BooleanOp::Intersection); + let (subtraction, rest) = operated(a, b, BooleanOp::Subtraction); + for (measured, expected) in [ + (common, overlap), + (united, va + vb - overlap), + (rest, va - overlap), + ] { + assert!( + (measured - expected).abs() <= 1.0e-9 * va.max(vb), + "{measured} != {expected}" + ); + } + [union, intersection, subtraction] +} + +#[test] +fn turned_bar_through_a_cube_follows_the_volume_identities() { + let (a, b) = (cube(), bar_turned_about_x()); + let [union, intersection, subtraction] = assert_identities(&a, &b, bar_in_cube()); + assert_eq!(union.brep.solids.len(), 1); + assert_eq!(intersection.brep.solids.len(), 1); + assert_eq!(subtraction.brep.solids.len(), 4); + let tip = [0.0, 0.0, 1.3]; + assert_eq!( + classify_point(&union.brep, tip).unwrap(), + PointClassification::Inside + ); + assert_eq!( + classify_point(&intersection.brep, tip).unwrap(), + PointClassification::Outside + ); + assert_eq!( + classify_point(&subtraction.brep, [0.0, 0.95, 0.95]).unwrap(), + PointClassification::Inside + ); +} + +#[test] +fn union_and_intersection_keep_cutter_faces_unturned_and_mark_whole_faces_preserved() { + let (a, b) = (cube(), bar_turned_about_x()); + let [union, intersection, subtraction] = assert_identities(&a, &b, bar_in_cube()); + for result in [&union, &intersection] { + assert!(result + .brep + .topology + .faces + .iter() + .all(|face| { !face.provenance.reversed && face.provenance.role != FaceRole::Cut })); + } + let roles = |result: &BooleanResult, body: &str| { + result + .brep + .topology + .faces + .iter() + .filter(|face| face.provenance.sources[0].body == body) + .map(|face| face.provenance.role) + .collect::>() + }; + let bar_roles = roles(&union, &b.id); + assert_eq!( + bar_roles + .iter() + .filter(|role| **role == FaceRole::Preserved) + .count(), + 2 + ); + assert!(roles(&union, &a.id) + .iter() + .all(|role| *role == FaceRole::Split)); + assert!(roles(&intersection, &a.id) + .iter() + .chain(&roles(&intersection, &b.id)) + .all(|role| *role == FaceRole::Split)); + assert!(subtraction + .brep + .topology + .faces + .iter() + .any(|face| { face.provenance.reversed && face.provenance.role == FaceRole::Cut })); +} + +#[test] +fn pitched_bar_through_a_u_profile_intersects_in_two_solids() { + let host = extrusion_span( + "u-profile", + 0.0, + 1.0, + vec![ + [0.0, 0.0], + [3.0, 0.0], + [3.0, 3.0], + [2.0, 3.0], + [2.0, 1.0], + [1.0, 1.0], + [1.0, 3.0], + [0.0, 3.0], + ], + Vec::new(), + ); + let pitch = 5.0_f64.to_radians(); + let (x, z) = ( + [pitch.cos(), 0.0, pitch.sin()], + [-pitch.sin(), 0.0, pitch.cos()], + ); + let center = [1.5, 2.25, 0.5]; + let bar = primitives::cuboid( + "pitched-bar".into(), + Frame3 { + origin: std::array::from_fn(|i| { + center[i] - 2.5 * x[i] - 0.25 * z[i] - 0.25 * [0.0, 1.0, 0.0][i] + }), + x, + y: [0.0, 1.0, 0.0], + z, + }, + [5.0, 0.5, 0.5], + ACCURACY, + ) + .unwrap(); + let [union, intersection, subtraction] = assert_identities(&host, &bar, 0.5 / pitch.cos()); + assert_eq!(union.brep.solids.len(), 1); + assert_eq!(intersection.brep.solids.len(), 2); + assert_eq!(subtraction.brep.solids.len(), 1); +} + +fn corner_cube() -> BrepEnvelope { + primitives::cuboid("cube".into(), Frame3::IDENTITY, [2.0, 2.0, 2.0], ACCURACY).unwrap() +} + +fn corner_tetrahedron(id: &str, apex: f64, faces: &[Vec]) -> BrepEnvelope { + primitives::polyhedron( + id.into(), + &[ + [1.0, 1.0, 2.0], + [3.0, 1.0, 2.0], + [1.0, 3.0, 2.0], + [1.0, 1.0, apex], + ], + faces, + ACCURACY, + ) + .unwrap() +} + +#[test] +fn a_face_shared_in_the_same_direction_is_kept_once_by_union_and_intersection() { + let host = corner_cube(); + let pit = corner_tetrahedron( + "pit", + 0.5, + &[vec![0, 1, 2], vec![0, 3, 1], vec![0, 2, 3], vec![1, 3, 2]], + ); + let [union, intersection, subtraction] = assert_identities(&host, &pit, 0.75); + let inside_shared = [1.2, 1.2, 2.0 - 1.0e-3]; + assert_eq!( + classify_point(&union.brep, inside_shared).unwrap(), + PointClassification::Inside + ); + assert_eq!( + classify_point(&intersection.brep, inside_shared).unwrap(), + PointClassification::Inside + ); + assert_eq!( + classify_point(&subtraction.brep, inside_shared).unwrap(), + PointClassification::Outside + ); +} + +#[test] +fn union_drops_a_face_shared_back_to_back_from_both_bodies() { + let host = corner_cube(); + let cap = corner_tetrahedron( + "cap", + 3.5, + &[vec![0, 2, 1], vec![0, 1, 3], vec![0, 3, 2], vec![1, 2, 3]], + ); + let (union, united) = operated(&host, &cap, BooleanOp::Union); + assert!((united - 9.0).abs() <= 1.0e-9 * 9.0, "{united}"); + assert_eq!(union.brep.solids.len(), 1); + assert_eq!( + classify_point(&union.brep, [1.2, 1.2, 2.0]).unwrap(), + PointClassification::Inside + ); +} + +fn faces_on(result: &BooleanResult, height: f64, facing: f64) -> Vec<&Face> { + result + .brep + .topology + .faces + .iter() + .filter(|face| { + let origin = result.brep.geometry.surfaces[face.surface as usize] + .frame() + .origin; + let normal = result.brep.face_normal_at(face.id, origin).unwrap(); + (origin[2] - height).abs() < 1.0e-9 && normal[2] * facing > 1.0 - 1.0e-9 + }) + .collect() +} + +#[test] +fn faces_sharing_a_plane_and_a_facing_merge_into_one_coincident_face() { + let host = corner_cube(); + let tent = primitives::polyhedron( + "tent".into(), + &[ + [1.0, 1.0, 0.0], + [3.0, 1.0, 0.0], + [1.0, 3.0, 0.0], + [1.0, 1.0, 1.5], + ], + &[vec![0, 2, 1], vec![0, 1, 3], vec![0, 3, 2], vec![1, 2, 3]], + ACCURACY, + ) + .unwrap(); + let [union, intersection, _] = assert_identities(&host, &tent, 0.75); + for result in [&union, &intersection] { + let ground = faces_on(result, 0.0, -1.0); + assert_eq!(ground.len(), 1); + assert_eq!(ground[0].provenance.role, FaceRole::Coincident); + let bodies = ground[0] + .provenance + .sources + .iter() + .map(|source| source.body.as_str()) + .collect::>(); + assert_eq!(bodies, [host.id.as_str(), tent.id.as_str()]); + } +} + +#[test] +fn bodies_touching_only_along_an_edge_unite_apart_and_intersect_empty() { + let host = corner_cube(); + let half = std::f64::consts::FRAC_1_SQRT_2; + let ridge = primitives::cuboid( + "ridge".into(), + Frame3 { + origin: [-0.5, 2.0, 2.0], + x: [1.0, 0.0, 0.0], + y: [0.0, half, half], + z: [0.0, -half, half], + }, + [3.0, 0.5, 0.5], + ACCURACY, + ) + .unwrap(); + for (operation, solids, handler) in [ + (BooleanOp::Union, 2, "generic_non_intersecting_boolean"), + (BooleanOp::Intersection, 0, "boolean_planar_polyhedra"), + ] { + let (result, handlers) = + boolean_brep_with_handlers(&host, &ridge, operation, "edge".into()).unwrap(); + assert_eq!(handlers, [handler]); + assert_eq!(result.brep.solids.len(), solids); + } +} + +#[test] +fn subtracting_a_body_standing_on_a_face_leaves_the_host_whole() { + let host = corner_cube(); + let standing = corner_tetrahedron( + "standing", + 3.5, + &[vec![0, 2, 1], vec![0, 1, 3], vec![0, 3, 2], vec![1, 2, 3]], + ); + let (result, measured) = operated(&host, &standing, BooleanOp::Subtraction); + assert!((measured - 8.0).abs() <= 1.0e-9 * 8.0, "{measured}"); + assert_eq!(result.brep.solids.len(), 1); +} + +const WORLD: Accuracy = Accuracy { + geometric: 1.0e-8, + intersection: 1.0e-9, + tessellation: 0.01, + exchange: 1.0e-6, +}; + +fn cube_with_top(id: &str, top: &[Vec]) -> BrepEnvelope { + let mut faces = vec![ + vec![3, 2, 1, 0], + vec![0, 1, 5, 4], + vec![1, 2, 6, 5], + vec![2, 3, 7, 6], + vec![3, 0, 4, 7], + ]; + faces.extend_from_slice(top); + primitives::polyhedron( + id.into(), + &[ + [0.0, 0.0, 0.0], + [2.0, 0.0, 0.0], + [2.0, 2.0, 0.0], + [0.0, 2.0, 0.0], + [0.0, 0.0, 2.0], + [2.0, 0.0, 2.0], + [2.0, 2.0, 2.0], + [0.0, 2.0, 2.0], + ], + &faces, + WORLD, + ) + .unwrap() +} + +fn spike(id: &str, apex: Point3, base: [Point3; 3]) -> BrepEnvelope { + primitives::polyhedron( + id.into(), + &[apex, base[0], base[1], base[2]], + &[vec![0, 2, 1], vec![0, 3, 2], vec![0, 1, 3], vec![1, 2, 3]], + WORLD, + ) + .unwrap() +} + +#[test] +fn a_dip_within_the_tolerance_band_is_contact_for_union_and_intersection() { + let cube = cube_with_top("cube", &[vec![4, 5, 6, 7]]); + for depth in [5.0e-9, 1.0e-8, 2.0e-8] { + let dip = spike( + "dip", + [1.0, 1.0, 2.0 - depth], + [[0.5, 0.5, 3.0], [1.5, 0.5, 3.0], [1.0, 1.5, 3.0]], + ); + let (union, handlers) = + boolean_brep_with_handlers(&cube, &dip, BooleanOp::Union, "dip".into()).unwrap(); + assert_eq!(handlers, ["generic_non_intersecting_boolean"], "{depth}"); + assert_eq!(union.brep.solids.len(), 2); + assert!((volume(&union.brep) - volume(&cube) - volume(&dip)).abs() <= 1.0e-9); + let (intersection, handlers) = + boolean_brep_with_handlers(&cube, &dip, BooleanOp::Intersection, "dip".into()).unwrap(); + assert_eq!(handlers, ["boolean_planar_polyhedra"], "{depth}"); + assert!(intersection.brep.solids.is_empty(), "{depth}"); + assert!(intersection.brep.topology.faces.is_empty(), "{depth}"); + } +} + +#[test] +fn a_dip_deeper_than_the_tolerance_intersects_in_a_solid() { + let cube = cube_with_top("cube", &[vec![4, 5, 6, 7]]); + let dip = spike( + "dip", + [1.0, 1.0, 2.0 - 1.0e-4], + [[0.5, 0.5, 3.0], [1.5, 0.5, 3.0], [1.0, 1.5, 3.0]], + ); + let (intersection, handlers) = + boolean_brep_with_handlers(&cube, &dip, BooleanOp::Intersection, "dip".into()).unwrap(); + assert_eq!(handlers, ["boolean_planar_polyhedra"]); + assert_eq!(intersection.brep.solids.len(), 1); + assert!(volume(&intersection.brep) > 0.0); +} + +#[test] +fn coplanar_faces_of_one_body_stay_apart_where_the_other_body_is_absent() { + let cube = cube_with_top("split-top", &[vec![4, 5, 6], vec![4, 6, 7]]); + let side = spike( + "side", + [1.5, 1.0, 1.0], + [[3.0, 0.5, 0.5], [3.0, 1.5, 0.5], [3.0, 1.0, 1.5]], + ); + let (union, _) = operated(&cube, &side, BooleanOp::Union); + let top = faces_on(&union, 2.0, 1.0); + assert_eq!(top.len(), 2); + for face in top { + assert_eq!(face.provenance.sources.len(), 1); + assert_ne!(face.provenance.role, FaceRole::Coincident); + } +} + +fn turned_slab(overlap: f64) -> BrepEnvelope { + let turn = 30.0_f64.to_radians(); + let corner = |u: f64, v: f64| { + [ + 1.0 + 0.5 * (u * turn.cos() - v * turn.sin()), + 1.0 + 0.5 * (u * turn.sin() + v * turn.cos()), + ] + }; + let ring = [ + corner(-1.0, -1.0), + corner(1.0, -1.0), + corner(1.0, 1.0), + corner(-1.0, 1.0), + ]; + let mut points = ring.map(|[x, y]| [x, y, 2.0 - overlap]).to_vec(); + points.extend(ring.map(|[x, y]| [x, y, 3.0])); + primitives::polyhedron( + "slab".into(), + &points, + &[ + vec![3, 2, 1, 0], + vec![4, 5, 6, 7], + vec![0, 1, 5, 4], + vec![1, 2, 6, 5], + vec![2, 3, 7, 6], + vec![3, 0, 4, 7], + ], + WORLD, + ) + .unwrap() +} + +#[test] +fn a_slab_overlapping_within_the_tolerance_band_never_breaks_topology() { + let cube = cube_with_top("cube", &[vec![4, 5, 6, 7]]); + for overlap in [1.1e-8, 1.5e-8, 2.0e-8] { + for operation in [BooleanOp::Union, BooleanOp::Intersection] { + let result = + boolean_brep_with_handlers(&cube, &turned_slab(overlap), operation, "slab".into()); + assert!( + !matches!(result, Err(GeometryError::InvalidTopology(_))), + "{overlap} {operation:?}: {:?}", + result.err() + ); + } + } + for overlap in [2.5e-8, 1.0e-6] { + let (intersection, _) = boolean_brep_with_handlers( + &cube, + &turned_slab(overlap), + BooleanOp::Intersection, + "slab".into(), + ) + .unwrap(); + assert!( + (volume(&intersection.brep) - overlap).abs() <= 1.0e-12, + "{overlap}" + ); + } +} diff --git a/main/opengeometry/tests/booleans/point_classification.rs b/main/opengeometry/tests/booleans/point_classification.rs index cbd4fba6..f177f403 100644 --- a/main/opengeometry/tests/booleans/point_classification.rs +++ b/main/opengeometry/tests/booleans/point_classification.rs @@ -1,8 +1,11 @@ -use opengeometry::brep::{BrepEnvelope, Frame3}; +use opengeometry::brep::{BrepEnvelope, Frame3, GeometryError}; use opengeometry::operations::modifying::boolean::{boolean_brep, BooleanOp}; use opengeometry::primitives; use opengeometry::query::{classify_point, PointClassification}; +use opengeometry::world_graph::{CreatingOperation, Primitive}; use opengeometry_test_support::accuracy::accuracy; +use opengeometry_test_support::volume; +use opengeometry_test_support::world_graph::{graph, named}; #[test] fn analytic_ray_classification_covers_all_surface_families() { @@ -153,3 +156,254 @@ fn each_solid_of_a_body_cut_in_two_classifies_its_own_points() { ); } } + +fn ball(id: &str, origin: [f64; 3], radius: f64) -> BrepEnvelope { + let frame = Frame3 { + origin, + ..Frame3::IDENTITY + }; + primitives::sphere(id.into(), frame, radius, accuracy(1e-5)).unwrap() +} + +fn cube(id: &str, origin: [f64; 3], size: f64) -> BrepEnvelope { + let frame = Frame3 { + origin, + ..Frame3::IDENTITY + }; + primitives::cuboid(id.into(), frame, [size; 3], accuracy(1e-5)).unwrap() +} + +fn assert_refused(a: &BrepEnvelope, b: &BrepEnvelope) { + for (first, second) in [(a, b), (b, a)] { + for operation in [ + BooleanOp::Union, + BooleanOp::Intersection, + BooleanOp::Subtraction, + ] { + let Err(error) = boolean_brep(first, second, operation, "refused".into()) else { + panic!("{} {operation:?} {} built", first.id, second.id); + }; + assert!( + matches!( + &error, + GeometryError::UnresolvedIntersection(message) + if message == "generic boolean containment classification is unresolved" + ), + "{} {operation:?} {}: {error:?}", + first.id, + second.id + ); + } + } +} + +#[test] +fn ball_around_the_cavity_of_a_hollow_cube_is_refused_not_measured_wrong() { + let outer = cube("outer", [0.0; 3], 4.0); + let hollow = boolean_brep( + &outer, + &cube("cavity", [1.0; 3], 2.0), + BooleanOp::Subtraction, + "hollow".into(), + ) + .unwrap() + .brep; + assert_refused(&hollow, &ball("ball", [2.0; 3], 1.9)); +} + +#[test] +fn two_balls_in_one_body_against_a_cube_holding_one_are_refused_not_measured_wrong() { + let pair = boolean_brep( + &ball("near", [2.0; 3], 0.5), + &ball("far", [12.0, 2.0, 2.0], 0.5), + BooleanOp::Union, + "balls".into(), + ) + .unwrap() + .brep; + assert_eq!(solid_count(&pair), 2); + assert_refused(&pair, &cube("cube", [0.0; 3], 4.0)); +} + +#[test] +fn cube_minus_two_balls_inside_it_keeps_both_balls_as_cavities() { + let pair = boolean_brep( + &ball("left", [2.0, 4.0, 4.0], 0.5), + &ball("right", [6.0, 4.0, 4.0], 0.5), + BooleanOp::Union, + "balls".into(), + ) + .unwrap() + .brep; + let cube = cube("cube", [0.0; 3], 8.0); + let result = boolean_brep(&cube, &pair, BooleanOp::Subtraction, "hollowed".into()) + .unwrap() + .brep; + assert_eq!(solid_count(&result), 1); + for (point, place) in [ + ([2.0, 4.0, 4.0], PointClassification::Outside), + ([6.0, 4.0, 4.0], PointClassification::Outside), + ([4.0, 4.0, 4.0], PointClassification::Inside), + ] { + assert_eq!(classify_point(&result, point).unwrap(), place, "{point:?}"); + } + let measured = volume::estimate(&result, 0.001); + let expected = 512.0 - 2.0 * std::f64::consts::PI / 6.0; + assert!((measured.value - expected).abs() <= measured.error_bound); +} + +#[test] +fn open_tube_inside_a_cube_is_refused() { + let mut world = graph(); + world + .create_primitive(Primitive::Circle { radius: 0.3 }, named("profile")) + .unwrap(); + world + .create_primitive( + Primitive::Polyline { + points: vec![[0.0; 3], [0.0, 1.0, 0.0]], + closed: false, + }, + named("path"), + ) + .unwrap(); + world + .create_operation( + CreatingOperation::Sweep { + profile: "profile".into(), + path: "path".into(), + caps: Some(false), + }, + named("tube"), + ) + .unwrap(); + let tube = world.placed_brep("tube").unwrap().as_ref().clone(); + let cube = cube("cube", [-2.0, -1.5, -2.0], 4.0); + for (first, second) in [(&cube, &tube), (&tube, &cube)] { + for operation in [ + BooleanOp::Union, + BooleanOp::Intersection, + BooleanOp::Subtraction, + ] { + assert!( + boolean_brep(first, second, operation, "sheet".into()).is_err(), + "{} {operation:?} {}", + first.id, + second.id + ); + } + } +} + +#[test] +fn ball_inside_the_second_of_two_cubes_becomes_that_cubes_cavity() { + let cubes = boolean_brep( + &cube("first", [0.0; 3], 4.0), + &cube("second", [10.0, 0.0, 0.0], 4.0), + BooleanOp::Union, + "cubes".into(), + ) + .unwrap() + .brep; + assert_eq!(solid_count(&cubes), 2); + let ball = ball("ball", [12.0, 2.0, 2.0], 1.0); + let result = boolean_brep(&cubes, &ball, BooleanOp::Subtraction, "hollowed".into()) + .unwrap() + .brep; + result.validate().unwrap(); + let solids = serde_json::to_value(&result).unwrap()["solids"].clone(); + assert_eq!(solids[0]["cavity_shells"].as_array().unwrap().len(), 0); + assert_eq!(solids[1]["cavity_shells"].as_array().unwrap().len(), 1); + assert_eq!( + classify_point(&result, [12.0, 2.0, 2.0]).unwrap(), + PointClassification::Outside + ); + let measured = volume::estimate(&result, 0.001); + let expected = 128.0 - 4.0 / 3.0 * std::f64::consts::PI; + assert!((measured.value - expected).abs() <= measured.error_bound); +} + +fn hollow_ball(id: &str) -> BrepEnvelope { + boolean_brep( + &ball("outer", [4.0; 3], 2.0), + &ball("inner", [4.0; 3], 1.0), + BooleanOp::Subtraction, + id.into(), + ) + .unwrap() + .brep +} + +fn assert_cube_minus(cutter: &BrepEnvelope, host: &BrepEnvelope, places: &[([f64; 3], bool)]) { + let result = boolean_brep(host, cutter, BooleanOp::Subtraction, "hollowed".into()) + .unwrap() + .brep; + result.validate().unwrap(); + assert_eq!(solid_count(&result), 2); + for (point, inside) in places { + let place = if *inside { + PointClassification::Inside + } else { + PointClassification::Outside + }; + assert_eq!(classify_point(&result, *point).unwrap(), place, "{point:?}"); + } + let [measured, host, cutter] = + [&result, host, cutter].map(|body| volume::estimate(body, 0.001)); + let bound = measured.error_bound + host.error_bound + cutter.error_bound; + assert!((measured.value - (host.value - cutter.value)).abs() <= bound); +} + +#[test] +fn cube_minus_a_hollow_ball_inside_it_keeps_the_core_as_a_second_solid() { + assert_cube_minus( + &hollow_ball("hollow"), + &cube("cube", [0.0; 3], 8.0), + &[([4.0; 3], true), ([4.0, 4.0, 5.5], false), ([1.0; 3], true)], + ); +} + +#[test] +fn hollow_ball_around_the_cavity_of_a_cube_leaves_that_cavity_in_the_core() { + let host = boolean_brep( + &cube("cube", [0.0; 3], 8.0), + &ball("cavity", [4.0; 3], 0.5), + BooleanOp::Subtraction, + "host".into(), + ) + .unwrap() + .brep; + assert_cube_minus( + &hollow_ball("hollow"), + &host, + &[ + ([4.0; 3], false), + ([4.0, 4.0, 4.75], true), + ([4.0, 4.0, 5.5], false), + ([1.0; 3], true), + ], + ); +} + +#[test] +fn ball_in_the_hollow_of_a_cutter_becomes_a_cavity_of_the_core() { + let cutter = boolean_brep( + &hollow_ball("hollow"), + &ball("seed", [4.0; 3], 0.5), + BooleanOp::Union, + "cutter".into(), + ) + .unwrap() + .brep; + assert_eq!(solid_count(&cutter), 2); + assert_cube_minus( + &cutter, + &cube("cube", [0.0; 3], 8.0), + &[ + ([4.0; 3], false), + ([4.0, 4.0, 4.75], true), + ([4.0, 4.0, 5.5], false), + ([1.0; 3], true), + ], + ); +} diff --git a/main/opengeometry/tests/cases/matrix.rs b/main/opengeometry/tests/cases/matrix.rs index d9c3eeec..43d697bb 100644 --- a/main/opengeometry/tests/cases/matrix.rs +++ b/main/opengeometry/tests/cases/matrix.rs @@ -3,6 +3,7 @@ use opengeometry::operations::modifying::boolean::{ boolean_brep_outcome_with_handlers, boolean_brep_with_handlers, BooleanOp, }; use opengeometry::primitives; +use opengeometry::query::validity; use opengeometry_test_support::{stored, volume}; use serde_json::json; @@ -152,47 +153,47 @@ fn boolean_support_matrix_matches_stored_outcomes_and_handlers() { } #[test] -fn i2_uncovered_rotated_box_returns_coverage_gap_without_mesh_fallback() { - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests/fixtures/cases/boolean-matrix/rotated-box-union.json"); - let fixture: serde_json::Value = serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap(); - let a: BrepEnvelope = serde_json::from_value(fixture["a"].clone()).unwrap(); - let b: BrepEnvelope = serde_json::from_value(fixture["b"].clone()).unwrap(); - let nearest_vertex_to_plane = |vertices: &BrepEnvelope, planes: &BrepEnvelope| { - vertices - .topology - .vertices - .iter() - .flat_map(|vertex| { - planes.geometry.surfaces.iter().filter_map(move |surface| { - if let opengeometry::brep::SurfaceGeometry::Plane { frame } = surface { - let delta = std::array::from_fn::<_, 3, _>(|i| { - vertex.position[i] - frame.origin[i] - }); - Some((0..3).map(|i| delta[i] * frame.z[i]).sum::().abs()) - } else { - None - } - }) - }) - .fold(f64::INFINITY, f64::min) +fn rotated_box_union_and_intersection_build_through_planar_polyhedra() { + let fixture = |row: &str| -> serde_json::Value { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join(format!("tests/fixtures/cases/boolean-matrix/{row}.json")); + serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap() }; - let clearance = nearest_vertex_to_plane(&a, &b).min(nearest_vertex_to_plane(&b, &a)); + let rows = [ + (fixture("rotated-box-union"), BooleanOp::Union), + (fixture("rotated-box-intersection"), BooleanOp::Intersection), + ]; + assert_eq!(rows[0].0["a"], rows[1].0["a"]); + assert_eq!(rows[0].0["b"], rows[1].0["b"]); + let a: BrepEnvelope = serde_json::from_value(rows[0].0["a"].clone()).unwrap(); + let b: BrepEnvelope = serde_json::from_value(rows[0].0["b"].clone()).unwrap(); + let [union, intersection] = rows.map(|(_, operation)| { + let (result, handlers) = + boolean_brep_outcome_with_handlers(&a, &b, operation, format!("{operation:?}")); + let body = result.unwrap().brep; + body.validate().unwrap(); + let checked = validity(&body); + assert!(checked.valid && checked.closed, "{operation:?}"); + let value = serde_json::to_value(&body).unwrap(); + assert_eq!( + value["solids"].as_array().unwrap().len(), + 1, + "{operation:?}" + ); + assert_eq!(handlers, ["boolean_planar_polyhedra"], "{operation:?}"); + volume::estimate(&body, 0.01) + }); + let [a, b] = [&a, &b].map(|body| volume::estimate(body, 0.01)); + let slack = a.error_bound + b.error_bound + union.error_bound + intersection.error_bound; + assert!(intersection.value > 0.0); assert!( - clearance > 10.0 * a.accuracy.geometric, - "clearance={clearance}" - ); - let (result, handlers) = boolean_brep_outcome_with_handlers( - &a, - &b, - BooleanOp::Union, - "matrix-rotated-box-union".into(), + (union.value - (a.value + b.value - intersection.value)).abs() <= slack + 1e-9, + "union={} a={} b={} intersection={}", + union.value, + a.value, + b.value, + intersection.value ); - assert!(matches!( - result, - Err(opengeometry::brep::GeometryError::CoverageGap { .. }) - )); - assert_eq!(json!(handlers), fixture["handlers"]); } #[test] diff --git a/main/opengeometry/tests/creating/main.rs b/main/opengeometry/tests/creating/main.rs index 011270bb..2f869988 100644 --- a/main/opengeometry/tests/creating/main.rs +++ b/main/opengeometry/tests/creating/main.rs @@ -3,6 +3,9 @@ mod holes; mod mesh_walls; mod pipe_segment; mod polyhedron; +mod polyhedron_boolean_checks; +mod polyhedron_boolean_contacts; +mod polyhedron_booleans; mod polysolid; mod polysolid_arcs; mod profiles; diff --git a/main/opengeometry/tests/creating/polyhedron.rs b/main/opengeometry/tests/creating/polyhedron.rs index 287ec158..fd0208b4 100644 --- a/main/opengeometry/tests/creating/polyhedron.rs +++ b/main/opengeometry/tests/creating/polyhedron.rs @@ -7,7 +7,7 @@ use opengeometry_test_support::part21::Document; use opengeometry_test_support::volume; use opengeometry_test_support::world_graph::{graph, named}; -const HIP_FACES: [&[u32]; 5] = [ +pub(super) const HIP_FACES: [&[u32]; 5] = [ &[0, 1, 2, 3], &[1, 0, 4, 5], &[2, 1, 5], @@ -22,7 +22,7 @@ const EAVES: [Point3; 4] = [ [0.0, 0.0, 2.0], ]; -fn polyhedron(points: &[Point3], faces: &[&[u32]]) -> Primitive { +pub(super) fn polyhedron(points: &[Point3], faces: &[&[u32]]) -> Primitive { Primitive::Polyhedron { points: points.to_vec(), faces: faces.iter().map(|ring| ring.to_vec()).collect(), @@ -34,15 +34,15 @@ fn roof(ridge: [Point3; 2]) -> Primitive { polyhedron(&points, &HIP_FACES) } -fn hip_roof() -> Primitive { +pub(super) fn hip_roof() -> Primitive { roof([[1.0, 1.0, 1.0], [3.0, 1.0, 1.0]]) } -fn gable_roof() -> Primitive { +pub(super) fn gable_roof() -> Primitive { roof([[0.0, 1.0, 1.0], [4.0, 1.0, 1.0]]) } -fn l_roof() -> Primitive { +pub(super) fn l_roof() -> Primitive { let points = [ [0.0, 0.0, 0.0], [4.0, 0.0, 0.0], diff --git a/main/opengeometry/tests/creating/polyhedron_boolean_checks.rs b/main/opengeometry/tests/creating/polyhedron_boolean_checks.rs new file mode 100644 index 00000000..2b1b0ef9 --- /dev/null +++ b/main/opengeometry/tests/creating/polyhedron_boolean_checks.rs @@ -0,0 +1,347 @@ +use opengeometry::brep::{BrepEnvelope, FaceRole}; +use opengeometry::math::Point3; +use opengeometry::operations::modifying::boolean::{boolean_brep_outcome_with_handlers, BooleanOp}; +use opengeometry::query::{classify_point, validity, PointClassification}; +use opengeometry::tessellation::tessellate; +use opengeometry::world_graph::{Primitive, Transform}; +use opengeometry_test_support::volume::{self, VolumeEstimate}; +use opengeometry_test_support::world_graph::{graph, named}; +use serde_json::{json, Value}; +use std::collections::BTreeSet; + +const OFFSET: f64 = 1e-4; + +const NEAR: f64 = 1e-7; + +pub(super) struct Pair { + a: BrepEnvelope, + b: BrepEnvelope, +} + +pub(super) struct Built { + brep: BrepEnvelope, + handlers: Vec, + report: Value, + volume: VolumeEstimate, +} + +struct FaceSample { + face: u32, + point: Point3, + normal: Point3, +} + +impl Pair { + pub(super) fn new(a: Primitive, b: Primitive) -> Self { + Self::placed([(a, &[]), (b, &[])]) + } + + pub(super) fn placed(parts: [(Primitive, &[Transform]); 2]) -> Self { + let mut world = graph(); + for (og_id, (primitive, transforms)) in ["a", "b"].into_iter().zip(parts) { + world.create_primitive(primitive, named(og_id)).unwrap(); + for transform in transforms { + world.transform(og_id, *transform).unwrap(); + } + } + let [a, b] = ["a", "b"].map(|og_id| world.placed_brep(og_id).unwrap().as_ref().clone()); + assert_ne!(a.id, b.id); + Self { a, b } + } + + pub(super) fn built(&self, operation: BooleanOp, solids: usize) -> Built { + let label = format!("{operation:?}"); + let (result, handlers) = + boolean_brep_outcome_with_handlers(&self.a, &self.b, operation, label.clone()); + let result = result.unwrap_or_else(|error| panic!("{label} via {handlers:?}: {error:?}")); + let report = serde_json::to_value(&result.report).unwrap(); + assert_eq!(report["operation"], json!(operation), "{label}"); + assert_closed_solids(&result.brep, solids, &label); + assert_outward(&result.brep, &label); + let volume = volume::estimate(&result.brep, 0.001); + Built { + brep: result.brep, + handlers, + report, + volume, + } + } + + pub(super) fn planar(&self, operation: BooleanOp, solids: usize) -> Built { + let built = self.built(operation, solids); + assert_eq!( + built.handlers, + ["boolean_planar_polyhedra"], + "{operation:?}" + ); + for face in &built.brep.topology.faces { + let provenance = &face.provenance; + assert_ne!(provenance.role, FaceRole::Cut, "{operation:?} {}", face.id); + assert!(!provenance.sources.is_empty(), "{operation:?} {}", face.id); + assert!( + provenance + .sources + .iter() + .all(|source| source.body == self.a.id || source.body == self.b.id), + "{operation:?} {}", + face.id + ); + } + for input in [&self.a, &self.b] { + let every_face = (0..input.topology.faces.len() as u64).collect::>(); + assert_eq!( + mapped_faces(&built.report, &input.id), + every_face, + "{operation:?}" + ); + } + built + } + + pub(super) fn assert_touching_kept_apart(&self) { + let [a, b] = [&self.a, &self.b].map(|body| volume::estimate(body, 0.001)); + let union = self.built(BooleanOp::Union, 2); + assert_eq!(union.handlers, ["generic_non_intersecting_boolean"]); + union.assert_volume(a.value + b.value); + self.assert_empty_intersection(); + let difference = self.built(BooleanOp::Subtraction, 1); + assert_eq!(difference.handlers, ["subtract_planar_polyhedra"]); + difference.assert_volume(a.value); + } + + pub(super) fn assert_empty_intersection(&self) { + self.assert_empty(BooleanOp::Intersection, "boolean_planar_polyhedra"); + } + + pub(super) fn assert_empty(&self, operation: BooleanOp, handler: &str) { + let (result, handlers) = + boolean_brep_outcome_with_handlers(&self.a, &self.b, operation, "empty".into()); + let result = result.unwrap_or_else(|error| panic!("{operation:?}: {error:?}")); + assert_eq!(handlers, [handler]); + result.brep.validate().unwrap(); + let topology = &result.brep.topology; + assert!(topology.vertices.is_empty() && topology.edges.is_empty()); + assert!(topology.faces.is_empty()); + let value = serde_json::to_value(&result.brep).unwrap(); + assert_eq!(value["solids"], json!([])); + assert_eq!(value["topology"]["shells"], json!([])); + let report = serde_json::to_value(&result.report).unwrap(); + assert_eq!(report["operation"], json!(operation)); + } + + pub(super) fn assert_identities( + &self, + union: &Built, + intersection: &Built, + difference: &Built, + ) { + let [a, b] = [&self.a, &self.b].map(|body| volume::estimate(body, 0.001)); + let [u, i, d] = [union, intersection, difference].map(|built| &built.volume); + let union_slack = a.error_bound + b.error_bound + u.error_bound + i.error_bound + 1e-9; + assert!( + (u.value - (a.value + b.value - i.value)).abs() <= union_slack, + "union {} versus {} + {} - {}", + u.value, + a.value, + b.value, + i.value + ); + let difference_slack = a.error_bound + d.error_bound + i.error_bound + 1e-9; + assert!( + (d.value - (a.value - i.value)).abs() <= difference_slack, + "difference {} versus {} - {}", + d.value, + a.value, + i.value + ); + } + + pub(super) fn assert_both_contribute(&self, built: &Built) { + for input in [&self.a, &self.b] { + assert!( + built + .brep + .topology + .faces + .iter() + .any(|face| source_bodies(&built.brep, face.id).contains(&input.id)), + "{}", + input.id + ); + } + } + + pub(super) fn assert_edge_between_inputs(&self, built: &Built, ends: [Point3; 2]) { + let faces = edge_faces(&built.brep, ends); + assert_eq!(faces.len(), 2, "{ends:?}"); + let bodies = faces + .into_iter() + .map(|face| source_bodies(&built.brep, face)) + .collect::>(); + let [a, b] = [&self.a.id, &self.b.id].map(|id| BTreeSet::from([id.clone()])); + assert!( + bodies == [a.clone(), b.clone()] || bodies == [b, a], + "{ends:?}: {bodies:?}" + ); + } + + pub(super) fn assert_one_coincident_face(&self, built: &Built, point: Point3, normal: Point3) { + let faces = built.faces_on_plane(point, normal); + assert_eq!(faces.len(), 1, "{faces:?}"); + let face = &built.brep.topology.faces[faces[0] as usize]; + assert_eq!(face.provenance.role, FaceRole::Coincident); + assert_eq!( + source_bodies(&built.brep, face.id), + BTreeSet::from([self.a.id.clone(), self.b.id.clone()]) + ); + } +} + +impl Built { + pub(super) fn assert_volume(&self, expected: f64) { + assert!( + (self.volume.value - expected).abs() <= self.volume.error_bound, + "{} versus {expected} with bound {}", + self.volume.value, + self.volume.error_bound + ); + } + + pub(super) fn handlers(&self) -> &[String] { + &self.handlers + } + + pub(super) fn faces_on_plane(&self, point: Point3, normal: Point3) -> Vec { + face_samples(&self.brep) + .into_iter() + .filter(|sample| { + dot(sample.normal, normal) > 1.0 - NEAR + && dot(difference(sample.point, point), normal).abs() < NEAR + }) + .map(|sample| sample.face) + .collect() + } + + pub(super) fn assert_vertex(&self, point: Point3) { + assert!( + self.brep + .topology + .vertices + .iter() + .any(|vertex| near(vertex.position, point)), + "{point:?}" + ); + } +} + +fn assert_closed_solids(body: &BrepEnvelope, solids: usize, label: &str) { + body.validate().unwrap(); + let checked = validity(body); + assert!(checked.valid && checked.closed, "{label}: {checked:?}"); + let value = serde_json::to_value(body).unwrap(); + assert_eq!(value["solids"].as_array().unwrap().len(), solids, "{label}"); +} + +fn assert_outward(body: &BrepEnvelope, label: &str) { + for sample in face_samples(body) { + let side = |sign: f64| { + let point = std::array::from_fn(|i| sample.point[i] + sign * OFFSET * sample.normal[i]); + classify_point(body, point).unwrap() + }; + assert_eq!( + [side(1.0), side(-1.0)], + [PointClassification::Outside, PointClassification::Inside], + "{label} face {} at {:?}", + sample.face, + sample.point + ); + } +} + +fn face_samples(body: &BrepEnvelope) -> Vec { + let mesh = tessellate(body, 0.01, 1 << 20).unwrap(); + let corner = + |index: u32| -> Point3 { std::array::from_fn(|i| mesh.positions[index as usize * 3 + i]) }; + let triangles = mesh + .indices + .chunks(3) + .zip(&mesh.triangle_face_ids) + .map(|(corners, face)| (*face, [0, 1, 2].map(|i| corner(corners[i])))) + .collect::>(); + body.topology + .faces + .iter() + .map(|face| { + let [p, q, r] = triangles + .iter() + .filter(|(owner, _)| *owner == face.id) + .map(|(_, points)| *points) + .max_by(|x, y| area(*x).total_cmp(&area(*y))) + .unwrap_or_else(|| panic!("face {} has no triangle", face.id)); + let point = std::array::from_fn(|i| (p[i] + q[i] + r[i]) / 3.0); + FaceSample { + face: face.id, + point, + normal: body.face_normal_at(face.id, point).unwrap(), + } + }) + .collect() +} + +fn edge_faces(body: &BrepEnvelope, ends: [Point3; 2]) -> Vec { + let value = serde_json::to_value(body).unwrap(); + let halfedges = value["topology"]["halfedges"].as_array().unwrap(); + let position = |vertex: u32| body.topology.vertices[vertex as usize].position; + body.topology + .halfedges + .iter() + .zip(halfedges) + .filter(|(halfedge, _)| { + let found = [position(halfedge.from), position(halfedge.to)]; + (near(found[0], ends[0]) && near(found[1], ends[1])) + || (near(found[0], ends[1]) && near(found[1], ends[0])) + }) + .map(|(_, json)| json["face"].as_u64().unwrap() as u32) + .collect() +} + +fn source_bodies(body: &BrepEnvelope, face: u32) -> BTreeSet { + body.topology.faces[face as usize] + .provenance + .sources + .iter() + .map(|source| source.body.clone()) + .collect() +} + +fn mapped_faces(report: &Value, body: &str) -> BTreeSet { + report["face_mappings"] + .as_array() + .unwrap() + .iter() + .filter(|mapping| mapping["source"]["body"] == json!(body)) + .map(|mapping| mapping["source"]["face"].as_u64().unwrap()) + .collect() +} + +fn area([p, q, r]: [Point3; 3]) -> f64 { + let [u, v] = [difference(q, p), difference(r, p)]; + let normal = [ + u[1] * v[2] - u[2] * v[1], + u[2] * v[0] - u[0] * v[2], + u[0] * v[1] - u[1] * v[0], + ]; + dot(normal, normal).sqrt() / 2.0 +} + +fn difference(p: Point3, q: Point3) -> Point3 { + std::array::from_fn(|i| p[i] - q[i]) +} + +fn dot(p: Point3, q: Point3) -> f64 { + (0..3).map(|i| p[i] * q[i]).sum() +} + +fn near(p: Point3, q: Point3) -> bool { + (0..3).all(|i| (p[i] - q[i]).abs() < NEAR) +} diff --git a/main/opengeometry/tests/creating/polyhedron_boolean_contacts.rs b/main/opengeometry/tests/creating/polyhedron_boolean_contacts.rs new file mode 100644 index 00000000..2d55e11e --- /dev/null +++ b/main/opengeometry/tests/creating/polyhedron_boolean_contacts.rs @@ -0,0 +1,353 @@ +use super::polyhedron::{gable_roof, l_roof, polyhedron}; +use super::polyhedron_boolean_checks::Pair; +use super::polyhedron_booleans::{tetrahedron, triangular_prism}; +use opengeometry::math::Point3; +use opengeometry::operations::modifying::boolean::BooleanOp; +use opengeometry::world_graph::{Primitive, Transform}; + +fn cuboid(size: f64) -> Primitive { + Primitive::Cuboid { + width: size, + height: size, + depth: size, + } +} + +fn turned(degrees: f64, pivot: Point3) -> Transform { + Transform::Rotate { + axis: [0.0, 1.0, 0.0], + degrees, + pivot: Some(pivot), + } +} + +fn hexahedron(origin: Point3, [a, b, c]: [Point3; 3]) -> Primitive { + let points = (0..8) + .map(|corner| { + let steps = [corner & 1, (corner >> 1) & 1, (corner >> 2) & 1].map(f64::from); + std::array::from_fn(|i| origin[i] + steps[0] * a[i] + steps[1] * b[i] + steps[2] * c[i]) + }) + .collect::>(); + let faces: [&[u32]; 6] = [ + &[0, 4, 6, 2], + &[1, 3, 7, 5], + &[0, 1, 5, 4], + &[2, 6, 7, 3], + &[0, 2, 3, 1], + &[4, 5, 7, 6], + ]; + polyhedron(&points, &faces) +} + +#[test] +fn turned_unit_cuboid_unions_and_intersects_with_a_cuboid() { + let pair = Pair::placed([ + (cuboid(2.0), &[]), + ( + cuboid(1.0), + &[ + Transform::Translate { + offset: [1.5, 0.5, 0.0], + }, + turned(30.0, [1.5, 0.5, 0.0]), + ], + ), + ]); + let clipped = (2.0 - 3f64.sqrt()) / (4.0 * 3f64.sqrt()); + let union = pair.planar(BooleanOp::Union, 1); + let intersection = pair.planar(BooleanOp::Intersection, 1); + let difference = pair.built(BooleanOp::Subtraction, 1); + union.assert_volume(9.0 - clipped); + intersection.assert_volume(clipped); + difference.assert_volume(8.0 - clipped); + pair.assert_identities(&union, &intersection, &difference); + pair.assert_both_contribute(&union); + pair.assert_both_contribute(&intersection); +} + +#[test] +fn bar_across_both_arms_of_an_l_roof_intersects_in_two_solids() { + let bar = hexahedron( + [-0.75, -1.0, 5.25], + [[6.0, 0.0, -6.0], [0.0, 3.0, 0.0], [0.5, 0.0, 0.5]], + ); + let pair = Pair::new(l_roof(), bar); + let union = pair.planar(BooleanOp::Union, 1); + let intersection = pair.planar(BooleanOp::Intersection, 2); + let difference = pair.built(BooleanOp::Subtraction, 3); + union.assert_volume(547.0 / 24.0); + intersection.assert_volume(13.0 / 24.0); + difference.assert_volume(115.0 / 24.0); + pair.assert_identities(&union, &intersection, &difference); + pair.assert_both_contribute(&intersection); +} + +#[test] +fn l_roof_and_a_box_through_one_arm_union_into_one_solid() { + let post = hexahedron( + [0.5, -0.5, 0.5], + [[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]], + ); + let pair = Pair::new(l_roof(), post); + let union = pair.planar(BooleanOp::Union, 1); + let intersection = pair.planar(BooleanOp::Intersection, 1); + let difference = pair.built(BooleanOp::Subtraction, 1); + union.assert_volume(73.0 / 8.0); + intersection.assert_volume(53.0 / 24.0); + difference.assert_volume(25.0 / 8.0); + pair.assert_identities(&union, &intersection, &difference); + pair.assert_both_contribute(&union); + pair.assert_both_contribute(&intersection); +} + +#[test] +fn turned_box_inside_a_turned_box_unions_to_the_outer_and_intersects_to_the_inner() { + let pair = Pair::placed([ + (cuboid(2.0), &[turned(30.0, [0.0, 1.0, 0.0])]), + ( + cuboid(1.0), + &[ + Transform::Translate { + offset: [0.0, 0.5, 0.0], + }, + turned(45.0, [0.0, 1.0, 0.0]), + ], + ), + ]); + let union = pair.built(BooleanOp::Union, 1); + let intersection = pair.built(BooleanOp::Intersection, 1); + let difference = pair.built(BooleanOp::Subtraction, 1); + union.assert_volume(8.0); + intersection.assert_volume(1.0); + difference.assert_volume(7.0); + pair.assert_identities(&union, &intersection, &difference); +} + +#[test] +fn wedge_touching_the_ridge_along_an_edge_unions_apart_and_intersects_empty() { + let pair = Pair::new( + gable_roof(), + triangular_prism([[1.0, 1.0], [2.0, 0.5], [2.0, 1.5]], [1.0, 3.0]), + ); + pair.assert_touching_kept_apart(); +} + +#[test] +fn tetrahedron_touching_the_ridge_end_at_a_corner_unions_apart_and_intersects_empty() { + let tetrahedron = tetrahedron([ + [4.0, 1.0, 1.0], + [5.0, 1.0, 2.0], + [5.0, 1.0, 0.0], + [5.0, 2.0, 1.0], + ]); + let pair = Pair::new(gable_roof(), tetrahedron); + pair.assert_touching_kept_apart(); +} + +#[test] +fn box_standing_on_a_turned_cuboid_subtracts_to_the_unchanged_host() { + let pair = Pair::placed([ + (cuboid(1.0), &[turned(30.0, [0.0, 0.5, 0.0])]), + ( + cuboid(0.2), + &[Transform::Translate { + offset: [0.0, 1.0, 0.0], + }], + ), + ]); + pair.built(BooleanOp::Subtraction, 1).assert_volume(1.0); +} + +fn eave_slab(plate: bool, shift: f64) -> Primitive { + let (ridge, eave, sole) = (5.3006226146291295, 2.8498962308951454, 2.6498962308951453); + let inner = if plate { 5.100622614629129 } else { sole }; + let points = [ + [-0.5, ridge, 3.0], + [8.5, ridge, 3.0], + [8.5, eave, -0.5], + [-0.5, eave, -0.5], + [-0.5, sole, -0.5], + [8.5, sole, -0.5], + [8.5, inner, 3.0], + [-0.5, inner, 3.0], + [-0.5, eave, 6.5], + [8.5, eave, 6.5], + [8.5, sole, 6.5], + [-0.5, sole, 6.5], + ] + .map(|[x, y, z]| [x, y, z + shift]); + let faces: [&[u32]; 10] = [ + &[0, 1, 2, 3], + &[4, 5, 6, 7], + &[1, 0, 8, 9], + &[10, 11, 7, 6], + &[2, 1, 6, 5], + &[3, 2, 5, 4], + &[0, 3, 4, 7], + &[8, 0, 7, 11], + &[9, 8, 11, 10], + &[1, 9, 10, 6], + ]; + polyhedron(&points, &faces) +} + +#[test] +fn mass_roofs_touching_along_an_eave_intersect_empty() { + Pair::new(eave_slab(false, 0.0), eave_slab(false, 7.0)).assert_empty_intersection(); +} + +#[test] +fn plate_roofs_touching_along_an_eave_intersect_empty() { + Pair::new(eave_slab(true, 0.0), eave_slab(true, 7.0)).assert_empty_intersection(); +} + +fn gable_on_a_wall_top(length: f64, span: f64, ridge: f64) -> Primitive { + let points = [ + [0.0, ridge, span / 2.0], + [length, ridge, span / 2.0], + [length, 3.2, 0.0], + [0.0, 3.2, 0.0], + [0.0, 3.0, 0.0], + [length, 3.0, 0.0], + [length, 3.0, span / 2.0], + [0.0, 3.0, span / 2.0], + [0.0, 3.2, span], + [length, 3.2, span], + [length, 3.0, span], + [0.0, 3.0, span], + ]; + let faces: [&[u32]; 10] = [ + &[0, 1, 2, 3], + &[4, 5, 6, 7], + &[1, 0, 8, 9], + &[10, 11, 7, 6], + &[2, 1, 6, 5], + &[3, 2, 5, 4], + &[0, 3, 4, 7], + &[8, 0, 7, 11], + &[9, 8, 11, 10], + &[1, 9, 10, 6], + ]; + polyhedron(&points, &faces) +} + +#[test] +fn roof_wholly_inside_a_roof_on_the_same_wall_top_subtracts_to_nothing() { + let (main_ridge, wing_ridge) = (5.3006226146291295, 3.9002075382097097); + let main = 8.0 * (6.0 * 0.2 + 3.0 * (main_ridge - 3.2)); + let wing = 3.0 * (2.0 * 0.2 + (wing_ridge - 3.2)); + let wing_place = [ + turned(90.0, [0.0, 0.0, 0.0]), + Transform::Translate { + offset: [3.0, 0.0, 4.5], + }, + ]; + let main_roof = || gable_on_a_wall_top(8.0, 6.0, main_ridge); + let wing_roof = || gable_on_a_wall_top(3.0, 2.0, wing_ridge); + let pair = Pair::placed([(wing_roof(), &wing_place), (main_roof(), &[])]); + pair.assert_empty(BooleanOp::Subtraction, "subtract_planar_polyhedra"); + pair.built(BooleanOp::Union, 1).assert_volume(main); + pair.built(BooleanOp::Intersection, 1).assert_volume(wing); + Pair::placed([(main_roof(), &[]), (wing_roof(), &wing_place)]) + .built(BooleanOp::Subtraction, 1) + .assert_volume(main - wing); +} + +#[test] +fn box_on_the_floor_inside_an_l_roof_subtracts_to_nothing() { + let block = || { + hexahedron( + [0.8, 0.0, 0.8], + [[0.4, 0.0, 0.0], [0.0, 0.5, 0.0], [0.0, 0.0, 0.4]], + ) + }; + let pair = Pair::new(block(), l_roof()); + pair.assert_empty(BooleanOp::Subtraction, "subtract_planar_polyhedra"); + pair.built(BooleanOp::Union, 1).assert_volume(16.0 / 3.0); + pair.built(BooleanOp::Intersection, 1).assert_volume(0.08); + Pair::new(l_roof(), block()) + .built(BooleanOp::Subtraction, 1) + .assert_volume(16.0 / 3.0 - 0.08); +} + +fn turned_points(primitive: Primitive, (axis, degrees): (Point3, f64)) -> Primitive { + let Primitive::Polyhedron { points, faces } = primitive else { + panic!("not a polyhedron"); + }; + let length = axis.iter().map(|value| value * value).sum::().sqrt(); + let axis = axis.map(|value| value / length); + let (sin, cos) = degrees.to_radians().sin_cos(); + let points = points + .into_iter() + .map(|point: Point3| { + let along = (0..3).map(|i| axis[i] * point[i]).sum::(); + let across = [ + axis[1] * point[2] - axis[2] * point[1], + axis[2] * point[0] - axis[0] * point[2], + axis[0] * point[1] - axis[1] * point[0], + ]; + std::array::from_fn(|i| { + point[i] * cos + across[i] * sin + axis[i] * along * (1.0 - cos) + }) + }) + .collect(); + Primitive::Polyhedron { points, faces } +} + +#[test] +fn box_strictly_inside_a_gable_roof_with_turned_points_subtracts_to_nothing() { + for turn in [ + ([1.0, 2.0, 3.0], 37.0), + ( + [-0.5252771006644055, 0.9494761331925581, 0.9087649454590425], + 207.2570567402658, + ), + ] { + let block = || { + turned_points( + hexahedron( + [1.0, 0.1, 0.8], + [[1.0, 0.0, 0.0], [0.0, 0.2, 0.0], [0.0, 0.0, 0.4]], + ), + turn, + ) + }; + let roof = || turned_points(gable_roof(), turn); + let pair = Pair::new(block(), roof()); + pair.assert_empty(BooleanOp::Subtraction, "generic_non_intersecting_boolean"); + pair.built(BooleanOp::Union, 1).assert_volume(4.0); + pair.built(BooleanOp::Intersection, 1).assert_volume(0.08); + Pair::new(roof(), block()) + .built(BooleanOp::Subtraction, 1) + .assert_volume(3.92); + } +} + +fn scaled(primitive: Primitive, factor: f64) -> Primitive { + let Primitive::Polyhedron { points, faces } = primitive else { + panic!("not a polyhedron"); + }; + let points = points + .into_iter() + .map(|point: Point3| point.map(|value| value * factor)) + .collect(); + Primitive::Polyhedron { points, faces } +} + +#[test] +fn box_just_above_the_floor_inside_a_large_gable_roof_subtracts_to_nothing() { + let block = || { + hexahedron( + [150.0, 2.0e-4, 80.0], + [[100.0, 0.0, 0.0], [0.0, 30.0, 0.0], [0.0, 0.0, 40.0]], + ) + }; + let roof = || scaled(gable_roof(), 100.0); + let pair = Pair::new(block(), roof()); + pair.assert_empty(BooleanOp::Subtraction, "generic_non_intersecting_boolean"); + pair.built(BooleanOp::Union, 1).assert_volume(4.0e6); + pair.built(BooleanOp::Intersection, 1).assert_volume(1.2e5); + Pair::new(roof(), block()) + .built(BooleanOp::Subtraction, 1) + .assert_volume(3.88e6); +} diff --git a/main/opengeometry/tests/creating/polyhedron_booleans.rs b/main/opengeometry/tests/creating/polyhedron_booleans.rs new file mode 100644 index 00000000..0e01847e --- /dev/null +++ b/main/opengeometry/tests/creating/polyhedron_booleans.rs @@ -0,0 +1,199 @@ +use super::polyhedron::{gable_roof, hip_roof, polyhedron, HIP_FACES}; +use super::polyhedron_boolean_checks::Pair; +use opengeometry::math::Point3; +use opengeometry::operations::modifying::boolean::BooleanOp; +use opengeometry::world_graph::Primitive; + +const GROUND: Point3 = [2.0, 0.0, 1.0]; + +const DOWN: Point3 = [0.0, -1.0, 0.0]; + +const SUNK_VALLEYS: [[Point3; 2]; 4] = [ + [[1.625, 0.0, 0.0], [1.875, 1.0, 1.0]], + [[1.875, 1.0, 1.0], [1.625, 0.0, 2.0]], + [[2.375, 0.0, 0.0], [2.125, 1.0, 1.0]], + [[2.125, 1.0, 1.0], [2.375, 0.0, 2.0]], +]; + +const GROUND_VALLEYS: [[Point3; 2]; 4] = [ + [[1.5, 0.0, 0.0], [1.75, 1.0, 1.0]], + [[1.75, 1.0, 1.0], [1.5, 0.0, 2.0]], + [[2.5, 0.0, 0.0], [2.25, 1.0, 1.0]], + [[2.25, 1.0, 1.0], [2.5, 0.0, 2.0]], +]; + +fn cross_gable(base: f64, ridge: f64) -> Primitive { + let points = [ + [1.5, base, -1.0], + [2.5, base, -1.0], + [2.5, base, 3.0], + [1.5, base, 3.0], + [2.0, ridge, -1.0], + [2.0, ridge, 3.0], + ]; + let faces: [&[u32]; 5] = [ + &[0, 1, 2, 3], + &[1, 0, 4], + &[3, 2, 5], + &[0, 3, 5, 4], + &[2, 1, 4, 5], + ]; + polyhedron(&points, &faces) +} + +fn poking_hip() -> Primitive { + let points = [ + [1.0, -0.5, 0.5], + [3.0, -0.5, 0.5], + [3.0, -0.5, 1.5], + [1.0, -0.5, 1.5], + [1.5, 2.0, 1.0], + [2.5, 2.0, 1.0], + ]; + polyhedron(&points, &HIP_FACES) +} + +pub(super) fn triangular_prism(section: [[f64; 2]; 3], ends: [f64; 2]) -> Primitive { + let points = ends + .iter() + .flat_map(|x| section.map(|[y, z]| [*x, y, z])) + .collect::>(); + let faces: [&[u32]; 5] = [ + &[0, 2, 1], + &[3, 4, 5], + &[0, 1, 4, 3], + &[1, 2, 5, 4], + &[2, 0, 3, 5], + ]; + polyhedron(&points, &faces) +} + +pub(super) fn tetrahedron(points: [Point3; 4]) -> Primitive { + let faces: [&[u32]; 4] = [&[0, 2, 1], &[0, 1, 3], &[0, 3, 2], &[1, 2, 3]]; + polyhedron(&points, &faces) +} + +#[test] +fn cross_gable_sunk_below_the_eaves_unions_and_intersects_along_its_valleys() { + let pair = Pair::new(gable_roof(), cross_gable(-0.5, 1.5)); + let union = pair.planar(BooleanOp::Union, 1); + let intersection = pair.planar(BooleanOp::Intersection, 1); + let difference = pair.built(BooleanOp::Subtraction, 2); + union.assert_volume(89.0 / 12.0); + intersection.assert_volume(7.0 / 12.0); + difference.assert_volume(41.0 / 12.0); + pair.assert_identities(&union, &intersection, &difference); + for built in [&union, &intersection] { + pair.assert_both_contribute(built); + for valley in SUNK_VALLEYS { + pair.assert_edge_between_inputs(built, valley); + } + } +} + +#[test] +fn crossing_gable_subtraction_keeps_subtract_planar_polyhedra() { + let pair = Pair::new(gable_roof(), cross_gable(-0.5, 1.5)); + let difference = pair.built(BooleanOp::Subtraction, 2); + assert_eq!(difference.handlers(), ["subtract_planar_polyhedra"]); + difference.assert_volume(41.0 / 12.0); +} + +#[test] +fn cross_gable_on_the_same_ground_unions_into_one_merged_bottom_face() { + let pair = Pair::new(gable_roof(), cross_gable(0.0, 2.0)); + let union = pair.planar(BooleanOp::Union, 1); + let intersection = pair.planar(BooleanOp::Intersection, 1); + let difference = pair.built(BooleanOp::Subtraction, 2); + union.assert_volume(43.0 / 6.0); + intersection.assert_volume(5.0 / 6.0); + difference.assert_volume(19.0 / 6.0); + pair.assert_identities(&union, &intersection, &difference); + pair.assert_one_coincident_face(&union, GROUND, DOWN); + assert_eq!(intersection.faces_on_plane(GROUND, DOWN).len(), 1); + for built in [&union, &intersection] { + pair.assert_both_contribute(built); + for valley in GROUND_VALLEYS { + pair.assert_edge_between_inputs(built, valley); + } + } +} + +#[test] +fn hip_poking_through_the_gable_unions_and_intersects() { + let pair = Pair::new(gable_roof(), poking_hip()); + let union = pair.planar(BooleanOp::Union, 1); + let intersection = pair.planar(BooleanOp::Intersection, 1); + let difference = pair.built(BooleanOp::Subtraction, 1); + union.assert_volume(3109.0 / 600.0); + intersection.assert_volume(541.0 / 600.0); + difference.assert_volume(1859.0 / 600.0); + pair.assert_identities(&union, &intersection, &difference); + pair.assert_both_contribute(&union); + pair.assert_both_contribute(&intersection); +} + +#[test] +fn hip_and_gable_on_shared_eaves_union_to_the_gable_and_intersect_to_the_hip() { + let pair = Pair::new(gable_roof(), hip_roof()); + let union = pair.planar(BooleanOp::Union, 1); + let intersection = pair.planar(BooleanOp::Intersection, 1); + let difference = pair.built(BooleanOp::Subtraction, 2); + union.assert_volume(4.0); + intersection.assert_volume(10.0 / 3.0); + difference.assert_volume(2.0 / 3.0); + pair.assert_identities(&union, &intersection, &difference); + pair.assert_one_coincident_face(&union, GROUND, DOWN); + assert_eq!(intersection.faces_on_plane(GROUND, DOWN).len(), 1); + for built in [&union, &intersection] { + for corner in [ + [0.0, 0.0, 0.0], + [4.0, 0.0, 0.0], + [4.0, 0.0, 2.0], + [0.0, 0.0, 2.0], + ] { + built.assert_vertex(corner); + } + } +} + +#[test] +fn prism_with_an_edge_on_a_gable_slope_unions_and_intersects() { + let pair = Pair::new( + gable_roof(), + triangular_prism([[0.5, 0.5], [1.0, 0.5], [0.5, 1.0]], [1.0, 3.0]), + ); + let union = pair.planar(BooleanOp::Union, 1); + let intersection = pair.planar(BooleanOp::Intersection, 1); + let difference = pair.built(BooleanOp::Subtraction, 1); + union.assert_volume(33.0 / 8.0); + intersection.assert_volume(1.0 / 8.0); + difference.assert_volume(31.0 / 8.0); + pair.assert_identities(&union, &intersection, &difference); + for built in [&union, &intersection] { + pair.assert_both_contribute(built); + pair.assert_edge_between_inputs(built, [[1.0, 0.5, 0.5], [3.0, 0.5, 0.5]]); + } +} + +#[test] +fn tetrahedron_sharing_the_ridge_end_unions_and_intersects() { + let tetrahedron = tetrahedron([ + [4.0, 1.0, 1.0], + [3.0, 2.0, 0.5], + [2.0, 0.5, 1.0], + [3.0, 0.25, 2.5], + ]); + let pair = Pair::new(gable_roof(), tetrahedron); + let union = pair.planar(BooleanOp::Union, 1); + let intersection = pair.planar(BooleanOp::Intersection, 1); + let difference = pair.built(BooleanOp::Subtraction, 1); + union.assert_volume(6051.0 / 1360.0); + intersection.assert_volume(377.0 / 4080.0); + difference.assert_volume(15943.0 / 4080.0); + pair.assert_identities(&union, &intersection, &difference); + for built in [&union, &intersection] { + pair.assert_both_contribute(built); + built.assert_vertex([4.0, 1.0, 1.0]); + } +} diff --git a/main/opengeometry/tests/errors.rs b/main/opengeometry/tests/errors.rs index 6e359a54..a5ce3926 100644 --- a/main/opengeometry/tests/errors.rs +++ b/main/opengeometry/tests/errors.rs @@ -186,24 +186,36 @@ fn translate(world: &mut WorldGraph, og_id: &str, offset: [f64; 3]) { .unwrap(); } -#[test] -fn multi_tool_operate_failure_carries_handlers_tool_index_and_og_ids() { - let mut world = graph(); - unit_cuboid(&mut world, "target", 2.0); - unit_cuboid(&mut world, "tool-1", 2.0); - translate(&mut world, "tool-1", [1.0, 0.0, 0.0]); - unit_cuboid(&mut world, "tool-2", 1.0); - translate(&mut world, "tool-2", [1.5, 0.5, 0.0]); +fn tilted_cylinder(world: &mut WorldGraph, og_id: &str, offset: [f64; 3]) { + world + .create_primitive( + Primitive::Cylinder { + radius: 0.1, + height: 0.7, + }, + named(og_id), + ) + .unwrap(); world .transform( - "tool-2", + og_id, Transform::Rotate { - axis: [0.0, 1.0, 0.0], - degrees: 30.0, - pivot: Some([1.5, 0.5, 0.0]), + axis: [1.0, 0.0, 0.0], + degrees: 10.0, + pivot: Some([0.0, 0.35, 0.0]), }, ) .unwrap(); + translate(world, og_id, offset); +} + +#[test] +fn multi_tool_operate_failure_carries_handlers_tool_index_and_og_ids() { + let mut world = graph(); + unit_cuboid(&mut world, "target", 2.0); + unit_cuboid(&mut world, "tool-1", 2.0); + translate(&mut world, "tool-1", [1.0, 0.0, 0.0]); + tilted_cylinder(&mut world, "tool-2", [2.0, 1.6, 0.0]); let mut step_one = graph(); unit_cuboid(&mut step_one, "target", 2.0); unit_cuboid(&mut step_one, "tool-1", 2.0); @@ -238,9 +250,7 @@ fn multi_tool_operate_failure_carries_handlers_tool_index_and_og_ids() { "separate_boolean", "boolean_boxes", "separate_boolean", - "boolean_boxes", - "boolean_planar_extrusions", - "boolean_rectilinear", + "cylinder_box_boolean", "subtract_vertical_arc_extrusion", "generic_two_sided_cutter_band_subtraction", "generic_multiple_closed_loops_boolean", diff --git a/main/opengeometry/tests/fixtures/cases/boolean-matrix/rotated-box-intersection.json b/main/opengeometry/tests/fixtures/cases/boolean-matrix/rotated-box-intersection.json index 6947f538..5cbce7ca 100644 --- a/main/opengeometry/tests/fixtures/cases/boolean-matrix/rotated-box-intersection.json +++ b/main/opengeometry/tests/fixtures/cases/boolean-matrix/rotated-box-intersection.json @@ -1 +1 @@ -{"a":{"accuracy":{"exchange":1e-6,"geometric":1e-8,"intersection":1e-9,"tessellation":0.01},"geometry":{"curves":[{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[3.0,0.0,0.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.0,0.0,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,0.0,-3.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[0.0,0.0,-3.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.0,0.0,-3.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,3.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[0.0,3.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[3.0,3.0,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,3.0,-3.0]}],"intersections":[],"pcurves":[{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]}],"surfaces":[{"frame":{"origin":[0.0,0.0,0.0],"x":[0.0,0.0,-1.0],"y":[1.0,-0.0,0.0],"z":[-0.0,-1.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,3.0,0.0],"x":[1.0,0.0,0.0],"y":[0.0,0.0,-1.0],"z":[0.0,1.0,0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,0.0],"x":[0.0,1.0,0.0],"y":[0.0,0.0,-1.0],"z":[-1.0,-0.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[3.0,0.0,0.0],"x":[0.0,0.0,-1.0],"y":[-0.0,1.0,0.0],"z":[1.0,0.0,0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,0.0],"x":[1.0,0.0,0.0],"y":[-0.0,1.0,0.0],"z":[-0.0,-0.0,1.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,-3.0],"x":[0.0,1.0,0.0],"y":[1.0,-0.0,0.0],"z":[0.0,0.0,-1.0]},"kind":"Plane"}]},"id":"matrix-box-host","quality":{"kind":"Analytic"},"revision":0,"schema_version":2,"solids":[{"cavity_shells":[],"outer_shell":0}],"topology":{"edges":[{"chart_seam":false,"geometry":{"curve":0,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":3,"id":0,"tolerance":1e-8,"twin_halfedge":16},{"chart_seam":false,"geometry":{"curve":1,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":0,"id":1,"tolerance":1e-8,"twin_halfedge":11},{"chart_seam":false,"geometry":{"curve":2,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":8,"id":2,"tolerance":1e-8,"twin_halfedge":19},{"chart_seam":false,"geometry":{"curve":3,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":2,"id":3,"tolerance":1e-8,"twin_halfedge":12},{"chart_seam":false,"geometry":{"curve":4,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":15,"id":4,"tolerance":1e-8,"twin_halfedge":17},{"chart_seam":false,"geometry":{"curve":5,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":1,"id":5,"tolerance":1e-8,"twin_halfedge":23},{"chart_seam":false,"geometry":{"curve":6,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":10,"id":6,"tolerance":1e-8,"twin_halfedge":20},{"chart_seam":false,"geometry":{"curve":7,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":13,"id":7,"tolerance":1e-8,"twin_halfedge":22},{"chart_seam":false,"geometry":{"curve":8,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":4,"id":8,"tolerance":1e-8,"twin_halfedge":18},{"chart_seam":false,"geometry":{"curve":9,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":7,"id":9,"tolerance":1e-8,"twin_halfedge":9},{"chart_seam":false,"geometry":{"curve":10,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":5,"id":10,"tolerance":1e-8,"twin_halfedge":14},{"chart_seam":false,"geometry":{"curve":11,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":6,"id":11,"tolerance":1e-8,"twin_halfedge":21}],"faces":[{"id":0,"key":"bottom","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":0,"key":"bottom"}]},"sense":"Forward","shell_ref":0,"surface":0,"trim":{"chart":0,"holes":[],"outer":0,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":1,"key":"top","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":1,"key":"top"}]},"sense":"Forward","shell_ref":0,"surface":1,"trim":{"chart":0,"holes":[],"outer":1,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":2,"key":"left","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":2,"key":"left"}]},"sense":"Forward","shell_ref":0,"surface":2,"trim":{"chart":0,"holes":[],"outer":2,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":3,"key":"right","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":3,"key":"right"}]},"sense":"Forward","shell_ref":0,"surface":3,"trim":{"chart":0,"holes":[],"outer":3,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":4,"key":"front","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":4,"trim":{"chart":0,"holes":[],"outer":4,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":5,"key":"back","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":5,"key":"back"}]},"sense":"Forward","shell_ref":0,"surface":5,"trim":{"chart":0,"holes":[],"outer":5,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}}],"halfedges":[{"edge":1,"face":0,"from":0,"geometry_use":{"pcurve":0,"periodic_lift":[0,0],"sense":"Forward"},"id":0,"loop_ref":0,"next":1,"prev":3,"to":2,"twin":11,"wire_ref":null},{"edge":5,"face":0,"from":2,"geometry_use":{"pcurve":1,"periodic_lift":[0,0],"sense":"Forward"},"id":1,"loop_ref":0,"next":2,"prev":0,"to":3,"twin":23,"wire_ref":null},{"edge":3,"face":0,"from":3,"geometry_use":{"pcurve":2,"periodic_lift":[0,0],"sense":"Reverse"},"id":2,"loop_ref":0,"next":3,"prev":1,"to":1,"twin":12,"wire_ref":null},{"edge":0,"face":0,"from":1,"geometry_use":{"pcurve":3,"periodic_lift":[0,0],"sense":"Reverse"},"id":3,"loop_ref":0,"next":0,"prev":2,"to":0,"twin":16,"wire_ref":null},{"edge":8,"face":1,"from":4,"geometry_use":{"pcurve":4,"periodic_lift":[0,0],"sense":"Forward"},"id":4,"loop_ref":1,"next":5,"prev":7,"to":5,"twin":18,"wire_ref":null},{"edge":10,"face":1,"from":5,"geometry_use":{"pcurve":5,"periodic_lift":[0,0],"sense":"Forward"},"id":5,"loop_ref":1,"next":6,"prev":4,"to":7,"twin":14,"wire_ref":null},{"edge":11,"face":1,"from":7,"geometry_use":{"pcurve":6,"periodic_lift":[0,0],"sense":"Reverse"},"id":6,"loop_ref":1,"next":7,"prev":5,"to":6,"twin":21,"wire_ref":null},{"edge":9,"face":1,"from":6,"geometry_use":{"pcurve":7,"periodic_lift":[0,0],"sense":"Reverse"},"id":7,"loop_ref":1,"next":4,"prev":6,"to":4,"twin":9,"wire_ref":null},{"edge":2,"face":2,"from":0,"geometry_use":{"pcurve":8,"periodic_lift":[0,0],"sense":"Forward"},"id":8,"loop_ref":2,"next":9,"prev":11,"to":4,"twin":19,"wire_ref":null},{"edge":9,"face":2,"from":4,"geometry_use":{"pcurve":9,"periodic_lift":[0,0],"sense":"Forward"},"id":9,"loop_ref":2,"next":10,"prev":8,"to":6,"twin":7,"wire_ref":null},{"edge":6,"face":2,"from":6,"geometry_use":{"pcurve":10,"periodic_lift":[0,0],"sense":"Reverse"},"id":10,"loop_ref":2,"next":11,"prev":9,"to":2,"twin":20,"wire_ref":null},{"edge":1,"face":2,"from":2,"geometry_use":{"pcurve":11,"periodic_lift":[0,0],"sense":"Reverse"},"id":11,"loop_ref":2,"next":8,"prev":10,"to":0,"twin":0,"wire_ref":null},{"edge":3,"face":3,"from":1,"geometry_use":{"pcurve":12,"periodic_lift":[0,0],"sense":"Forward"},"id":12,"loop_ref":3,"next":13,"prev":15,"to":3,"twin":2,"wire_ref":null},{"edge":7,"face":3,"from":3,"geometry_use":{"pcurve":13,"periodic_lift":[0,0],"sense":"Forward"},"id":13,"loop_ref":3,"next":14,"prev":12,"to":7,"twin":22,"wire_ref":null},{"edge":10,"face":3,"from":7,"geometry_use":{"pcurve":14,"periodic_lift":[0,0],"sense":"Reverse"},"id":14,"loop_ref":3,"next":15,"prev":13,"to":5,"twin":5,"wire_ref":null},{"edge":4,"face":3,"from":5,"geometry_use":{"pcurve":15,"periodic_lift":[0,0],"sense":"Reverse"},"id":15,"loop_ref":3,"next":12,"prev":14,"to":1,"twin":17,"wire_ref":null},{"edge":0,"face":4,"from":0,"geometry_use":{"pcurve":16,"periodic_lift":[0,0],"sense":"Forward"},"id":16,"loop_ref":4,"next":17,"prev":19,"to":1,"twin":3,"wire_ref":null},{"edge":4,"face":4,"from":1,"geometry_use":{"pcurve":17,"periodic_lift":[0,0],"sense":"Forward"},"id":17,"loop_ref":4,"next":18,"prev":16,"to":5,"twin":15,"wire_ref":null},{"edge":8,"face":4,"from":5,"geometry_use":{"pcurve":18,"periodic_lift":[0,0],"sense":"Reverse"},"id":18,"loop_ref":4,"next":19,"prev":17,"to":4,"twin":4,"wire_ref":null},{"edge":2,"face":4,"from":4,"geometry_use":{"pcurve":19,"periodic_lift":[0,0],"sense":"Reverse"},"id":19,"loop_ref":4,"next":16,"prev":18,"to":0,"twin":8,"wire_ref":null},{"edge":6,"face":5,"from":2,"geometry_use":{"pcurve":20,"periodic_lift":[0,0],"sense":"Forward"},"id":20,"loop_ref":5,"next":21,"prev":23,"to":6,"twin":10,"wire_ref":null},{"edge":11,"face":5,"from":6,"geometry_use":{"pcurve":21,"periodic_lift":[0,0],"sense":"Forward"},"id":21,"loop_ref":5,"next":22,"prev":20,"to":7,"twin":6,"wire_ref":null},{"edge":7,"face":5,"from":7,"geometry_use":{"pcurve":22,"periodic_lift":[0,0],"sense":"Reverse"},"id":22,"loop_ref":5,"next":23,"prev":21,"to":3,"twin":13,"wire_ref":null},{"edge":5,"face":5,"from":3,"geometry_use":{"pcurve":23,"periodic_lift":[0,0],"sense":"Reverse"},"id":23,"loop_ref":5,"next":20,"prev":22,"to":2,"twin":1,"wire_ref":null}],"loops":[{"face_ref":0,"id":0,"is_hole":false,"start_halfedge":0},{"face_ref":1,"id":1,"is_hole":false,"start_halfedge":4},{"face_ref":2,"id":2,"is_hole":false,"start_halfedge":8},{"face_ref":3,"id":3,"is_hole":false,"start_halfedge":12},{"face_ref":4,"id":4,"is_hole":false,"start_halfedge":16},{"face_ref":5,"id":5,"is_hole":false,"start_halfedge":20}],"shells":[{"faces":[0,1,2,3,4,5],"id":0,"is_closed":true}],"vertices":[{"id":0,"outgoing_halfedge":0,"position":[0.0,0.0,0.0],"tolerance":1e-8},{"id":1,"outgoing_halfedge":3,"position":[3.0,0.0,0.0],"tolerance":1e-8},{"id":2,"outgoing_halfedge":1,"position":[0.0,0.0,-3.0],"tolerance":1e-8},{"id":3,"outgoing_halfedge":2,"position":[3.0,0.0,-3.0],"tolerance":1e-8},{"id":4,"outgoing_halfedge":4,"position":[0.0,3.0,0.0],"tolerance":1e-8},{"id":5,"outgoing_halfedge":5,"position":[3.0,3.0,0.0],"tolerance":1e-8},{"id":6,"outgoing_halfedge":7,"position":[0.0,3.0,-3.0],"tolerance":1e-8},{"id":7,"outgoing_halfedge":6,"position":[3.0,3.0,-3.0],"tolerance":1e-8}],"wires":[]}},"b":{"accuracy":{"exchange":1e-6,"geometric":1e-8,"intersection":1e-9,"tessellation":0.01},"geometry":{"curves":[{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[2.7320508075688776,0.5,0.5000000000000002]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[2.7320508075688776,0.5,0.5000000000000002]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[2.0,0.5,-2.2320508075688776]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[2.0,0.5,-2.2320508075688776]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.7320508075688776,0.5,-1.2320508075688772]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[1.0,2.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[1.0,2.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[2.7320508075688776,2.5,0.5000000000000002]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[2.0,2.5,-2.2320508075688776]}],"intersections":[],"pcurves":[{"direction":[1.0,1.5165928555043604e-16],"kind":"Line2","origin":[0.0,0.0]},{"direction":[-1.5165928555043604e-16,1.0],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,1.5165928555043604e-16],"kind":"Line2","origin":[-1.9229626863835643e-16,2.0000000000000004]},{"direction":[-1.5165928555043604e-16,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,-1.487416814333745e-17],"kind":"Line2","origin":[0.0,0.0]},{"direction":[4.0636983087920364e-17,1.0000000000000002],"kind":"Line2","origin":[2.0000000000000004,8.127396617584078e-17]},{"direction":[1.0,-1.487416814333745e-17],"kind":"Line2","origin":[-2.220446049250313e-16,2.0000000000000004]},{"direction":[4.0636983087920364e-17,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.0000000000000004]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0000000000000002,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0000000000000002,0.0],"kind":"Line2","origin":[0.0,2.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[2.0000000000000004,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.000000000000001]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]}],"surfaces":[{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.4999999999999999,0.0,-0.8660254037844386],"y":[0.8660254037844387,-0.0,0.49999999999999994],"z":[-0.0,-1.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[1.0,2.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[0.5000000000000001,0.0,-0.8660254037844387],"z":[0.0,1.0,0.0]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.0,1.0,0.0],"y":[0.5000000000000001,0.0,-0.8660254037844387],"z":[-0.8660254037844387,-0.0,-0.5000000000000001]},"kind":"Plane"},{"frame":{"origin":[2.7320508075688776,0.5,0.5000000000000002],"x":[0.5000000000000001,0.0,-0.8660254037844386],"y":[-0.0,1.0,0.0],"z":[0.8660254037844387,0.0,0.5000000000000001]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[-0.0,1.0,0.0],"z":[-0.5000000000000001,-0.0,0.8660254037844387]},"kind":"Plane"},{"frame":{"origin":[2.0,0.5,-2.2320508075688776],"x":[0.0,1.0,0.0],"y":[0.8660254037844387,-0.0,0.5000000000000001],"z":[0.5000000000000001,0.0,-0.8660254037844387]},"kind":"Plane"}]},"id":"matrix-rotated-cutter","quality":{"kind":"Analytic"},"revision":0,"schema_version":2,"solids":[{"cavity_shells":[],"outer_shell":0}],"topology":{"edges":[{"chart_seam":false,"geometry":{"curve":0,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":3,"id":0,"tolerance":1e-8,"twin_halfedge":16},{"chart_seam":false,"geometry":{"curve":1,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":0,"id":1,"tolerance":1e-8,"twin_halfedge":11},{"chart_seam":false,"geometry":{"curve":2,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":8,"id":2,"tolerance":1e-8,"twin_halfedge":19},{"chart_seam":false,"geometry":{"curve":3,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":2,"id":3,"tolerance":1e-8,"twin_halfedge":12},{"chart_seam":false,"geometry":{"curve":4,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":15,"id":4,"tolerance":1e-8,"twin_halfedge":17},{"chart_seam":false,"geometry":{"curve":5,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":1,"id":5,"tolerance":1e-8,"twin_halfedge":23},{"chart_seam":false,"geometry":{"curve":6,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":10,"id":6,"tolerance":1e-8,"twin_halfedge":20},{"chart_seam":false,"geometry":{"curve":7,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":13,"id":7,"tolerance":1e-8,"twin_halfedge":22},{"chart_seam":false,"geometry":{"curve":8,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":4,"id":8,"tolerance":1e-8,"twin_halfedge":18},{"chart_seam":false,"geometry":{"curve":9,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":7,"id":9,"tolerance":1e-8,"twin_halfedge":9},{"chart_seam":false,"geometry":{"curve":10,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":5,"id":10,"tolerance":1e-8,"twin_halfedge":14},{"chart_seam":false,"geometry":{"curve":11,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":6,"id":11,"tolerance":1e-8,"twin_halfedge":21}],"faces":[{"id":0,"key":"bottom","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":0,"key":"bottom"}]},"sense":"Forward","shell_ref":0,"surface":0,"trim":{"chart":0,"holes":[],"outer":0,"uv_bounds":[{"hi":2.00000001,"lo":-1.0000000192296269e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":1,"key":"top","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":1,"key":"top"}]},"sense":"Forward","shell_ref":0,"surface":1,"trim":{"chart":0,"holes":[],"outer":1,"uv_bounds":[{"hi":2.0000000100000004,"lo":-1.0000000222044605e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":2,"key":"left","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":2,"key":"left"}]},"sense":"Forward","shell_ref":0,"surface":2,"trim":{"chart":0,"holes":[],"outer":2,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":3,"key":"right","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":3,"key":"right"}]},"sense":"Forward","shell_ref":0,"surface":3,"trim":{"chart":0,"holes":[],"outer":3,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.00000001,"lo":-1e-8}]}},{"id":4,"key":"front","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":4,"trim":{"chart":0,"holes":[],"outer":4,"uv_bounds":[{"hi":2.0000000100000004,"lo":-1e-8},{"hi":2.00000001,"lo":-1e-8}]}},{"id":5,"key":"back","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":5,"key":"back"}]},"sense":"Forward","shell_ref":0,"surface":5,"trim":{"chart":0,"holes":[],"outer":5,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.000000010000001,"lo":-1e-8}]}}],"halfedges":[{"edge":1,"face":0,"from":0,"geometry_use":{"pcurve":0,"periodic_lift":[0,0],"sense":"Forward"},"id":0,"loop_ref":0,"next":1,"prev":3,"to":2,"twin":11,"wire_ref":null},{"edge":5,"face":0,"from":2,"geometry_use":{"pcurve":1,"periodic_lift":[0,0],"sense":"Forward"},"id":1,"loop_ref":0,"next":2,"prev":0,"to":3,"twin":23,"wire_ref":null},{"edge":3,"face":0,"from":3,"geometry_use":{"pcurve":2,"periodic_lift":[0,0],"sense":"Reverse"},"id":2,"loop_ref":0,"next":3,"prev":1,"to":1,"twin":12,"wire_ref":null},{"edge":0,"face":0,"from":1,"geometry_use":{"pcurve":3,"periodic_lift":[0,0],"sense":"Reverse"},"id":3,"loop_ref":0,"next":0,"prev":2,"to":0,"twin":16,"wire_ref":null},{"edge":8,"face":1,"from":4,"geometry_use":{"pcurve":4,"periodic_lift":[0,0],"sense":"Forward"},"id":4,"loop_ref":1,"next":5,"prev":7,"to":5,"twin":18,"wire_ref":null},{"edge":10,"face":1,"from":5,"geometry_use":{"pcurve":5,"periodic_lift":[0,0],"sense":"Forward"},"id":5,"loop_ref":1,"next":6,"prev":4,"to":7,"twin":14,"wire_ref":null},{"edge":11,"face":1,"from":7,"geometry_use":{"pcurve":6,"periodic_lift":[0,0],"sense":"Reverse"},"id":6,"loop_ref":1,"next":7,"prev":5,"to":6,"twin":21,"wire_ref":null},{"edge":9,"face":1,"from":6,"geometry_use":{"pcurve":7,"periodic_lift":[0,0],"sense":"Reverse"},"id":7,"loop_ref":1,"next":4,"prev":6,"to":4,"twin":9,"wire_ref":null},{"edge":2,"face":2,"from":0,"geometry_use":{"pcurve":8,"periodic_lift":[0,0],"sense":"Forward"},"id":8,"loop_ref":2,"next":9,"prev":11,"to":4,"twin":19,"wire_ref":null},{"edge":9,"face":2,"from":4,"geometry_use":{"pcurve":9,"periodic_lift":[0,0],"sense":"Forward"},"id":9,"loop_ref":2,"next":10,"prev":8,"to":6,"twin":7,"wire_ref":null},{"edge":6,"face":2,"from":6,"geometry_use":{"pcurve":10,"periodic_lift":[0,0],"sense":"Reverse"},"id":10,"loop_ref":2,"next":11,"prev":9,"to":2,"twin":20,"wire_ref":null},{"edge":1,"face":2,"from":2,"geometry_use":{"pcurve":11,"periodic_lift":[0,0],"sense":"Reverse"},"id":11,"loop_ref":2,"next":8,"prev":10,"to":0,"twin":0,"wire_ref":null},{"edge":3,"face":3,"from":1,"geometry_use":{"pcurve":12,"periodic_lift":[0,0],"sense":"Forward"},"id":12,"loop_ref":3,"next":13,"prev":15,"to":3,"twin":2,"wire_ref":null},{"edge":7,"face":3,"from":3,"geometry_use":{"pcurve":13,"periodic_lift":[0,0],"sense":"Forward"},"id":13,"loop_ref":3,"next":14,"prev":12,"to":7,"twin":22,"wire_ref":null},{"edge":10,"face":3,"from":7,"geometry_use":{"pcurve":14,"periodic_lift":[0,0],"sense":"Reverse"},"id":14,"loop_ref":3,"next":15,"prev":13,"to":5,"twin":5,"wire_ref":null},{"edge":4,"face":3,"from":5,"geometry_use":{"pcurve":15,"periodic_lift":[0,0],"sense":"Reverse"},"id":15,"loop_ref":3,"next":12,"prev":14,"to":1,"twin":17,"wire_ref":null},{"edge":0,"face":4,"from":0,"geometry_use":{"pcurve":16,"periodic_lift":[0,0],"sense":"Forward"},"id":16,"loop_ref":4,"next":17,"prev":19,"to":1,"twin":3,"wire_ref":null},{"edge":4,"face":4,"from":1,"geometry_use":{"pcurve":17,"periodic_lift":[0,0],"sense":"Forward"},"id":17,"loop_ref":4,"next":18,"prev":16,"to":5,"twin":15,"wire_ref":null},{"edge":8,"face":4,"from":5,"geometry_use":{"pcurve":18,"periodic_lift":[0,0],"sense":"Reverse"},"id":18,"loop_ref":4,"next":19,"prev":17,"to":4,"twin":4,"wire_ref":null},{"edge":2,"face":4,"from":4,"geometry_use":{"pcurve":19,"periodic_lift":[0,0],"sense":"Reverse"},"id":19,"loop_ref":4,"next":16,"prev":18,"to":0,"twin":8,"wire_ref":null},{"edge":6,"face":5,"from":2,"geometry_use":{"pcurve":20,"periodic_lift":[0,0],"sense":"Forward"},"id":20,"loop_ref":5,"next":21,"prev":23,"to":6,"twin":10,"wire_ref":null},{"edge":11,"face":5,"from":6,"geometry_use":{"pcurve":21,"periodic_lift":[0,0],"sense":"Forward"},"id":21,"loop_ref":5,"next":22,"prev":20,"to":7,"twin":6,"wire_ref":null},{"edge":7,"face":5,"from":7,"geometry_use":{"pcurve":22,"periodic_lift":[0,0],"sense":"Reverse"},"id":22,"loop_ref":5,"next":23,"prev":21,"to":3,"twin":13,"wire_ref":null},{"edge":5,"face":5,"from":3,"geometry_use":{"pcurve":23,"periodic_lift":[0,0],"sense":"Reverse"},"id":23,"loop_ref":5,"next":20,"prev":22,"to":2,"twin":1,"wire_ref":null}],"loops":[{"face_ref":0,"id":0,"is_hole":false,"start_halfedge":0},{"face_ref":1,"id":1,"is_hole":false,"start_halfedge":4},{"face_ref":2,"id":2,"is_hole":false,"start_halfedge":8},{"face_ref":3,"id":3,"is_hole":false,"start_halfedge":12},{"face_ref":4,"id":4,"is_hole":false,"start_halfedge":16},{"face_ref":5,"id":5,"is_hole":false,"start_halfedge":20}],"shells":[{"faces":[0,1,2,3,4,5],"id":0,"is_closed":true}],"vertices":[{"id":0,"outgoing_halfedge":0,"position":[1.0,0.5,-0.5],"tolerance":1e-8},{"id":1,"outgoing_halfedge":3,"position":[2.7320508075688776,0.5,0.5000000000000002],"tolerance":1e-8},{"id":2,"outgoing_halfedge":1,"position":[2.0,0.5,-2.2320508075688776],"tolerance":1e-8},{"id":3,"outgoing_halfedge":2,"position":[3.7320508075688776,0.5,-1.2320508075688772],"tolerance":1e-8},{"id":4,"outgoing_halfedge":4,"position":[1.0,2.5,-0.5],"tolerance":1e-8},{"id":5,"outgoing_halfedge":5,"position":[2.7320508075688776,2.5,0.5000000000000002],"tolerance":1e-8},{"id":6,"outgoing_halfedge":7,"position":[2.0,2.5,-2.2320508075688776],"tolerance":1e-8},{"id":7,"outgoing_halfedge":6,"position":[3.7320508075688776,2.5,-1.2320508075688772],"tolerance":1e-8}],"wires":[]}},"handlers":[],"operation":"intersection","result":{"error":{"CoverageGap":{"families":["generic analytic solid","intersecting analytic solid"]}}}} \ No newline at end of file +{"a":{"accuracy":{"exchange":1e-6,"geometric":1e-8,"intersection":1e-9,"tessellation":0.01},"geometry":{"curves":[{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[3.0,0.0,0.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.0,0.0,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,0.0,-3.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[0.0,0.0,-3.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.0,0.0,-3.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,3.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[0.0,3.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[3.0,3.0,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,3.0,-3.0]}],"intersections":[],"pcurves":[{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]}],"surfaces":[{"frame":{"origin":[0.0,0.0,0.0],"x":[0.0,0.0,-1.0],"y":[1.0,-0.0,0.0],"z":[-0.0,-1.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,3.0,0.0],"x":[1.0,0.0,0.0],"y":[0.0,0.0,-1.0],"z":[0.0,1.0,0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,0.0],"x":[0.0,1.0,0.0],"y":[0.0,0.0,-1.0],"z":[-1.0,-0.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[3.0,0.0,0.0],"x":[0.0,0.0,-1.0],"y":[-0.0,1.0,0.0],"z":[1.0,0.0,0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,0.0],"x":[1.0,0.0,0.0],"y":[-0.0,1.0,0.0],"z":[-0.0,-0.0,1.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,-3.0],"x":[0.0,1.0,0.0],"y":[1.0,-0.0,0.0],"z":[0.0,0.0,-1.0]},"kind":"Plane"}]},"id":"matrix-box-host","quality":{"kind":"Analytic"},"revision":0,"schema_version":2,"solids":[{"cavity_shells":[],"outer_shell":0}],"topology":{"edges":[{"chart_seam":false,"geometry":{"curve":0,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":3,"id":0,"tolerance":1e-8,"twin_halfedge":16},{"chart_seam":false,"geometry":{"curve":1,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":0,"id":1,"tolerance":1e-8,"twin_halfedge":11},{"chart_seam":false,"geometry":{"curve":2,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":8,"id":2,"tolerance":1e-8,"twin_halfedge":19},{"chart_seam":false,"geometry":{"curve":3,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":2,"id":3,"tolerance":1e-8,"twin_halfedge":12},{"chart_seam":false,"geometry":{"curve":4,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":15,"id":4,"tolerance":1e-8,"twin_halfedge":17},{"chart_seam":false,"geometry":{"curve":5,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":1,"id":5,"tolerance":1e-8,"twin_halfedge":23},{"chart_seam":false,"geometry":{"curve":6,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":10,"id":6,"tolerance":1e-8,"twin_halfedge":20},{"chart_seam":false,"geometry":{"curve":7,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":13,"id":7,"tolerance":1e-8,"twin_halfedge":22},{"chart_seam":false,"geometry":{"curve":8,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":4,"id":8,"tolerance":1e-8,"twin_halfedge":18},{"chart_seam":false,"geometry":{"curve":9,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":7,"id":9,"tolerance":1e-8,"twin_halfedge":9},{"chart_seam":false,"geometry":{"curve":10,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":5,"id":10,"tolerance":1e-8,"twin_halfedge":14},{"chart_seam":false,"geometry":{"curve":11,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":6,"id":11,"tolerance":1e-8,"twin_halfedge":21}],"faces":[{"id":0,"key":"bottom","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":0,"key":"bottom"}]},"sense":"Forward","shell_ref":0,"surface":0,"trim":{"chart":0,"holes":[],"outer":0,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":1,"key":"top","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":1,"key":"top"}]},"sense":"Forward","shell_ref":0,"surface":1,"trim":{"chart":0,"holes":[],"outer":1,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":2,"key":"left","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":2,"key":"left"}]},"sense":"Forward","shell_ref":0,"surface":2,"trim":{"chart":0,"holes":[],"outer":2,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":3,"key":"right","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":3,"key":"right"}]},"sense":"Forward","shell_ref":0,"surface":3,"trim":{"chart":0,"holes":[],"outer":3,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":4,"key":"front","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":4,"trim":{"chart":0,"holes":[],"outer":4,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":5,"key":"back","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":5,"key":"back"}]},"sense":"Forward","shell_ref":0,"surface":5,"trim":{"chart":0,"holes":[],"outer":5,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}}],"halfedges":[{"edge":1,"face":0,"from":0,"geometry_use":{"pcurve":0,"periodic_lift":[0,0],"sense":"Forward"},"id":0,"loop_ref":0,"next":1,"prev":3,"to":2,"twin":11,"wire_ref":null},{"edge":5,"face":0,"from":2,"geometry_use":{"pcurve":1,"periodic_lift":[0,0],"sense":"Forward"},"id":1,"loop_ref":0,"next":2,"prev":0,"to":3,"twin":23,"wire_ref":null},{"edge":3,"face":0,"from":3,"geometry_use":{"pcurve":2,"periodic_lift":[0,0],"sense":"Reverse"},"id":2,"loop_ref":0,"next":3,"prev":1,"to":1,"twin":12,"wire_ref":null},{"edge":0,"face":0,"from":1,"geometry_use":{"pcurve":3,"periodic_lift":[0,0],"sense":"Reverse"},"id":3,"loop_ref":0,"next":0,"prev":2,"to":0,"twin":16,"wire_ref":null},{"edge":8,"face":1,"from":4,"geometry_use":{"pcurve":4,"periodic_lift":[0,0],"sense":"Forward"},"id":4,"loop_ref":1,"next":5,"prev":7,"to":5,"twin":18,"wire_ref":null},{"edge":10,"face":1,"from":5,"geometry_use":{"pcurve":5,"periodic_lift":[0,0],"sense":"Forward"},"id":5,"loop_ref":1,"next":6,"prev":4,"to":7,"twin":14,"wire_ref":null},{"edge":11,"face":1,"from":7,"geometry_use":{"pcurve":6,"periodic_lift":[0,0],"sense":"Reverse"},"id":6,"loop_ref":1,"next":7,"prev":5,"to":6,"twin":21,"wire_ref":null},{"edge":9,"face":1,"from":6,"geometry_use":{"pcurve":7,"periodic_lift":[0,0],"sense":"Reverse"},"id":7,"loop_ref":1,"next":4,"prev":6,"to":4,"twin":9,"wire_ref":null},{"edge":2,"face":2,"from":0,"geometry_use":{"pcurve":8,"periodic_lift":[0,0],"sense":"Forward"},"id":8,"loop_ref":2,"next":9,"prev":11,"to":4,"twin":19,"wire_ref":null},{"edge":9,"face":2,"from":4,"geometry_use":{"pcurve":9,"periodic_lift":[0,0],"sense":"Forward"},"id":9,"loop_ref":2,"next":10,"prev":8,"to":6,"twin":7,"wire_ref":null},{"edge":6,"face":2,"from":6,"geometry_use":{"pcurve":10,"periodic_lift":[0,0],"sense":"Reverse"},"id":10,"loop_ref":2,"next":11,"prev":9,"to":2,"twin":20,"wire_ref":null},{"edge":1,"face":2,"from":2,"geometry_use":{"pcurve":11,"periodic_lift":[0,0],"sense":"Reverse"},"id":11,"loop_ref":2,"next":8,"prev":10,"to":0,"twin":0,"wire_ref":null},{"edge":3,"face":3,"from":1,"geometry_use":{"pcurve":12,"periodic_lift":[0,0],"sense":"Forward"},"id":12,"loop_ref":3,"next":13,"prev":15,"to":3,"twin":2,"wire_ref":null},{"edge":7,"face":3,"from":3,"geometry_use":{"pcurve":13,"periodic_lift":[0,0],"sense":"Forward"},"id":13,"loop_ref":3,"next":14,"prev":12,"to":7,"twin":22,"wire_ref":null},{"edge":10,"face":3,"from":7,"geometry_use":{"pcurve":14,"periodic_lift":[0,0],"sense":"Reverse"},"id":14,"loop_ref":3,"next":15,"prev":13,"to":5,"twin":5,"wire_ref":null},{"edge":4,"face":3,"from":5,"geometry_use":{"pcurve":15,"periodic_lift":[0,0],"sense":"Reverse"},"id":15,"loop_ref":3,"next":12,"prev":14,"to":1,"twin":17,"wire_ref":null},{"edge":0,"face":4,"from":0,"geometry_use":{"pcurve":16,"periodic_lift":[0,0],"sense":"Forward"},"id":16,"loop_ref":4,"next":17,"prev":19,"to":1,"twin":3,"wire_ref":null},{"edge":4,"face":4,"from":1,"geometry_use":{"pcurve":17,"periodic_lift":[0,0],"sense":"Forward"},"id":17,"loop_ref":4,"next":18,"prev":16,"to":5,"twin":15,"wire_ref":null},{"edge":8,"face":4,"from":5,"geometry_use":{"pcurve":18,"periodic_lift":[0,0],"sense":"Reverse"},"id":18,"loop_ref":4,"next":19,"prev":17,"to":4,"twin":4,"wire_ref":null},{"edge":2,"face":4,"from":4,"geometry_use":{"pcurve":19,"periodic_lift":[0,0],"sense":"Reverse"},"id":19,"loop_ref":4,"next":16,"prev":18,"to":0,"twin":8,"wire_ref":null},{"edge":6,"face":5,"from":2,"geometry_use":{"pcurve":20,"periodic_lift":[0,0],"sense":"Forward"},"id":20,"loop_ref":5,"next":21,"prev":23,"to":6,"twin":10,"wire_ref":null},{"edge":11,"face":5,"from":6,"geometry_use":{"pcurve":21,"periodic_lift":[0,0],"sense":"Forward"},"id":21,"loop_ref":5,"next":22,"prev":20,"to":7,"twin":6,"wire_ref":null},{"edge":7,"face":5,"from":7,"geometry_use":{"pcurve":22,"periodic_lift":[0,0],"sense":"Reverse"},"id":22,"loop_ref":5,"next":23,"prev":21,"to":3,"twin":13,"wire_ref":null},{"edge":5,"face":5,"from":3,"geometry_use":{"pcurve":23,"periodic_lift":[0,0],"sense":"Reverse"},"id":23,"loop_ref":5,"next":20,"prev":22,"to":2,"twin":1,"wire_ref":null}],"loops":[{"face_ref":0,"id":0,"is_hole":false,"start_halfedge":0},{"face_ref":1,"id":1,"is_hole":false,"start_halfedge":4},{"face_ref":2,"id":2,"is_hole":false,"start_halfedge":8},{"face_ref":3,"id":3,"is_hole":false,"start_halfedge":12},{"face_ref":4,"id":4,"is_hole":false,"start_halfedge":16},{"face_ref":5,"id":5,"is_hole":false,"start_halfedge":20}],"shells":[{"faces":[0,1,2,3,4,5],"id":0,"is_closed":true}],"vertices":[{"id":0,"outgoing_halfedge":0,"position":[0.0,0.0,0.0],"tolerance":1e-8},{"id":1,"outgoing_halfedge":3,"position":[3.0,0.0,0.0],"tolerance":1e-8},{"id":2,"outgoing_halfedge":1,"position":[0.0,0.0,-3.0],"tolerance":1e-8},{"id":3,"outgoing_halfedge":2,"position":[3.0,0.0,-3.0],"tolerance":1e-8},{"id":4,"outgoing_halfedge":4,"position":[0.0,3.0,0.0],"tolerance":1e-8},{"id":5,"outgoing_halfedge":5,"position":[3.0,3.0,0.0],"tolerance":1e-8},{"id":6,"outgoing_halfedge":7,"position":[0.0,3.0,-3.0],"tolerance":1e-8},{"id":7,"outgoing_halfedge":6,"position":[3.0,3.0,-3.0],"tolerance":1e-8}],"wires":[]}},"b":{"accuracy":{"exchange":1e-6,"geometric":1e-8,"intersection":1e-9,"tessellation":0.01},"geometry":{"curves":[{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[2.7320508075688776,0.5,0.5000000000000002]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[2.7320508075688776,0.5,0.5000000000000002]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[2.0,0.5,-2.2320508075688776]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[2.0,0.5,-2.2320508075688776]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.7320508075688776,0.5,-1.2320508075688772]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[1.0,2.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[1.0,2.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[2.7320508075688776,2.5,0.5000000000000002]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[2.0,2.5,-2.2320508075688776]}],"intersections":[],"pcurves":[{"direction":[1.0,1.5165928555043604e-16],"kind":"Line2","origin":[0.0,0.0]},{"direction":[-1.5165928555043604e-16,1.0],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,1.5165928555043604e-16],"kind":"Line2","origin":[-1.9229626863835643e-16,2.0000000000000004]},{"direction":[-1.5165928555043604e-16,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,-1.487416814333745e-17],"kind":"Line2","origin":[0.0,0.0]},{"direction":[4.0636983087920364e-17,1.0000000000000002],"kind":"Line2","origin":[2.0000000000000004,8.127396617584078e-17]},{"direction":[1.0,-1.487416814333745e-17],"kind":"Line2","origin":[-2.220446049250313e-16,2.0000000000000004]},{"direction":[4.0636983087920364e-17,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.0000000000000004]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0000000000000002,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0000000000000002,0.0],"kind":"Line2","origin":[0.0,2.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[2.0000000000000004,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.000000000000001]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]}],"surfaces":[{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.4999999999999999,0.0,-0.8660254037844386],"y":[0.8660254037844387,-0.0,0.49999999999999994],"z":[-0.0,-1.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[1.0,2.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[0.5000000000000001,0.0,-0.8660254037844387],"z":[0.0,1.0,0.0]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.0,1.0,0.0],"y":[0.5000000000000001,0.0,-0.8660254037844387],"z":[-0.8660254037844387,-0.0,-0.5000000000000001]},"kind":"Plane"},{"frame":{"origin":[2.7320508075688776,0.5,0.5000000000000002],"x":[0.5000000000000001,0.0,-0.8660254037844386],"y":[-0.0,1.0,0.0],"z":[0.8660254037844387,0.0,0.5000000000000001]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[-0.0,1.0,0.0],"z":[-0.5000000000000001,-0.0,0.8660254037844387]},"kind":"Plane"},{"frame":{"origin":[2.0,0.5,-2.2320508075688776],"x":[0.0,1.0,0.0],"y":[0.8660254037844387,-0.0,0.5000000000000001],"z":[0.5000000000000001,0.0,-0.8660254037844387]},"kind":"Plane"}]},"id":"matrix-rotated-cutter","quality":{"kind":"Analytic"},"revision":0,"schema_version":2,"solids":[{"cavity_shells":[],"outer_shell":0}],"topology":{"edges":[{"chart_seam":false,"geometry":{"curve":0,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":3,"id":0,"tolerance":1e-8,"twin_halfedge":16},{"chart_seam":false,"geometry":{"curve":1,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":0,"id":1,"tolerance":1e-8,"twin_halfedge":11},{"chart_seam":false,"geometry":{"curve":2,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":8,"id":2,"tolerance":1e-8,"twin_halfedge":19},{"chart_seam":false,"geometry":{"curve":3,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":2,"id":3,"tolerance":1e-8,"twin_halfedge":12},{"chart_seam":false,"geometry":{"curve":4,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":15,"id":4,"tolerance":1e-8,"twin_halfedge":17},{"chart_seam":false,"geometry":{"curve":5,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":1,"id":5,"tolerance":1e-8,"twin_halfedge":23},{"chart_seam":false,"geometry":{"curve":6,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":10,"id":6,"tolerance":1e-8,"twin_halfedge":20},{"chart_seam":false,"geometry":{"curve":7,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":13,"id":7,"tolerance":1e-8,"twin_halfedge":22},{"chart_seam":false,"geometry":{"curve":8,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":4,"id":8,"tolerance":1e-8,"twin_halfedge":18},{"chart_seam":false,"geometry":{"curve":9,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":7,"id":9,"tolerance":1e-8,"twin_halfedge":9},{"chart_seam":false,"geometry":{"curve":10,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":5,"id":10,"tolerance":1e-8,"twin_halfedge":14},{"chart_seam":false,"geometry":{"curve":11,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":6,"id":11,"tolerance":1e-8,"twin_halfedge":21}],"faces":[{"id":0,"key":"bottom","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":0,"key":"bottom"}]},"sense":"Forward","shell_ref":0,"surface":0,"trim":{"chart":0,"holes":[],"outer":0,"uv_bounds":[{"hi":2.00000001,"lo":-1.0000000192296269e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":1,"key":"top","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":1,"key":"top"}]},"sense":"Forward","shell_ref":0,"surface":1,"trim":{"chart":0,"holes":[],"outer":1,"uv_bounds":[{"hi":2.0000000100000004,"lo":-1.0000000222044605e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":2,"key":"left","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":2,"key":"left"}]},"sense":"Forward","shell_ref":0,"surface":2,"trim":{"chart":0,"holes":[],"outer":2,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":3,"key":"right","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":3,"key":"right"}]},"sense":"Forward","shell_ref":0,"surface":3,"trim":{"chart":0,"holes":[],"outer":3,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.00000001,"lo":-1e-8}]}},{"id":4,"key":"front","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":4,"trim":{"chart":0,"holes":[],"outer":4,"uv_bounds":[{"hi":2.0000000100000004,"lo":-1e-8},{"hi":2.00000001,"lo":-1e-8}]}},{"id":5,"key":"back","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":5,"key":"back"}]},"sense":"Forward","shell_ref":0,"surface":5,"trim":{"chart":0,"holes":[],"outer":5,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.000000010000001,"lo":-1e-8}]}}],"halfedges":[{"edge":1,"face":0,"from":0,"geometry_use":{"pcurve":0,"periodic_lift":[0,0],"sense":"Forward"},"id":0,"loop_ref":0,"next":1,"prev":3,"to":2,"twin":11,"wire_ref":null},{"edge":5,"face":0,"from":2,"geometry_use":{"pcurve":1,"periodic_lift":[0,0],"sense":"Forward"},"id":1,"loop_ref":0,"next":2,"prev":0,"to":3,"twin":23,"wire_ref":null},{"edge":3,"face":0,"from":3,"geometry_use":{"pcurve":2,"periodic_lift":[0,0],"sense":"Reverse"},"id":2,"loop_ref":0,"next":3,"prev":1,"to":1,"twin":12,"wire_ref":null},{"edge":0,"face":0,"from":1,"geometry_use":{"pcurve":3,"periodic_lift":[0,0],"sense":"Reverse"},"id":3,"loop_ref":0,"next":0,"prev":2,"to":0,"twin":16,"wire_ref":null},{"edge":8,"face":1,"from":4,"geometry_use":{"pcurve":4,"periodic_lift":[0,0],"sense":"Forward"},"id":4,"loop_ref":1,"next":5,"prev":7,"to":5,"twin":18,"wire_ref":null},{"edge":10,"face":1,"from":5,"geometry_use":{"pcurve":5,"periodic_lift":[0,0],"sense":"Forward"},"id":5,"loop_ref":1,"next":6,"prev":4,"to":7,"twin":14,"wire_ref":null},{"edge":11,"face":1,"from":7,"geometry_use":{"pcurve":6,"periodic_lift":[0,0],"sense":"Reverse"},"id":6,"loop_ref":1,"next":7,"prev":5,"to":6,"twin":21,"wire_ref":null},{"edge":9,"face":1,"from":6,"geometry_use":{"pcurve":7,"periodic_lift":[0,0],"sense":"Reverse"},"id":7,"loop_ref":1,"next":4,"prev":6,"to":4,"twin":9,"wire_ref":null},{"edge":2,"face":2,"from":0,"geometry_use":{"pcurve":8,"periodic_lift":[0,0],"sense":"Forward"},"id":8,"loop_ref":2,"next":9,"prev":11,"to":4,"twin":19,"wire_ref":null},{"edge":9,"face":2,"from":4,"geometry_use":{"pcurve":9,"periodic_lift":[0,0],"sense":"Forward"},"id":9,"loop_ref":2,"next":10,"prev":8,"to":6,"twin":7,"wire_ref":null},{"edge":6,"face":2,"from":6,"geometry_use":{"pcurve":10,"periodic_lift":[0,0],"sense":"Reverse"},"id":10,"loop_ref":2,"next":11,"prev":9,"to":2,"twin":20,"wire_ref":null},{"edge":1,"face":2,"from":2,"geometry_use":{"pcurve":11,"periodic_lift":[0,0],"sense":"Reverse"},"id":11,"loop_ref":2,"next":8,"prev":10,"to":0,"twin":0,"wire_ref":null},{"edge":3,"face":3,"from":1,"geometry_use":{"pcurve":12,"periodic_lift":[0,0],"sense":"Forward"},"id":12,"loop_ref":3,"next":13,"prev":15,"to":3,"twin":2,"wire_ref":null},{"edge":7,"face":3,"from":3,"geometry_use":{"pcurve":13,"periodic_lift":[0,0],"sense":"Forward"},"id":13,"loop_ref":3,"next":14,"prev":12,"to":7,"twin":22,"wire_ref":null},{"edge":10,"face":3,"from":7,"geometry_use":{"pcurve":14,"periodic_lift":[0,0],"sense":"Reverse"},"id":14,"loop_ref":3,"next":15,"prev":13,"to":5,"twin":5,"wire_ref":null},{"edge":4,"face":3,"from":5,"geometry_use":{"pcurve":15,"periodic_lift":[0,0],"sense":"Reverse"},"id":15,"loop_ref":3,"next":12,"prev":14,"to":1,"twin":17,"wire_ref":null},{"edge":0,"face":4,"from":0,"geometry_use":{"pcurve":16,"periodic_lift":[0,0],"sense":"Forward"},"id":16,"loop_ref":4,"next":17,"prev":19,"to":1,"twin":3,"wire_ref":null},{"edge":4,"face":4,"from":1,"geometry_use":{"pcurve":17,"periodic_lift":[0,0],"sense":"Forward"},"id":17,"loop_ref":4,"next":18,"prev":16,"to":5,"twin":15,"wire_ref":null},{"edge":8,"face":4,"from":5,"geometry_use":{"pcurve":18,"periodic_lift":[0,0],"sense":"Reverse"},"id":18,"loop_ref":4,"next":19,"prev":17,"to":4,"twin":4,"wire_ref":null},{"edge":2,"face":4,"from":4,"geometry_use":{"pcurve":19,"periodic_lift":[0,0],"sense":"Reverse"},"id":19,"loop_ref":4,"next":16,"prev":18,"to":0,"twin":8,"wire_ref":null},{"edge":6,"face":5,"from":2,"geometry_use":{"pcurve":20,"periodic_lift":[0,0],"sense":"Forward"},"id":20,"loop_ref":5,"next":21,"prev":23,"to":6,"twin":10,"wire_ref":null},{"edge":11,"face":5,"from":6,"geometry_use":{"pcurve":21,"periodic_lift":[0,0],"sense":"Forward"},"id":21,"loop_ref":5,"next":22,"prev":20,"to":7,"twin":6,"wire_ref":null},{"edge":7,"face":5,"from":7,"geometry_use":{"pcurve":22,"periodic_lift":[0,0],"sense":"Reverse"},"id":22,"loop_ref":5,"next":23,"prev":21,"to":3,"twin":13,"wire_ref":null},{"edge":5,"face":5,"from":3,"geometry_use":{"pcurve":23,"periodic_lift":[0,0],"sense":"Reverse"},"id":23,"loop_ref":5,"next":20,"prev":22,"to":2,"twin":1,"wire_ref":null}],"loops":[{"face_ref":0,"id":0,"is_hole":false,"start_halfedge":0},{"face_ref":1,"id":1,"is_hole":false,"start_halfedge":4},{"face_ref":2,"id":2,"is_hole":false,"start_halfedge":8},{"face_ref":3,"id":3,"is_hole":false,"start_halfedge":12},{"face_ref":4,"id":4,"is_hole":false,"start_halfedge":16},{"face_ref":5,"id":5,"is_hole":false,"start_halfedge":20}],"shells":[{"faces":[0,1,2,3,4,5],"id":0,"is_closed":true}],"vertices":[{"id":0,"outgoing_halfedge":0,"position":[1.0,0.5,-0.5],"tolerance":1e-8},{"id":1,"outgoing_halfedge":3,"position":[2.7320508075688776,0.5,0.5000000000000002],"tolerance":1e-8},{"id":2,"outgoing_halfedge":1,"position":[2.0,0.5,-2.2320508075688776],"tolerance":1e-8},{"id":3,"outgoing_halfedge":2,"position":[3.7320508075688776,0.5,-1.2320508075688772],"tolerance":1e-8},{"id":4,"outgoing_halfedge":4,"position":[1.0,2.5,-0.5],"tolerance":1e-8},{"id":5,"outgoing_halfedge":5,"position":[2.7320508075688776,2.5,0.5000000000000002],"tolerance":1e-8},{"id":6,"outgoing_halfedge":7,"position":[2.0,2.5,-2.2320508075688776],"tolerance":1e-8},{"id":7,"outgoing_halfedge":6,"position":[3.7320508075688776,2.5,-1.2320508075688772],"tolerance":1e-8}],"wires":[]}},"handlers":["boolean_planar_polyhedra"],"operation":"intersection","result":{"brep":{"accuracy":{"exchange":1e-6,"geometric":1e-8,"intersection":1e-9,"tessellation":0.01},"geometry":{"curves":[{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[3.0,0.5,0.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.0,0.5,-1.6547005383792517]},{"direction":[0.0,0.0,1.0],"kind":"Line","origin":[3.0,2.5,-1.6547005383792517]},{"direction":[0.0,-1.0,0.0],"kind":"Line","origin":[3.0,2.5,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[1.8660254037844386,0.5,0.0]},{"direction":[-1.0,0.0,0.0],"kind":"Line","origin":[3.0,2.5,0.0]},{"direction":[0.0,-1.0,0.0],"kind":"Line","origin":[1.8660254037844386,2.5,0.0]},{"direction":[-0.8660254037844386,0.0,-0.4999999999999999],"kind":"Line","origin":[1.8660254037844388,0.5,0.0]},{"direction":[0.49999999999999994,0.0,-0.8660254037844387],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.8660254037844387,0.0,0.4999999999999999],"kind":"Line","origin":[1.9999999999999998,0.5,-2.232050807568877]},{"direction":[-0.8660254037844387,0.0,-0.5],"kind":"Line","origin":[3.0000000000000004,2.5,-1.6547005383792521]},{"direction":[-0.4999999999999999,0.0,0.8660254037844388],"kind":"Line","origin":[2.0,2.5,-2.232050807568878]},{"direction":[0.8660254037844386,0.0,0.5000000000000001],"kind":"Line","origin":[1.0,2.5,-0.5]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.0,-1.0,0.0],"kind":"Line","origin":[2.0000000000000004,2.5,-2.2320508075688776]}],"intersections":[],"pcurves":[{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.5]},{"direction":[0.0,1.0],"kind":"Line2","origin":[1.6547005383792517,0.5]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[1.6547005383792517,2.5]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[0.0,2.5]},{"direction":[1.0,0.0],"kind":"Line2","origin":[1.8660254037844386,0.5]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[3.0,2.5]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[3.0,2.5]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[1.8660254037844386,2.5]},{"direction":[0.8660254037844386,-0.49999999999999994],"kind":"Line2","origin":[0.5669872981077805,1.9820508075688774]},{"direction":[0.4999999999999999,0.8660254037844387],"kind":"Line2","origin":[-9.614813431917819e-17,1.0]},{"direction":[-1.4874168143337463e-17,-0.9999999999999999],"kind":"Line2","origin":[1.487416814333744e-17,1.0000000000000002]},{"direction":[0.9999999999999999,7.437084071668725e-18],"kind":"Line2","origin":[0.0,0.0]},{"direction":[7.038531937459528e-17,1.0],"kind":"Line2","origin":[1.9999999999999996,2.97483362866749e-17]},{"direction":[0.5,-0.8660254037844387],"kind":"Line2","origin":[1.1547005383792515,2.0000000000000004]},{"direction":[-1.0,-9.61481343191782e-17],"kind":"Line2","origin":[1.1547005383792515,2.0000000000000013]},{"direction":[2.0717043678169384e-16,-1.0000000000000002],"kind":"Line2","origin":[-4.440892098500626e-16,2.000000000000001]},{"direction":[1.0,-7.038531937459529e-17],"kind":"Line2","origin":[0.0,0.0]},{"direction":[-0.8660254037844386,-0.5000000000000001],"kind":"Line2","origin":[1.9820508075688772,0.5669872981077808]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,-1.0000000000000002],"kind":"Line2","origin":[2.0,2.000000000000001]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[2.0,2.000000000000001]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[0.9999999999999999,2.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[-0.9999999999999999,0.0],"kind":"Line2","origin":[1.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,1.1547005383792517]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,2.974833628667495e-17]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[2.0,3.845925372767128e-16]},{"direction":[0.0,-1.0000000000000002],"kind":"Line2","origin":[2.0,1.154700538379252]}],"surfaces":[{"frame":{"origin":[3.0,0.0,0.0],"x":[0.0,0.0,-1.0],"y":[-0.0,1.0,0.0],"z":[1.0,0.0,0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,0.0],"x":[1.0,0.0,0.0],"y":[-0.0,1.0,0.0],"z":[-0.0,-0.0,1.0]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.4999999999999999,0.0,-0.8660254037844386],"y":[0.8660254037844387,-0.0,0.49999999999999994],"z":[-0.0,-1.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[1.0,2.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[0.5000000000000001,0.0,-0.8660254037844387],"z":[0.0,1.0,0.0]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.0,1.0,0.0],"y":[0.5000000000000001,0.0,-0.8660254037844387],"z":[-0.8660254037844387,-0.0,-0.5000000000000001]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[-0.0,1.0,0.0],"z":[-0.5000000000000001,-0.0,0.8660254037844387]},"kind":"Plane"},{"frame":{"origin":[2.0,0.5,-2.2320508075688776],"x":[0.0,1.0,0.0],"y":[0.8660254037844387,-0.0,0.5000000000000001],"z":[0.5000000000000001,0.0,-0.8660254037844387]},"kind":"Plane"}]},"id":"matrix-rotated-box-intersection","quality":{"kind":"Analytic"},"revision":1,"schema_version":2,"solids":[{"cavity_shells":[],"outer_shell":0}],"topology":{"edges":[{"chart_seam":false,"geometry":{"curve":0,"kind":"Curve","range":{"hi":1.6547005383792517,"lo":0.0}},"halfedge":0,"id":0,"tolerance":1e-8,"twin_halfedge":8},{"chart_seam":false,"geometry":{"curve":1,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":1,"id":1,"tolerance":1e-8,"twin_halfedge":26},{"chart_seam":false,"geometry":{"curve":2,"kind":"Curve","range":{"hi":1.6547005383792517,"lo":0.0}},"halfedge":2,"id":2,"tolerance":1e-8,"twin_halfedge":13},{"chart_seam":false,"geometry":{"curve":3,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":3,"id":3,"tolerance":1e-8,"twin_halfedge":5},{"chart_seam":false,"geometry":{"curve":4,"kind":"Curve","range":{"hi":1.1339745962155614,"lo":0.0}},"halfedge":4,"id":4,"tolerance":1e-8,"twin_halfedge":9},{"chart_seam":false,"geometry":{"curve":5,"kind":"Curve","range":{"hi":1.1339745962155614,"lo":0.0}},"halfedge":6,"id":5,"tolerance":1e-8,"twin_halfedge":17},{"chart_seam":false,"geometry":{"curve":6,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":7,"id":6,"tolerance":1e-8,"twin_halfedge":22},{"chart_seam":false,"geometry":{"curve":7,"kind":"Curve","range":{"hi":1.0000000000000002,"lo":0.0}},"halfedge":10,"id":7,"tolerance":1e-8,"twin_halfedge":25},{"chart_seam":false,"geometry":{"curve":8,"kind":"Curve","range":{"hi":1.9999999999999998,"lo":0.0}},"halfedge":11,"id":8,"tolerance":1e-8,"twin_halfedge":21},{"chart_seam":false,"geometry":{"curve":9,"kind":"Curve","range":{"hi":1.1547005383792517,"lo":0.0}},"halfedge":12,"id":9,"tolerance":1e-8,"twin_halfedge":27},{"chart_seam":false,"geometry":{"curve":10,"kind":"Curve","range":{"hi":1.154700538379252,"lo":0.0}},"halfedge":14,"id":10,"tolerance":1e-8,"twin_halfedge":29},{"chart_seam":false,"geometry":{"curve":11,"kind":"Curve","range":{"hi":2.0000000000000004,"lo":0.0}},"halfedge":15,"id":11,"tolerance":1e-8,"twin_halfedge":19},{"chart_seam":false,"geometry":{"curve":12,"kind":"Curve","range":{"hi":0.9999999999999998,"lo":0.0}},"halfedge":16,"id":12,"tolerance":1e-8,"twin_halfedge":23},{"chart_seam":false,"geometry":{"curve":13,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":18,"id":13,"tolerance":1e-8,"twin_halfedge":24},{"chart_seam":false,"geometry":{"curve":14,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":20,"id":14,"tolerance":1e-8,"twin_halfedge":28}],"faces":[{"id":0,"key":"planar-host-3-0","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":3,"key":"right"}]},"sense":"Forward","shell_ref":0,"surface":0,"trim":{"chart":0,"holes":[],"outer":0,"uv_bounds":[{"hi":1.6547005483792516,"lo":-1e-8},{"hi":2.50000001,"lo":0.49999999}]}},{"id":1,"key":"planar-host-4-1","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":1,"trim":{"chart":0,"holes":[],"outer":1,"uv_bounds":[{"hi":3.00000001,"lo":1.8660253937844387},{"hi":2.50000001,"lo":0.49999999}]}},{"id":2,"key":"planar-cut-0-2","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":0,"key":"bottom"}]},"sense":"Forward","shell_ref":0,"surface":2,"trim":{"chart":0,"holes":[],"outer":2,"uv_bounds":[{"hi":2.0000000099999995,"lo":-1e-8},{"hi":1.9820508175688774,"lo":-1e-8}]}},{"id":3,"key":"planar-cut-1-3","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":1,"key":"top"}]},"sense":"Forward","shell_ref":0,"surface":3,"trim":{"chart":0,"holes":[],"outer":3,"uv_bounds":[{"hi":1.9820508175688771,"lo":-1.000000044408921e-8},{"hi":2.0000000100000013,"lo":-1.000000007038532e-8}]}},{"id":4,"key":"planar-cut-2-4","provenance":{"reversed":false,"role":"Preserved","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":2,"key":"left"}]},"sense":"Forward","shell_ref":0,"surface":4,"trim":{"chart":0,"holes":[],"outer":4,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.000000010000001,"lo":-1e-8}]}},{"id":5,"key":"planar-cut-4-5","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":5,"trim":{"chart":0,"holes":[],"outer":5,"uv_bounds":[{"hi":1.00000001,"lo":-1e-8},{"hi":2.00000001,"lo":-1e-8}]}},{"id":6,"key":"planar-cut-5-6","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":5,"key":"back"}]},"sense":"Forward","shell_ref":0,"surface":6,"trim":{"chart":0,"holes":[],"outer":6,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":1.154700548379252,"lo":-1e-8}]}}],"halfedges":[{"edge":0,"face":0,"from":0,"geometry_use":{"pcurve":0,"periodic_lift":[0,0],"sense":"Forward"},"id":0,"loop_ref":0,"next":1,"prev":3,"to":1,"twin":8,"wire_ref":null},{"edge":1,"face":0,"from":1,"geometry_use":{"pcurve":1,"periodic_lift":[0,0],"sense":"Forward"},"id":1,"loop_ref":0,"next":2,"prev":0,"to":2,"twin":26,"wire_ref":null},{"edge":2,"face":0,"from":2,"geometry_use":{"pcurve":2,"periodic_lift":[0,0],"sense":"Forward"},"id":2,"loop_ref":0,"next":3,"prev":1,"to":3,"twin":13,"wire_ref":null},{"edge":3,"face":0,"from":3,"geometry_use":{"pcurve":3,"periodic_lift":[0,0],"sense":"Forward"},"id":3,"loop_ref":0,"next":0,"prev":2,"to":0,"twin":5,"wire_ref":null},{"edge":4,"face":1,"from":4,"geometry_use":{"pcurve":4,"periodic_lift":[0,0],"sense":"Forward"},"id":4,"loop_ref":1,"next":5,"prev":7,"to":0,"twin":9,"wire_ref":null},{"edge":3,"face":1,"from":0,"geometry_use":{"pcurve":5,"periodic_lift":[0,0],"sense":"Reverse"},"id":5,"loop_ref":1,"next":6,"prev":4,"to":3,"twin":3,"wire_ref":null},{"edge":5,"face":1,"from":3,"geometry_use":{"pcurve":6,"periodic_lift":[0,0],"sense":"Forward"},"id":6,"loop_ref":1,"next":7,"prev":5,"to":5,"twin":17,"wire_ref":null},{"edge":6,"face":1,"from":5,"geometry_use":{"pcurve":7,"periodic_lift":[0,0],"sense":"Forward"},"id":7,"loop_ref":1,"next":4,"prev":6,"to":4,"twin":22,"wire_ref":null},{"edge":0,"face":2,"from":1,"geometry_use":{"pcurve":8,"periodic_lift":[0,0],"sense":"Reverse"},"id":8,"loop_ref":2,"next":9,"prev":12,"to":0,"twin":0,"wire_ref":null},{"edge":4,"face":2,"from":0,"geometry_use":{"pcurve":9,"periodic_lift":[0,0],"sense":"Reverse"},"id":9,"loop_ref":2,"next":10,"prev":8,"to":4,"twin":4,"wire_ref":null},{"edge":7,"face":2,"from":4,"geometry_use":{"pcurve":10,"periodic_lift":[0,0],"sense":"Forward"},"id":10,"loop_ref":2,"next":11,"prev":9,"to":6,"twin":25,"wire_ref":null},{"edge":8,"face":2,"from":6,"geometry_use":{"pcurve":11,"periodic_lift":[0,0],"sense":"Forward"},"id":11,"loop_ref":2,"next":12,"prev":10,"to":7,"twin":21,"wire_ref":null},{"edge":9,"face":2,"from":7,"geometry_use":{"pcurve":12,"periodic_lift":[0,0],"sense":"Forward"},"id":12,"loop_ref":2,"next":8,"prev":11,"to":1,"twin":27,"wire_ref":null},{"edge":2,"face":3,"from":3,"geometry_use":{"pcurve":13,"periodic_lift":[0,0],"sense":"Reverse"},"id":13,"loop_ref":3,"next":14,"prev":17,"to":2,"twin":2,"wire_ref":null},{"edge":10,"face":3,"from":2,"geometry_use":{"pcurve":14,"periodic_lift":[0,0],"sense":"Forward"},"id":14,"loop_ref":3,"next":15,"prev":13,"to":8,"twin":29,"wire_ref":null},{"edge":11,"face":3,"from":8,"geometry_use":{"pcurve":15,"periodic_lift":[0,0],"sense":"Forward"},"id":15,"loop_ref":3,"next":16,"prev":14,"to":9,"twin":19,"wire_ref":null},{"edge":12,"face":3,"from":9,"geometry_use":{"pcurve":16,"periodic_lift":[0,0],"sense":"Forward"},"id":16,"loop_ref":3,"next":17,"prev":15,"to":5,"twin":23,"wire_ref":null},{"edge":5,"face":3,"from":5,"geometry_use":{"pcurve":17,"periodic_lift":[0,0],"sense":"Reverse"},"id":17,"loop_ref":3,"next":13,"prev":16,"to":3,"twin":6,"wire_ref":null},{"edge":13,"face":4,"from":6,"geometry_use":{"pcurve":18,"periodic_lift":[0,0],"sense":"Forward"},"id":18,"loop_ref":4,"next":19,"prev":21,"to":9,"twin":24,"wire_ref":null},{"edge":11,"face":4,"from":9,"geometry_use":{"pcurve":19,"periodic_lift":[0,0],"sense":"Reverse"},"id":19,"loop_ref":4,"next":20,"prev":18,"to":8,"twin":15,"wire_ref":null},{"edge":14,"face":4,"from":8,"geometry_use":{"pcurve":20,"periodic_lift":[0,0],"sense":"Forward"},"id":20,"loop_ref":4,"next":21,"prev":19,"to":7,"twin":28,"wire_ref":null},{"edge":8,"face":4,"from":7,"geometry_use":{"pcurve":21,"periodic_lift":[0,0],"sense":"Reverse"},"id":21,"loop_ref":4,"next":18,"prev":20,"to":6,"twin":11,"wire_ref":null},{"edge":6,"face":5,"from":4,"geometry_use":{"pcurve":22,"periodic_lift":[0,0],"sense":"Reverse"},"id":22,"loop_ref":5,"next":23,"prev":25,"to":5,"twin":7,"wire_ref":null},{"edge":12,"face":5,"from":5,"geometry_use":{"pcurve":23,"periodic_lift":[0,0],"sense":"Reverse"},"id":23,"loop_ref":5,"next":24,"prev":22,"to":9,"twin":16,"wire_ref":null},{"edge":13,"face":5,"from":9,"geometry_use":{"pcurve":24,"periodic_lift":[0,0],"sense":"Reverse"},"id":24,"loop_ref":5,"next":25,"prev":23,"to":6,"twin":18,"wire_ref":null},{"edge":7,"face":5,"from":6,"geometry_use":{"pcurve":25,"periodic_lift":[0,0],"sense":"Reverse"},"id":25,"loop_ref":5,"next":22,"prev":24,"to":4,"twin":10,"wire_ref":null},{"edge":1,"face":6,"from":2,"geometry_use":{"pcurve":26,"periodic_lift":[0,0],"sense":"Reverse"},"id":26,"loop_ref":6,"next":27,"prev":29,"to":1,"twin":1,"wire_ref":null},{"edge":9,"face":6,"from":1,"geometry_use":{"pcurve":27,"periodic_lift":[0,0],"sense":"Reverse"},"id":27,"loop_ref":6,"next":28,"prev":26,"to":7,"twin":12,"wire_ref":null},{"edge":14,"face":6,"from":7,"geometry_use":{"pcurve":28,"periodic_lift":[0,0],"sense":"Reverse"},"id":28,"loop_ref":6,"next":29,"prev":27,"to":8,"twin":20,"wire_ref":null},{"edge":10,"face":6,"from":8,"geometry_use":{"pcurve":29,"periodic_lift":[0,0],"sense":"Reverse"},"id":29,"loop_ref":6,"next":26,"prev":28,"to":2,"twin":14,"wire_ref":null}],"loops":[{"face_ref":0,"id":0,"is_hole":false,"start_halfedge":0},{"face_ref":1,"id":1,"is_hole":false,"start_halfedge":4},{"face_ref":2,"id":2,"is_hole":false,"start_halfedge":8},{"face_ref":3,"id":3,"is_hole":false,"start_halfedge":13},{"face_ref":4,"id":4,"is_hole":false,"start_halfedge":18},{"face_ref":5,"id":5,"is_hole":false,"start_halfedge":22},{"face_ref":6,"id":6,"is_hole":false,"start_halfedge":26}],"shells":[{"faces":[0,2,6,3,1,5,4],"id":0,"is_closed":true}],"vertices":[{"id":0,"outgoing_halfedge":0,"position":[3.0,0.5,0.0],"tolerance":1e-8},{"id":1,"outgoing_halfedge":1,"position":[3.0,0.5,-1.6547005383792517],"tolerance":1e-8},{"id":2,"outgoing_halfedge":2,"position":[3.0,2.5,-1.6547005383792517],"tolerance":1e-8},{"id":3,"outgoing_halfedge":3,"position":[3.0,2.5,0.0],"tolerance":1e-8},{"id":4,"outgoing_halfedge":4,"position":[1.8660254037844386,0.5,0.0],"tolerance":1e-8},{"id":5,"outgoing_halfedge":7,"position":[1.8660254037844386,2.5,0.0],"tolerance":1e-8},{"id":6,"outgoing_halfedge":11,"position":[1.0,0.5,-0.5],"tolerance":1e-8},{"id":7,"outgoing_halfedge":12,"position":[1.9999999999999998,0.5,-2.232050807568877],"tolerance":1e-8},{"id":8,"outgoing_halfedge":15,"position":[2.0,2.5,-2.232050807568878],"tolerance":1e-8},{"id":9,"outgoing_halfedge":16,"position":[1.0,2.5,-0.5],"tolerance":1e-8}],"wires":[]}}}} \ No newline at end of file diff --git a/main/opengeometry/tests/fixtures/cases/boolean-matrix/rotated-box-union.json b/main/opengeometry/tests/fixtures/cases/boolean-matrix/rotated-box-union.json index ffd4e10a..e46a6d27 100644 --- a/main/opengeometry/tests/fixtures/cases/boolean-matrix/rotated-box-union.json +++ b/main/opengeometry/tests/fixtures/cases/boolean-matrix/rotated-box-union.json @@ -1 +1 @@ -{"a":{"accuracy":{"exchange":1e-6,"geometric":1e-8,"intersection":1e-9,"tessellation":0.01},"geometry":{"curves":[{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[3.0,0.0,0.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.0,0.0,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,0.0,-3.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[0.0,0.0,-3.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.0,0.0,-3.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,3.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[0.0,3.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[3.0,3.0,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,3.0,-3.0]}],"intersections":[],"pcurves":[{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]}],"surfaces":[{"frame":{"origin":[0.0,0.0,0.0],"x":[0.0,0.0,-1.0],"y":[1.0,-0.0,0.0],"z":[-0.0,-1.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,3.0,0.0],"x":[1.0,0.0,0.0],"y":[0.0,0.0,-1.0],"z":[0.0,1.0,0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,0.0],"x":[0.0,1.0,0.0],"y":[0.0,0.0,-1.0],"z":[-1.0,-0.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[3.0,0.0,0.0],"x":[0.0,0.0,-1.0],"y":[-0.0,1.0,0.0],"z":[1.0,0.0,0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,0.0],"x":[1.0,0.0,0.0],"y":[-0.0,1.0,0.0],"z":[-0.0,-0.0,1.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,-3.0],"x":[0.0,1.0,0.0],"y":[1.0,-0.0,0.0],"z":[0.0,0.0,-1.0]},"kind":"Plane"}]},"id":"matrix-box-host","quality":{"kind":"Analytic"},"revision":0,"schema_version":2,"solids":[{"cavity_shells":[],"outer_shell":0}],"topology":{"edges":[{"chart_seam":false,"geometry":{"curve":0,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":3,"id":0,"tolerance":1e-8,"twin_halfedge":16},{"chart_seam":false,"geometry":{"curve":1,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":0,"id":1,"tolerance":1e-8,"twin_halfedge":11},{"chart_seam":false,"geometry":{"curve":2,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":8,"id":2,"tolerance":1e-8,"twin_halfedge":19},{"chart_seam":false,"geometry":{"curve":3,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":2,"id":3,"tolerance":1e-8,"twin_halfedge":12},{"chart_seam":false,"geometry":{"curve":4,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":15,"id":4,"tolerance":1e-8,"twin_halfedge":17},{"chart_seam":false,"geometry":{"curve":5,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":1,"id":5,"tolerance":1e-8,"twin_halfedge":23},{"chart_seam":false,"geometry":{"curve":6,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":10,"id":6,"tolerance":1e-8,"twin_halfedge":20},{"chart_seam":false,"geometry":{"curve":7,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":13,"id":7,"tolerance":1e-8,"twin_halfedge":22},{"chart_seam":false,"geometry":{"curve":8,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":4,"id":8,"tolerance":1e-8,"twin_halfedge":18},{"chart_seam":false,"geometry":{"curve":9,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":7,"id":9,"tolerance":1e-8,"twin_halfedge":9},{"chart_seam":false,"geometry":{"curve":10,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":5,"id":10,"tolerance":1e-8,"twin_halfedge":14},{"chart_seam":false,"geometry":{"curve":11,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":6,"id":11,"tolerance":1e-8,"twin_halfedge":21}],"faces":[{"id":0,"key":"bottom","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":0,"key":"bottom"}]},"sense":"Forward","shell_ref":0,"surface":0,"trim":{"chart":0,"holes":[],"outer":0,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":1,"key":"top","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":1,"key":"top"}]},"sense":"Forward","shell_ref":0,"surface":1,"trim":{"chart":0,"holes":[],"outer":1,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":2,"key":"left","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":2,"key":"left"}]},"sense":"Forward","shell_ref":0,"surface":2,"trim":{"chart":0,"holes":[],"outer":2,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":3,"key":"right","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":3,"key":"right"}]},"sense":"Forward","shell_ref":0,"surface":3,"trim":{"chart":0,"holes":[],"outer":3,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":4,"key":"front","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":4,"trim":{"chart":0,"holes":[],"outer":4,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":5,"key":"back","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":5,"key":"back"}]},"sense":"Forward","shell_ref":0,"surface":5,"trim":{"chart":0,"holes":[],"outer":5,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}}],"halfedges":[{"edge":1,"face":0,"from":0,"geometry_use":{"pcurve":0,"periodic_lift":[0,0],"sense":"Forward"},"id":0,"loop_ref":0,"next":1,"prev":3,"to":2,"twin":11,"wire_ref":null},{"edge":5,"face":0,"from":2,"geometry_use":{"pcurve":1,"periodic_lift":[0,0],"sense":"Forward"},"id":1,"loop_ref":0,"next":2,"prev":0,"to":3,"twin":23,"wire_ref":null},{"edge":3,"face":0,"from":3,"geometry_use":{"pcurve":2,"periodic_lift":[0,0],"sense":"Reverse"},"id":2,"loop_ref":0,"next":3,"prev":1,"to":1,"twin":12,"wire_ref":null},{"edge":0,"face":0,"from":1,"geometry_use":{"pcurve":3,"periodic_lift":[0,0],"sense":"Reverse"},"id":3,"loop_ref":0,"next":0,"prev":2,"to":0,"twin":16,"wire_ref":null},{"edge":8,"face":1,"from":4,"geometry_use":{"pcurve":4,"periodic_lift":[0,0],"sense":"Forward"},"id":4,"loop_ref":1,"next":5,"prev":7,"to":5,"twin":18,"wire_ref":null},{"edge":10,"face":1,"from":5,"geometry_use":{"pcurve":5,"periodic_lift":[0,0],"sense":"Forward"},"id":5,"loop_ref":1,"next":6,"prev":4,"to":7,"twin":14,"wire_ref":null},{"edge":11,"face":1,"from":7,"geometry_use":{"pcurve":6,"periodic_lift":[0,0],"sense":"Reverse"},"id":6,"loop_ref":1,"next":7,"prev":5,"to":6,"twin":21,"wire_ref":null},{"edge":9,"face":1,"from":6,"geometry_use":{"pcurve":7,"periodic_lift":[0,0],"sense":"Reverse"},"id":7,"loop_ref":1,"next":4,"prev":6,"to":4,"twin":9,"wire_ref":null},{"edge":2,"face":2,"from":0,"geometry_use":{"pcurve":8,"periodic_lift":[0,0],"sense":"Forward"},"id":8,"loop_ref":2,"next":9,"prev":11,"to":4,"twin":19,"wire_ref":null},{"edge":9,"face":2,"from":4,"geometry_use":{"pcurve":9,"periodic_lift":[0,0],"sense":"Forward"},"id":9,"loop_ref":2,"next":10,"prev":8,"to":6,"twin":7,"wire_ref":null},{"edge":6,"face":2,"from":6,"geometry_use":{"pcurve":10,"periodic_lift":[0,0],"sense":"Reverse"},"id":10,"loop_ref":2,"next":11,"prev":9,"to":2,"twin":20,"wire_ref":null},{"edge":1,"face":2,"from":2,"geometry_use":{"pcurve":11,"periodic_lift":[0,0],"sense":"Reverse"},"id":11,"loop_ref":2,"next":8,"prev":10,"to":0,"twin":0,"wire_ref":null},{"edge":3,"face":3,"from":1,"geometry_use":{"pcurve":12,"periodic_lift":[0,0],"sense":"Forward"},"id":12,"loop_ref":3,"next":13,"prev":15,"to":3,"twin":2,"wire_ref":null},{"edge":7,"face":3,"from":3,"geometry_use":{"pcurve":13,"periodic_lift":[0,0],"sense":"Forward"},"id":13,"loop_ref":3,"next":14,"prev":12,"to":7,"twin":22,"wire_ref":null},{"edge":10,"face":3,"from":7,"geometry_use":{"pcurve":14,"periodic_lift":[0,0],"sense":"Reverse"},"id":14,"loop_ref":3,"next":15,"prev":13,"to":5,"twin":5,"wire_ref":null},{"edge":4,"face":3,"from":5,"geometry_use":{"pcurve":15,"periodic_lift":[0,0],"sense":"Reverse"},"id":15,"loop_ref":3,"next":12,"prev":14,"to":1,"twin":17,"wire_ref":null},{"edge":0,"face":4,"from":0,"geometry_use":{"pcurve":16,"periodic_lift":[0,0],"sense":"Forward"},"id":16,"loop_ref":4,"next":17,"prev":19,"to":1,"twin":3,"wire_ref":null},{"edge":4,"face":4,"from":1,"geometry_use":{"pcurve":17,"periodic_lift":[0,0],"sense":"Forward"},"id":17,"loop_ref":4,"next":18,"prev":16,"to":5,"twin":15,"wire_ref":null},{"edge":8,"face":4,"from":5,"geometry_use":{"pcurve":18,"periodic_lift":[0,0],"sense":"Reverse"},"id":18,"loop_ref":4,"next":19,"prev":17,"to":4,"twin":4,"wire_ref":null},{"edge":2,"face":4,"from":4,"geometry_use":{"pcurve":19,"periodic_lift":[0,0],"sense":"Reverse"},"id":19,"loop_ref":4,"next":16,"prev":18,"to":0,"twin":8,"wire_ref":null},{"edge":6,"face":5,"from":2,"geometry_use":{"pcurve":20,"periodic_lift":[0,0],"sense":"Forward"},"id":20,"loop_ref":5,"next":21,"prev":23,"to":6,"twin":10,"wire_ref":null},{"edge":11,"face":5,"from":6,"geometry_use":{"pcurve":21,"periodic_lift":[0,0],"sense":"Forward"},"id":21,"loop_ref":5,"next":22,"prev":20,"to":7,"twin":6,"wire_ref":null},{"edge":7,"face":5,"from":7,"geometry_use":{"pcurve":22,"periodic_lift":[0,0],"sense":"Reverse"},"id":22,"loop_ref":5,"next":23,"prev":21,"to":3,"twin":13,"wire_ref":null},{"edge":5,"face":5,"from":3,"geometry_use":{"pcurve":23,"periodic_lift":[0,0],"sense":"Reverse"},"id":23,"loop_ref":5,"next":20,"prev":22,"to":2,"twin":1,"wire_ref":null}],"loops":[{"face_ref":0,"id":0,"is_hole":false,"start_halfedge":0},{"face_ref":1,"id":1,"is_hole":false,"start_halfedge":4},{"face_ref":2,"id":2,"is_hole":false,"start_halfedge":8},{"face_ref":3,"id":3,"is_hole":false,"start_halfedge":12},{"face_ref":4,"id":4,"is_hole":false,"start_halfedge":16},{"face_ref":5,"id":5,"is_hole":false,"start_halfedge":20}],"shells":[{"faces":[0,1,2,3,4,5],"id":0,"is_closed":true}],"vertices":[{"id":0,"outgoing_halfedge":0,"position":[0.0,0.0,0.0],"tolerance":1e-8},{"id":1,"outgoing_halfedge":3,"position":[3.0,0.0,0.0],"tolerance":1e-8},{"id":2,"outgoing_halfedge":1,"position":[0.0,0.0,-3.0],"tolerance":1e-8},{"id":3,"outgoing_halfedge":2,"position":[3.0,0.0,-3.0],"tolerance":1e-8},{"id":4,"outgoing_halfedge":4,"position":[0.0,3.0,0.0],"tolerance":1e-8},{"id":5,"outgoing_halfedge":5,"position":[3.0,3.0,0.0],"tolerance":1e-8},{"id":6,"outgoing_halfedge":7,"position":[0.0,3.0,-3.0],"tolerance":1e-8},{"id":7,"outgoing_halfedge":6,"position":[3.0,3.0,-3.0],"tolerance":1e-8}],"wires":[]}},"b":{"accuracy":{"exchange":1e-6,"geometric":1e-8,"intersection":1e-9,"tessellation":0.01},"geometry":{"curves":[{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[2.7320508075688776,0.5,0.5000000000000002]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[2.7320508075688776,0.5,0.5000000000000002]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[2.0,0.5,-2.2320508075688776]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[2.0,0.5,-2.2320508075688776]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.7320508075688776,0.5,-1.2320508075688772]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[1.0,2.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[1.0,2.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[2.7320508075688776,2.5,0.5000000000000002]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[2.0,2.5,-2.2320508075688776]}],"intersections":[],"pcurves":[{"direction":[1.0,1.5165928555043604e-16],"kind":"Line2","origin":[0.0,0.0]},{"direction":[-1.5165928555043604e-16,1.0],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,1.5165928555043604e-16],"kind":"Line2","origin":[-1.9229626863835643e-16,2.0000000000000004]},{"direction":[-1.5165928555043604e-16,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,-1.487416814333745e-17],"kind":"Line2","origin":[0.0,0.0]},{"direction":[4.0636983087920364e-17,1.0000000000000002],"kind":"Line2","origin":[2.0000000000000004,8.127396617584078e-17]},{"direction":[1.0,-1.487416814333745e-17],"kind":"Line2","origin":[-2.220446049250313e-16,2.0000000000000004]},{"direction":[4.0636983087920364e-17,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.0000000000000004]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0000000000000002,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0000000000000002,0.0],"kind":"Line2","origin":[0.0,2.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[2.0000000000000004,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.000000000000001]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]}],"surfaces":[{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.4999999999999999,0.0,-0.8660254037844386],"y":[0.8660254037844387,-0.0,0.49999999999999994],"z":[-0.0,-1.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[1.0,2.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[0.5000000000000001,0.0,-0.8660254037844387],"z":[0.0,1.0,0.0]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.0,1.0,0.0],"y":[0.5000000000000001,0.0,-0.8660254037844387],"z":[-0.8660254037844387,-0.0,-0.5000000000000001]},"kind":"Plane"},{"frame":{"origin":[2.7320508075688776,0.5,0.5000000000000002],"x":[0.5000000000000001,0.0,-0.8660254037844386],"y":[-0.0,1.0,0.0],"z":[0.8660254037844387,0.0,0.5000000000000001]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[-0.0,1.0,0.0],"z":[-0.5000000000000001,-0.0,0.8660254037844387]},"kind":"Plane"},{"frame":{"origin":[2.0,0.5,-2.2320508075688776],"x":[0.0,1.0,0.0],"y":[0.8660254037844387,-0.0,0.5000000000000001],"z":[0.5000000000000001,0.0,-0.8660254037844387]},"kind":"Plane"}]},"id":"matrix-rotated-cutter","quality":{"kind":"Analytic"},"revision":0,"schema_version":2,"solids":[{"cavity_shells":[],"outer_shell":0}],"topology":{"edges":[{"chart_seam":false,"geometry":{"curve":0,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":3,"id":0,"tolerance":1e-8,"twin_halfedge":16},{"chart_seam":false,"geometry":{"curve":1,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":0,"id":1,"tolerance":1e-8,"twin_halfedge":11},{"chart_seam":false,"geometry":{"curve":2,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":8,"id":2,"tolerance":1e-8,"twin_halfedge":19},{"chart_seam":false,"geometry":{"curve":3,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":2,"id":3,"tolerance":1e-8,"twin_halfedge":12},{"chart_seam":false,"geometry":{"curve":4,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":15,"id":4,"tolerance":1e-8,"twin_halfedge":17},{"chart_seam":false,"geometry":{"curve":5,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":1,"id":5,"tolerance":1e-8,"twin_halfedge":23},{"chart_seam":false,"geometry":{"curve":6,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":10,"id":6,"tolerance":1e-8,"twin_halfedge":20},{"chart_seam":false,"geometry":{"curve":7,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":13,"id":7,"tolerance":1e-8,"twin_halfedge":22},{"chart_seam":false,"geometry":{"curve":8,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":4,"id":8,"tolerance":1e-8,"twin_halfedge":18},{"chart_seam":false,"geometry":{"curve":9,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":7,"id":9,"tolerance":1e-8,"twin_halfedge":9},{"chart_seam":false,"geometry":{"curve":10,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":5,"id":10,"tolerance":1e-8,"twin_halfedge":14},{"chart_seam":false,"geometry":{"curve":11,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":6,"id":11,"tolerance":1e-8,"twin_halfedge":21}],"faces":[{"id":0,"key":"bottom","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":0,"key":"bottom"}]},"sense":"Forward","shell_ref":0,"surface":0,"trim":{"chart":0,"holes":[],"outer":0,"uv_bounds":[{"hi":2.00000001,"lo":-1.0000000192296269e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":1,"key":"top","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":1,"key":"top"}]},"sense":"Forward","shell_ref":0,"surface":1,"trim":{"chart":0,"holes":[],"outer":1,"uv_bounds":[{"hi":2.0000000100000004,"lo":-1.0000000222044605e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":2,"key":"left","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":2,"key":"left"}]},"sense":"Forward","shell_ref":0,"surface":2,"trim":{"chart":0,"holes":[],"outer":2,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":3,"key":"right","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":3,"key":"right"}]},"sense":"Forward","shell_ref":0,"surface":3,"trim":{"chart":0,"holes":[],"outer":3,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.00000001,"lo":-1e-8}]}},{"id":4,"key":"front","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":4,"trim":{"chart":0,"holes":[],"outer":4,"uv_bounds":[{"hi":2.0000000100000004,"lo":-1e-8},{"hi":2.00000001,"lo":-1e-8}]}},{"id":5,"key":"back","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":5,"key":"back"}]},"sense":"Forward","shell_ref":0,"surface":5,"trim":{"chart":0,"holes":[],"outer":5,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.000000010000001,"lo":-1e-8}]}}],"halfedges":[{"edge":1,"face":0,"from":0,"geometry_use":{"pcurve":0,"periodic_lift":[0,0],"sense":"Forward"},"id":0,"loop_ref":0,"next":1,"prev":3,"to":2,"twin":11,"wire_ref":null},{"edge":5,"face":0,"from":2,"geometry_use":{"pcurve":1,"periodic_lift":[0,0],"sense":"Forward"},"id":1,"loop_ref":0,"next":2,"prev":0,"to":3,"twin":23,"wire_ref":null},{"edge":3,"face":0,"from":3,"geometry_use":{"pcurve":2,"periodic_lift":[0,0],"sense":"Reverse"},"id":2,"loop_ref":0,"next":3,"prev":1,"to":1,"twin":12,"wire_ref":null},{"edge":0,"face":0,"from":1,"geometry_use":{"pcurve":3,"periodic_lift":[0,0],"sense":"Reverse"},"id":3,"loop_ref":0,"next":0,"prev":2,"to":0,"twin":16,"wire_ref":null},{"edge":8,"face":1,"from":4,"geometry_use":{"pcurve":4,"periodic_lift":[0,0],"sense":"Forward"},"id":4,"loop_ref":1,"next":5,"prev":7,"to":5,"twin":18,"wire_ref":null},{"edge":10,"face":1,"from":5,"geometry_use":{"pcurve":5,"periodic_lift":[0,0],"sense":"Forward"},"id":5,"loop_ref":1,"next":6,"prev":4,"to":7,"twin":14,"wire_ref":null},{"edge":11,"face":1,"from":7,"geometry_use":{"pcurve":6,"periodic_lift":[0,0],"sense":"Reverse"},"id":6,"loop_ref":1,"next":7,"prev":5,"to":6,"twin":21,"wire_ref":null},{"edge":9,"face":1,"from":6,"geometry_use":{"pcurve":7,"periodic_lift":[0,0],"sense":"Reverse"},"id":7,"loop_ref":1,"next":4,"prev":6,"to":4,"twin":9,"wire_ref":null},{"edge":2,"face":2,"from":0,"geometry_use":{"pcurve":8,"periodic_lift":[0,0],"sense":"Forward"},"id":8,"loop_ref":2,"next":9,"prev":11,"to":4,"twin":19,"wire_ref":null},{"edge":9,"face":2,"from":4,"geometry_use":{"pcurve":9,"periodic_lift":[0,0],"sense":"Forward"},"id":9,"loop_ref":2,"next":10,"prev":8,"to":6,"twin":7,"wire_ref":null},{"edge":6,"face":2,"from":6,"geometry_use":{"pcurve":10,"periodic_lift":[0,0],"sense":"Reverse"},"id":10,"loop_ref":2,"next":11,"prev":9,"to":2,"twin":20,"wire_ref":null},{"edge":1,"face":2,"from":2,"geometry_use":{"pcurve":11,"periodic_lift":[0,0],"sense":"Reverse"},"id":11,"loop_ref":2,"next":8,"prev":10,"to":0,"twin":0,"wire_ref":null},{"edge":3,"face":3,"from":1,"geometry_use":{"pcurve":12,"periodic_lift":[0,0],"sense":"Forward"},"id":12,"loop_ref":3,"next":13,"prev":15,"to":3,"twin":2,"wire_ref":null},{"edge":7,"face":3,"from":3,"geometry_use":{"pcurve":13,"periodic_lift":[0,0],"sense":"Forward"},"id":13,"loop_ref":3,"next":14,"prev":12,"to":7,"twin":22,"wire_ref":null},{"edge":10,"face":3,"from":7,"geometry_use":{"pcurve":14,"periodic_lift":[0,0],"sense":"Reverse"},"id":14,"loop_ref":3,"next":15,"prev":13,"to":5,"twin":5,"wire_ref":null},{"edge":4,"face":3,"from":5,"geometry_use":{"pcurve":15,"periodic_lift":[0,0],"sense":"Reverse"},"id":15,"loop_ref":3,"next":12,"prev":14,"to":1,"twin":17,"wire_ref":null},{"edge":0,"face":4,"from":0,"geometry_use":{"pcurve":16,"periodic_lift":[0,0],"sense":"Forward"},"id":16,"loop_ref":4,"next":17,"prev":19,"to":1,"twin":3,"wire_ref":null},{"edge":4,"face":4,"from":1,"geometry_use":{"pcurve":17,"periodic_lift":[0,0],"sense":"Forward"},"id":17,"loop_ref":4,"next":18,"prev":16,"to":5,"twin":15,"wire_ref":null},{"edge":8,"face":4,"from":5,"geometry_use":{"pcurve":18,"periodic_lift":[0,0],"sense":"Reverse"},"id":18,"loop_ref":4,"next":19,"prev":17,"to":4,"twin":4,"wire_ref":null},{"edge":2,"face":4,"from":4,"geometry_use":{"pcurve":19,"periodic_lift":[0,0],"sense":"Reverse"},"id":19,"loop_ref":4,"next":16,"prev":18,"to":0,"twin":8,"wire_ref":null},{"edge":6,"face":5,"from":2,"geometry_use":{"pcurve":20,"periodic_lift":[0,0],"sense":"Forward"},"id":20,"loop_ref":5,"next":21,"prev":23,"to":6,"twin":10,"wire_ref":null},{"edge":11,"face":5,"from":6,"geometry_use":{"pcurve":21,"periodic_lift":[0,0],"sense":"Forward"},"id":21,"loop_ref":5,"next":22,"prev":20,"to":7,"twin":6,"wire_ref":null},{"edge":7,"face":5,"from":7,"geometry_use":{"pcurve":22,"periodic_lift":[0,0],"sense":"Reverse"},"id":22,"loop_ref":5,"next":23,"prev":21,"to":3,"twin":13,"wire_ref":null},{"edge":5,"face":5,"from":3,"geometry_use":{"pcurve":23,"periodic_lift":[0,0],"sense":"Reverse"},"id":23,"loop_ref":5,"next":20,"prev":22,"to":2,"twin":1,"wire_ref":null}],"loops":[{"face_ref":0,"id":0,"is_hole":false,"start_halfedge":0},{"face_ref":1,"id":1,"is_hole":false,"start_halfedge":4},{"face_ref":2,"id":2,"is_hole":false,"start_halfedge":8},{"face_ref":3,"id":3,"is_hole":false,"start_halfedge":12},{"face_ref":4,"id":4,"is_hole":false,"start_halfedge":16},{"face_ref":5,"id":5,"is_hole":false,"start_halfedge":20}],"shells":[{"faces":[0,1,2,3,4,5],"id":0,"is_closed":true}],"vertices":[{"id":0,"outgoing_halfedge":0,"position":[1.0,0.5,-0.5],"tolerance":1e-8},{"id":1,"outgoing_halfedge":3,"position":[2.7320508075688776,0.5,0.5000000000000002],"tolerance":1e-8},{"id":2,"outgoing_halfedge":1,"position":[2.0,0.5,-2.2320508075688776],"tolerance":1e-8},{"id":3,"outgoing_halfedge":2,"position":[3.7320508075688776,0.5,-1.2320508075688772],"tolerance":1e-8},{"id":4,"outgoing_halfedge":4,"position":[1.0,2.5,-0.5],"tolerance":1e-8},{"id":5,"outgoing_halfedge":5,"position":[2.7320508075688776,2.5,0.5000000000000002],"tolerance":1e-8},{"id":6,"outgoing_halfedge":7,"position":[2.0,2.5,-2.2320508075688776],"tolerance":1e-8},{"id":7,"outgoing_halfedge":6,"position":[3.7320508075688776,2.5,-1.2320508075688772],"tolerance":1e-8}],"wires":[]}},"handlers":[],"operation":"union","result":{"error":{"CoverageGap":{"families":["generic analytic solid","intersecting analytic solid"]}}}} \ No newline at end of file +{"a":{"accuracy":{"exchange":1e-6,"geometric":1e-8,"intersection":1e-9,"tessellation":0.01},"geometry":{"curves":[{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[3.0,0.0,0.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.0,0.0,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,0.0,-3.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[0.0,0.0,-3.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.0,0.0,-3.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,3.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[0.0,3.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[3.0,3.0,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,3.0,-3.0]}],"intersections":[],"pcurves":[{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]}],"surfaces":[{"frame":{"origin":[0.0,0.0,0.0],"x":[0.0,0.0,-1.0],"y":[1.0,-0.0,0.0],"z":[-0.0,-1.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,3.0,0.0],"x":[1.0,0.0,0.0],"y":[0.0,0.0,-1.0],"z":[0.0,1.0,0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,0.0],"x":[0.0,1.0,0.0],"y":[0.0,0.0,-1.0],"z":[-1.0,-0.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[3.0,0.0,0.0],"x":[0.0,0.0,-1.0],"y":[-0.0,1.0,0.0],"z":[1.0,0.0,0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,0.0],"x":[1.0,0.0,0.0],"y":[-0.0,1.0,0.0],"z":[-0.0,-0.0,1.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,-3.0],"x":[0.0,1.0,0.0],"y":[1.0,-0.0,0.0],"z":[0.0,0.0,-1.0]},"kind":"Plane"}]},"id":"matrix-box-host","quality":{"kind":"Analytic"},"revision":0,"schema_version":2,"solids":[{"cavity_shells":[],"outer_shell":0}],"topology":{"edges":[{"chart_seam":false,"geometry":{"curve":0,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":3,"id":0,"tolerance":1e-8,"twin_halfedge":16},{"chart_seam":false,"geometry":{"curve":1,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":0,"id":1,"tolerance":1e-8,"twin_halfedge":11},{"chart_seam":false,"geometry":{"curve":2,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":8,"id":2,"tolerance":1e-8,"twin_halfedge":19},{"chart_seam":false,"geometry":{"curve":3,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":2,"id":3,"tolerance":1e-8,"twin_halfedge":12},{"chart_seam":false,"geometry":{"curve":4,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":15,"id":4,"tolerance":1e-8,"twin_halfedge":17},{"chart_seam":false,"geometry":{"curve":5,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":1,"id":5,"tolerance":1e-8,"twin_halfedge":23},{"chart_seam":false,"geometry":{"curve":6,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":10,"id":6,"tolerance":1e-8,"twin_halfedge":20},{"chart_seam":false,"geometry":{"curve":7,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":13,"id":7,"tolerance":1e-8,"twin_halfedge":22},{"chart_seam":false,"geometry":{"curve":8,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":4,"id":8,"tolerance":1e-8,"twin_halfedge":18},{"chart_seam":false,"geometry":{"curve":9,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":7,"id":9,"tolerance":1e-8,"twin_halfedge":9},{"chart_seam":false,"geometry":{"curve":10,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":5,"id":10,"tolerance":1e-8,"twin_halfedge":14},{"chart_seam":false,"geometry":{"curve":11,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":6,"id":11,"tolerance":1e-8,"twin_halfedge":21}],"faces":[{"id":0,"key":"bottom","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":0,"key":"bottom"}]},"sense":"Forward","shell_ref":0,"surface":0,"trim":{"chart":0,"holes":[],"outer":0,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":1,"key":"top","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":1,"key":"top"}]},"sense":"Forward","shell_ref":0,"surface":1,"trim":{"chart":0,"holes":[],"outer":1,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":2,"key":"left","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":2,"key":"left"}]},"sense":"Forward","shell_ref":0,"surface":2,"trim":{"chart":0,"holes":[],"outer":2,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":3,"key":"right","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":3,"key":"right"}]},"sense":"Forward","shell_ref":0,"surface":3,"trim":{"chart":0,"holes":[],"outer":3,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":4,"key":"front","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":4,"trim":{"chart":0,"holes":[],"outer":4,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":5,"key":"back","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":5,"key":"back"}]},"sense":"Forward","shell_ref":0,"surface":5,"trim":{"chart":0,"holes":[],"outer":5,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}}],"halfedges":[{"edge":1,"face":0,"from":0,"geometry_use":{"pcurve":0,"periodic_lift":[0,0],"sense":"Forward"},"id":0,"loop_ref":0,"next":1,"prev":3,"to":2,"twin":11,"wire_ref":null},{"edge":5,"face":0,"from":2,"geometry_use":{"pcurve":1,"periodic_lift":[0,0],"sense":"Forward"},"id":1,"loop_ref":0,"next":2,"prev":0,"to":3,"twin":23,"wire_ref":null},{"edge":3,"face":0,"from":3,"geometry_use":{"pcurve":2,"periodic_lift":[0,0],"sense":"Reverse"},"id":2,"loop_ref":0,"next":3,"prev":1,"to":1,"twin":12,"wire_ref":null},{"edge":0,"face":0,"from":1,"geometry_use":{"pcurve":3,"periodic_lift":[0,0],"sense":"Reverse"},"id":3,"loop_ref":0,"next":0,"prev":2,"to":0,"twin":16,"wire_ref":null},{"edge":8,"face":1,"from":4,"geometry_use":{"pcurve":4,"periodic_lift":[0,0],"sense":"Forward"},"id":4,"loop_ref":1,"next":5,"prev":7,"to":5,"twin":18,"wire_ref":null},{"edge":10,"face":1,"from":5,"geometry_use":{"pcurve":5,"periodic_lift":[0,0],"sense":"Forward"},"id":5,"loop_ref":1,"next":6,"prev":4,"to":7,"twin":14,"wire_ref":null},{"edge":11,"face":1,"from":7,"geometry_use":{"pcurve":6,"periodic_lift":[0,0],"sense":"Reverse"},"id":6,"loop_ref":1,"next":7,"prev":5,"to":6,"twin":21,"wire_ref":null},{"edge":9,"face":1,"from":6,"geometry_use":{"pcurve":7,"periodic_lift":[0,0],"sense":"Reverse"},"id":7,"loop_ref":1,"next":4,"prev":6,"to":4,"twin":9,"wire_ref":null},{"edge":2,"face":2,"from":0,"geometry_use":{"pcurve":8,"periodic_lift":[0,0],"sense":"Forward"},"id":8,"loop_ref":2,"next":9,"prev":11,"to":4,"twin":19,"wire_ref":null},{"edge":9,"face":2,"from":4,"geometry_use":{"pcurve":9,"periodic_lift":[0,0],"sense":"Forward"},"id":9,"loop_ref":2,"next":10,"prev":8,"to":6,"twin":7,"wire_ref":null},{"edge":6,"face":2,"from":6,"geometry_use":{"pcurve":10,"periodic_lift":[0,0],"sense":"Reverse"},"id":10,"loop_ref":2,"next":11,"prev":9,"to":2,"twin":20,"wire_ref":null},{"edge":1,"face":2,"from":2,"geometry_use":{"pcurve":11,"periodic_lift":[0,0],"sense":"Reverse"},"id":11,"loop_ref":2,"next":8,"prev":10,"to":0,"twin":0,"wire_ref":null},{"edge":3,"face":3,"from":1,"geometry_use":{"pcurve":12,"periodic_lift":[0,0],"sense":"Forward"},"id":12,"loop_ref":3,"next":13,"prev":15,"to":3,"twin":2,"wire_ref":null},{"edge":7,"face":3,"from":3,"geometry_use":{"pcurve":13,"periodic_lift":[0,0],"sense":"Forward"},"id":13,"loop_ref":3,"next":14,"prev":12,"to":7,"twin":22,"wire_ref":null},{"edge":10,"face":3,"from":7,"geometry_use":{"pcurve":14,"periodic_lift":[0,0],"sense":"Reverse"},"id":14,"loop_ref":3,"next":15,"prev":13,"to":5,"twin":5,"wire_ref":null},{"edge":4,"face":3,"from":5,"geometry_use":{"pcurve":15,"periodic_lift":[0,0],"sense":"Reverse"},"id":15,"loop_ref":3,"next":12,"prev":14,"to":1,"twin":17,"wire_ref":null},{"edge":0,"face":4,"from":0,"geometry_use":{"pcurve":16,"periodic_lift":[0,0],"sense":"Forward"},"id":16,"loop_ref":4,"next":17,"prev":19,"to":1,"twin":3,"wire_ref":null},{"edge":4,"face":4,"from":1,"geometry_use":{"pcurve":17,"periodic_lift":[0,0],"sense":"Forward"},"id":17,"loop_ref":4,"next":18,"prev":16,"to":5,"twin":15,"wire_ref":null},{"edge":8,"face":4,"from":5,"geometry_use":{"pcurve":18,"periodic_lift":[0,0],"sense":"Reverse"},"id":18,"loop_ref":4,"next":19,"prev":17,"to":4,"twin":4,"wire_ref":null},{"edge":2,"face":4,"from":4,"geometry_use":{"pcurve":19,"periodic_lift":[0,0],"sense":"Reverse"},"id":19,"loop_ref":4,"next":16,"prev":18,"to":0,"twin":8,"wire_ref":null},{"edge":6,"face":5,"from":2,"geometry_use":{"pcurve":20,"periodic_lift":[0,0],"sense":"Forward"},"id":20,"loop_ref":5,"next":21,"prev":23,"to":6,"twin":10,"wire_ref":null},{"edge":11,"face":5,"from":6,"geometry_use":{"pcurve":21,"periodic_lift":[0,0],"sense":"Forward"},"id":21,"loop_ref":5,"next":22,"prev":20,"to":7,"twin":6,"wire_ref":null},{"edge":7,"face":5,"from":7,"geometry_use":{"pcurve":22,"periodic_lift":[0,0],"sense":"Reverse"},"id":22,"loop_ref":5,"next":23,"prev":21,"to":3,"twin":13,"wire_ref":null},{"edge":5,"face":5,"from":3,"geometry_use":{"pcurve":23,"periodic_lift":[0,0],"sense":"Reverse"},"id":23,"loop_ref":5,"next":20,"prev":22,"to":2,"twin":1,"wire_ref":null}],"loops":[{"face_ref":0,"id":0,"is_hole":false,"start_halfedge":0},{"face_ref":1,"id":1,"is_hole":false,"start_halfedge":4},{"face_ref":2,"id":2,"is_hole":false,"start_halfedge":8},{"face_ref":3,"id":3,"is_hole":false,"start_halfedge":12},{"face_ref":4,"id":4,"is_hole":false,"start_halfedge":16},{"face_ref":5,"id":5,"is_hole":false,"start_halfedge":20}],"shells":[{"faces":[0,1,2,3,4,5],"id":0,"is_closed":true}],"vertices":[{"id":0,"outgoing_halfedge":0,"position":[0.0,0.0,0.0],"tolerance":1e-8},{"id":1,"outgoing_halfedge":3,"position":[3.0,0.0,0.0],"tolerance":1e-8},{"id":2,"outgoing_halfedge":1,"position":[0.0,0.0,-3.0],"tolerance":1e-8},{"id":3,"outgoing_halfedge":2,"position":[3.0,0.0,-3.0],"tolerance":1e-8},{"id":4,"outgoing_halfedge":4,"position":[0.0,3.0,0.0],"tolerance":1e-8},{"id":5,"outgoing_halfedge":5,"position":[3.0,3.0,0.0],"tolerance":1e-8},{"id":6,"outgoing_halfedge":7,"position":[0.0,3.0,-3.0],"tolerance":1e-8},{"id":7,"outgoing_halfedge":6,"position":[3.0,3.0,-3.0],"tolerance":1e-8}],"wires":[]}},"b":{"accuracy":{"exchange":1e-6,"geometric":1e-8,"intersection":1e-9,"tessellation":0.01},"geometry":{"curves":[{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[1.0,0.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[2.7320508075688776,0.5,0.5000000000000002]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[2.7320508075688776,0.5,0.5000000000000002]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[2.0,0.5,-2.2320508075688776]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[2.0,0.5,-2.2320508075688776]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.7320508075688776,0.5,-1.2320508075688772]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[1.0,2.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[1.0,2.5,-0.5]},{"direction":[0.5000000000000001,0.0,-0.8660254037844387],"kind":"Line","origin":[2.7320508075688776,2.5,0.5000000000000002]},{"direction":[0.8660254037844387,0.0,0.5000000000000001],"kind":"Line","origin":[2.0,2.5,-2.2320508075688776]}],"intersections":[],"pcurves":[{"direction":[1.0,1.5165928555043604e-16],"kind":"Line2","origin":[0.0,0.0]},{"direction":[-1.5165928555043604e-16,1.0],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,1.5165928555043604e-16],"kind":"Line2","origin":[-1.9229626863835643e-16,2.0000000000000004]},{"direction":[-1.5165928555043604e-16,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,-1.487416814333745e-17],"kind":"Line2","origin":[0.0,0.0]},{"direction":[4.0636983087920364e-17,1.0000000000000002],"kind":"Line2","origin":[2.0000000000000004,8.127396617584078e-17]},{"direction":[1.0,-1.487416814333745e-17],"kind":"Line2","origin":[-2.220446049250313e-16,2.0000000000000004]},{"direction":[4.0636983087920364e-17,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.0000000000000004]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0000000000000002,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0000000000000002,0.0],"kind":"Line2","origin":[0.0,2.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[2.0000000000000004,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.000000000000001]},{"direction":[0.0,1.0000000000000002],"kind":"Line2","origin":[0.0,0.0]}],"surfaces":[{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.4999999999999999,0.0,-0.8660254037844386],"y":[0.8660254037844387,-0.0,0.49999999999999994],"z":[-0.0,-1.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[1.0,2.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[0.5000000000000001,0.0,-0.8660254037844387],"z":[0.0,1.0,0.0]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.0,1.0,0.0],"y":[0.5000000000000001,0.0,-0.8660254037844387],"z":[-0.8660254037844387,-0.0,-0.5000000000000001]},"kind":"Plane"},{"frame":{"origin":[2.7320508075688776,0.5,0.5000000000000002],"x":[0.5000000000000001,0.0,-0.8660254037844386],"y":[-0.0,1.0,0.0],"z":[0.8660254037844387,0.0,0.5000000000000001]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[-0.0,1.0,0.0],"z":[-0.5000000000000001,-0.0,0.8660254037844387]},"kind":"Plane"},{"frame":{"origin":[2.0,0.5,-2.2320508075688776],"x":[0.0,1.0,0.0],"y":[0.8660254037844387,-0.0,0.5000000000000001],"z":[0.5000000000000001,0.0,-0.8660254037844387]},"kind":"Plane"}]},"id":"matrix-rotated-cutter","quality":{"kind":"Analytic"},"revision":0,"schema_version":2,"solids":[{"cavity_shells":[],"outer_shell":0}],"topology":{"edges":[{"chart_seam":false,"geometry":{"curve":0,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":3,"id":0,"tolerance":1e-8,"twin_halfedge":16},{"chart_seam":false,"geometry":{"curve":1,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":0,"id":1,"tolerance":1e-8,"twin_halfedge":11},{"chart_seam":false,"geometry":{"curve":2,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":8,"id":2,"tolerance":1e-8,"twin_halfedge":19},{"chart_seam":false,"geometry":{"curve":3,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":2,"id":3,"tolerance":1e-8,"twin_halfedge":12},{"chart_seam":false,"geometry":{"curve":4,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":15,"id":4,"tolerance":1e-8,"twin_halfedge":17},{"chart_seam":false,"geometry":{"curve":5,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":1,"id":5,"tolerance":1e-8,"twin_halfedge":23},{"chart_seam":false,"geometry":{"curve":6,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":10,"id":6,"tolerance":1e-8,"twin_halfedge":20},{"chart_seam":false,"geometry":{"curve":7,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":13,"id":7,"tolerance":1e-8,"twin_halfedge":22},{"chart_seam":false,"geometry":{"curve":8,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":4,"id":8,"tolerance":1e-8,"twin_halfedge":18},{"chart_seam":false,"geometry":{"curve":9,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":7,"id":9,"tolerance":1e-8,"twin_halfedge":9},{"chart_seam":false,"geometry":{"curve":10,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":5,"id":10,"tolerance":1e-8,"twin_halfedge":14},{"chart_seam":false,"geometry":{"curve":11,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":6,"id":11,"tolerance":1e-8,"twin_halfedge":21}],"faces":[{"id":0,"key":"bottom","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":0,"key":"bottom"}]},"sense":"Forward","shell_ref":0,"surface":0,"trim":{"chart":0,"holes":[],"outer":0,"uv_bounds":[{"hi":2.00000001,"lo":-1.0000000192296269e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":1,"key":"top","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":1,"key":"top"}]},"sense":"Forward","shell_ref":0,"surface":1,"trim":{"chart":0,"holes":[],"outer":1,"uv_bounds":[{"hi":2.0000000100000004,"lo":-1.0000000222044605e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":2,"key":"left","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":2,"key":"left"}]},"sense":"Forward","shell_ref":0,"surface":2,"trim":{"chart":0,"holes":[],"outer":2,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.0000000100000004,"lo":-1e-8}]}},{"id":3,"key":"right","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":3,"key":"right"}]},"sense":"Forward","shell_ref":0,"surface":3,"trim":{"chart":0,"holes":[],"outer":3,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.00000001,"lo":-1e-8}]}},{"id":4,"key":"front","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":4,"trim":{"chart":0,"holes":[],"outer":4,"uv_bounds":[{"hi":2.0000000100000004,"lo":-1e-8},{"hi":2.00000001,"lo":-1e-8}]}},{"id":5,"key":"back","provenance":{"reversed":false,"role":"Authored","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":5,"key":"back"}]},"sense":"Forward","shell_ref":0,"surface":5,"trim":{"chart":0,"holes":[],"outer":5,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.000000010000001,"lo":-1e-8}]}}],"halfedges":[{"edge":1,"face":0,"from":0,"geometry_use":{"pcurve":0,"periodic_lift":[0,0],"sense":"Forward"},"id":0,"loop_ref":0,"next":1,"prev":3,"to":2,"twin":11,"wire_ref":null},{"edge":5,"face":0,"from":2,"geometry_use":{"pcurve":1,"periodic_lift":[0,0],"sense":"Forward"},"id":1,"loop_ref":0,"next":2,"prev":0,"to":3,"twin":23,"wire_ref":null},{"edge":3,"face":0,"from":3,"geometry_use":{"pcurve":2,"periodic_lift":[0,0],"sense":"Reverse"},"id":2,"loop_ref":0,"next":3,"prev":1,"to":1,"twin":12,"wire_ref":null},{"edge":0,"face":0,"from":1,"geometry_use":{"pcurve":3,"periodic_lift":[0,0],"sense":"Reverse"},"id":3,"loop_ref":0,"next":0,"prev":2,"to":0,"twin":16,"wire_ref":null},{"edge":8,"face":1,"from":4,"geometry_use":{"pcurve":4,"periodic_lift":[0,0],"sense":"Forward"},"id":4,"loop_ref":1,"next":5,"prev":7,"to":5,"twin":18,"wire_ref":null},{"edge":10,"face":1,"from":5,"geometry_use":{"pcurve":5,"periodic_lift":[0,0],"sense":"Forward"},"id":5,"loop_ref":1,"next":6,"prev":4,"to":7,"twin":14,"wire_ref":null},{"edge":11,"face":1,"from":7,"geometry_use":{"pcurve":6,"periodic_lift":[0,0],"sense":"Reverse"},"id":6,"loop_ref":1,"next":7,"prev":5,"to":6,"twin":21,"wire_ref":null},{"edge":9,"face":1,"from":6,"geometry_use":{"pcurve":7,"periodic_lift":[0,0],"sense":"Reverse"},"id":7,"loop_ref":1,"next":4,"prev":6,"to":4,"twin":9,"wire_ref":null},{"edge":2,"face":2,"from":0,"geometry_use":{"pcurve":8,"periodic_lift":[0,0],"sense":"Forward"},"id":8,"loop_ref":2,"next":9,"prev":11,"to":4,"twin":19,"wire_ref":null},{"edge":9,"face":2,"from":4,"geometry_use":{"pcurve":9,"periodic_lift":[0,0],"sense":"Forward"},"id":9,"loop_ref":2,"next":10,"prev":8,"to":6,"twin":7,"wire_ref":null},{"edge":6,"face":2,"from":6,"geometry_use":{"pcurve":10,"periodic_lift":[0,0],"sense":"Reverse"},"id":10,"loop_ref":2,"next":11,"prev":9,"to":2,"twin":20,"wire_ref":null},{"edge":1,"face":2,"from":2,"geometry_use":{"pcurve":11,"periodic_lift":[0,0],"sense":"Reverse"},"id":11,"loop_ref":2,"next":8,"prev":10,"to":0,"twin":0,"wire_ref":null},{"edge":3,"face":3,"from":1,"geometry_use":{"pcurve":12,"periodic_lift":[0,0],"sense":"Forward"},"id":12,"loop_ref":3,"next":13,"prev":15,"to":3,"twin":2,"wire_ref":null},{"edge":7,"face":3,"from":3,"geometry_use":{"pcurve":13,"periodic_lift":[0,0],"sense":"Forward"},"id":13,"loop_ref":3,"next":14,"prev":12,"to":7,"twin":22,"wire_ref":null},{"edge":10,"face":3,"from":7,"geometry_use":{"pcurve":14,"periodic_lift":[0,0],"sense":"Reverse"},"id":14,"loop_ref":3,"next":15,"prev":13,"to":5,"twin":5,"wire_ref":null},{"edge":4,"face":3,"from":5,"geometry_use":{"pcurve":15,"periodic_lift":[0,0],"sense":"Reverse"},"id":15,"loop_ref":3,"next":12,"prev":14,"to":1,"twin":17,"wire_ref":null},{"edge":0,"face":4,"from":0,"geometry_use":{"pcurve":16,"periodic_lift":[0,0],"sense":"Forward"},"id":16,"loop_ref":4,"next":17,"prev":19,"to":1,"twin":3,"wire_ref":null},{"edge":4,"face":4,"from":1,"geometry_use":{"pcurve":17,"periodic_lift":[0,0],"sense":"Forward"},"id":17,"loop_ref":4,"next":18,"prev":16,"to":5,"twin":15,"wire_ref":null},{"edge":8,"face":4,"from":5,"geometry_use":{"pcurve":18,"periodic_lift":[0,0],"sense":"Reverse"},"id":18,"loop_ref":4,"next":19,"prev":17,"to":4,"twin":4,"wire_ref":null},{"edge":2,"face":4,"from":4,"geometry_use":{"pcurve":19,"periodic_lift":[0,0],"sense":"Reverse"},"id":19,"loop_ref":4,"next":16,"prev":18,"to":0,"twin":8,"wire_ref":null},{"edge":6,"face":5,"from":2,"geometry_use":{"pcurve":20,"periodic_lift":[0,0],"sense":"Forward"},"id":20,"loop_ref":5,"next":21,"prev":23,"to":6,"twin":10,"wire_ref":null},{"edge":11,"face":5,"from":6,"geometry_use":{"pcurve":21,"periodic_lift":[0,0],"sense":"Forward"},"id":21,"loop_ref":5,"next":22,"prev":20,"to":7,"twin":6,"wire_ref":null},{"edge":7,"face":5,"from":7,"geometry_use":{"pcurve":22,"periodic_lift":[0,0],"sense":"Reverse"},"id":22,"loop_ref":5,"next":23,"prev":21,"to":3,"twin":13,"wire_ref":null},{"edge":5,"face":5,"from":3,"geometry_use":{"pcurve":23,"periodic_lift":[0,0],"sense":"Reverse"},"id":23,"loop_ref":5,"next":20,"prev":22,"to":2,"twin":1,"wire_ref":null}],"loops":[{"face_ref":0,"id":0,"is_hole":false,"start_halfedge":0},{"face_ref":1,"id":1,"is_hole":false,"start_halfedge":4},{"face_ref":2,"id":2,"is_hole":false,"start_halfedge":8},{"face_ref":3,"id":3,"is_hole":false,"start_halfedge":12},{"face_ref":4,"id":4,"is_hole":false,"start_halfedge":16},{"face_ref":5,"id":5,"is_hole":false,"start_halfedge":20}],"shells":[{"faces":[0,1,2,3,4,5],"id":0,"is_closed":true}],"vertices":[{"id":0,"outgoing_halfedge":0,"position":[1.0,0.5,-0.5],"tolerance":1e-8},{"id":1,"outgoing_halfedge":3,"position":[2.7320508075688776,0.5,0.5000000000000002],"tolerance":1e-8},{"id":2,"outgoing_halfedge":1,"position":[2.0,0.5,-2.2320508075688776],"tolerance":1e-8},{"id":3,"outgoing_halfedge":2,"position":[3.7320508075688776,0.5,-1.2320508075688772],"tolerance":1e-8},{"id":4,"outgoing_halfedge":4,"position":[1.0,2.5,-0.5],"tolerance":1e-8},{"id":5,"outgoing_halfedge":5,"position":[2.7320508075688776,2.5,0.5000000000000002],"tolerance":1e-8},{"id":6,"outgoing_halfedge":7,"position":[2.0,2.5,-2.2320508075688776],"tolerance":1e-8},{"id":7,"outgoing_halfedge":6,"position":[3.7320508075688776,2.5,-1.2320508075688772],"tolerance":1e-8}],"wires":[]}},"handlers":["boolean_planar_polyhedra"],"operation":"union","result":{"brep":{"accuracy":{"exchange":1e-6,"geometric":1e-8,"intersection":1e-9,"tessellation":0.01},"geometry":{"curves":[{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,0.0,-3.0]},{"direction":[0.0,0.0,1.0],"kind":"Line","origin":[3.0,0.0,-3.0]},{"direction":[-1.0,0.0,0.0],"kind":"Line","origin":[3.0,0.0,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[0.0,3.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[3.0,3.0,0.0]},{"direction":[-1.0,0.0,0.0],"kind":"Line","origin":[3.0,3.0,-3.0]},{"direction":[0.0,0.0,1.0],"kind":"Line","origin":[0.0,3.0,-3.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[0.0,0.0,0.0]},{"direction":[0.0,-1.0,0.0],"kind":"Line","origin":[0.0,3.0,-3.0]},{"direction":[0.0,0.0,1.0],"kind":"Line","origin":[3.0,0.5,-1.654700538379252]},{"direction":[0.0,-1.0,0.0],"kind":"Line","origin":[3.0,0.5,0.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.0,0.0,-3.0]},{"direction":[0.0,-1.0,0.0],"kind":"Line","origin":[3.0,3.0,0.0]},{"direction":[0.0,0.0,-1.0],"kind":"Line","origin":[3.0,2.5,0.0]},{"direction":[0.0,-1.0,0.0],"kind":"Line","origin":[3.0,2.5,-1.654700538379252]},{"direction":[-1.0,0.0,0.0],"kind":"Line","origin":[3.0,0.5,0.0]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[1.8660254037844386,0.5,0.0]},{"direction":[1.0,0.0,0.0],"kind":"Line","origin":[1.8660254037844386,2.5,0.0]},{"direction":[0.8660254037844387,0.0,0.4999999999999999],"kind":"Line","origin":[3.0,0.5,-1.6547005383792512]},{"direction":[-0.5000000000000001,0.0,0.8660254037844387],"kind":"Line","origin":[3.7320508075688776,0.5,-1.232050807568877]},{"direction":[-0.8660254037844387,0.0,-0.5],"kind":"Line","origin":[2.7320508075688776,0.5,0.5000000000000002]},{"direction":[0.8660254037844387,0.0,0.5],"kind":"Line","origin":[1.8660254037844386,2.5,-5.551115123125783e-17]},{"direction":[0.4999999999999999,0.0,-0.8660254037844388],"kind":"Line","origin":[2.7320508075688776,2.5,0.5000000000000002]},{"direction":[-0.8660254037844386,0.0,-0.5000000000000001],"kind":"Line","origin":[3.7320508075688776,2.5,-1.2320508075688779]},{"direction":[0.0,1.0,0.0],"kind":"Line","origin":[3.7320508075688776,0.5,-1.232050807568877]},{"direction":[0.0,-1.0,0.0],"kind":"Line","origin":[2.7320508075688776,2.5,0.5000000000000002]}],"intersections":[],"pcurves":[{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[3.0,3.0]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[3.0,3.0]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[3.0,3.0]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[3.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[1.654700538379252,0.5]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[0.0,0.5]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.5]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[1.654700538379252,2.5]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[3.0,0.5]},{"direction":[0.0,1.0],"kind":"Line2","origin":[1.8660254037844386,0.5]},{"direction":[1.0,0.0],"kind":"Line2","origin":[1.8660254037844386,2.5]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[3.0,3.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[3.0,0.5]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[3.0,0.0]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[3.0,3.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,3.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,0.0]},{"direction":[-0.8660254037844386,0.49999999999999994],"kind":"Line2","origin":[2.0,1.1547005383792515]},{"direction":[7.038531937459528e-17,1.0],"kind":"Line2","origin":[1.9999999999999996,1.154700538379252]},{"direction":[-1.0,-1.5165928555043604e-16],"kind":"Line2","origin":[1.9999999999999996,2.000000000000001]},{"direction":[4.0636983087920376e-17,-1.0],"kind":"Line2","origin":[-1.9229626863835643e-16,2.0000000000000004]},{"direction":[-0.4999999999999999,-0.8660254037844387],"kind":"Line2","origin":[0.5669872981077805,1.9820508075688774]},{"direction":[-0.5,0.8660254037844387],"kind":"Line2","origin":[1.9820508075688772,0.5669872981077808]},{"direction":[0.8660254037844386,0.5000000000000001],"kind":"Line2","origin":[0.9999999999999999,4.0636983087920364e-17]},{"direction":[1.0,9.61481343191782e-17],"kind":"Line2","origin":[0.9999999999999999,9.614813431917819e-17]},{"direction":[-2.0717043678169384e-16,1.0000000000000002],"kind":"Line2","origin":[2.0000000000000004,8.127396617584078e-17]},{"direction":[-1.0,7.038531937459529e-17],"kind":"Line2","origin":[1.9999999999999998,2.000000000000001]},{"direction":[-1.0000000000000002,0.0],"kind":"Line2","origin":[2.0,0.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[2.0,0.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.0]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[0.0,2.0]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.9999999999999999,0.0]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[2.0000000000000004,0.0]},{"direction":[0.0,-1.0],"kind":"Line2","origin":[2.0000000000000004,2.0]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.9999999999999999,2.0]},{"direction":[-1.0,0.0],"kind":"Line2","origin":[2.0,1.1547005383792517]},{"direction":[0.0,-1.0000000000000002],"kind":"Line2","origin":[2.0,2.0000000000000004]},{"direction":[1.0,0.0],"kind":"Line2","origin":[0.0,2.000000000000001]},{"direction":[0.0,1.0],"kind":"Line2","origin":[0.0,1.154700538379252]}],"surfaces":[{"frame":{"origin":[0.0,0.0,0.0],"x":[0.0,0.0,-1.0],"y":[1.0,-0.0,0.0],"z":[-0.0,-1.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,3.0,0.0],"x":[1.0,0.0,0.0],"y":[0.0,0.0,-1.0],"z":[0.0,1.0,0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,0.0],"x":[0.0,1.0,0.0],"y":[0.0,0.0,-1.0],"z":[-1.0,-0.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[3.0,0.0,0.0],"x":[0.0,0.0,-1.0],"y":[-0.0,1.0,0.0],"z":[1.0,0.0,0.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,0.0],"x":[1.0,0.0,0.0],"y":[-0.0,1.0,0.0],"z":[-0.0,-0.0,1.0]},"kind":"Plane"},{"frame":{"origin":[0.0,0.0,-3.0],"x":[0.0,1.0,0.0],"y":[1.0,-0.0,0.0],"z":[0.0,0.0,-1.0]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.4999999999999999,0.0,-0.8660254037844386],"y":[0.8660254037844387,-0.0,0.49999999999999994],"z":[-0.0,-1.0,-0.0]},"kind":"Plane"},{"frame":{"origin":[1.0,2.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[0.5000000000000001,0.0,-0.8660254037844387],"z":[0.0,1.0,0.0]},"kind":"Plane"},{"frame":{"origin":[2.7320508075688776,0.5,0.5000000000000002],"x":[0.5000000000000001,0.0,-0.8660254037844386],"y":[-0.0,1.0,0.0],"z":[0.8660254037844387,0.0,0.5000000000000001]},"kind":"Plane"},{"frame":{"origin":[1.0,0.5,-0.5],"x":[0.8660254037844386,0.0,0.5],"y":[-0.0,1.0,0.0],"z":[-0.5000000000000001,-0.0,0.8660254037844387]},"kind":"Plane"},{"frame":{"origin":[2.0,0.5,-2.2320508075688776],"x":[0.0,1.0,0.0],"y":[0.8660254037844387,-0.0,0.5000000000000001],"z":[0.5000000000000001,0.0,-0.8660254037844387]},"kind":"Plane"}]},"id":"matrix-rotated-box-union","quality":{"kind":"Analytic"},"revision":1,"schema_version":2,"solids":[{"cavity_shells":[],"outer_shell":0}],"topology":{"edges":[{"chart_seam":false,"geometry":{"curve":0,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":0,"id":0,"tolerance":1e-8,"twin_halfedge":11},{"chart_seam":false,"geometry":{"curve":1,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":1,"id":1,"tolerance":1e-8,"twin_halfedge":31},{"chart_seam":false,"geometry":{"curve":2,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":2,"id":2,"tolerance":1e-8,"twin_halfedge":14},{"chart_seam":false,"geometry":{"curve":3,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":3,"id":3,"tolerance":1e-8,"twin_halfedge":26},{"chart_seam":false,"geometry":{"curve":4,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":4,"id":4,"tolerance":1e-8,"twin_halfedge":24},{"chart_seam":false,"geometry":{"curve":5,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":5,"id":5,"tolerance":1e-8,"twin_halfedge":16},{"chart_seam":false,"geometry":{"curve":6,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":6,"id":6,"tolerance":1e-8,"twin_halfedge":29},{"chart_seam":false,"geometry":{"curve":7,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":7,"id":7,"tolerance":1e-8,"twin_halfedge":9},{"chart_seam":false,"geometry":{"curve":8,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":8,"id":8,"tolerance":1e-8,"twin_halfedge":25},{"chart_seam":false,"geometry":{"curve":9,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":10,"id":9,"tolerance":1e-8,"twin_halfedge":28},{"chart_seam":false,"geometry":{"curve":10,"kind":"Curve","range":{"hi":1.654700538379252,"lo":0.0}},"halfedge":12,"id":10,"tolerance":1e-8,"twin_halfedge":32},{"chart_seam":false,"geometry":{"curve":11,"kind":"Curve","range":{"hi":0.5,"lo":0.0}},"halfedge":13,"id":11,"tolerance":1e-8,"twin_halfedge":27},{"chart_seam":false,"geometry":{"curve":12,"kind":"Curve","range":{"hi":3.0,"lo":0.0}},"halfedge":15,"id":12,"tolerance":1e-8,"twin_halfedge":30},{"chart_seam":false,"geometry":{"curve":13,"kind":"Curve","range":{"hi":0.5,"lo":0.0}},"halfedge":17,"id":13,"tolerance":1e-8,"twin_halfedge":23},{"chart_seam":false,"geometry":{"curve":14,"kind":"Curve","range":{"hi":1.654700538379252,"lo":0.0}},"halfedge":18,"id":14,"tolerance":1e-8,"twin_halfedge":37},{"chart_seam":false,"geometry":{"curve":15,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":19,"id":15,"tolerance":1e-8,"twin_halfedge":50},{"chart_seam":false,"geometry":{"curve":16,"kind":"Curve","range":{"hi":1.1339745962155614,"lo":0.0}},"halfedge":20,"id":16,"tolerance":1e-8,"twin_halfedge":36},{"chart_seam":false,"geometry":{"curve":17,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":21,"id":17,"tolerance":1e-8,"twin_halfedge":46},{"chart_seam":false,"geometry":{"curve":18,"kind":"Curve","range":{"hi":1.1339745962155614,"lo":0.0}},"halfedge":22,"id":18,"tolerance":1e-8,"twin_halfedge":38},{"chart_seam":false,"geometry":{"curve":19,"kind":"Curve","range":{"hi":0.8452994616207488,"lo":0.0}},"halfedge":33,"id":19,"tolerance":1e-8,"twin_halfedge":53},{"chart_seam":false,"geometry":{"curve":20,"kind":"Curve","range":{"hi":1.9999999999999998,"lo":0.0}},"halfedge":34,"id":20,"tolerance":1e-8,"twin_halfedge":42},{"chart_seam":false,"geometry":{"curve":21,"kind":"Curve","range":{"hi":1.0000000000000004,"lo":0.0}},"halfedge":35,"id":21,"tolerance":1e-8,"twin_halfedge":47},{"chart_seam":false,"geometry":{"curve":22,"kind":"Curve","range":{"hi":1.0000000000000004,"lo":0.0}},"halfedge":39,"id":22,"tolerance":1e-8,"twin_halfedge":49},{"chart_seam":false,"geometry":{"curve":23,"kind":"Curve","range":{"hi":2.0000000000000004,"lo":0.0}},"halfedge":40,"id":23,"tolerance":1e-8,"twin_halfedge":44},{"chart_seam":false,"geometry":{"curve":24,"kind":"Curve","range":{"hi":0.8452994616207484,"lo":0.0}},"halfedge":41,"id":24,"tolerance":1e-8,"twin_halfedge":51},{"chart_seam":false,"geometry":{"curve":25,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":43,"id":25,"tolerance":1e-8,"twin_halfedge":52},{"chart_seam":false,"geometry":{"curve":26,"kind":"Curve","range":{"hi":2.0,"lo":0.0}},"halfedge":45,"id":26,"tolerance":1e-8,"twin_halfedge":48}],"faces":[{"id":0,"key":"planar-host-0-0","provenance":{"reversed":false,"role":"Preserved","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":0,"key":"bottom"}]},"sense":"Forward","shell_ref":0,"surface":0,"trim":{"chart":0,"holes":[],"outer":0,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":1,"key":"planar-host-1-1","provenance":{"reversed":false,"role":"Preserved","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":1,"key":"top"}]},"sense":"Forward","shell_ref":0,"surface":1,"trim":{"chart":0,"holes":[],"outer":1,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":2,"key":"planar-host-2-2","provenance":{"reversed":false,"role":"Preserved","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":2,"key":"left"}]},"sense":"Forward","shell_ref":0,"surface":2,"trim":{"chart":0,"holes":[],"outer":2,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":3,"key":"planar-host-3-3","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":3,"key":"right"}]},"sense":"Forward","shell_ref":0,"surface":3,"trim":{"chart":0,"holes":[],"outer":3,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":4,"key":"planar-host-4-4","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":4,"trim":{"chart":0,"holes":[],"outer":4,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":5,"key":"planar-host-5-5","provenance":{"reversed":false,"role":"Preserved","sources":[{"body":"matrix-box-host","entity":"matrix-box-host","face":5,"key":"back"}]},"sense":"Forward","shell_ref":0,"surface":5,"trim":{"chart":0,"holes":[],"outer":5,"uv_bounds":[{"hi":3.00000001,"lo":-1e-8},{"hi":3.00000001,"lo":-1e-8}]}},{"id":6,"key":"planar-cut-0-6","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":0,"key":"bottom"}]},"sense":"Forward","shell_ref":0,"surface":6,"trim":{"chart":0,"holes":[],"outer":6,"uv_bounds":[{"hi":2.0000000099999995,"lo":-1.0000000192296269e-8},{"hi":2.000000010000001,"lo":0.99999999}]}},{"id":7,"key":"planar-cut-1-7","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":1,"key":"top"}]},"sense":"Forward","shell_ref":0,"surface":7,"trim":{"chart":0,"holes":[],"outer":7,"uv_bounds":[{"hi":2.0000000100000004,"lo":0.9999999899999998},{"hi":2.0000000100000013,"lo":-9.999999918726034e-9}]}},{"id":8,"key":"planar-cut-3-8","provenance":{"reversed":false,"role":"Preserved","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":3,"key":"right"}]},"sense":"Forward","shell_ref":0,"surface":8,"trim":{"chart":0,"holes":[],"outer":8,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.00000001,"lo":-1e-8}]}},{"id":9,"key":"planar-cut-4-9","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":4,"key":"front"}]},"sense":"Forward","shell_ref":0,"surface":9,"trim":{"chart":0,"holes":[],"outer":9,"uv_bounds":[{"hi":2.0000000100000004,"lo":0.9999999899999996},{"hi":2.00000001,"lo":-1e-8}]}},{"id":10,"key":"planar-cut-5-10","provenance":{"reversed":false,"role":"Split","sources":[{"body":"matrix-rotated-cutter","entity":"matrix-rotated-cutter","face":5,"key":"back"}]},"sense":"Forward","shell_ref":0,"surface":10,"trim":{"chart":0,"holes":[],"outer":10,"uv_bounds":[{"hi":2.00000001,"lo":-1e-8},{"hi":2.0000000100000013,"lo":1.1547005283792517}]}}],"halfedges":[{"edge":0,"face":0,"from":0,"geometry_use":{"pcurve":0,"periodic_lift":[0,0],"sense":"Forward"},"id":0,"loop_ref":0,"next":1,"prev":3,"to":1,"twin":11,"wire_ref":null},{"edge":1,"face":0,"from":1,"geometry_use":{"pcurve":1,"periodic_lift":[0,0],"sense":"Forward"},"id":1,"loop_ref":0,"next":2,"prev":0,"to":2,"twin":31,"wire_ref":null},{"edge":2,"face":0,"from":2,"geometry_use":{"pcurve":2,"periodic_lift":[0,0],"sense":"Forward"},"id":2,"loop_ref":0,"next":3,"prev":1,"to":3,"twin":14,"wire_ref":null},{"edge":3,"face":0,"from":3,"geometry_use":{"pcurve":3,"periodic_lift":[0,0],"sense":"Forward"},"id":3,"loop_ref":0,"next":0,"prev":2,"to":0,"twin":26,"wire_ref":null},{"edge":4,"face":1,"from":4,"geometry_use":{"pcurve":4,"periodic_lift":[0,0],"sense":"Forward"},"id":4,"loop_ref":1,"next":5,"prev":7,"to":5,"twin":24,"wire_ref":null},{"edge":5,"face":1,"from":5,"geometry_use":{"pcurve":5,"periodic_lift":[0,0],"sense":"Forward"},"id":5,"loop_ref":1,"next":6,"prev":4,"to":6,"twin":16,"wire_ref":null},{"edge":6,"face":1,"from":6,"geometry_use":{"pcurve":6,"periodic_lift":[0,0],"sense":"Forward"},"id":6,"loop_ref":1,"next":7,"prev":5,"to":7,"twin":29,"wire_ref":null},{"edge":7,"face":1,"from":7,"geometry_use":{"pcurve":7,"periodic_lift":[0,0],"sense":"Forward"},"id":7,"loop_ref":1,"next":4,"prev":6,"to":4,"twin":9,"wire_ref":null},{"edge":8,"face":2,"from":0,"geometry_use":{"pcurve":8,"periodic_lift":[0,0],"sense":"Forward"},"id":8,"loop_ref":2,"next":9,"prev":11,"to":4,"twin":25,"wire_ref":null},{"edge":7,"face":2,"from":4,"geometry_use":{"pcurve":9,"periodic_lift":[0,0],"sense":"Reverse"},"id":9,"loop_ref":2,"next":10,"prev":8,"to":7,"twin":7,"wire_ref":null},{"edge":9,"face":2,"from":7,"geometry_use":{"pcurve":10,"periodic_lift":[0,0],"sense":"Forward"},"id":10,"loop_ref":2,"next":11,"prev":9,"to":1,"twin":28,"wire_ref":null},{"edge":0,"face":2,"from":1,"geometry_use":{"pcurve":11,"periodic_lift":[0,0],"sense":"Reverse"},"id":11,"loop_ref":2,"next":8,"prev":10,"to":0,"twin":0,"wire_ref":null},{"edge":10,"face":3,"from":8,"geometry_use":{"pcurve":12,"periodic_lift":[0,0],"sense":"Forward"},"id":12,"loop_ref":3,"next":13,"prev":19,"to":9,"twin":32,"wire_ref":null},{"edge":11,"face":3,"from":9,"geometry_use":{"pcurve":13,"periodic_lift":[0,0],"sense":"Forward"},"id":13,"loop_ref":3,"next":14,"prev":12,"to":3,"twin":27,"wire_ref":null},{"edge":2,"face":3,"from":3,"geometry_use":{"pcurve":14,"periodic_lift":[0,0],"sense":"Reverse"},"id":14,"loop_ref":3,"next":15,"prev":13,"to":2,"twin":2,"wire_ref":null},{"edge":12,"face":3,"from":2,"geometry_use":{"pcurve":15,"periodic_lift":[0,0],"sense":"Forward"},"id":15,"loop_ref":3,"next":16,"prev":14,"to":6,"twin":30,"wire_ref":null},{"edge":5,"face":3,"from":6,"geometry_use":{"pcurve":16,"periodic_lift":[0,0],"sense":"Reverse"},"id":16,"loop_ref":3,"next":17,"prev":15,"to":5,"twin":5,"wire_ref":null},{"edge":13,"face":3,"from":5,"geometry_use":{"pcurve":17,"periodic_lift":[0,0],"sense":"Forward"},"id":17,"loop_ref":3,"next":18,"prev":16,"to":10,"twin":23,"wire_ref":null},{"edge":14,"face":3,"from":10,"geometry_use":{"pcurve":18,"periodic_lift":[0,0],"sense":"Forward"},"id":18,"loop_ref":3,"next":19,"prev":17,"to":11,"twin":37,"wire_ref":null},{"edge":15,"face":3,"from":11,"geometry_use":{"pcurve":19,"periodic_lift":[0,0],"sense":"Forward"},"id":19,"loop_ref":3,"next":12,"prev":18,"to":8,"twin":50,"wire_ref":null},{"edge":16,"face":4,"from":9,"geometry_use":{"pcurve":20,"periodic_lift":[0,0],"sense":"Forward"},"id":20,"loop_ref":4,"next":21,"prev":27,"to":12,"twin":36,"wire_ref":null},{"edge":17,"face":4,"from":12,"geometry_use":{"pcurve":21,"periodic_lift":[0,0],"sense":"Forward"},"id":21,"loop_ref":4,"next":22,"prev":20,"to":13,"twin":46,"wire_ref":null},{"edge":18,"face":4,"from":13,"geometry_use":{"pcurve":22,"periodic_lift":[0,0],"sense":"Forward"},"id":22,"loop_ref":4,"next":23,"prev":21,"to":10,"twin":38,"wire_ref":null},{"edge":13,"face":4,"from":10,"geometry_use":{"pcurve":23,"periodic_lift":[0,0],"sense":"Reverse"},"id":23,"loop_ref":4,"next":24,"prev":22,"to":5,"twin":17,"wire_ref":null},{"edge":4,"face":4,"from":5,"geometry_use":{"pcurve":24,"periodic_lift":[0,0],"sense":"Reverse"},"id":24,"loop_ref":4,"next":25,"prev":23,"to":4,"twin":4,"wire_ref":null},{"edge":8,"face":4,"from":4,"geometry_use":{"pcurve":25,"periodic_lift":[0,0],"sense":"Reverse"},"id":25,"loop_ref":4,"next":26,"prev":24,"to":0,"twin":8,"wire_ref":null},{"edge":3,"face":4,"from":0,"geometry_use":{"pcurve":26,"periodic_lift":[0,0],"sense":"Reverse"},"id":26,"loop_ref":4,"next":27,"prev":25,"to":3,"twin":3,"wire_ref":null},{"edge":11,"face":4,"from":3,"geometry_use":{"pcurve":27,"periodic_lift":[0,0],"sense":"Reverse"},"id":27,"loop_ref":4,"next":20,"prev":26,"to":9,"twin":13,"wire_ref":null},{"edge":9,"face":5,"from":1,"geometry_use":{"pcurve":28,"periodic_lift":[0,0],"sense":"Reverse"},"id":28,"loop_ref":5,"next":29,"prev":31,"to":7,"twin":10,"wire_ref":null},{"edge":6,"face":5,"from":7,"geometry_use":{"pcurve":29,"periodic_lift":[0,0],"sense":"Reverse"},"id":29,"loop_ref":5,"next":30,"prev":28,"to":6,"twin":6,"wire_ref":null},{"edge":12,"face":5,"from":6,"geometry_use":{"pcurve":30,"periodic_lift":[0,0],"sense":"Reverse"},"id":30,"loop_ref":5,"next":31,"prev":29,"to":2,"twin":15,"wire_ref":null},{"edge":1,"face":5,"from":2,"geometry_use":{"pcurve":31,"periodic_lift":[0,0],"sense":"Reverse"},"id":31,"loop_ref":5,"next":28,"prev":30,"to":1,"twin":1,"wire_ref":null},{"edge":10,"face":6,"from":9,"geometry_use":{"pcurve":32,"periodic_lift":[0,0],"sense":"Reverse"},"id":32,"loop_ref":6,"next":33,"prev":36,"to":8,"twin":12,"wire_ref":null},{"edge":19,"face":6,"from":8,"geometry_use":{"pcurve":33,"periodic_lift":[0,0],"sense":"Forward"},"id":33,"loop_ref":6,"next":34,"prev":32,"to":14,"twin":53,"wire_ref":null},{"edge":20,"face":6,"from":14,"geometry_use":{"pcurve":34,"periodic_lift":[0,0],"sense":"Forward"},"id":34,"loop_ref":6,"next":35,"prev":33,"to":15,"twin":42,"wire_ref":null},{"edge":21,"face":6,"from":15,"geometry_use":{"pcurve":35,"periodic_lift":[0,0],"sense":"Forward"},"id":35,"loop_ref":6,"next":36,"prev":34,"to":12,"twin":47,"wire_ref":null},{"edge":16,"face":6,"from":12,"geometry_use":{"pcurve":36,"periodic_lift":[0,0],"sense":"Reverse"},"id":36,"loop_ref":6,"next":32,"prev":35,"to":9,"twin":20,"wire_ref":null},{"edge":14,"face":7,"from":11,"geometry_use":{"pcurve":37,"periodic_lift":[0,0],"sense":"Reverse"},"id":37,"loop_ref":7,"next":38,"prev":41,"to":10,"twin":18,"wire_ref":null},{"edge":18,"face":7,"from":10,"geometry_use":{"pcurve":38,"periodic_lift":[0,0],"sense":"Reverse"},"id":38,"loop_ref":7,"next":39,"prev":37,"to":13,"twin":22,"wire_ref":null},{"edge":22,"face":7,"from":13,"geometry_use":{"pcurve":39,"periodic_lift":[0,0],"sense":"Forward"},"id":39,"loop_ref":7,"next":40,"prev":38,"to":16,"twin":49,"wire_ref":null},{"edge":23,"face":7,"from":16,"geometry_use":{"pcurve":40,"periodic_lift":[0,0],"sense":"Forward"},"id":40,"loop_ref":7,"next":41,"prev":39,"to":17,"twin":44,"wire_ref":null},{"edge":24,"face":7,"from":17,"geometry_use":{"pcurve":41,"periodic_lift":[0,0],"sense":"Forward"},"id":41,"loop_ref":7,"next":37,"prev":40,"to":11,"twin":51,"wire_ref":null},{"edge":20,"face":8,"from":15,"geometry_use":{"pcurve":42,"periodic_lift":[0,0],"sense":"Reverse"},"id":42,"loop_ref":8,"next":43,"prev":45,"to":14,"twin":34,"wire_ref":null},{"edge":25,"face":8,"from":14,"geometry_use":{"pcurve":43,"periodic_lift":[0,0],"sense":"Forward"},"id":43,"loop_ref":8,"next":44,"prev":42,"to":17,"twin":52,"wire_ref":null},{"edge":23,"face":8,"from":17,"geometry_use":{"pcurve":44,"periodic_lift":[0,0],"sense":"Reverse"},"id":44,"loop_ref":8,"next":45,"prev":43,"to":16,"twin":40,"wire_ref":null},{"edge":26,"face":8,"from":16,"geometry_use":{"pcurve":45,"periodic_lift":[0,0],"sense":"Forward"},"id":45,"loop_ref":8,"next":42,"prev":44,"to":15,"twin":48,"wire_ref":null},{"edge":17,"face":9,"from":13,"geometry_use":{"pcurve":46,"periodic_lift":[0,0],"sense":"Reverse"},"id":46,"loop_ref":9,"next":47,"prev":49,"to":12,"twin":21,"wire_ref":null},{"edge":21,"face":9,"from":12,"geometry_use":{"pcurve":47,"periodic_lift":[0,0],"sense":"Reverse"},"id":47,"loop_ref":9,"next":48,"prev":46,"to":15,"twin":35,"wire_ref":null},{"edge":26,"face":9,"from":15,"geometry_use":{"pcurve":48,"periodic_lift":[0,0],"sense":"Reverse"},"id":48,"loop_ref":9,"next":49,"prev":47,"to":16,"twin":45,"wire_ref":null},{"edge":22,"face":9,"from":16,"geometry_use":{"pcurve":49,"periodic_lift":[0,0],"sense":"Reverse"},"id":49,"loop_ref":9,"next":46,"prev":48,"to":13,"twin":39,"wire_ref":null},{"edge":15,"face":10,"from":8,"geometry_use":{"pcurve":50,"periodic_lift":[0,0],"sense":"Reverse"},"id":50,"loop_ref":10,"next":51,"prev":53,"to":11,"twin":19,"wire_ref":null},{"edge":24,"face":10,"from":11,"geometry_use":{"pcurve":51,"periodic_lift":[0,0],"sense":"Reverse"},"id":51,"loop_ref":10,"next":52,"prev":50,"to":17,"twin":41,"wire_ref":null},{"edge":25,"face":10,"from":17,"geometry_use":{"pcurve":52,"periodic_lift":[0,0],"sense":"Reverse"},"id":52,"loop_ref":10,"next":53,"prev":51,"to":14,"twin":43,"wire_ref":null},{"edge":19,"face":10,"from":14,"geometry_use":{"pcurve":53,"periodic_lift":[0,0],"sense":"Reverse"},"id":53,"loop_ref":10,"next":50,"prev":52,"to":8,"twin":33,"wire_ref":null}],"loops":[{"face_ref":0,"id":0,"is_hole":false,"start_halfedge":0},{"face_ref":1,"id":1,"is_hole":false,"start_halfedge":4},{"face_ref":2,"id":2,"is_hole":false,"start_halfedge":8},{"face_ref":3,"id":3,"is_hole":false,"start_halfedge":12},{"face_ref":4,"id":4,"is_hole":false,"start_halfedge":20},{"face_ref":5,"id":5,"is_hole":false,"start_halfedge":28},{"face_ref":6,"id":6,"is_hole":false,"start_halfedge":32},{"face_ref":7,"id":7,"is_hole":false,"start_halfedge":37},{"face_ref":8,"id":8,"is_hole":false,"start_halfedge":42},{"face_ref":9,"id":9,"is_hole":false,"start_halfedge":46},{"face_ref":10,"id":10,"is_hole":false,"start_halfedge":50}],"shells":[{"faces":[0,2,5,3,4,1,6,7,10,9,8],"id":0,"is_closed":true}],"vertices":[{"id":0,"outgoing_halfedge":0,"position":[0.0,0.0,0.0],"tolerance":1e-8},{"id":1,"outgoing_halfedge":1,"position":[0.0,0.0,-3.0],"tolerance":1e-8},{"id":2,"outgoing_halfedge":2,"position":[3.0,0.0,-3.0],"tolerance":1e-8},{"id":3,"outgoing_halfedge":3,"position":[3.0,0.0,0.0],"tolerance":1e-8},{"id":4,"outgoing_halfedge":4,"position":[0.0,3.0,0.0],"tolerance":1e-8},{"id":5,"outgoing_halfedge":5,"position":[3.0,3.0,0.0],"tolerance":1e-8},{"id":6,"outgoing_halfedge":6,"position":[3.0,3.0,-3.0],"tolerance":1e-8},{"id":7,"outgoing_halfedge":7,"position":[0.0,3.0,-3.0],"tolerance":1e-8},{"id":8,"outgoing_halfedge":12,"position":[3.0,0.5,-1.654700538379252],"tolerance":1e-8},{"id":9,"outgoing_halfedge":13,"position":[3.0,0.5,0.0],"tolerance":1e-8},{"id":10,"outgoing_halfedge":18,"position":[3.0,2.5,0.0],"tolerance":1e-8},{"id":11,"outgoing_halfedge":19,"position":[3.0,2.5,-1.654700538379252],"tolerance":1e-8},{"id":12,"outgoing_halfedge":21,"position":[1.8660254037844386,0.5,0.0],"tolerance":1e-8},{"id":13,"outgoing_halfedge":22,"position":[1.8660254037844386,2.5,0.0],"tolerance":1e-8},{"id":14,"outgoing_halfedge":34,"position":[3.7320508075688776,0.5,-1.232050807568877],"tolerance":1e-8},{"id":15,"outgoing_halfedge":35,"position":[2.7320508075688776,0.5,0.5000000000000002],"tolerance":1e-8},{"id":16,"outgoing_halfedge":40,"position":[2.7320508075688776,2.5,0.5000000000000002],"tolerance":1e-8},{"id":17,"outgoing_halfedge":41,"position":[3.7320508075688776,2.5,-1.2320508075688779],"tolerance":1e-8}],"wires":[]}}}} \ No newline at end of file diff --git a/main/opengeometry/tests/fixtures/golden/aarch64-apple-darwin.json b/main/opengeometry/tests/fixtures/golden/aarch64-apple-darwin.json index 1f59b51f..fc949e06 100644 --- a/main/opengeometry/tests/fixtures/golden/aarch64-apple-darwin.json +++ b/main/opengeometry/tests/fixtures/golden/aarch64-apple-darwin.json @@ -222,8 +222,8 @@ "booleans.matrix.cylinder-offset": "6857b607947dc823d23280ac2a7ce9fb014e939ce37785a40f05ff51a26ca243", "booleans.matrix.cylinder-sliced": "87d109122c1c383c90421c8fbcce2d31cbf3f2f25654460cbdea041fafc3d175", "booleans.matrix.planar-extrusions": "5e7d3466e930688f454d33f75575856633104232e4d768a91cd46df3c0838332", - "booleans.matrix.rotated-box-intersection": "8ff89e8fb45061169615439444fd8bcba05fcce79b9321ec752ddd1b8deee21f", - "booleans.matrix.rotated-box-union": "4515ec1c8edc2d0bd509d91839f33f955015ef9107e2718682fe2c57000e3489", + "booleans.matrix.rotated-box-intersection": "f8f7865294a54262b5185690a1402cf71cd9d11f9edc830dccdf1726b55faca3", + "booleans.matrix.rotated-box-union": "5c67020529c1e871b6c5a69bd21c3e290b6eb968ac0aa561efb9d8c272812750", "booleans.matrix.sphere-contained": "da881786860c019848c83cfcc54ac2171f3c8cbe4b3e063ba4e1f565e9a54776", "booleans.matrix.torus-contained": "1ab0f5f8c7d85f09ae0f41699ab6a8d2f577618d0e2b6a25549f335c0ce6976d", "booleans.matrix.wall-rectilinear": "ecf494cd48f3687d5ebdf8405bca1770e31694bf6adfb67d83ff694c6496272a", @@ -549,8 +549,8 @@ "graph.scenario.reparenting": "a98d8485874040b931f50155a2c61ab0f08b11a17b5cdc017d233d099c426921", "graph.scenario.shared-rebuild": "b780cbc13d3a6cd62a50c332b04368fd8d5e74d577bc9f24845740474fa52273", "graph.scenario.subtree-dispose": "3393656291d922a7f7206b51849264b6e68092170b8a6f67849135ea6179f00b", - "handlers-covered": "786eb944d6f4ae8bb198fa31a8ed05290b2d8940aff4acaaeed14f2408010868", - "handlers-expected": "9c692231727b9c7acfc2c0303b66ccb7ec4a3b3c3d014ad848c8fc2a64ee83c5", + "handlers-covered": "3b879c7ba799f678660ebfab0d91220039d0f534a9afc0efe6ad0aa012e529d2", + "handlers-expected": "d2a2a8fdbac778a10faba5007ca1da75d1f63e07231c5ba4f5f44620cb57804e", "handlers-uncovered": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "level.step.metre.y": "3bfe75db34f9ee266e502be4b3b641abd5d26b3a51c1bf8657f41252ee614f49", "level.step.metre.z": "6108f7b562c88424cd12180dcb82584d60810117a73b36b6ac410bfcdbabb0c2", diff --git a/main/opengeometry/tests/step_oracle/fixtures.rs b/main/opengeometry/tests/step_oracle/fixtures.rs index b9c30868..df19b6c8 100644 --- a/main/opengeometry/tests/step_oracle/fixtures.rs +++ b/main/opengeometry/tests/step_oracle/fixtures.rs @@ -172,7 +172,7 @@ fn exported_matrix_rows( fn boolean_matrix_results_export_through_the_oracle_or_pin_an_error() { assert_eq!( exported_matrix_rows("boolean-matrix", 18, &[], &["cylinder-empty"]), - 15 + 17 ); }