The truck crates leave the workspace; the rig and threads run on SMLib alone

truck-modeling, -topology, -shapeops, -stepio and the vendored truck-geometry, -meshalgo,
-shapeops, -stepio and matext4cgmath (with their attribution rows and vendor notes) are removed.
The kernel-cases runner has one kernel, SMLib, and no build-mismatch check; a modelled thread is
cut by SMLib's exact screw-swept groove (the faceted groove path is gone).

(not built: base in progress)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 07:23:52 -04:00
parent 08f061b02f
commit 166ebe2ee9
99 changed files with 12 additions and 29968 deletions
-5
View File
@@ -607,11 +607,6 @@ copied into SolidCraft.
| Directory | Source | Licence | Changes |
|---|---|---|---|
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, collapsed-boundary projection, subdivision and provisional fixed-grid previews (vendor/README.md) |
| vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) |
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro; corner choice at vertices several intersection curves end on; reuse of an operand's meshes (vendor/README.md) |
| vendor/truck-geometry | truck-geometry 0.5.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | intersection curves divided from their neighbours' surface parameters; surface second derivatives (vendor/README.md) |
| vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) |
## Vendored renderer compatibility fix
+4 -17
View File
@@ -47,13 +47,7 @@ sha2 = "0.10"
moxcms = { version = "0.8.1", default-features = false, features = ["extended_range"] }
semver = "1"
# geometry kernel (Apache-2.0; see plan/adr/0001-geometry-kernel.md). Only crates/kernel uses it.
truck-modeling = "0.6"
truck-shapeops = { version = "0.4", default-features = false }
truck-stepio = { version = "0.3", default-features = false }
truck-topology = "0.6"
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0); already in the
# tree through truck-meshalgo.
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0).
spade = { version = "2.15", default-features = false, features = ["std"] }
rayon = "1.12"
@@ -126,9 +120,7 @@ opt-level = 2
opt-level = 1
# Tests and lints (`cargo xtask ci`, GitHub CI): quick to build, quick enough to run. Light
# optimisation, line tables only (file:line in backtraces), no LTO. Debug assertions are off because
# truck's debug-only checks reject valid geometry (seam edges, closed edges); that was the only
# reason the tests used to build with --release, which cost most of CI's time.
# optimisation, line tables only (file:line in backtraces), no LTO, no debug assertions.
[profile.ci]
inherits = "dev"
opt-level = 1
@@ -151,14 +143,9 @@ codegen-units = 16
# binary). See docs/crash-reports.md.
debug = "line-tables-only"
# truck-shapeops with SolidCraft fixes (Apache-2.0, see vendor/truck-shapeops and ATTRIBUTION.md).
[patch.crates-io]
truck-geometry = { path = "vendor/truck-geometry" }
truck-shapeops = { path = "vendor/truck-shapeops" }
truck-stepio = { path = "vendor/truck-stepio" }
truck-meshalgo = { path = "vendor/truck-meshalgo" }
matext4cgmath = { path = "vendor/matext4cgmath" }
# Vendored crates with SolidCraft fixes (see vendor/README.md and ATTRIBUTION.md).
[patch.crates-io]
# Keep OpenGL pipelines, resolve textures and CAD callbacks at the same safe sample count.
egui-wgpu = { path = "vendor/egui-wgpu" }
+3 -27
View File
@@ -9,18 +9,9 @@ use serde_json::{Value, json};
use crate::oracle::{ABS_FRAC, Checks, REL_TOL, totals, v3};
use crate::{read_json, recipe};
/// The kernel this process runs on (`SOLIDCRAFT_KERNEL`, set by the caller).
/// The kernel the cases run on: SMLib, the only one.
fn kernel() -> String {
kernel_name(std::env::var("SOLIDCRAFT_KERNEL").ok().as_deref())
}
/// The rig's name for a `SOLIDCRAFT_KERNEL` setting: unset, empty or `truck` (all of which the
/// kernel runs as truck) is `default`, the name `boolean_kernel` reports for truck.
fn kernel_name(setting: Option<&str>) -> String {
match setting.map(str::trim) {
None | Some("") | Some("truck") => "default".into(),
Some(other) => other.into(),
}
"smlib".into()
}
/// How many cases a chain of `base` recipes may go through (a ladder case builds on the one
@@ -165,17 +156,11 @@ fn check_case(dir: &str) -> Value {
return out;
}
};
if kernel() == "smlib" && !solidcraft_engine::kernel::smlib_built() {
if !solidcraft_engine::kernel::smlib_built() {
out["status"] = json!("unsupported");
out["error"] = json!("this build has no SMLib: build solidcraft-cli with --features smlib");
return out;
}
let running = solidcraft_engine::kernel::boolean_kernel();
if kernel() != running {
out["status"] = json!("unsupported");
out["error"] = json!(format!("this build runs booleans on the {running} kernel, not {}", kernel()));
return out;
}
// `refused`: only a clean refusal is right; `solid_or_refused`: a clean refusal, or a result
// graded as usual (where the geometry has no manifold answer, such as bodies touching along a
// curve only).
@@ -380,15 +365,6 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn truck_named_explicitly_is_the_default_kernel() {
// `SOLIDCRAFT_KERNEL=truck` runs truck, so the rig must not call it a build mismatch.
for setting in [None, Some(""), Some(" "), Some("truck")] {
assert_eq!(kernel_name(setting), "default");
}
assert_eq!(kernel_name(Some("smlib")), "smlib");
}
#[test]
fn a_base_cycle_is_an_error_not_a_hang() {
let root = std::env::temp_dir().join(format!("kernel-cases-cycle-{}", std::process::id()));
+5 -16
View File
@@ -7275,22 +7275,11 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
None => (input, cyl.radius),
};
let open = ((t.start - cyl.start).abs() < tol, (t.end - cyl.end).abs() < tol);
let (nb, groove_faces) = match kernel::backend()? {
kernel::Backend::Truck => {
let groove = thread_groove(&t, radius, open)?;
// The B-rep boolean can't take helical walls in reasonable time: the body is cut
// faceted (curved faces within a two-hundredth of the pitch).
let nb = kernel::faceted_boolean(&input.body, &groove, BoolOp::Cut, t.pitch * 0.005)?;
(nb, groove.triangles.len())
}
kernel::Backend::Smlib => {
// SMLib cuts the exact groove: its screw sweep, a B-rep boolean.
let g = thread_groove_shape(&t, radius, open)?;
let section = Region2 { outer: solidcraft_geom::Loop2::polygon(&g.section), holes: Vec::new() };
let groove = kernel::sweep_helix(&g.plane, &section, g.base, g.axis, g.pitch, g.turns)?;
(kernel::boolean(&input.body, &groove, BoolOp::Cut)?, groove.face_count())
}
};
// The exact groove: SMLib's screw sweep, cut by a B-rep boolean.
let g = thread_groove_shape(&t, radius, open)?;
let section = Region2 { outer: solidcraft_geom::Loop2::polygon(&g.section), holes: Vec::new() };
let groove = kernel::sweep_helix(&g.plane, &section, g.base, g.axis, g.pitch, g.turns)?;
let (nb, groove_faces) = (kernel::boolean(&input.body, &groove, BoolOp::Cut)?, groove.face_count());
let nb = nb.ok_or_else(|| DocError::Invalid("the thread removed everything".into()))?;
// The groove's faces have no role to name them by.
let groove = crate::lineage::Lineage::unnamed_faces(groove_faces);
-266
View File
@@ -4,272 +4,6 @@ Copies of third-party crates that SolidCraft builds with local fixes, wired in w
`[patch.crates-io]` in the workspace `Cargo.toml`. They keep their own licence; changed lines
are marked `SolidCraft:` in the source.
## truck-shapeops 0.4.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/truck)
Changes:
- `transversal/intersection_curve/mod.rs`: reject empty/single-point polylines and
collapsed or non-finite segments before endpoint indexing and normalization. A hidden
constructor re-export lets the kernel workspace tests exercise the actual vendored code.
- `transversal/integrate/mod.rs`: mesh both shells with `robust_triangulation` (faces made by
earlier booleans have boundaries that lie on their surface only within tolerance).
- `transversal/divide_face/mod.rs`: when projecting a loop onto a face's surface, fall back to
the nearest parameter when the exact search fails (intersection curves are approximations).
- `transversal/integrate/mod.rs`: the result's shells and faces keep the order of the shell
they come from (truck's connected components followed hash order of face addresses, so a
boolean's faces came out in a different order on every run).
- `transversal/loops_store/mod.rs`: a point at an end of a boundary curve (within the point
tolerance) is that end, whatever its parameter: a polyline's parameter counts segments, so
an intersection curve ending 2e-7 mm from another's end could read as inside it, cut it
into a sliver with a vertex of its own, and leave the result's shell open (#391).
- `transversal/loops_store/mod.rs`: an intersection edge added to a face at a vertex that more
than one wire visits (several curves end on it: the touching point of two equal cylinders) goes
into the corner that holds its direction, swept counterclockwise about the face's normal from
the wire's outgoing edge to the way back along its incoming one (`add_edge_by_angle`, told on
the polygon loops and followed by the geometry loops). Taking the first visit of each end
joined wires that are different regions of a face and left a wire visiting a vertex twice.
With no direction to go by, the edge goes where both of its ends are in one wire.
- `transversal/divide_face/mod.rs`: a face piece whose wire is not simple makes the operation
give up (`Face::try_new`) instead of panicking.
- `transversal/divide_face/mod.rs`: a loop whose polygon in the face's parameters has no area
(a small hole's rim divided at the tolerance of a much larger operand: one chord per half
circle) is divided again, finer, before its area's sign tells a hole from a face piece. The
sign of a zero area was random: the hole became a face of its own, inside out, and the face it
was cut in stayed whole.
- `lib.rs`: `tr!` reports where an operation gives up when `SHAPEOPS_TRACE` is set; compiler
warnings no longer fail the build.
- Face and edge history (`transversal/history.rs`, new): `and_with_history` / `or_with_history`
also return, for each face of the result (in `face_iter` order), the operand face it is a
region of and whether it is that face unchanged, and for each edge (distinct edges in order
of first use along the faces) the operand edge it is (a piece of) or the two operand faces
whose intersection made it. The code that creates faces and edges records them as it goes:
`divide_face` (face pieces), `loops_store` (intersection edges, split edges, edges rebuilt on
a moved vertex), across the passes that fold in the operands' extra shells. `and` / `or` are
wrappers that drop the history; the `*_with_history` forms return the same solid with the
history as an `Option` (`None` where a face or edge went unrecorded).
- `transversal/integrate/mod.rs`: `meshes` and `and_with_history_meshed` take the first
operand's shell meshes made beforehand (copied for each use, since the loops stores cut their
edges): a large body cut down to the regions of many small ones is meshed once, which was most
of each operation's time.
- `transversal/integrate/mod.rs`: in an AND, a face no intersection divides (a shell that meets
nothing of the other) is placed by the crossings of a ray with the other shell counted as each
face is oriented, not by its surface alone, and an inward shell (a void, a cut's inverted tool)
holds what lies outside it. Faces with reversed surfaces (a revolved body's) counted the wrong
way, and a void dropped every face outside it: a cut beside a sealed void came out empty or
with the body gone. An OR keeps truck's count.
## truck-geometry 0.5.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/truck)
Changes:
- `decorators/intersection_curve.rs`: `IntersectionCurve::parameter_division` divides as
`algo::curve::parameter_division` does (the same split points, in the same order), but each
point's projection onto the two surfaces starts from the parameters of the point that begins
its piece, instead of a search of the whole surfaces (a grid pre-search of a B-spline surface
for every point: a small hole on a large curved face took seconds, #1038). A hinted result
farther from the leader's point than the curve's tolerance (the projection settled on another
part of a surface, such as the far side of a small hole) is dropped for the search from
nothing, as before. `crates/kernel/src/issue_1038_tests.rs` holds it to that search.
- `decorators/revolved_curve.rs`: a point on a revolved surface's seam (angle 0, the same place
as 2π) takes the end of the period nearer the hinted angle. The angle was decided by the sign of
a cross product that is rounding noise there, so a boundary followed point by point could come
back to the seam at the other end of the period and fold its parameter polygon over itself (no
intersection loop then lay inside the face). `crates/kernel/src/seam_faces_tests.rs` holds it
to a cone read from STEP.
- `nurbs/bspsurface.rs`: `uuder` and `vvder` multiply by p·(p − 1), as `BSplineCurve::der2`
does. They multiplied by p only, so the second derivatives of every B-spline and NURBS surface
of degree p ≥ 3 were 1/(p − 1) of their value (a cubic surface's curvature read as half, #763).
`crates/kernel/src/surface_second_derivative_tests.rs` holds them to finite differences.
- `decorators/intersection_curve.rs`: `IntersectionCurve::parameter_division` with a tolerance
below truck's `TOLERANCE` (1e-6), or one that is not a number, divides at `TOLERANCE` instead
of panicking in truck's tolerance assertion; a join failed on that panic (#4623).
`crates/kernel/src/issue_1038_tests.rs` holds it to the division at 1e-6.
- `decorators/intersection_curve.rs`: where the hinted projection behind `parameter_division`
and the search from nothing both fail, the leader's point stands in, the division stops, and
the failure is recorded for `take_projection_failure`. It fell back to `subs`, which unwraps
that same failed search: a revolve cut whose sphere's pole sat on a box's edge panicked
dividing an edge (#5562).
- truck-shapeops `transversal/divide_face/mod.rs`: an edge whose division recorded such a
failure gives up the face division (`None`), so the Boolean attempt fails as quickly as the
panic made it, and the kernel goes on to its other tries. Carrying the approximate points on
made one test's Boolean run for minutes.
## truck-stepio 0.3.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/truck)
Only the writer (`out`) is used; SolidCraft reads STEP with its own reader.
Changes:
- `out/geometry.rs`: an `INTERSECTION_CURVE`'s second surface was written at the first
surface's entity index, so every boolean-made edge produced duplicate entity ids.
- `out/geometry.rs`: surfaces of revolution carry the right `same_sense` for inverted and
mirrored revolutions (ISO 10303-42 parameterises them by angle then profile, so their normal
is the opposite of truck's; a mirroring transform reverses it again), and the modeling-surface
wrapper passes the revolution's sense through instead of always `true`.
- `out/geometry.rs`: a surface of revolution under a transform that commutes with turning about
its axis (a scale across the axis that differs from the scale along it, as a shell or offset
makes of a revolved face) is written as the transformed profile revolved about the
transformed axis. Only uniform scales were taken. Other transforms are still refused: the
writer stops with a formatting error, which SolidCraft's `step.rs` now reports as an export
error (`to_string` panicked on it).
- `lib.rs`: compiler warnings no longer fail the build. Example and test targets are dropped
(their sources are not vendored).
## truck-meshalgo 0.4.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/truck)
Changes:
- `tessellation/triangulation.rs`: a boundary segment that crosses constraints already added
is split where it crosses by `add_split_constraint` (a vertex at each crossing, the pieces
added where they can be), not by spade's `add_constraint_and_split`, which asserts when a
split lands so near another vertex or constraint that the piece up to it is blocked. That
assertion lost whole bodies and operations (#4031, #4598); a blocked piece is now left out, as
truck did before #1653.
- `tessellation/triangulation.rs`, `closed.rs`: retry nearest trim projections outside a
spline's knot domain without the preceding point's hint, preferring a bounded fit within
the caller's chord tolerance, with a bounded damped least-squares fallback where
Newton's method diverges. Affine plane charts remain unbounded; modelling surfaces may
retain genuine continuation when no bounded fit is accurate enough. Geometrically closed,
smooth spline seams get a local periodic chart during meshing, with wrapped evaluation,
so a trim crossing the seam stays a small patch. The stored geometry is unchanged (#1974).
- `tessellation/mod.rs`, `triangulation.rs`: a separate provisional `preview_face`
path accepts at most 4,096 already sampled boundary points and a fixed grid of
at most 16 subdivisions per direction, including periodic seam connections.
It does not run adaptive subdivision or grid balancing. The kernel caller bounds
projection complexity and total allocation before using it; ordinary modelling
tessellation keeps its existing accuracy policy.
- `tessellation/triangulation.rs`: exact, robust and provisional-preview projections prefer an
equivalent in-domain parameter at collapsed surface edges, and snap to exact bounded
endpoints only at collapsed edges when that improves the spatial fit within the existing
search tolerance. Closed spline seams retain their distinct parameter endpoints.
This prevents huge arbitrary pole parameters from corrupting trimmed meshes (#796),
while retaining genuine extrapolation for unbounded planes. Work is bounded to four
endpoint evaluations per projected point; no search or tessellation tolerance is relaxed.
- `tessellation/triangulation.rs`: a boundary loop is projected onto its surface point by point,
each searched from the one before; the first has no hint. When that walk fails, it is tried
once more from the first point an unhinted search finds. An unhinted search can miss a point
that lies on the surface, such as the corner of a fillet strip on a surface extended past it,
and the face was then left unmeshed, which also failed every boolean with the body (#882). A
walk that succeeds is kept as it was: where a loop starts decides how it is laid out across a
periodic surface's seam.
- `tessellation/mod.rs`, `triangulation.rs`: edges are sampled through a new `TrySubs` trait
(`try_subs`, `approx_subs`), a bound of `PolylineableCurve`, instead of `subs`. truck's
`IntersectionCurve::subs` unwraps a projection onto its two surfaces that can fail to
converge, which panicked while meshing (#1252). A failed sample at an interval's end uses the
intersection curve's leader point, so the polyline stays finite; a failed interior probe is
NaN, which already counts as flat. `truck-shapeops`' `ShapeOpsCurve` carries the same bound,
and `truck-modeling` is now a regular dependency (it was a dev-dependency).
- `tessellation/triangulation.rs`: the inside test of a face's boundary skips loops whose
parameter box is clear of the point (it tested every segment of every loop, so a face with
hundreds of holes meshed in quadratic time).
- `tessellation/triangulation.rs`: meshed faces and edges are built unchecked: faces whose loop
runs along a seam edge both ways and closed edges (a full circle on one vertex) are valid,
but the debug-build checks rejected them (debug builds could not mesh them).
- `tessellation/triangulation.rs`: on a periodic surface, a boundary point's projected parameter
is moved by whole periods to within half a period of the point before it, however many periods
away the search put it (it was moved by two periods at most). A hinted search on a revolved
surface can return an angle millions of turns from its hint; the loop then spanned those turns,
the polyline closing it across the seam had millions of points, and meshing never finished,
which hung the STEP export that reads its own file back (#1985).
- `tessellation/triangulation.rs`: parameter searches that land outside a surface's bounded,
non-periodic domain are rejected (a hinted B-spline search can run off and extrapolate to
thousands of parameter units, and meshing that grid never finishes); the last-resort nearest
search is clamped into the domain.
- `tessellation/triangulation.rs`, bounded work (hostile files must not hang): edge polylines
and surface grids come from our own bounded divisions (at most 20 000 points per edge, a
grid budget per face shared out of 10 million points per shell, 24 refinement rounds; a
point that does not evaluate counts as flat instead of recursing). Surface grids refine only
the direction that bends, and a ruling left undivided gets cells comparable to the other
direction (long triangles from a trim loop to the grid's far ends cut through cylinders).
- `tessellation/triangulation.rs`: an edge's division evaluates each parameter once. A piece
carries the points of its ends and its midpoint is an end of both halves: 2 evaluations per
piece instead of 4, and the same polyline (a point of an intersection curve costs two grid
searches of its surfaces). `bounded_polyline` and `Deflection::turn2`/`turns` are public but
hidden, for the kernel's regression against the old division.
- `tessellation/mod.rs`, `tessellation/triangulation.rs`, for display: a `Deflection` is the
chord tolerance plus an optional bound on the angle one facet spans, and
`RoundMeshableShape::robust_triangulation_within` divides by it: an edge or a grid direction
within the chord tolerance is still halved while a piece turns more than the angle, until its
sag is under a floor (a radius that is small beside the tolerance was one flat facet).
Bounded work: a face that would be bounded by more than 4096 segments has its curved edges
divided at twice the angle, and again, down to the chord tolerance alone, and the angle
stops refining a face's grid at 16 384 points. Each edge is still divided once for both its
faces. `triangulation` and `robust_triangulation` divide by the chord tolerance alone, as
before.
- `analyzers/collision.rs`: the collision search builds each triangle's box once, filters the
sweep's open list by box overlap as it goes (it cloned the list for every triangle and built
both boxes for every pair), and runs the exact triangle tests on the rayon pool, in order
(sequential on wasm). Same collisions in the same order. The old search is kept as
`analyzers::collision_test_hooks`, behind a `test-hooks` feature that only
`solidcraft-kernel`'s tests turn on, and `crates/kernel/tests/mesh_collision_equivalence.rs`
compares the two.
- `tessellation/triangulation.rs`: the inside test of a face's boundary also skips runs of 32
segments whose box, grown by the tolerance, the test's ray cannot meet. Same answers. The
old test is kept as `tessellation::include_test_hooks`, behind a `test-hooks` feature that
only `solidcraft-kernel`'s tests turn on, and `crates/kernel/tests/tess_include_equivalence.rs`
compares the two.
- `tessellation/triangulation.rs`: the constrained Delaunay triangulation of a trimmed face
works in (u, v) scaled by the surface's mean speed along each (a power of two, so exact both
ways). In raw parameters, a loft side ruled across (about 30 mm) and long along the loft
(about 600 mm) is a strip stretched twentyfold, and a trimming node was fanned to boundary
nodes hundreds of millimetres away: a flat sheet through a joined tube.
- `tessellation/triangulation.rs`: every segment of a face's boundary becomes a constraint; one
that crosses or overlaps a constraint already added (an intersection edge's polyline can hook
back a few micrometres past its vertex) is split where they meet. truck dropped such a
segment and joined its start to the first later node it could reach, which on a joined tube
left a trimming curve's 130 nodes behind one constraint across the face: slivers from it
sagged millimetres off the surface, and a face pinched to a point (#1648) got no triangles.
All meshes; faces whose segments all go in mesh as before.
- `tessellation/mod.rs`: `Deflection::rulings` is now `Deflection::display`: a mesh to display
or export (the kernel's `tessellate` and display meshes), which gets the ruling grid lines,
and the speed-scaled triangulation above. The meshes a Boolean operation intersects
(`Deflection::chord`) keep truck's (u, v) and insertion order.
- `lib.rs`: compiler warnings no longer fail the build. Example, test and bench targets are
dropped (their sources are not vendored).
- `tessellation/triangulation.rs`: adaptive division also splits a cell that bows off its
corners' plane by more than the chord tolerance (a quarter of its twist `p00 - p01 - p10 +
p11`, across that plane, at the middle). A bilinear patch equals the blend of its corners the
division compared against, so a twisted ruled face (a loft between polylines) went out as two
triangles per face (#1666); a flat cell such as a cone's trapezoid between rulings is not split.
- `tessellation/triangulation.rs`: a shell's face meshes share their edges' points exactly. A
face's mesh puts each point at its parameters on the face's own surface, so a point of an edge
that lies off a surface within tolerance (an intersection-curve sample whose projection did not
converge, an imported edge) came out in a different place on each of the edge's faces, and the
exported mesh had a crack there. In display and export meshes (`Deflection::display`), a
vertex at a point of a face's boundary loops that is farther than `TOLERANCE` from the nearest
point of the face's edges now goes to that point, when it is within the chord tolerance
(`onto_edges`). Points within `TOLERANCE` stay on the surface, so exact geometry meshes as
before. The meshes Boolean operations intersect keep truck's points.
- `tessellation/triangulation.rs`: in display and export meshes (`Deflection::display`), a
direction left undivided along the rulings of a face ruled one way also gets a grid line
through each point of the face's boundary that lies inside the face along it
(`with_boundary_lines`), when there are at most 16 such points: corners standing into the
face, not a boundary curve running through it, which the triangulation already follows. A hole in a cone or cylinder smaller than one cell (a notch into a
chamfer) or a trim crossing the rulings of a narrow band (an oblique plane through a bore's
chamfer) had no grid point beside it, and the triangulation fanned from it to the far ends of
the face, many degrees round: those triangles cut through the surface and folded over each
other (a cone face with a small hole meshed several mm² too large, and the boolean that made
it was refused for its volume).
- `tessellation/triangulation.rs`: a face that comes out of the triangulation with no triangle
at all, though the points of its one boundary loop enclose an area in space, is given the
triangles of that loop (ears clipped on the plane of the loop, no point added, so the faces on
either side of its edges still meet it). Its two edges can be different curves that, sampled
as they are, run along the same line in the face's parameters (a lens thinner than the
tolerance), and the loop then has no area there. The shell's mesh had a hole. Faces that
mesh are untouched, and so are preview meshes (they have their own step grid). Only a face
with one loop of at most 256 points is filled, and not one whose loop crosses itself, whose
ears would overlap (the pinched face of #1648 is one): it stays as it was.
`crates/kernel/src/thin_face_tests.rs` holds it, `crates/kernel/tests/tess_loop_fill.rs` the
ear clipping (through the `test-hooks` feature).
## matext4cgmath 0.1.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/matext4cgmath)
truck-geometry uses it for a matrix's operator norm (an intersection curve scales its tolerance
by it whenever it is moved).
Changes:
- `solver.rs`: the Newton refinement of cubic and quartic roots stops after 100 steps. Near a
double root it could cycle forever: moving a body whose edges a boolean made (the retries
move both bodies and the result back) hung the app.
- `lib.rs`: compiler warnings no longer fail the build. Test targets are dropped (their sources
are not vendored).
## egui-wgpu 0.36.2 (MIT OR Apache-2.0, Emil Ernerfeldt and contributors)
Original package sources from https://github.com/emilk/egui/tree/0.36.2/crates/egui-wgpu,
-40
View File
@@ -1,40 +0,0 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "matext4cgmath"
version = "0.1.0"
authors = ["Yoshinori Tanimura <tanimura@ricos.co.jp>"]
description = "Unofficial third-party cgmath extensions for calculate eigenvalues, operator norms and Iwasawa decomposition."
homepage = "https://github.com/ricosjp/matext4cgmath"
keywords = [
"cgmath",
"graphics",
"linear-algebra",
]
categories = ["graphics"]
license = "Apache-2.0"
repository = "https://github.com/ricosjp/matext4cgmath"
resolver = "2"
autotests = false
autoexamples = false
autobenches = false
[dependencies.cgmath]
version = "0.18.0"
[dependencies.katexit]
version = "0.1.2"
[dependencies.num-complex]
version = "0.4.0"
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 Research Institute for Computational Science Co. Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-5
View File
@@ -1,5 +0,0 @@
# matext4cgmath
[![Crates.io](https://img.shields.io/crates/v/matext4cgmath.svg)](https://crates.io/crates/matext4cgmath) [![Docs.rs](https://docs.rs/matext4cgmath/badge.svg)](https://docs.rs/matext4cgmath)
Unofficial third-party cgmath extensions for calculate eigenvalues, operator norms and Iwasawa decomposition.
-153
View File
@@ -1,153 +0,0 @@
use crate::*;
impl<F: BaseFloat> EigenValues for Matrix2<F> {
type EigenValues = [Complex<F>; 2];
fn eigenvalues(self) -> [Complex<F>; 2] {
solver::solve_quadratic(-self.trace(), self.determinant())
}
}
impl<F: BaseFloat> EigenValues for Matrix3<F> {
type EigenValues = [Complex<F>; 3];
fn eigenvalues(self) -> [Complex<F>; 3] {
let poly2 = self[0][0] * self[1][1] + self[0][0] * self[2][2] + self[1][1] * self[2][2]
- self[0][1] * self[1][0]
- self[1][2] * self[2][1]
- self[0][2] * self[2][0];
solver::solve_cubic(-self.trace(), poly2, -self.determinant())
}
}
impl<F: BaseFloat> EigenValues for Matrix4<F> {
type EigenValues = [Complex<F>; 4];
fn eigenvalues(self) -> [Complex<F>; 4] {
let poly2 = self[0][0] * self[1][1]
+ self[0][0] * self[2][2]
+ self[0][0] * self[3][3]
+ self[1][1] * self[2][2]
+ self[1][1] * self[3][3]
+ self[2][2] * self[3][3]
- self[0][1] * self[1][0]
- self[0][2] * self[2][0]
- self[0][3] * self[3][0]
- self[1][2] * self[2][1]
- self[1][3] * self[3][1]
- self[2][3] * self[3][2];
let poly3 = (self[0][0] * self[1][1] + self[0][0] * self[2][2] + self[1][1] * self[2][2]
- self[0][1] * self[1][0]
- self[0][2] * self[2][0]
- self[1][2] * self[2][1])
* self[3][3]
+ (self[2][0] * self[3][2] - self[2][2] * self[3][0] + self[1][0] * self[3][1]
- self[1][1] * self[3][0])
* self[0][3]
+ (self[2][1] * self[3][2] - self[2][2] * self[3][1] + self[3][0] * self[0][1]
- self[3][1] * self[0][0])
* self[1][3]
+ (self[3][1] * self[1][2] - self[3][2] * self[1][1] + self[3][0] * self[0][2]
- self[3][2] * self[0][0])
* self[2][3]
+ Matrix3::from_cols(self[0].truncate(), self[1].truncate(), self[2].truncate())
.determinant();
solver::solve_quartic(-self.trace(), poly2, -poly3, self.determinant())
}
}
impl<F: BaseFloat> OperatorNorm for Matrix2<F> {
#[inline]
fn norm_l1(self) -> F {
F::max(
F::abs(self[0][0]) + F::abs(self[0][1]),
F::abs(self[1][0]) + F::abs(self[1][1]),
)
}
#[inline]
fn norm_linf(self) -> F {
F::max(
F::abs(self[0][0]) + F::abs(self[1][0]),
F::abs(self[0][1]) + F::abs(self[1][1]),
)
}
#[inline]
fn norm_l2(self) -> F {
let eigens = (self.transpose() * self).eigenvalues();
F::sqrt(F::max(eigens[0].re, eigens[1].re))
}
}
impl<F: BaseFloat> OperatorNorm for Matrix3<F> {
#[inline]
fn norm_l1(self) -> F {
F::max(
F::max(
F::abs(self[0][0]) + F::abs(self[0][1]) + F::abs(self[0][2]),
F::abs(self[1][0]) + F::abs(self[1][1]) + F::abs(self[1][2]),
),
F::abs(self[2][0]) + F::abs(self[2][1]) + F::abs(self[2][2]),
)
}
#[inline]
fn norm_linf(self) -> F {
F::max(
F::max(
F::abs(self[0][0]) + F::abs(self[1][0]) + F::abs(self[2][0]),
F::abs(self[0][1]) + F::abs(self[1][1]) + F::abs(self[2][1]),
),
F::abs(self[0][2]) + F::abs(self[1][2]) + F::abs(self[2][2]),
)
}
#[inline]
fn norm_l2(self) -> F {
let eigens = (self.transpose() * self).eigenvalues();
F::sqrt(F::max(F::max(eigens[0].re, eigens[1].re), eigens[2].re))
}
}
impl<F: BaseFloat> OperatorNorm for Matrix4<F> {
#[inline]
fn norm_l1(self) -> F {
F::max(
F::max(
F::max(
F::abs(self[0][0])
+ F::abs(self[0][1])
+ F::abs(self[0][2])
+ F::abs(self[0][3]),
F::abs(self[1][0])
+ F::abs(self[1][1])
+ F::abs(self[1][2])
+ F::abs(self[1][3]),
),
F::abs(self[2][0]) + F::abs(self[2][1]) + F::abs(self[2][2]) + F::abs(self[2][3]),
),
F::abs(self[3][0]) + F::abs(self[3][1]) + F::abs(self[3][2]) + F::abs(self[3][3]),
)
}
#[inline]
fn norm_linf(self) -> F {
F::max(
F::max(
F::max(
F::abs(self[0][0])
+ F::abs(self[1][0])
+ F::abs(self[2][0])
+ F::abs(self[3][0]),
F::abs(self[0][1])
+ F::abs(self[1][1])
+ F::abs(self[2][1])
+ F::abs(self[3][1]),
),
F::abs(self[0][2]) + F::abs(self[1][2]) + F::abs(self[2][2]) + F::abs(self[3][2]),
),
F::abs(self[0][3]) + F::abs(self[1][3]) + F::abs(self[2][3]) + F::abs(self[3][3]),
)
}
#[inline]
fn norm_l2(self) -> F {
let eigens = (self.transpose() * self).eigenvalues();
F::sqrt(F::max(
F::max(F::max(eigens[0].re, eigens[1].re), eigens[2].re),
eigens[3].re,
))
}
}
-106
View File
@@ -1,106 +0,0 @@
use crate::*;
impl<F: BaseFloat> Exponential for Matrix2<F> {}
impl<F: BaseFloat> Exponential for Matrix3<F> {}
impl<F: BaseFloat> Exponential for Matrix4<F> {}
impl<F: BaseFloat> Decomposition for Matrix2<F> {
fn iwasawa_decomposition(self) -> Option<(Self, Self, Self)> {
let v0 = self[0];
let mag0 = dot(v0, v0);
if mag0 <= F::zero() {
return None;
}
let n0 = dot(self[1], v0) / mag0;
let v1 = self[1] - v0 * n0;
let mag1 = dot(v1, v1);
if mag1 <= F::zero() {
return None;
}
let (a0, a1) = (F::sqrt(mag0), F::sqrt(mag1));
Some((
Matrix2::from_cols(v0 / a0, v1 / a1),
Matrix2::from_diagonal(Vector2::new(a0, a1)),
Matrix2::new(F::one(), F::zero(), n0, F::one()),
))
}
}
impl<F: BaseFloat> Decomposition for Matrix3<F> {
fn iwasawa_decomposition(self) -> Option<(Self, Self, Self)> {
let v0 = self[0];
let mag0 = dot(v0, v0);
if mag0 <= F::zero() {
return None;
}
let n0 = dot(self[1], v0) / mag0;
let v1 = self[1] - v0 * n0;
let mag1 = dot(v1, v1);
if mag1 <= F::zero() {
return None;
}
let n1 = dot(self[2], v0) / mag0;
let n2 = dot(self[2], v1) / mag1;
let v2 = self[2] - v0 * n1 - v1 * n2;
let mag2 = dot(v2, v2);
if mag2 <= F::zero() {
return None;
}
let (a0, a1, a2) = (F::sqrt(mag0), F::sqrt(mag1), F::sqrt(mag2));
#[rustfmt::skip]
let res = Some((
Matrix3::from_cols(v0 / a0, v1 / a1, v2 / a2),
Matrix3::from_diagonal(Vector3::new(a0, a1, a2)),
Matrix3::new(
F::one(), F::zero(), F::zero(),
n0, F::one(), F::zero(),
n1, n2, F::one(),
),
));
res
}
}
impl<F: BaseFloat> Decomposition for Matrix4<F> {
fn iwasawa_decomposition(self) -> Option<(Self, Self, Self)> {
let v0 = self[0];
let mag0 = dot(v0, v0);
if mag0 <= F::zero() {
return None;
}
let n0 = dot(self[1], v0) / mag0;
let v1 = self[1] - v0 * n0;
let mag1 = dot(v1, v1);
if mag1 <= F::zero() {
return None;
}
let n1 = dot(self[2], v0) / mag0;
let n2 = dot(self[2], v1) / mag1;
let v2 = self[2] - v0 * n1 - v1 * n2;
let mag2 = dot(v2, v2);
if mag2 <= F::zero() {
return None;
}
let n3 = dot(self[3], v0) / mag0;
let n4 = dot(self[3], v1) / mag1;
let n5 = dot(self[3], v2) / mag2;
let v3 = self[3] - v0 * n3 - v1 * n4 - v2 * n5;
let mag3 = dot(v3, v3);
if mag3 <= F::zero() {
return None;
}
let (a0, a1, a2, a3) = (F::sqrt(mag0), F::sqrt(mag1), F::sqrt(mag2), F::sqrt(mag3));
#[rustfmt::skip]
let res = Some((
Matrix4::from_cols(v0 / a0, v1 / a1, v2 / a2, v3 / a3),
Matrix4::from_diagonal(Vector4::new(a0, a1, a2, a3)),
Matrix4::new(
F::one(), F::zero(), F::zero(), F::zero(),
n0, F::one(), F::zero(), F::zero(),
n1, n2, F::one(), F::zero(),
n3, n4, n5, F::one(),
),
));
res
}
}
-120
View File
@@ -1,120 +0,0 @@
//! Unofficial third-party cgmath extensions for calculate eigenvalues, operator norms and Iwasawa decomposition.
// SolidCraft: compiler warnings no longer fail the build.
#![deny(clippy::all, rust_2018_idioms)]
#![warn(
missing_docs,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications
)]
#![allow(clippy::many_single_char_names)]
pub use cgmath;
use cgmath::*;
use num_complex::Complex;
mod eigens;
mod exp_decomp;
/// solvers for low dimensional algebraic equations.
pub mod solver;
/// extension for eigen values
pub trait EigenValues: VectorSpace {
/// the type of the array of eigen values
type EigenValues;
/// calculate eigen values.
///
/// # Examples
///
/// ```
/// use cgmath::*;
/// use matext4cgmath::*;
/// use num_complex::Complex;
/// const EPS: f64 = 1.0e-10;
///
/// let mat = Matrix2::new(4.0, -2.0, 3.0, -1.0);
/// let mut eigens = mat.eigenvalues();
/// // Even in the case of real solutions, the order in the array is not guaranteed.
/// eigens.sort_by(|x, y| x.re.partial_cmp(&y.re).unwrap());
/// assert!(Complex::norm(eigens[0] - 1.0) < EPS);
/// assert!(Complex::norm(eigens[1] - 2.0) < EPS);
/// ```
fn eigenvalues(self) -> Self::EigenValues;
}
#[cfg_attr(doc, katexit::katexit)]
/// [operator norms](https://en.wikipedia.org/wiki/Matrix_norm): $L^1$, $L^2$, and $L^\infty$.
/// # Examples
///
/// ```
/// use cgmath::*;
/// use matext4cgmath::*;
///
/// let mat = Matrix2::new(1.0, 3.0, -2.0, 4.0);
/// // L^1 operator norm is the maximum column absolute sumation.
/// assert_eq!(mat.norm_l1(), 6.0);
/// // L^2 operator norm is the maximum singular value.
/// let ans_norm_l2 = (5.0 + f64::sqrt(5.0)) / f64::sqrt(2.0);
/// assert!(f64::abs(mat.norm_l2() - ans_norm_l2) < 1.0e-10);
/// // L^∞ operator norm is the maximum row absolute sumation.
/// assert_eq!(mat.norm_linf(), 7.0);
/// ```
pub trait OperatorNorm: VectorSpace {
/// operator norm for absolute value sumation: $L^1$.
fn norm_l1(self) -> Self::Scalar;
/// operator norm for Euclidean norm: $L^2$.
fn norm_l2(self) -> Self::Scalar;
/// operator norm for absolute value maximum: $L^{\infty}$.
fn norm_linf(self) -> Self::Scalar;
}
/// calculate exponential value
pub trait Exponential: OperatorNorm + std::ops::AddAssign<Self> + One
where
Self::Scalar: BaseFloat,
{
/// calculate exponential
///
/// # Examples
///
/// ```
/// use cgmath::*;
/// use matext4cgmath::*;
///
/// let x = Matrix2::new(0.0, 1.0, -1.0, 0.0);
/// let res = x.exp();
/// let ans = Matrix2::from_angle(Rad(1.0));
/// assert!((res - ans).norm_l1() < 1.0e-10);
/// ```
fn exp(self) -> Self {
use num_traits::{Float, NumCast};
let eps = <Self::Scalar as Float>::epsilon();
let mut a = self;
let mut res = Self::one();
for i in 2_u16..=64 {
res += a;
a = a * self / <Self::Scalar as NumCast>::from(i).unwrap();
if a.norm_linf() < eps * self.norm_linf() {
break;
}
}
res
}
}
#[cfg_attr(doc, katexit::katexit)]
/// some decompositions of matrix
pub trait Decomposition: VectorSpace {
#[cfg_attr(doc, katexit::katexit)]
/// Returns $(K, A, N)$ of the Iwasawa decomposition: $M = KAN$.
///
/// - $K$: orthonormal matrix
/// - $A$: diagonal matrix
/// - $N$: upper-half unipotent matrix
fn iwasawa_decomposition(self) -> Option<(Self, Self, Self)>;
}
-246
View File
@@ -1,246 +0,0 @@
use crate::*;
#[cfg_attr(doc, katexit::katexit)]
/// Solve equation: $x^2 + ax + b = 0$.
///
/// # Examples
///
/// ```
/// use matext4cgmath::solver;
/// use num_complex::Complex;
/// const EPS: f64 = 1.0e-10;
///
/// let res = solver::solve_quadratic(3.0, -4.0);
/// // Real solutions are always sorted in order of solution size.
/// assert!(Complex::norm(res[0] + 4.0) < EPS);
/// assert!(Complex::norm(res[1] - 1.0) < EPS);
/// ```
pub fn solve_quadratic<F: BaseFloat>(a: F, b: F) -> [Complex<F>; 2] {
let two = F::one() + F::one();
let four = two + two;
let det = a * a - four * b;
match det >= F::zero() {
true => {
let h = F::sqrt(det);
[
Complex::new((-a - h) / two, F::zero()),
Complex::new((-a + h) / two, F::zero()),
]
}
false => {
let h = F::sqrt(-det);
[Complex::new(-a, h) / two, Complex::new(-a, -h) / two]
}
}
}
#[cfg_attr(doc, katexit::katexit)]
/// Solve equation: $x^3 + px + q = 0$.
///
/// # Examples
///
/// ```
/// use matext4cgmath::solver;
/// use num_complex::Complex;
/// const EPS: f64 = 1.0e-10;
///
/// let mut res = solver::pre_solve_cubic(-7.0, -6.0);
/// // Even in the case of real solutions, the order in the array is not guaranteed.
/// res.sort_by(|x, y| x.re.partial_cmp(&y.re).unwrap());
/// let ans = [Complex::from(-2.0), Complex::from(-1.0), Complex::from(3.0)];
/// res.iter().zip(ans).for_each(|(x, y)| {
/// assert!(Complex::norm(x - y) < EPS);
/// });
/// ```
pub fn pre_solve_cubic<F: BaseFloat>(p: F, q: F) -> [Complex<F>; 3] {
let two = F::one() + F::one();
let three = two + F::one();
let sqrt3_2 = F::sqrt(three) / two;
let omega = Complex::new(-F::one() / two, sqrt3_2);
let omega2 = Complex::new(-F::one() / two, -sqrt3_2);
let eps_2 = F::sqrt(F::epsilon());
let p_3 = p / three;
let q_2 = q / two;
let alpha2 = q_2 * q_2 + p_3 * p_3 * p_3;
let (x, y) = match alpha2 >= F::zero() {
true => {
let alpha = F::sqrt(alpha2);
let tmpx = -q_2 - alpha;
let tmpy = -q_2 + alpha;
(
Complex::new(
F::signum(tmpx) * F::powf(F::abs(tmpx), F::one() / three),
F::zero(),
),
Complex::new(
F::signum(tmpy) * F::powf(F::abs(tmpy), F::one() / three),
F::zero(),
),
)
}
false => {
let alphai = F::sqrt(-alpha2);
(
Complex::powf(Complex::new(-q_2, alphai), F::one() / three),
Complex::powf(Complex::new(-q_2, -alphai), F::one() / three),
)
}
};
let mut res = [x + y, omega * x + omega2 * y, omega2 * x + omega * y];
// precision by Newton method
res.iter_mut().for_each(|x| {
let mut f = *x * *x * *x + *x * p + q;
let mut f_prime = *x * *x * three + p;
// SolidCraft: at most 100 steps. Near a double root (the eigenvalues of a
// translation matrix) the iteration can cycle forever without meeting the tolerance.
let mut steps = 0;
while f.norm() > eps_2 * f_prime.norm() && steps < 100 {
steps += 1;
if f_prime.norm() < eps_2 {
return;
}
*x -= f / f_prime;
f = *x * *x * *x + *x * p + q;
f_prime = *x * *x * three + p;
}
});
res
}
#[cfg_attr(doc, katexit::katexit)]
/// solve equation: $x^3 + ax^2 + bx + c = 0$.
///
/// # Examples
///
/// ```
/// use matext4cgmath::solver;
/// use num_complex::Complex;
/// const EPS: f64 = 1.0e-10;
///
/// let mut res = solver::solve_cubic(-3.0, 0.0, 4.0);
/// // Even in the case of real solutions, the order in the array is not guaranteed.
/// res.sort_by(|x, y| x.re.partial_cmp(&y.re).unwrap());
/// let ans = [Complex::from(-1.0), Complex::from(2.0), Complex::from(2.0)];
/// res.iter().zip(ans).for_each(|(x, y)| {
/// assert!(Complex::norm(x - y) < EPS);
/// });
/// ```
pub fn solve_cubic<F: BaseFloat>(a: F, b: F, c: F) -> [Complex<F>; 3] {
let two = F::one() + F::one();
let three = two + F::one();
let twenty_seven = three * three * three;
let p = b - a * a / three;
let q = c - a * b / three + two * a * a * a / twenty_seven;
let mut res = pre_solve_cubic(p, q);
res.iter_mut().for_each(|x| {
*x -= a / three;
});
res
}
#[cfg_attr(doc, katexit::katexit)]
/// solve equation: $x^4 + px^2 + qx + r = 0$.
///
/// # Examples
///
/// ```
/// use matext4cgmath::solver;
/// use num_complex::Complex;
/// const EPS: f64 = 1e-7;
///
/// let mut res = solver::pre_solve_quartic(-5.0, 0.0, 4.0);
/// // Even in the case of real solutions, the order in the array is not guaranteed.
/// res.sort_by(|x, y| x.re.partial_cmp(&y.re).unwrap());
/// let ans = [Complex::from(-2.0), Complex::from(-1.0), Complex::from(1.0), Complex::from(2.0)];
/// res.iter().zip(ans).for_each(|(x, y)| {
/// assert!(Complex::norm(x - y) < EPS);
/// });
/// ```
pub fn pre_solve_quartic<F: BaseFloat>(p: F, q: F, r: F) -> [Complex<F>; 4] {
let one = F::one();
let two = one + one;
let four = two + two;
let eps_2 = F::sqrt(F::epsilon());
let a = two * p;
let b = p * p - four * r;
let c = -q * q;
let f = solve_cubic(a, b, c);
let a = f[0].sqrt() / two;
let b = f[1].sqrt() / two;
let c = f[2].sqrt() / two;
let mut res = (0..8)
.map(|i| {
let a = a * F::powi(-F::one(), i % 2);
let b = b * F::powi(-F::one(), (i / 2) % 2);
let c = c * F::powi(-F::one(), (i / 4) % 2);
[-a - b - c, -a + b + c, a - b + c, a + b - c]
})
.map(|x| {
let f = x
.iter()
.map(|t| (t * t * t * t + t * t * p + t * q + r).norm_sqr())
.max_by(|x, y| x.partial_cmp(y).unwrap())
.unwrap();
(x, f)
})
.min_by(|x, y| x.1.partial_cmp(&y.1).unwrap())
.unwrap()
.0;
// refinement by Newton method
res.iter_mut().for_each(|x| {
let mut f = *x * *x * *x * *x + *x * *x * p + *x * q + r;
let mut f_prime = *x * *x * *x * four + *x * p * two + q;
// SolidCraft: at most 100 steps. Near a double root (the eigenvalues of a
// translation matrix) the iteration can cycle forever without meeting the tolerance.
let mut steps = 0;
while f.norm() > eps_2 * f_prime.norm() && steps < 100 {
steps += 1;
if f_prime.norm() < eps_2 {
return;
}
*x -= f / f_prime;
f_prime = *x * *x * *x * four + *x * p * two + q;
f = *x * *x * *x * *x + *x * *x * p + *x * q + r;
}
});
res
}
#[cfg_attr(doc, katexit::katexit)]
/// solve equation: $x^4 + ax^3 + bx^2 + cx + d = 0$.
///
/// # Examples
///
/// ```
/// use matext4cgmath::solver;
/// use num_complex::Complex;
/// const EPS: f64 = 1.0e-10;
///
/// let mut res = solver::solve_quartic(1.0, -7.0, -1.0, 6.0);
/// // Even in the case of real solutions, the order in the array is not guaranteed.
/// res.sort_by(|x, y| x.re.partial_cmp(&y.re).unwrap());
/// let ans = [Complex::from(-3.0), Complex::from(-1.0), Complex::from(1.0), Complex::from(2.0)];
/// res.iter().zip(ans).for_each(|(x, y)| {
/// assert!(Complex::norm(x * x * x * x + x * x * x - 7.0 * x * x - x + 6.0) < EPS);
/// assert!(Complex::norm(x - y) < EPS, "{x} {y}");
/// });
/// ```
pub fn solve_quartic<F: BaseFloat>(a: F, b: F, c: F, d: F) -> [Complex<F>; 4] {
let one = F::one();
let two = one + one;
let three = one + two;
let four = two + two;
let six = two * three;
let eight = four + four;
let a_4 = a / four;
let p = b - six * a_4 * a_4;
let q = c - two * b * a_4 + eight * a_4 * a_4 * a_4;
let r = d - c * a_4 + b * a_4 * a_4 - three * a_4 * a_4 * a_4 * a_4;
let mut res = pre_solve_quartic(p, q, r);
res.iter_mut().for_each(|x| *x -= a_4);
res
}
-1
View File
@@ -1 +0,0 @@
{"v":1}
-55
View File
@@ -1,55 +0,0 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "truck-geometry"
version = "0.5.0"
authors = ["Yoshinori Tanimura <tanimura@ricos.co.jp>"]
build = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "geometrical structs: knot vector, B-spline and NURBS"
homepage = "https://github.com/ricosjp/truck"
readme = "README.md"
keywords = [
"truck",
"graphics",
]
categories = ["graphics"]
license = "Apache-2.0"
repository = "https://github.com/ricosjp/truck"
[lib]
name = "truck_geometry"
path = "src/lib.rs"
[dependencies.serde]
version = "1.0.210"
features = ["derive"]
[dependencies.thiserror]
version = "1.0.63"
[dependencies.truck-base]
version = "0.5.0"
[dependencies.truck-geotrait]
version = "0.4.0"
[dev-dependencies.rand]
version = "0.8.5"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.getrandom]
version = "0.2.15"
features = ["js"]
-177
View File
@@ -1,177 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
-5
View File
@@ -1,5 +0,0 @@
# truck-geometry
[![Crates.io](https://img.shields.io/crates/v/truck-geometry.svg)](https://crates.io/crates/truck-geometry) [![Docs.rs](https://docs.rs/truck-geometry/badge.svg)](https://docs.rs/truck-geometry)
Geometrical structs: knot vector, B-spline and NURBS
-228
View File
@@ -1,228 +0,0 @@
use super::*;
impl<C, S> PCurve<C, S> {
/// Creates composited
#[inline(always)]
pub const fn new(curve: C, surface: S) -> PCurve<C, S> { PCurve { curve, surface } }
/// Returns the reference to the parameter curve
#[inline(always)]
pub const fn curve(&self) -> &C { &self.curve }
/// Returns the reference to the surface
#[inline(always)]
pub const fn surface(&self) -> &S { &self.surface }
/// Decompose the pcurve into its parameter curve and surface.
#[inline(always)]
pub fn decompose(self) -> (C, S) { (self.curve, self.surface) }
}
impl<C, S> ParametricCurve for PCurve<C, S>
where
C: ParametricCurve2D,
S: ParametricSurface,
S::Vector: VectorSpace<Scalar = f64>,
{
type Point = S::Point;
type Vector = S::Vector;
#[inline(always)]
fn subs(&self, t: f64) -> Self::Point {
let pt = self.curve.subs(t);
self.surface.subs(pt[0], pt[1])
}
#[inline(always)]
fn der(&self, t: f64) -> Self::Vector {
let pt = self.curve.subs(t);
let der = self.curve.der(t);
self.surface.uder(pt[0], pt[1]) * der[0] + self.surface.vder(pt[0], pt[1]) * der[1]
}
#[inline(always)]
fn parameter_range(&self) -> ParameterRange { self.curve.parameter_range() }
#[inline(always)]
fn der2(&self, t: f64) -> Self::Vector {
let pt = self.curve.subs(t);
let der = self.curve.der(t);
let der2 = self.curve.der2(t);
self.surface.uuder(pt[0], pt[1]) * der[0] * der[0]
+ self.surface.uvder(pt[0], pt[1]) * der[0] * der[1] * 2.0
+ self.surface.vvder(pt[0], pt[1]) * der[1] * der[1]
+ self.surface.uder(pt[0], pt[1]) * der2[0]
+ self.surface.vder(pt[0], pt[1]) * der2[1]
}
}
impl<C, S> BoundedCurve for PCurve<C, S>
where
C: BoundedCurve,
PCurve<C, S>: ParametricCurve,
{
}
impl<C, S> Cut for PCurve<C, S>
where
C: Cut,
S: Clone,
PCurve<C, S>: ParametricCurve,
{
fn cut(&mut self, t: f64) -> Self {
let curve = self.curve.cut(t);
Self {
curve,
surface: self.surface.clone(),
}
}
}
impl<C, S> SearchParameter<D1> for PCurve<C, S>
where
C: ParametricCurve2D + SearchParameter<D1, Point = Point2>,
S: SearchParameter<D2>,
{
type Point = <S as SearchParameter<D2>>::Point;
fn search_parameter<H: Into<SPHint1D>>(
&self,
point: Self::Point,
hint: H,
trials: usize,
) -> Option<f64> {
let hint = hint.into();
let shint = match hint {
SPHint1D::Parameter(hint) => {
let p = self.curve.subs(hint);
SPHint2D::Parameter(p.x, p.y)
}
SPHint1D::Range(x, y) => {
let p = self.curve.subs(y);
let ranges = (0..PRESEARCH_DIVISION).fold(
((p.x, p.x), (p.y, p.y)),
|((x0, x1), (y0, y1)), i| {
let t = x + (y - x) * i as f64 / PRESEARCH_DIVISION as f64;
let p = self.curve.subs(t);
(
(f64::min(x0, p.x), f64::max(x1, p.x)),
(f64::min(y0, p.y), f64::max(y1, p.y)),
)
},
);
SPHint2D::Range(ranges.0, ranges.1)
}
SPHint1D::None => SPHint2D::None,
};
let (x, y) = self.surface.search_parameter(point, shint, trials)?;
self.curve.search_parameter(Point2::new(x, y), hint, trials)
}
}
impl<C, S> SearchNearestParameter<D1> for PCurve<C, S>
where
Self: BoundedCurve,
<Self as ParametricCurve>::Point: EuclideanSpace<Scalar = f64, Diff = <Self as ParametricCurve>::Vector>
+ MetricSpace<Metric = f64>,
<Self as ParametricCurve>::Vector: InnerSpace<Scalar = f64> + Tolerance,
{
type Point = <Self as ParametricCurve>::Point;
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
point: Self::Point,
hint: H,
trials: usize,
) -> Option<f64> {
let hint = match hint.into() {
SPHint1D::Parameter(hint) => hint,
SPHint1D::Range(x, y) => {
algo::curve::presearch(self, point, (x, y), PRESEARCH_DIVISION)
}
SPHint1D::None => {
algo::curve::presearch(self, point, self.range_tuple(), PRESEARCH_DIVISION)
}
};
algo::curve::search_nearest_parameter(self, point, hint, trials)
}
}
impl<C, S> ParameterDivision1D for PCurve<C, S>
where
C: ParametricCurve2D,
S: ParametricSurface,
S::Point: EuclideanSpace<Scalar = f64> + MetricSpace<Metric = f64> + HashGen<f64>,
S::Vector: VectorSpace<Scalar = f64>,
{
type Point = S::Point;
fn parameter_division(&self, range: (f64, f64), tol: f64) -> (Vec<f64>, Vec<S::Point>) {
algo::curve::parameter_division(self, range, tol)
}
}
impl<C, S> Invertible for PCurve<C, S>
where
C: Invertible,
S: Clone,
{
fn invert(&mut self) { self.curve.invert() }
}
impl<C: Clone, S: Clone, T> Transformed<T> for PCurve<C, S>
where S: Transformed<T>
{
#[inline(always)]
fn transform_by(&mut self, trans: T) { self.surface.transform_by(trans); }
fn transformed(&self, trans: T) -> Self {
Self {
curve: self.curve.clone(),
surface: self.surface.transformed(trans),
}
}
}
#[test]
fn pcurve_test() {
let curve = BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Point2::new(1.0, 1.0),
Point2::new(1.0, 0.0),
Point2::new(0.0, 0.0),
],
);
let surface = BSplineSurface::new(
(KnotVec::bezier_knot(2), KnotVec::bezier_knot(1)),
vec![
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 1.0, 0.0)],
vec![Point3::new(0.0, 0.0, 1.0), Point3::new(0.0, 1.0, 1.0)],
vec![Point3::new(1.0, 0.0, 1.0), Point3::new(1.0, 1.0, 1.0)],
],
);
let pcurve = PCurve::new(curve, surface);
assert_eq!(pcurve.range_tuple(), (0.0, 1.0));
const N: usize = 100;
for i in 0..=N {
let t = i as f64 / N as f64;
assert_near!(
pcurve.subs(t),
Point3::new(
(1.0 - t * t) * (1.0 - t * t),
(1.0 - t) * (1.0 - t),
1.0 - t * t * t * t,
),
);
assert_near!(
pcurve.der(t),
Vector3::new(4.0 * t * (t * t - 1.0), 2.0 * (t - 1.0), -4.0 * t * t * t,),
);
assert_near!(
pcurve.der2(t),
Vector3::new(4.0 * (3.0 * t * t - 1.0), 2.0, -12.0 * t * t,),
);
}
let t = 0.675;
let pt = pcurve.subs(t);
assert_near!(pcurve.search_parameter(pt, None, 100).unwrap(), t);
let pt = pt + Vector3::new(0.01, 0.06, -0.03);
assert!(pcurve.search_parameter(pt, None, 100).is_none());
let t = pcurve.search_nearest_parameter(pt, None, 100).unwrap();
assert!(pcurve.der(t).dot(pcurve.subs(t) - pt).so_small());
}
-203
View File
@@ -1,203 +0,0 @@
use super::*;
impl<C, V: Copy> ExtrudedCurve<C, V> {
/// Creates a linear extruded curve by extrusion.
#[inline(always)]
pub const fn by_extrusion(curve: C, vector: V) -> Self { Self { curve, vector } }
/// Returns the curve before extruded.
#[inline(always)]
pub const fn entity_curve(&self) -> &C { &self.curve }
/// Into the curve before revoluted.
#[inline(always)]
pub fn into_entity_curve(self) -> C { self.curve }
/// Returns the vector of extruded curve.
#[inline(always)]
pub const fn extruding_vector(&self) -> V { self.vector }
}
impl<C> ParametricSurface for ExtrudedCurve<C, C::Vector>
where
C: ParametricCurve,
C::Point: EuclideanSpace<Scalar = f64, Diff = C::Vector>,
C::Vector: VectorSpace<Scalar = f64>,
{
type Point = C::Point;
type Vector = C::Vector;
#[inline(always)]
fn subs(&self, u: f64, v: f64) -> C::Point { self.curve.subs(u) + self.vector * v }
#[inline(always)]
fn uder(&self, u: f64, _: f64) -> C::Vector { self.curve.der(u) }
#[inline(always)]
fn vder(&self, _: f64, _: f64) -> C::Vector { self.vector }
#[inline(always)]
fn uuder(&self, u: f64, _: f64) -> C::Vector { self.curve.der2(u) }
#[inline(always)]
fn uvder(&self, _: f64, _: f64) -> C::Vector { C::Vector::zero() }
#[inline(always)]
fn vvder(&self, _: f64, _: f64) -> C::Vector { C::Vector::zero() }
#[inline(always)]
fn parameter_range(&self) -> (ParameterRange, ParameterRange) {
(
self.curve.parameter_range(),
(Bound::Included(0.0), Bound::Included(1.0)),
)
}
#[inline(always)]
fn u_period(&self) -> Option<f64> { self.curve.period() }
}
impl<C: ParametricCurve3D> ParametricSurface3D for ExtrudedCurve<C, Vector3> {
#[inline(always)]
fn normal(&self, u: f64, _: f64) -> C::Vector {
self.curve.der(u).cross(self.vector).normalize()
}
}
impl<C, V> BoundedSurface for ExtrudedCurve<C, V>
where
C: BoundedCurve,
Self: ParametricSurface,
{
}
impl<C: ParameterDivision1D, V> ParameterDivision2D for ExtrudedCurve<C, V> {
#[inline(always)]
fn parameter_division(
&self,
(urange, vrange): ((f64, f64), (f64, f64)),
tol: f64,
) -> (Vec<f64>, Vec<f64>) {
(
self.curve.parameter_division(urange, tol).0,
vec![vrange.0, vrange.1],
)
}
}
impl<C: ParametricCurve2D + BoundedCurve> SearchParameter<D2> for ExtrudedCurve<C, Vector2> {
type Point = Point2;
#[inline(always)]
fn search_parameter<H: Into<SPHint2D>>(
&self,
point: Point2,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
let hint = match hint.into() {
SPHint2D::Parameter(x, y) => (x, y),
SPHint2D::Range(range0, range1) => {
algo::surface::presearch(self, point, (range0, range1), PRESEARCH_DIVISION)
}
SPHint2D::None => {
algo::surface::presearch(self, point, self.range_tuple(), PRESEARCH_DIVISION)
}
};
algo::surface::search_parameter2d(self, point, hint, trials)
}
}
impl<C: ParametricCurve3D + BoundedCurve> SearchParameter<D2> for ExtrudedCurve<C, Vector3> {
type Point = Point3;
#[inline(always)]
fn search_parameter<H: Into<SPHint2D>>(
&self,
point: Point3,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
let hint = match hint.into() {
SPHint2D::Parameter(x, y) => (x, y),
SPHint2D::Range(range0, range1) => {
algo::surface::presearch(self, point, (range0, range1), PRESEARCH_DIVISION)
}
SPHint2D::None => {
algo::surface::presearch(self, point, self.range_tuple(), PRESEARCH_DIVISION)
}
};
algo::surface::search_parameter3d(self, point, hint, trials)
}
}
impl<C: ParametricCurve3D + BoundedCurve> SearchNearestParameter<D2> for ExtrudedCurve<C, Vector3> {
type Point = Point3;
#[inline(always)]
fn search_nearest_parameter<H: Into<SPHint2D>>(
&self,
point: Point3,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
let hint = match hint.into() {
SPHint2D::Parameter(x, y) => (x, y),
SPHint2D::Range(range0, range1) => {
algo::surface::presearch(self, point, (range0, range1), PRESEARCH_DIVISION)
}
SPHint2D::None => {
algo::surface::presearch(self, point, self.range_tuple(), PRESEARCH_DIVISION)
}
};
algo::surface::search_nearest_parameter(self, point, hint, trials)
}
}
impl<C: Invertible> Invertible for ExtrudedCurve<C, Vector3> {
#[inline(always)]
fn invert(&mut self) { self.curve.invert() }
#[inline(always)]
fn inverse(&self) -> Self {
Self {
curve: self.curve.inverse(),
vector: self.vector,
}
}
}
impl<C: Transformed<Matrix4>> Transformed<Matrix4> for ExtrudedCurve<C, Vector3> {
fn transform_by(&mut self, trans: Matrix4) {
self.curve.transform_by(trans);
self.vector = trans.transform_vector(self.vector);
}
fn transformed(&self, trans: Matrix4) -> Self {
Self {
curve: self.curve.transformed(trans),
vector: trans.transform_vector(self.vector),
}
}
}
#[test]
fn extruded_curve_test() {
let cpts = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
];
let spts = vec![
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 1.0)],
vec![Point3::new(0.0, 1.0, 0.0), Point3::new(0.0, 1.0, 1.0)],
vec![Point3::new(1.0, 0.0, 0.0), Point3::new(1.0, 0.0, 1.0)],
];
let curve = BSplineCurve::new(KnotVec::bezier_knot(2), cpts);
let surface0 = ExtrudedCurve::by_extrusion(curve, Vector3::unit_z());
let surface1 = BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(1)), spts);
assert_eq!(surface0.range_tuple(), surface1.range_tuple());
const N: usize = 10;
for i in 0..=N {
for j in 0..=N {
let u = i as f64 / N as f64;
let v = j as f64 / N as f64;
assert_near!(
surface0.subs(u, v),
ParametricSurface::subs(&surface1, u, v)
);
assert_near!(surface0.uder(u, v), surface1.uder(u, v));
assert_near!(surface0.vder(u, v), surface1.vder(u, v));
assert_near!(surface0.uuder(u, v), surface1.uuder(u, v));
assert_near!(surface0.uvder(u, v), surface1.uvder(u, v));
assert_near!(surface0.vvder(u, v), surface1.vvder(u, v));
assert_near!(surface0.normal(u, v), surface1.normal(u, v));
}
}
}
@@ -1,353 +0,0 @@
use super::*;
#[doc(hidden)]
pub fn double_projection<S>(
surface0: &S,
hint0: Option<(f64, f64)>,
surface1: &S,
hint1: Option<(f64, f64)>,
mut point: Point3,
normal: Vector3,
trials: usize,
) -> Option<(Point3, Point2, Point2)>
where
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
#[cfg(all(test, debug_assertions))]
let mut log = Vec::new();
let mut uv0 = surface0.search_nearest_parameter(point, hint0, 10)?;
let mut uv1 = surface1.search_nearest_parameter(point, hint1, 10)?;
for _ in 0..trials {
#[cfg(all(test, debug_assertions))]
log.push((point, uv0, uv1));
uv0 = surface0.search_nearest_parameter(point, Some(uv0), 10)?;
let pt0 = surface0.subs(uv0.0, uv0.1);
uv1 = surface1.search_nearest_parameter(point, Some(uv1), 10)?;
let pt1 = surface1.subs(uv1.0, uv1.1);
if point.near(&pt0) && point.near(&pt1) && pt0.near(&pt1) {
return Some((point, Point2::from(uv0), Point2::from(uv1)));
} else {
let n0 = surface0.normal(uv0.0, uv0.1);
let n1 = surface1.normal(uv1.0, uv1.1);
let mat = Matrix3::from_cols(n0, n1, normal).transpose();
let inv = mat.invert()?;
let pt = inv * Vector3::new(pt0.dot(n0), pt1.dot(n1), point.dot(normal));
point = Point3::from_vec(pt);
}
}
#[cfg(all(test, debug_assertions))]
{
eprintln!("Newton method is not converges");
log.into_iter().for_each(|t| eprintln!("{t:?}"));
}
None
}
/// Mutable editor for `IntersectionCurve`.
#[doc(hidden)]
#[derive(Debug)]
pub struct IntersectionCurveEditor<'a, C, S> {
pub surface0: &'a mut S,
pub surface1: &'a mut S,
pub leader: &'a mut C,
pub tol: &'a mut f64,
}
impl<C, S> IntersectionCurve<C, S> {
/// This curve is a part of intersection of `self.surface0()` and `self.surface1()`.
#[inline(always)]
pub fn surface0(&self) -> &S { &self.surface0 }
/// This curve is a part of intersection of `self.surface0()` and `self.surface1()`.
#[inline(always)]
pub fn surface1(&self) -> &S { &self.surface1 }
/// Returns the polyline leading this curve.
#[inline(always)]
pub fn leader(&self) -> &C { &self.leader }
/// Returns editor for `IntersectionCurve`. This method is only for developers, do not use.
#[doc(hidden)]
#[inline(always)]
pub fn editor(&mut self) -> IntersectionCurveEditor<'_, C, S> {
IntersectionCurveEditor {
surface0: &mut self.surface0,
surface1: &mut self.surface1,
leader: &mut self.leader,
tol: &mut self.tol,
}
}
/// Change leader.
#[doc(hidden)]
#[inline(always)]
pub fn change_leader<D>(self, f: impl FnOnce(C) -> D) -> IntersectionCurve<D, S> {
IntersectionCurve {
surface0: self.surface0,
surface1: self.surface1,
leader: f(self.leader),
tol: self.tol,
}
}
/// The tolerance for generating this intersection curve.
#[inline(always)]
pub fn tolerance(&self) -> f64 { self.tol }
/// Creates intersection curve with unchecked bound. This method is only for developer of `truck`, deplicated for users.
#[inline(always)]
pub fn new_unchecked(surface0: Box<S>, surface1: Box<S>, leader: C, tol: f64) -> Self {
Self {
surface0,
surface1,
leader,
tol,
}
}
}
impl<C, S> IntersectionCurve<C, S>
where
C: ParametricCurve3D,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
/// Search triple value of the point corresponding to the parameter `t`.
/// - the coordinate on 3D space
/// - the uv coordinate on `self.surface0()`
/// - the uv coordinate on `self.surface1()`
#[inline(always)]
pub fn search_triple(&self, t: f64) -> Option<(Point3, Point2, Point2)> {
double_projection(
self.surface0(),
None,
self.surface1(),
None,
self.leader.subs(t),
self.leader.der(t),
100,
)
}
}
impl<C, S> ParametricCurve for IntersectionCurve<C, S>
where
C: ParametricCurve3D,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
type Point = Point3;
type Vector = Vector3;
fn subs(&self, t: f64) -> Point3 { self.search_triple(t).unwrap().0 }
fn der(&self, t: f64) -> Vector3 {
let n = self.leader.der(t);
let (_, p0, p1) = self.search_triple(t).unwrap();
let d = self
.surface0
.normal(p0.x, p0.y)
.cross(self.surface1.normal(p1.x, p1.y))
.normalize();
d * (n.dot(n) / d.dot(n))
}
/// This method is unimplemented! Should panic!!
#[inline(always)]
fn der2(&self, _: f64) -> Vector3 {
unimplemented!();
}
#[inline(always)]
fn parameter_range(&self) -> ParameterRange { self.leader.parameter_range() }
}
impl<C, S> BoundedCurve for IntersectionCurve<C, S>
where
C: ParametricCurve3D + BoundedCurve,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
}
impl<C, S> ParameterDivision1D for IntersectionCurve<C, S>
where
C: ParametricCurve3D,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
type Point = Point3;
// SolidCraft: the division of `algo::curve::parameter_division` (the same split points and
// the same order), but each point's projection onto the two surfaces starts from the
// parameters of the point that begins its piece instead of a search of the whole surfaces:
// that search ran for every point and made dividing an intersection edge on a large B-spline
// surface take seconds. A point the hinted projection misses is searched for as before.
fn parameter_division(&self, range: (f64, f64), tol: f64) -> (Vec<f64>, Vec<Point3>) {
// SolidCraft: a tolerance finer than truck's (or not a number) divides at truck's
// instead of panicking: the points are only found to within it anyway. truck asserted
// the tolerance, and a join whose caller asked for less failed on the panic (#4623).
let tol = if tol >= TOLERANCE { tol } else { TOLERANCE };
let start = self.hinted_triple(range.0, None);
let end = self.hinted_triple(range.1, None);
hinted_sub_division(self, range, (start, end), tol, 100)
}
}
// SolidCraft: a point of the curve with its parameters on both surfaces.
type Triple = (Point3, Point2, Point2);
thread_local! {
// SolidCraft: a point of an intersection curve projected onto neither surface since the last
// `take_projection_failure` (the leader's point stood in for it).
static PROJECTION_FAILED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
/// SolidCraft: whether `parameter_division` of an intersection curve met a point that projects
/// onto neither surface on this thread since the last call, which clears it. Such a division
/// holds the leader's points there and stops dividing: a Boolean gives up on the curve, as it
/// did when the failure panicked, instead of carrying approximate points on (#5562).
#[doc(hidden)]
pub fn take_projection_failure() -> bool { PROJECTION_FAILED.with(|f| f.replace(false)) }
impl<C, S> IntersectionCurve<C, S>
where
C: ParametricCurve3D,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
// SolidCraft: `search_triple` started from `hint`'s parameters, falling back to the search
// from nothing. Where that fails too, the leader's point stands in and the failure is
// recorded (`take_projection_failure`): `subs` unwrapped that same failed search and
// panicked, failing a revolve cut through a box's corner (#5562). A hinted result is taken
// only within the curve's tolerance of the leader's point, where the leader lies: started
// from a neighbour (or from a periodic surface's seam), the projection can settle on another
// part of a surface, such as the far side of a small hole, which the search from nothing
// would not.
fn hinted_triple(&self, t: f64, hint: Option<Triple>) -> Triple {
let lead = self.leader.subs(t);
let near = self.tol.max(TOLERANCE);
let hinted = hint.and_then(|(_, uv0, uv1)| {
double_projection(
self.surface0(),
Some((uv0.x, uv0.y)),
self.surface1(),
Some((uv1.x, uv1.y)),
lead,
self.leader.der(t),
100,
)
.filter(|(p, _, _)| p.distance(lead) <= near)
});
match hinted.or_else(|| self.search_triple(t)) {
Some(triple) => triple,
None => {
PROJECTION_FAILED.with(|f| f.set(true));
(lead, Point2::origin(), Point2::origin())
}
}
}
}
// SolidCraft: `algo::curve::sub_parameter_division`, carrying each end's parameters.
fn hinted_sub_division<C, S>(
curve: &IntersectionCurve<C, S>,
range: (f64, f64),
ends: (Triple, Triple),
tol: f64,
trials: usize,
) -> (Vec<f64>, Vec<Point3>)
where
C: ParametricCurve3D,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
let (p0, p1) = (ends.0 .0, ends.1 .0);
let gen = p0.midpoint(p1);
let p = 0.5 + (0.2 * HashGen::hash1(gen) - 0.1);
let t = range.0 * (1.0 - p) + range.1 * p;
let mid = p0 + (p1 - p0) * p;
let dist2 = curve.hinted_triple(t, Some(ends.0)).0.distance2(mid);
// SolidCraft: a point that projects nowhere ends the division (see `take_projection_failure`).
let failed = PROJECTION_FAILED.with(std::cell::Cell::get);
if dist2 < tol * tol || trials == 0 || failed {
(vec![range.0, range.1], vec![p0, p1])
} else {
let mid_param = (range.0 + range.1) / 2.0;
let mid_value = curve.hinted_triple(mid_param, Some(ends.0));
let (mut params, mut pts) =
hinted_sub_division(curve, (range.0, mid_param), (ends.0, mid_value), tol, trials - 1);
let _ = (params.pop(), pts.pop());
let (new_params, new_pts) =
hinted_sub_division(curve, (mid_param, range.1), (mid_value, ends.1), tol, trials - 1);
params.extend(new_params);
pts.extend(new_pts);
(params, pts)
}
}
impl<C, S> Cut for IntersectionCurve<C, S>
where
C: Cut<Point = Point3, Vector = Vector3>,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
#[inline(always)]
fn cut(&mut self, t: f64) -> Self {
Self {
surface0: self.surface0.clone(),
surface1: self.surface1.clone(),
leader: self.leader.cut(t),
tol: self.tol,
}
}
}
impl<C: Invertible, S: Clone> Invertible for IntersectionCurve<C, S> {
fn invert(&mut self) { self.leader.invert(); }
}
impl<C, S> SearchParameter<D1> for IntersectionCurve<C, S>
where
C: ParametricCurve3D + SearchNearestParameter<D1, Point = Point3>,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
type Point = Point3;
fn search_parameter<H: Into<SPHint1D>>(
&self,
point: Point3,
hint: H,
trials: usize,
) -> Option<f64> {
let t = self
.leader()
.search_nearest_parameter(point, hint, trials)
.unwrap();
let pt = self.subs(t);
match pt.near(&point) {
true => Some(t),
false => None,
}
}
}
/// Only derive from leading curve. Not precise.
impl<C, S> SearchNearestParameter<D1> for IntersectionCurve<C, S>
where
C: ParametricCurve3D + SearchNearestParameter<D1, Point = Point3>,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
type Point = Point3;
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
point: Point3,
hint: H,
trials: usize,
) -> Option<f64> {
self.leader().search_nearest_parameter(point, hint, trials)
}
}
impl<C, S> Transformed<Matrix4> for IntersectionCurve<C, S>
where
C: Transformed<Matrix4>,
S: Transformed<Matrix4>,
{
fn transform_by(&mut self, trans: Matrix4) {
self.surface0.transform_by(trans);
self.surface1.transform_by(trans);
self.leader.transform_by(trans);
self.tol *= trans.norm_l2();
}
}
impl<C: BoundedCurve> IntersectionCurve<C, Plane> {
/// Optimizes intersection curve of [`Plane`] into [`Line`].
#[inline]
pub fn optimize(&self) -> Line<C::Point> {
let (s, t) = self.leader.range_tuple();
Line(self.leader.subs(s), self.leader.subs(t))
}
}
-149
View File
@@ -1,149 +0,0 @@
use crate::{prelude::*, *};
use std::ops::{Deref, DerefMut, Mul};
/// revolution
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
struct Revolution {
origin: Point3,
axis: Vector3,
}
/// surface constructed by revoluting a curve
/// # Examples
/// Revoluted sphere
/// ```
/// use truck_geometry::prelude::*;
/// use std::f64::consts::PI;
/// let knot_vec = KnotVec::bezier_knot(2);
/// let control_points = vec![
/// Vector4::new(1.0, 0.0, 0.0, 1.0),
/// Vector4::new(0.0, 1.0, 0.0, 0.0),
/// Vector4::new(-1.0, 0.0, 0.0, 1.0),
/// ];
/// // upper half circle on xy-plane
/// let uhcircle = NurbsCurve::new(BSplineCurve::new(knot_vec, control_points));
/// // sphere constructed by revolute circle
/// let sphere = RevolutedCurve::by_revolution(
/// uhcircle, Point3::origin(), Vector3::unit_x(),
/// );
/// const N: usize = 30;
/// for i in 0..=N {
/// for j in 0..=N {
/// let u = i as f64 / N as f64;
/// let v = 2.0 * PI * j as f64 / N as f64;
/// let pt: Vector3 = sphere.subs(u, v).to_vec();
/// assert_near2!(pt.magnitude2(), 1.0);
/// assert_near!(pt, sphere.normal(u, v));
/// }
/// }
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct RevolutedCurve<C> {
curve: C,
revolution: Revolution,
}
/// Linearly extruded curve
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct ExtrudedCurve<C, V> {
curve: C,
vector: V,
}
/// invertible and transformable geometric element
/// # Examples
/// Curve processing example
/// ```
/// use truck_geometry::prelude::*;
/// let curve: BSplineCurve<Point3> = BSplineCurve::new(
/// KnotVec::bezier_knot(2),
/// vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(0.0, 0.0, 1.0),
/// Point3::new(1.0, 0.0, 0.0),
/// ],
/// );
/// let mut processed = Processor::<_, Matrix4>::new(curve.clone());
///
/// // both curves are the same curve
/// const N: usize = 100;
/// for i in 0..=N {
/// let t = i as f64 / N as f64;
/// assert_eq!(curve.subs(t), processed.subs(t));
/// }
///
/// // Processed curve can inverted!
/// processed.invert();
/// for i in 0..=N {
/// let t = i as f64 / N as f64;
/// assert_eq!(curve.subs(1.0 - t), processed.subs(t));
/// }
/// ```
/// Surface processing example
/// ```
/// use truck_geometry::prelude::*;
/// use std::f64::consts::PI;
///
/// let sphere = Sphere::new(Point3::new(1.0, 2.0, 3.0), 2.45);
/// let mut processed = Processor::<_, Matrix4>::new(sphere);
///
/// // both surfaces are the same surface
/// const N: usize = 100;
/// for i in 0..=N {
/// for j in 0..=N {
/// let u = PI * i as f64 / N as f64;
/// let v = 2.0 * PI * j as f64 / N as f64;
/// assert_eq!(sphere.subs(u, v), processed.subs(u, v));
/// }
/// }
///
/// // Processed surface can be inverted!
/// // Here, "invert surface" means swap (u, v)-axes.
/// processed.invert();
/// for i in 0..=N {
/// for j in 0..=N {
/// let u = PI * i as f64 / N as f64;
/// let v = 2.0 * PI * j as f64 / N as f64;
/// assert_eq!(sphere.subs(u, v), processed.subs(v, u));
/// }
/// }
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Processor<E, T> {
entity: E,
transform: T,
orientation: bool,
}
/// The composited maps
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct PCurve<C, S> {
curve: C,
surface: S,
}
/// Intersection curve between two surfaces.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IntersectionCurve<C, S> {
// Considering rotational surfaces, we can consider the case
// where the class `S` holds the curve `C` as a variable.
surface0: Box<S>,
surface1: Box<S>,
leader: C,
tol: f64,
}
/// trimmed curve for parametric curve
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct TrimmedCurve<C> {
curve: C,
range: (f64, f64),
}
mod curve_on_surface;
mod extruded_curve;
mod intersection_curve;
mod processor;
mod revolved_curve;
mod trimmied_curve;
pub use intersection_curve::{double_projection, take_projection_failure};
-658
View File
@@ -1,658 +0,0 @@
use super::*;
impl<E, T: One> Processor<E, T> {
/// Creates new processor
#[inline(always)]
pub fn new(entity: E) -> Processor<E, T> {
Processor {
entity,
transform: T::one(),
orientation: true,
}
}
/// Returns the reference of entity
#[inline(always)]
pub const fn entity(&self) -> &E { &self.entity }
/// Returns the reference of transform
#[inline(always)]
pub const fn transform(&self) -> &T { &self.transform }
/// Returns the orientation of surface
#[inline(always)]
pub const fn orientation(&self) -> bool { self.orientation }
#[inline(always)]
fn sign(&self) -> f64 {
match self.orientation {
true => 1.0,
false => -1.0,
}
}
/// apply the function to the entity geometry
#[inline(always)]
pub fn map<G, F: FnOnce(E) -> G>(self, f: F) -> Processor<G, T> {
Processor {
entity: f(self.entity),
transform: self.transform,
orientation: self.orientation,
}
}
/// apply the function to the entity geometry
#[inline(always)]
pub fn map_ref<G, F: FnOnce(&E) -> G>(&self, f: F) -> Processor<G, T>
where T: Copy {
Processor {
entity: f(&self.entity),
transform: self.transform,
orientation: self.orientation,
}
}
/// apply the transform and inverse
pub fn constract(self) -> E
where E: Transformed<T> + Invertible {
let mut res = self.entity;
res.transform_by(self.transform);
if !self.orientation {
res.invert();
}
res
}
}
impl<E: Clone, T: Clone> Invertible for Processor<E, T> {
#[inline(always)]
fn invert(&mut self) { self.orientation = !self.orientation; }
#[inline(always)]
fn inverse(&self) -> Self {
Processor {
entity: self.entity.clone(),
transform: self.transform.clone(),
orientation: !self.orientation,
}
}
}
impl<C: BoundedCurve, T> Processor<C, T> {
#[inline(always)]
fn get_curve_parameter(&self, t: f64) -> f64 {
let (t0, t1) = self.range_tuple();
match self.orientation {
true => t,
false => t0 + t1 - t,
}
}
}
impl<C, T> ParametricCurve for Processor<C, T>
where
C: BoundedCurve,
C::Point: EuclideanSpace<Diff = C::Vector>,
C::Vector: VectorSpace<Scalar = f64>,
T: Transform<C::Point> + Clone,
{
type Point = C::Point;
type Vector = C::Vector;
#[inline(always)]
fn subs(&self, t: f64) -> C::Point {
let t = self.get_curve_parameter(t);
self.transform.transform_point(self.entity.subs(t))
}
#[inline(always)]
fn der(&self, t: f64) -> Self::Vector {
let t = self.get_curve_parameter(t);
self.transform.transform_vector(self.entity.der(t)) * self.sign()
}
#[inline(always)]
fn der2(&self, t: f64) -> Self::Vector {
let t = self.get_curve_parameter(t);
self.transform.transform_vector(self.entity.der2(t))
}
#[inline(always)]
fn parameter_range(&self) -> ParameterRange { self.entity.parameter_range() }
#[inline(always)]
fn period(&self) -> Option<f64> { self.entity.period() }
}
impl<C, T> BoundedCurve for Processor<C, T>
where
C: BoundedCurve,
C::Point: EuclideanSpace<Diff = C::Vector>,
C::Vector: VectorSpace<Scalar = f64>,
T: Transform<C::Point> + Clone,
{
}
impl<C, T> Cut for Processor<C, T>
where
C: BoundedCurve + Cut,
C::Point: EuclideanSpace<Diff = C::Vector>,
C::Vector: VectorSpace<Scalar = f64>,
T: Transform<C::Point> + Clone,
{
fn cut(&mut self, t: f64) -> Self {
let t = self.get_curve_parameter(t);
let mut entity = self.entity.cut(t);
if !self.orientation {
std::mem::swap(&mut entity, &mut self.entity);
}
Self {
entity,
transform: self.transform.clone(),
orientation: self.orientation,
}
}
}
impl<S, T> ParametricSurface for Processor<S, T>
where
S: ParametricSurface,
S::Point: EuclideanSpace<Scalar = f64, Diff = S::Vector>,
T: Transform<S::Point> + SquareMatrix<Scalar = f64> + Clone,
{
type Point = S::Point;
type Vector = S::Vector;
#[inline(always)]
fn subs(&self, u: f64, v: f64) -> Self::Point {
match self.orientation {
true => self.transform.transform_point(self.entity.subs(u, v)),
false => self.transform.transform_point(self.entity.subs(v, u)),
}
}
#[inline(always)]
fn uder(&self, u: f64, v: f64) -> Self::Vector {
match self.orientation {
true => self.transform.transform_vector(self.entity.uder(u, v)),
false => self.transform.transform_vector(self.entity.vder(v, u)),
}
}
#[inline(always)]
fn vder(&self, u: f64, v: f64) -> Self::Vector {
match self.orientation {
true => self.transform.transform_vector(self.entity.vder(u, v)),
false => self.transform.transform_vector(self.entity.uder(v, u)),
}
}
#[inline(always)]
fn uuder(&self, u: f64, v: f64) -> Self::Vector {
match self.orientation {
true => self.transform.transform_vector(self.entity.uuder(u, v)),
false => self.transform.transform_vector(self.entity.vvder(v, u)),
}
}
#[inline(always)]
fn uvder(&self, u: f64, v: f64) -> Self::Vector {
match self.orientation {
true => self.transform.transform_vector(self.entity.uvder(u, v)),
false => self.transform.transform_vector(self.entity.uvder(v, u)),
}
}
#[inline(always)]
fn vvder(&self, u: f64, v: f64) -> Self::Vector {
match self.orientation {
true => self.transform.transform_vector(self.entity.vvder(u, v)),
false => self.transform.transform_vector(self.entity.uuder(v, u)),
}
}
#[inline(always)]
fn parameter_range(&self) -> (ParameterRange, ParameterRange) {
let (urange, vrange) = self.entity.parameter_range();
match self.orientation {
true => (urange, vrange),
false => (vrange, urange),
}
}
#[inline(always)]
fn u_period(&self) -> Option<f64> {
match self.orientation {
true => self.entity.u_period(),
false => self.entity.v_period(),
}
}
#[inline(always)]
fn v_period(&self) -> Option<f64> {
match self.orientation {
true => self.entity.v_period(),
false => self.entity.u_period(),
}
}
}
impl<S, T> ParametricSurface3D for Processor<S, T>
where
S: ParametricSurface3D,
T: Transform<Point3> + SquareMatrix<Scalar = f64> + Clone,
{
#[inline(always)]
fn normal(&self, u: f64, v: f64) -> Self::Vector {
let transtrans = self.transform.transpose();
let n = match self.orientation {
true => self.entity.normal(u, v),
false => -self.entity.normal(v, u),
};
let n = transtrans
.inverse_transform_vector(n)
.expect("invalid transform");
(n / self.transform.determinant()).normalize()
}
}
impl<S, T> BoundedSurface for Processor<S, T>
where
S: BoundedSurface<Point = Point3, Vector = Vector3>,
T: Transform<S::Point> + SquareMatrix<Scalar = f64> + Clone,
{
}
impl<E, T> Deref for Processor<E, T> {
type Target = E;
#[inline(always)]
fn deref(&self) -> &E { &self.entity }
}
impl<E, T> DerefMut for Processor<E, T> {
#[inline(always)]
fn deref_mut(&mut self) -> &mut E { &mut self.entity }
}
impl<E, T> Transformed<T> for Processor<E, T>
where
T: Mul<T, Output = T> + Copy,
E: Clone,
{
#[inline(always)]
fn transform_by(&mut self, trans: T) { self.transform = trans * self.transform; }
#[inline(always)]
fn transformed(&self, trans: T) -> Self {
Self {
entity: self.entity.clone(),
transform: trans * self.transform,
orientation: self.orientation,
}
}
}
impl<E, T, C> IncludeCurve<C> for Processor<E, T>
where
C: ParametricCurve + Transformed<T> + Clone,
C::Point: EuclideanSpace,
E: IncludeCurve<C>,
T: Transform<C::Point>,
{
fn include(&self, curve: &C) -> bool {
let inv = self
.transform
.inverse_transform()
.expect("irregular transform");
let curve = curve.clone().transformed(inv);
self.entity.include(&curve)
}
}
impl<C> ParameterDivision1D for Processor<C, Matrix3>
where C: ParameterDivision1D<Point = Point2> + BoundedCurve<Point = Point2>
{
type Point = Point2;
fn parameter_division(&self, range: (f64, f64), tol: f64) -> (Vec<f64>, Vec<Self::Point>) {
let a = self.transform;
let range = match self.orientation {
true => range,
false => (
self.get_curve_parameter(range.1),
self.get_curve_parameter(range.0),
),
};
let (_, k, _) = a
.iwasawa_decomposition()
.expect("transform matrix must be invertible!");
let n = f64::abs(k[0][0])
.max(f64::abs(k[1][1]))
.max(f64::abs(k[2][2]));
let (mut params, mut points) = self.entity.parameter_division(range, tol / n);
points
.iter_mut()
.for_each(|pt| *pt = a.transform_point(*pt));
if !self.orientation {
params
.iter_mut()
.for_each(|t| *t = self.get_curve_parameter(*t));
points.reverse();
}
(params, points)
}
}
impl<C> ParameterDivision1D for Processor<C, Matrix4>
where C: ParameterDivision1D<Point = Point3> + BoundedCurve<Point = Point3>
{
type Point = Point3;
fn parameter_division(&self, range: (f64, f64), tol: f64) -> (Vec<f64>, Vec<Self::Point>) {
let a = self.transform;
let range = match self.orientation {
true => range,
false => (
self.get_curve_parameter(range.1),
self.get_curve_parameter(range.0),
),
};
let (_, k, _) = a
.iwasawa_decomposition()
.expect("transform matrix must be invertible!");
let n = f64::abs(k[0][0])
.max(f64::abs(k[1][1]))
.max(f64::abs(k[2][2]))
/ f64::abs(k[3][3]);
let (mut params, mut points) = self.entity.parameter_division(range, tol / n);
points
.iter_mut()
.for_each(|pt| *pt = a.transform_point(*pt));
if !self.orientation {
params
.iter_mut()
.for_each(|t| *t = self.get_curve_parameter(*t));
points.reverse();
}
(params, points)
}
}
impl<S: ParameterDivision2D> ParameterDivision2D for Processor<S, Matrix3> {
fn parameter_division(
&self,
range: ((f64, f64), (f64, f64)),
tol: f64,
) -> (Vec<f64>, Vec<f64>) {
let a = self.transform;
let range = match self.orientation {
true => range,
false => (range.1, range.0),
};
let (_, k, _) = a
.iwasawa_decomposition()
.expect("transform matrix must be invertible!");
let n = f64::abs(k[0][0])
.max(f64::abs(k[1][1]))
.max(f64::abs(k[2][2]));
let (udiv, vdiv) = self.entity.parameter_division(range, tol / n);
match self.orientation {
true => (udiv, vdiv),
false => (vdiv, udiv),
}
}
}
impl<S: ParameterDivision2D> ParameterDivision2D for Processor<S, Matrix4> {
fn parameter_division(
&self,
range: ((f64, f64), (f64, f64)),
tol: f64,
) -> (Vec<f64>, Vec<f64>) {
let a = self.transform;
let range = match self.orientation {
true => range,
false => (range.1, range.0),
};
let (_, k, _) = a
.iwasawa_decomposition()
.expect("transform matrix must be invertible!");
let n = f64::abs(k[0][0])
.max(f64::abs(k[1][1]))
.max(f64::abs(k[2][2]))
/ f64::abs(k[3][3]);
let (udiv, vdiv) = self.entity.parameter_division(range, tol / n);
match self.orientation {
true => (udiv, vdiv),
false => (vdiv, udiv),
}
}
}
impl<E, T> SearchParameter<D1> for Processor<E, T>
where
E: SearchParameter<D1> + BoundedCurve,
<E as SearchParameter<D1>>::Point: EuclideanSpace,
T: Transform<<E as SearchParameter<D1>>::Point>,
{
type Point = <E as SearchParameter<D1>>::Point;
fn search_parameter<H: Into<SPHint1D>>(
&self,
point: <E as SearchParameter<D1>>::Point,
hint: H,
trials: usize,
) -> Option<f64> {
let inv = self.transform.inverse_transform().unwrap();
let t = self
.entity
.search_parameter(inv.transform_point(point), hint, trials)?;
Some(self.get_curve_parameter(t))
}
}
impl<E, T> SearchParameter<D2> for Processor<E, T>
where
E: SearchParameter<D2>,
E::Point: EuclideanSpace,
T: Transform<E::Point>,
{
type Point = E::Point;
fn search_parameter<H: Into<SPHint2D>>(
&self,
point: E::Point,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
let inv = self.transform.inverse_transform().unwrap();
let (u, v) = self
.entity
.search_parameter(inv.transform_point(point), hint, trials)?;
match self.orientation {
true => Some((u, v)),
false => Some((v, u)),
}
}
}
impl<P, E, T> SearchNearestParameter<D1> for Processor<E, T>
where
E: BoundedCurve<Point = P> + SearchNearestParameter<D1, Point = P>,
P: EuclideanSpace<Scalar = f64, Diff = E::Vector>,
E::Vector: InnerSpace<Scalar = f64> + Tolerance,
T: Transform<P> + Clone,
{
type Point = P;
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
point: Self::Point,
hint: H,
trials: usize,
) -> Option<f64> {
let inv = self.transform.inverse_transform().unwrap();
let hint =
self.entity
.search_nearest_parameter(inv.transform_point(point), hint, trials)?;
let hint = self.get_curve_parameter(hint);
algo::curve::search_nearest_parameter(self, point, hint, trials)
}
}
impl<P, E, T> SearchNearestParameter<D2> for Processor<E, T>
where
E: ParametricSurface<Point = P> + SearchNearestParameter<D2, Point = P>,
P: EuclideanSpace<Scalar = f64, Diff = E::Vector>,
E::Vector: InnerSpace<Scalar = f64> + Tolerance,
T: Transform<P> + SquareMatrix<Scalar = f64> + Clone,
{
type Point = P;
fn search_nearest_parameter<H: Into<SPHint2D>>(
&self,
point: Self::Point,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
let inv = self.transform.inverse_transform().unwrap();
let hint =
self.entity
.search_nearest_parameter(inv.transform_point(point), hint, trials)?;
let hint = match self.orientation {
true => hint,
false => (hint.1, hint.0),
};
algo::surface::search_nearest_parameter(self, point, hint, trials)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn exec_compatible_with_bspcurve() {
const DEGREE: usize = 3;
const DIVISION: usize = 4;
let knot_vec = KnotVec::uniform_knot(DEGREE, DIVISION);
let control_points: Vec<Point3> = (0..DEGREE + DIVISION)
.map(|i| Point3::new(i as f64, 20.0 * rand::random::<f64>() - 10.0, 0.0))
.collect();
let mut curve = BSplineCurve::new(knot_vec, control_points);
let mut processor = Processor::new(curve.clone());
let mat = Matrix3::new(
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
);
if mat.determinant().so_small() {
println!("omitted: {mat:?}");
return;
}
curve.transform_by(mat);
processor.transform_by(mat);
assert_eq!(curve.parameter_range(), processor.parameter_range());
const N: usize = 100;
for i in 0..=N {
let t = i as f64 / N as f64;
assert_near!(ParametricCurve::subs(&curve, t), processor.subs(t));
assert_near!(ParametricCurve::der(&curve, t), processor.der(t));
assert_near!(ParametricCurve::der2(&curve, t), processor.der2(t));
}
curve.invert();
processor.invert();
assert_eq!(curve.parameter_range(), processor.parameter_range());
for i in 0..=N {
let t = i as f64 / N as f64;
assert_near!(ParametricCurve::subs(&curve, t), processor.subs(t));
assert_near!(ParametricCurve::der(&curve, t), processor.der(t));
assert_near!(ParametricCurve::der2(&curve, t), processor.der2(t));
}
}
#[test]
fn compatible_with_bspcurve() { (0..10).for_each(|_| exec_compatible_with_bspcurve()) }
fn exec_compatible_with_bspsurface() {
const DEGREE: usize = 3;
const DIVISION: usize = 4;
let knot_vec = KnotVec::uniform_knot(DEGREE, DIVISION);
let knot_vecs = (knot_vec.clone(), knot_vec);
let control_points: Vec<Vec<Point3>> = (0..DEGREE + DIVISION)
.map(|i| {
(0..DEGREE + DIVISION)
.map(|j| Point3::new(i as f64, j as f64, 20.0 * rand::random::<f64>() - 10.0))
.collect()
})
.collect();
let mut surface = BSplineSurface::new(knot_vecs, control_points);
let mut processor = Processor::new(surface.clone());
let mat = Matrix3::new(
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
4.0 * rand::random::<f64>() - 2.0,
);
if mat.determinant().so_small() {
println!("omitted: {mat:?}");
return;
}
surface.transform_by(mat);
processor.transform_by(mat);
assert_eq!(surface.range_tuple(), processor.range_tuple());
const N: usize = 30;
for i in 0..=N {
for j in 0..=N {
let u = i as f64 / N as f64;
let v = j as f64 / N as f64;
let pt0 = ParametricSurface::subs(&surface, u, v);
let pt1 = processor.subs(u, v);
assert_near!(pt0, pt1);
let uder0 = surface.uder(u, v);
let uder1 = processor.uder(u, v);
assert_near!(uder0, uder1);
let vder0 = surface.vder(u, v);
let vder1 = processor.vder(u, v);
assert_near!(vder0, vder1);
let uuder0 = surface.uuder(u, v);
let uuder1 = processor.uuder(u, v);
assert_near!(uuder0, uuder1);
let uvder0 = surface.uvder(u, v);
let uvder1 = processor.uvder(u, v);
assert_near!(uvder0, uvder1);
let vvder0 = surface.vvder(u, v);
let vvder1 = processor.vvder(u, v);
assert_near!(vvder0, vvder1);
let n0 = surface.normal(u, v);
let n1 = processor.normal(u, v);
assert_near!(n0, n1);
}
}
surface.swap_axes();
processor.invert();
assert_eq!(surface.range_tuple(), processor.range_tuple());
for i in 0..=N {
for j in 0..=N {
let u = i as f64 / N as f64;
let v = j as f64 / N as f64;
let pt0 = ParametricSurface::subs(&surface, u, v);
let pt1 = processor.subs(u, v);
assert_near!(pt0, pt1);
let uder0 = surface.uder(u, v);
let uder1 = processor.uder(u, v);
assert_near!(uder0, uder1);
let vder0 = surface.vder(u, v);
let vder1 = processor.vder(u, v);
assert_near!(vder0, vder1);
let uuder0 = surface.uuder(u, v);
let uuder1 = processor.uuder(u, v);
assert_near!(uuder0, uuder1);
let uvder0 = surface.uvder(u, v);
let uvder1 = processor.uvder(u, v);
assert_near!(uvder0, uvder1);
let vvder0 = surface.vvder(u, v);
let vvder1 = processor.vvder(u, v);
assert_near!(vvder0, vvder1);
let n0 = surface.normal(u, v);
let n1 = processor.normal(u, v);
assert_near!(n0, n1);
}
}
}
#[test]
fn compatible_with_bspsurface() { (0..3).for_each(|_| exec_compatible_with_bspsurface()) }
}
-713
View File
@@ -1,713 +0,0 @@
use super::*;
use std::f64::consts::PI;
impl Revolution {
fn new(origin: Point3, axis: Vector3) -> Self {
Self {
origin,
axis: axis.normalize(),
}
}
#[inline(always)]
fn point_rotation_matrix(self, v: f64) -> Matrix4 {
Matrix4::from_translation(self.origin.to_vec())
* Matrix4::from_axis_angle(self.axis, Rad(v))
* Matrix4::from_translation(-self.origin.to_vec())
}
#[inline(always)]
fn vector_rotation_matrix(self, v: f64) -> Matrix4 {
Matrix4::from_axis_angle(self.axis, Rad(v))
}
#[inline(always)]
fn derivation_rotation_matrix(self, v: f64) -> Matrix4 {
let n = self.axis;
Matrix3::new(
n[0] * n[0] * f64::sin(v) - f64::sin(v),
n[0] * n[1] * f64::sin(v) + n[2] * f64::cos(v),
n[0] * n[2] * f64::sin(v) - n[1] * f64::cos(v),
n[0] * n[1] * f64::sin(v) - n[2] * f64::cos(v),
n[1] * n[1] * f64::sin(v) - f64::sin(v),
n[1] * n[2] * f64::sin(v) + n[0] * f64::cos(v),
n[0] * n[2] * f64::sin(v) + n[1] * f64::cos(v),
n[1] * n[2] * f64::sin(v) - n[0] * f64::cos(v),
n[2] * n[2] * f64::sin(v) - f64::sin(v),
)
.into()
}
#[inline(always)]
fn invert(&mut self) { self.axis *= -1.0; }
#[inline(always)]
fn inverse(mut self) -> Self {
self.axis *= -1.0;
self
}
#[inline(always)]
fn contains(self, p: Point3) -> bool { (p - self.origin).cross(self.axis).so_small() }
#[inline(always)]
fn proj_point(&self, p: Point3) -> Point2 {
let r = p - self.origin;
let z = r.dot(self.axis);
let h = r - z * self.axis;
Point2::new(z, h.magnitude())
}
#[inline(always)]
fn proj_vector(&self, p: Point3, v: Vector3) -> Vector2 {
let r = self.proj_point(p).y;
let vz = v.dot(self.axis);
let vxy = v - vz * self.axis;
let vq = (p - self.origin).dot(vxy) / r;
Vector2::new(vz, vq)
}
#[inline(always)]
fn proj_vector2(&self, p: Point3, v: Vector3, v2: Vector3) -> Vector2 {
let r = self.proj_point(p).y;
let v2z = v2.dot(self.axis);
let v2xy = v2 - v2z * self.axis;
let vz = v.dot(self.axis);
let vxy = v - vz * self.axis;
let a = (vxy.dot(vxy) + (p - self.origin).dot(v2xy)) / r;
let b = f64::powi((p - self.origin).dot(vxy), 2) / (r * r * r);
Vector2::new(v2z, a - b)
}
#[inline(always)]
fn proj_angle(&self, p: Point3, q: Point3) -> f64 {
let (p, q) = (p - self.origin, q - self.origin);
let hp = (p - p.dot(self.axis) * self.axis).normalize();
let hq = (q - q.dot(self.axis) * self.axis).normalize();
let t = f64::acos(f64::clamp(hp.dot(hq), -1.0, 1.0));
match hp.cross(hq).dot(self.axis) < 0.0 {
false => t,
true => 2.0 * PI - t,
}
}
}
/// SolidCraft: the angle of a point on the seam, where 0 and 2π are the same place, taken at the end
/// of the period nearer the hint's angle. [`Revolution::proj_angle`] decides it by the sign of a cross
/// product that is rounding noise there, so a boundary followed point by point could come back to
/// the seam at the other end of the period from where it left, and its parameter polygon folded
/// over itself (a whole turn of a cylinder had no area, and no intersection loop lay inside it).
fn seam_side(angle: f64, hint: Option<f64>) -> f64 {
const SEAM: f64 = 1.0e-7;
match hint {
Some(h) if angle < SEAM && h > PI => angle + 2.0 * PI,
Some(h) if angle > 2.0 * PI - SEAM && h < PI => angle - 2.0 * PI,
_ => angle,
}
}
impl<C: ParametricCurve3D> ParametricSurface for RevolutedCurve<C> {
type Point = Point3;
type Vector = Vector3;
#[inline(always)]
fn subs(&self, u: f64, v: f64) -> Point3 {
let mat = self.revolution.point_rotation_matrix(v);
mat.transform_point(self.curve.subs(u))
}
#[inline(always)]
fn uder(&self, u: f64, v: f64) -> Vector3 {
let mat = self.revolution.vector_rotation_matrix(v);
mat.transform_vector(self.curve.der(u))
}
#[inline(always)]
fn vder(&self, u: f64, v: f64) -> Vector3 {
let pt = self.curve.subs(u);
let radius = self.axis().cross(pt - self.origin());
let mat = self.revolution.vector_rotation_matrix(v);
mat.transform_vector(radius)
}
#[inline(always)]
fn uuder(&self, u: f64, v: f64) -> Vector3 {
let mat = self.revolution.vector_rotation_matrix(v);
mat.transform_vector(self.curve.der2(u))
}
#[inline(always)]
fn vvder(&self, u: f64, v: f64) -> Vector3 {
let pt = self.curve.subs(u);
let z = self.revolution.proj_point(pt).x;
let radius = pt - self.origin() - z * self.axis();
let mat = self.revolution.vector_rotation_matrix(v);
-mat.transform_vector(radius)
}
#[inline(always)]
fn uvder(&self, u: f64, v: f64) -> Vector3 {
let mat = self.revolution.derivation_rotation_matrix(v);
mat.transform_vector(self.curve.der(u))
}
#[inline(always)]
fn parameter_range(&self) -> (ParameterRange, ParameterRange) {
(
self.curve.parameter_range(),
(Bound::Included(0.0), Bound::Excluded(2.0 * PI)),
)
}
#[inline(always)]
fn u_period(&self) -> Option<f64> { self.curve.period() }
#[inline(always)]
fn v_period(&self) -> Option<f64> { Some(2.0 * PI) }
}
impl<C: ParametricCurve3D + BoundedCurve> ParametricSurface3D for RevolutedCurve<C> {
#[inline(always)]
fn normal(&self, u: f64, v: f64) -> Vector3 {
let (u0, u1) = self.curve.range_tuple();
let (uder, vder) = if u.near(&u0) {
let pt = self.curve.subs(u);
let radius = self.axis().cross(pt - self.origin());
if radius.so_small() {
let uder = self.curve.der(u);
(uder, self.axis().cross(uder))
} else {
(self.uder(u, v), self.vder(u, v))
}
} else if u.near(&u1) {
let pt = self.curve.subs(u);
let radius = self.axis().cross(pt - self.origin());
if radius.so_small() {
let uder = self.curve.der(u);
(uder, uder.cross(self.axis()))
} else {
(self.uder(u, v), self.vder(u, v))
}
} else {
(self.uder(u, v), self.vder(u, v))
};
uder.cross(vder).normalize()
}
}
impl<C: ParametricCurve3D + BoundedCurve> BoundedSurface for RevolutedCurve<C> {}
impl<C: Clone> Invertible for RevolutedCurve<C> {
#[inline(always)]
fn invert(&mut self) { self.revolution.invert() }
#[inline(always)]
fn inverse(&self) -> Self {
RevolutedCurve {
curve: self.curve.clone(),
revolution: self.revolution.inverse(),
}
}
}
#[derive(Clone, Debug)]
struct ProjectedCurve<C> {
curve: C,
revolution: Revolution,
}
impl<C: ParametricCurve3D> ParametricCurve for ProjectedCurve<C> {
type Point = Point2;
type Vector = Vector2;
#[inline(always)]
fn subs(&self, t: f64) -> Self::Point { self.revolution.proj_point(self.curve.subs(t)) }
#[inline(always)]
fn der(&self, t: f64) -> Self::Vector {
self.revolution
.proj_vector(self.curve.subs(t), self.curve.der(t))
}
#[inline(always)]
fn der2(&self, t: f64) -> Self::Vector {
self.revolution
.proj_vector2(self.curve.subs(t), self.curve.der(t), self.curve.der2(t))
}
#[inline(always)]
fn parameter_range(&self) -> ParameterRange { self.curve.parameter_range() }
#[inline(always)]
fn period(&self) -> Option<f64> { self.curve.period() }
}
impl<C: ParametricCurve3D + BoundedCurve> BoundedCurve for ProjectedCurve<C> {}
impl<C: ParametricCurve3D + BoundedCurve> SearchParameter<D1> for ProjectedCurve<C> {
type Point = Point2;
fn search_parameter<H: Into<SPHint1D>>(
&self,
point: Self::Point,
hint: H,
trials: usize,
) -> Option<f64> {
let hint = match hint.into() {
SPHint1D::Parameter(t) => t,
SPHint1D::Range(x, y) => {
algo::curve::presearch(self, point, (x, y), PRESEARCH_DIVISION)
}
SPHint1D::None => {
algo::curve::presearch(self, point, self.range_tuple(), PRESEARCH_DIVISION)
}
};
algo::curve::search_parameter(self, point, hint, trials)
}
}
impl<C: ParametricCurve3D + BoundedCurve> SearchNearestParameter<D1> for ProjectedCurve<C> {
type Point = Point2;
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
point: Self::Point,
hint: H,
trials: usize,
) -> Option<f64> {
let hint = match hint.into() {
SPHint1D::Parameter(t) => t,
SPHint1D::Range(x, y) => {
algo::curve::presearch(self, point, (x, y), PRESEARCH_DIVISION)
}
SPHint1D::None => {
algo::curve::presearch(self, point, self.range_tuple(), PRESEARCH_DIVISION)
}
};
algo::curve::search_nearest_parameter(self, point, hint, trials)
}
}
impl<C> RevolutedCurve<C> {
/// Creates a surface by revoluting a curve.
#[inline(always)]
pub fn by_revolution(curve: C, origin: Point3, axis: Vector3) -> Self {
RevolutedCurve {
curve,
revolution: Revolution::new(origin, axis),
}
}
/// Returns the curve before revoluted.
#[inline(always)]
pub const fn entity_curve(&self) -> &C { &self.curve }
/// Into the curve before revoluted.
#[inline(always)]
pub fn into_entity_curve(self) -> C { self.curve }
/// Returns origin of revolution
#[inline(always)]
pub const fn origin(&self) -> Point3 { self.revolution.origin }
/// Returns axis of revolution
#[inline(always)]
pub const fn axis(&self) -> Vector3 { self.revolution.axis }
}
impl<C: ParametricCurve3D + BoundedCurve> RevolutedCurve<C> {
/// Returns true if the front point of the curve is on the axis of rotation.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let line = BSplineCurve::new(
/// KnotVec::bezier_knot(1),
/// vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 1.0)],
/// );
/// let surface0 = RevolutedCurve::by_revolution(line.clone(), Point3::origin(), Vector3::unit_y());
/// assert!(surface0.is_front_fixed());
/// let surface1 = RevolutedCurve::by_revolution(line, Point3::new(1.0, 0.0, 0.0), Vector3::unit_y());
/// assert!(!surface1.is_front_fixed());
/// ```
#[inline(always)]
pub fn is_front_fixed(&self) -> bool { self.revolution.contains(self.curve.front()) }
/// Returns true if the back point of the curve is on the axis of rotation.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let line = BSplineCurve::new(
/// KnotVec::bezier_knot(1),
/// vec![Point3::new(0.0, 0.0, 1.0), Point3::new(0.0, 0.0, 0.0)],
/// );
/// let surface0 = RevolutedCurve::by_revolution(line.clone(), Point3::origin(), Vector3::unit_y());
/// assert!(surface0.is_back_fixed());
/// let surface1 = RevolutedCurve::by_revolution(line, Point3::new(1.0, 0.0, 0.0), Vector3::unit_y());
/// assert!(!surface1.is_back_fixed());
/// ```
#[inline(always)]
pub fn is_back_fixed(&self) -> bool { self.revolution.contains(self.curve.back()) }
}
impl<C: ParametricCurve3D + BoundedCurve> SearchParameter<D2> for RevolutedCurve<C> {
type Point = Point3;
fn search_parameter<H: Into<SPHint2D>>(
&self,
point: Point3,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
let (t0, t1) = self.curve.range_tuple();
if self.is_front_fixed() && self.curve.front().near(&point) {
match hint.into() {
SPHint2D::Parameter(_, y) => Some((t0, y)),
SPHint2D::Range((_, y), _) => Some((t0, y)),
SPHint2D::None => Some((t0, 0.0)),
}
} else if self.is_back_fixed() && self.curve.back().near(&point) {
match hint.into() {
SPHint2D::Parameter(_, y) => Some((t1, y)),
SPHint2D::Range(_, (_, y)) => Some((t1, y)),
SPHint2D::None => Some((t1, 2.0 * PI)),
}
} else {
let proj_curve = ProjectedCurve {
curve: &self.curve,
revolution: self.revolution,
};
let p = self.revolution.proj_point(point);
let hint = hint.into();
let hint_angle = match hint {
SPHint2D::Parameter(_, y) => Some(y),
_ => None,
};
let hint0 = match hint {
SPHint2D::Parameter(x, _) => SPHint1D::Parameter(x),
SPHint2D::Range((x0, _), (x1, _)) => SPHint1D::Range(x0, x1),
SPHint2D::None => SPHint1D::None,
};
let t = proj_curve.search_parameter(p, hint0, trials)?;
let p = self.curve.subs(t);
let ang = seam_side(self.revolution.proj_angle(p, point), hint_angle);
match self.subs(t, ang).near(&point) {
true => Some((t, ang)),
false => None,
}
}
}
}
impl<C: ParametricCurve3D + BoundedCurve> SearchNearestParameter<D2> for RevolutedCurve<C> {
type Point = Point3;
fn search_nearest_parameter<H: Into<SPHint2D>>(
&self,
point: Point3,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
let (t0, t1) = self.curve.range_tuple();
let on_axis = move |o: Point3, normal: Vector3| {
let op = point - o;
op.cross(self.revolution.axis).so_small() && op.dot(normal) >= 0.0
};
if self.is_front_fixed() && on_axis(self.curve.front(), self.normal(t0, 0.0)) {
match hint.into() {
SPHint2D::Parameter(_, y) => Some((t0, y)),
SPHint2D::Range((_, y), _) => Some((t0, y)),
SPHint2D::None => Some((t0, 0.0)),
}
} else if self.is_back_fixed() && on_axis(self.curve.back(), self.normal(t1, 0.0)) {
match hint.into() {
SPHint2D::Parameter(_, y) => Some((t1, y)),
SPHint2D::Range(_, (_, y)) => Some((t1, y)),
SPHint2D::None => Some((t1, 2.0 * PI)),
}
} else {
let proj_curve = ProjectedCurve {
curve: &self.curve,
revolution: self.revolution,
};
let p = self.revolution.proj_point(point);
let hint = hint.into();
let hint_angle = match hint {
SPHint2D::Parameter(_, y) => Some(y),
_ => None,
};
let hint0 = match hint {
SPHint2D::Parameter(x, _) => SPHint1D::Parameter(x),
SPHint2D::Range((x0, _), (x1, _)) => SPHint1D::Range(x0, x1),
SPHint2D::None => SPHint1D::None,
};
let t = proj_curve.search_nearest_parameter(p, hint0, trials)?;
let p = self.curve.subs(t);
Some((t, seam_side(self.revolution.proj_angle(p, point), hint_angle)))
}
}
}
fn sub_include<C0, C1>(
surface: &RevolutedCurve<C0>,
curve: &C1,
knots: &[f64],
degree: usize,
) -> bool
where
C0: ParametricCurve3D + BoundedCurve,
C1: ParametricCurve3D + BoundedCurve,
{
let first = curve.subs(knots[0]);
let mut hint = match surface.search_parameter(first, None, INCLUDE_CURVE_TRIALS) {
Some(hint) => hint,
None => return false,
};
knots
.windows(2)
.flat_map(move |knot| {
(1..=degree).map(move |i| {
let s = i as f64 / degree as f64;
knot[0] * (1.0 - s) + knot[1] * s
})
})
.all(move |t| {
let pt = ParametricCurve::subs(curve, t);
surface
.search_parameter(pt, Some(hint), INCLUDE_CURVE_TRIALS)
.or_else(|| surface.search_parameter(pt, None, INCLUDE_CURVE_TRIALS))
.map(|res| hint = res)
.is_some()
})
}
impl<'a> IncludeCurve<BSplineCurve<Point3>> for RevolutedCurve<&'a BSplineCurve<Point3>> {
fn include(&self, curve: &BSplineCurve<Point3>) -> bool {
let knots = curve.knot_vec().to_single_multi().0;
let degree = usize::max(2, usize::max(curve.degree(), self.curve.degree()));
sub_include(self, curve, &knots, degree)
}
}
impl IncludeCurve<BSplineCurve<Point3>> for RevolutedCurve<BSplineCurve<Point3>> {
fn include(&self, curve: &BSplineCurve<Point3>) -> bool {
let knots = curve.knot_vec().to_single_multi().0;
let degree = usize::max(2, usize::max(curve.degree(), self.curve.degree()));
sub_include(self, curve, &knots, degree)
}
}
impl<'a> IncludeCurve<BSplineCurve<Point3>> for RevolutedCurve<&'a NurbsCurve<Vector4>> {
fn include(&self, curve: &BSplineCurve<Point3>) -> bool {
let knots = curve.knot_vec().to_single_multi().0;
let degree = curve.degree() + usize::max(2, self.curve.degree());
sub_include(self, curve, &knots, degree)
}
}
impl IncludeCurve<BSplineCurve<Point3>> for RevolutedCurve<NurbsCurve<Vector4>> {
fn include(&self, curve: &BSplineCurve<Point3>) -> bool {
let knots = curve.knot_vec().to_single_multi().0;
let degree = curve.degree() + usize::max(2, self.curve.degree());
sub_include(self, curve, &knots, degree)
}
}
impl<'a> IncludeCurve<NurbsCurve<Vector4>> for RevolutedCurve<&'a BSplineCurve<Point3>> {
fn include(&self, curve: &NurbsCurve<Vector4>) -> bool {
let knots = curve.knot_vec().to_single_multi().0;
let degree = curve.degree() + usize::max(2, self.curve.degree());
sub_include(self, curve, &knots, degree)
}
}
impl IncludeCurve<NurbsCurve<Vector4>> for RevolutedCurve<BSplineCurve<Point3>> {
fn include(&self, curve: &NurbsCurve<Vector4>) -> bool {
let knots = curve.knot_vec().to_single_multi().0;
let degree = curve.degree() + usize::max(2, self.curve.degree());
sub_include(self, curve, &knots, degree)
}
}
impl<'a> IncludeCurve<NurbsCurve<Vector4>> for RevolutedCurve<&'a NurbsCurve<Vector4>> {
fn include(&self, curve: &NurbsCurve<Vector4>) -> bool {
let knots = curve.knot_vec().to_single_multi().0;
let degree = curve.degree() + usize::max(2, self.curve.degree());
sub_include(self, curve, &knots, degree)
}
}
impl IncludeCurve<NurbsCurve<Vector4>> for RevolutedCurve<NurbsCurve<Vector4>> {
fn include(&self, curve: &NurbsCurve<Vector4>) -> bool {
let knots = curve.knot_vec().to_single_multi().0;
let degree = curve.degree() + usize::max(2, self.curve.degree());
sub_include(self, curve, &knots, degree)
}
}
impl<C> ParameterDivision2D for RevolutedCurve<C>
where C: ParametricCurve3D + ParameterDivision1D<Point = Point3>
{
fn parameter_division(
&self,
(urange, vrange): ((f64, f64), (f64, f64)),
tol: f64,
) -> (Vec<f64>, Vec<f64>) {
let curve_division = self.curve.parameter_division(urange, tol);
let max = curve_division
.1
.into_iter()
.fold(0.0, |max2, pt| {
let h = self.revolution.proj_point(pt).y;
f64::max(max2, h)
})
.sqrt();
let acos = f64::acos(1.0 - tol / max);
let div: usize = 1 + ((vrange.1 - vrange.0) / acos).floor() as usize;
let circle_division = (0..=div)
.map(|j| vrange.0 + (vrange.1 - vrange.0) * j as f64 / div as f64)
.collect();
(curve_division.0, circle_division)
}
}
#[test]
fn revolve_test() {
let pt0 = Point3::new(0.0, 2.0, 1.0);
let pt1 = Point3::new(1.0, 0.0, 0.0);
let curve = BSplineCurve::new(KnotVec::bezier_knot(1), vec![pt0, pt1]);
let surface = RevolutedCurve::by_revolution(curve, Point3::origin(), Vector3::unit_y());
const N: usize = 100;
for i in 0..=N {
for j in 0..=N {
let u = i as f64 / N as f64;
let v = 2.0 * PI * j as f64 / N as f64;
let res = surface.subs(u, v);
let ans = Point3::new(
u * f64::cos(v) + (1.0 - u) * f64::sin(v),
2.0 * (1.0 - u),
-u * f64::sin(v) + (1.0 - u) * f64::cos(v),
);
assert_near!(res, ans);
let res_uder = surface.uder(u, v);
let ans_uder =
Vector3::new(f64::cos(v) - f64::sin(v), -2.0, -f64::sin(v) - f64::cos(v));
assert_near!(res_uder, ans_uder);
let res_vder = surface.vder(u, v);
let ans_vder = Vector3::new(
-u * f64::sin(v) + (1.0 - u) * f64::cos(v),
0.0,
-u * f64::cos(v) - (1.0 - u) * f64::sin(v),
);
assert_near!(res_vder, ans_vder);
let res_uuder = surface.uuder(u, v);
let ans_uuder = Vector3::zero();
assert_near!(res_uuder, ans_uuder);
let res_uvder = surface.uvder(u, v);
let ans_uvder =
Vector3::new(-f64::sin(v) - f64::cos(v), 0.0, -f64::cos(v) + f64::sin(v));
assert_near!(res_uvder, ans_uvder);
let res_vvder = surface.vvder(u, v);
let ans_vvder = Vector3::new(
-u * f64::cos(v) - (1.0 - u) * f64::sin(v),
0.0,
u * f64::sin(v) - (1.0 - u) * f64::cos(v),
);
assert_near!(res_vvder, ans_vvder);
let normal = surface.normal(u, v);
assert!(normal.dot(res_uder).so_small());
assert!(normal.dot(res_vder).so_small());
}
}
}
#[test]
fn search_parameter() {
let line = BSplineCurve::new(
KnotVec::bezier_knot(1),
vec![Point3::new(0.0, 2.0, 1.0), Point3::new(1.0, 0.0, 0.0)],
);
let surface = RevolutedCurve::by_revolution(line, Point3::origin(), Vector3::unit_y());
let pt = Point3::new(-0.5, 1.0, 0.5);
let (u, v) = surface.search_parameter(pt, Some((0.4, 1.2)), 100).unwrap();
assert_near!(surface.subs(u, v), pt);
}
#[test]
fn search_parameter_with_fixed_points() {
let line = BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Point3::new(0.0, 1.0, 0.0),
Point3::new(0.0, 0.0, 1.0),
Point3::new(0.0, -1.0, 0.0),
],
);
let surface = RevolutedCurve::by_revolution(line, Point3::origin(), Vector3::unit_y());
let (u, v) = surface
.search_parameter(Point3::new(0.0, 1.0, 0.0), Some((0.5, 0.3)), 10)
.unwrap();
assert_near!(Vector2::new(u, v), Vector2::new(0.0, 0.3));
let (u, v) = surface
.search_parameter(Point3::new(0.0, -1.0, 0.0), Some((0.5, 0.3)), 10)
.unwrap();
assert_near!(Vector2::new(u, v), Vector2::new(1.0, 0.3));
}
#[test]
fn search_nearest_parameter() {
let line = BSplineCurve::new(
KnotVec::bezier_knot(1),
vec![Point3::new(0.0, 2.0, 1.0), Point3::new(1.0, 0.0, 0.0)],
);
let surface = RevolutedCurve::by_revolution(line, Point3::origin(), Vector3::unit_y());
let pt = surface.subs(0.4, 1.2) + 0.1 * surface.normal(0.4, 1.2);
let (u, v) = surface
.search_nearest_parameter(pt, Some((0.4, 1.2)), 100)
.unwrap();
assert_near!(Vector2::new(u, v), Vector2::new(0.4, 1.2));
}
#[test]
fn search_nearest_parameter_with_fixed_points() {
let line = BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Point3::new(0.0, 1.0, 0.0),
Point3::new(0.0, 0.0, 1.0),
Point3::new(0.0, -1.0, 0.0),
],
);
let surface = RevolutedCurve::by_revolution(line, Point3::origin(), Vector3::unit_y());
let (u, v) = surface
.search_nearest_parameter(Point3::new(0.0, 2.0, 0.0), Some((0.5, 0.3)), 10)
.unwrap();
assert_near!(Vector2::new(u, v), Vector2::new(0.0, 0.3));
let (u, v) = surface
.search_nearest_parameter(Point3::new(0.0, -2.0, 0.0), Some((0.5, 0.3)), 10)
.unwrap();
assert_near!(Vector2::new(u, v), Vector2::new(1.0, 0.3));
}
#[test]
fn include_curve_normal() {
let line = BSplineCurve::new(
KnotVec::bezier_knot(1),
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 2.0, 2.0)],
);
let surface = RevolutedCurve::by_revolution(line, Point3::origin(), Vector3::unit_y());
let parabola = BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Point3::new(1.0, 1.0, 0.0),
Point3::new(0.0, 0.0, 1.0),
Point3::new(-1.0, 1.0, 0.0),
],
);
assert!(surface.include(&parabola));
}
#[test]
fn include_curve_abnormal0() {
let line = BSplineCurve::new(
KnotVec::bezier_knot(1),
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 2.0, 2.0)],
);
let surface = RevolutedCurve::by_revolution(line, Point3::origin(), Vector3::unit_y());
let parabola = BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Point3::new(1.0, 1.0, 0.0),
Point3::new(0.0, 0.0, 2.0),
Point3::new(-1.0, 1.0, 0.0),
],
);
assert!(!surface.include(&parabola));
}
#[test]
fn include_curve_abnormal1() {
let curve = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(3),
vec![
Vector4::new(0.0, 3.0, 0.0, 1.0),
Vector4::new(0.0, 3.0, 3.0, 0.5),
Vector4::new(0.0, 0.0, 0.0, 1.0),
Vector4::new(0.0, 0.0, 3.0, 1.0),
],
));
let pt0 = curve.subs(0.2);
let pt1 = curve.subs(0.6);
let surface = RevolutedCurve::by_revolution(curve, Point3::origin(), Vector3::unit_y());
let line = BSplineCurve::new(KnotVec::bezier_knot(1), vec![pt0, pt1]);
assert!(!surface.include(&line));
}
-73
View File
@@ -1,73 +0,0 @@
use super::*;
impl<C> TrimmedCurve<C> {
/// constructor
#[inline(always)]
pub const fn new(curve: C, range: (f64, f64)) -> Self { Self { curve, range } }
/// Returns the reference of non-trimmed curve
#[inline(always)]
pub const fn curve(&self) -> &C { &self.curve }
/// Returns the mutable reference of non-trimmed curve
#[inline(always)]
pub fn curve_mut(&mut self) -> &mut C { &mut self.curve }
}
impl<C: ParametricCurve> ParametricCurve for TrimmedCurve<C> {
type Point = C::Point;
type Vector = C::Vector;
#[inline(always)]
fn subs(&self, t: f64) -> Self::Point { self.curve.subs(t) }
#[inline(always)]
fn der(&self, t: f64) -> Self::Vector { self.curve.der(t) }
#[inline(always)]
fn der2(&self, t: f64) -> Self::Vector { self.curve.der2(t) }
#[inline(always)]
fn period(&self) -> Option<f64> { self.curve.period() }
#[inline(always)]
fn parameter_range(&self) -> ParameterRange {
(Bound::Included(self.range.0), Bound::Included(self.range.1))
}
}
impl<C: ParametricCurve> BoundedCurve for TrimmedCurve<C> {}
impl<C: ParametricCurve> Cut for TrimmedCurve<C> {
fn cut(&mut self, t: f64) -> Self {
let (t0, t1) = self.range;
self.range = (t0, t);
Self::new(self.curve.clone(), (t, t1))
}
}
impl<C: SearchNearestParameter<D1>> SearchNearestParameter<D1> for TrimmedCurve<C> {
type Point = C::Point;
#[inline(always)]
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
pt: C::Point,
hint: H,
trials: usize,
) -> Option<f64> {
self.curve.search_nearest_parameter(pt, hint, trials)
}
}
impl<C: SearchParameter<D1>> SearchParameter<D1> for TrimmedCurve<C> {
type Point = C::Point;
#[inline(always)]
fn search_parameter<H: Into<SPHint1D>>(
&self,
pt: C::Point,
hint: H,
trials: usize,
) -> Option<f64> {
self.curve.search_parameter(pt, hint, trials)
}
}
impl<C: ParameterDivision1D> ParameterDivision1D for TrimmedCurve<C> {
type Point = C::Point;
fn parameter_division(&self, range: (f64, f64), tol: f64) -> (Vec<f64>, Vec<Self::Point>) {
self.curve.parameter_division(range, tol)
}
}
-190
View File
@@ -1,190 +0,0 @@
use thiserror::Error;
/// Error handler for [`Error`](./errors/enum.Error.html)
pub type Result<T> = std::result::Result<T, Error>;
/// Geometrical Errors
#[derive(Debug, PartialEq, Error)]
pub enum Error {
/// The following operations are failed if the knot vector has zero range.
/// * Creating `BSplineCurve` or `BSplineSurface`,
/// * Calculating bspline basis functions, or
/// * Normalizing the knot vector.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// use truck_geometry::errors::Error;
/// let mut knot_vec = KnotVec::from(vec![0.0, 0.0, 0.0, 0.0]);
/// assert_eq!(knot_vec.try_normalize(), Err(Error::ZeroRange));
/// assert_eq!(knot_vec.try_bspline_basis_functions(1, 0.0), Err(Error::ZeroRange));
///
/// let ctrl_pts = vec![Vector2::new(0.0, 0.0), Vector2::new(1.0, 1.0)];
/// assert!(matches!(BSplineCurve::try_new(knot_vec, ctrl_pts), Err(Error::ZeroRange)));
/// ```
#[error("This knot vector consists single value.")]
ZeroRange,
/// Fails concatting two knot vectors if there is a difference between the back knot of
/// the former knot vector and the front knot of the latter knot vector.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// use truck_geometry::errors::Error;
/// let mut knot_vec0 = KnotVec::from(vec![0.0, 0.0, 1.0, 1.0]);
/// let knot_vec1 = KnotVec::from(vec![2.0, 2.0, 3.0, 3.0]);
/// assert!(matches!(knot_vec0.try_concat(&knot_vec1, 1), Err(Error::DifferentBackFront(1.0, 2.0))));
/// ```
#[error("Cannot concat two knot vectors whose the back of the first and the front of the second are different.
the back of the first knot vector: {0}
the front of the second knot vector: {1}")]
DifferentBackFront(f64, f64),
/// If the knot vector is not clamped, then one cannot concat the vector with another knot vector.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// use truck_geometry::errors::Error;
/// let mut knot_vec0 = KnotVec::from(vec![0.0, 0.0, 1.0, 1.0]);
/// let knot_vec1 = KnotVec::from(vec![2.0, 2.0, 3.0, 3.0]);
/// assert!(matches!(knot_vec0.try_concat(&knot_vec1, 2), Err(Error::NotClampedKnotVector)));
/// ```
#[error("This knot vector is not clamped.")]
NotClampedKnotVector,
/// Creating a knot vector by `KnotVec::try_from()` is failed if the given vector is not sorted.
/// `<KnotVec as From<Vec<f64>>>::from()` does not panic by this error because sorts the given
/// vector before creating the knot vector. So, `KnotVec::try_from()` is more efficient than
/// `<KnotVec as From<Vec<f64>>>::from()`.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// use truck_geometry::errors::Error;
/// use std::convert::*;
///
/// assert!(matches!(KnotVec::try_from(vec![1.0, 3.0, 0.0, 2.0]), Err(Error::NotSortedVector)));
/// assert_eq!(
/// <KnotVec as From<Vec<f64>>>::from(vec![1.0, 3.0, 0.0, 2.0]),
/// KnotVec::try_from(vec![0.0, 1.0, 2.0, 3.0]).unwrap(),
/// );
/// ```
#[error("This knot vector is not sorted.")]
NotSortedVector,
/// The given degree is too large to calculate bspline basis functions.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// use truck_geometry::errors::Error;
///
/// // a knot vector with length = 4.
/// let knot_vec = KnotVec::from(vec![0.0, 0.0, 1.0, 1.0]);
/// assert!(matches!(
/// knot_vec.try_bspline_basis_functions(5, 0.5),
/// Err(Error::TooLargeDegree(4, 5)),
/// ));
/// ```
#[error(
"This knot vector is too short compared to the degree.
the length of knot_vec: {0}
the degree: {1}"
)]
TooLargeDegree(usize, usize),
/// The specified knot cannot be removed.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// use truck_geometry::errors::Error;
/// let knot_vec = KnotVec::bezier_knot(2);
/// let ctrl_pts = vec![Vector2::new(-1.0, 1.0), Vector2::new(0.0, -1.0), Vector2::new(1.0, 1.0)];
/// let mut bspcurve = BSplineCurve::new(knot_vec, ctrl_pts);
/// let org_curve = bspcurve.clone();
/// bspcurve.add_knot(0.5).add_knot(0.5).add_knot(0.25).add_knot(0.75);
/// assert!(bspcurve.try_remove_knot(3).is_ok());
/// assert!(matches!(bspcurve.try_remove_knot(2), Err(Error::CannotRemoveKnot(2))));
/// ```
#[error("The {0}th knot in this knot vector cannot be removed.")]
CannotRemoveKnot(usize),
/// Empty vector of points cannot construct B-spline.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// use truck_geometry::errors::Error;
///
/// let knot_vec = KnotVec::bezier_knot(2);
/// let ctrl_pts: Vec<Vector4> = Vec::new();
/// assert!(matches!(
/// BSplineCurve::try_new(knot_vec, ctrl_pts),
/// Err(Error::EmptyControlPoints),
/// ));
/// ```
#[error("The control point must not be empty.")]
EmptyControlPoints,
/// The knot vector of B-spline curves or B-spline surfaces must be longer than the corresponded
/// array of control points.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// use truck_geometry::errors::Error;
/// let knot_vec = KnotVec::from(vec![0.0, 1.0, 2.0]);
/// let ctrl_pts = vec![Vector2::new(0.0, 0.0), Vector2::new(0.0, 0.0), Vector2::new(0.0, 0.0), Vector2::new(0.0, 0.0)];
/// assert!(matches!(
/// BSplineCurve::try_new(knot_vec, ctrl_pts),
/// Err(Error::TooShortKnotVector(3, 4)),
/// ));
/// ```
#[error(
"The knot vector must be more than the control points.
the length of knot_vec: {0}
the number of control points: {1}"
)]
TooShortKnotVector(usize, usize),
/// The length of the given arrays of control points to create a B-spline surface is irregular.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// use truck_geometry::errors::Error;
/// let knot_vecs = (KnotVec::bezier_knot(2), KnotVec::bezier_knot(2));
/// let ctrl_pts = vec![
/// vec![Vector2::new(1.0, 2.0), Vector2::new(1.0, 2.0)], // length = 2
/// vec![Vector2::new(1.0, 2.0)] // length = 1
/// ];
/// assert!(matches!(
/// BSplineSurface::try_new(knot_vecs, ctrl_pts),
/// Err(Error::IrregularControlPoints),
/// ));
/// ```
#[error("The number of control points is irregular")]
IrregularControlPoints,
/// The vector of control points and the one of weights have different length.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// use truck_geometry::errors::Error;
/// let bspcurve = BSplineCurve::new(
/// KnotVec::bezier_knot(2),
/// vec![Point2::new(0.0, 1.0), Point2::new(2.0, 3.0), Point2::new(4.0, 5.0)],
/// );
/// let weights = vec![1.0, 2.0]; // less than control points
///
/// assert!(matches!(
/// NurbsCurve::<Vector3>::try_from_bspline_and_weights(bspcurve, weights),
/// Err(Error::DifferentLength),
/// ));
/// ```
#[error("The vector of control points and the one of weights have different length.")]
DifferentLength,
}
#[test]
#[rustfmt::skip]
fn print_messages() {
use std::io::Write;
let stderr = &mut std::io::stderr();
writeln!(stderr, "****** test of the expressions of error messages ******\n").unwrap();
writeln!(stderr, "{}\n", Error::ZeroRange).unwrap();
writeln!(stderr, "{}\n", Error::DifferentBackFront(0.0, 1.0)).unwrap();
writeln!(stderr, "{}\n", Error::NotClampedKnotVector).unwrap();
writeln!(stderr, "{}\n", Error::NotSortedVector).unwrap();
writeln!(stderr, "{}\n", Error::TooLargeDegree(1, 2)).unwrap();
writeln!(stderr, "{}\n", Error::CannotRemoveKnot(7)).unwrap();
writeln!(stderr, "{}\n", Error::EmptyControlPoints).unwrap();
writeln!(stderr, "{}\n", Error::TooShortKnotVector(1, 2)).unwrap();
writeln!(stderr, "{}\n", Error::IrregularControlPoints).unwrap();
writeln!(stderr, "*******************************************************").unwrap();
}
-52
View File
@@ -1,52 +0,0 @@
//! Geometrical structs: knot vector, B-spline and NURBS
#![cfg_attr(not(debug_assertions), deny(warnings))]
#![deny(clippy::all, rust_2018_idioms)]
#![warn(
missing_docs,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications
)]
use serde::{Deserialize, Serialize};
use std::{fmt::Debug, ops::Bound};
use truck_base::bounding_box::Bounded;
const INCLUDE_CURVE_TRIALS: usize = 100;
const PRESEARCH_DIVISION: usize = 50;
/// re-export `truck_base`
pub mod base {
pub use truck_base::bounding_box::BoundingBox;
pub use truck_base::cgmath64::*;
pub use truck_base::tolerance::*;
pub use truck_base::{assert_near, assert_near2};
pub use truck_base::{hash, hash::HashGen};
pub use truck_geotrait::*;
}
/// Declares the nurbs
pub mod nurbs;
/// Enumerats `Error`.
pub mod errors;
/// Declares the specified gememetric items: Plane, Sphere, and so on.
pub mod specifieds;
/// Declares some decorators
pub mod decorators;
/// re-export all modules.
pub mod prelude {
use crate::*;
pub use base::*;
pub use decorators::*;
pub use errors::*;
pub use nurbs::*;
pub use specifieds::*;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-596
View File
@@ -1,596 +0,0 @@
use super::*;
use crate::errors::Error;
use std::slice::SliceIndex;
use std::vec::Vec;
impl KnotVec {
/// empty constructor
pub const fn new() -> KnotVec { KnotVec(Vec::new()) }
/// Returns the length of range.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vec = KnotVec::from(vec![0.0, 6.0]);
/// assert_eq!(knot_vec.range_length(), 6.0);
/// ```
#[inline(always)]
pub fn range_length(&self) -> f64 {
match self.is_empty() {
true => 0.0,
false => self[self.len() - 1] - self[0],
}
}
/// Returns whether two knot vectors have the same range.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vec0 = KnotVec::new(); // empty knot vector
/// let knot_vec1 = KnotVec::from(vec![0.0, 0.0, 1.0, 1.0]);
/// let knot_vec2 = KnotVec::from(vec![0.0, 0.5, 1.0]);
/// let knot_vec3 = KnotVec::from(vec![0.0, 0.0, 2.0, 2.0]);
/// assert!(knot_vec0.same_range(&KnotVec::new())); // both empty knot vector
/// assert!(!knot_vec0.same_range(&knot_vec1));
/// assert!(knot_vec1.same_range(&knot_vec2)); // the range of both knot vector is [0, 1].
/// assert!(!knot_vec1.same_range(&knot_vec3));
/// ```
#[inline(always)]
pub fn same_range(&self, other: &KnotVec) -> bool {
match (self.is_empty(), other.is_empty()) {
(false, false) => {
self[0].near(&other[0]) && self.range_length().near(&other.range_length())
}
(true, true) => true,
_ => false,
}
}
/// Removes one item.
#[inline(always)]
pub fn remove(&mut self, idx: usize) -> f64 { self.0.remove(idx) }
/// Returns the maximum index `i` of `self[i] <= x`
/// Return `None` if `x < self[0] or self.len() == 0`.
/// # Examples
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let mut knot_vec = KnotVec::from(vec![0.0, 0.0, 1.0, 2.0, 3.0, 3.0]);
/// let idx = knot_vec.floor(1.5).unwrap();
/// assert_eq!(idx, 2);
/// ```
#[inline(always)]
pub fn floor(&self, x: f64) -> Option<usize> { self.iter().rposition(|t| *t <= x) }
/// Returns the multiplicity of the `i`th knot
/// # Examples
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let knot_vec = KnotVec::from(vec![0.0, 0.0, 1.0, 2.0, 2.0, 2.0, 3.0, 3.0]);
/// assert_eq!(knot_vec.multiplicity(5), 3);
/// ```
#[inline(always)]
pub fn multiplicity(&self, i: usize) -> usize {
self.iter().filter(|u| self[i].near(u)).count()
}
/// Adds a knot and return the index of the added knot.
/// # Examples
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let mut knot_vec = KnotVec::from(vec![0.0, 0.0, 1.0, 2.0, 3.0, 3.0]);
/// let idx0 = knot_vec.add_knot(1.5);
/// assert_eq!(idx0, 3);
/// let idx1 = knot_vec.add_knot(-1.0);
/// assert_eq!(idx1, 0);
/// let ansvec = KnotVec::from(vec![-1.0, 0.0, 0.0, 1.0, 1.5, 2.0, 3.0, 3.0]);
/// assert_eq!(knot_vec, ansvec);
/// ```
#[inline(always)]
pub fn add_knot(&mut self, knot: f64) -> usize {
match self.floor(knot) {
Some(idx) => {
self.0.insert(idx + 1, knot);
idx + 1
}
None => {
self.0.insert(0, knot);
0
}
}
}
/// Calculates B-spline basis functions at `t` with degree `degree`.
/// # Panics
/// If the length of `self` is not more than `degree`, panic occurs.
/// # Remarks
/// In this package, the B-spline basis function is based on the characteristic function of
/// the right-open intervals [s, t). So, the value corresponding to the end point t = t_n is always 0.0.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// const N : usize = 100; // sample size in tests
///
/// // B-spline basis functions is a partition of unity in (t_k, t_{n - k}).
/// let vec = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
/// let knot_vec = KnotVec::from(vec);
/// let degree = 2;
/// for i in 0..N {
/// let t = 2.0 + 4.0 / (N as f64) * (i as f64);
/// let res = knot_vec.bspline_basis_functions(degree, t);
/// let sum = res.iter().fold(0.0, |sum, a| sum + a);
/// assert_near2!(sum, 1.0);
/// }
/// ```
/// ```
/// use truck_geometry::prelude::*;
/// const N : usize = 100; // sample size in tests
///
/// // In some case, B-spline basis functions coincide with Bernstein polynomials.
/// let vec = vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
/// let knot_vec = KnotVec::from(vec);
/// let degree = 3;
/// for i in 0..=N {
/// let t = 1.0 / (N as f64) * (i as f64);
/// let res = knot_vec.bspline_basis_functions(degree, t);
/// let ans = [
/// 1.0 * (1.0 - t) * (1.0 - t) * (1.0 - t),
/// 3.0 * t * (1.0 - t) * (1.0 - t),
/// 3.0 * t * t * (1.0 - t),
/// 1.0 * t * t * t,
/// ];
/// for i in 0..4 { assert_near2!(res[i], ans[i]); }
/// }
/// ```
pub fn bspline_basis_functions(&self, degree: usize, t: f64) -> Vec<f64> {
match self.try_bspline_basis_functions(degree, t) {
Ok(got) => got,
Err(error) => panic!("{}", error),
}
}
/// Calculates B-spline basis functions at `t` with degree `degree`.
/// # Failures
/// - If the range of the knot vector is zero, returns [`Error::ZeroRange`].
/// - If the length of `self` is not more than `degree`, returns [`Error::TooLargeDegree`].
/// # Remarks
/// In this package, the B-spline basis function is based on the characteristic function of
/// the right-open intervals [s, t). So, the value corresponding to the end point t = t_n is always 0.0.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// const N : usize = 100; // sample size in tests
///
/// // B-spline basis functions is a partition of unity in (t_k, t_{n - k}).
/// let vec = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
/// let knot_vec = KnotVec::from(vec);
/// let degree = 2;
/// for i in 0..N {
/// let t = 2.0 + 4.0 / (N as f64) * (i as f64);
/// let res = knot_vec.try_bspline_basis_functions(degree, t).unwrap();
/// let sum = res.iter().fold(0.0, |sum, a| sum + a);
/// assert_near2!(sum, 1.0);
/// }
/// ```
/// ```
/// use truck_geometry::prelude::*;
/// const N : usize = 100; // sample size in tests
///
/// // In some case, B-spline basis functions coincide with Bernstein polynomials.
/// let vec = vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
/// let knot_vec = KnotVec::from(vec);
/// let degree = 3;
/// for i in 0..=N {
/// let t = i as f64 / N as f64;
/// let res = knot_vec.try_bspline_basis_functions(degree, t).unwrap();
/// let ans = [
/// 1.0 * (1.0 - t) * (1.0 - t) * (1.0 - t),
/// 3.0 * t * (1.0 - t) * (1.0 - t),
/// 3.0 * t * t * (1.0 - t),
/// 1.0 * t * t * t,
/// ];
/// for i in 0..4 { assert_near2!(res[i], ans[i]); }
/// }
/// ```
pub fn try_bspline_basis_functions(&self, degree: usize, t: f64) -> Result<Vec<f64>> {
let n = self.len() - 1;
if self[0].near(&self[n]) {
return Err(Error::ZeroRange);
} else if n < degree {
return Err(Error::TooLargeDegree(n + 1, degree));
}
let idx = {
let idx = self
.floor(t)
.unwrap_or_else(|| self.floor(self[0]).unwrap());
if idx == n {
n - self.multiplicity(n)
} else {
idx
}
};
let mut res = vec![0.0; n];
res[idx] = 1.0;
for k in 1..=degree {
let base = if idx < k { 0 } else { idx - k };
let delta = self[base + k] - self[base];
let max = if idx + k < n { idx } else { n - k - 1 };
let mut a = inv_or_zero(delta) * (t - self[base]);
for i in base..=max {
let delta = self[i + k + 1] - self[i + 1];
let b = inv_or_zero(delta) * (self[i + k + 1] - t);
res[i] = a * res[i] + b * res[i + 1];
a = 1.0 - b;
}
}
res.truncate(n - degree);
Ok(res)
}
#[doc(hidden)]
pub fn maximum_points(&self, degree: usize) -> Vec<f64> {
let n = self.len();
let m = n - degree - 1;
let range = self.range_length();
const N: i32 = 100;
let mut res = vec![0.0; m];
let mut max = vec![0.0; m];
for i in 1..N {
let t = self[0] + range * (i as f64) / (N as f64);
let vals = self.try_bspline_basis_functions(degree, t).unwrap();
for j in 0..m {
if max[j] < vals[j] {
max[j] = vals[j];
res[j] = t;
}
}
}
res
}
/// Transforms the knot vector
/// # Examples
/// ```
/// use std::vec::Vec;
/// use truck_geometry::prelude::KnotVec;
/// let mut knot_vec = KnotVec::from(vec![1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0]);
/// knot_vec.transform(2.0, 3.0);
/// let res : Vec<f64> = knot_vec.into();
/// assert_eq!(res, vec![5.0, 5.0, 7.0, 9.0, 11.0, 13.0, 13.0]);
/// ```
/// # Panics
/// Panic occurs if `scalar` is not positive.
pub fn transform(&mut self, scalar: f64, r#move: f64) -> &mut Self {
assert!(scalar > 0.0, "The scalar {scalar} is not positive.");
self.0
.iter_mut()
.for_each(move |vec| *vec = *vec * scalar + r#move);
self
}
/// Normalizes the knot vector i.e. makes the first value 0 and the last value 1.
/// # Failures
/// Returns [`Error::ZeroRange`] if the range of the knot vector is so small.
/// # Examples
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let mut knot_vec = KnotVec::from(vec![1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0]);
/// knot_vec.try_normalize().unwrap();
/// let res : Vec<f64> = knot_vec.into();
/// assert_eq!(res, vec![0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0]);
/// ```
pub fn try_normalize(&mut self) -> Result<&mut Self> {
let range = self.range_length();
if range.so_small() {
return Err(Error::ZeroRange);
}
Ok(self.transform(1.0 / range, -self[0] / range))
}
/// Normalizes the knot vector i.e. makes the first value 0 and the last value 1.
/// # Panics
/// Panic occurs if the range of the knot vector is so small.
/// # Examples
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let mut knot_vec = KnotVec::from(vec![1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0]);
/// knot_vec.normalize();
/// let res : Vec<f64> = knot_vec.into();
/// assert_eq!(res, vec![0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0]);
/// ```
#[inline(always)]
pub fn normalize(&mut self) -> &mut Self {
self.try_normalize()
.unwrap_or_else(|error| panic!("{}", error))
}
/// Translates the knot vector
/// # Example
/// ```
/// use std::vec::Vec;
/// use truck_geometry::prelude::KnotVec;
/// let mut knot_vec = KnotVec::from(vec![1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0]);
/// knot_vec.translate(3.0);
/// let res : Vec<f64> = knot_vec.into();
/// assert_eq!(res, vec![4.0, 4.0, 5.0, 6.0, 7.0, 8.0, 8.0]);
/// ```
pub fn translate(&mut self, x: f64) -> &mut Self { self.transform(1.0, x) }
/// Inverts the knot vector
/// # Example
/// ```
/// use std::vec::Vec;
/// use truck_geometry::prelude::KnotVec;
/// let mut knot_vec = KnotVec::from(vec![1.0, 1.0, 1.0, 3.0, 5.0, 6.0]);
/// knot_vec.invert();
/// let res : Vec<f64> = knot_vec.into();
/// assert_eq!(res, vec![1.0, 2.0, 4.0, 6.0, 6.0, 6.0]);
///
/// ```
pub fn invert(&mut self) -> &mut Self {
let n = self.len();
if n == 0 {
return self;
}
let range = self[0] + self[n - 1];
let clone = self.0.clone();
for (knot1, knot0) in clone.iter().rev().zip(&mut self.0) {
*knot0 = range - knot1;
}
self
}
/// Determines the knot vector is clamped for the given degree.
/// # Examples
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let knot_vec = KnotVec::from(vec![0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0]);
/// assert!(knot_vec.is_clamped(2));
/// assert!(!knot_vec.is_clamped(3));
/// ```
#[inline(always)]
pub fn is_clamped(&self, degree: usize) -> bool {
self.multiplicity(0) > degree && self.multiplicity(self.len() - 1) > degree
}
/// Concats two knot vectors.
/// # Examples
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let mut knot_vec0 = KnotVec::from(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
/// let knot_vec1 = KnotVec::from(vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
/// knot_vec0.try_concat(&knot_vec1, 2).unwrap();
/// assert_eq!(knot_vec0.as_slice(), &[0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
/// ```
/// # Failures
/// - If at least one of `self` or `other` is not clamped, returns [`Error::NotClampedKnotVector`]
/// - If the last knot of `self` and the first knot of `other` are different, returns [`Error::DifferentBackFront`].
pub fn try_concat(&mut self, other: &KnotVec, degree: usize) -> Result<&mut Self> {
if !self.is_clamped(degree) || !other.is_clamped(degree) {
return Err(Error::NotClampedKnotVector);
}
let back = self.0.last().unwrap();
let front = other.0.first().unwrap();
if front < back || !front.near(back) {
return Err(Error::DifferentBackFront(*back, *front));
}
self.0.truncate(self.len() - degree - 1);
for knot in &other.0 {
self.0.push(*knot);
}
Ok(self)
}
/// Concats two knot vectors.
/// # Examples
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let mut knot_vec0 = KnotVec::from(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
/// let knot_vec1 = KnotVec::from(vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
/// knot_vec0.concat(&knot_vec1, 2);
/// assert_eq!(knot_vec0.as_slice(), &[0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
/// ```
/// # Panics
/// Panic occurs if:
/// - at least one of `self` or `other` is not clamped.
/// - the last knot of `self` and the first knot of `other` are different.
#[inline(always)]
pub fn concat(&mut self, other: &KnotVec, degree: usize) -> &mut Self {
self.try_concat(other, degree)
.unwrap_or_else(|error| panic!("{}", error))
}
/// Returns trimmed vector by the specified range.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vec = KnotVec::from(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]);
/// let sub_vec = knot_vec.sub_vec(1..3);
/// assert_eq!(sub_vec, KnotVec::from(vec![1.0, 2.0]));
/// ```
#[inline(always)]
pub fn sub_vec<I: SliceIndex<[f64], Output = [f64]>>(&self, range: I) -> KnotVec {
KnotVec(Vec::from(&self.0[range]))
}
/// To single-multi description. i.e. decompose the unique vector of knots and the vector of
/// multiplicity of knots.
/// # Examples
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let knot_vec = KnotVec::from(vec![0.0, 0.0, 0.0, 1.0, 2.0, 2.0, 2.0, 2.0, 3.0, 3.0]);
/// let (knots, mults) = knot_vec.to_single_multi();
/// assert_eq!(knots, vec![0.0, 1.0, 2.0, 3.0]);
/// assert_eq!(mults, vec![3, 1, 4, 2]);
/// ```
pub fn to_single_multi(&self) -> (Vec<f64>, Vec<usize>) {
let mut knots = Vec::new();
let mut mults = Vec::new();
let mut iter = self.as_slice().iter().peekable();
let mut mult = 1;
while let Some(knot) = iter.next() {
if let Some(next) = iter.peek() {
if knot.near(next) {
mult += 1;
} else {
knots.push(*knot);
mults.push(mult);
mult = 1;
}
} else {
knots.push(*knot);
mults.push(mult);
}
}
(knots, mults)
}
/// Constructs from single-multi description.
/// # Examples
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let knots = vec![0.0, 1.0, 2.0, 3.0];
/// let mults = vec![3, 1, 4, 2];
/// let knot_vec = KnotVec::from_single_multi(knots, mults).unwrap();
/// assert_eq!(knot_vec, KnotVec::from(vec![0.0, 0.0, 0.0, 1.0, 2.0, 2.0, 2.0, 2.0, 3.0, 3.0]));
/// ```
pub fn from_single_multi(knots: Vec<f64>, mults: Vec<usize>) -> Result<KnotVec> {
for i in 1..knots.len() {
if knots[i - 1] > knots[i] {
return Err(Error::NotSortedVector);
}
}
let mut vec = Vec::new();
for i in 0..knots.len() {
for _ in 0..mults[i] {
vec.push(knots[i]);
}
}
Ok(KnotVec(vec))
}
/// Constructs from `Vec<f64>`. do not sort, only check sorted.
pub fn try_from(vec: Vec<f64>) -> Result<KnotVec> {
for i in 1..vec.len() {
if vec[i - 1] > vec[i] {
return Err(Error::NotSortedVector);
}
}
Ok(KnotVec(vec))
}
/// Constructs the knot vector for the bezier spline.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// assert_eq!(
/// *KnotVec::bezier_knot(3),
/// vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
/// );
/// ```
pub fn bezier_knot(degree: usize) -> KnotVec {
let mut vec = vec![0.0; degree + 1];
vec.extend(std::iter::repeat(1.0).take(degree + 1));
KnotVec(vec)
}
/// Constructs the uniform knot vector
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// assert_eq!(
/// *KnotVec::uniform_knot(2, 5),
/// vec![0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.0, 1.0],
/// );
/// ```
pub fn uniform_knot(degree: usize, division: usize) -> KnotVec {
let mut vec = vec![0.0; degree + 1];
vec.extend((1..division).map(|i| (i as f64) / (division as f64)));
vec.extend(std::iter::repeat(1.0).take(degree + 1));
KnotVec(vec)
}
}
impl From<Vec<f64>> for KnotVec {
/// constructs from `Vec<f64>`. The vector will sorted by the order.
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let knot_vec = KnotVec::from(vec![1.0, 0.0, 3.0, 2.0]);
/// let arr : Vec<f64> = knot_vec.into();
/// assert_eq!(arr, vec![0.0, 1.0, 2.0, 3.0]);
/// ```
fn from(mut vec: Vec<f64>) -> KnotVec {
vec.sort_by(|a, b| a.partial_cmp(b).unwrap());
KnotVec(vec)
}
}
impl From<&Vec<f64>> for KnotVec {
/// Constructs by the reference of vector. The clone of vector is sorted by the order.
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let knot_vec = KnotVec::from(&vec![1.0, 0.0, 3.0, 2.0]);
/// let arr : Vec<f64> = knot_vec.into();
/// assert_eq!(arr, vec![0.0, 1.0, 2.0, 3.0]);
/// ```
#[inline(always)]
fn from(vec: &Vec<f64>) -> KnotVec {
let mut copy_vec = vec.clone();
copy_vec.sort_by(|a, b| a.partial_cmp(b).unwrap());
KnotVec(copy_vec)
}
}
impl From<KnotVec> for Vec<f64> {
/// `KnotVec` into `Vec<f64>`
/// ```
/// use truck_geometry::prelude::KnotVec;
/// let vec = vec![0.0, 1.0, 2.0, 3.0];
/// let knot_vec = KnotVec::from(&vec);
/// let vec0 : Vec<f64> = knot_vec.into();
/// assert_eq!(vec, vec0);
/// ```
#[inline(always)]
fn from(knotvec: KnotVec) -> Vec<f64> { knotvec.0 }
}
impl FromIterator<f64> for KnotVec {
#[inline(always)]
fn from_iter<I: IntoIterator<Item = f64>>(iter: I) -> KnotVec {
KnotVec::try_from(iter.into_iter().collect::<Vec<_>>()).unwrap()
}
}
impl<'a> IntoIterator for &'a KnotVec {
type Item = &'a f64;
type IntoIter = std::slice::Iter<'a, f64>;
#[inline(always)]
fn into_iter(self) -> Self::IntoIter { self.0.iter() }
}
impl std::ops::Deref for KnotVec {
type Target = Vec<f64>;
#[inline(always)]
fn deref(&self) -> &Vec<f64> { &self.0 }
}
impl AsRef<[f64]> for KnotVec {
#[inline(always)]
fn as_ref(&self) -> &[f64] { &self.0 }
}
impl<'de> Deserialize<'de> for KnotVec {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where D: serde::Deserializer<'de> {
let vec = Vec::<f64>::deserialize(deserializer)?;
Self::try_from(vec).map_err(serde::de::Error::custom)
}
}
-135
View File
@@ -1,135 +0,0 @@
use crate::{prelude::*, *};
use truck_base::cgmath64::control_point::ControlPoint;
/// knot vector
#[derive(Clone, PartialEq, Debug, Default, Serialize)]
pub struct KnotVec(Vec<f64>);
/// B-spline curve
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
///
/// // the knot vector
/// let knot_vec = KnotVec::from(
/// vec![0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0]
/// );
///
/// // sign up the control points in the vector of all points
/// let ctrl_pts = vec![ // the vector of the indices of control points
/// Vector4::new(0.0, -2.0, 0.0, 2.0),
/// Vector4::new(1.0, -1.0, 0.0, 1.0),
/// Vector4::new(1.0, 0.0, 0.0, 1.0),
/// Vector4::new(1.0, 1.0, 0.0, 1.0),
/// Vector4::new(0.0, 2.0, 0.0, 2.0),
/// Vector4::new(-1.0, 1.0, 0.0, 1.0),
/// Vector4::new(-1.0, 0.0, 0.0, 1.0),
/// Vector4::new(-1.0, -1.0, 0.0, 1.0),
/// Vector4::new(0.0, -2.0, 0.0, 2.0),
/// ];
///
/// // construct the B-spline curve
/// let bspline = BSplineCurve::new(knot_vec, ctrl_pts);
///
/// // This B-spline curve is a nurbs representation of the unit circle.
/// const N : usize = 100; // sample size in test
/// for i in 0..N {
/// let t = 1.0 / (N as f64) * (i as f64);
/// let v = bspline.subs(t); // We can use the instances as a function.
/// let c = (v[0] / v[3]).powi(2) + (v[1] / v[3]).powi(2);
/// assert_near2!(c, 1.0);
/// }
/// ```
#[derive(Clone, PartialEq, Debug, Serialize)]
pub struct BSplineCurve<P> {
knot_vec: KnotVec, // the knot vector
control_points: Vec<P>, // the indices of control points
}
/// B-spline surface
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// const N : usize = 100; // sample size in test
///
/// // the knot vectors
/// let knot_vec0 = KnotVec::bezier_knot(3);
/// let knot_vec1 = KnotVec::from(
/// vec![0.0, 0.0, 0.0, 0.0, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0]
/// );
/// let knot_vecs = (knot_vec0, knot_vec1);
///
/// // the control points
/// let mut v = vec![vec![Vector4::zero(); 7]; 4];
/// v[0][0] = Vector4::new(0.0, 0.0, 1.0, 1.0);
/// v[0][1] = &v[0][0] / 3.0;
/// v[0][2] = v[0][1].clone();
/// v[0][3] = v[0][0].clone();
/// v[0][4] = v[0][1].clone();
/// v[0][5] = v[0][1].clone();
/// v[0][6] = v[0][0].clone();
/// v[1][0] = Vector4::new(2.0, 0.0, 1.0, 1.0) / 3.0;
/// v[1][1] = Vector4::new(2.0, 4.0, 1.0, 1.0) / 9.0;
/// v[1][2] = Vector4::new(-2.0, 4.0, 1.0, 1.0) / 9.0;
/// v[1][3] = Vector4::new(-2.0, 0.0, 1.0, 1.0) / 3.0;
/// v[1][4] = Vector4::new(-2.0, -4.0, 1.0, 1.0) / 9.0;
/// v[1][5] = Vector4::new(2.0, -4.0, 1.0, 1.0) / 9.0;
/// v[1][6] = Vector4::new(2.0, 0.0, 1.0, 1.0) / 3.0;
/// v[2][0] = Vector4::new(2.0, 0.0, -1.0, 1.0) / 3.0;
/// v[2][1] = Vector4::new(2.0, 4.0, -1.0, 1.0) / 9.0;
/// v[2][2] = Vector4::new(-2.0, 4.0, -1.0, 1.0) / 9.0;
/// v[2][3] = Vector4::new(-2.0, 0.0, -1.0, 1.0) / 3.0;
/// v[2][4] = Vector4::new(-2.0, -4.0, -1.0, 1.0) / 9.0;
/// v[2][5] = Vector4::new(2.0, -4.0, -1.0, 1.0) / 9.0;
/// v[2][6] = Vector4::new(2.0, 0.0, -1.0, 1.0) / 3.0;
/// v[3][0] = Vector4::new(0.0, 0.0, -1.0, 1.0);
/// v[3][1] = &v[3][0] / 3.0;
/// v[3][2] = v[3][1].clone();
/// v[3][3] = v[3][0].clone();
/// v[3][4] = v[3][1].clone();
/// v[3][5] = v[3][1].clone();
/// v[3][6] = v[3][0].clone();
///
/// // cunstruct the B-spline curve
/// let bspline = BSplineSurface::new(knot_vecs, v);
///
/// // This B-spline curve is a nurbs representation of the unit sphere.
/// for i in 0..N {
/// for j in 0..N {
/// let u = 1.0 / (N as f64) * (i as f64);
/// let v = 1.0 / (N as f64) * (j as f64);
/// let v = bspline.subs(u, v); // We can use the instances as a function.
/// let c = (v[0] / v[3]).powi(2) + (v[1] / v[3]).powi(2) + (v[2] / v[3]).powi(2);
/// assert_near2!(c, 1.0);
/// }
/// }
/// ```
#[derive(Clone, PartialEq, Debug, Serialize)]
pub struct BSplineSurface<P> {
knot_vecs: (KnotVec, KnotVec),
control_points: Vec<Vec<P>>,
}
/// NURBS curve
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct NurbsCurve<V>(BSplineCurve<V>);
/// NURBS surface
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct NurbsSurface<V>(BSplineSurface<V>);
mod bspcurve;
mod bspsurface;
mod knot_vec;
mod nurbscurve;
mod nurbssurface;
#[doc(hidden)]
#[inline(always)]
pub fn inv_or_zero(delta: f64) -> f64 {
if delta.so_small() {
0.0
} else {
1.0 / delta
}
}
-619
View File
@@ -1,619 +0,0 @@
use super::*;
impl<V> NurbsCurve<V> {
/// Constructs the rationalized B-spline curve.
#[inline(always)]
pub const fn new(curve: BSplineCurve<V>) -> Self { NurbsCurve(curve) }
/// Returns the BSpline curve before rationalized.
#[inline(always)]
pub const fn non_rationalized(&self) -> &BSplineCurve<V> { &self.0 }
/// Returns the BSpline curve before rationalized.
#[inline(always)]
pub fn into_non_rationalized(self) -> BSplineCurve<V> { self.0 }
/// Returns the reference of the knot vector. cf.[`BSplineCurve::knot_vec`]
#[inline(always)]
pub const fn knot_vec(&self) -> &KnotVec { &self.0.knot_vec }
/// Returns the `idx`th knot. cf.[`BSplineCurve::knot`]
#[inline(always)]
pub fn knot(&self, idx: usize) -> f64 { self.0.knot_vec[idx] }
/// Returns the reference of the control points. cf.[`BSplineCurve::control_points`]
#[inline(always)]
pub const fn control_points(&self) -> &Vec<V> { &self.0.control_points }
/// Returns the reference of the control point corresponding to the index `idx`.
/// cf.[`BSplineCurve::control_point`]
#[inline(always)]
pub fn control_point(&self, idx: usize) -> &V { &self.0.control_points[idx] }
/// Returns the mutable reference of the control point corresponding to index `idx`.
/// cf.[`BSplineCurve::control_point_mut`]
#[inline(always)]
pub fn control_point_mut(&mut self, idx: usize) -> &mut V { &mut self.0.control_points[idx] }
/// Returns the iterator on all control points. cf.[`BSplineCurve::control_points_mut`]
#[inline(always)]
pub fn control_points_mut(&mut self) -> impl Iterator<Item = &mut V> {
self.0.control_points.iter_mut()
}
/// Applies the given transformation to all control points. cf.[`BSplineCurve::transform_control_points`]
#[inline(always)]
pub fn transform_control_points<F: FnMut(&mut V)>(&mut self, f: F) {
self.0.transform_control_points(f)
}
/// Returns the degree of NURBS curve. cf.[`BSplineCurve::degree`]
#[inline(always)]
pub fn degree(&self) -> usize { self.0.degree() }
/// Inverts a curve. cf.[`BSplineCurve::invert`]
#[inline(always)]
pub fn invert(&mut self) -> &mut Self {
self.0.invert();
self
}
/// Returns whether the knot vector is clamped or not. cf.[`BSplineCurve::is_clamped`]
#[inline(always)]
pub fn is_clamped(&self) -> bool { self.0.knot_vec.is_clamped(self.0.degree()) }
/// Normalizes the knot vector. cf.[`BSplineCurve::knot_normalize`]
#[inline(always)]
pub fn knot_normalize(&mut self) -> &mut Self {
self.0.knot_vec.try_normalize().unwrap();
self
}
/// Translates the knot vector. cf.[`BSplineCurve::knot_translate`]
#[inline(always)]
pub fn knot_translate(&mut self, x: f64) -> &mut Self {
self.0.knot_vec.translate(x);
self
}
}
impl<V: Homogeneous<f64>> NurbsCurve<V> {
/// Constructs a rationalization curve from the non-rationalized curve and weights.
/// # Failures
/// the length of `curve.control_points()` and `weights` must be the same.
#[inline(always)]
pub fn try_from_bspline_and_weights(
curve: BSplineCurve<V::Point>,
weights: Vec<f64>,
) -> Result<Self> {
let BSplineCurve {
knot_vec,
control_points,
} = curve;
if control_points.len() != weights.len() {
return Err(Error::DifferentLength);
}
let control_points = control_points
.into_iter()
.zip(weights)
.map(|(pt, w)| V::from_point_weight(pt, w))
.collect();
Ok(Self(BSplineCurve::new_unchecked(knot_vec, control_points)))
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V>> NurbsCurve<V> {
/// Returns the closure of substitution.
#[inline(always)]
pub fn get_closure(&self) -> impl Fn(f64) -> V::Point + '_ { move |t| self.subs(t) }
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V>> NurbsCurve<V>
where V::Point: Tolerance
{
/// Returns whether all control points are the same or not.
/// If the knot vector is clamped, it means whether the curve is constant or not.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
///
/// let knot_vec = KnotVec::bezier_knot(2);
/// let pt = Vector3::new(1.0, 2.0, 1.0);
/// // allows differences upto scalars
/// let mut ctrl_pts = vec![pt.clone(), pt.clone() * 2.0, pt.clone() * 3.0];
/// let bspcurve = BSplineCurve::new(knot_vec.clone(), ctrl_pts.clone());
/// assert!(!bspcurve.is_const());
/// let const_curve = NurbsCurve::new(bspcurve);
/// assert!(const_curve.is_const());
///
/// ctrl_pts.push(Vector3::new(2.0, 3.0, 1.0));
/// let curve = NurbsCurve::new(BSplineCurve::new(knot_vec.clone(), ctrl_pts.clone()));
/// assert!(!curve.is_const());
/// ```
/// # Remarks
/// If the knot vector is not clamped and the BSpline basis function is not partition of unity,
/// then perhaps returns true even if the curve is not constant.
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vec = KnotVec::uniform_knot(1, 5);
/// let ctrl_pts = vec![Vector2::new(1.0, 2.0), Vector2::new(1.0, 2.0)];
/// let bspcurve = BSplineCurve::new(knot_vec, ctrl_pts);
///
/// // bspcurve is not constant.
/// assert_eq!(bspcurve.subs(0.0), Vector2::new(0.0, 0.0));
/// assert_ne!(bspcurve.subs(0.5), Vector2::new(0.0, 0.0));
///
/// // bspcurve.is_const() is true
/// assert!(bspcurve.is_const());
/// ```
pub fn is_const(&self) -> bool {
let pt = self.0.control_points[0].to_point();
self.0
.control_points
.iter()
.all(move |vec| vec.to_point().near(&pt))
}
/// Determines whether `self` and `other` is near as the B-spline curves or not.
///
/// Divides each knot interval into the number of degree equal parts,
/// and check `|self(t) - other(t)| < TOLERANCE` for each end points `t`.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vec = KnotVec::from(
/// vec![0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 4.0, 4.0]
/// );
/// let ctrl_pts = vec![
/// Vector3::new(1.0, 1.0, 1.0),
/// Vector3::new(3.0, 2.0, 2.0),
/// Vector3::new(2.0, 3.0, 1.0),
/// Vector3::new(4.0, 5.0, 2.0),
/// Vector3::new(5.0, 4.0, 1.0),
/// Vector3::new(1.0, 1.0, 2.0),
/// ];
/// let curve0 = NurbsCurve::new(BSplineCurve::new(knot_vec, ctrl_pts));
/// let mut curve1 = curve0.clone();
/// assert!(curve0.near_as_curve(&curve1));
/// *curve1.control_point_mut(1) += Vector3::new(0.01, 0.0002, 0.0);
/// assert!(!curve0.near_as_curve(&curve1));
/// ```
#[inline(always)]
pub fn near_as_curve(&self, other: &Self) -> bool {
self.0
.sub_near_as_curve(&other.0, 2, move |x, y| x.to_point().near(&y.to_point()))
}
/// Determines `self` and `other` is near in square order as the B-spline curves or not.
///
/// Divide each knot interval into the number of degree equal parts,
/// and check `|self(t) - other(t)| < TOLERANCE`for each end points `t`.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vec = KnotVec::from(
/// vec![0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 4.0, 4.0]
/// );
/// let ctrl_pts = vec![
/// Vector3::new(1.0, 1.0, 1.0),
/// Vector3::new(3.0, 2.0, 2.0),
/// Vector3::new(2.0, 3.0, 1.0),
/// Vector3::new(4.0, 5.0, 2.0),
/// Vector3::new(5.0, 4.0, 1.0),
/// Vector3::new(1.0, 1.0, 2.0),
/// ];
/// let curve0 = NurbsCurve::new(BSplineCurve::new(knot_vec, ctrl_pts));
/// let mut curve1 = curve0.clone();
/// assert!(curve0.near_as_curve(&curve1));
/// *curve1.control_point_mut(1) += Vector3::new(0.01, TOLERANCE, 0.0);
/// assert!(!curve0.near2_as_curve(&curve1));
/// ```
#[inline(always)]
pub fn near2_as_curve(&self, other: &Self) -> bool {
self.0
.sub_near_as_curve(&other.0, 2, move |x, y| x.to_point().near2(&y.to_point()))
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V> + Tolerance> NurbsCurve<V> {
/// Adds a knot `x`, and do not change `self` as a curve. cf.[`BSplineCurve::add_knot`]
pub fn add_knot(&mut self, x: f64) -> &mut Self {
self.0.add_knot(x);
self
}
/// Removes a knot corresponding to the indices `idx`, and do not change `self` as a curve.
/// If cannot remove the knot, do not change `self` and return `self`.
/// cf.[`BSplineCurve::remove_knot`]
pub fn remove_knot(&mut self, idx: usize) -> &mut Self {
let _ = self.try_remove_knot(idx);
self
}
/// Removes a knot corresponding to the indice `idx`, and do not change `self` as a curve.
/// If the knot cannot be removed, returns [`Error::CannotRemoveKnot`].
/// cf.[`BSplineCurve::try_remove_knot`]
pub fn try_remove_knot(&mut self, idx: usize) -> Result<&mut Self> {
self.0.try_remove_knot(idx)?;
Ok(self)
}
/// Elevates 1 degree. cf.[`BSplineCurve::elevate_degree`]
pub fn elevate_degree(&mut self) -> &mut Self {
self.0.elevate_degree();
self
}
/// Makes the NURBS curve clamped. cf.[`BSplineCurve::clamp`]
#[inline(always)]
pub fn clamp(&mut self) -> &mut Self {
self.0.clamp();
self
}
/// Repeats `Self::try_remove_knot()` from the back knot in turn until the knot cannot be removed.
/// cf.[`BSplineCurve::optimize`]
pub fn optimize(&mut self) -> &mut Self {
self.0.optimize();
self
}
/// Makes two splines having the same degrees. cf.[`BSplineCurve::syncro_degree`]
pub fn syncro_degree(&mut self, other: &mut Self) {
let (degree0, degree1) = (self.degree(), other.degree());
for _ in degree0..degree1 {
self.elevate_degree();
}
for _ in degree1..degree0 {
other.elevate_degree();
}
}
/// Makes two splines having the same normalized knot vectors. cf.[`BSplineCurve::syncro_knots`]
pub fn syncro_knots(&mut self, other: &mut Self) { self.0.syncro_knots(&mut other.0) }
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V> + Tolerance> ParameterTransform
for NurbsCurve<V>
{
#[inline(always)]
fn parameter_transform(&mut self, scalar: f64, r#move: f64) -> &mut Self {
self.0.parameter_transform(scalar, r#move);
self
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V> + Tolerance> Cut for NurbsCurve<V> {
#[inline(always)]
fn cut(&mut self, t: f64) -> Self { NurbsCurve(self.0.cut(t)) }
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V> + Tolerance> Concat<NurbsCurve<V>>
for NurbsCurve<V>
where <V as Homogeneous<f64>>::Point: Debug
{
type Output = NurbsCurve<V>;
fn try_concat(
&self,
other: &Self,
) -> std::result::Result<Self, ConcatError<<V as Homogeneous<f64>>::Point>> {
let curve0 = self.clone();
let mut curve1 = other.clone();
let w0 = curve0.0.control_points.last().unwrap().weight();
let w1 = curve1.0.control_points[0].weight();
curve1.transform_control_points(|pt| *pt *= w0 / w1);
match curve0.0.try_concat(&curve1.0) {
Ok(curve) => Ok(NurbsCurve::new(curve)),
Err(err) => Err(err.point_map(|v| v.to_point())),
}
}
}
#[test]
fn concat_positive_test() {
let mut part0 = NurbsCurve::new(BSplineCurve::new(
KnotVec::uniform_knot(4, 4),
(0..8)
.map(|_| {
Vector4::new(
rand::random::<f64>(),
rand::random::<f64>(),
rand::random::<f64>(),
rand::random::<f64>() + 0.5,
)
})
.collect(),
));
let mut part1 = part0.cut(0.56);
let w = 20.0 * rand::random::<f64>() - 10.0;
part1.transform_control_points(|vec| *vec *= w);
assert_near!(part0.back(), part1.front());
concat_random_test(&part0, &part1, 10);
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V> + Tolerance> NurbsCurve<V>
where V::Point: Tolerance
{
/// Makes the rational curve locally injective.
/// # Example
/// ```
/// use truck_geometry::prelude::*;
/// const N : usize = 100; // sample size for test
///
/// let knot_vec = KnotVec::from(
/// vec![0.0, 0.0, 0.0, 1.0, 3.0, 4.0, 4.0, 4.0]
/// );
/// let control_points = vec![
/// Vector4::new(1.0, 0.0, 0.0, 1.0),
/// Vector4::new(0.0, 1.0, 0.0, 1.0),
/// Vector4::new(0.0, 2.0, 0.0, 2.0),
/// Vector4::new(0.0, 3.0, 0.0, 3.0),
/// Vector4::new(0.0, 0.0, 3.0, 3.0),
/// ];
///
/// let mut curve = NurbsCurve::new(BSplineCurve::new(knot_vec, control_points));
/// let mut flag = false;
/// for i in 0..N {
/// let t = 4.0 * (i as f64) / (N as f64);
/// let pt0 = curve.subs(t);
/// let pt1 = curve.subs(t + 1.0 / (N as f64));
/// flag = flag || pt0.near(&pt1);
/// }
/// // There exists t such that bspcurve(t) == bspcurve(t + 0.01).
/// assert!(flag);
///
/// curve.make_locally_injective().knot_normalize();
/// let mut flag = false;
/// for i in 0..N {
/// let t = 1.0 * (i as f64) / (N as f64);
/// let pt0 = curve.subs(t);
/// let pt1 = curve.subs(t + 1.0 / (N as f64));
/// flag = flag || pt0.near(&pt1);
/// }
/// // There does not exist t such that bspcurve(t) == bspcurve(t + 0.01).
/// assert!(!flag);
/// ```
/// # Remarks
/// If `self` is a constant curve, then does nothing.
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vec = KnotVec::from(vec![0.0, 0.0, 0.0, 1.0, 2.0, 2.0, 2.0]);
/// let ctrl_pts = vec![
/// Vector3::new(1.0, 1.0, 1.0),
/// Vector3::new(2.0, 2.0, 2.0),
/// Vector3::new(3.0, 3.0, 3.0),
/// Vector3::new(4.0, 4.0, 4.0),
/// ];
/// let mut curve = NurbsCurve::new(BSplineCurve::new(knot_vec, ctrl_pts));
/// let org_curve = curve.clone();
/// curve.make_locally_injective();
/// assert_eq!(curve, org_curve);
/// ```
pub fn make_locally_injective(&mut self) -> &mut Self {
let mut iter = self.0.bezier_decomposition().into_iter();
while let Some(bezier) = iter.next().map(NurbsCurve::new) {
if !bezier.is_const() {
*self = bezier;
break;
}
}
let mut x = 0.0;
for mut bezier in iter.map(NurbsCurve::new) {
if bezier.is_const() {
x += bezier.0.knot_vec.range_length();
} else {
let s0 = self.0.control_points.last().unwrap().weight();
let s1 = bezier.0.control_points[0].weight();
bezier
.0
.control_points
.iter_mut()
.for_each(move |vec| *vec *= s0 / s1);
self.concat(bezier.knot_translate(-x));
}
}
self
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V>> ParameterDivision1D for NurbsCurve<V>
where V::Point: MetricSpace<Metric = f64> + HashGen<f64>
{
type Point = V::Point;
#[inline(always)]
fn parameter_division(&self, range: (f64, f64), tol: f64) -> (Vec<f64>, Vec<V::Point>) {
algo::curve::parameter_division(self, range, tol)
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V>> SearchNearestParameter<D1> for NurbsCurve<V>
where
V::Point: MetricSpace<Metric = f64>,
<V::Point as EuclideanSpace>::Diff: InnerSpace + Tolerance,
{
type Point = V::Point;
/// Searches the parameter `t` which minimize |self(t) - point| by Newton's method with initial guess `hint`.
/// Returns `None` if the number of attempts exceeds `trial` i.e. if `trial == 0`, then the trial is only one time.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
///
/// // Defines the half unit circle in x > 0.
/// let knot_vec = KnotVec::bezier_knot(2);
/// let ctrl_pts = vec![Vector3::new(0.0, -1.0, 1.0), Vector3::new(1.0, 0.0, 0.0), Vector3::new(0.0, 1.0, 1.0)];
/// let curve = NurbsCurve::new(BSplineCurve::new(knot_vec, ctrl_pts));
///
/// // search rational nearest parameter
/// let pt = Point2::new(1.0, 2.0);
/// let hint = 0.8;
/// let t = curve.search_nearest_parameter(pt, Some(hint), 100).unwrap();
///
/// // check the answer
/// let res = curve.subs(t);
/// let ans = Point2::from_vec(pt.to_vec().normalize());
/// assert_near!(res, ans);
/// ```
/// # Remarks
/// It may converge to a local solution depending on the hint.
/// ```
/// use truck_geometry::prelude::*;
///
/// // Same curve and point as above example
/// let knot_vec = KnotVec::bezier_knot(2);
/// let ctrl_pts = vec![Vector3::new(0.0, -1.0, 1.0), Vector3::new(1.0, 0.0, 0.0), Vector3::new(0.0, 1.0, 1.0)];
/// let curve = NurbsCurve::new(BSplineCurve::new(knot_vec, ctrl_pts));
/// let pt = Point2::new(1.0, 2.0);
///
/// // another hint
/// let hint = 0.5;
///
/// // Newton's method is vibration divergent.
/// assert!(curve.search_nearest_parameter(pt, Some(hint), 100).is_none());
/// ```
#[inline(always)]
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
point: V::Point,
hint: H,
trial: usize,
) -> Option<f64> {
let hint = match hint.into() {
SPHint1D::Parameter(hint) => hint,
SPHint1D::Range(x, y) => {
algo::curve::presearch(self, point, (x, y), PRESEARCH_DIVISION)
}
SPHint1D::None => {
algo::curve::presearch(self, point, self.range_tuple(), PRESEARCH_DIVISION)
}
};
algo::curve::search_nearest_parameter(self, point, hint, trial)
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V>> SearchParameter<D1> for NurbsCurve<V>
where
V::Point: MetricSpace<Metric = f64>,
<V::Point as EuclideanSpace>::Diff: InnerSpace + Tolerance,
{
type Point = V::Point;
#[inline(always)]
fn search_parameter<H: Into<SPHint1D>>(
&self,
point: V::Point,
hint: H,
trial: usize,
) -> Option<f64> {
let hint = match hint.into() {
SPHint1D::Parameter(hint) => hint,
SPHint1D::Range(x, y) => {
algo::curve::presearch(self, point, (x, y), PRESEARCH_DIVISION)
}
SPHint1D::None => {
algo::curve::presearch(self, point, self.range_tuple(), PRESEARCH_DIVISION)
}
};
algo::curve::search_parameter(self, point, hint, trial)
}
}
impl<V: Homogeneous<f64>> NurbsCurve<V>
where V::Point: Bounded<Scalar = f64>
{
/// Returns the bounding box including all control points.
#[inline(always)]
pub fn roughly_bounding_box(&self) -> BoundingBox<V::Point> {
self.0.control_points.iter().map(|p| p.to_point()).collect()
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V>> ParametricCurve for NurbsCurve<V> {
type Point = V::Point;
type Vector = <V::Point as EuclideanSpace>::Diff;
#[inline(always)]
fn subs(&self, t: f64) -> Self::Point { self.0.subs(t).to_point() }
#[inline(always)]
fn der(&self, t: f64) -> Self::Vector {
let pt = self.0.subs(t);
let der = self.0.der(t);
pt.rat_der(der)
}
#[inline(always)]
fn der2(&self, t: f64) -> Self::Vector {
let pt = self.0.subs(t);
let der = self.0.der(t);
let der2 = self.0.der2(t);
pt.rat_der2(der, der2)
}
#[inline(always)]
fn parameter_range(&self) -> ParameterRange {
(
Bound::Included(self.0.knot_vec[0]),
Bound::Included(self.0.knot_vec[self.0.knot_vec.len() - 1]),
)
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V>> BoundedCurve for NurbsCurve<V> {}
impl<V: Clone> Invertible for NurbsCurve<V> {
#[inline(always)]
fn invert(&mut self) { self.invert(); }
#[inline(always)]
fn inverse(&self) -> Self {
let mut curve = self.0.clone();
curve.invert();
NurbsCurve(curve)
}
}
impl<M, V: Copy> Transformed<M> for NurbsCurve<V>
where M: Copy + std::ops::Mul<V, Output = V>
{
#[inline(always)]
fn transform_by(&mut self, trans: M) {
self.0
.control_points
.iter_mut()
.for_each(move |v| *v = trans * *v)
}
}
impl<V: Homogeneous<f64>> From<BSplineCurve<V::Point>> for NurbsCurve<V> {
fn from(bspcurve: BSplineCurve<V::Point>) -> NurbsCurve<V> {
NurbsCurve::new(BSplineCurve::new_unchecked(
bspcurve.knot_vec,
bspcurve
.control_points
.into_iter()
.map(V::from_point)
.collect(),
))
}
}
#[test]
fn test_parameter_division() {
let knot_vec = KnotVec::uniform_knot(2, 3);
let ctrl_pts = vec![
Vector4::new(0.0, 0.0, 0.0, 1.0),
Vector4::new(2.0, 0.0, 0.0, 2.0),
Vector4::new(0.0, 3.0, 0.0, 3.0),
Vector4::new(0.0, 0.0, 2.0, 2.0),
Vector4::new(1.0, 1.0, 1.0, 1.0),
];
let curve = NurbsCurve::new(BSplineCurve::new(knot_vec, ctrl_pts));
let tol = 0.01;
let (div, pts) = curve.parameter_division(curve.range_tuple(), tol * 0.5);
let knot_vec = curve.knot_vec();
assert_eq!(knot_vec[0], div[0]);
assert_eq!(knot_vec.range_length(), div.last().unwrap() - div[0]);
for i in 1..div.len() {
let pt0 = curve.subs(div[i - 1]);
assert_eq!(pt0, pts[i - 1]);
let pt1 = curve.subs(div[i]);
assert_eq!(pt1, pts[i]);
let value_middle = pt0.midpoint(pt1);
let param_middle = curve.subs((div[i - 1] + div[i]) / 2.0);
let dist = value_middle.distance(param_middle);
assert!(dist < tol, "large distance: {dist}");
}
}
-920
View File
@@ -1,920 +0,0 @@
use super::*;
impl<V> NurbsSurface<V> {
/// constructor
#[inline(always)]
pub const fn new(bspsurface: BSplineSurface<V>) -> Self { NurbsSurface(bspsurface) }
/// Returns the nurbs surface before rationalized
#[inline(always)]
pub const fn non_rationalized(&self) -> &BSplineSurface<V> { &self.0 }
/// Returns the nurbs surface before rationalized
#[inline(always)]
pub fn non_rationalized_mut(&mut self) -> &mut BSplineSurface<V> { &mut self.0 }
/// Returns the nurbs surface before rationalized
#[inline(always)]
pub fn into_non_rationalized(self) -> BSplineSurface<V> { self.0 }
/// Returns the reference of the knot vectors
#[inline(always)]
pub const fn knot_vecs(&self) -> &(KnotVec, KnotVec) { &self.0.knot_vecs }
/// Returns the u knot vector.
#[inline(always)]
pub const fn uknot_vec(&self) -> &KnotVec { &self.0.knot_vecs.0 }
/// Returns the v knot vector.
#[inline(always)]
pub const fn vknot_vec(&self) -> &KnotVec { &self.0.knot_vecs.1 }
/// Returns the `idx`th u knot.
#[inline(always)]
pub fn uknot(&self, idx: usize) -> f64 { self.0.knot_vecs.0[idx] }
/// returns the `idx`th v knot.
#[inline(always)]
pub fn vknot(&self, idx: usize) -> f64 { self.0.knot_vecs.1[idx] }
/// Returns the reference of the vector of the control points
#[inline(always)]
pub const fn control_points(&self) -> &Vec<Vec<V>> { &self.0.control_points }
/// Returns the reference of the control point corresponding to the index `(idx0, idx1)`.
#[inline(always)]
pub fn control_point(&self, idx0: usize, idx1: usize) -> &V {
&self.0.control_points[idx0][idx1]
}
/// Apply the given transformation to all control points.
#[inline(always)]
pub fn transform_control_points<F: FnMut(&mut V)>(&mut self, f: F) {
self.0.transform_control_points(f)
}
/// Returns the iterator over the control points in the `column_idx`th row.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let uknot_vec = KnotVec::bezier_knot(1);
/// let vknot_vec = KnotVec::bezier_knot(2);
/// let knot_vecs = (uknot_vec, vknot_vec);
/// let ctrl_pts = vec![
/// vec![Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 1.0), Vector3::new(2.0, 0.0, 2.0)],
/// vec![Vector3::new(0.0, 1.0, 0.0), Vector3::new(1.0, 1.0, 1.0), Vector3::new(2.0, 1.0, 2.0)],
/// ];
/// let bspsurface = BSplineSurface::new(knot_vecs, ctrl_pts);
/// let mut iter = bspsurface.ctrl_pts_row_iter(1);
/// assert_eq!(iter.next(), Some(&Vector3::new(1.0, 0.0, 1.0)));
/// assert_eq!(iter.next(), Some(&Vector3::new(1.0, 1.0, 1.0)));
/// assert_eq!(iter.next(), None);
/// ```
#[inline(always)]
pub fn ctrl_pts_row_iter(
&self,
column_idx: usize,
) -> impl ExactSizeIterator<Item = &V> + std::iter::FusedIterator<Item = &V> {
self.0.ctrl_pts_row_iter(column_idx)
}
/// Returns the iterator over the control points in the `row_idx`th row.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let uknot_vec = KnotVec::bezier_knot(1);
/// let vknot_vec = KnotVec::bezier_knot(2);
/// let knot_vecs = (uknot_vec, vknot_vec);
/// let ctrl_pts = vec![
/// vec![Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 1.0), Vector3::new(2.0, 0.0, 2.0)],
/// vec![Vector3::new(0.0, 1.0, 0.0), Vector3::new(1.0, 1.0, 1.0), Vector3::new(2.0, 1.0, 2.0)],
/// ];
/// let bspsurface = BSplineSurface::new(knot_vecs, ctrl_pts);
/// let mut iter = bspsurface.ctrl_pts_column_iter(1);
/// assert_eq!(iter.next(), Some(&Vector3::new(0.0, 1.0, 0.0)));
/// assert_eq!(iter.next(), Some(&Vector3::new(1.0, 1.0, 1.0)));
/// assert_eq!(iter.next(), Some(&Vector3::new(2.0, 1.0, 2.0)));
/// assert_eq!(iter.next(), None);
/// ```
#[inline(always)]
pub fn ctrl_pts_column_iter(&self, row_idx: usize) -> std::slice::Iter<'_, V> {
self.0.control_points[row_idx].iter()
}
/// Returns the mutable reference of the control point corresponding to index `(idx0, idx1)`.
#[inline(always)]
pub fn control_point_mut(&mut self, idx0: usize, idx1: usize) -> &mut V {
&mut self.0.control_points[idx0][idx1]
}
/// Returns the iterator on all control points
#[inline(always)]
pub fn control_points_mut(&mut self) -> impl Iterator<Item = &mut V> {
self.0.control_points.iter_mut().flatten()
}
/// Returns the degrees of B-spline surface
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let uknot_vec = KnotVec::from(vec![0.0, 0.0, 1.0, 1.0]);
/// let vknot_vec = KnotVec::from(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
/// let knot_vecs = (uknot_vec, vknot_vec);
/// let ctrl_pts = vec![
/// vec![Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 1.0), Vector3::new(2.0, 0.0, 2.0)],
/// vec![Vector3::new(0.0, 1.0, 0.0), Vector3::new(1.0, 1.0, 1.0), Vector3::new(2.0, 1.0, 2.0)],
/// ];
/// let bspsurface = BSplineSurface::new(knot_vecs, ctrl_pts);
/// assert_eq!(bspsurface.udegree(), 1);
/// ```
#[inline(always)]
pub fn udegree(&self) -> usize { self.0.udegree() }
/// Returns the degrees of B-spline surface
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let uknot_vec = KnotVec::from(vec![0.0, 0.0, 1.0, 1.0]);
/// let vknot_vec = KnotVec::from(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
/// let knot_vecs = (uknot_vec, vknot_vec);
/// let ctrl_pts = vec![
/// vec![Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 1.0), Vector3::new(2.0, 0.0, 2.0)],
/// vec![Vector3::new(0.0, 1.0, 0.0), Vector3::new(1.0, 1.0, 1.0), Vector3::new(2.0, 1.0, 2.0)],
/// ];
/// let bspsurface = BSplineSurface::new(knot_vecs, ctrl_pts);
/// assert_eq!(bspsurface.vdegree(), 2);
/// ```
#[inline(always)]
pub fn vdegree(&self) -> usize { self.0.vdegree() }
/// Returns the degrees of B-spline surface
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let uknot_vec = KnotVec::from(vec![0.0, 0.0, 1.0, 1.0]);
/// let vknot_vec = KnotVec::from(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
/// let knot_vecs = (uknot_vec, vknot_vec);
/// let ctrl_pts = vec![
/// vec![Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 1.0), Vector3::new(2.0, 0.0, 2.0)],
/// vec![Vector3::new(0.0, 1.0, 0.0), Vector3::new(1.0, 1.0, 1.0), Vector3::new(2.0, 1.0, 2.0)],
/// ];
/// let bspsurface = BSplineSurface::new(knot_vecs, ctrl_pts);
/// assert_eq!(bspsurface.degrees(), (1, 2));
/// ```
#[inline(always)]
pub fn degrees(&self) -> (usize, usize) { (self.udegree(), self.vdegree()) }
/// Returns whether the knot vectors are clamped or not.
#[inline(always)]
pub fn is_clamped(&self) -> bool { self.0.is_clamped() }
/// Swaps two parameters.
pub fn swap_axes(&mut self) -> &mut Self
where V: Clone {
self.0.swap_axes();
self
}
/// The range of the parameter of the surface.
#[inline(always)]
pub fn parameter_range(&self) -> (ParameterRange, ParameterRange) { self.0.parameter_range() }
/// Creates the curve whose control points are the `idx`th column control points of `self`.
#[inline(always)]
pub fn column_curve(&self, row_idx: usize) -> NurbsCurve<V>
where V: Clone {
NurbsCurve(self.0.column_curve(row_idx))
}
/// Creates the column sectional curve.
#[inline(always)]
pub fn row_curve(&self, column_idx: usize) -> NurbsCurve<V>
where V: Clone {
NurbsCurve(self.0.row_curve(column_idx))
}
}
impl<V: Homogeneous<f64>> NurbsSurface<V> {
/// Constructs a rationalization surface from the non-rationalized surface and weights.
/// # Failures
/// the length of `surface.control_points()` and `weights` must be the same.
#[inline(always)]
pub fn try_from_bspline_and_weights(
surface: BSplineSurface<V::Point>,
weights: Vec<Vec<f64>>,
) -> Result<Self> {
let BSplineSurface {
knot_vecs,
control_points,
} = surface;
if control_points.len() != weights.len() {
return Err(Error::DifferentLength);
}
let control_points = control_points
.into_iter()
.zip(weights)
.map(|(control_points, weights)| {
if control_points.len() != weights.len() {
return Err(Error::DifferentLength);
}
Ok(control_points
.into_iter()
.zip(weights)
.map(|(pt, w)| V::from_point_weight(pt, w))
.collect::<Vec<_>>())
})
.collect::<Result<Vec<_>>>()?;
Ok(Self(BSplineSurface::new_unchecked(
knot_vecs,
control_points,
)))
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V>> NurbsSurface<V> {
/// Substitutes to a NURBS surface.
#[inline(always)]
pub fn subs(&self, u: f64, v: f64) -> V::Point { self.0.subs(u, v).to_point() }
/// Substitutes derived NURBS surface by the first parameter `u`.
#[inline(always)]
pub fn uder(&self, u: f64, v: f64) -> <V::Point as EuclideanSpace>::Diff {
let pt = self.0.subs(u, v);
let ud = self.0.uder(u, v);
pt.rat_der(ud)
}
/// Substitutes derived NURBS surface by the first parameter `v`.
#[inline(always)]
pub fn vder(&self, u: f64, v: f64) -> <V::Point as EuclideanSpace>::Diff {
let pt = self.0.subs(u, v);
let vd = self.0.vder(u, v);
pt.rat_der(vd)
}
/// Substitutes 2nd-ord derived NURBS surface by the first parameter `u`.
#[inline(always)]
pub fn uuder(&self, u: f64, v: f64) -> <V::Point as EuclideanSpace>::Diff {
let pt = self.0.subs(u, v);
let ud = self.0.uder(u, v);
let uud = self.0.uuder(u, v);
pt.rat_der2(ud, uud)
}
/// Substitutes 2nd-ord derived NURBS surface by the first parameter `v`.
#[inline(always)]
pub fn vvder(&self, u: f64, v: f64) -> <V::Point as EuclideanSpace>::Diff {
let pt = self.0.subs(u, v);
let vd = self.0.vder(u, v);
let vvd = self.0.vvder(u, v);
pt.rat_der2(vd, vvd)
}
/// Substitutes 2nd-ord derived NURBS surface by both parameter `u, v`.
#[inline(always)]
pub fn uvder(&self, u: f64, v: f64) -> <V::Point as EuclideanSpace>::Diff {
let pt = self.0.subs(u, v);
let ud = self.0.uder(u, v);
let vd = self.0.vder(u, v);
let uvd = self.0.uvder(u, v);
pt.rat_cross_der(ud, vd, uvd)
}
/// Returns the closure of substitution.
#[inline(always)]
pub fn get_closure(&self) -> impl Fn(f64, f64) -> V::Point + '_ { move |u, v| self.subs(u, v) }
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V>> NurbsSurface<V>
where V::Point: Tolerance
{
/// Returns whether constant curve or not, i.e. all control points are same or not.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let uknot_vec = KnotVec::bezier_knot(1);
/// let vknot_vec = KnotVec::bezier_knot(2);
/// let pt = Vector3::new(1.0, 2.0, 1.0);
/// // allows differences upto scalars
/// let ctrl_pts = vec![
/// vec![pt.clone(), pt.clone() * 2.0, pt.clone() * 3.0],
/// vec![pt.clone() * 0.5, pt.clone() * 0.25, pt.clone() * 0.125],
/// ];
/// let mut surface = NurbsSurface::new(BSplineSurface::new((uknot_vec, vknot_vec), ctrl_pts));
/// assert!(surface.is_const());
///
/// *surface.control_point_mut(1, 2) = Vector3::new(2.0, 3.0, 1.0);
/// assert!(!surface.is_const());
/// ```
#[inline(always)]
pub fn is_const(&self) -> bool {
let pt = self.0.control_points[0][0].to_point();
for vec in self.0.control_points.iter().flat_map(|pts| pts.iter()) {
if !vec.to_point().near(&pt) {
return false;
}
}
true
}
/// Determines whether `self` and `other` is near as the B-spline rational surfaces or not.
///
/// Divides each knot domain into the number of degree equal parts,
/// and check `|self(u, v) - other(u, v)| < TOLERANCE` for each end points `(u, v)`.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vecs = (KnotVec::bezier_knot(3), KnotVec::bezier_knot(2));
/// let ctrl_pts = vec![
/// vec![Vector3::new(0.0, 0.0, 1.0), Vector3::new(0.5, -1.0, 2.0), Vector3::new(1.0, 0.0, 1.0)],
/// vec![Vector3::new(0.0, 1.0, 1.0), Vector3::new(0.5, 1.0, 1.0), Vector3::new(1.0, 1.0, 1.0)],
/// vec![Vector3::new(0.0, 2.0, 1.0), Vector3::new(0.5, 2.0, 3.0), Vector3::new(1.0, 2.0, 1.0)],
/// vec![Vector3::new(0.0, 3.0, 1.0), Vector3::new(0.5, 3.5, 2.0), Vector3::new(1.0, 3.0, 1.0)],
/// ];
/// let surface0 = NurbsSurface::new(BSplineSurface::new(knot_vecs, ctrl_pts));
/// let mut surface1 = surface0.clone();
/// assert!(surface0.near_as_surface(&surface1));
///
/// *surface1.control_point_mut(1, 1) = Vector3::new(0.5, 1.0, 0.9);
/// assert!(!surface0.near_as_surface(&surface1));
/// ```
#[inline(always)]
pub fn near_as_surface(&self, other: &Self) -> bool {
self.0
.sub_near_as_surface(&other.0, 2, move |x, y| x.to_point().near(&y.to_point()))
}
/// Determines whether `self` and `other` is near in square order as the B-spline rational
/// surfaces or not.
///
/// Divides each knot domain into the number of degree equal parts,
/// and check `|self(u, v) - other(u, v)| < TOLERANCE` for each end points `(u, v)`.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let eps = TOLERANCE;
/// let knot_vecs = (KnotVec::bezier_knot(3), KnotVec::bezier_knot(2));
/// let ctrl_pts = vec![
/// vec![Vector3::new(0.0, 0.0, 1.0), Vector3::new(0.5, -1.0, 2.0), Vector3::new(1.0, 0.0, 1.0)],
/// vec![Vector3::new(0.0, 1.0, 1.0), Vector3::new(0.5, 1.0, 1.0), Vector3::new(1.0, 1.0, 1.0)],
/// vec![Vector3::new(0.0, 2.0, 1.0), Vector3::new(0.5, 2.0, 3.0), Vector3::new(1.0, 2.0, 1.0)],
/// vec![Vector3::new(0.0, 3.0, 1.0), Vector3::new(0.5, 3.5, 2.0), Vector3::new(1.0, 3.0, 1.0)],
/// ];
/// let surface0 = NurbsSurface::new(BSplineSurface::new(knot_vecs, ctrl_pts));
/// let mut surface1 = surface0.clone();
/// assert!(surface0.near_as_surface(&surface1));
///
/// *surface1.control_point_mut(1, 1) = Vector3::new(0.5, 1.0, 1.0 - eps);
/// assert!(surface0.near_as_surface(&surface1));
/// assert!(!surface0.near2_as_surface(&surface1));
/// ```
#[inline(always)]
pub fn near2_as_surface(&self, other: &Self) -> bool {
self.0
.sub_near_as_surface(&other.0, 2, move |x, y| x.to_point().near2(&y.to_point()))
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V> + Tolerance> NurbsSurface<V> {
/// Adds a knot `x` of the first parameter `u`, and do not change `self` as a surface.
#[inline(always)]
pub fn add_uknot(&mut self, x: f64) -> &mut Self {
self.0.add_uknot(x);
self
}
/// Adds a knot `x` of the first parameter `u`, and do not change `self` as a surface.
#[inline(always)]
pub fn add_vknot(&mut self, x: f64) -> &mut Self {
self.0.add_vknot(x);
self
}
/// Removes the uknot corresponding to the indice `idx`, and do not change `self` as a curve.
/// If the knot cannot be removed, returns [`Error::CannotRemoveKnot`].
#[inline(always)]
pub fn try_remove_uknot(&mut self, idx: usize) -> Result<&mut Self> {
match self.0.try_remove_uknot(idx) {
Ok(_) => Ok(self),
Err(error) => Err(error),
}
}
/// Removes the uknot corresponding to the indices `idx`, and do not change `self` as a curve.
/// If cannot remove the knot, do not change `self` and return `self`.
#[inline(always)]
pub fn remove_uknot(&mut self, idx: usize) -> &mut Self {
self.0.remove_uknot(idx);
self
}
/// Removes the uknot corresponding to the indice `idx`, and do not change `self` as a curve.
/// If the knot cannot be removed, returns [`Error::CannotRemoveKnot`].
#[inline(always)]
pub fn try_remove_vknot(&mut self, idx: usize) -> Result<&mut Self> {
match self.0.try_remove_vknot(idx) {
Ok(_) => Ok(self),
Err(error) => Err(error),
}
}
/// Removes the uknot corresponding to the indices `idx`, and do not change `self` as a curve.
/// If cannot remove the knot, do not change `self` and return `self`.
#[inline(always)]
pub fn remove_vknot(&mut self, idx: usize) -> &mut Self {
self.0.remove_vknot(idx);
self
}
/// Elevates the udegree.
#[inline(always)]
pub fn elevate_udegree(&mut self) -> &mut Self {
self.0.elevate_udegree();
self
}
/// Elevates the vdegree.
#[inline(always)]
pub fn elevate_vdegree(&mut self) -> &mut Self {
self.0.elevate_vdegree();
self
}
/// Aligns the udegree with the same degrees.
#[inline(always)]
pub fn syncro_uvdegrees(&mut self) -> &mut Self {
self.0.syncro_uvdegrees();
self
}
/// Makes the uknot vector and the vknot vector the same knot vector.
#[inline(always)]
pub fn syncro_uvknots(&mut self) -> &mut Self {
self.0.syncro_uvknots();
self
}
/// Cuts the surface into two surfaces at the parameter `u`
#[inline(always)]
pub fn ucut(&mut self, u: f64) -> Self { Self::new(self.0.ucut(u)) }
/// Cuts the surface into two surfaces at the parameter `v`
#[inline(always)]
pub fn vcut(&mut self, v: f64) -> Self { Self::new(self.0.vcut(v)) }
/// Normalizes the knot vectors
#[inline(always)]
pub fn knot_normalize(&mut self) -> &mut Self {
self.0.knot_normalize();
self
}
/// Translates the knot vectors.
#[inline(always)]
pub fn knot_translate(&mut self, x: f64, y: f64) -> &mut Self {
self.0.knot_translate(x, y);
self
}
/// Removes knots in order from the back
#[inline(always)]
pub fn optimize(&mut self) -> &mut Self {
self.0.optimize();
self
}
/// Get the boundary by four splitted curves.
/// # Example
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vecs = (KnotVec::bezier_knot(3), KnotVec::bezier_knot(2));
/// let ctrl_pts = vec![
/// vec![Vector3::new(0.0, 0.0, 1.0), Vector3::new(0.5, -1.0, 2.0), Vector3::new(1.0, 0.0, 1.0)],
/// vec![Vector3::new(0.0, 1.0, 2.0), Vector3::new(0.5, 1.0, 3.0), Vector3::new(1.0, 1.0, 2.0)],
/// vec![Vector3::new(0.0, 2.0, 2.0), Vector3::new(0.5, 2.0, 3.0), Vector3::new(1.0, 2.0, 2.0)],
/// vec![Vector3::new(0.0, 3.0, 1.0), Vector3::new(0.5, 3.5, 2.0), Vector3::new(1.0, 3.0, 1.0)],
/// ];
/// let bspsurface = NurbsSurface::new(BSplineSurface::new(knot_vecs, ctrl_pts));
/// let curves = bspsurface.splitted_boundary();
/// assert_eq!(
/// curves[0].control_points(),
/// &vec![
/// Vector3::new(0.0, 0.0, 1.0),
/// Vector3::new(0.0, 1.0, 2.0),
/// Vector3::new(0.0, 2.0, 2.0),
/// Vector3::new(0.0, 3.0, 1.0),
/// ],
/// );
/// assert_eq!(
/// curves[1].control_points(),
/// &vec![
/// Vector3::new(0.0, 3.0, 1.0),
/// Vector3::new(0.5, 3.5, 2.0),
/// Vector3::new(1.0, 3.0, 1.0),
/// ],
/// );
/// assert_eq!(
/// curves[2].control_points(),
/// &vec![
/// Vector3::new(1.0, 3.0, 1.0),
/// Vector3::new(1.0, 2.0, 2.0),
/// Vector3::new(1.0, 1.0, 2.0),
/// Vector3::new(1.0, 0.0, 1.0),
/// ],
/// );
/// assert_eq!(
/// curves[3].control_points(),
/// &vec![
/// Vector3::new(1.0, 0.0, 1.0),
/// Vector3::new(0.5, -1.0, 2.0),
/// Vector3::new(0.0, 0.0, 1.0),
/// ],
/// );
/// ```
#[inline(always)]
pub fn splitted_boundary(&self) -> [NurbsCurve<V>; 4] {
TryFrom::try_from(
self.0
.splitted_boundary()
.iter()
.cloned()
.map(NurbsCurve::new)
.collect::<Vec<_>>(),
)
.unwrap()
}
/// Extracts the boundary of surface
#[inline(always)]
pub fn boundary(&self) -> NurbsCurve<V> { NurbsCurve::new(self.0.boundary()) }
}
impl<V: Homogeneous<f64>> SearchNearestParameter<D2> for NurbsSurface<V>
where
Self: ParametricSurface<Point = V::Point, Vector = <V::Point as EuclideanSpace>::Diff>,
V::Point: EuclideanSpace<Scalar = f64> + MetricSpace<Metric = f64>,
<V::Point as EuclideanSpace>::Diff: InnerSpace<Scalar = f64> + Tolerance,
{
type Point = V::Point;
/// Searches the parameter `(u, v)` which minimize `|self(u, v) - point|` by Newton's method
/// with initial guess `(u0, v0)`. If the repeated trial does not converge, then returns `None`.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vecs = (KnotVec::bezier_knot(3), KnotVec::bezier_knot(2));
/// let ctrl_pts = vec![
/// vec![Vector3::new(0.0, 0.0, 1.0), Vector3::new(1.0, -2.0, 2.0), Vector3::new(1.0, 0.0, 1.0)],
/// vec![Vector3::new(0.0, 2.0, 2.0), Vector3::new(2.0, 4.0, 4.0), Vector3::new(2.0, 2.0, 2.0)],
/// vec![Vector3::new(0.0, 4.0, 2.0), Vector3::new(2.0, 8.0, 4.0), Vector3::new(2.0, 4.0, 2.0)],
/// vec![Vector3::new(0.0, 3.0, 1.0), Vector3::new(1.0, 7.0, 2.0), Vector3::new(1.0, 3.0, 1.0)],
/// ];
/// let surface = NurbsSurface::new(BSplineSurface::new(knot_vecs, ctrl_pts));
/// let pt = surface.subs(0.3, 0.7);
/// let (u, v) = surface.search_nearest_parameter(pt, Some((0.5, 0.5)), 100).unwrap();
/// assert!(u.near(&0.3) && v.near(&0.7));
/// ```
/// # Remarks
/// It may converge to a local solution depending on the hint.
/// cf. [`BSplineCurve::search_rational_nearest_parameter`](struct.BSplineCurve.html#method.search_rational_nearest_parameter)
#[inline(always)]
fn search_nearest_parameter<H: Into<SPHint2D>>(
&self,
point: V::Point,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
let hint = match hint.into() {
SPHint2D::Parameter(x, y) => (x, y),
SPHint2D::Range(range0, range1) => {
algo::surface::presearch(self, point, (range0, range1), PRESEARCH_DIVISION)
}
SPHint2D::None => {
algo::surface::presearch(self, point, self.range_tuple(), PRESEARCH_DIVISION)
}
};
algo::surface::search_nearest_parameter(self, point, hint, trials)
}
}
impl<V: Homogeneous<f64>> NurbsSurface<V>
where V::Point: Bounded<Scalar = f64>
{
/// Returns the bounding box including all control points.
#[inline(always)]
pub fn roughly_bounding_box(&self) -> BoundingBox<V::Point> {
self.0
.control_points
.iter()
.flatten()
.map(|pt| pt.to_point())
.collect()
}
}
impl SearchParameter<D2> for NurbsSurface<Vector3> {
type Point = Point2;
/// Search the parameter `(u, v)` such that `self.subs(u, v).rational_projection()` is near `pt`.
/// If cannot find, then return `None`.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vec = KnotVec::uniform_knot(2, 2);
/// let ctrl_pts = vec![
/// vec![Vector3::new(0.0, 0.0, 1.0), Vector3::new(0.1, 0.0, 0.5), Vector3::new(0.5, 0.0, 0.3), Vector3::new(1.0, 0.0, 1.0)],
/// vec![Vector3::new(0.0, 0.1, 0.1), Vector3::new(0.2, 0.2, 0.1), Vector3::new(0.4, 0.3, 0.4), Vector3::new(1.0, 0.3, 0.7)],
/// vec![Vector3::new(0.0, 0.5, 0.4), Vector3::new(0.3, 0.6, 0.5), Vector3::new(0.6, 0.4, 1.0), Vector3::new(1.0, 0.5, 0.4)],
/// vec![Vector3::new(0.0, 1.0, 1.0), Vector3::new(0.1, 1.0, 1.0), Vector3::new(0.5, 1.0, 0.5), Vector3::new(1.0, 1.0, 0.3)],
/// ];
/// let bspsurface = BSplineSurface::new((knot_vec.clone(), knot_vec), ctrl_pts);
/// let surface = NurbsSurface::new(bspsurface);
///
/// let pt = surface.subs(0.3, 0.7);
/// let (u, v) = surface.search_parameter(pt, Some((0.5, 0.5)), 100).unwrap();
/// assert_near!(surface.subs(u, v), pt);
/// ```
#[inline(always)]
fn search_parameter<H: Into<SPHint2D>>(
&self,
point: Point2,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
let hint = match hint.into() {
SPHint2D::Parameter(x, y) => (x, y),
SPHint2D::Range(range0, range1) => {
algo::surface::presearch(self, point, (range0, range1), PRESEARCH_DIVISION)
}
SPHint2D::None => {
algo::surface::presearch(self, point, self.range_tuple(), PRESEARCH_DIVISION)
}
};
algo::surface::search_parameter2d(self, point, hint, trials)
}
}
impl<V: Clone> Invertible for NurbsSurface<V> {
#[inline(always)]
fn invert(&mut self) { self.swap_axes(); }
#[inline(always)]
fn inverse(&self) -> Self {
let mut surface = self.clone();
surface.swap_axes();
surface
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V>> ParametricSurface for NurbsSurface<V> {
type Point = V::Point;
type Vector = <V::Point as EuclideanSpace>::Diff;
#[inline(always)]
fn subs(&self, u: f64, v: f64) -> Self::Point { self.subs(u, v) }
#[inline(always)]
fn uder(&self, u: f64, v: f64) -> Self::Vector { self.uder(u, v) }
#[inline(always)]
fn vder(&self, u: f64, v: f64) -> Self::Vector { self.vder(u, v) }
#[inline(always)]
fn uuder(&self, u: f64, v: f64) -> Self::Vector { self.uuder(u, v) }
#[inline(always)]
fn uvder(&self, u: f64, v: f64) -> Self::Vector { self.uvder(u, v) }
#[inline(always)]
fn vvder(&self, u: f64, v: f64) -> Self::Vector { self.vvder(u, v) }
#[inline(always)]
fn parameter_range(&self) -> (ParameterRange, ParameterRange) { self.parameter_range() }
}
impl ParametricSurface3D for NurbsSurface<Vector4> {
#[inline(always)]
fn normal(&self, u: f64, v: f64) -> Vector3 {
let pt = self.0.subs(u, v);
let ud = self.0.uder(u, v);
let vd = self.0.vder(u, v);
pt.rat_der(ud).cross(pt.rat_der(vd)).normalize()
}
}
impl<V: Homogeneous<f64> + ControlPoint<f64, Diff = V>> ParameterDivision2D for NurbsSurface<V>
where V::Point: MetricSpace<Metric = f64> + HashGen<f64>
{
#[inline(always)]
fn parameter_division(
&self,
range: ((f64, f64), (f64, f64)),
tol: f64,
) -> (Vec<f64>, Vec<f64>) {
algo::surface::parameter_division(self, range, tol)
}
}
impl<V> BoundedSurface for NurbsSurface<V> where Self: ParametricSurface {}
impl IncludeCurve<NurbsCurve<Vector3>> for NurbsSurface<Vector3> {
#[inline(always)]
fn include(&self, curve: &NurbsCurve<Vector3>) -> bool {
let pt = curve.subs(curve.knot_vec()[0]);
let mut hint = match self.search_parameter(pt, None, INCLUDE_CURVE_TRIALS) {
Some(got) => got,
None => return false,
};
let uknot_vec = self.uknot_vec();
let vknot_vec = self.vknot_vec();
let degree = curve.degree() * 6;
let (knots, _) = curve.knot_vec().to_single_multi();
for i in 1..knots.len() {
for j in 1..=degree {
let p = j as f64 / degree as f64;
let t = knots[i - 1] * (1.0 - p) + knots[i] * p;
let pt = curve.subs(t);
hint = match self.search_parameter(pt, Some(hint), INCLUDE_CURVE_TRIALS) {
Some(got) => got,
None => return false,
};
if !self.subs(hint.0, hint.1).near(&pt)
|| hint.0 < uknot_vec[0] - TOLERANCE
|| hint.0 - uknot_vec[0] > uknot_vec.range_length() + TOLERANCE
|| hint.1 < vknot_vec[0] - TOLERANCE
|| hint.1 - vknot_vec[0] > vknot_vec.range_length() + TOLERANCE
{
return false;
}
}
}
true
}
}
impl IncludeCurve<BSplineCurve<Point3>> for NurbsSurface<Vector4> {
#[inline(always)]
fn include(&self, curve: &BSplineCurve<Point3>) -> bool {
let pt = curve.front();
let mut hint = match self.search_parameter(pt, None, INCLUDE_CURVE_TRIALS) {
Some(got) => got,
None => return false,
};
let uknot_vec = self.uknot_vec();
let vknot_vec = self.vknot_vec();
let degree = curve.degree() * 6;
let (knots, _) = curve.knot_vec().to_single_multi();
for i in 1..knots.len() {
for j in 1..=degree {
let p = j as f64 / degree as f64;
let t = knots[i - 1] * (1.0 - p) + knots[i] * p;
let pt = curve.subs(t);
hint = match self.search_parameter(pt, Some(hint), INCLUDE_CURVE_TRIALS) {
Some(got) => got,
None => return false,
};
if !self.subs(hint.0, hint.1).near(&pt)
|| hint.0 < uknot_vec[0] - TOLERANCE
|| hint.0 - uknot_vec[0] > uknot_vec.range_length() + TOLERANCE
|| hint.1 < vknot_vec[0] - TOLERANCE
|| hint.1 - vknot_vec[0] > vknot_vec.range_length() + TOLERANCE
{
return false;
}
}
}
true
}
}
impl IncludeCurve<NurbsCurve<Vector4>> for NurbsSurface<Vector4> {
#[inline(always)]
fn include(&self, curve: &NurbsCurve<Vector4>) -> bool {
let pt = curve.front();
let mut hint = match self.search_parameter(pt, None, INCLUDE_CURVE_TRIALS) {
Some(got) => got,
None => return false,
};
let uknot_vec = self.uknot_vec();
let vknot_vec = self.vknot_vec();
let degree = curve.degree() * 6;
let (knots, _) = curve.knot_vec().to_single_multi();
for i in 1..knots.len() {
for j in 1..=degree {
let p = j as f64 / degree as f64;
let t = knots[i - 1] * (1.0 - p) + knots[i] * p;
let pt = curve.subs(t);
hint = match self.search_parameter(pt, Some(hint), INCLUDE_CURVE_TRIALS) {
Some(got) => got,
None => return false,
};
if !self.subs(hint.0, hint.1).near(&pt)
|| hint.0 < uknot_vec[0] - TOLERANCE
|| hint.0 - uknot_vec[0] > uknot_vec.range_length() + TOLERANCE
|| hint.1 < vknot_vec[0] - TOLERANCE
|| hint.1 - vknot_vec[0] > vknot_vec.range_length() + TOLERANCE
{
return false;
}
}
}
true
}
}
impl<M, V: Copy> Transformed<M> for NurbsSurface<V>
where M: Copy + std::ops::Mul<V, Output = V>
{
#[inline(always)]
fn transform_by(&mut self, trans: M) {
self.0
.control_points
.iter_mut()
.flatten()
.for_each(move |v| *v = trans * *v)
}
}
impl SearchParameter<D2> for NurbsSurface<Vector4> {
type Point = Point3;
/// Search the parameter `(u, v)` such that `self.subs(u, v).rational_projection()` is near `pt`.
/// If cannot find, then return `None`.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let knot_vec = KnotVec::uniform_knot(2, 2);
/// let ctrl_pts = vec![
/// vec![Vector4::new(0.0, 0.0, 0.0, 1.0), Vector4::new(0.1, 0.0, 0.5, 0.4), Vector4::new(1.0, 0.0, 0.6, 2.0), Vector4::new(0.4, 0.0, 0.4, 0.4)],
/// vec![Vector4::new(0.0, 0.2, 0.2, 2.0), Vector4::new(0.24, 0.24, 0.1, 1.2), Vector4::new(2.4, 1.8, 2.4, 0.6), Vector4::new(1.4, 0.42, 0.98, 1.4)],
/// vec![Vector4::new(0.0, 1.5, 1.2, 3.0), Vector4::new(1.02, 2.04, 1.7, 3.4), Vector4::new(0.42, 0.28, 0.7, 0.7), Vector4::new(0.6, 0.3, 0.0, 0.6)],
/// vec![Vector4::new(0.0, 1.0, 1.0, 1.0), Vector4::new(0.2, 2.0, 2.0, 2.0), Vector4::new(0.85, 1.7, 0.85, 1.7), Vector4::new(1.0, 1.0, 0.3, 1.0)],
/// ];
/// let bspsurface = BSplineSurface::new((knot_vec.clone(), knot_vec), ctrl_pts);
/// let surface = NurbsSurface::new(bspsurface);
///
/// let pt = surface.subs(0.3, 0.7);
/// let (u, v) = surface.search_parameter(pt, Some((0.5, 0.5)), 100).unwrap();
/// assert_near!(surface.subs(u, v), pt);
/// ```
fn search_parameter<H: Into<SPHint2D>>(
&self,
point: Point3,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
let hint = match hint.into() {
SPHint2D::Parameter(x, y) => (x, y),
SPHint2D::Range(range0, range1) => {
algo::surface::presearch(self, point, (range0, range1), PRESEARCH_DIVISION)
}
SPHint2D::None => {
algo::surface::presearch(self, point, self.range_tuple(), PRESEARCH_DIVISION)
}
};
algo::surface::search_parameter3d(self, point, hint, trials)
}
}
#[test]
fn test_include2d() {
let knot_vec = KnotVec::uniform_knot(2, 3);
let ctrl_pts = vec![
vec![
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.05, 0.0, 0.5),
Vector3::new(0.15, 0.0, 0.3),
Vector3::new(1.0, 0.0, 1.0),
],
vec![
Vector3::new(0.0, 0.01, 0.1),
Vector3::new(0.02, 0.02, 0.1),
Vector3::new(0.16, 0.12, 0.4),
Vector3::new(0.7, 0.21, 0.7),
],
vec![
Vector3::new(0.0, 0.02, 0.4),
Vector3::new(0.15, 0.3, 0.5),
Vector3::new(0.6, 0.4, 1.0),
Vector3::new(0.4, 0.2, 0.4),
],
vec![
Vector3::new(0.0, 1.0, 1.0),
Vector3::new(0.1, 1.0, 1.0),
Vector3::new(0.25, 0.5, 0.5),
Vector3::new(0.3, 0.3, 0.3),
],
];
let surface = BSplineSurface::new((knot_vec.clone(), knot_vec), ctrl_pts);
let bnd_box = BoundingBox::from_iter(&[Vector2::new(0.2, 0.3), Vector2::new(0.8, 0.6)]);
let mut curve = surface.sectional_curve(bnd_box);
curve.control_points_mut().for_each(|pt| *pt *= 3.0);
let surface = NurbsSurface::new(surface);
let curve = NurbsCurve::new(curve);
assert!(surface.include(&curve));
}
#[test]
fn test_include3d() {
let knot_vec = KnotVec::bezier_knot(2);
let ctrl_pts = vec![
vec![
Vector4::new(-1.0, -1.0, 2.0, 1.0),
Vector4::new(-1.0, 0.0, 0.0, 1.0),
Vector4::new(-1.0, 1.0, 2.0, 1.0),
],
vec![
Vector4::new(0.0, -1.0, 0.0, 1.0),
Vector4::new(0.0, 0.0, -2.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 1.0),
],
vec![
Vector4::new(1.0, -1.0, 2.0, 1.0),
Vector4::new(1.0, 0.0, 0.0, 1.0),
Vector4::new(1.0, 1.0, 2.0, 1.0),
],
];
let surface = NurbsSurface::new(BSplineSurface::new((knot_vec.clone(), knot_vec), ctrl_pts));
let knot_vec = KnotVec::from(vec![
0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0,
]);
let ctrl_pts = vec![
// the vector of the indices of control points
Vector4::new(0.0, -2.0, 2.0, 2.0),
Vector4::new(1.0, -1.0, 1.0, 1.0),
Vector4::new(1.0, 0.0, 1.0, 1.0),
Vector4::new(1.0, 1.0, 1.0, 1.0),
Vector4::new(0.0, 2.0, 2.0, 2.0),
Vector4::new(-1.0, 1.0, 1.0, 1.0),
Vector4::new(-1.0, 0.0, 1.0, 1.0),
Vector4::new(-1.0, -1.0, 1.0, 1.0),
Vector4::new(0.0, -2.0, 2.0, 2.0),
];
let mut curve = NurbsCurve::new(BSplineCurve::new(knot_vec, ctrl_pts));
assert!(surface.include(&curve));
*curve.control_point_mut(1) += Vector4::new(0.0, 0.0, 0.00001, 0.0);
assert!(!surface.include(&curve));
}
-161
View File
@@ -1,161 +0,0 @@
use super::*;
use std::f64::consts::PI;
impl<P> UnitCircle<P> {
/// constructor
#[inline]
pub const fn new() -> Self { Self(std::marker::PhantomData) }
}
impl ParametricCurve for UnitCircle<Point2> {
type Point = Point2;
type Vector = Vector2;
#[inline]
fn subs(&self, t: f64) -> Self::Point { Point2::new(f64::cos(t), f64::sin(t)) }
#[inline]
fn der(&self, t: f64) -> Self::Vector { Vector2::new(-f64::sin(t), f64::cos(t)) }
#[inline]
fn der2(&self, t: f64) -> Self::Vector { Vector2::new(-f64::cos(t), -f64::sin(t)) }
#[inline]
fn parameter_range(&self) -> ParameterRange {
(Bound::Included(0.0), Bound::Excluded(2.0 * PI))
}
}
impl BoundedCurve for UnitCircle<Point2> {}
impl ParametricCurve for UnitCircle<Point3> {
type Point = Point3;
type Vector = Vector3;
#[inline]
fn subs(&self, t: f64) -> Self::Point { Point3::new(f64::cos(t), f64::sin(t), 0.0) }
#[inline]
fn der(&self, t: f64) -> Self::Vector { Vector3::new(-f64::sin(t), f64::cos(t), 0.0) }
#[inline]
fn der2(&self, t: f64) -> Self::Vector { Vector3::new(-f64::cos(t), -f64::sin(t), 0.0) }
#[inline]
fn period(&self) -> Option<f64> { Some(2.0 * PI) }
#[inline]
fn parameter_range(&self) -> ParameterRange {
(Bound::Included(0.0), Bound::Excluded(2.0 * PI))
}
}
impl BoundedCurve for UnitCircle<Point3> {}
impl<P> ParameterDivision1D for UnitCircle<P>
where UnitCircle<P>: ParametricCurve<Point = P>
{
type Point = P;
fn parameter_division(&self, range: (f64, f64), tol: f64) -> (Vec<f64>, Vec<P>) {
nonpositive_tolerance!(tol);
let tol = f64::min(tol, 0.8);
let delta = 2.0 * f64::acos(1.0 - tol);
let n = 1 + ((range.1 - range.0) / delta) as usize;
let params = (0..=n)
.map(|i| {
let t = i as f64 / n as f64;
range.0 * (1.0 - t) + range.1 * t
})
.collect::<Vec<_>>();
let pts = params.iter().map(|t| self.subs(*t)).collect();
(params, pts)
}
}
impl SearchNearestParameter<D1> for UnitCircle<Point2> {
type Point = Point2;
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
pt: Point2,
_: H,
_: usize,
) -> Option<f64> {
let v = pt.to_vec();
if v.magnitude().so_small() {
return None;
}
let v = v.normalize();
let theta = f64::acos(f64::clamp(v.x, -1.0, 1.0));
let theta = match v.y > 0.0 {
true => theta,
false => 2.0 * PI - theta,
};
Some(theta)
}
}
#[test]
fn search_nearest_parameter() {
const N: usize = 100;
let circle = UnitCircle::<Point2>::new();
for i in 0..N {
let t = 2.0 * PI * i as f64 / N as f64;
let a = 5.0 * rand::random::<f64>() + 0.1;
let p = a * circle.subs(t);
let s = circle.search_nearest_parameter(p, None, 1).unwrap();
let q = a * circle.subs(s);
assert_near!(p, q);
}
}
impl SearchParameter<D1> for UnitCircle<Point2> {
type Point = Point2;
fn search_parameter<H: Into<SPHint1D>>(&self, pt: Point2, _: H, _: usize) -> Option<f64> {
let v = pt.to_vec();
if !v.magnitude().near(&1.0) {
return None;
}
let v = v.normalize();
let theta = f64::acos(f64::clamp(v.x, -1.0, 1.0));
let theta = match v.y > 0.0 {
true => theta,
false => 2.0 * PI - theta,
};
Some(theta)
}
}
#[test]
fn search_parameter() {
const N: usize = 100;
let circle = UnitCircle::<Point2>::new();
for i in 1..N {
let t = 2.0 * PI * i as f64 / N as f64;
let p = circle.subs(t);
let s = circle.search_parameter(p, None, 1).unwrap();
assert_near!(s, t);
}
}
impl SearchNearestParameter<D1> for UnitCircle<Point3> {
type Point = Point3;
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
pt: Point3,
_: H,
_: usize,
) -> Option<f64> {
UnitCircle::<Point2>::new().search_nearest_parameter(Point2::new(pt.x, pt.y), None, 0)
}
}
impl SearchParameter<D1> for UnitCircle<Point3> {
type Point = Point3;
fn search_parameter<H: Into<SPHint1D>>(&self, pt: Point3, _: H, _: usize) -> Option<f64> {
if !f64::abs(pt.z).so_small() {
return None;
}
UnitCircle::<Point2>::new().search_parameter(Point2::new(pt.x, pt.y), None, 0)
}
}
#[test]
fn parameter_division() {
let c = UnitCircle::<Point2>::new();
let (_div, pts) = c.parameter_division(c.range_tuple(), 0.05);
for a in pts.windows(2) {
let p = a[0].midpoint(a[1]);
assert!(p.to_vec().magnitude() > 0.95);
}
}
-125
View File
@@ -1,125 +0,0 @@
use super::*;
impl<P> UnitHyperbola<P> {
/// constructor
#[inline]
pub const fn new() -> UnitHyperbola<P> { UnitHyperbola(std::marker::PhantomData) }
}
impl ParametricCurve for UnitHyperbola<Point2> {
type Point = Point2;
type Vector = Vector2;
#[inline]
fn subs(&self, t: f64) -> Self::Point { Point2::new(f64::cosh(t), f64::sinh(t)) }
#[inline]
fn der(&self, t: f64) -> Self::Vector { Vector2::new(f64::sinh(t), f64::cosh(t)) }
#[inline]
fn der2(&self, t: f64) -> Self::Vector { Vector2::new(f64::cosh(t), f64::sinh(t)) }
}
impl ParametricCurve for UnitHyperbola<Point3> {
type Point = Point3;
type Vector = Vector3;
#[inline]
fn subs(&self, t: f64) -> Self::Point { Point3::new(f64::cosh(t), f64::sinh(t), 0.0) }
#[inline]
fn der(&self, t: f64) -> Self::Vector { Vector3::new(f64::sinh(t), f64::cosh(t), 0.0) }
#[inline]
fn der2(&self, t: f64) -> Self::Vector { Vector3::new(f64::cosh(t), f64::sinh(t), 0.0) }
}
impl<P> ParameterDivision1D for UnitHyperbola<P>
where
UnitHyperbola<P>: ParametricCurve<Point = P>,
P: EuclideanSpace<Scalar = f64> + MetricSpace<Metric = f64> + HashGen<f64>,
{
type Point = P;
fn parameter_division(&self, range: (f64, f64), tol: f64) -> (Vec<f64>, Vec<P>) {
algo::curve::parameter_division(self, range, tol)
}
}
impl SearchNearestParameter<D1> for UnitHyperbola<Point2> {
type Point = Point2;
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
p: Point2,
_: H,
_: usize,
) -> Option<f64> {
let a = -p.y;
let b = (p.y * p.y - p.x * p.x) / 4.0 + 1.0;
let c = -p.y;
let d = p.y * p.y / 4.0;
let y = solver::solve_quartic(a, b, c, d)
.into_iter()
.filter_map(|z| match z.im.so_small() {
true => Some(z.re),
false => None,
})
.min_by(|s, t| {
p.distance2(self.subs(*s))
.partial_cmp(&p.distance2(self.subs(*t)))
.unwrap()
})?;
Some(f64::asinh(y))
}
}
impl SearchNearestParameter<D1> for UnitHyperbola<Point3> {
type Point = Point3;
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
p: Point3,
_: H,
_trials: usize,
) -> Option<f64> {
UnitHyperbola::<Point2>::new().search_nearest_parameter(
Point2::new(p.x, p.y),
None,
_trials,
)
}
}
impl SearchParameter<D1> for UnitHyperbola<Point2> {
type Point = Point2;
fn search_parameter<H: Into<SPHint1D>>(&self, p: Point2, _: H, _: usize) -> Option<f64> {
let t = f64::asinh(p.y);
match p.near(&self.subs(t)) {
true => Some(t),
false => None,
}
}
}
impl SearchParameter<D1> for UnitHyperbola<Point3> {
type Point = Point3;
fn search_parameter<H: Into<SPHint1D>>(&self, p: Point3, _: H, _: usize) -> Option<f64> {
let t = f64::asinh(p.y);
match p.near(&self.subs(t)) {
true => Some(t),
false => None,
}
}
}
#[test]
fn snp_test() {
let curve = UnitHyperbola::<Point2>::new();
let p = curve.subs(2.0);
let q = p + Vector2::new(-p.x, p.y);
let t = curve.search_nearest_parameter(q, None, 0).unwrap();
assert_near!(t, 2.0);
}
#[test]
fn sp_test() {
let curve = UnitHyperbola::<Point2>::new();
let t = 100.0 * rand::random::<f64>() - 50.0;
let p = curve.subs(t);
assert_near!(curve.search_parameter(p, None, 0).unwrap(), t);
let q = Point2::new(-1.0, 0.0);
assert!(curve.search_parameter(q, None, 0).is_none());
}
-217
View File
@@ -1,217 +0,0 @@
use super::*;
use truck_base::cgmath64::control_point::ControlPoint;
impl<P: Copy> Line<P> {
/// initialize line from vector
#[inline]
pub fn from_origin_direction<V>(origin: P, direction: V) -> Self
where P: std::ops::Add<V, Output = P> {
Self(origin, origin + direction)
}
/// to a bspline curve
#[inline]
pub fn to_bspline(self) -> BSplineCurve<P> {
BSplineCurve::new(KnotVec::bezier_knot(1), vec![self.0, self.1])
}
}
impl<P> Line<P>
where
P: EuclideanSpace<Scalar = f64>,
P::Diff: InnerSpace<Scalar = f64>,
{
/// Returns the projected point to the line.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let line = Line(Point2::new(0.0, 0.0), Point2::new(1.0, 2.0));
/// let pt = Point2::new(0.0, 1.0);
/// assert_near!(line.projection(pt), Point2::new(0.4, 0.8));
/// ```
pub fn projection(self, point: P) -> P {
let (u, v) = (point - self.0, self.1 - self.0);
self.0 + v * u.dot(v) / v.dot(v)
}
/// Returns the distance between the line and the point `point`.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let line = Line(Point2::new(0.0, 0.0), Point2::new(3.0, 4.0));
///
/// // The foot of the perpendicular line is on a line segment
/// let pt = Point2::new(3.0, 0.0);
/// assert_near!(line.distance_to_point(pt), 2.4);
///
/// // The foot of the perpendicular line is not on a line segment
/// let pt = Point2::new(0.0, -4.0);
/// assert_near!(line.distance_to_point(pt), 2.4);
/// ```
pub fn distance_to_point(self, point: P) -> f64 {
let (u, v) = (point - self.0, self.1 - self.0);
(u - v * u.dot(v) / v.dot(v)).magnitude()
}
/// Returns the distance between the sengment and the point `point`.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let line = Line(Point2::new(0.0, 0.0), Point2::new(3.0, 4.0));
///
/// // The foot of the perpendicular line is on a line segment
/// let pt = Point2::new(3.0, 0.0);
/// assert_near!(line.distance_to_point_as_segment(pt), 2.4);
///
/// // The foot of the perpendicular line is not on a line segment
/// let pt = Point2::new(0.0, -4.0);
/// assert_near!(line.distance_to_point_as_segment(pt), 4.0);
/// ```
pub fn distance_to_point_as_segment(self, point: P) -> f64 {
let (u, v) = (point - self.0, self.1 - self.0);
let t = f64::clamp(u.dot(v) / v.dot(v), 0.0, 1.0);
(u - v * t).magnitude()
}
}
impl Line<Point2> {
/// Returns the intersection of two lines and its parameters.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let line0 = Line(Point2::new(0.0, 0.0), Point2::new(9.0, 3.0));
/// let line1 = Line(Point2::new(0.0, 6.0), Point2::new(9.0, 0.0));
/// let (s, t, p) = line0.intersection(line1).unwrap();
/// assert_near!(line0.subs(s), Point2::new(6.0, 2.0));
/// assert_near!(line1.subs(t), Point2::new(6.0, 2.0));
/// assert_near!(p, Point2::new(6.0, 2.0));
/// ```
/// # Failures
/// Returns `None` if two lines are parallel.
/// ```
/// use truck_geometry::prelude::*;
/// let line0 = Line(Point2::new(0.0, 0.0), Point2::new(9.0, 3.0));
/// let line1 = Line(Point2::new(-1.0, 0.0), Point2::new(8.0, 3.0));
/// assert!(line0.intersection(line1).is_none());
/// ```
pub fn intersection(self, other: Line<Point2>) -> Option<(f64, f64, Point2)> {
let mat = Matrix2::from_cols(self.1 - self.0, other.0 - other.1);
let v = other.0 - self.0;
let params = mat.invert().map(|inv| inv * v)?;
Some((params.x, params.y, self.subs(params.x)))
}
}
impl<P: ControlPoint<f64>> ParametricCurve for Line<P> {
type Point = P;
type Vector = P::Diff;
#[inline]
fn subs(&self, t: f64) -> Self::Point { self.0 + (self.1 - self.0) * t }
#[inline]
fn der(&self, _: f64) -> Self::Vector { self.1 - self.0 }
#[inline]
fn der2(&self, _: f64) -> Self::Vector { Self::Vector::zero() }
/// Return `0.0..=1.0` i.e. we regard it as a segment
#[inline]
fn parameter_range(&self) -> ParameterRange { (Bound::Included(0.0), Bound::Included(1.0)) }
}
impl<P: ControlPoint<f64>> BoundedCurve for Line<P> {}
impl<P: ControlPoint<f64>> Cut for Line<P> {
#[inline]
fn cut(&mut self, t: f64) -> Self {
let r = self.subs(t);
let res = Self(r, self.1);
self.1 = r;
res
}
}
impl<P: ControlPoint<f64>> ParameterDivision1D for Line<P> {
type Point = P;
#[inline]
fn parameter_division(&self, range: (f64, f64), _: f64) -> (Vec<f64>, Vec<P>) {
(
vec![range.0, range.1],
vec![self.subs(range.0), self.subs(range.1)],
)
}
}
impl<P: Copy> Invertible for Line<P> {
#[inline]
fn invert(&mut self) { *self = Self(self.1, self.0); }
#[inline]
fn inverse(&self) -> Self { Self(self.1, self.0) }
}
impl<P> SearchNearestParameter<D1> for Line<P>
where
P: ControlPoint<f64>,
P::Diff: InnerSpace<Scalar = f64>,
{
type Point = P;
#[inline]
fn search_nearest_parameter<H: Into<SPHint1D>>(&self, pt: P, _: H, _: usize) -> Option<f64> {
let b = self.1 - self.0;
Some((pt - self.0).dot(b) / b.dot(b))
}
}
impl<P> SearchParameter<D1> for Line<P>
where
P: ControlPoint<f64> + Tolerance,
P::Diff: InnerSpace<Scalar = f64>,
{
type Point = P;
#[inline]
fn search_parameter<H: Into<SPHint1D>>(&self, pt: P, _: H, _: usize) -> Option<f64> {
let b = self.1 - self.0;
let t = (pt - self.0).dot(b) / b.dot(b);
match self.subs(t).near(&pt) {
true => Some(t),
false => None,
}
}
}
impl<P: EuclideanSpace, M: Transform<P>> Transformed<M> for Line<P> {
#[inline]
fn transform_by(&mut self, trans: M) {
self.0 = trans.transform_point(self.0);
self.1 = trans.transform_point(self.1);
}
#[inline]
fn transformed(&self, trans: M) -> Self {
Line(trans.transform_point(self.0), trans.transform_point(self.1))
}
}
#[test]
fn line() {
let line = Line(Point2::new(1.0, 0.0), Point2::new(0.0, 1.0));
// subs
assert_near!(line.subs(0.4), Point2::new(0.6, 0.4));
// inverse
let line_inverse = line.inverse();
assert_eq!(line.0, line_inverse.1);
assert_eq!(line.1, line_inverse.0);
// cut
let mut line0 = line;
let line1 = line0.cut(0.4);
assert_eq!(line.0, line0.0);
assert_near!(line0.1, line.subs(0.4));
assert_eq!(line0.1, line1.0);
assert_eq!(line1.1, line.1);
// SNP
assert_near!(
line.search_nearest_parameter(Point2::new(1.0, 1.0), None, 0)
.unwrap(),
0.5
);
assert!(line
.search_parameter(Point2::new(1.0, 1.0), None, 0)
.is_none());
}
-115
View File
@@ -1,115 +0,0 @@
use crate::{prelude::*, *};
/// line
/// # Example
/// ```
/// use truck_geometry::prelude::*;
/// let line = Line(Point2::new(0.0, 0.0), Point2::new(1.0, 1.0));
/// assert_near!(line.subs(0.5), Point2::new(0.5, 0.5));
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Line<P>(pub P, pub P);
/// unit circle
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub struct UnitCircle<P>(std::marker::PhantomData<P>);
/// unit hyperbola
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub struct UnitHyperbola<P>(std::marker::PhantomData<P>);
/// parabola whose apex is the origin.
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub struct UnitParabola<P>(std::marker::PhantomData<P>);
/// plane
/// # Example
/// ```
/// use truck_geometry::prelude::*;
///
/// // arbitrary three points
/// let pt0 = Point3::new(0.0, 1.0, 2.0);
/// let pt1 = Point3::new(1.0, 1.0, 3.0);
/// let pt2 = Point3::new(0.0, 2.0, 3.0);
///
/// // Creates a plane
/// let plane: Plane = Plane::new(pt0, pt1, pt2);
/// // The origin of the plane is pt0.
/// assert_near!(plane.origin(), pt0);
/// // The u-axis of the plane is the vector from pt0 to pt1.
/// assert_near!(plane.u_axis(), pt1 - pt0);
/// // The v-axis of the plane is the vector from pt0 to pt2.
/// assert_near!(plane.v_axis(), pt2 - pt0);
/// // The normal is the normalized u-axis × v-axis
/// assert_near!(plane.normal(), (pt1 - pt0).cross(pt2 - pt0).normalize());
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Plane {
o: Point3,
p: Point3,
q: Point3,
}
/// sphere
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// use std::f64::consts::PI;
///
/// let center = Point3::new(1.0, 2.0, 3.0);
/// let radius = 4.56;
///
/// let sphere = Sphere::new(center, radius);
/// const N: usize = 100;
/// for i in 0..=N {
/// for j in 0..=N {
/// // the parameter u is latitude
/// let u = PI * i as f64 / N as f64;
/// // the parameter v is longitude
/// let v = 2.0 * PI * j as f64 / N as f64;
///
/// // simple relation between a point and its normal.
/// let pt = sphere.subs(u, v);
/// let n = sphere.normal(u, v);
/// assert_near!(pt - center, n * radius);
///
/// // the proof of u is latitude and v is longitude
/// assert!((PI / 2.0 - u) * (pt.z - center.z) >= 0.0);
/// assert!((PI - v) * (pt.y - center.y) >= 0.0);
/// }
/// }
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Sphere {
center: Point3,
radius: f64,
}
/// torus
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Torus {
center: Point3,
large_radius: f64,
small_radius: f64,
}
mod circle;
mod hyperbola;
mod line;
mod parabola;
mod plane;
mod sphere;
mod torus;
macro_rules! always_true {
($ty: tt) => {
impl<P> PartialEq for $ty<P> {
fn eq(&self, _: &Self) -> bool { true }
}
impl<P> Eq for $ty<P> {}
};
}
always_true!(UnitCircle);
always_true!(UnitParabola);
always_true!(UnitHyperbola);
-137
View File
@@ -1,137 +0,0 @@
use super::*;
impl<P> UnitParabola<P> {
/// constructor
#[inline]
pub const fn new() -> Self { Self(std::marker::PhantomData) }
}
impl ParametricCurve for UnitParabola<Point2> {
type Point = Point2;
type Vector = Vector2;
#[inline]
fn subs(&self, t: f64) -> Self::Point { Point2::new(t * t, 2.0 * t) }
#[inline]
fn der(&self, t: f64) -> Self::Vector { Vector2::new(2.0 * t, 2.0) }
#[inline]
fn der2(&self, _: f64) -> Self::Vector { Vector2::new(2.0, 0.0) }
}
impl ParametricCurve for UnitParabola<Point3> {
type Point = Point3;
type Vector = Vector3;
#[inline]
fn subs(&self, t: f64) -> Self::Point { Point3::new(t * t, 2.0 * t, 0.0) }
#[inline]
fn der(&self, t: f64) -> Self::Vector { Vector3::new(2.0 * t, 2.0, 0.0) }
#[inline]
fn der2(&self, _: f64) -> Self::Vector { Vector3::new(2.0, 0.0, 0.0) }
}
impl<P> ParameterDivision1D for UnitParabola<P>
where
UnitParabola<P>: ParametricCurve<Point = P>,
P: EuclideanSpace<Scalar = f64> + MetricSpace<Metric = f64> + HashGen<f64>,
{
type Point = P;
fn parameter_division(&self, range: (f64, f64), tol: f64) -> (Vec<f64>, Vec<P>) {
algo::curve::parameter_division(self, range, tol)
}
}
impl SearchNearestParameter<D1> for UnitParabola<Point2> {
type Point = Point2;
#[inline]
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
pt: Point2,
_: H,
_: usize,
) -> Option<f64> {
let p = 2.0 - pt.x;
let q = -pt.y;
solver::pre_solve_cubic(p, q)
.into_iter()
.filter_map(|x| match x.im.so_small() {
true => Some(x.re),
false => None,
})
.min_by(|s, t| {
pt.distance2(self.subs(*s))
.partial_cmp(&pt.distance2(self.subs(*t)))
.unwrap()
})
}
}
impl SearchNearestParameter<D1> for UnitParabola<Point3> {
type Point = Point3;
#[inline]
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
pt: Point3,
_hint: H,
_trials: usize,
) -> Option<f64> {
UnitParabola::<Point2>::new().search_nearest_parameter(
Point2::new(pt.x, pt.y),
_hint,
_trials,
)
}
}
impl SearchParameter<D1> for UnitParabola<Point2> {
type Point = Point2;
#[inline]
fn search_parameter<H: Into<SPHint1D>>(&self, pt: Point2, _: H, _: usize) -> Option<f64> {
let t = pt.y / 2.0;
let pt0 = self.subs(t);
match pt.near(&pt0) {
true => Some(t),
false => None,
}
}
}
impl SearchParameter<D1> for UnitParabola<Point3> {
type Point = Point3;
#[inline]
fn search_parameter<H: Into<SPHint1D>>(
&self,
pt: Point3,
_hint: H,
_trials: usize,
) -> Option<f64> {
match pt.z.so_small() {
true => UnitParabola::<Point2>::new().search_parameter(
Point2::new(pt.x, pt.y),
_hint,
_trials,
),
false => None,
}
}
}
#[test]
fn snp_test() {
let curve = UnitParabola::<Point2>::new();
let p = Point2::new(-3.0, 0.0);
assert_near!(curve.search_nearest_parameter(p, None, 0).unwrap(), 0.0);
let p = Point2::new(-3.0, 6.0);
assert_near!(curve.search_nearest_parameter(p, None, 0).unwrap(), 1.0);
let p = Point2::new(1.5, 1.5);
assert_near!(curve.search_nearest_parameter(p, None, 0).unwrap(), 1.0);
}
#[test]
fn sp_test() {
let curve = UnitParabola::<Point2>::new();
let p = Point2::new(4.0, -4.0);
assert_near!(curve.search_parameter(p, None, 0).unwrap(), -2.0);
let p = Point2::new(-3.0, 6.0);
assert!(curve.search_parameter(p, None, 0).is_none());
}
-256
View File
@@ -1,256 +0,0 @@
use super::*;
impl Plane {
/// Creates a new plane from three points.
#[inline(always)]
pub const fn new(origin: Point3, one: Point3, another: Point3) -> Plane {
Plane {
o: origin,
p: one,
q: another,
}
}
/// Returns the origin
#[inline(always)]
pub const fn origin(&self) -> Point3 { self.o }
/// Returns the u-axis
#[inline(always)]
pub fn u_axis(&self) -> Vector3 { self.p - self.o }
/// Returns the v-axis
#[inline(always)]
pub fn v_axis(&self) -> Vector3 { self.q - self.o }
/// Returns the normal
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let plane = Plane::new(
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// );
/// assert_near!(plane.normal(), Vector3::unit_z());
/// ```
#[inline(always)]
pub fn normal(&self) -> Vector3 { self.u_axis().cross(self.v_axis()).normalize() }
/// Gets the parameter of `pt` in plane's matrix.
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let plane = Plane::new(
/// Point3::new(1.0, 2.0, 3.0),
/// Point3::new(2.0, 1.0, 3.0),
/// Point3::new(3.0, 4.0, -1.0),
/// );
///
/// let pt = Point3::new(2.1, -6.5, 4.7);
/// let prm = plane.get_parameter(pt);
/// let rev = plane.origin()
/// + prm[0] * plane.u_axis()
/// + prm[1] * plane.v_axis()
/// + prm[2] * plane.normal();
/// assert_near!(pt, rev);
/// ```
#[inline(always)]
pub fn get_parameter(&self, pt: Point3) -> Vector3 {
let a = self.u_axis();
let b = self.v_axis();
let c = self.normal();
let mat = Matrix3::from_cols(a, b, c).invert().unwrap();
mat * (pt - self.o)
}
/// into B-spline surface
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let pt0 = Point3::new(0.0, 1.0, 2.0);
/// let pt1 = Point3::new(1.0, 1.0, 3.0);
/// let pt2 = Point3::new(0.0, 2.0, 3.0);
/// let plane: Plane = Plane::new(pt0, pt1, pt2);
/// let surface: BSplineSurface<Point3> = plane.into_bspline();
/// assert_eq!(surface.range_tuple(), ((0.0, 1.0), (0.0, 1.0)));
///
/// const N: usize = 100;
/// for i in 0..=N {
/// for j in 0..=N {
/// let u = i as f64 / N as f64;
/// let v = j as f64 / N as f64;
/// let res = surface.subs(u, v);
/// let ans = plane.subs(u, v);
/// assert_near!(ans, res);
/// }
/// }
/// ```
#[inline(always)]
pub fn into_bspline(&self) -> BSplineSurface<Point3> {
let o = self.o;
let p = self.p;
let q = self.q;
BSplineSurface::debug_new(
(KnotVec::bezier_knot(1), KnotVec::bezier_knot(1)),
vec![vec![o, q], vec![p, p + (q - o)]],
)
}
/// into NURBS surface
/// # Examples
/// ```
/// use truck_geometry::prelude::*;
/// let pt0 = Point3::new(0.0, 1.0, 2.0);
/// let pt1 = Point3::new(1.0, 1.0, 3.0);
/// let pt2 = Point3::new(0.0, 2.0, 3.0);
/// let plane: Plane = Plane::new(pt0, pt1, pt2);
/// let surface: NurbsSurface<Vector4> = plane.into_nurbs();
/// assert_eq!(surface.range_tuple(), ((0.0, 1.0), (0.0, 1.0)));
///
/// const N: usize = 100;
/// for i in 0..=N {
/// for j in 0..=N {
/// let u = i as f64 / N as f64;
/// let v = j as f64 / N as f64;
/// let res = surface.subs(u, v);
/// let ans = plane.subs(u, v);
/// assert_near!(ans, res);
/// }
/// }
/// ```
#[inline(always)]
pub fn into_nurbs(&self) -> NurbsSurface<Vector4> {
let o = self.o.to_homogeneous();
let p = self.p.to_homogeneous();
let q = self.q.to_homogeneous();
NurbsSurface::new(BSplineSurface::debug_new(
(KnotVec::bezier_knot(1), KnotVec::bezier_knot(1)),
vec![vec![o, q], vec![p, p + q - o]],
))
}
}
impl ParametricSurface for Plane {
type Point = Point3;
type Vector = Vector3;
#[inline(always)]
fn subs(&self, u: f64, v: f64) -> Point3 {
self.o + u * (self.p - self.o) + v * (self.q - self.o)
}
#[inline(always)]
fn uder(&self, _: f64, _: f64) -> Vector3 { self.p - self.o }
#[inline(always)]
fn vder(&self, _: f64, _: f64) -> Vector3 { self.q - self.o }
#[inline(always)]
fn uuder(&self, _: f64, _: f64) -> Vector3 { Vector3::zero() }
#[inline(always)]
fn uvder(&self, _: f64, _: f64) -> Vector3 { Vector3::zero() }
#[inline(always)]
fn vvder(&self, _: f64, _: f64) -> Vector3 { Vector3::zero() }
/// as square
#[inline(always)]
fn parameter_range(&self) -> (ParameterRange, ParameterRange) {
let range = (Bound::Included(0.0), Bound::Included(1.0));
(range, range)
}
}
impl ParametricSurface3D for Plane {
#[inline(always)]
fn normal(&self, _: f64, _: f64) -> Vector3 { self.normal() }
}
impl BoundedSurface for Plane {}
impl Invertible for Plane {
#[inline(always)]
fn inverse(&self) -> Self {
Plane {
o: self.o,
p: self.q,
q: self.p,
}
}
#[inline(always)]
fn invert(&mut self) { *self = self.inverse(); }
}
impl IncludeCurve<BSplineCurve<Point3>> for Plane {
#[inline(always)]
fn include(&self, curve: &BSplineCurve<Point3>) -> bool {
let origin = self.origin();
let normal = self.normal();
curve
.control_points()
.iter()
.all(|pt| (pt - origin).dot(normal).so_small())
}
}
impl IncludeCurve<NurbsCurve<Vector4>> for Plane {
fn include(&self, curve: &NurbsCurve<Vector4>) -> bool {
let origin = self.origin();
let normal = self.normal();
let (s, e) = (curve.front(), curve.back());
if !(s - origin).dot(normal).so_small() || !(e - origin).dot(normal).so_small() {
return false;
}
curve.non_rationalized().control_points().iter().all(|pt| {
if pt[3].so_small() {
true
} else {
let pt = Point3::from_homogeneous(*pt);
(pt - origin).dot(normal).so_small()
}
})
}
}
impl ParameterDivision2D for Plane {
#[inline(always)]
fn parameter_division(&self, range: ((f64, f64), (f64, f64)), _: f64) -> (Vec<f64>, Vec<f64>) {
(vec![range.0 .0, range.0 .1], vec![range.1 .0, range.1 .1])
}
}
impl<T: Transform3<Scalar = f64>> Transformed<T> for Plane {
#[inline(always)]
fn transform_by(&mut self, trans: T) {
self.o = trans.transform_point(self.o);
self.p = trans.transform_point(self.p);
self.q = trans.transform_point(self.q);
}
#[inline(always)]
fn transformed(&self, trans: T) -> Self {
Plane {
o: trans.transform_point(self.o),
p: trans.transform_point(self.p),
q: trans.transform_point(self.q),
}
}
}
impl SearchParameter<D2> for Plane {
type Point = Point3;
#[inline(always)]
fn search_parameter<H: Into<SPHint2D>>(
&self,
point: Point3,
_: H,
_: usize,
) -> Option<(f64, f64)> {
let v = self.get_parameter(point);
match v[2].so_small() {
true => Some((v[0], v[1])),
false => None,
}
}
}
impl SearchNearestParameter<D2> for Plane {
type Point = Point3;
#[inline(always)]
fn search_nearest_parameter<H: Into<SPHint2D>>(
&self,
point: Point3,
_: H,
_: usize,
) -> Option<(f64, f64)> {
let v = self.get_parameter(point);
Some((v[0], v[1]))
}
}
-219
View File
@@ -1,219 +0,0 @@
use super::*;
use std::f64::consts::PI;
impl Sphere {
/// Creates a sphere
#[inline(always)]
pub const fn new(center: Point3, radius: f64) -> Sphere { Sphere { center, radius } }
/// Returns the center
#[inline(always)]
pub const fn center(&self) -> Point3 { self.center }
/// Returns the radius
#[inline(always)]
pub const fn radius(&self) -> f64 { self.radius }
/// Returns whether the point `pt` is on sphere
#[inline(always)]
pub fn include(&self, pt: Point3) -> bool { self.center.distance(pt).near(&self.radius) }
}
impl ParametricSurface for Sphere {
type Point = Point3;
type Vector = Vector3;
#[inline(always)]
fn subs(&self, u: f64, v: f64) -> Point3 { self.center() + self.radius * self.normal(u, v) }
#[inline(always)]
fn uder(&self, u: f64, v: f64) -> Vector3 {
self.radius
* Vector3::new(
f64::cos(u) * f64::cos(v),
f64::cos(u) * f64::sin(v),
-f64::sin(u),
)
}
#[inline(always)]
fn vder(&self, u: f64, v: f64) -> Vector3 {
self.radius * f64::sin(u) * Vector3::new(-f64::sin(v), f64::cos(v), 0.0)
}
#[inline(always)]
fn uuder(&self, u: f64, v: f64) -> Vector3 { -self.radius * self.normal(u, v) }
#[inline(always)]
fn uvder(&self, u: f64, v: f64) -> Vector3 {
self.radius * f64::cos(u) * Vector3::new(-f64::sin(v), f64::cos(v), 0.0)
}
#[inline(always)]
fn vvder(&self, u: f64, v: f64) -> Vector3 {
-self.radius * f64::sin(u) * Vector3::new(f64::cos(v), f64::sin(v), 0.0)
}
#[inline(always)]
fn parameter_range(&self) -> (ParameterRange, ParameterRange) {
(
(Bound::Included(0.0), Bound::Included(PI)),
(Bound::Included(0.0), Bound::Excluded(2.0 * PI)),
)
}
#[inline(always)]
fn v_period(&self) -> Option<f64> { Some(2.0 * PI) }
}
impl ParametricSurface3D for Sphere {
#[inline(always)]
fn normal(&self, u: f64, v: f64) -> Vector3 {
Vector3::new(
f64::sin(u) * f64::cos(v),
f64::sin(u) * f64::sin(v),
f64::cos(u),
)
}
}
#[test]
fn sphere_derivation_test() {
let center = Point3::new(1.0, 2.0, 3.0);
let radius = 4.56;
let sphere = Sphere::new(center, radius);
const N: usize = 100;
for i in 0..N {
for j in 0..N {
let u = PI * i as f64 / N as f64;
let v = 2.0 * PI * j as f64 / N as f64;
let normal = sphere.normal(u, v);
assert!(normal.dot(sphere.uder(u, v)).so_small());
assert!(normal.dot(sphere.vder(u, v)).so_small());
}
}
}
impl BoundedSurface for Sphere {}
impl IncludeCurve<BSplineCurve<Point3>> for Sphere {
#[inline(always)]
fn include(&self, curve: &BSplineCurve<Point3>) -> bool {
curve.is_const() && self.include(curve.front())
}
}
impl IncludeCurve<NurbsCurve<Vector4>> for Sphere {
fn include(&self, curve: &NurbsCurve<Vector4>) -> bool {
let (knots, _) = curve.knot_vec().to_single_multi();
let degree = curve.degree() * 2;
knots
.windows(2)
.flat_map(move |window| (1..degree).map(move |i| (window, i)))
.all(move |(window, i)| {
let t = i as f64 / degree as f64;
let t = window[0] * (1.0 - t) + window[1] * t;
self.include(curve.subs(t))
})
}
}
impl ParameterDivision2D for Sphere {
#[inline(always)]
fn parameter_division(
&self,
(urange, vrange): ((f64, f64), (f64, f64)),
tol: f64,
) -> (Vec<f64>, Vec<f64>) {
nonpositive_tolerance!(tol);
assert!(
tol < self.radius,
"Tolerance is larger than the radius of sphere."
);
let delta = 2.0 * f64::acos(1.0 - tol / self.radius);
let u_div = 1 + ((urange.1 - urange.0) / delta).floor() as usize;
let v_div = 1 + ((vrange.1 - vrange.0) / delta).floor() as usize;
(
(0..=u_div)
.map(|i| urange.0 + (urange.1 - urange.0) * i as f64 / u_div as f64)
.collect(),
(0..=v_div)
.map(|j| vrange.0 + (vrange.1 - vrange.0) * j as f64 / v_div as f64)
.collect(),
)
}
}
impl SearchParameter<D2> for Sphere {
type Point = Point3;
#[inline(always)]
fn search_parameter<H: Into<SPHint2D>>(
&self,
point: Point3,
hint: H,
_: usize,
) -> Option<(f64, f64)> {
let radius = point - self.center;
if (self.radius * self.radius).near(&radius.magnitude2()) {
let radius = radius.normalize();
let u = f64::acos(radius[2]);
let sinu = f64::sqrt(1.0 - radius[2] * radius[2]);
let cosv = f64::clamp(radius[0] / sinu, -1.0, 1.0);
let v = if sinu.so_small() {
match hint.into() {
SPHint2D::Parameter(_, hint) => hint,
_ => 0.0,
}
} else if radius[1] > 0.0 {
f64::acos(cosv)
} else {
2.0 * PI - f64::acos(cosv)
};
Some((u, v))
} else {
None
}
}
}
impl SearchNearestParameter<D2> for Sphere {
type Point = Point3;
#[inline(always)]
fn search_nearest_parameter<H: Into<SPHint2D>>(
&self,
point: Point3,
_: H,
_: usize,
) -> Option<(f64, f64)> {
let radius = (point - self.center).normalize();
let u = f64::acos(radius[2]);
let sinu = f64::sqrt(1.0 - radius[2] * radius[2]);
let cosv = radius[0] / sinu;
let v = if radius[1] > 0.0 {
f64::acos(cosv)
} else {
2.0 * PI - f64::acos(cosv)
};
Some((u, v))
}
}
#[cfg(test)]
fn exec_search_parameter_test() {
let center = Point3::new(
100.0 * rand::random::<f64>() - 50.0,
100.0 * rand::random::<f64>() - 50.0,
100.0 * rand::random::<f64>() - 50.0,
);
let radius = 100.0 * rand::random::<f64>();
let sphere = Sphere::new(center, radius);
let u = PI * rand::random::<f64>();
let v = 2.0 * PI * rand::random::<f64>();
let pt = sphere.subs(u, v);
let (u0, v0) = sphere.search_parameter(pt, None, 100).unwrap();
assert_near!(Vector2::new(u, v), Vector2::new(u0, v0));
let pt = pt
+ Vector3::new(
(0.1 * rand::random::<f64>() + 0.01) * f64::signum(rand::random::<f64>() - 0.5),
(0.1 * rand::random::<f64>() + 0.01) * f64::signum(rand::random::<f64>() - 0.5),
(0.1 * rand::random::<f64>() + 0.01) * f64::signum(rand::random::<f64>() - 0.5),
);
assert!(sphere.search_parameter(pt, None, 100).is_none());
let (u, v) = sphere.search_nearest_parameter(pt, None, 100).unwrap();
assert_near!(
sphere.subs(u, v),
center + (pt - center).normalize() * radius
);
}
#[test]
fn search_parameter_test() { (0..10).for_each(|_| exec_search_parameter_test()) }
-238
View File
@@ -1,238 +0,0 @@
use super::*;
use std::f64::consts::PI;
impl Torus {
/// constructor
#[inline(always)]
pub fn new(center: Point3, large_radius: f64, small_radius: f64) -> Self {
if large_radius <= 0.0 || small_radius <= 0.0 {
panic!("radius must be larger than 0");
}
Self {
center,
large_radius,
small_radius,
}
}
/// get center
#[inline(always)]
pub const fn center(&self) -> Point3 { self.center }
/// get large radius
#[inline(always)]
pub const fn large_radius(&self) -> f64 { self.large_radius }
/// get small radius
#[inline(always)]
pub const fn small_radius(&self) -> f64 { self.small_radius }
}
impl ParametricSurface for Torus {
type Point = Point3;
type Vector = Vector3;
#[inline(always)]
fn subs(&self, u: f64, v: f64) -> Point3 {
let sr = self.small_radius() * Vector2::new(f64::cos(v), f64::sin(v));
let lr = (self.large_radius() + sr.x) * Vector2::new(f64::cos(u), f64::sin(u));
self.center() + Vector3::new(lr.x, lr.y, sr.y)
}
#[inline(always)]
fn uder(&self, u: f64, v: f64) -> Vector3 {
let sr = self.small_radius() * f64::cos(v);
let lr = (self.large_radius() + sr) * Vector2::new(f64::cos(u), f64::sin(u));
Vector3::new(-lr.y, lr.x, 0.0)
}
#[inline(always)]
fn vder(&self, u: f64, v: f64) -> Vector3 {
let sv = self.small_radius() * Vector2::new(-f64::sin(v), f64::cos(v));
Vector3::new(sv.x * f64::cos(u), sv.x * f64::sin(u), sv.y)
}
#[inline(always)]
fn uuder(&self, u: f64, v: f64) -> Vector3 {
let sr = self.small_radius() * f64::cos(v);
let lr = (self.large_radius() + sr) * Vector2::new(f64::cos(u), f64::sin(u));
Vector3::new(-lr.x, -lr.y, 0.0)
}
#[inline(always)]
fn uvder(&self, u: f64, v: f64) -> Vector3 {
let sr = -self.small_radius() * f64::sin(v);
let lr = sr * Vector2::new(f64::cos(u), f64::sin(u));
Vector3::new(-lr.y, lr.x, 0.0)
}
#[inline(always)]
fn vvder(&self, u: f64, v: f64) -> Vector3 {
let sv = -self.small_radius() * Vector2::new(f64::cos(v), f64::sin(v));
Vector3::new(sv.x * f64::cos(u), sv.x * f64::sin(u), sv.y)
}
#[inline(always)]
fn parameter_range(&self) -> (ParameterRange, ParameterRange) {
const RANGE: (Bound<f64>, Bound<f64>) = (Bound::Included(0.0), Bound::Excluded(2.0 * PI));
(RANGE, RANGE)
}
#[inline(always)]
fn u_period(&self) -> Option<f64> { Some(2.0 * PI) }
#[inline(always)]
fn v_period(&self) -> Option<f64> { Some(2.0 * PI) }
}
impl ParametricSurface3D for Torus {
fn normal(&self, u: f64, v: f64) -> Vector3 {
let sv = Vector2::new(f64::cos(v), f64::sin(v));
Vector3::new(sv.x * f64::cos(u), sv.x * f64::sin(u), sv.y)
}
}
impl BoundedSurface for Torus {}
impl SearchParameter<D2> for Torus {
type Point = Point3;
fn search_parameter<H: Into<SPHint2D>>(
&self,
point: Point3,
_: H,
_: usize,
) -> Option<(f64, f64)> {
let r = point - self.center();
let rxy = Vector2::new(r.x, r.y);
let v = f64::asin(f64::clamp(r.z / self.small_radius(), -1.0, 1.0));
let minus = rxy.magnitude2() < self.large_radius() * self.large_radius();
let v = match (minus, v < 0.0) {
(true, _) => PI - v,
(false, false) => v,
(false, true) => 2.0 * PI + v,
};
let rxy_n = rxy.normalize();
let u = f64::acos(f64::clamp(rxy_n.x, -1.0, 1.0));
let u = match rxy_n.y < 0.0 {
true => 2.0 * PI - u,
false => u,
};
match self.subs(u, v).near(&point) {
true => Some((u, v)),
false => None,
}
}
}
impl SearchNearestParameter<D2> for Torus {
type Point = Point3;
fn search_nearest_parameter<H: Into<SPHint2D>>(
&self,
point: Point3,
_: H,
_: usize,
) -> Option<(f64, f64)> {
let r = point - self.center();
let rxy = Vector2::new(r.x, r.y);
if rxy.so_small() {
return None;
}
let rxy_n = rxy.normalize();
let large_r = self.large_radius() * rxy_n.extend(0.0);
let diff = r - large_r;
if diff.so_small() {
return None;
}
let small_r = diff.normalize();
let u = f64::acos(f64::clamp(rxy_n.x, -1.0, 1.0));
let u = match rxy_n.y < 0.0 {
true => 2.0 * PI - u,
false => u,
};
let v = f64::asin(f64::clamp(small_r.z, -1.0, 1.0));
let v = match (small_r.dot(large_r) < 0.0, v < 0.0) {
(true, _) => PI - v,
(false, false) => v,
(false, true) => 2.0 * PI + v,
};
Some((u, v))
}
}
impl ParameterDivision2D for Torus {
fn parameter_division(
&self,
(urange, vrange): ((f64, f64), (f64, f64)),
tol: f64,
) -> (Vec<f64>, Vec<f64>) {
let circle = UnitCircle::<Point2>::new();
let utol = tol / (self.small_radius() + self.large_radius());
let (udiv, _) = circle.parameter_division(urange, utol);
let vtol = tol / self.small_radius();
let (vdiv, _) = circle.parameter_division(vrange, vtol);
(udiv, vdiv)
}
}
#[test]
fn surface() {
use std::ops::RangeBounds;
let small_radius = 5.0 * rand::random::<f64>() + 1.0;
let large_radius = 2.0 * small_radius + 5.0 * rand::random::<f64>();
let torus = Torus::new(Point3::new(0.0, 0.0, 0.0), large_radius, small_radius);
const N: usize = 10;
const EPS: f64 = 1.0e-2;
for i in 0..N {
for j in 0..N {
let u = 2.0 * PI * i as f64 / N as f64;
let v = 2.0 * PI * j as f64 / N as f64;
let p = torus.subs(u, v);
let r = large_radius * Point3::new(f64::cos(u), f64::sin(u), 0.0);
assert_near!(p.distance(r), small_radius);
assert_near!(p.z, small_radius * f64::sin(v));
let uder0 = torus.uder(u, v);
let uder1 = (torus.subs(u + EPS, v) - torus.subs(u - EPS, v)) / (2.0 * EPS);
assert!((uder0 - uder1).magnitude() < EPS, "{:?} {:?}", uder0, uder1);
let vder0 = torus.vder(u, v);
let vder1 = (torus.subs(u, v + EPS) - torus.subs(u, v - EPS)) / (2.0 * EPS);
assert!((vder0 - vder1).magnitude() < EPS, "{:?} {:?}", vder0, vder1);
let uuder0 = torus.uuder(u, v);
let uuder1 = (torus.uder(u + EPS, v) - torus.uder(u - EPS, v)) / (2.0 * EPS);
assert!(
(uuder0 - uuder1).magnitude() < EPS,
"{:?} {:?}",
uuder0,
uuder1
);
let uvder0 = torus.uvder(u, v);
let uvder1 = (torus.vder(u + EPS, v) - torus.vder(u - EPS, v)) / (2.0 * EPS);
assert!(
(uvder0 - uvder1).magnitude() < EPS,
"{:?} {:?}",
uvder0,
uvder1
);
let vvder0 = torus.vvder(u, v);
let vvder1 = (torus.vder(u, v + EPS) - torus.vder(u, v - EPS)) / (2.0 * EPS);
assert!(
(vvder0 - vvder1).magnitude() < EPS,
"{:?} {:?}",
vvder0,
vvder1
);
let n0 = torus.normal(u, v);
let n1 = torus.uder(u, v).cross(torus.vder(u, v)).normalize();
assert_near!(n0, n1);
let (u0, v0) = torus.search_parameter(p, None, 1).unwrap();
let (urange, vrange) = torus.parameter_range();
assert!(urange.contains(&u0) && vrange.contains(&v0), "{u0}, {v0}");
assert_near!(torus.subs(u0, v0), p);
let deform = (0.8 * rand::random::<f64>() - 0.4) * small_radius;
let q = p + deform * n0;
let (u0, v0) = torus.search_nearest_parameter(q, None, 1).unwrap();
let (urange, vrange) = torus.parameter_range();
assert!(urange.contains(&u0) && vrange.contains(&v0), "{u0}, {v0}");
assert_near!(torus.subs(u0, v0), p);
}
}
}
-117
View File
@@ -1,117 +0,0 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "truck-meshalgo"
version = "0.4.0"
build = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Mesh algorigthms, include tessellations of the shape."
homepage = "https://github.com/ricosjp/truck"
readme = "README.md"
keywords = [
"truck",
"graphics",
"mesh",
]
categories = ["graphics"]
license = "Apache-2.0"
repository = "https://github.com/ricosjp/truck"
[lib]
name = "truck_meshalgo"
path = "src/lib.rs"
[dependencies.array-macro]
version = "2.1.8"
[dependencies.derive_more]
version = "0.99.18"
[dependencies.itertools]
version = "0.13.0"
[dependencies.rustc-hash]
version = "2.0.0"
[dependencies.spade]
version = "2.12.1"
optional = true
[dependencies.truck-base]
version = "0.5.0"
[dependencies.truck-geometry]
version = "0.5.0"
[dependencies.truck-modeling]
version = "0.6.0"
[dependencies.truck-polymesh]
version = "0.6.0"
[dependencies.truck-topology]
version = "0.6.0"
[dev-dependencies.proptest]
version = "1.5.0"
[dev-dependencies.rand]
version = "0.8.5"
[dev-dependencies.serde_json]
version = "1.0.128"
[dev-dependencies.truck-modeling]
version = "0.6.0"
[features]
analyzers = []
default = [
"analyzers",
"filters",
"tessellation",
]
filters = []
# Exposes equivalence checks for the SolidCraft kernel tests. Never on in shipped builds.
test-hooks = []
tessellation = [
"spade",
"filters",
]
vtk = ["vtkio"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.rayon]
version = "1.10.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.vtkio]
version = "0.6.3"
optional = true
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.getrandom]
version = "0.2.15"
features = ["js"]
-177
View File
@@ -1,177 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
-68
View File
@@ -1,68 +0,0 @@
# truck-meshalgo
[![Crates.io](https://img.shields.io/crates/v/truck-meshalgo.svg)](https://crates.io/crates/truck-meshalgo) [![Docs.rs](https://docs.rs/truck-meshalgo/badge.svg)](https://docs.rs/truck-meshalgo)
Mesh algorigthms, include tessellations of the shape.
## Sample Codes
### filleted-cube
An experiment to decompose a mesh into elements for future NURBS-shape approximation of the mesh.
Contains `doc(hidden)` methods.
- Input: filleted_cube.obj
- Output: planes.obj, lower.obj, upper.obj
### irregular-sphere
Add the normal to the sphere containing
the irregular normal generated from the NURBS containing the critical point.
- Input: irregular_sphere.obj
- Output: regular_sphere.obj
### octahedron-subdivision
Apply loop subdivision to regular octahedron.
- Input: hardcoded octahedron
- Output: octahedron.obj, subdivision-octahedron.obj
### requadrangulate-buddha
A benchmark that reads in heavy mesh data, applies triangulation and quadrangulation, and writes it out.
- Input: happy-buddha.obj
- Output: requadrangulated-buddha.obj
### smoothing-bunny
Adds smooth normals to the stanford bunny.
- Input: bunny.obj
- Output: smooth_bunny.obj
### splitting-sample
An experiment to decompose a mesh into elements for future NURBS-shape approximation of the mesh.
- Input: sample.obj
- Output: planes_parts_#.obj, others_parts_#.obj
### teapot
Adds smooth normals to and quadrangulate the famous teapot.
- Input: teapot.obj
- Output: quaded_pot.obj
### tessellate-shape
Tessellate a shape and output an obj file.
```bash
usage: tessellate-shape <input json file> <output json file>
```
The default `<output file>` is output.obj.
-420
View File
@@ -1,420 +0,0 @@
use super::*;
use array_macro::array;
/// Find collisions between two polygon meshes and extract interference lines.
///
/// # Details
///
/// The algorithm for mesh extraction is as follows.
/// All the triangles are projected toward the axis to form intervals.
/// By looking at the overlap of these intervals, we can narrow down the interfering triangles.
/// The overlap of the intervals is obtained by sorting the endpoints.
///
/// # Remarks
///
/// We see that the surfaces that are in contact are not interfering with each other.
/// Therefore, polygon meshes included in one plane are not interfering.
pub trait Collision {
/// If `self` and `other` collide, then returns only one interference line.
/// Otherwise, returns `None`.
fn collide_with(&self, other: &PolygonMesh) -> Option<(Point3, Point3)>;
/// Extract all interference lines between `self` and `other`.
/// # Remarks
/// The results is not arranged so that included lines make continuous maximal polyline curve.
fn extract_interference(&self, other: &PolygonMesh) -> Vec<(Point3, Point3)>;
}
impl Collision for PolygonMesh {
#[inline(always)]
fn collide_with(&self, other: &PolygonMesh) -> Option<(Point3, Point3)> {
are_colliding(self, other)
}
#[inline(always)]
fn extract_interference(&self, other: &PolygonMesh) -> Vec<(Point3, Point3)> {
collision(self, other)
}
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum EndPointType {
Front,
Back,
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
struct EndPoint {
entity: f64,
r#type: EndPointType,
segnum: usize,
index: usize,
}
impl EndPoint {
#[inline(always)]
fn new(entity: f64, r#type: EndPointType, segnum: usize, index: usize) -> EndPoint {
EndPoint {
entity,
r#type,
segnum,
index,
}
}
#[inline(always)]
fn from_seg(seg: (f64, f64), segnum: usize, index: usize) -> Vec<EndPoint> {
vec![
EndPoint::new(seg.0, EndPointType::Front, segnum, index),
EndPoint::new(seg.1, EndPointType::Back, segnum, index),
]
}
}
fn tri_to_seg(tri: [Point3; 3], unit: Vector3) -> (f64, f64) {
let a = tri[0].to_vec().dot(unit);
let b = tri[1].to_vec().dot(unit);
let c = tri[2].to_vec().dot(unit);
(f64::min(f64::min(a, b), c), f64::max(f64::max(a, b), c))
}
fn sorted_endpoints<I, J>(iter0: I, iter1: J, unit: Vector3) -> Vec<EndPoint>
where
I: IntoIterator<Item = [Point3; 3]>,
J: IntoIterator<Item = [Point3; 3]>, {
let mut res: Vec<EndPoint> = iter0
.into_iter()
.enumerate()
.filter(|(_, tri)| !(tri[1] - tri[0]).cross(tri[2] - tri[0]).so_small())
.flat_map(|(i, tri)| EndPoint::from_seg(tri_to_seg(tri, unit), 0, i))
.chain(
iter1
.into_iter()
.enumerate()
.filter(|(_, tri)| !(tri[1] - tri[0]).cross(tri[2] - tri[0]).so_small())
.flat_map(|(i, tri)| EndPoint::from_seg(tri_to_seg(tri, unit), 1, i)),
)
.collect();
res.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Greater));
res
}
fn collide_seg_triangle(seg: [Point3; 2], tri: [Point3; 3]) -> Option<Point3> {
let ab = tri[1] - tri[0];
let bc = tri[2] - tri[1];
let ca = tri[0] - tri[2];
let nor = ab.cross(ca);
if nor.so_small() {
return None;
}
let ap = seg[0] - tri[0];
let aq = seg[1] - tri[0];
let dotapnor = ap.dot(nor);
let dotaqnor = aq.dot(nor);
if dotapnor * dotaqnor > 0.0 {
return None;
}
let h = seg[0] + dotapnor / (dotapnor - dotaqnor) * (seg[1] - seg[0]);
if f64::signum(ab.cross(nor).dot(h - tri[0]) + TOLERANCE2)
+ f64::signum(bc.cross(nor).dot(h - tri[1]) + TOLERANCE2)
+ f64::signum(ca.cross(nor).dot(h - tri[2]) + TOLERANCE2)
>= 2.0
{
Some(h)
} else {
None
}
}
fn collide_triangles(tri0: [Point3; 3], tri1: [Point3; 3]) -> Option<(Point3, Point3)> {
let mut tuple = (None, None);
[
collide_seg_triangle([tri0[0], tri0[1]], tri1),
collide_seg_triangle([tri0[1], tri0[2]], tri1),
collide_seg_triangle([tri0[2], tri0[0]], tri1),
collide_seg_triangle([tri1[0], tri1[1]], tri0),
collide_seg_triangle([tri1[1], tri1[2]], tri0),
collide_seg_triangle([tri1[2], tri1[0]], tri0),
]
.into_iter()
.for_each(|pt| match tuple {
(None, _) => tuple.0 = pt,
(Some(_), None) => tuple.1 = pt,
(Some(ref mut p), Some(ref mut q)) => {
if let Some(pt) = pt {
let dist0 = pt.distance2(*p);
let dist1 = pt.distance2(*q);
let dist2 = p.distance2(*q);
if dist2 < dist0 {
*q = pt;
} else if dist2 < dist1 {
*p = pt;
}
}
}
});
match tuple {
(Some(a), Some(b)) => Some((a, b)),
_ => None,
}
}
fn make_pos_tri(poly: &PolygonMesh, face: [StandardVertex; 3]) -> [Point3; 3] {
array![i => poly.positions()[face[i].pos]; 3]
}
/// Triangles of a mesh with their bounding boxes, each computed once.
fn boxed_triangles(poly: &PolygonMesh) -> Vec<([Point3; 3], BoundingBox<Point3>)> {
poly.faces()
.triangle_iter()
.map(|face| {
let tri = make_pos_tri(poly, face);
(tri, tri.iter().collect())
})
.collect()
}
/// Whether two triangles' bounding boxes miss each other (touching boxes do not miss).
#[inline(always)]
fn disjoint_boxes(bdb0: &BoundingBox<Point3>, bdb1: &BoundingBox<Point3>) -> bool {
bdb0.max()[0] < bdb1.min()[0]
|| bdb1.max()[0] < bdb0.min()[0]
|| bdb0.max()[1] < bdb1.min()[1]
|| bdb1.max()[1] < bdb0.min()[1]
|| bdb0.max()[2] < bdb1.min()[2]
|| bdb1.max()[2] < bdb0.min()[2]
}
/// Pairs of triangles (one from each mesh) whose bounding boxes overlap, in the order the
/// sweep along `unit` meets them.
///
/// The sweep keeps the triangles whose interval along `unit` is open. When a triangle opens,
/// it pairs with every open triangle of the other mesh whose box it overlaps. The pairs and
/// their order are the same as the earlier version, which cloned the open list for every
/// triangle and rejected disjoint boxes afterwards (rebuilding both boxes for each pair).
fn candidate_pairs(
tris0: &[([Point3; 3], BoundingBox<Point3>)],
tris1: &[([Point3; 3], BoundingBox<Point3>)],
unit: Vector3,
) -> Vec<(usize, usize)> {
let endpoints = sorted_endpoints(
tris0.iter().map(|(t, _)| *t),
tris1.iter().map(|(t, _)| *t),
unit,
);
let boxes = [tris0, tris1];
let mut open = [Vec::<usize>::new(), Vec::<usize>::new()];
let mut pairs = Vec::new();
for EndPoint {
r#type,
segnum,
index,
..
} in endpoints
{
match r#type {
EndPointType::Front => {
let mine = &boxes[segnum][index].1;
for &other in &open[1 - segnum] {
if !disjoint_boxes(mine, &boxes[1 - segnum][other].1) {
pairs.push(if segnum == 0 {
(index, other)
} else {
(other, index)
});
}
}
open[segnum].push(index);
}
EndPointType::Back => {
// Every interval opens before it closes unless a NaN broke the sort. Then the
// interval stays open: more box tests, never a panic (the web build aborts on any).
if let Some(i) = open[segnum].iter().position(|idx| *idx == index) {
open[segnum].swap_remove(i);
}
}
}
}
pairs
}
fn collision(poly0: &PolygonMesh, poly1: &PolygonMesh) -> Vec<(Point3, Point3)> {
let unit = match poly0.positions().is_empty() {
true => return Vec::new(),
false => hash::take_one_unit(poly0.positions()[0]),
};
let (tris0, tris1) = (boxed_triangles(poly0), boxed_triangles(poly1));
let pairs = candidate_pairs(&tris0, &tris1, unit);
let test = |&(i, j): &(usize, usize)| collide_triangles(tris0[i].0, tris1[j].0);
#[cfg(not(target_arch = "wasm32"))]
{
use rayon::prelude::*;
// Indexed parallel collect: results in the same order as the sequential loop.
pairs.par_iter().filter_map(test).collect()
}
#[cfg(target_arch = "wasm32")]
{
pairs.iter().filter_map(test).collect()
}
}
fn are_colliding(poly0: &PolygonMesh, poly1: &PolygonMesh) -> Option<(Point3, Point3)> {
let unit = match poly0.positions().is_empty() {
true => return None,
false => hash::take_one_unit(poly0.positions()[0]),
};
let (tris0, tris1) = (boxed_triangles(poly0), boxed_triangles(poly1));
let pairs = candidate_pairs(&tris0, &tris1, unit);
let test = |&(i, j): &(usize, usize)| collide_triangles(tris0[i].0, tris1[j].0);
#[cfg(not(target_arch = "wasm32"))]
{
use rayon::prelude::*;
// The first collision in sweep order, as the sequential search returns.
pairs.par_iter().find_map_first(test)
}
#[cfg(target_arch = "wasm32")]
{
pairs.iter().find_map(test)
}
}
#[test]
fn collide_triangles_test() {
let tri0 = [
Point3::origin(),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
];
let tri1 = [
Point3::new(0.0, 0.0, -1.0),
Point3::new(-1.0, -1.0, 1.0),
Point3::new(1.0, 1.0, 1.0),
];
assert!(collide_triangles(tri0, tri1).is_some());
let tri0 = [
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
];
let tri1 = [
Point3::new(0.0, 0.0, 0.5),
Point3::new(1.0, 0.0, 1.0),
Point3::new(1.0, 1.0, 1.0),
];
assert!(collide_triangles(tri0, tri1).is_none());
}
/// The candidate search before boxes were computed once and the open list stopped being
/// cloned: kept to check the new search finds the same collisions in the same order.
///
/// Not part of the API. Built only for tests, or with the `test-hooks` feature, so the
/// SolidCraft kernel tests can run these checks in CI (this crate's own tests do not run there).
#[cfg(any(test, feature = "test-hooks"))]
#[doc(hidden)]
pub mod test_hooks {
use super::*;
fn old_pairs(sort_endpoints: Vec<EndPoint>) -> Vec<(usize, usize)> {
let mut current = [Vec::<usize>::new(), Vec::<usize>::new()];
let mut out = Vec::new();
for EndPoint { r#type, segnum, index, .. } in sort_endpoints {
match r#type {
EndPointType::Front => {
current[segnum].push(index);
for i in current[1 - segnum].clone() {
out.push(if segnum == 0 { (index, i) } else { (i, index) });
}
}
EndPointType::Back => {
if let Some(i) = current[segnum].iter().position(|idx| *idx == index) {
current[segnum].swap_remove(i);
}
}
}
}
out
}
fn old_disjoint(tri0: [Point3; 3], tri1: [Point3; 3]) -> bool {
let b0: BoundingBox<Point3> = tri0.iter().collect();
let b1: BoundingBox<Point3> = tri1.iter().collect();
disjoint_boxes(&b0, &b1)
}
/// Small seeded generator (no extra dependency).
struct Lcg(u64);
impl Lcg {
fn unit(&mut self) -> f64 {
self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
(self.0 >> 11) as f64 / (1u64 << 53) as f64
}
/// On a coarse grid half the time, so shared vertices, touching boxes and
/// degenerate triangles come up.
fn coord(&mut self) -> f64 {
if self.unit() < 0.5 { (self.unit() * 8.0).floor() * 0.5 } else { self.unit() * 4.0 }
}
fn tri(&mut self) -> [Point3; 3] {
array![_ => Point3::new(self.coord(), self.coord(), self.coord()); 3]
}
}
/// A NaN coordinate breaks the endpoint sort: the search must go on without a panic.
pub fn nan_triangles_do_not_panic() -> Result<(), String> {
let good = [Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0), Point3::new(0.0, 1.0, 0.0)];
let nan = [Point3::new(f64::NAN, 0.0, 0.0), Point3::new(1.0, f64::NAN, 0.0), Point3::new(0.0, 1.0, 0.5)];
let crossing = [Point3::new(0.2, 0.2, -1.0), Point3::new(0.2, 0.2, 1.0), Point3::new(0.6, 0.2, 0.0)];
for (a, b) in [(vec![good, nan], vec![crossing, nan]), (vec![nan, nan], vec![nan]), (vec![nan], vec![good])] {
let ba: Vec<_> = a.iter().map(|t| (*t, t.iter().collect())).collect();
let bb: Vec<_> = b.iter().map(|t| (*t, t.iter().collect())).collect();
let r = std::panic::catch_unwind(|| candidate_pairs(&ba, &bb, Vector3::new(1.0, 0.0, 0.0)));
if r.is_err() {
return Err(format!("panicked on {a:?} / {b:?}"));
}
}
Ok(())
}
/// The new candidate search finds the same collisions, in the same order, as the old one
/// on random triangle sets. Returns how many collisions were compared.
pub fn same_collisions_in_the_same_order(seeds: std::ops::Range<u64>) -> Result<usize, String> {
let mut compared = 0;
for seed in seeds {
let mut r = Lcg(seed * 7919 + 1);
let n0 = 1 + (r.unit() * 60.0) as usize;
let n1 = 1 + (r.unit() * 60.0) as usize;
let t0: Vec<[Point3; 3]> = (0..n0).map(|_| r.tri()).collect();
let t1: Vec<[Point3; 3]> = (0..n1).map(|_| r.tri()).collect();
let unit = hash::take_one_unit(t0[0][0]);
let old: Vec<(Point3, Point3)> = old_pairs(sorted_endpoints(t0.iter().copied(), t1.iter().copied(), unit))
.into_iter()
.filter(|&(i, j)| !old_disjoint(t0[i], t1[j]))
.filter_map(|(i, j)| collide_triangles(t0[i], t1[j]))
.collect();
let b0: Vec<_> = t0.iter().map(|t| (*t, t.iter().collect())).collect();
let b1: Vec<_> = t1.iter().map(|t| (*t, t.iter().collect())).collect();
let new: Vec<(Point3, Point3)> = candidate_pairs(&b0, &b1, unit)
.into_iter()
.filter_map(|(i, j)| collide_triangles(t0[i], t1[j]))
.collect();
if old.len() != new.len() {
return Err(format!("seed {seed}: {} collisions before, {} now", old.len(), new.len()));
}
for (k, (a, b)) in old.iter().zip(&new).enumerate() {
if !(a.0 == b.0 && a.1 == b.1) {
return Err(format!("seed {seed}: collision {k} differs: {a:?} vs {b:?}"));
}
}
compared += old.len();
}
Ok(compared)
}
#[cfg(test)]
#[test]
fn hooks_pass() {
assert_eq!(nan_triangles_do_not_panic(), Ok(()));
let n = same_collisions_in_the_same_order(0..200);
assert!(matches!(n, Ok(n) if n > 0), "{n:?}");
}
}
-140
View File
@@ -1,140 +0,0 @@
use super::*;
use derive_more::{Deref, DerefMut};
#[derive(Clone, Copy, Debug)]
struct Ray {
origin: Point3,
direction: Vector3,
}
#[derive(Clone, Copy, Debug, Deref, DerefMut)]
struct Triangle([Point3; 3]);
impl Triangle {
#[inline(always)]
fn normal(self) -> Vector3 { (self[1] - self[0]).cross(self[2] - self[0]).normalize() }
fn is_crossing(self, ray: Ray) -> bool {
let a = self[0] - self[1];
let b = self[0] - self[2];
let mat = Matrix3::from_cols(a, b, ray.direction);
if mat.determinant().so_small() {
false
} else {
let inv = mat.invert().unwrap();
let uvt = inv * (self[0] - ray.origin);
uvt[0] > 0.0 && uvt[1] > 0.0 && uvt[0] + uvt[1] < 1.0 && uvt[2] > 0.0
}
}
}
/// whether a point is in a domain rounded by a closed polygon.
pub trait IncludingPointInDomain {
/// Count signed number of faces crossing ray with origin `point` and direction `ray_direction`.
/// Counter increase if the dot product of the ray and the normal of a face is positive,
/// and decrease if it is negative.
///
/// # Examples
/// ```
/// use truck_meshalgo::prelude::*;
/// let simplex = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// Point3::new(0.0, 0.0, 1.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(vec![
/// [0, 2, 1],
/// [0, 1, 3],
/// [0, 3, 2],
/// [1, 2, 3],
/// ]),
/// );
///
/// assert_eq!(simplex.signed_crossing_faces(Point3::new(0.1, 0.1, 0.1), Vector3::unit_x()), 1);
/// assert_eq!(simplex.signed_crossing_faces(Point3::new(-0.1, 0.1, 0.1), Vector3::unit_x()), 0);
/// ```
fn signed_crossing_faces(&self, point: Point3, ray_direction: Vector3) -> isize;
/// whether `point` is in a domain rounded by a closed polygon.
///
/// # Examples
/// ```
/// use truck_meshalgo::prelude::*;
/// let simplex = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// Point3::new(0.0, 0.0, 1.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(vec![
/// [0, 2, 1],
/// [0, 1, 3],
/// [0, 3, 2],
/// [1, 2, 3],
/// ]),
/// );
///
/// assert!(simplex.inside(Point3::new(0.1, 0.1, 0.1)));
/// assert!(!simplex.inside(Point3::new(-0.1, 0.1, 0.1)));
/// ```
fn inside(&self, point: Point3) -> bool;
}
impl IncludingPointInDomain for PolygonMesh {
fn signed_crossing_faces(&self, point: Point3, ray_direction: Vector3) -> isize {
let ray = Ray {
origin: point,
direction: ray_direction,
};
self.face_iter().fold(0, |mut counter, face| {
for i in 2..face.len() {
let tri = Triangle([
self.positions()[face[0].pos],
self.positions()[face[i - 1].pos],
self.positions()[face[i].pos],
]);
if tri.is_crossing(ray) {
counter += f64::signum(tri.normal().dot(ray.direction)) as isize;
}
}
counter
})
}
fn inside(&self, point: Point3) -> bool {
let dir = hash::take_one_unit(point);
self.signed_crossing_faces(point, dir) >= 1
}
}
#[test]
fn inside100() {
let positions = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(0.0, 0.0, 1.0),
];
let faces: Faces = vec![[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]]
.into_iter()
.collect();
let simplex = PolygonMesh::new(
StandardAttributes {
positions,
..Default::default()
},
faces,
);
for _ in 0..100 {
assert!(simplex.inside(Point3::new(0.1, 0.1, 0.1)));
assert!(!simplex.inside(Point3::new(-0.1, 0.1, 0.1)));
}
}
-20
View File
@@ -1,20 +0,0 @@
use crate::*;
mod collision;
mod in_out_judge;
mod point_cloud;
mod splitting;
mod topology;
mod volume;
pub use collision::Collision;
#[cfg(feature = "test-hooks")]
#[doc(hidden)]
pub use collision::test_hooks as collision_test_hooks;
pub use in_out_judge::IncludingPointInDomain;
pub use point_cloud::WithPointCloud;
pub use splitting::ExperimentalSplitters;
pub use splitting::Splitting;
pub use topology::Topology;
pub use truck_topology::shell::ShellCondition;
pub use volume::CalcVolume;
@@ -1,283 +0,0 @@
use super::*;
use array_macro::array;
#[derive(Clone, Debug)]
pub struct HashedPointCloud {
space: Vec<Vec<Point3>>,
size: [usize; 3],
range: [[f64; 2]; 3],
num_points: usize,
}
#[allow(dead_code)]
impl HashedPointCloud {
#[inline(always)]
pub fn new(size: [usize; 3], range: [[f64; 2]; 3]) -> Self {
HashedPointCloud {
space: vec![Vec::new(); size[0] * size[1] * size[2]],
size,
range,
num_points: 0,
}
}
#[inline(always)]
pub fn from_points<'a, I>(points: I, inf: f64) -> Self
where
I: IntoIterator + Clone,
I::IntoIter: Iterator<Item = &'a Point3>, {
let mut bdb = BoundingBox::<Point3>::new();
let len = points.clone().into_iter().copied().fold(0, |counter, pt| {
bdb.push(pt);
counter + 1
});
let size = 1.0 + f64::powf(len as f64, 1.0 / 3.0);
let size = bdb.diagonal().map(|a| {
if a / size < inf {
1 + (a / inf) as usize
} else {
size as usize
}
});
let mut res =
HashedPointCloud::new(size.into(), array![i => [bdb.min()[i], bdb.max()[i]]; 3]);
points.into_iter().for_each(|pt| res.push(*pt));
res
}
#[inline(always)]
pub fn size(&self) -> [usize; 3] { self.size }
#[inline(always)]
pub fn push(&mut self, point: Point3) {
self.num_points += 1;
let idx = point.hash(self);
self[idx].push(point);
}
#[inline(always)]
pub fn distance(&self, t: impl DistanceWithPointCloud) -> f64 { t.distance(self) }
#[inline(always)]
pub fn distance2(&self, t: impl DistanceWithPointCloud) -> f64 { t.distance2(self) }
#[inline(always)]
pub fn is_colliding(&self, t: impl DistanceWithPointCloud, tol: f64) -> bool {
t.is_colliding(self, tol)
}
}
impl std::ops::Index<[usize; 3]> for HashedPointCloud {
type Output = Vec<Point3>;
#[inline(always)]
fn index(&self, idcs: [usize; 3]) -> &Vec<Point3> {
&self.space[(idcs[0] * self.size[1] + idcs[1]) * self.size[2] + idcs[2]]
}
}
impl std::ops::IndexMut<[usize; 3]> for HashedPointCloud {
#[inline(always)]
fn index_mut(&mut self, idcs: [usize; 3]) -> &mut Vec<Point3> {
&mut self.space[(idcs[0] * self.size[1] + idcs[1]) * self.size[2] + idcs[2]]
}
}
pub trait SpaceHash {
fn hash(self, space: &HashedPointCloud) -> [usize; 3];
}
impl SpaceHash for Point3 {
#[inline(always)]
fn hash(self, space: &HashedPointCloud) -> [usize; 3] {
let x = (self[0] - space.range[0][0]) / (space.range[0][1] - space.range[0][0]);
let ix = f64::clamp(x * space.size[0] as f64, 0.0, space.size[0] as f64 - 1.0) as usize;
let y = (self[1] - space.range[1][0]) / (space.range[1][1] - space.range[1][0]);
let iy = f64::clamp(y * space.size[1] as f64, 0.0, space.size[1] as f64 - 1.0) as usize;
let z = (self[2] - space.range[2][0]) / (space.range[2][1] - space.range[2][0]);
let iz = f64::clamp(z * space.size[2] as f64, 0.0, space.size[2] as f64 - 1.0) as usize;
[ix, iy, iz]
}
}
impl SpaceHash for usize {
#[inline(always)]
fn hash(self, space: &HashedPointCloud) -> [usize; 3] {
[
self / (space.size[1] * space.size[2]),
self % (space.size[1] * space.size[2]) / space.size[2],
self % space.size[2],
]
}
}
pub trait DistanceWithPointCloud: Sized {
fn distance2(&self, space: &HashedPointCloud) -> f64;
fn distance(&self, space: &HashedPointCloud) -> f64 { f64::sqrt(self.distance2(space)) }
fn is_colliding(&self, space: &HashedPointCloud, tol: f64) -> bool {
nonpositive_tolerance!(tol, 0.0);
self.distance2(space) < tol * tol
}
}
impl DistanceWithPointCloud for Point3 {
fn distance2(&self, space: &HashedPointCloud) -> f64 {
let idcs = self.hash(space);
let closure = |dist2: f64, pt: &Point3| f64::min(dist2, MetricSpace::distance2(*self, *pt));
let mut dist2 = space[idcs].iter().fold(f64::INFINITY, closure);
if idcs[0] > 0 {
dist2 = space[[idcs[0] - 1, idcs[1], idcs[2]]]
.iter()
.fold(dist2, closure);
}
if idcs[0] + 1 < space.size[0] {
dist2 = space[[idcs[0] + 1, idcs[1], idcs[2]]]
.iter()
.fold(dist2, closure);
}
if idcs[1] > 0 {
dist2 = space[[idcs[0], idcs[1] - 1, idcs[2]]]
.iter()
.fold(dist2, closure);
}
if idcs[1] + 1 < space.size[1] {
dist2 = space[[idcs[0], idcs[1] + 1, idcs[2]]]
.iter()
.fold(dist2, closure);
}
if idcs[2] > 0 {
dist2 = space[[idcs[0], idcs[1], idcs[2] - 1]]
.iter()
.fold(dist2, closure);
}
if idcs[2] + 1 < space.size[2] {
dist2 = space[[idcs[0], idcs[1], idcs[2] + 1]]
.iter()
.fold(dist2, closure);
}
dist2
}
}
impl DistanceWithPointCloud for [Point3; 3] {
fn distance2(&self, space: &HashedPointCloud) -> f64 {
let bdd: BoundingBox<Point3> = self.iter().collect();
let mut range: [[usize; 3]; 2] = [bdd.min().hash(space), bdd.max().hash(space)];
range[0][0] = usize::max(range[0][0], 1) - 1;
range[0][1] = usize::max(range[0][1], 1) - 1;
range[0][2] = usize::max(range[0][2], 1) - 1;
range[1][0] = usize::min(range[1][0] + 1, space.size[0] - 1);
range[1][1] = usize::min(range[1][1] + 1, space.size[1] - 1);
range[1][2] = usize::min(range[1][2] + 1, space.size[2] - 1);
let mut dist2 = f64::INFINITY;
(range[0][0]..=range[1][0]).for_each(|ix| {
(range[0][1]..=range[1][1]).for_each(|iy| {
(range[0][2]..=range[1][2]).for_each(|iz| {
dist2 = space[[ix, iy, iz]].iter().fold(dist2, |dist2, pt| {
f64::min(dist2, distance2_point_triangle(*pt, *self))
});
})
})
});
dist2
}
}
impl<'a> DistanceWithPointCloud for &'a PolygonMesh {
fn distance2(&self, space: &HashedPointCloud) -> f64 {
let dist2 = self.faces().triangle_iter().fold(-1.0, |dist2, tri| {
let tri = array![i => self.positions()[tri[i].pos]; 3];
f64::max(dist2, tri.distance2(space))
});
match dist2 < 0.0 {
true => f64::INFINITY,
false => dist2,
}
}
fn is_colliding(&self, space: &HashedPointCloud, tol: f64) -> bool {
nonpositive_tolerance!(tol, 0.0);
self.faces().triangle_iter().any(|tri| {
let tri = array![i => self.positions()[tri[i].pos]; 3];
tri.distance2(space) < tol * tol
})
}
}
#[test]
fn distance_point_triangle_test() {
let triangle = [
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
];
assert_eq!(
12.45 * 12.45,
distance2_point_triangle(Point3::new(0.25, 0.25, 12.45), triangle)
);
assert_eq!(
12.45 * 12.45,
distance2_point_triangle(Point3::new(0.25, 0.25, -12.45), triangle)
);
assert_eq!(
2.0,
distance2_point_triangle(Point3::new(-1.0, 0.5, -1.0), triangle)
);
assert_eq!(
2.0,
distance2_point_triangle(Point3::new(0.5, -1.0, 1.0), triangle)
);
assert_eq!(
1.5,
distance2_point_triangle(Point3::new(1.0, 1.0, 1.0), triangle)
);
assert_eq!(
3.0,
distance2_point_triangle(Point3::new(-1.0, -1.0, -1.0), triangle)
);
}
#[cfg(test)]
fn exec_space_division_distance() {
const SPACE_SIZE: f64 = 100.0;
const NUM_POINTS: usize = 1000;
const NUM_TRIANGLES: usize = 10;
const TRIANGLE_DISPLACEMENT: f64 = 1.0;
let points = (0..NUM_POINTS)
.map(|_| {
Point3::new(
SPACE_SIZE * rand::random::<f64>() - SPACE_SIZE / 2.0,
SPACE_SIZE * rand::random::<f64>() - SPACE_SIZE / 2.0,
SPACE_SIZE * rand::random::<f64>() - SPACE_SIZE / 2.0,
)
})
.collect::<Vec<_>>();
let triangles = (0..NUM_TRIANGLES)
.map(|_| {
let pt = Point3::new(
SPACE_SIZE * rand::random::<f64>() - SPACE_SIZE / 2.0,
SPACE_SIZE * rand::random::<f64>() - SPACE_SIZE / 2.0,
SPACE_SIZE * rand::random::<f64>() - SPACE_SIZE / 2.0,
);
[
pt,
pt + Vector3::new(
TRIANGLE_DISPLACEMENT * 2.0 * rand::random::<f64>() - TRIANGLE_DISPLACEMENT,
TRIANGLE_DISPLACEMENT * 2.0 * rand::random::<f64>() - TRIANGLE_DISPLACEMENT,
TRIANGLE_DISPLACEMENT * 2.0 * rand::random::<f64>() - TRIANGLE_DISPLACEMENT,
),
pt + Vector3::new(
TRIANGLE_DISPLACEMENT * 2.0 * rand::random::<f64>() - TRIANGLE_DISPLACEMENT,
TRIANGLE_DISPLACEMENT * 2.0 * rand::random::<f64>() - TRIANGLE_DISPLACEMENT,
TRIANGLE_DISPLACEMENT * 2.0 * rand::random::<f64>() - TRIANGLE_DISPLACEMENT,
),
]
})
.collect::<Vec<_>>();
let hashed = HashedPointCloud::from_points(&points, 1.0);
let dist_0 = triangles.iter().fold(std::f64::INFINITY, |dist, triangle| {
f64::min(dist, hashed.distance2(*triangle))
});
let dist_1 = points.iter().fold(std::f64::INFINITY, |dist2, pt| {
triangles.iter().fold(dist2, |dist2, triangle| {
f64::min(dist2, distance2_point_triangle(*pt, *triangle))
})
});
assert_eq!(dist_0, dist_1);
}
#[test]
fn space_division_distance() { (0..10).for_each(|_| exec_space_division_distance()) }
-84
View File
@@ -1,84 +0,0 @@
use super::*;
mod hashed_point_cloud;
use hashed_point_cloud::HashedPointCloud;
mod sort_end_points;
/// Investigates positional relation between polygon mesh and point cloud.
pub trait WithPointCloud {
/// Whether all faces of the polygon mesh `self` has intersection with the neighborhood of `point_cloud`.
/// # Arguments
/// - `tol`: the radius of the neighborhoods of points in point cloud.
/// # Panics
/// `tol` must be more than `TOLERANCE`.
/// # Examples
/// ```
/// use truck_meshalgo::prelude::*;
/// let mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// Point3::new(0.0, 0.0, 2.0),
/// Point3::new(1.0, 0.0, 2.0),
/// Point3::new(0.0, 1.0, 2.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(vec![[0, 1, 2], [3, 4, 5]]),
/// );
///
/// let mut point_cloud = vec![Point3::new(0.25, 0.25, 0.0)];
/// assert!(!mesh.is_clung_to_by(&point_cloud, 0.001));
/// point_cloud.push(Point3::new(0.25, 0.25, 2.0));
/// assert!(mesh.is_clung_to_by(&point_cloud, 0.001));
/// ```
fn is_clung_to_by(&self, point_cloud: &[Point3], tol: f64) -> bool;
/// Whether the neighborhood of the polygon mesh `self` includes `point_cloud`.
/// # Panics
/// `tol` must be more than `TOLERANCE`.
fn neighborhood_include(&self, point_cloud: &[Point3], tol: f64) -> bool;
/// Whether the polygon mesh `self` and `point_cloud` collides.
/// # Panics
/// `tol` must be more than `TOLERANCE`.
fn collide_with_neiborhood_of(&self, point_cloud: &[Point3], tol: f64) -> bool;
}
impl WithPointCloud for PolygonMesh {
#[inline(always)]
fn is_clung_to_by(&self, point_cloud: &[Point3], tol: f64) -> bool {
nonpositive_tolerance!(tol);
HashedPointCloud::from_points(point_cloud, tol * 2.0).distance2(self) < tol * tol
}
#[inline(always)]
fn collide_with_neiborhood_of(&self, point_cloud: &[Point3], tol: f64) -> bool {
HashedPointCloud::from_points(point_cloud, tol * 2.0).is_colliding(self, tol)
}
#[inline(always)]
fn neighborhood_include(&self, point_cloud: &[Point3], tol: f64) -> bool {
sort_end_points::pointcloud_in_polygon_neighborhood(self, point_cloud, tol)
}
}
// https://iquilezles.org/www/articles/distfunctions/distfunctions.htm
fn distance2_point_triangle(point: Point3, triangle: [Point3; 3]) -> f64 {
let ab = triangle[1] - triangle[0];
let ap = point - triangle[0];
let bc = triangle[2] - triangle[1];
let bp = point - triangle[1];
let ca = triangle[0] - triangle[2];
let cp = point - triangle[2];
let nor = ab.cross(ca);
let coef = f64::signum(ab.cross(nor).dot(ap))
+ f64::signum(bc.cross(nor).dot(bp))
+ f64::signum(ca.cross(nor).dot(cp));
if coef < 2.0 || nor.magnitude().so_small() {
let a = (ap - ab * f64::clamp(ab.dot(ap) / ab.dot(ab), 0.0, 1.0)).magnitude2();
let b = (bp - bc * f64::clamp(bc.dot(bp) / bc.dot(bc), 0.0, 1.0)).magnitude2();
let c = (cp - ca * f64::clamp(ca.dot(cp) / ca.dot(ca), 0.0, 1.0)).magnitude2();
f64::min(f64::min(a, b), c)
} else {
nor.dot(ap) * nor.dot(ap) / nor.magnitude2()
}
}
@@ -1,121 +0,0 @@
use super::*;
use array_macro::array;
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum EndPointType {
Front,
Middle,
Back,
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
struct EndPoint {
entity: f64,
r#type: EndPointType,
index: usize,
}
impl EndPoint {
#[inline(always)]
fn new(entity: f64, r#type: EndPointType, index: usize) -> EndPoint {
EndPoint {
entity,
r#type,
index,
}
}
#[inline(always)]
fn from_seg(seg: (f64, f64), index: usize) -> Vec<EndPoint> {
vec![
EndPoint::new(seg.0, EndPointType::Front, index),
EndPoint::new(seg.1, EndPointType::Back, index),
]
}
}
fn tri_to_seg(tri: [Point3; 3], unit: Vector3, tol: f64) -> (f64, f64) {
let a = tri[0].to_vec().dot(unit);
let b = tri[1].to_vec().dot(unit);
let c = tri[2].to_vec().dot(unit);
(
f64::min(f64::min(a, b), c) - tol,
f64::max(f64::max(a, b), c) + tol,
)
}
fn sorted_endpoints<'a, I, J>(iter0: I, iter1: J, unit: Vector3, tol: f64) -> Vec<EndPoint>
where
I: IntoIterator<Item = [Point3; 3]>,
J: IntoIterator<Item = &'a Point3>, {
let mut res: Vec<EndPoint> = iter0
.into_iter()
.enumerate()
.filter(|(_, tri)| !(tri[1] - tri[0]).cross(tri[2] - tri[0]).so_small())
.flat_map(|(i, tri)| EndPoint::from_seg(tri_to_seg(tri, unit, tol), i))
.chain(
iter1
.into_iter()
.enumerate()
.map(|(i, point)| EndPoint::new(point.to_vec().dot(unit), EndPointType::Middle, i)),
)
.collect();
res.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Greater));
res
}
fn sorted_endpoints_by_polymesh_points(
polygon: &PolygonMesh,
points: &[Point3],
tol: f64,
) -> Vec<EndPoint> {
let unit = if !polygon.positions().is_empty() {
hash::take_one_unit(polygon.positions()[0])
} else if !points.is_empty() {
hash::take_one_unit(points[0])
} else {
return Vec::new();
};
sorted_endpoints(
polygon
.faces()
.triangle_iter()
.map(|tri| array![i => polygon.positions()[tri[i].pos]; 3]),
points.iter(),
unit,
tol,
)
}
pub fn pointcloud_in_polygon_neighborhood(
polygon: &PolygonMesh,
points: &[Point3],
tol: f64,
) -> bool {
nonpositive_tolerance!(tol, 0.0);
let mut current = Vec::new();
let triangles = polygon.faces().triangle_iter().collect::<Vec<_>>();
sorted_endpoints_by_polymesh_points(polygon, points, tol)
.into_iter()
.all(move |EndPoint { r#type, index, .. }| match r#type {
EndPointType::Front => {
current.push(index);
true
}
EndPointType::Back => {
let i = current
.iter()
.enumerate()
.find(|(_, idx)| **idx == index)
.unwrap()
.0;
current.swap_remove(i);
true
}
EndPointType::Middle => current.iter().any(|i| {
let tri = triangles[*i];
let tri = array![i => polygon.positions()[tri[i].pos]; 3];
distance2_point_triangle(points[index], tri) < tol * tol
}),
})
}
-343
View File
@@ -1,343 +0,0 @@
use super::*;
use std::f64::consts::PI;
/// Splitting the faces into several clusters.
pub trait Splitting {
/// Creates a sub mesh by the face indices.
/// # Examples
/// ```
/// use truck_polymesh::*;
/// use truck_meshalgo::analyzers::*;
///
/// // cube
/// let mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(1.0, 1.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// Point3::new(0.0, 0.0, 1.0),
/// Point3::new(1.0, 0.0, 1.0),
/// Point3::new(1.0, 1.0, 1.0),
/// Point3::new(0.0, 1.0, 1.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(&[
/// &[3, 2, 1, 0], &[0, 1, 5, 4], &[1, 2, 6, 5],
/// &[2, 3, 7, 6], &[3, 0, 4, 7], &[4, 5, 6, 7],
/// ]),
/// );
///
/// let submesh = mesh.create_mesh_by_face_indices(&[0, 3, 5]);
///
/// // the same attributes vector
/// assert_eq!(mesh.positions(), submesh.positions());
///
/// let faces0 = Faces::from_iter(&[
/// &[3, 2, 1, 0], &[2, 3, 7, 6], &[4, 5, 6, 7],
/// ]);
/// assert_eq!(submesh.faces().len(), faces0.len());
/// assert_eq!(submesh.faces()[0], faces0[0]);
/// assert_eq!(submesh.faces()[1], faces0[1]);
/// assert_eq!(submesh.faces()[2], faces0[2]);
/// ```
fn create_mesh_by_face_indices(&self, indices: &[usize]) -> PolygonMesh;
/// Extracts polygons such that there exists its normal is the same as its face normal.
/// # Arguments
/// - `tol`: tolerance to be regarded as the same normal as the face normal
/// # Returns
/// - The first polygon consists the faces included in planes.
/// - The second polygon is the extracted remainder.
/// # Panics
/// `tol` must be more than `TOLERANCE`.
/// # Examples
/// ```
/// use truck_polymesh::*;
/// use truck_meshalgo::analyzers::*;
/// let mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.5, 0.0),
/// Point3::new(0.0, 0.5, 1.0),
/// Point3::new(1.0, 0.5, 1.0),
/// Point3::new(1.0, 0.5, 0.0),
/// Point3::new(0.0, 0.0, 2.0),
/// Point3::new(1.0, 0.0, 2.0),
/// ],
/// normals: vec![
/// Vector3::new(0.0, 1.0, 0.0),
/// // displaced normals for smooth rendering
/// Vector3::new(0.0, 1.0, 1.0).normalize(),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(&[
/// &[(0, None, Some(0)), (1, None, Some(0)), (2, None, Some(0)), (3, None, Some(0))],
/// &[(2, None, Some(0)), (1, None, Some(0)), (4, None, Some(1)), (5, None, Some(1))],
/// ]),
/// );
/// let (plane, remained) = mesh.extract_planes(TOLERANCE); // TOLERANCE == 1.0e-7
/// assert_eq!(plane.len(), 1); assert_eq!(plane[0], 0);
/// assert_eq!(remained.len(), 1); assert_eq!(remained[0], 1);
/// ```
fn extract_planes(&self, tol: f64) -> (Vec<usize>, Vec<usize>);
/// Splits into the components.
/// # Details
/// Two polygons are considered to be in the same component if they share an edge
/// whose vertices has the same positions (and normals if `use_normal == true`).
/// # Examples
/// ```
/// use truck_polymesh::*;
/// use truck_meshalgo::{analyzers::*, filters::*};
///
/// // cube consisting tri_faces
/// let mut mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(1.0, 1.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// Point3::new(0.0, 0.0, 1.0),
/// Point3::new(1.0, 0.0, 1.0),
/// Point3::new(1.0, 1.0, 1.0),
/// Point3::new(0.0, 1.0, 1.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(&[
/// &[3, 2, 0], &[1, 0, 2], &[0, 1, 4], &[5, 4, 1],
/// &[1, 2, 5], &[6, 5, 2], &[2, 3, 6], &[7, 6, 3],
/// &[3, 0, 7], &[4, 7, 0], &[4, 5, 7], &[6, 7, 5],
/// ]),
/// );
///
/// // sign up normals
/// mesh.add_naive_normals(true).put_together_same_attrs(TOLERANCE);
///
/// // into components with normals
/// let components = mesh.components(true);
/// // The number of components is six because the mesh is a cube.
/// assert_eq!(components.len(), 6);
///
/// // into components without normals
/// let components = mesh.components(false);
/// // The number of components is one because the mesh is a connected cube.
/// assert_eq!(components.len(), 1);
/// ```
fn components(&self, use_normal: bool) -> Vec<Vec<usize>>;
}
impl Splitting for PolygonMesh {
fn create_mesh_by_face_indices(&self, indices: &[usize]) -> PolygonMesh {
let positions = self.positions().clone();
let uv_coords = self.uv_coords().clone();
let normals = self.normals().clone();
let faces: Faces = indices.iter().map(|i| &self.faces()[*i]).collect();
PolygonMesh::new(
StandardAttributes {
positions,
uv_coords,
normals,
},
faces,
)
}
fn extract_planes(&self, tol: f64) -> (Vec<usize>, Vec<usize>) {
nonpositive_tolerance!(tol, 0.0);
self.faces_into_two_clusters(|face: &[Vertex]| {
is_in_the_plane(self.positions(), self.normals(), face, tol * tol)
})
}
fn components(&self, use_normal: bool) -> Vec<Vec<usize>> {
let face_adjacency = self.faces().face_adjacency(use_normal);
get_components(&face_adjacency)
}
}
#[doc(hidden)]
pub trait ExperimentalSplitters {
fn faces_into_two_clusters<F: Fn(&[Vertex]) -> bool>(
&self,
func: F,
) -> (Vec<usize>, Vec<usize>);
fn clustering_faces_by_gcurvature(
&self,
threshold: f64,
preferred_upper: bool,
) -> (Vec<usize>, Vec<usize>);
fn get_gcurve(&self) -> Vec<f64>;
}
/// splitting the global faces
impl ExperimentalSplitters for PolygonMesh {
/// separate all faces into two clusters, one with `func` returning true and the other with false.
/// # Returns
/// (the vector of all faces `f` which `func(f) == true`, the one of the other faces)
fn faces_into_two_clusters<F: Fn(&[Vertex]) -> bool>(
&self,
func: F,
) -> (Vec<usize>, Vec<usize>) {
let mut true_faces = Vec::new();
let mut false_faces = Vec::new();
for (i, face) in self.face_iter().enumerate() {
match func(face) {
true => true_faces.push(i),
false => false_faces.push(i),
}
}
(true_faces, false_faces)
}
fn clustering_faces_by_gcurvature(
&self,
threshold: f64,
preferred_upper: bool,
) -> (Vec<usize>, Vec<usize>) {
let gcurve = self.get_gcurve();
self.faces_into_two_clusters(|face: &[Vertex]| {
is_signed_up_upper(face, &gcurve, preferred_upper, threshold)
})
}
fn get_gcurve(&self) -> Vec<f64> {
let positions = self.positions();
let mut angles = vec![0.0; positions.len()];
let mut weights = vec![0.0; positions.len()];
for face in self.tri_faces() {
angles[face[0].pos] += get_angle(positions, face, 0, 1, 2);
angles[face[1].pos] += get_angle(positions, face, 1, 2, 0);
angles[face[2].pos] += get_angle(positions, face, 2, 0, 1);
add_weights(&mut weights, positions, face);
}
for face in self.quad_faces() {
angles[face[0].pos] += get_angle(positions, face, 0, 1, 3);
angles[face[1].pos] += get_angle(positions, face, 1, 2, 0);
angles[face[2].pos] += get_angle(positions, face, 2, 3, 1);
angles[face[3].pos] += get_angle(positions, face, 3, 0, 1);
add_weights(&mut weights, positions, face);
}
for face in self.other_faces() {
let n = face.len() - 1;
angles[face[0].pos] += get_angle(positions, face, 0, 1, n);
for i in 1..n {
angles[face[i].pos] += get_angle(positions, face, i, i + 1, i - 1);
}
angles[face[n].pos] += get_angle(positions, face, n, 0, n - 1);
add_weights(&mut weights, positions, face);
}
angles
.into_iter()
.zip(weights)
.map(|(ang, weight)| (PI * 2.0 - ang) / weight)
.collect()
}
}
/// divide the graph to the connected components.
/// # Arguments
/// * adjacency - the adjacency matrix
/// # Return
/// * the list of the indices of faces contained in each components
fn get_components(adjacency: &[Vec<usize>]) -> Vec<Vec<usize>> {
let mut unchecked = vec![true; adjacency.len()];
let mut components = Vec::new();
loop {
let first = match unchecked.iter().position(|x| *x) {
Some(idx) => idx,
None => return components,
};
let mut stack = vec![first];
let mut component = vec![first];
unchecked[first] = false;
while let Some(cursor) = stack.pop() {
for i in &adjacency[cursor] {
if unchecked[*i] {
unchecked[*i] = false;
component.push(*i);
stack.push(*i);
}
}
}
components.push(component);
}
}
fn is_in_the_plane(positions: &[Point3], normals: &[Vector3], face: &[Vertex], tol2: f64) -> bool {
let n = FaceNormal::new(positions, face, 0).normal;
for v in face {
if let Some(nor) = v.nor {
if n.distance2(normals[nor]) < tol2 {
return true;
}
}
}
false
}
fn is_signed_up_upper(
face: &[Vertex],
gcurve: &[f64],
preferred_upper: bool,
threshold: f64,
) -> bool {
if preferred_upper {
face.as_ref().iter().any(|v| gcurve[v.pos] > threshold)
} else {
face.as_ref().iter().all(|v| gcurve[v.pos] > threshold)
}
}
fn get_angle(positions: &[Point3], face: &[Vertex], idx0: usize, idx1: usize, idx2: usize) -> f64 {
let vec0 = positions[face[idx1].pos] - positions[face[idx0].pos];
let vec1 = positions[face[idx2].pos] - positions[face[idx0].pos];
vec0.angle(vec1).0
}
fn add_weights(weights: &mut [f64], positions: &[Point3], face: &[Vertex]) {
let area = (2..face.len()).fold(0.0, |sum, i| {
let vec0 = positions[face[i - 1].pos] - positions[face[0].pos];
let vec1 = positions[face[i].pos] - positions[face[0].pos];
sum + (vec0.cross(vec1)).magnitude() / 2.0
}) / (face.len() as f64);
for v in face {
weights[v.pos] += area;
}
}
#[test]
fn into_components_test() {
let faces = vec![
[(0, None, Some(0)), (1, None, Some(1)), (2, None, Some(2))].as_ref(),
&[(0, None, Some(0)), (5, None, Some(5)), (1, None, Some(1))],
&[(0, None, Some(7)), (5, None, Some(5)), (6, None, Some(6))],
&[(5, None, Some(5)), (6, None, Some(6)), (7, None, Some(7))],
&[
(1, None, Some(1)),
(4, None, Some(4)),
(3, None, Some(3)),
(2, None, Some(2)),
],
]
.into_iter()
.collect();
let positions = vec![Point3::origin(); 8];
let normals = vec![Vector3::unit_x(); 8];
let mesh = PolygonMesh::new(
StandardAttributes {
positions,
normals,
..Default::default()
},
faces,
);
let comp = mesh.components(true);
assert_eq!(comp.len(), 2);
assert_eq!(comp[0], vec![0, 1, 4]);
assert_eq!(comp[1], vec![2, 3]);
}
-117
View File
@@ -1,117 +0,0 @@
use super::*;
use itertools::Itertools;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use truck_topology::shell::ShellCondition;
/// Extracts boundaries, and check shell condition: closed or orientation.
pub trait Topology {
/// Returns a vector of all boundaries as line strip.
fn extract_boundaries(&self) -> Vec<Vec<usize>>;
/// Determines the shell conditions: non-regular, regular, oriented, or closed.
/// The complexity increases in proportion to the number of edges.
///
/// Examples for each condition can be found on the page of
/// [`ShellCondition`](https://docs.rs/truck-topology/0.2.0/truck_topology/shell/enum.ShellCondition.html).
fn shell_condition(&self) -> ShellCondition;
}
#[derive(Clone, Debug)]
struct Boundaries {
checked: HashSet<[usize; 2]>,
boundary: HashMap<[usize; 2], bool>,
condition: ShellCondition,
}
impl Boundaries {
#[inline(always)]
fn new() -> Self {
Boundaries {
checked: Default::default(),
boundary: Default::default(),
condition: ShellCondition::Oriented,
}
}
#[inline(always)]
fn insert(&mut self, edge: [Vertex; 2]) {
let ori = edge[0].pos < edge[1].pos;
let edge = match ori {
true => [edge[0].pos, edge[1].pos],
false => [edge[1].pos, edge[0].pos],
};
self.condition = self.condition
& match (self.checked.insert(edge), self.boundary.insert(edge, ori)) {
(true, None) => ShellCondition::Oriented,
(false, None) => ShellCondition::Irregular,
(true, Some(_)) => panic!("unexpected case!"),
(false, Some(ori0)) => {
self.boundary.remove(&edge);
match ori == ori0 {
true => ShellCondition::Regular,
false => ShellCondition::Oriented,
}
}
};
}
#[inline(always)]
fn condition(&self) -> ShellCondition {
if self.condition == ShellCondition::Oriented && self.boundary.is_empty() {
ShellCondition::Closed
} else {
self.condition
}
}
}
impl FromIterator<[Vertex; 2]> for Boundaries {
fn from_iter<I: IntoIterator<Item = [Vertex; 2]>>(iter: I) -> Boundaries {
let mut boundaries = Boundaries::new();
iter.into_iter().for_each(|edge| boundaries.insert(edge));
boundaries
}
}
fn face_edge_iter<T: Copy>(face: &[T]) -> impl Iterator<Item = [T; 2]> + '_ {
face.iter().circular_tuple_windows().map(|(v, w)| [*v, *w])
}
impl Topology for Faces {
fn extract_boundaries(&self) -> Vec<Vec<usize>> {
let boundaries: Boundaries = self.face_iter().flat_map(face_edge_iter).collect();
let mut vemap: HashMap<usize, usize> = boundaries
.boundary
.into_iter()
.map(|(edge, ori)| match ori {
true => (edge[0], edge[1]),
false => (edge[1], edge[0]),
})
.collect();
let mut res = Vec::new();
while !vemap.is_empty() {
let mut wire = Vec::new();
let front = vemap.iter().next().unwrap();
let front = (*front.0, *front.1);
vemap.remove(&front.0);
wire.push(front.0);
let mut cursor = front.1;
while cursor != front.0 {
wire.push(cursor);
cursor = vemap.remove(&cursor).unwrap_or(front.0);
}
res.push(wire);
}
res
}
fn shell_condition(&self) -> ShellCondition {
self.face_iter()
.flat_map(face_edge_iter)
.collect::<Boundaries>()
.condition()
}
}
impl Topology for PolygonMesh {
fn extract_boundaries(&self) -> Vec<Vec<usize>> { self.faces().extract_boundaries() }
fn shell_condition(&self) -> ShellCondition { self.faces().shell_condition() }
}
-175
View File
@@ -1,175 +0,0 @@
use super::*;
use array_macro::array;
/// Calculate the volume and the center of gravity of mesh.
pub trait CalcVolume {
/// Returns the volume of the mesh if the mesh is closed.
///
/// # Details
/// More strictly, it returns the integral value of `xdydz`. Because of the linearity of this operation,
/// if multiple meshes represent a single closed geometry, the overall volume can be calculated by computing
/// this value for each and adding them together.
///
/// # Examples
/// ```
/// // The regular trihedron in the unit sphere.
/// use truck_meshalgo::prelude::*;
/// let a = 1.0 / f64::sqrt(3.0);
/// let positions = vec![
/// Point3::new(-a, -a, -a),
/// Point3::new(a, a, -a),
/// Point3::new(a, -a, a),
/// Point3::new(-a, a, a),
/// ];
/// let attrs = StandardAttributes {
/// positions,
/// ..Default::default()
/// };
/// let faces = Faces::from_iter(&[
/// [0, 1, 2],
/// [1, 3, 2],
/// [1, 0, 3],
/// [0, 2, 3],
/// ]);
/// let polygon = PolygonMesh::new(attrs, faces);
/// assert_near!(polygon.volume(), 8.0 * f64::sqrt(3.0) / 27.0);
/// ```
/// ```
/// // Cube with a hollow inside, defined by two boundaries.
/// use truck_meshalgo::prelude::*;
/// let positions0 = vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(3.0, 0.0, 0.0),
/// Point3::new(0.0, 3.0, 0.0),
/// Point3::new(0.0, 0.0, 3.0),
/// Point3::new(0.0, 3.0, 3.0),
/// Point3::new(3.0, 0.0, 3.0),
/// Point3::new(3.0, 3.0, 0.0),
/// Point3::new(3.0, 3.0, 3.0),
/// ];
/// let attrs0 = StandardAttributes {
/// positions: positions0,
/// ..Default::default()
/// };
/// let faces0 = Faces::from_iter(&[
/// [0, 2, 6, 1],
/// [0, 1, 5, 3],
/// [1, 6, 7, 5],
/// [6, 2, 4, 7],
/// [2, 0, 3, 4],
/// [3, 5, 7, 4],
/// ]);
/// let polygon0 = PolygonMesh::new(attrs0, faces0);
/// let positions1 = vec![
/// Point3::new(1.0, 1.0, 1.0),
/// Point3::new(2.0, 1.0, 1.0),
/// Point3::new(1.0, 2.0, 1.0),
/// Point3::new(1.0, 1.0, 2.0),
/// Point3::new(1.0, 2.0, 2.0),
/// Point3::new(2.0, 1.0, 2.0),
/// Point3::new(2.0, 2.0, 1.0),
/// Point3::new(2.0, 2.0, 2.0),
/// ];
/// let attrs1 = StandardAttributes {
/// positions: positions1,
/// ..Default::default()
/// };
/// let faces1 = Faces::from_iter(&[
/// [1, 6, 2, 0],
/// [3, 5, 1, 0],
/// [5, 7, 6, 1],
/// [7, 4, 2, 6],
/// [4, 3, 0, 2],
/// [4, 7, 5, 3],
/// ]);
/// let polygon1 = PolygonMesh::new(attrs1, faces1);
/// // We consider the solid defined by two boundaries(polygon0 and polygon1).
/// // The volume will be calculated by `position0.volume() + position1.volume()`.
/// assert_near!(polygon0.volume() + polygon1.volume(), 26.0);
/// ```
fn volume(&self) -> f64;
/// Returns the center of gravity of the mesh by homogeneous coordinate if the mesh is closed.
///
/// # Details
/// More strictly, it returns the integral value of `(1/2 x^2 dydz, 1/2 y^2dzdx, 1/2 z^2 dxdy, xdydz)`.
/// Because of the linearity of this operation, if multiple meshes represent a single closed geometry,
/// the overall volume can be calculated by computing this value for each and adding them together.
///
/// # Examples
/// ```
/// // The triangular prism
/// use truck_meshalgo::prelude::*;
/// let positions = vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(2.0, 5.0, 0.0),
/// Point3::new(-5.0, 1.0, 0.0),
/// Point3::new(0.0, 0.0, 2.0),
/// Point3::new(2.0, 5.0, 2.0),
/// Point3::new(-5.0, 1.0, 2.0),
/// ];
/// let attrs = StandardAttributes {
/// positions,
/// ..Default::default()
/// };
/// let faces = Faces::from_iter(&[
/// [2, 1, 0].as_slice(),
/// &[0, 1, 4, 3],
/// &[1, 2, 5, 4],
/// &[2, 0, 3, 5],
/// &[3, 4, 5],
/// ]);
/// let polygon = PolygonMesh::new(attrs, faces);
/// let homog = polygon.center_of_gravity();
/// assert_near!(homog.to_point(), Point3::new(-1.0, 2.0, 1.0));
/// ```
fn center_of_gravity(&self) -> Vector4;
}
impl CalcVolume for PolygonMesh {
fn volume(&self) -> f64 {
point_triangles(self).fold(0.0, |sum, [p, q, r]| {
sum + (p.x + q.x + r.x) * ((q.y - p.y) * (r.z - p.z) - (r.y - p.y) * (q.z - p.z))
}) / 6.0
}
fn center_of_gravity(&self) -> Vector4 {
let arr = point_triangles(self).fold([0.0; 4], |sum, [p, q, r]| {
let det = array![i => {
let (j, k) = ((i + 1) % 3, (i + 2) % 3);
(q[j] - p[j]) * (r[k] - p[k]) - (r[j] - p[j]) * (q[k] - p[k])
}; 3];
let vals = array![i => {
let s = p[i] + q[i] + r[i];
s * s - p[i] * q[i] - q[i] * r[i] - r[i] * p[i]
}; 3];
let res = array![i => sum[i] + vals[i] * det[i]; 3];
let res3 = sum[3] + (p.x + q.x + r.x) * det[0];
[res[0], res[1], res[2], res3]
});
Vector4::new(arr[0] / 24.0, arr[1] / 24.0, arr[2] / 24.0, arr[3] / 6.0)
}
}
fn point_triangles(poly: &PolygonMesh) -> impl Iterator<Item = [Point3; 3]> + '_ {
poly.faces()
.triangle_iter()
.map(|faces| array![i => poly.positions()[faces[i].pos]; 3])
}
impl CalcVolume for truck_topology::Solid<Point3, PolylineCurve<Point3>, PolygonMesh> {
fn volume(&self) -> f64 {
self.face_iter()
.map(|face| match face.orientation() {
true => face.surface().volume(),
false => -face.surface().volume(),
})
.sum::<f64>()
}
fn center_of_gravity(&self) -> Vector4 {
self.face_iter()
.map(|face| match face.orientation() {
true => face.surface().center_of_gravity(),
false => -face.surface().center_of_gravity(),
})
.sum::<Vector4>()
}
}
-77
View File
@@ -1,77 +0,0 @@
use super::*;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
pub trait Adjacency {
#[allow(dead_code)]
fn vertex_adjacency(&self, num_of_vertices: usize) -> Vec<Vec<usize>>;
/// create the adjacency list of the faces
fn face_adjacency(&self, use_normal: bool) -> Vec<Vec<usize>>;
}
impl Adjacency for Faces {
fn vertex_adjacency(&self, num_of_vertices: usize) -> Vec<Vec<usize>> {
let mut already = HashSet::default();
let mut res = vec![Vec::new(); num_of_vertices];
for face in self.face_iter() {
face.windows(2)
.chain(std::iter::once([face[face.len() - 1], face[0]].as_ref()))
.map(|edge| [edge[0].pos, edge[1].pos])
.for_each(|edge| {
let first = match edge[0] < edge[1] {
true => already.insert((edge[0], edge[1])),
false => already.insert((edge[1], edge[0])),
};
if first {
res[edge[0]].push(edge[1]);
res[edge[1]].push(edge[0]);
}
})
}
res
}
fn face_adjacency(&self, use_normal: bool) -> Vec<Vec<usize>> {
let len = self.len();
let mut face_adjacency = vec![Vec::<usize>::new(); len];
let mut edge_face_map = HashMap::default();
for (i, face) in self.face_iter().enumerate() {
face.windows(2)
.chain(std::iter::once([face[face.len() - 1], face[0]].as_ref()))
.for_each(|v| {
signup_adjacency(
i,
v[0],
v[1],
&mut face_adjacency,
&mut edge_face_map,
use_normal,
)
})
}
face_adjacency
}
}
fn signup_adjacency(
i: usize,
v0: Vertex,
v1: Vertex,
face_adjacency: &mut [Vec<usize>],
edge_face_map: &mut HashMap<[(usize, Option<usize>); 2], usize>,
use_normal: bool,
) {
let edge = match (v0.pos < v1.pos, use_normal) {
(true, true) => [(v0.pos, v0.nor), (v1.pos, v1.nor)],
(false, true) => [(v1.pos, v1.nor), (v0.pos, v0.nor)],
(true, false) => [(v0.pos, None), (v1.pos, None)],
(false, false) => [(v1.pos, None), (v0.pos, None)],
};
match edge_face_map.get(&edge) {
Some(j) => {
face_adjacency[i].push(*j);
face_adjacency[*j].push(i);
}
None => {
edge_face_map.insert(edge, i);
}
}
}
-26
View File
@@ -1,26 +0,0 @@
use super::*;
#[derive(Clone, Copy, Debug)]
pub struct FaceNormal {
pub face_id: usize,
pub normal: Vector3,
}
impl FaceNormal {
pub fn new(positions: &[Point3], face: &[Vertex], face_id: usize) -> FaceNormal {
let center = face
.iter()
.fold(Vector3::zero(), |sum, v| sum + positions[v.pos].to_vec())
/ face.len() as f64;
let normal = face
.windows(2)
.chain(std::iter::once([face[face.len() - 1], face[0]].as_ref()))
.fold(Vector3::zero(), |sum, v| {
let vec0 = positions[v[0].pos].to_vec() - center;
let vec1 = positions[v[1].pos].to_vec() - center;
sum + vec0.cross(vec1)
})
.normalize();
FaceNormal { face_id, normal }
}
}
-6
View File
@@ -1,6 +0,0 @@
use crate::*;
mod adjacency;
mod face_normal;
pub(super) use adjacency::Adjacency;
pub(super) use face_normal::FaceNormal;
-105
View File
@@ -1,105 +0,0 @@
use super::*;
/// STL face generate from `PolygonMesh`
#[derive(Debug)]
pub struct TriangleIter<'a> {
tri_faces: std::slice::Iter<'a, [Vertex; 3]>,
quad_faces: std::slice::Iter<'a, [Vertex; 4]>,
other_faces: std::slice::Iter<'a, [Vertex; 3]>,
current_face: Option<&'a [Vertex]>,
current_vertex: usize,
len: usize,
}
impl<'a> Iterator for TriangleIter<'a> {
type Item = [Vertex; 3];
fn next(&mut self) -> Option<[Vertex; 3]> {
if self.current_face.is_none() {
self.current_face = if let Some(face) = self.tri_faces.next() {
Some(face)
} else if let Some(face) = self.quad_faces.next() {
Some(face)
} else if let Some(face) = self.other_faces.next() {
Some(face)
} else {
return None;
}
}
let face = self.current_face.unwrap();
let res = [
face[0],
face[self.current_vertex + 1],
face[self.current_vertex + 2],
];
if self.current_vertex + 3 == face.len() {
self.current_face = None;
self.current_vertex = 0;
} else {
self.current_vertex += 1;
}
self.len -= 1;
Some(res)
}
#[inline(always)]
fn size_hint(&self) -> (usize, Option<usize>) { (self.len, Some(self.len)) }
}
impl<'a> ExactSizeIterator for TriangleIter<'a> {}
#[derive(Clone, Debug)]
pub struct Triangulate<'a> {
entity: &'a PolygonMesh,
other_faces: Vec<[Vertex; 3]>,
}
impl<'a> Triangulate<'a> {
#[inline(always)]
pub fn new(entity: &'a PolygonMesh) -> Self {
fn divide_mesh<T: Copy>(face: &Vec<T>) -> impl Iterator<Item = [T; 3]> + '_ {
face.windows(2).skip(1).map(move |a| [face[0], a[0], a[1]])
}
Triangulate {
other_faces: entity.other_faces().iter().flat_map(divide_mesh).collect(),
entity,
}
}
#[inline(always)]
pub fn get(&self, idx: usize) -> [Vertex; 3] {
let tri_and_quad = self.entity.tri_faces().len() + 2 * self.entity.quad_faces().len();
if idx < self.entity.tri_faces().len() {
self.entity.tri_faces()[idx]
} else if idx < tri_and_quad {
let idx = idx - self.entity.tri_faces().len();
let face = self.entity.quad_faces()[idx / 2];
if idx % 2 == 0 {
[face[0], face[1], face[2]]
} else {
[face[0], face[2], face[3]]
}
} else {
self.other_faces[idx - tri_and_quad]
}
}
#[inline(always)]
pub fn entity(&self) -> &PolygonMesh { self.entity }
}
impl<'a> IntoIterator for &'a Triangulate<'a> {
type Item = [Vertex; 3];
type IntoIter = TriangleIter<'a>;
#[inline(always)]
fn into_iter(self) -> TriangleIter<'a> {
let len = self
.entity
.face_iter()
.fold(0, |len, face| len + face.len());
TriangleIter {
tri_faces: self.entity.tri_faces().iter(),
quad_faces: self.entity.quad_faces().iter(),
other_faces: self.other_faces.iter(),
current_face: None,
current_vertex: 0,
len,
}
}
}
-12
View File
@@ -1,12 +0,0 @@
use crate::*;
use polygon_mesh::PolygonMeshEditor;
mod normal_filters;
mod optimizing;
mod structuring;
mod subdivision;
pub use normal_filters::NormalFilters;
pub use optimizing::OptimizingFilter;
pub use structuring::StructuringFilter;
pub use subdivision::Subdivision;
-396
View File
@@ -1,396 +0,0 @@
use super::*;
use rustc_hash::FxHashMap as HashMap;
/// Filters for adding normals
pub trait NormalFilters {
/// Normalize all normals and assign `None` to the `nor` index of the vertices
/// that has irregular normals.
/// # Examples
/// ```
/// use truck_polymesh::*;
/// use truck_meshalgo::filters::*;
///
/// // Morbid data only for testing
/// let mut mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![Point3::new(0.0, 0.0, 0.0)],
/// normals: vec![
/// Vector3::new(100.0, 20.0, 56.0),
/// Vector3::new(1.0e-12, 3.536e10, std::f64::NAN),
/// Vector3::new(0.0, 1.0, 0.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(&[&[
/// (0, None, Some(0)),
/// (0, None, Some(1)),
/// (0, None, Some(2))
/// ]]),
/// );
///
/// mesh.normalize_normals();
/// assert!(mesh.normals()[0].magnitude().near(&1.0));
/// assert_eq!(mesh.faces()[0][1].nor, None);
/// ```
fn normalize_normals(&mut self) -> &mut Self;
/// Adds face normals to each vertices.
/// # Arguments
/// - If `overwrite == true`, clear all normals and update all normals in vertices.
/// - If `overwrite == false`, add normals only for `nor` is `None`.
/// # Examples
/// Compare with the examples of [`add_smooth_normals`](./trait.NormalFilters.html#tymethod.add_smooth_normals).
/// ```
/// use truck_meshalgo::prelude::*;
/// let mut mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(-5.0, 0.0, 0.0),
/// Point3::new(0.0, 2.0, -2.0),
/// Point3::new(0.0, 2.0, 0.0),
/// Point3::new(0.0, 2.0, 2.0),
/// Point3::new(5.0, 0.0, 0.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(&[
/// &[0, 2, 1],
/// &[0, 3, 2],
/// &[1, 2, 4],
/// &[2, 3, 4],
/// ]),
/// );
///
/// mesh.add_naive_normals(true);
/// let v0: StandardVertex = mesh.faces()[0][1];
/// let v1: StandardVertex = mesh.faces()[3][0];
///
/// // those vertices are at position with the index 2.
/// assert_eq!(v0.pos, 2); assert_eq!(v1.pos, 2);
///
/// // Each normal is each face normal.
/// assert!(mesh.normals()[v0.nor.unwrap()].near(&Vector3::new(-2.0, 5.0, 0.0).normalize()));
/// assert!(mesh.normals()[v1.nor.unwrap()].near(&Vector3::new(2.0, 5.0, 0.0).normalize()));
/// ```
fn add_naive_normals(&mut self, overwrite: bool) -> &mut Self;
/// add the smooth normal vectors to the mesh.
/// # Details
/// For each vertex, apply the following algorithm:
/// 1. prepare vectors that enumerate the normals of the faces containing the target vertices in order.
/// 1. cluster each normal `n` in turn in the following manner.
/// * If there is an existing cluster `A` in which the angle between the weighted
/// average of `A` and `n` is less than or equal to `tol_ang`, add `n` to `A`.
/// * If cluster `A` as described above does not exist, create a new cluster that contains only `n`.
/// # Arguments
/// - If `overwrite == true`, clear all normals and update all normals in vertices.
/// - If `overwrite == false`, add normals only for `nor` is `None`.
/// # Examples
/// Compare with the examples of [`add_smooth_normals`](./trait.NormalFilters.html#tymethod.add_smooth_normals).
/// ```
/// use truck_meshalgo::prelude::*;
/// let mut mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(-5.0, 0.0, 0.0),
/// Point3::new(0.0, 2.0, -2.0),
/// Point3::new(0.0, 2.0, 0.0),
/// Point3::new(0.0, 2.0, 2.0),
/// Point3::new(5.0, 0.0, 0.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(&[
/// &[0, 2, 1], &[0, 3, 2], &[1, 2, 4], &[2, 3, 4],
/// ]),
/// );
///
/// mesh.add_smooth_normals(0.8, true);
/// let v0: StandardVertex = mesh.faces()[0][1];
/// let v1: StandardVertex = mesh.faces()[3][0];
///
/// // those vertices are at position with the index 2.
/// assert_eq!(v0.pos, 2); assert_eq!(v1.pos, 2);
///
/// // Normals are avaraged!
/// assert!(mesh.normals()[v0.nor.unwrap()].near(&Vector3::new(0.0, 1.0, 0.0)));
/// assert!(mesh.normals()[v1.nor.unwrap()].near(&Vector3::new(0.0, 1.0, 0.0)));
/// assert_eq!(v0.nor, v1.nor);
///
/// // If the tolerance is enough little, the faces are recognized as edges.
/// mesh.add_smooth_normals(0.6, true); // Normals are overwritten!
/// let v0: StandardVertex = mesh.faces()[0][1];
/// let v1: StandardVertex = mesh.faces()[3][0];
/// assert!(mesh.normals()[v0.nor.unwrap()].near(&Vector3::new(-2.0, 5.0, 0.0).normalize()));
/// assert!(mesh.normals()[v1.nor.unwrap()].near(&Vector3::new(2.0, 5.0, 0.0).normalize()));
/// ```
fn add_smooth_normals(&mut self, tol_ang: f64, overwrite: bool) -> &mut Self;
/// Makes the orientation of faces compatible to the normal vectors.
/// # Examples
/// ```
/// use truck_polymesh::*;
/// use truck_meshalgo::filters::*;
/// let mut polymesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// ],
/// normals: vec![Vector3::new(0.0, 0.0, -1.0)],
/// ..Default::default()
/// },
/// Faces::from_tri_and_quad_faces(
/// vec![[
/// (0, None, Some(0)).into(),
/// (1, None, Some(0)).into(),
/// (2, None, Some(0)).into(),
/// ]],
/// Vec::new(),
/// ),
/// );
/// polymesh.make_face_compatible_to_normal();
/// assert_eq!(polymesh.faces()[0][0].pos, 2);
/// assert_eq!(polymesh.faces()[0][1].pos, 1);
/// assert_eq!(polymesh.faces()[0][2].pos, 0);
/// ```
fn make_face_compatible_to_normal(&mut self) -> &mut Self;
/// Makes the orientation of faces compatible to the normal vectors.
/// # Examples
/// ```
/// use truck_polymesh::*;
/// use truck_meshalgo::filters::*;
/// let mut polymesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// ],
/// normals: vec![Vector3::new(0.0, 0.0, -1.0)],
/// ..Default::default()
/// },
/// Faces::from_tri_and_quad_faces(
/// vec![[
/// (0, None, Some(0)).into(),
/// (1, None, Some(0)).into(),
/// (2, None, Some(0)).into(),
/// ]],
/// Vec::new(),
/// ),
/// );
/// polymesh.make_normal_compatible_to_face();
/// let nor = polymesh.faces()[0][0].nor.unwrap();
/// assert_eq!(polymesh.normals()[nor], Vector3::new(0.0, 0.0, 1.0));
/// ```
fn make_normal_compatible_to_face(&mut self) -> &mut Self;
}
impl NormalFilters for PolygonMesh {
fn normalize_normals(&mut self) -> &mut Self {
let mut mesh = self.debug_editor();
let PolygonMeshEditor {
attributes: StandardAttributes { normals, .. },
faces,
..
} = &mut mesh;
normals
.iter_mut()
.for_each(move |normal| *normal = normal.normalize());
faces.face_iter_mut().flatten().for_each(|v| {
if let Some(idx) = v.nor {
if !normals[idx].magnitude2().near(&1.0) {
v.nor = None;
}
}
});
drop(mesh);
self
}
fn make_face_compatible_to_normal(&mut self) -> &mut Self {
let mut mesh = self.debug_editor();
let PolygonMeshEditor {
attributes: StandardAttributes {
positions, normals, ..
},
faces,
..
} = &mut mesh;
for face in faces.face_iter_mut() {
let normal = face.iter().fold(Vector3::zero(), |normal, v| {
normal + v.nor.map(|i| normals[i]).unwrap_or_else(Vector3::zero)
});
let face_normal = FaceNormal::new(positions, face, 0).normal;
if normal.dot(face_normal) < 0.0 {
face.reverse();
}
}
drop(mesh);
self
}
fn make_normal_compatible_to_face(&mut self) -> &mut Self {
let mut mesh = self.debug_editor();
let PolygonMeshEditor {
attributes: StandardAttributes {
positions, normals, ..
},
faces,
..
} = &mut mesh;
for face in faces.face_iter_mut() {
let face_normal = FaceNormal::new(positions, face, 0).normal;
face.iter_mut().for_each(|v| {
v.nor = v.nor.map(|idx| {
if normals[idx].dot(face_normal) < 0.0 {
normals.push(-normals[idx]);
normals.len() - 1
} else {
idx
}
});
})
}
drop(mesh);
self
}
fn add_naive_normals(&mut self, overwrite: bool) -> &mut Self {
let mut mesh = self.debug_editor();
let PolygonMeshEditor {
attributes: StandardAttributes {
positions, normals, ..
},
faces,
..
} = &mut mesh;
if overwrite {
normals.clear()
}
faces.face_iter_mut().for_each(move |face| {
let normal = FaceNormal::new(positions, face, 0).normal;
let mut added = false;
face.iter_mut().for_each(|v| {
if v.nor.is_none() || overwrite {
if !added {
normals.push(normal);
added = true;
}
v.nor = Some(normals.len() - 1);
}
});
});
drop(mesh);
self
}
fn add_smooth_normals(&mut self, tol_ang: f64, overwrite: bool) -> &mut Self {
let vnmap = self.clustering_noraml_faces(tol_ang.cos());
self.reflect_normal_clusters(vnmap, overwrite);
self
}
}
trait SubNormalFilter {
fn clustering_noraml_faces(&self, inf: f64) -> HashMap<usize, Vec<Vec<FaceNormal>>>;
fn reflect_normal_clusters(
&mut self,
vnmap: HashMap<usize, Vec<Vec<FaceNormal>>>,
overwrite: bool,
);
}
impl SubNormalFilter for PolygonMesh {
fn clustering_noraml_faces(&self, inf: f64) -> HashMap<usize, Vec<Vec<FaceNormal>>> {
let positions = self.positions();
let mut vnmap = HashMap::default();
self.face_iter()
.enumerate()
.for_each(|(i, face)| add_face_normal(positions, i, face, &mut vnmap, inf));
vnmap
}
fn reflect_normal_clusters(
&mut self,
vnmap: HashMap<usize, Vec<Vec<FaceNormal>>>,
overwrite: bool,
) {
let mut mesh = self.debug_editor();
let PolygonMeshEditor {
attributes: StandardAttributes { normals, .. },
faces,
..
} = &mut mesh;
if overwrite {
normals.clear();
}
for (pos_id, vecs) in vnmap.into_iter() {
for vec in vecs {
let normal = vec
.iter()
.fold(Vector3::zero(), |sum, x| sum + x.normal)
.normalize();
for FaceNormal { face_id, .. } in vec {
signup_vertex_normal(pos_id, face_id, normals, normal, faces, overwrite);
}
}
}
}
}
fn add_face_normal(
positions: &[Point3],
face_id: usize,
face: &[Vertex],
vnmap: &mut HashMap<usize, Vec<Vec<FaceNormal>>>,
inf: f64,
) {
let face_normal = FaceNormal::new(positions, face, face_id);
face.iter().for_each(|v| {
add_to_vnmap(v.pos, face_normal, vnmap, inf);
})
}
fn add_to_vnmap(
pos_id: usize,
face_normal: FaceNormal,
vnmap: &mut HashMap<usize, Vec<Vec<FaceNormal>>>,
inf: f64,
) {
match vnmap.get_mut(&pos_id) {
Some(vecs) => {
for vec in vecs.iter_mut() {
let normal = vec
.iter()
.fold(Vector3::zero(), |sum, x| sum + x.normal)
.normalize();
if face_normal.normal.dot(normal) > inf {
vec.push(face_normal);
return;
}
}
vecs.push(vec![face_normal]);
}
None => {
let vecs = vec![vec![face_normal]];
vnmap.insert(pos_id, vecs);
}
}
}
fn signup_vertex_normal(
pos_id: usize,
face_id: usize,
normals: &mut Vec<Vector3>,
normal: Vector3,
faces: &mut Faces,
overwrite: bool,
) {
let face = faces[face_id].as_mut();
let j = (0..face.len()).find(|j| face[*j].pos == pos_id).unwrap();
if face[j].nor.is_none() || overwrite {
if let Some(n) = normals.last() {
if n != &normal {
normals.push(normal);
}
} else {
normals.push(normal);
}
face[j].nor = Some(normals.len() - 1);
}
}
-441
View File
@@ -1,441 +0,0 @@
use super::*;
use rustc_hash::FxHashMap as HashMap;
use std::iter::Iterator;
use std::ops::{Div, Mul};
/// Filters for optimizing data
pub trait OptimizingFilter {
/// remove all unused position, texture coordinates, and normal vectors.
/// # Examples
/// ```
/// use truck_polymesh::*;
/// use truck_meshalgo::filters::*;
/// let mut mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// Point3::new(0.0, 0.0, 1.0),
/// ],
/// ..Default::default()
/// },
/// // 0 is not used!
/// Faces::from_iter(&[&[1, 2, 3]]),
/// );
///
/// assert_eq!(mesh.positions().len(), 4);
/// mesh.remove_unused_attrs();
/// assert_eq!(mesh.positions().len(), 3);
/// ```
fn remove_unused_attrs(&mut self) -> &mut Self;
/// Removes degenerate polygons.
/// # Examples
/// ```
/// use truck_polymesh::*;
/// use truck_meshalgo::filters::*;
/// let mut mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// Point3::new(0.0, 0.0, 1.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(&[
/// &[0, 1, 2],
/// &[2, 1, 2], // degenerate face!
/// &[2, 1, 3],
/// ]),
/// );
///
/// assert_eq!(mesh.faces().len(), 3);
/// mesh.remove_degenerate_faces();
/// assert_eq!(mesh.faces().len(), 2);
/// ```
fn remove_degenerate_faces(&mut self) -> &mut Self;
/// Gives the same indices to the same positions, texture coordinate, and normal vectors, respectively.
/// # Remarks
/// No longer needed attributes are NOT autoremoved.
/// One can remove such attributes by running [`remove_unused_attrs`] manually.
///
/// [`remove_unused_attrs`]: ./trait.WasteEliminatingFilter.html#tymethod.remove_unused_attrs
///
/// # Examples
/// ```
/// use truck_meshalgo::prelude::*;
/// let mut mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// Point3::new(1.0, 1.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(&[
/// &[0, 1, 2],
/// &[3, 4, 5],
/// ]),
/// );
///
/// assert_eq!(mesh.faces()[1][1], StandardVertex { pos: 4, uv: None, nor: None });
/// mesh.put_together_same_attrs(TOLERANCE);
/// assert_eq!(mesh.faces()[1][1], StandardVertex { pos: 2, uv: None, nor: None });
///
/// // Remarks: No longer needed attributes are NOT autoremoved!
/// assert_eq!(mesh.positions().len(), 6);
/// mesh.remove_unused_attrs();
/// assert_eq!(mesh.positions().len(), 4);
/// ```
fn put_together_same_attrs(&mut self, tol: f64) -> &mut Self;
}
fn all_pos_mut(faces: &mut Faces) -> impl Iterator<Item = &mut usize> {
faces.face_iter_mut().flatten().map(move |v| &mut v.pos)
}
fn all_uv_mut(faces: &mut Faces) -> impl Iterator<Item = &mut usize> {
faces
.face_iter_mut()
.flatten()
.filter_map(move |v| v.uv.as_mut())
}
fn all_nor_mut(faces: &mut Faces) -> impl Iterator<Item = &mut usize> {
faces
.face_iter_mut()
.flatten()
.filter_map(move |v| v.nor.as_mut())
}
impl OptimizingFilter for PolygonMesh {
fn remove_unused_attrs(&mut self) -> &mut Self {
let mut mesh = self.debug_editor();
let PolygonMeshEditor {
attributes:
StandardAttributes {
positions,
uv_coords,
normals,
},
faces,
..
} = &mut mesh;
let pos_iter = all_pos_mut(faces);
let idcs = sub_remove_unused_attrs(pos_iter, positions.len());
*positions = idcs.iter().map(|i| positions[*i]).collect();
let uv_iter = all_uv_mut(faces);
let idcs = sub_remove_unused_attrs(uv_iter, uv_coords.len());
*uv_coords = idcs.iter().map(|i| uv_coords[*i]).collect();
let nor_iter = all_nor_mut(faces);
let idcs = sub_remove_unused_attrs(nor_iter, normals.len());
*normals = idcs.iter().map(|i| normals[*i]).collect();
drop(mesh);
self
}
fn remove_degenerate_faces(&mut self) -> &mut Self {
let mesh = self.debug_editor();
let mut faces = Faces::default();
for tri in mesh.faces.tri_faces() {
if !degenerate_triangle(*tri) {
faces.push(tri);
}
}
for quad in mesh.faces.quad_faces() {
match degenerate_quadrangle(*quad) {
QuadrangleType::TotallyDegenerate => {}
QuadrangleType::Triangle(tri) => faces.push(tri),
QuadrangleType::NonDegenerate => faces.push(quad),
}
}
for face in mesh.faces.other_faces() {
faces.extend(split_into_nondegenerate(face.clone()));
}
*mesh.faces = faces;
drop(mesh);
self
}
fn put_together_same_attrs(&mut self, tol: f64) -> &mut Self {
let mut mesh = self.debug_editor();
let PolygonMeshEditor {
attributes:
StandardAttributes {
positions,
uv_coords,
normals,
},
faces,
..
} = &mut mesh;
let bnd_box: BoundingBox<_> = positions.iter().collect();
let center = bnd_box.center();
let diag = bnd_box.diagonal().map(|a| f64::max(a.abs(), 1.0));
let normalized_positions = positions
.iter()
.map(move |position| 2.0 * (position - center).zip(diag, |a, b| a / b))
.collect::<Vec<_>>();
let pos_map = sub_put_together_same_attrs(&normalized_positions, tol);
all_pos_mut(faces).for_each(|idx| *idx = pos_map[*idx]);
let uv_map = sub_put_together_same_attrs(uv_coords, tol);
all_uv_mut(faces).for_each(|idx| *idx = uv_map[*idx]);
let nor_map = sub_put_together_same_attrs(normals, tol);
all_nor_mut(faces).for_each(|idx| *idx = nor_map[*idx]);
drop(mesh);
self
}
}
fn sub_remove_unused_attrs<'a, I: Iterator<Item = &'a mut usize>>(
iter: I,
old_len: usize,
) -> Vec<usize> {
let mut new2old = Vec::new();
let mut old2new = vec![None; old_len];
for idx in iter {
*idx = match old2new[*idx] {
Some(k) => k,
None => {
let k = new2old.len();
new2old.push(*idx);
old2new[*idx] = Some(k);
k
}
};
}
new2old
}
trait SameAttr: Copy + CastIntVector + MetricSpace<Metric = f64> {}
impl<T> SameAttr for T where T: Copy + CastIntVector + MetricSpace<Metric = f64> {}
fn sub_put_together_same_attrs<T: SameAttr>(attrs: &[T], tol: f64) -> Vec<usize> {
let map = create_blocks(attrs, tol);
let adj = create_adjacency(map, attrs, tol);
let components = create_components(adj, attrs);
centralize(components, attrs)
}
type BlockMap<'a, T> = HashMap<<T as CastIntVector>::IntVector, Vec<usize>>;
fn create_blocks<T: SameAttr>(attrs: &[T], tol: f64) -> BlockMap<'_, T> {
let mut map = HashMap::default();
attrs.iter().enumerate().for_each(|(i, attr)| {
attr.round(tol).neighborhood().into_iter().for_each(|idx| {
map.entry(idx).or_insert_with(Vec::new).push(i);
});
});
map
}
fn create_adjacency<T: SameAttr>(map: BlockMap<'_, T>, attrs: &[T], tol: f64) -> Vec<Vec<usize>> {
let mut adj = vec![Vec::new(); attrs.len()];
map.into_iter().for_each(|(_, vec)| {
if vec.len() > 1 {
vec.iter().copied().enumerate().for_each(|(k, i)| {
vec[(k + 1)..].iter().copied().for_each(|j| {
if attrs[i].distance2(attrs[j]) < tol * tol {
adj[i].push(j);
adj[j].push(i);
}
});
});
}
});
adj
}
fn create_components<T: SameAttr>(
mut adj: Vec<Vec<usize>>,
attrs: &[T],
) -> impl Iterator<Item = Vec<usize>> {
let mut already = vec![false; attrs.len()];
(0..adj.len()).filter_map(move |i| {
if already[i] {
return None;
}
let mut stack = vec![i];
let mut component = Vec::new();
while let Some(i) = stack.pop() {
if !already[i] {
component.push(i);
already[i] = true;
stack.append(&mut adj[i]);
}
}
Some(component)
})
}
fn centralize<T: SameAttr>(
components: impl Iterator<Item = Vec<usize>>,
attrs: &[T],
) -> Vec<usize> {
let mut res = vec![0; attrs.len()];
components.for_each(|component| {
let center = component
.iter()
.fold(T::zero(), |sum, j| sum.add_element_wise(attrs[*j]))
/ component.len() as f64;
let (mut mindist, mut mindx) = (f64::INFINITY, 0);
component.iter().copied().for_each(|i| {
let dist = attrs[i].distance2(center);
if dist < mindist {
mindist = dist;
mindx = i;
}
});
component.into_iter().for_each(|i| {
res[i] = mindx;
});
});
res
}
fn degenerate_triangle(tri: [Vertex; 3]) -> bool {
tri[0].pos == tri[1].pos || tri[1].pos == tri[2].pos || tri[2].pos == tri[0].pos
}
enum QuadrangleType {
NonDegenerate,
Triangle([Vertex; 3]),
TotallyDegenerate,
}
fn degenerate_quadrangle(quad: [Vertex; 4]) -> QuadrangleType {
if (quad[0].pos == quad[2].pos || quad[1].pos == quad[3].pos)
|| (quad[0].pos == quad[1].pos && quad[2].pos == quad[3].pos)
|| quad[1].pos == quad[2].pos && quad[3].pos == quad[0].pos
{
QuadrangleType::TotallyDegenerate
} else if quad[0].pos == quad[1].pos || quad[1].pos == quad[2].pos {
QuadrangleType::Triangle([quad[0], quad[2], quad[3]])
} else if quad[2].pos == quad[3].pos || quad[3].pos == quad[0].pos {
QuadrangleType::Triangle([quad[0], quad[1], quad[2]])
} else {
QuadrangleType::NonDegenerate
}
}
fn split_into_nondegenerate(poly: Vec<Vertex>) -> Vec<Vec<Vertex>> {
for i in 0..poly.len() {
for j in (i + 1)..poly.len() {
if poly[i].pos == poly[j].pos {
let polygon0: Vec<_> = (0..(j - i)).map(|k| poly[k + i]).collect();
let polygon1: Vec<_> = ((j - i)..poly.len())
.map(|k| poly[(k + i) % poly.len()])
.collect();
let mut result = split_into_nondegenerate(polygon0);
result.extend(split_into_nondegenerate(polygon1));
return result;
}
}
}
vec![poly]
}
trait CastIntVector:
Sized + ElementWise<Self> + Mul<f64, Output = Self> + Div<f64, Output = Self> {
type IntVector: IntVector;
fn cast_int(self) -> Self::IntVector;
fn round(self, tol: f64) -> Self::IntVector;
fn zero() -> Self;
}
macro_rules! impl_cast_int {
($typename: ident, $n: expr) => {
impl CastIntVector for $typename {
type IntVector = [i64; $n];
fn cast_int(self) -> [i64; $n] {
self.cast::<i64>()
.unwrap_or_else(|| panic!("failed to cast: {self:?}"))
.into()
}
fn round(self, tol: f64) -> Self::IntVector { (self / tol).cast_int() }
fn zero() -> Self { Self::from([0.0; $n]) }
}
};
}
impl_cast_int!(Vector2, 2);
impl_cast_int!(Vector3, 3);
impl_cast_int!(Point3, 3);
trait IntVector: Copy + std::hash::Hash + Eq {
type Neighborhood: IntoIterator<Item = Self>;
fn neighborhood(self) -> Self::Neighborhood;
}
impl IntVector for [i64; 2] {
type Neighborhood = [[i64; 2]; 9];
fn neighborhood(self) -> Self::Neighborhood {
let [a, b] = self;
[
[a - 1, b - 1],
[a, b - 1],
[a + 1, b - 1],
[a - 1, b],
[a, b],
[a + 1, b],
[a - 1, b + 1],
[a, b + 1],
[a + 1, b + 1],
]
}
}
impl IntVector for [i64; 3] {
type Neighborhood = [[i64; 3]; 27];
fn neighborhood(self) -> Self::Neighborhood {
let [a, b, c] = self;
[
[a - 1, b - 1, c - 1],
[a, b - 1, c - 1],
[a + 1, b - 1, c - 1],
[a - 1, b, c - 1],
[a, b, c - 1],
[a + 1, b, c - 1],
[a - 1, b + 1, c - 1],
[a, b + 1, c - 1],
[a + 1, b + 1, c - 1],
[a - 1, b - 1, c],
[a, b - 1, c],
[a + 1, b - 1, c],
[a - 1, b, c],
[a, b, c],
[a + 1, b, c],
[a - 1, b + 1, c],
[a, b + 1, c],
[a + 1, b + 1, c],
[a - 1, b - 1, c + 1],
[a, b - 1, c + 1],
[a + 1, b - 1, c + 1],
[a - 1, b, c + 1],
[a, b, c + 1],
[a + 1, b, c + 1],
[a - 1, b + 1, c + 1],
[a, b + 1, c + 1],
[a + 1, b + 1, c + 1],
]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn into_vertices(iter: &[usize]) -> Vec<Vertex> { iter.iter().map(|i| i.into()).collect() }
#[test]
fn degenerate_polygon_test() {
let poly = into_vertices(&[0, 1, 2, 0, 3, 4, 5, 6, 3, 7, 8, 9]);
let polys = split_into_nondegenerate(poly);
assert_eq!(polys[0], into_vertices(&[0, 1, 2]));
assert_eq!(polys[1], into_vertices(&[3, 4, 5, 6]));
assert_eq!(polys[2], into_vertices(&[3, 7, 8, 9, 0]));
}
}
-242
View File
@@ -1,242 +0,0 @@
use super::*;
/// triangulation, quadrangulation, give a structure
pub trait StructuringFilter {
/// triangulate all n-gons
/// # Examples
/// ```
/// use truck_polymesh::*;
/// use truck_meshalgo::filters::*;
///
/// // cube consisting quad faces
/// let mut mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(1.0, 1.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// Point3::new(0.0, 0.0, 1.0),
/// Point3::new(1.0, 0.0, 1.0),
/// Point3::new(1.0, 1.0, 1.0),
/// Point3::new(0.0, 1.0, 1.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(&[
/// &[3, 2, 1, 0], &[0, 1, 5, 4], &[1, 2, 6, 5],
/// &[2, 3, 7, 6], &[3, 0, 4, 7], &[4, 5, 6, 7],
/// ]),
/// );
///
/// // the number of face becomes twice since each quadrangle decompose into two triangles.
/// assert_eq!(mesh.faces().len(), 6);
/// mesh.triangulate();
/// assert_eq!(mesh.faces().len(), 12);
/// ```
fn triangulate(&mut self) -> &mut Self;
/// join two triangles into one quadrangle.
/// # Arguments
/// * `plane_tol` - the tolerance for determining that four points are in the same plane
/// * `score_tol` - The upper limit of the score to determine if the four points form an uncrushed rectangle
/// # Details
/// The overview of the algorithm is the following:
/// 1. make the list of pairs of triangles satisfying the following conditions:
/// * two faces are adjacent by one edge,
/// * the pair of faces consists four vertices in the same plane
/// 1. for each joined quadrangle, calculate the score by the sum of the absolute value of
/// the cosine for each angles,
/// 1. sort the list of the pairs of triangles by the score
/// 1. take a pair of triangles in order from the top of the list and register a new one
/// if it doesn't conflict with the one has been already registered.
/// # Panics
/// `plane_tol` and `score_tol` must be more than `TOLERANCE`.
/// # Examples
/// ```
/// use truck_polymesh::*;
/// use truck_meshalgo::filters::*;
///
/// // cube consisting tri_faces
/// let mut mesh = PolygonMesh::new(
/// StandardAttributes {
/// positions: vec![
/// Point3::new(0.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(1.0, 1.0, 0.0),
/// Point3::new(0.0, 1.0, 0.0),
/// Point3::new(0.0, 0.0, 1.0),
/// Point3::new(1.0, 0.0, 1.0),
/// Point3::new(1.0, 1.0, 1.0),
/// Point3::new(0.0, 1.0, 1.0),
/// ],
/// ..Default::default()
/// },
/// Faces::from_iter(&[
/// &[3, 2, 0], &[1, 0, 2], &[0, 1, 4], &[5, 4, 1],
/// &[1, 2, 5], &[6, 5, 2], &[2, 3, 6], &[7, 6, 3],
/// &[3, 0, 7], &[4, 7, 0], &[4, 5, 7], &[6, 7, 5],
/// ]),
/// );
///
/// // The number of faces becomes a half since each pair of triangles is combined.
/// assert_eq!(mesh.faces().len(), 12);
/// mesh.quadrangulate(0.01, 0.1);
/// assert_eq!(mesh.faces().len(), 6);
/// ```
fn quadrangulate(&mut self, plane_tol: f64, score_tol: f64) -> &mut Self;
}
impl StructuringFilter for PolygonMesh {
fn triangulate(&mut self) -> &mut Self {
let tri_faces = self.faces().triangle_iter().collect::<Vec<_>>();
*self.debug_editor().faces = Faces::from_tri_and_quad_faces(tri_faces, Vec::new());
self
}
fn quadrangulate(&mut self, plane_tol: f64, score_tol: f64) -> &mut Self {
nonpositive_tolerance!(plane_tol, 0.0);
nonpositive_tolerance!(score_tol, 0.0);
let list = self.create_face_edge_list(plane_tol, score_tol);
self.reflect_face_edge_list(list);
self
}
}
trait SubStructureFilter {
fn create_face_edge_list(&self, plane_tol: f64, score_tol: f64) -> Vec<FaceEdge>;
fn reflect_face_edge_list(&mut self, list: Vec<FaceEdge>);
fn get_face_edge(
&self,
face0_id: usize,
face1_id: usize,
plane_tol: f64,
score_tol: f64,
) -> Option<FaceEdge>;
}
impl SubStructureFilter for PolygonMesh {
fn create_face_edge_list(&self, plane_tol: f64, score_tol: f64) -> Vec<FaceEdge> {
let face_adjacency = self.faces().face_adjacency(true);
let mut passed = Vec::new();
let tri_len = self.faces().tri_faces().len();
for (i, face) in face_adjacency.iter().enumerate().take(tri_len) {
for j in face {
if i > *j {
continue;
} else if let Some(face_edge) = self.get_face_edge(i, *j, plane_tol, score_tol) {
passed.push(face_edge);
}
}
}
passed.sort_by(|x, y| x.score.partial_cmp(&y.score).unwrap());
passed
}
fn reflect_face_edge_list(&mut self, list: Vec<FaceEdge>) {
let mut used = vec![false; self.faces().tri_faces().len()];
let mut quad_faces = self.faces().quad_faces().clone();
quad_faces.extend(list.into_iter().filter_map(|face_edge| {
let (i, j) = face_edge.faces;
if used[i] || used[j] {
None
} else {
used[i] = true;
used[j] = true;
Some(face_edge.positions)
}
}));
let tri_faces = self.faces().tri_faces();
let tri_faces = used
.into_iter()
.enumerate()
.filter_map(move |(i, flag)| match flag {
true => None,
false => Some(tri_faces[i]),
})
.collect::<Vec<_>>();
*self.debug_editor().faces = Faces::from_tri_and_quad_faces(tri_faces, quad_faces);
}
fn get_face_edge(
&self,
face0_id: usize,
face1_id: usize,
plane_tol: f64,
score_tol: f64,
) -> Option<FaceEdge> {
let face0 = self.faces().tri_faces()[face0_id];
let face1 = self.faces().tri_faces()[face1_id];
let k = (0..3)
.find(|k| face0.iter().all(|x| x.pos != face1[*k].pos))
.unwrap();
let vec0 = self.positions()[face0[1].pos] - self.positions()[face0[0].pos];
let vec1 = self.positions()[face0[2].pos] - self.positions()[face0[0].pos];
let mut n = vec0.cross(vec1);
n /= n.magnitude();
let vec2 = self.positions()[face1[k].pos] - self.positions()[face0[0].pos];
let mat = Matrix3::from_cols(vec0, vec1, n);
let coef = mat.invert().unwrap() * vec2;
if coef[2] > plane_tol {
None
} else if coef[0] > 0.0 && coef[1] > 0.0 {
let score = calc_score(vec0, vec2 - vec0, vec1 - vec2, vec1);
if score < score_tol {
Some(FaceEdge {
faces: (face0_id, face1_id),
positions: [face0[0], face0[1], face1[k], face0[2]],
score,
})
} else {
None
}
} else if coef[0] < 0.0 && coef[1] > 0.0 && coef[0] + coef[1] < 1.0 {
let score = calc_score(vec0, vec1 - vec0, vec2 - vec1, vec2);
if score < score_tol {
Some(FaceEdge {
faces: (face0_id, face1_id),
positions: [face0[0], face0[1], face0[2], face1[k]],
score,
})
} else {
None
}
} else if coef[0] > 0.0 && coef[1] < 0.0 && coef[0] + coef[1] < 1.0 {
let score = calc_score(vec2, vec0 - vec2, vec1 - vec0, vec1);
if score < score_tol {
Some(FaceEdge {
faces: (face0_id, face1_id),
positions: [face0[0], face1[k], face0[1], face0[2]],
score,
})
} else {
None
}
} else {
None
}
}
}
struct FaceEdge {
faces: (usize, usize),
positions: [Vertex; 4],
score: f64,
}
#[inline(always)]
fn calc_score(edge0: Vector3, edge1: Vector3, edge2: Vector3, edge3: Vector3) -> f64 {
edge0.cos_angle(edge1).abs()
+ edge1.cos_angle(edge2).abs()
+ edge2.cos_angle(edge3).abs()
+ edge3.cos_angle(edge0).abs()
}
trait CosAngle {
fn cos_angle(self, other: Self) -> f64;
}
impl CosAngle for Vector3 {
fn cos_angle(self, other: Self) -> f64 {
self.dot(other) / (self.magnitude() * other.magnitude())
}
}
-199
View File
@@ -1,199 +0,0 @@
use super::*;
use rustc_hash::FxHashMap as HashMap;
use std::f64::consts::PI;
/// subdivision surface algorithms
pub trait Subdivision {
/// Extended Loop method
///
/// # Remarks
/// Confirm:
/// - All faces are triangles.
/// - `self.shell_condition()` is `Oriented` or `Closed` before use.
///
/// This method does NOT check these conditions.
fn loop_subdivision(&mut self) -> &mut Self;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Edge(usize, usize);
#[derive(Clone, Copy, Debug)]
struct EdgeInfo {
idx: usize,
first_wing: usize,
second_wing: Option<usize>,
}
impl Edge {
#[inline]
fn new(v0: usize, v1: usize) -> Self { Edge(usize::min(v0, v1), usize::max(v0, v1)) }
}
impl EdgeInfo {
#[inline]
fn new(idx: usize, first_wing: usize) -> Self {
Self {
idx,
first_wing,
second_wing: None,
}
}
#[inline]
fn add_second_wing(&mut self, second_wing: usize) { self.second_wing = Some(second_wing); }
}
impl Subdivision for PolygonMesh {
fn loop_subdivision(&mut self) -> &mut Self {
let mut edges = HashMap::default();
let mut vertex_adjacency = vec![Vec::new(); self.positions().len()];
self.tri_faces().iter().for_each(|face| {
let face = [face[0].pos, face[1].pos, face[2].pos];
add_vertex_edge(&mut edges, &mut vertex_adjacency, face[1], face[2], face[0]);
add_vertex_edge(&mut edges, &mut vertex_adjacency, face[2], face[0], face[1]);
add_vertex_edge(&mut edges, &mut vertex_adjacency, face[0], face[1], face[2]);
});
let positions = vertex_adjacency
.iter()
.enumerate()
.map(|t| calc_new_position(t.0, t.1, &edges, self.positions()))
.chain(edge_positions(&edges, &vertex_adjacency, self.positions()))
.collect::<Vec<_>>();
let tri_faces = self
.tri_faces()
.iter()
.flat_map(|v| {
let len = self.positions().len();
let e: [StandardVertex; 3] = [
(edges.get(&Edge::new(v[1].pos, v[2].pos)).unwrap().idx + len).into(),
(edges.get(&Edge::new(v[2].pos, v[0].pos)).unwrap().idx + len).into(),
(edges.get(&Edge::new(v[0].pos, v[1].pos)).unwrap().idx + len).into(),
];
vec![
[v[0], e[2], e[1]],
[e[0], v[2], e[1]],
[e[0], e[2], v[1]],
[e[0], e[1], e[2]],
]
})
.collect::<Vec<_>>();
{
let editor = self.debug_editor();
editor.attributes.positions = positions;
*editor.faces = Faces::from_tri_and_quad_faces(tri_faces, Vec::new());
}
self
}
}
fn add_vertex_edge(
edges: &mut HashMap<Edge, EdgeInfo>,
vertex_adjacency: &mut [Vec<usize>],
v0: usize,
v1: usize,
v2: usize,
) {
let idx = edges.len();
edges
.entry(Edge::new(v0, v1))
.and_modify(|tuple| tuple.add_second_wing(v2))
.or_insert_with(|| {
vertex_adjacency[v0].push(v1);
vertex_adjacency[v1].push(v0);
EdgeInfo::new(idx, v2)
});
}
#[derive(Clone, Copy, Debug)]
enum VertexBoundaryCondition {
#[allow(dead_code)]
Corner(usize, usize),
Boundary(usize, usize),
Inner,
}
impl VertexBoundaryCondition {
fn new(v: usize, adjacency: &[usize], edges: &HashMap<Edge, EdgeInfo>) -> Self {
let binfo = adjacency.iter().copied().fold((None, None), |binfo, w| {
let edge = edges.get(&Edge::new(v, w)).unwrap();
match (edge.second_wing, binfo) {
(None, (None, _)) => (Some(w), None),
(None, (Some(x), None)) => (Some(x), Some(w)),
_ => binfo,
}
});
if let (Some(x), Some(y)) = binfo {
if adjacency.len() == 2 {
Self::Corner(x, y)
} else {
Self::Boundary(x, y)
}
} else {
Self::Inner
}
}
}
fn calc_new_position(
v: usize,
adjacency: &[usize],
edges: &HashMap<Edge, EdgeInfo>,
positions: &[Point3],
) -> Point3 {
use VertexBoundaryCondition::*;
let point = positions[v];
match VertexBoundaryCondition::new(v, adjacency, edges) {
Corner(_, _) => point,
Boundary(w0, w1) => point + ((positions[w0] - point) + (positions[w1] - point)) / 8.0,
Inner => {
let alpha = 3.0 / 8.0 + f64::cos(2.0 * PI / adjacency.len() as f64) / 4.0;
let alpha = (5.0 / 8.0 - alpha * alpha) / adjacency.len() as f64;
point
+ adjacency
.iter()
.copied()
.map(|w| positions[w] - point)
.sum::<Vector3>()
* alpha
}
}
}
fn edge_positions(
edges: &HashMap<Edge, EdgeInfo>,
vertex_adjacency: &[Vec<usize>],
positions: &[Point3],
) -> Vec<Point3> {
use VertexBoundaryCondition::*;
let mut res = vec![Point3::origin(); edges.len()];
edges.iter().for_each(|tuple| {
let Edge(v0, v1) = *tuple.0;
let EdgeInfo {
idx,
first_wing,
second_wing,
} = *tuple.1;
res[idx] = match second_wing {
Some(second_wing) => {
let gamma = match VertexBoundaryCondition::new(v0, &vertex_adjacency[v0], edges) {
Boundary(_, _) => {
5.0 / 8.0 - f64::cos(PI / vertex_adjacency[v0].len() as f64) / 4.0
}
Inner => match VertexBoundaryCondition::new(v1, &vertex_adjacency[v1], edges) {
Boundary(_, _) => {
3.0 / 8.0 + f64::cos(PI / vertex_adjacency[v1].len() as f64) / 4.0
}
Inner => 0.5,
Corner(_, _) => unreachable!(),
},
Corner(_, _) => unreachable!(),
};
positions[v0]
+ (positions[v1] - positions[v0]) * gamma
+ (positions[first_wing] - positions[v0]) / 8.0
+ (positions[second_wing] - positions[v1]) / 8.0
}
None => positions[v0].midpoint(positions[v1]),
};
});
res
}
-56
View File
@@ -1,56 +0,0 @@
//! Mesh algorigthms, include tessellations of the shape.
// SolidCraft: compiler warnings no longer fail the build.
#![deny(clippy::all, rust_2018_idioms)]
#![warn(
missing_docs,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications
)]
use common::*;
/// re-export polymesh
pub mod rexport_polymesh {
pub use truck_polymesh::*;
}
use truck_polymesh::{StandardVertex as Vertex, *};
/// polygon mesh analizers, including
///
/// - determines topological properties: connectivity, boundary extraction, or shell conditions (closed or oriented)
/// - detects collisions between two meshes and extracts interference lines
/// - investigates positional relations between mesh and point clouds.
#[cfg(feature = "analyzers")]
pub mod analyzers;
mod common;
/// Edits meshes. Add normals, optimizing data, and so on.
#[cfg(feature = "filters")]
pub mod filters;
/// Tessellates shapes.
#[cfg(feature = "tessellation")]
pub mod tessellation;
/// VTK Output
#[cfg(feature = "vtk")]
#[cfg(not(target_arch = "wasm32"))]
pub mod vtk;
/// This module contains all traits and re-exports `truck_polymesh`.
pub mod prelude {
#[cfg(feature = "analyzers")]
pub use crate::analyzers::*;
#[cfg(feature = "filters")]
pub use crate::filters::*;
pub use crate::rexport_polymesh::*;
#[cfg(feature = "tessellation")]
pub use crate::tessellation::*;
#[cfg(feature = "vtk")]
#[cfg(not(target_arch = "wasm32"))]
pub use crate::vtk::*;
}
-121
View File
@@ -1,121 +0,0 @@
//! SolidCraft: a local periodic chart for geometrically closed spline surfaces.
//! The stored spline still has a finite knot domain; only meshing wraps its parameters.
use super::*;
#[derive(Clone, Debug)]
pub(super) struct Closed<'a, S> {
pub base: &'a S,
ranges: [Option<(f64, f64)>; 2],
periods: [Option<f64>; 2],
inferred: [bool; 2],
}
impl<'a, S: PreMeshableSurface> Closed<'a, S> {
pub fn new(base: &'a S) -> Self {
let (u, v) = base.try_range_tuple();
let mut out = Self { base, ranges: [u, v], periods: [base.u_period(), base.v_period()], inferred: [false; 2] };
let (Some((u0, u1)), Some((v0, v1))) = (u, v) else { return out };
if ![u0, u1, v0, v1].iter().all(|v| v.is_finite()) || u1 <= u0 || v1 <= v0 {
return out;
}
let origin = base.subs(u0, v0);
let scale = (0..=4)
.flat_map(|i| (0..=4).map(move |j| (i, j)))
.map(|(i, j)| base.subs(u0 + (u1 - u0) * i as f64 / 4., v0 + (v1 - v0) * j as f64 / 4.).distance(origin))
.fold(0.0_f64, f64::max);
let tol2 = (scale * 1e-8).max(1e-9).powi(2);
for d in 0..2 {
if out.periods[d].is_some() {
continue;
}
let (lo, hi, a, b) = if d == 0 { (u0, u1, v0, v1) } else { (v0, v1, u0, u1) };
let mut noncollapsed = false;
let closed = (0..=8).all(|k| {
let t = a + (b - a) * k as f64 / 8.;
let at = |x| if d == 0 { (x, t) } else { (t, x) };
let (p, q, m) = (at(lo), at(hi), at((lo + hi) * 0.5));
let first = base.subs(p.0, p.1);
let (second, middle) = (base.subs(q.0, q.1), base.subs(m.0, m.1));
if ![first.x, first.y, first.z, second.x, second.y, second.z, middle.x, middle.y, middle.z].iter().all(|x| x.is_finite()) {
return false;
}
noncollapsed |= first.distance2(middle) > tol2;
if first.distance2(second) > tol2 {
return false;
}
let (n0, n1) = (base.normal(p.0, p.1), base.normal(q.0, q.1));
!n0.magnitude2().is_finite() || !n1.magnitude2().is_finite() || n0.dot(n1) > 1.0 - 1e-6
});
if closed && noncollapsed {
out.periods[d] = Some(hi - lo);
out.inferred[d] = true;
}
}
out
}
pub fn periods(&self) -> [Option<f64>; 2] {
self.periods
}
pub fn wrap(&self, (u, v): (f64, f64)) -> (f64, f64) {
let mut p = [u, v];
for (d, x) in p.iter_mut().enumerate() {
if let (true, Some((lo, hi)), Some(period)) = (self.inferred[d], self.ranges[d], self.periods[d]) {
if *x < lo || *x > hi {
*x = lo + (*x - lo).rem_euclid(period);
}
}
}
(p[0], p[1])
}
}
impl<S: PreMeshableSurface> ParametricSurface for Closed<'_, S> {
type Point = Point3;
type Vector = Vector3;
fn subs(&self, u: f64, v: f64) -> Point3 {
let (u, v) = self.wrap((u, v));
self.base.subs(u, v)
}
fn uder(&self, u: f64, v: f64) -> Vector3 {
let (u, v) = self.wrap((u, v));
self.base.uder(u, v)
}
fn vder(&self, u: f64, v: f64) -> Vector3 {
let (u, v) = self.wrap((u, v));
self.base.vder(u, v)
}
fn uuder(&self, u: f64, v: f64) -> Vector3 {
let (u, v) = self.wrap((u, v));
self.base.uuder(u, v)
}
fn uvder(&self, u: f64, v: f64) -> Vector3 {
let (u, v) = self.wrap((u, v));
self.base.uvder(u, v)
}
fn vvder(&self, u: f64, v: f64) -> Vector3 {
let (u, v) = self.wrap((u, v));
self.base.vvder(u, v)
}
fn parameter_range(&self) -> (ParameterRange, ParameterRange) {
self.base.parameter_range()
}
fn u_period(&self) -> Option<f64> {
self.periods[0]
}
fn v_period(&self) -> Option<f64> {
self.periods[1]
}
}
impl<S: PreMeshableSurface> ParametricSurface3D for Closed<'_, S> {
fn normal(&self, u: f64, v: f64) -> Vector3 {
let (u, v) = self.wrap((u, v));
self.base.normal(u, v)
}
}
impl<S: PreMeshableSurface> ParameterDivision2D for Closed<'_, S> {
fn parameter_division(&self, range: ((f64, f64), (f64, f64)), tol: f64) -> (Vec<f64>, Vec<f64>) {
triangulation::bounded_surface_division(self, range, Deflection::chord(tol), triangulation::MAX_GRID)
}
}
-530
View File
@@ -1,530 +0,0 @@
use crate::*;
use spade::{iterators::*, *};
use truck_topology::{compress::*, *};
#[cfg(not(target_arch = "wasm32"))]
mod parallelizable {
/// Parallelizable by `rayon`.
pub trait Parallelizable: Send + Sync {}
impl<T: Send + Sync> Parallelizable for T {}
}
#[cfg(target_arch = "wasm32")]
mod parallelizable {
/// No parallelization in the case of wasm.
pub trait Parallelizable {}
impl<T> Parallelizable for T {}
}
pub use parallelizable::*;
/// SolidCraft: a disposable preview of a face from already sampled boundary loops.
/// Caps input points and uses a fixed parameter grid, including periodic seams.
/// This is approximate display, never an accuracy-controlled modelling mesh.
pub fn preview_face<S: RobustMeshableSurface>(surface: &S, loops: &[Vec<Point3>], steps: usize) -> Option<PolygonMesh> {
triangulation::preview_face(surface, loops, steps)
}
/// Periods of a surface, including smooth geometric closure of bounded splines.
/// Meshing wraps inferred periods into the original knot domain before evaluation.
pub fn surface_periods<S: PreMeshableSurface>(surface: &S) -> [Option<f64>; 2] {
closed::Closed::new(surface).periods()
}
/// Project a trim point with the same bounded, tolerance-aware search used by the mesher.
/// A preceding parameter is a hint, never proof that the search converged correctly.
pub fn project_trim_point<S: RobustMeshableSurface>(surface: &S, point: Point3, hint: Option<(f64, f64)>, tolerance: f64) -> Option<(f64, f64)> {
triangulation::nearest_projection(surface, point, hint, tolerance, 100)
}
/// SolidCraft: an edge's polyline as the meshers divide it (at most 20 000 points). Exposed for
/// the kernel's regressions in the workspace CI.
#[doc(hidden)]
pub fn bounded_polyline<C: PolylineableCurve>(curve: &C, range: (f64, f64), d: Deflection) -> PolylineCurve {
triangulation::bounded_polyline(curve, range, d)
}
/// SolidCraft: sampling a curve that may fail to evaluate. truck's `IntersectionCurve::subs`
/// unwraps the projection of its leader's point onto both surfaces, which does not converge
/// everywhere; the mesher samples through this trait so that a failed sample is data, not a panic.
pub trait TrySubs: ParametricCurve3D {
/// The point at `t`, or `None` where the curve does not evaluate.
fn try_subs(&self, t: f64) -> Option<Point3> { Some(self.subs(t)) }
/// A point near the curve at `t` that always exists (an intersection curve's leader).
fn approx_subs(&self, t: f64) -> Point3 { self.subs(t) }
}
impl TrySubs for truck_geometry::prelude::Line<Point3> {}
impl TrySubs for truck_geometry::prelude::BSplineCurve<Point3> {}
impl TrySubs for truck_geometry::prelude::NurbsCurve<Vector4> {}
impl TrySubs for PolylineCurve {}
impl<C, S> TrySubs for truck_geometry::prelude::IntersectionCurve<C, S>
where
C: ParametricCurve3D,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
fn try_subs(&self, t: f64) -> Option<Point3> { self.search_triple(t).map(|triple| triple.0) }
fn approx_subs(&self, t: f64) -> Point3 { self.leader().subs(t) }
}
impl TrySubs for truck_modeling::Curve {
fn try_subs(&self, t: f64) -> Option<Point3> {
match self {
truck_modeling::Curve::IntersectionCurve(curve) => TrySubs::try_subs(curve, t),
other => Some(other.subs(t)),
}
}
fn approx_subs(&self, t: f64) -> Point3 {
match self {
truck_modeling::Curve::IntersectionCurve(curve) => TrySubs::approx_subs(curve, t),
other => other.subs(t),
}
}
}
/// Gathered the traits used in tessellation.
pub trait PolylineableCurve:
ParametricCurve3D + BoundedCurve + ParameterDivision1D<Point = Point3> + Parallelizable + TrySubs {
}
impl<
C: ParametricCurve3D + BoundedCurve + ParameterDivision1D<Point = Point3> + Parallelizable + TrySubs,
> PolylineableCurve for C
{
}
/// It can be meshed, but not necessarily trimmed.
pub trait PreMeshableSurface: ParametricSurface3D + ParameterDivision2D + Parallelizable {}
impl<S: ParametricSurface3D + ParameterDivision2D + Parallelizable> PreMeshableSurface for S {}
/// The generated mesh can be trimmed only if the boundary curves ride strictly on a surface.
pub trait MeshableSurface: PreMeshableSurface + SearchParameter<D2, Point = Point3> {}
impl<S: PreMeshableSurface + SearchParameter<D2, Point = Point3>> MeshableSurface for S {}
/// The generated mesh can be trimmed if the boundary curves does not ride strictly on a surface.
pub trait RobustMeshableSurface:
MeshableSurface + SearchNearestParameter<D2, Point = Point3> {
}
impl<S: MeshableSurface + SearchNearestParameter<D2, Point = Point3>> RobustMeshableSurface for S {}
type PolylineCurve = truck_polymesh::PolylineCurve<Point3>;
/// SolidCraft: how finely curves and surfaces are divided into facets. `chord` is the usual
/// tolerance: the most a facet may sag from the true shape. `angle`, when finite, also bounds
/// what one facet spans: a facet that turns more than `angle` is split whatever its size (a
/// radius that is small beside the tolerance would otherwise stay one flat facet), until its
/// sag is below `floor`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Deflection {
/// Most a facet may sag from the curve or surface.
pub chord: f64,
/// Most a facet may turn (radians: the arc it spans on a circle); infinite for no bound.
pub angle: f64,
/// Sag below which `angle` no longer splits a facet.
pub floor: f64,
/// SolidCraft: a mesh to display or export, which must lie on its faces, rather than one a
/// Boolean operation intersects (whose results must not depend on what follows). Such a
/// mesh gets grid lines through the vertices that split a face's ruling edges (see
/// `with_ruling_vertices`), triangulates a trimmed face in speed-scaled parameters
/// (`UvScale`), keeps every boundary segment as a constraint (`PolyBoundary::insert_to`),
/// and puts its boundary points on the face's edges (`onto_edges`).
pub display: bool,
}
impl Deflection {
/// Most boundary segments a face may get from the angle bound. Triangulating a face tests
/// each triangle against the boundary loops around it, so the work grows faster than the
/// boundary (with its square for one long loop): the curved edges of a face that would
/// get more are divided at twice the angle, and again, down to the chord tolerance alone.
pub const MAX_ANGLE_BOUNDARY: usize = 4096;
/// Most grid points a face may get from the angle bound (beyond them only `chord` refines).
pub const MAX_ANGLE_GRID: usize = 16_384;
/// How often the angle is doubled for a crowded face before only `chord` is left.
pub const MAX_RELAX: u8 = 4;
/// The chord tolerance alone: what `triangulation(tol)` divides by.
pub const fn chord(tol: f64) -> Self {
Self {
chord: tol,
angle: f64::INFINITY,
floor: 0.0,
display: false,
}
}
/// Whether `angle` bounds anything.
pub fn angular(&self) -> bool { self.angle.is_finite() && self.angle > 0.0 }
/// The same with the angle doubled `level` times; from [`Deflection::MAX_RELAX`] times, or
/// past half a turn, the chord tolerance alone.
pub fn relaxed(&self, level: u8) -> Self {
let angle = self.angle * f64::from(1u32 << level.min(Self::MAX_RELAX));
match level == 0 || (level < Self::MAX_RELAX && angle < std::f64::consts::PI) {
true => Self { angle, ..*self },
false => Self { display: self.display, ..Self::chord(self.chord) },
}
}
/// The squared sag over squared length above which a facet turns more than `angle`: a
/// chord spanning `a` of a circle sags `tan(a / 4) / 2` of its length. Infinite without
/// an angle bound. SolidCraft: public for the kernel's regressions.
#[doc(hidden)]
pub fn turn2(&self) -> f64 {
match self.angular() && self.angle < std::f64::consts::PI {
true => (f64::tan(self.angle / 4.0) / 2.0).powi(2),
false => f64::INFINITY,
}
}
/// Whether the angle bound splits a facet of squared sag `sag2` and squared length `len2`
/// (`turn2` from [`Deflection::turn2`]). SolidCraft: public for the kernel's regressions.
#[doc(hidden)]
pub fn turns(&self, turn2: f64, sag2: f64, len2: f64) -> bool {
turn2.is_finite() && sag2 > self.floor * self.floor && sag2 > turn2 * len2
}
}
/// Trait for converting tessellated shape into polygon.
pub trait MeshedShape {
/// Converts tessellated shape into polygon.
fn to_polygon(&self) -> PolygonMesh;
}
impl MeshedShape for Shell<Point3, PolylineCurve, PolygonMesh> {
fn to_polygon(&self) -> PolygonMesh {
let mut polygon = PolygonMesh::default();
self.face_iter().for_each(|face| {
polygon.merge(face.oriented_surface());
});
polygon
}
}
impl MeshedShape for Shell<Point3, PolylineCurve, Option<PolygonMesh>> {
fn to_polygon(&self) -> PolygonMesh {
let mut polygon = PolygonMesh::default();
self.face_iter().for_each(|face| {
if let Some(mut poly) = face.surface() {
if !face.orientation() {
poly.invert();
}
polygon.merge(poly);
}
});
polygon
}
}
impl<P, C, S> MeshedShape for Solid<P, C, S>
where Shell<P, C, S>: MeshedShape
{
fn to_polygon(&self) -> PolygonMesh {
let mut polygon = PolygonMesh::default();
self.boundaries().iter().for_each(|shell| {
polygon.merge(shell.to_polygon());
});
polygon
}
}
impl MeshedShape for CompressedShell<Point3, PolylineCurve, PolygonMesh> {
fn to_polygon(&self) -> PolygonMesh {
let mut polygon = PolygonMesh::default();
self.faces.iter().for_each(|face| match face.orientation {
true => polygon.merge(face.surface.clone()),
false => polygon.merge(face.surface.inverse()),
});
polygon
}
}
impl MeshedShape for CompressedShell<Point3, PolylineCurve, Option<PolygonMesh>> {
fn to_polygon(&self) -> PolygonMesh {
let mut polygon = PolygonMesh::default();
self.faces.iter().for_each(|face| {
if let Some(surface) = &face.surface {
match face.orientation {
true => polygon.merge(surface.clone()),
false => polygon.merge(surface.inverse()),
}
}
});
polygon
}
}
impl<P, C, S> MeshedShape for CompressedSolid<P, C, S>
where CompressedShell<P, C, S>: MeshedShape
{
fn to_polygon(&self) -> PolygonMesh {
let mut polygon = PolygonMesh::default();
self.boundaries.iter().for_each(|shell| {
polygon.merge(shell.to_polygon());
});
polygon
}
}
/// Trait for tessellating `Shell` and `Solid`.
pub trait MeshableShape {
/// Shape whose edges are made polylines and faces polygon surface.
type MeshedShape: MeshedShape;
/// Tessellates shapes. The division of curves and surfaces are by `ParameterDivision1D` and `ParameterDivision2D`,
/// and the constrained Delauney triangulation is based on the crate [`spade`](https://crates.io/crates/spade).
///
/// # Panics
///
/// `tol` must be more than `TOLERANCE`.
///
/// # Remarks
///
/// - The tessellated mesh is not necessarily closed even if `self` is `Solid`.
/// If you want to get closed mesh, use [`OptimizingFilter::put_together_same_attrs`].
/// - This method requires that the curve ride strictly on a surface. If not, try [`RobustMeshableShape`].
///
/// [`OptimizingFilter::put_together_same_attrs`]: crate::filters::OptimizingFilter::put_together_same_attrs
///
/// # Examples
/// ```
/// use truck_meshalgo::prelude::*;
/// use truck_modeling::builder;
/// use truck_topology::shell::ShellCondition;
///
/// // modeling a unit cube
/// let v = builder::vertex(Point3::origin());
/// let e = builder::tsweep(&v, Vector3::unit_x());
/// let f = builder::tsweep(&e, Vector3::unit_y());
/// let cube = builder::tsweep(&f, Vector3::unit_z());
///
/// // cube is Solid, however, the tessellated mesh is not closed.
/// let mut mesh = cube.triangulation(0.01).to_polygon();
/// assert_ne!(mesh.shell_condition(), ShellCondition::Closed);
///
/// // use optimization filters!
/// mesh.put_together_same_attrs(TOLERANCE);
/// assert_eq!(mesh.shell_condition(), ShellCondition::Closed);
/// ```
fn triangulation(&self, tol: f64) -> Self::MeshedShape;
}
/// Trait for tessellating `Shell` and `Solid` in `truck-modeling`.
pub trait RobustMeshableShape {
/// Shape whose edges are made polylines and faces polygon surface.
type MeshedShape: MeshedShape;
/// Tessellates shapes. The division of curves and surfaces are by `ParameterDivision1D` and `ParameterDivision2D`,
/// and the constrained Delauney triangulation is based on the crate [`spade`](https://crates.io/crates/spade).
///
/// # Panics
///
/// `tol` must be more than `TOLERANCE`.
///
/// # Remarks
///
/// Since polyline vertices are projected onto the surface, processing speed is often slower than with [`MeshableShape::triangulation`].
/// It also does not close the mesh of a solid even if one uses [`OptimizingFilter::put_together_same_attrs`].
///
/// [`OptimizingFilter::put_together_same_attrs`]: crate::filters::OptimizingFilter::put_together_same_attrs
///
/// # Examples
/// ```
/// use truck_meshalgo::prelude::*;
/// use truck_modeling::*;
/// use truck_topology::shell::ShellCondition;
///
/// // manual modeling an open half cylinder
///
/// // points
/// let p = [
/// Point3::new(1.0, 0.0, 0.0),
/// Point3::new(-1.0, 0.0, 0.0),
/// Point3::new(1.0, 0.0, 1.0),
/// Point3::new(-1.0, 0.0, 1.0),
/// ];
/// // vertices
/// let v = Vertex::news(&p);
/// // Curves that do not ride on a cylinder
/// let bsp0: Curve = BSplineCurve::new(
/// KnotVec::bezier_knot(3),
/// vec![
/// p[0],
/// Point3::new(1.0, 4.0 / 3.0, 0.0),
/// Point3::new(-1.0, 4.0 / 3.0, 0.0),
/// p[1],
/// ],
/// )
/// .into();
/// let bsp1: Curve = BSplineCurve::new(
/// KnotVec::bezier_knot(3),
/// vec![
/// p[3],
/// Point3::new(-1.0, 4.0 / 3.0, 1.0),
/// Point3::new(1.0, 4.0 / 3.0, 1.0),
/// p[2],
/// ],
/// )
/// .into();
/// // wire
/// let w: Wire = vec![
/// builder::line(&v[2], &v[0]),
/// Edge::new(&v[0], &v[1], bsp0),
/// builder::line(&v[1], &v[3]),
/// Edge::new(&v[3], &v[2], bsp1),
/// ]
/// .into();
/// // revoluted curve
/// let surface_raw = RevolutedCurve::by_revolution(
/// Curve::Line(Line(p[2], p[0])),
/// Point3::origin(),
/// Vector3::unit_z(),
/// );
/// let surface: Surface = Processor::new(surface_raw).into();
/// // shell
/// let shell: Shell = vec![Face::new(vec![w], surface)].into();
///
/// // Simple triangulation fails since some edges do not ride on a cylinder
/// let poly_shell = shell.triangulation(0.01);
/// assert!(poly_shell[0].surface().is_none());
///
/// // Robust triangulation!
/// let poly_shell = shell.robust_triangulation(0.01);
/// let poly = poly_shell[0].surface().unwrap();
/// assert!(!poly.positions().is_empty());
/// ```
fn robust_triangulation(&self, tol: f64) -> Self::MeshedShape;
}
impl<C: PolylineableCurve, S: MeshableSurface> MeshableShape for Shell<Point3, C, S> {
type MeshedShape = Shell<Point3, PolylineCurve, Option<PolygonMesh>>;
fn triangulation(&self, tol: f64) -> Self::MeshedShape {
nonpositive_tolerance!(tol);
#[cfg(not(target_arch = "wasm32"))]
let res = triangulation::shell_tessellation(
self,
Deflection::chord(tol),
triangulation::by_search_parameter,
);
#[cfg(target_arch = "wasm32")]
let res = triangulation::shell_tessellation_single_thread(
self,
Deflection::chord(tol),
triangulation::by_search_parameter,
);
res
}
}
impl<C: PolylineableCurve, S: RobustMeshableSurface> RobustMeshableShape for Shell<Point3, C, S> {
type MeshedShape = Shell<Point3, PolylineCurve, Option<PolygonMesh>>;
fn robust_triangulation(&self, tol: f64) -> Self::MeshedShape {
self.robust_triangulation_within(Deflection::chord(tol))
}
}
/// SolidCraft: tessellation with a bound on the angle one facet spans as well as on its sag.
pub trait RoundMeshableShape {
/// Shape whose edges are made polylines and faces polygon surface.
type MeshedShape: MeshedShape;
/// [`RobustMeshableShape::robust_triangulation`] divided by `deflection`: with
/// [`Deflection::chord`] the very same shape; with an angle, curved edges and faces whose
/// radius is small beside the chord tolerance are divided as well. Every edge is still
/// divided once, for both faces it bounds, so neighbouring faces keep sharing its points.
///
/// # Panics
///
/// `deflection.chord` must be more than `TOLERANCE`.
fn robust_triangulation_within(&self, deflection: Deflection) -> Self::MeshedShape;
}
impl<C: PolylineableCurve, S: RobustMeshableSurface> RoundMeshableShape for Shell<Point3, C, S> {
type MeshedShape = Shell<Point3, PolylineCurve, Option<PolygonMesh>>;
fn robust_triangulation_within(&self, deflection: Deflection) -> Self::MeshedShape {
nonpositive_tolerance!(deflection.chord);
#[cfg(not(target_arch = "wasm32"))]
let res = triangulation::shell_tessellation(
self,
deflection,
triangulation::by_search_nearest_parameter,
);
#[cfg(target_arch = "wasm32")]
let res = triangulation::shell_tessellation_single_thread(
self,
deflection,
triangulation::by_search_nearest_parameter,
);
res
}
}
impl<C: PolylineableCurve, S: MeshableSurface> MeshableShape for Solid<Point3, C, S> {
type MeshedShape = Solid<Point3, PolylineCurve, Option<PolygonMesh>>;
fn triangulation(&self, tol: f64) -> Self::MeshedShape {
let boundaries = self
.boundaries()
.iter()
.map(|shell| shell.triangulation(tol))
.collect::<Vec<_>>();
Solid::new(boundaries)
}
}
impl<C: PolylineableCurve, S: RobustMeshableSurface> RobustMeshableShape for Solid<Point3, C, S> {
type MeshedShape = Solid<Point3, PolylineCurve, Option<PolygonMesh>>;
fn robust_triangulation(&self, tol: f64) -> Self::MeshedShape {
let boundaries = self
.boundaries()
.iter()
.map(|shell| shell.robust_triangulation(tol))
.collect::<Vec<_>>();
Solid::new(boundaries)
}
}
impl<C: PolylineableCurve, S: MeshableSurface> MeshableShape for CompressedShell<Point3, C, S> {
type MeshedShape = CompressedShell<Point3, PolylineCurve, Option<PolygonMesh>>;
fn triangulation(&self, tol: f64) -> Self::MeshedShape {
nonpositive_tolerance!(tol);
triangulation::cshell_tessellation(self, tol, triangulation::by_search_parameter)
}
}
impl<C: PolylineableCurve, S: RobustMeshableSurface> RobustMeshableShape
for CompressedShell<Point3, C, S>
{
type MeshedShape = CompressedShell<Point3, PolylineCurve, Option<PolygonMesh>>;
fn robust_triangulation(&self, tol: f64) -> Self::MeshedShape {
nonpositive_tolerance!(tol);
triangulation::cshell_tessellation(self, tol, triangulation::by_search_nearest_parameter)
}
}
impl<C: PolylineableCurve, S: MeshableSurface> MeshableShape for CompressedSolid<Point3, C, S> {
type MeshedShape = CompressedSolid<Point3, PolylineCurve, Option<PolygonMesh>>;
fn triangulation(&self, tol: f64) -> Self::MeshedShape {
let boundaries = self
.boundaries
.iter()
.map(|shell| shell.triangulation(tol))
.collect::<Vec<_>>();
CompressedSolid { boundaries }
}
}
impl<C: PolylineableCurve, S: RobustMeshableSurface> RobustMeshableShape
for CompressedSolid<Point3, C, S>
{
type MeshedShape = CompressedSolid<Point3, PolylineCurve, Option<PolygonMesh>>;
fn robust_triangulation(&self, tol: f64) -> Self::MeshedShape {
let boundaries = self
.boundaries
.iter()
.map(|shell| shell.robust_triangulation(tol))
.collect::<Vec<_>>();
CompressedSolid { boundaries }
}
}
mod closed;
mod triangulation;
#[cfg(feature = "test-hooks")]
#[doc(hidden)]
pub use triangulation::{include_test_hooks, loop_fill_test_hooks};
File diff suppressed because it is too large Load Diff
-525
View File
@@ -1,525 +0,0 @@
use crate::*;
use rustc_hash::FxHashMap as HashMap;
use std::convert::identity;
use truck_base::tolerance::TOLERANCE;
use truck_topology::{compress::*, Vertex, *};
pub use vtkio;
use vtkio::model::{Attributes, *};
/// Trait for convert to [`DataSet`]
pub trait ToDataSet {
/// Converts to [`DataSet`]
/// # Remarks
/// The `DataSet` is generated assuming output in XML format.
fn to_data_set(&self) -> DataSet;
}
fn to_vertex_numbers(faces: &Faces<usize>) -> VertexNumbers {
let connectivity = faces
.face_iter()
.flat_map(|face| face.iter().map(|idx| *idx as u64))
.collect::<Vec<_>>();
let mut offset: u64 = 0;
let offsets = faces
.face_iter()
.map(|face| {
offset += face.len() as u64;
offset
})
.collect::<Vec<_>>();
VertexNumbers::XML {
connectivity,
offsets,
}
}
impl ToDataSet for PolygonMesh<usize, Vec<Point3>> {
fn to_data_set(&self) -> DataSet {
let flatten_points = self
.attributes()
.iter()
.flat_map(|p| Into::<[f64; 3]>::into(*p))
.collect::<Vec<_>>();
DataSet::PolyData {
meta: None,
pieces: vec![Piece::Inline(Box::new(PolyDataPiece {
points: IOBuffer::F64(flatten_points),
polys: Some(to_vertex_numbers(self.faces())),
..Default::default()
}))],
}
}
}
impl ToDataSet for PolygonMesh<usize, Vec<(Point3, Vector2)>> {
fn to_data_set(&self) -> DataSet {
let flatten_points = self
.attributes()
.iter()
.flat_map(|(p, _)| Into::<[f64; 3]>::into(*p))
.collect::<Vec<_>>();
let flatten_uvs = self
.attributes()
.iter()
.flat_map(|(_, uv)| Into::<[f64; 2]>::into(*uv))
.collect::<Vec<_>>();
let uvs = Attribute::DataArray(DataArray {
name: "TCoords".to_owned(),
elem: ElementType::TCoords(2),
data: IOBuffer::F64(flatten_uvs),
});
DataSet::PolyData {
meta: None,
pieces: vec![Piece::Inline(Box::new(PolyDataPiece {
points: IOBuffer::F64(flatten_points),
polys: Some(to_vertex_numbers(self.faces())),
data: Attributes {
point: vec![uvs],
..Default::default()
},
..Default::default()
}))],
}
}
}
impl ToDataSet for PolygonMesh<usize, Vec<(Point3, Vector3)>> {
fn to_data_set(&self) -> DataSet {
let flatten_points = self
.attributes()
.iter()
.flat_map(|(p, _)| Into::<[f64; 3]>::into(*p))
.collect::<Vec<_>>();
let flatten_normals = self
.attributes()
.iter()
.flat_map(|(_, n)| Into::<[f64; 3]>::into(*n))
.collect::<Vec<_>>();
let normals = Attribute::DataArray(DataArray {
name: "Normals".to_owned(),
elem: ElementType::Normals,
data: IOBuffer::F64(flatten_normals),
});
DataSet::PolyData {
meta: None,
pieces: vec![Piece::Inline(Box::new(PolyDataPiece {
points: IOBuffer::F64(flatten_points),
polys: Some(to_vertex_numbers(self.faces())),
data: Attributes {
point: vec![normals],
..Default::default()
},
..Default::default()
}))],
}
}
}
impl ToDataSet for PolygonMesh<usize, Vec<StandardAttribute>> {
fn to_data_set(&self) -> DataSet {
let flatten_points = self
.attributes()
.iter()
.flat_map(|attr| Into::<[f64; 3]>::into(attr.position))
.collect::<Vec<_>>();
let flatten_uvs = self
.attributes()
.iter()
.flat_map(|attr| match attr.uv_coord {
Some(uv) => Into::<[f64; 2]>::into(uv),
None => [f64::NAN; 2],
})
.collect::<Vec<_>>();
let uvs = Attribute::DataArray(DataArray {
name: "TCoords".to_owned(),
elem: ElementType::TCoords(2),
data: IOBuffer::F64(flatten_uvs),
});
let flatten_normals = self
.attributes()
.iter()
.flat_map(|attr| match attr.normal {
Some(normal) => Into::<[f64; 3]>::into(normal),
None => [f64::NAN; 3],
})
.collect::<Vec<_>>();
let normals = Attribute::DataArray(DataArray {
name: "Normals".to_owned(),
elem: ElementType::Normals,
data: IOBuffer::F64(flatten_normals),
});
DataSet::PolyData {
meta: None,
pieces: vec![Piece::Inline(Box::new(PolyDataPiece {
points: IOBuffer::F64(flatten_points),
polys: Some(to_vertex_numbers(self.faces())),
data: Attributes {
point: vec![uvs, normals],
..Default::default()
},
..Default::default()
}))],
}
}
}
impl ToDataSet for PolygonMesh {
fn to_data_set(&self) -> DataSet {
let attrs = self.attributes();
match (attrs.uv_coords().is_empty(), attrs.normals().is_empty()) {
(true, true) => self.to_positions_mesh().to_data_set(),
(false, true) => self
.expands(|attr| {
let uv = match attr.uv_coord {
Some(uv) => uv,
None => Vector2::new(f64::NAN, f64::NAN),
};
(attr.position, uv)
})
.to_data_set(),
(true, false) => self
.expands(|attr| {
let normal = match attr.normal {
Some(normal) => normal,
None => Vector3::new(f64::NAN, f64::NAN, f64::NAN),
};
(attr.position, normal)
})
.to_data_set(),
(false, false) => self.expands(identity).to_data_set(),
}
}
}
fn hash_point(p: Point3) -> Option<[i64; 3]> {
(p / (TOLERANCE * 50.0) - Vector3::new(0.5, 0.5, 0.5))
.cast()
.map(Into::into)
}
impl ToDataSet for Vertex<Point3> {
fn to_data_set(&self) -> DataSet {
DataSet::UnstructuredGrid {
meta: None,
pieces: vec![Piece::Inline(Box::new(UnstructuredGridPiece {
points: IOBuffer::F64(Into::<[f64; 3]>::into(self.point()).to_vec()),
cells: Cells {
cell_verts: VertexNumbers::XML {
connectivity: vec![0],
offsets: vec![1],
},
types: vec![CellType::Vertex],
},
data: Default::default(),
}))],
}
}
}
impl ToDataSet for Edge<Point3, PolylineCurve<Point3>> {
fn to_data_set(&self) -> DataSet {
let curve = self.oriented_curve();
let points = curve
.0
.iter()
.copied()
.flat_map(Into::<[f64; 3]>::into)
.collect::<Vec<_>>();
let len = curve.0.len() as u64;
let connectivity = (0..len).chain(vec![0, len - 1]).collect();
DataSet::UnstructuredGrid {
meta: None,
pieces: vec![Piece::Inline(Box::new(UnstructuredGridPiece {
points: IOBuffer::F64(points),
cells: Cells {
cell_verts: VertexNumbers::XML {
connectivity,
offsets: vec![len, len + 1, len + 2],
},
types: vec![CellType::PolyLine, CellType::Vertex, CellType::Vertex],
},
data: Default::default(),
}))],
}
}
}
impl ToDataSet for Wire<Point3, PolylineCurve<Point3>> {
fn to_data_set(&self) -> DataSet {
let mut points = Vec::<[f64; 3]>::new();
let mut connectivity = Vec::<u64>::new();
let mut offsets = Vec::<u64>::new();
let mut types = Vec::<CellType>::new();
let mut vmap = truck_base::entry_map::FxEntryMap::new(
|v: &Vertex<Point3>| v.id(),
|v: &Vertex<Point3>| {
points.push(v.point().into());
points.len() as u64 - 1
},
);
let tmp: Vec<_> = self.vertex_iter().collect();
tmp.iter().for_each(|v| {
vmap.entry_or_insert(v);
});
let vmap: HashMap<_, _> = vmap.into();
self.edge_iter().for_each(|edge| {
let pts = edge.oriented_curve().0;
connectivity.push(*vmap.get(&edge.front().id()).unwrap());
connectivity.extend((1..pts.len() - 1).map(|i| (points.len() + i - 1) as u64));
connectivity.push(*vmap.get(&edge.back().id()).unwrap());
offsets.push(connectivity.len() as u64);
points.extend(
pts[1..pts.len() - 1]
.iter()
.copied()
.map(Into::<[f64; 3]>::into),
);
types.push(CellType::PolyLine);
});
self.vertex_iter().enumerate().for_each(|(idx, _)| {
connectivity.push(idx as u64);
offsets.push(connectivity.len() as u64);
types.push(CellType::Vertex);
});
DataSet::UnstructuredGrid {
meta: None,
pieces: vec![Piece::Inline(Box::new(UnstructuredGridPiece {
points: IOBuffer::F64(points.concat().to_vec()),
cells: Cells {
cell_verts: VertexNumbers::XML {
connectivity,
offsets,
},
types,
},
data: Default::default(),
}))],
}
}
}
fn face_piece(
face: &Face<Point3, PolylineCurve<Point3>, PolygonMesh>,
) -> Piece<UnstructuredGridPiece> {
let polygon = face.oriented_surface().expands(identity);
let mut length = 0;
let map: HashMap<Option<[i64; 3]>, usize> = polygon
.attributes()
.iter()
.map(move |attr| {
length += 1;
(hash_point(attr.position), length - 1)
})
.collect();
let flatten_points = polygon
.attributes()
.iter()
.flat_map(|attr| Into::<[f64; 3]>::into(attr.position))
.collect::<Vec<_>>();
let flatten_uvs = polygon
.attributes()
.iter()
.flat_map(|attr| match attr.uv_coord {
Some(uv) => Into::<[f64; 2]>::into(uv),
None => [f64::NAN; 2],
})
.collect::<Vec<_>>();
let uvs = Attribute::DataArray(DataArray {
name: "TCoords".to_owned(),
elem: ElementType::TCoords(2),
data: IOBuffer::F64(flatten_uvs),
});
let flatten_normals = polygon
.attributes()
.iter()
.flat_map(|attr| match attr.normal {
Some(normal) => Into::<[f64; 3]>::into(normal),
None => [f64::NAN; 3],
})
.collect::<Vec<_>>();
let normals = Attribute::DataArray(DataArray {
name: "Normals".to_owned(),
elem: ElementType::Normals,
data: IOBuffer::F64(flatten_normals),
});
let mut cell_verts = to_vertex_numbers(polygon.faces());
let mut types = vec![CellType::Polygon; cell_verts.num_cells()];
face.boundaries().into_iter().flatten().for_each(|edge| {
types.push(CellType::PolyLine);
let curve = edge.oriented_curve().0;
let polyline = curve
.into_iter()
.filter_map(|p| map.get(&hash_point(p)).map(|i| *i as u64));
if let VertexNumbers::XML {
connectivity,
offsets,
} = &mut cell_verts
{
connectivity.extend(polyline);
offsets.push(connectivity.len() as u64);
}
});
face.vertex_iter().try_for_each(|v| {
types.push(CellType::Vertex);
let idx = map.get(&hash_point(v.point())).map(|i| *i as u64)?;
if let VertexNumbers::XML {
connectivity,
offsets,
} = &mut cell_verts
{
connectivity.push(idx);
offsets.push(connectivity.len() as u64)
}
Some(())
});
Piece::Inline(Box::new(UnstructuredGridPiece {
points: IOBuffer::F64(flatten_points),
cells: Cells { cell_verts, types },
data: Attributes {
point: vec![uvs, normals],
..Default::default()
},
}))
}
impl ToDataSet for Face<Point3, PolylineCurve<Point3>, PolygonMesh> {
fn to_data_set(&self) -> DataSet {
DataSet::UnstructuredGrid {
meta: None,
pieces: vec![face_piece(self)],
}
}
}
impl ToDataSet for Shell<Point3, PolylineCurve<Point3>, PolygonMesh> {
fn to_data_set(&self) -> DataSet {
DataSet::UnstructuredGrid {
meta: None,
pieces: self.face_iter().map(face_piece).collect(),
}
}
}
fn cface_piece(
vertices: &[Point3],
edges: &[CompressedEdge<PolylineCurve<Point3>>],
face: &CompressedFace<PolygonMesh>,
) -> Option<Piece<UnstructuredGridPiece>> {
let polygon = match face.orientation {
true => face.surface.expands(identity),
false => face.surface.inverse().expands(identity),
};
let mut length = 0;
let map: HashMap<Option<[i64; 3]>, usize> = polygon
.attributes()
.iter()
.map(move |attr| {
length += 1;
(hash_point(attr.position), length - 1)
})
.collect();
let flatten_points = polygon
.attributes()
.iter()
.flat_map::<[f64; 3], _>(|attr| attr.position.into())
.collect::<Vec<_>>();
let flatten_uvs = polygon
.attributes()
.iter()
.flat_map(|attr| match attr.uv_coord {
Some(uv) => uv.into(),
None => [f64::NAN; 2],
})
.collect::<Vec<_>>();
let uvs = Attribute::DataArray(DataArray {
name: "TCoords".to_owned(),
elem: ElementType::TCoords(2),
data: IOBuffer::F64(flatten_uvs),
});
let flatten_normals = polygon
.attributes()
.iter()
.flat_map(|attr| match attr.normal {
Some(normal) => normal.into(),
None => [f64::NAN; 3],
})
.collect::<Vec<_>>();
let normals = Attribute::DataArray(DataArray {
name: "Normals".to_owned(),
elem: ElementType::Normals,
data: IOBuffer::F64(flatten_normals),
});
let mut cell_verts = to_vertex_numbers(polygon.faces());
let mut types = vec![CellType::Polygon; cell_verts.num_cells()];
let edge_indices: Vec<_> = face
.boundaries
.iter()
.flat_map::<Box<dyn Iterator<Item = &CompressedEdgeIndex>>, _>(|boundary| {
match face.orientation {
true => Box::new(boundary.iter()),
false => Box::new(boundary.iter().rev()),
}
})
.collect();
edge_indices.iter().try_for_each(|edge| {
let curve = match face.orientation == edge.orientation {
true => edges[edge.index].curve.clone(),
false => edges[edge.index].curve.inverse(),
};
let polyline = curve
.iter()
.filter_map(|p| map.get(&hash_point(*p)).map(|i| *i as u64))
.collect::<Vec<_>>();
if polyline.is_empty() {
return None;
}
types.push(CellType::PolyLine);
if let VertexNumbers::XML {
connectivity,
offsets,
} = &mut cell_verts
{
connectivity.extend(polyline);
offsets.push(connectivity.len() as u64);
}
Some(())
})?;
edge_indices.into_iter().try_for_each(|edge| {
let v = match face.orientation == edge.orientation {
true => edges[edge.index].vertices.0,
false => edges[edge.index].vertices.1,
};
let idx = map.get(&hash_point(vertices[v])).map(|i| *i as u64)?;
types.push(CellType::Vertex);
if let VertexNumbers::XML {
connectivity,
offsets,
} = &mut cell_verts
{
connectivity.push(idx);
offsets.push(connectivity.len() as u64)
}
Some(())
})?;
Some(Piece::Inline(Box::new(UnstructuredGridPiece {
points: IOBuffer::F64(flatten_points),
cells: Cells { cell_verts, types },
data: Attributes {
point: vec![uvs, normals],
..Default::default()
},
})))
}
impl ToDataSet for CompressedShell<Point3, PolylineCurve<Point3>, PolygonMesh> {
fn to_data_set(&self) -> DataSet {
DataSet::UnstructuredGrid {
meta: None,
pieces: self
.faces
.iter()
.filter_map(|face| cface_piece(&self.vertices, &self.edges, face))
.collect(),
}
}
}
-1
View File
@@ -1 +0,0 @@
{"v":1}
-78
View File
@@ -1,78 +0,0 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "truck-shapeops"
version = "0.4.0"
build = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Provides boolean operations to Solid"
homepage = "https://github.com/ricosjp/truck"
readme = "README.md"
license = "Apache-2.0"
repository = "https://github.com/ricosjp/truck"
[lib]
name = "truck_shapeops"
path = "src/lib.rs"
[[example]]
name = "punched-cube-shapeops"
path = "examples/punched-cube-shapeops.rs"
[dependencies.derive_more]
version = "0.99.18"
[dependencies.itertools]
version = "0.13.0"
[dependencies.rustc-hash]
version = "2.0.0"
[dependencies.truck-base]
version = "0.5.0"
[dependencies.truck-geometry]
version = "0.5.0"
[dependencies.truck-geotrait]
version = "0.4.0"
[dependencies.truck-meshalgo]
version = "0.4.0"
[dependencies.truck-stepio]
version = "0.3.0"
optional = true
[dependencies.truck-topology]
version = "0.6.0"
[dev-dependencies.rand]
version = "0.8.5"
[dev-dependencies.serde_json]
version = "1.0.128"
[dev-dependencies.truck-modeling]
version = "0.6.0"
[features]
default = ["step-test"]
step-test = ["truck-stepio"]
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.getrandom]
version = "0.2.15"
features = ["js"]
-177
View File
@@ -1,177 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
-11
View File
@@ -1,11 +0,0 @@
# truck-shapeops
[![Crates.io](https://img.shields.io/crates/v/truck-shapeops.svg)](https://crates.io/crates/truck-shapeops) [![Docs.rs](https://docs.rs/truck-shapeops/badge.svg)](https://docs.rs/truck-shapeops)
Crate for operationg shapes. Provides boolean operations to Solid, and shape healing for importing shapes from other CAD systems.
## Sample Codes
### punched-cube-shapeops
A punched cube generated by shape operator `AND`.
-30
View File
@@ -1,30 +0,0 @@
//! A punched cube generated by shape operator `AND`.
use truck_modeling::*;
fn main() {
let v = builder::vertex(Point3::origin());
let e = builder::tsweep(&v, Vector3::unit_x());
let f = builder::tsweep(&e, Vector3::unit_y());
let cube = builder::tsweep(&f, Vector3::unit_z());
let v = builder::vertex(Point3::new(0.5, 0.25, -0.5));
let w = builder::rsweep(&v, Point3::new(0.5, 0.5, 0.0), Vector3::unit_z(), Rad(7.0));
let f = builder::try_attach_plane(&[w]).unwrap();
let mut cylinder = builder::tsweep(&f, Vector3::unit_z() * 2.0);
cylinder.not();
let and = truck_shapeops::and(&cube, &cylinder, 0.05).unwrap();
and.edge_iter().for_each(|edge| {
let mut curve = edge.curve();
if let Curve::IntersectionCurve(inter) = &curve {
if matches! { inter.leader(), Leader::Polyline(_) } {
let flag = curve.to_bspline_leader(0.01, 0.1, 20);
println!("{flag}");
}
}
edge.set_curve(curve);
});
let json = serde_json::to_vec_pretty(&and).unwrap();
std::fs::write("punched-cube-shapeops.json", json).unwrap();
}
-182
View File
@@ -1,182 +0,0 @@
use truck_meshalgo::prelude::*;
#[derive(Clone, Debug)]
pub enum Alternative<T, U> {
FirstType(T),
SecondType(U),
}
#[doc(hidden)]
#[macro_export]
macro_rules! impl_from {
($firsttype: ty, $secondtype: ty) => {
impl From<$firsttype> for $crate::alternative::Alternative<$firsttype, $secondtype> {
#[inline(always)]
fn from(t: $firsttype) -> Self { $crate::alternative::Alternative::FirstType(t) }
}
};
}
impl<T, U> From<U> for Alternative<T, U> {
#[inline(always)]
fn from(u: U) -> Self { Alternative::SecondType(u) }
}
// test for impl_from
impl_from!((), usize);
macro_rules! derive_method {
($method: tt, $return_type: ty, $($var: ident : $paramtype: ty),*) => {
fn $method (&self, $($var: $paramtype),*) -> $return_type {
match &self {
Alternative::FirstType(got) => got.$method($($var),*),
Alternative::SecondType(got) => got.$method($($var),*),
}
}
};
($method: tt <$x: ident : $y: path>, $return_type: ty, $($var: ident : $paramtype: ty),*) => {
fn $method<$x : $y>(&self, $($var: $paramtype),*) -> $return_type {
match &self {
Alternative::FirstType(got) => got.$method($($var),*),
Alternative::SecondType(got) => got.$method($($var),*),
}
}
};
}
impl<C0, C1> ParametricCurve for Alternative<C0, C1>
where
C0: ParametricCurve,
C1: ParametricCurve<Point = C0::Point, Vector = C0::Vector>,
{
type Point = C0::Point;
type Vector = C0::Vector;
derive_method!(subs, C0::Point, t: f64);
derive_method!(der, C0::Vector, t: f64);
derive_method!(der2, C0::Vector, t: f64);
derive_method!(
parameter_range,
(std::ops::Bound<f64>, std::ops::Bound<f64>),
);
}
impl<C0, C1> BoundedCurve for Alternative<C0, C1>
where
C0: BoundedCurve,
C1: BoundedCurve<Point = C0::Point, Vector = C0::Vector>,
{
}
impl<S0, S1> ParametricSurface for Alternative<S0, S1>
where
S0: ParametricSurface,
S1: ParametricSurface<Point = S0::Point, Vector = S0::Vector>,
{
type Point = S0::Point;
type Vector = S0::Vector;
derive_method!(subs, S0::Point, u: f64, v: f64);
derive_method!(uder, S0::Vector, u: f64, v: f64);
derive_method!(vder, S0::Vector, u: f64, v: f64);
derive_method!(uuder, S0::Vector, u: f64, v: f64);
derive_method!(uvder, S0::Vector, u: f64, v: f64);
derive_method!(vvder, S0::Vector, u: f64, v: f64);
}
impl<S0, S1> ParametricSurface3D for Alternative<S0, S1>
where
S0: ParametricSurface3D,
S1: ParametricSurface3D,
{
derive_method!(normal, Vector3, u: f64, v: f64);
}
impl<C0, C1> Cut for Alternative<C0, C1>
where
C0: Cut,
C1: Cut<Point = C0::Point, Vector = C0::Vector>,
{
fn cut(&mut self, t: f64) -> Self {
match self {
Self::FirstType(curve) => Self::FirstType(curve.cut(t)),
Self::SecondType(curve) => Self::SecondType(curve.cut(t)),
}
}
}
impl<C0, C1> ParameterDivision1D for Alternative<C0, C1>
where
C0: ParameterDivision1D,
C1: ParameterDivision1D<Point = C0::Point>,
{
type Point = C0::Point;
derive_method!(
parameter_division,
(Vec<f64>, Vec<C0::Point>),
range: (f64, f64),
tol: f64
);
}
impl<S0, S1> ParameterDivision2D for Alternative<S0, S1>
where
S0: ParameterDivision2D,
S1: ParameterDivision2D,
{
derive_method!(
parameter_division,
(Vec<f64>, Vec<f64>),
range: ((f64, f64), (f64, f64)),
tol: f64
);
}
impl<D: SPDimension, T, U> SearchParameter<D> for Alternative<T, U>
where
T: SearchParameter<D>,
U: SearchParameter<D, Point = T::Point>,
{
type Point = T::Point;
derive_method!(
search_parameter<H: Into<D::Hint>>,
Option<D::Parameter>,
point: T::Point,
hint: H,
trials: usize
);
}
impl<D: SPDimension, T, U> SearchNearestParameter<D> for Alternative<T, U>
where
T: SearchNearestParameter<D>,
U: SearchNearestParameter<D, Point = T::Point>,
{
type Point = T::Point;
derive_method!(
search_nearest_parameter<H: Into<D::Hint>>,
Option<D::Parameter>,
point: T::Point,
hint: H,
trials: usize
);
}
impl<T, U> Invertible for Alternative<T, U>
where
T: Invertible,
U: Invertible,
{
#[inline(always)]
fn invert(&mut self) {
match self {
Self::FirstType(entity) => entity.invert(),
Self::SecondType(entity) => entity.invert(),
}
}
#[inline(always)]
fn inverse(&self) -> Self {
match self {
Self::FirstType(entity) => Self::FirstType(entity.inverse()),
Self::SecondType(entity) => Self::SecondType(entity.inverse()),
}
}
}
-132
View File
@@ -1,132 +0,0 @@
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use truck_geometry::prelude::*;
use truck_meshalgo::rexport_polymesh::*;
use truck_topology::compress::*;
type Edge<C> = CompressedEdge<C>;
type EdgeIndex = CompressedEdgeIndex;
type Wire = Vec<EdgeIndex>;
type Face<S> = CompressedFace<S>;
type Shell<P, C, S> = CompressedShell<P, C, S>;
trait SP<S>: Fn(&S, Point3, Option<(f64, f64)>) -> Option<(f64, f64)> {}
impl<S, F> SP<S> for F where F: Fn(&S, Point3, Option<(f64, f64)>) -> Option<(f64, f64)> {}
mod split_closed_edges;
use split_closed_edges::split_closed_edges;
mod split_closed_faces;
use split_closed_faces::split_closed_faces;
/// Splits closed edges and faces
///
/// # Details
/// The topology of the shapes handled by truck has the following rules
/// - The endpoints of the edges must be different.
/// - The boundaries of the faces must be a simple wire.
///
/// Shapes created in other CAD systems do not necessarily follow these rules.
/// When such shapes are handled by truck, this method is applied at the stage
/// of `CompressedShell` and `CompressedSolid`, which are intermediate forms.
///
/// # Remarks
/// Boundary simplification is still only implemented for cylinders.
/// It has not yet been implemented for cases involving singularities, such as spherical surfaces.
pub trait SplitClosedEdgesAndFaces {
/// Splits closed edges and faces
fn split_closed_edges_and_faces(&mut self, tol: f64);
}
impl<C, S> SplitClosedEdgesAndFaces for CompressedShell<Point3, C, S>
where
C: ParametricCurve3D
+ BoundedCurve
+ Cut
+ ParameterDivision1D<Point = Point3>
+ SearchNearestParameter<D1, Point = Point3>
+ TryFrom<PCurve<Line<Point2>, S>>,
S: ParametricSurface3D + SearchParameter<D2, Point = Point3>,
{
fn split_closed_edges_and_faces(&mut self, tol: f64) {
fn sp<S>(surface: &S, point: Point3, hint: Option<(f64, f64)>) -> Option<(f64, f64)>
where S: SearchParameter<D2, Point = Point3> {
surface.search_parameter(point, hint, 100)
}
split_closed_edges(self);
split_closed_faces(self, tol, sp);
}
}
impl<C, S> SplitClosedEdgesAndFaces for CompressedSolid<Point3, C, S>
where
C: ParametricCurve3D
+ BoundedCurve
+ Cut
+ ParameterDivision1D<Point = Point3>
+ SearchNearestParameter<D1, Point = Point3>
+ TryFrom<PCurve<Line<Point2>, S>>,
S: ParametricSurface3D + SearchParameter<D2, Point = Point3>,
{
fn split_closed_edges_and_faces(&mut self, tol: f64) {
self.boundaries
.iter_mut()
.for_each(|shell| shell.split_closed_edges_and_faces(tol))
}
}
/// robust version of splitting closed edges and faces.
///
/// # Details
/// Robust version of [`SplitClosedEdgesAndFaces`] based on [`SearchNearestParameter`].
pub trait RobustSplitClosedEdgesAndFaces {
/// Splits closed edges and faces
fn robust_split_closed_edges_and_faces(&mut self, tol: f64);
}
impl<C, S> RobustSplitClosedEdgesAndFaces for CompressedShell<Point3, C, S>
where
C: ParametricCurve3D
+ BoundedCurve
+ Cut
+ ParameterDivision1D<Point = Point3>
+ SearchNearestParameter<D1, Point = Point3>
+ TryFrom<PCurve<Line<Point2>, S>>,
S: ParametricSurface3D
+ SearchParameter<D2, Point = Point3>
+ SearchNearestParameter<D2, Point = Point3>,
{
fn robust_split_closed_edges_and_faces(&mut self, tol: f64) {
fn sp<S>(surface: &S, point: Point3, hint: Option<(f64, f64)>) -> Option<(f64, f64)>
where S: SearchParameter<D2, Point = Point3> + SearchNearestParameter<D2, Point = Point3>
{
surface
.search_parameter(point, hint, 100)
.or_else(|| surface.search_parameter(point, None, 100))
.or_else(|| surface.search_nearest_parameter(point, hint, 100))
.or_else(|| surface.search_nearest_parameter(point, None, 100))
}
split_closed_edges(self);
split_closed_faces(self, tol, sp);
}
}
impl<C, S> RobustSplitClosedEdgesAndFaces for CompressedSolid<Point3, C, S>
where
C: ParametricCurve3D
+ BoundedCurve
+ Cut
+ ParameterDivision1D<Point = Point3>
+ SearchNearestParameter<D1, Point = Point3>
+ TryFrom<PCurve<Line<Point2>, S>>,
S: ParametricSurface3D
+ SearchParameter<D2, Point = Point3>
+ SearchNearestParameter<D2, Point = Point3>,
{
fn robust_split_closed_edges_and_faces(&mut self, tol: f64) {
let fs = RobustSplitClosedEdgesAndFaces::robust_split_closed_edges_and_faces;
self.boundaries.iter_mut().for_each(|shell| fs(shell, tol))
}
}
#[cfg(test)]
mod tests;
-68
View File
@@ -1,68 +0,0 @@
use super::*;
/// Cuts the edge at the middle point of the bounded curve `curve`.
fn cut_edge<P, C>(
vertices: &mut Vec<P>,
Edge {
vertices: (_, v1),
curve,
}: &mut Edge<C>,
) -> Edge<C>
where
C: BoundedCurve<Point = P> + Cut,
{
let (t0, t1) = curve.range_tuple();
let t = (t0 + t1) / 2.0;
vertices.push(curve.subs(t));
let v2 = *v1;
*v1 = vertices.len() - 1;
let v1 = *v1;
let curve0 = curve.cut(t);
Edge {
vertices: (v1, v2),
curve: curve0,
}
}
/// Adds cut edges to the vector `edges`.
fn add_edges<P, C>(vertices: &mut Vec<P>, edges: &mut Vec<Edge<C>>) -> HashMap<usize, usize>
where C: BoundedCurve<Point = P> + Cut {
let len = edges.len();
let sub_add_edges = move |i: usize| {
let (v0, v1) = edges[i].vertices;
if v0 == v1 {
let new_edge = cut_edge(vertices, &mut edges[i]);
edges.push(new_edge);
Some((i, edges.len() - 1))
} else {
None
}
};
(0..len).filter_map(sub_add_edges).collect()
}
fn replace_edges(wire: &mut Wire, added: &HashMap<usize, usize>) {
let insert_one_edge = |edge: EdgeIndex| {
let Some(new_edge_index) = added.get(&edge.index) else {
return vec![edge];
};
let new_edge = EdgeIndex {
index: *new_edge_index,
orientation: edge.orientation,
};
match edge.orientation {
true => vec![edge, new_edge],
false => vec![new_edge, edge],
}
};
let new_wire = wire.iter().copied().flat_map(insert_one_edge).collect();
*wire = new_wire;
}
pub(super) fn split_closed_edges<P, C, S>(shell: &mut Shell<P, C, S>)
where C: BoundedCurve<Point = P> + Cut {
let added = add_edges(&mut shell.vertices, &mut shell.edges);
let wire_iter = shell.faces.iter_mut().flat_map(|face| &mut face.boundaries);
wire_iter.for_each(|wire| replace_edges(wire, &added));
}
-975
View File
@@ -1,975 +0,0 @@
use super::*;
use std::ops::Range;
pub(super) fn split_closed_faces<C, S>(shell: &mut Shell<Point3, C, S>, tol: f64, sp: impl SP<S>)
where
C: ParametricCurve3D
+ BoundedCurve
+ Cut
+ ParameterDivision1D<Point = Point3>
+ SearchNearestParameter<D1, Point = Point3>
+ TryFrom<PCurve<Line<Point2>, S>>,
S: ParametricSurface3D, {
let to_poly = closure_to_poly(tol);
let mut poly_edges: Vec<_> = shell.edges.iter().map(to_poly).collect();
let len = shell.faces.len();
(0..len).for_each(|i| {
split_face_with_non_closed_boundary(i, shell, &mut poly_edges, &sp, tol);
});
let len = shell.faces.len();
let closure = |i| split_face_with_non_simple_wire(i, shell, &mut poly_edges, &sp, tol);
let new_faces: Vec<_> = (0..len).filter_map(closure).flatten().collect();
shell.faces.extend(new_faces);
}
fn split_face_with_non_closed_boundary<C, S>(
face_index: usize,
shell: &mut Shell<Point3, C, S>,
poly_edges: &mut Vec<PolylineCurve<Point3>>,
sp: impl SP<S>,
tol: f64,
) -> Option<()>
where
C: ParametricCurve3D
+ BoundedCurve
+ Cut
+ ParameterDivision1D<Point = Point3>
+ SearchNearestParameter<D1, Point = Point3>
+ TryFrom<PCurve<Line<Point2>, S>>,
S: ParametricSurface3D,
{
let (divisor, open, closed) =
non_closed_wires_in_param_divisor(face_index, shell, poly_edges, &sp)?;
debug_assert_eq!(open.len(), 2);
take_vertices_to_intersections(divisor, face_index, shell, poly_edges, &sp, tol);
let Shell { faces, edges, .. } = shell;
let new_boundaries = split_boundaries_by_divisor(
&faces[face_index],
&closed,
divisor,
edges,
poly_edges,
&sp,
tol,
)?;
let boundaries = &mut faces[face_index].boundaries;
connect_open_boundaries(boundaries, new_boundaries, divisor, edges, open)
}
fn split_face_with_non_simple_wire<C, S>(
face_index: usize,
shell: &mut Shell<Point3, C, S>,
poly_edges: &mut Vec<PolylineCurve<Point3>>,
sp: impl SP<S>,
tol: f64,
) -> Option<Vec<Face<S>>>
where
C: ParametricCurve3D
+ BoundedCurve
+ Cut
+ ParameterDivision1D<Point = Point3>
+ SearchNearestParameter<D1, Point = Point3>
+ TryFrom<PCurve<Line<Point2>, S>>,
S: ParametricSurface3D,
{
let Face { boundaries, .. } = &shell.faces[face_index];
let divisor = find_non_simple_wire_divisor(boundaries, &shell.edges, &shell.vertices)?;
split_face_by_divisor(face_index, divisor, shell, poly_edges, sp, tol)
}
// --- find_non_simple_wire_divisor ---
fn find_non_simple_wire_divisor<C>(
boundaries: &[Wire],
edges: &[Edge<C>],
vertices: &[Point3],
) -> Option<(usize, usize)> {
let closure = |boundary| non_simple_wire_divisor(boundary, edges, vertices);
boundaries.iter().cloned().find_map(closure)
}
fn non_simple_wire_divisor<C>(
mut boundary: Wire,
edges: &[Edge<C>],
vertices: &[Point3],
) -> Option<(usize, usize)> {
let (i0, j0) = boundary.iter().enumerate().find_map(find_loop())?;
boundary.rotate_left(i0);
let j0 = j0 - i0;
let (i1, j1) = boundary
.iter()
.enumerate()
.skip(j0 + 1)
.find_map(find_loop())
.unwrap_or((j0, boundary.len()));
let (k0, k1) = ((j0 + 1) / 2, (i1 + j1 + 1) / 2);
let f = closure_take_front(edges);
let pre_divisor = (f(boundary[k0]), f(boundary[k1]));
nearest_correction(
pre_divisor,
&boundary[1..j0],
&boundary[i1 + 1..j1],
vertices,
closure_take_front(edges),
)
}
fn find_loop() -> impl FnMut((usize, &EdgeIndex)) -> Option<(usize, usize)> {
let mut map = HashMap::<usize, usize>::default();
move |(i, edge)| map.insert(edge.index, i).map(move |j| (j, i))
}
// --- find_non_closed_wires_in_param_divisor ---
type FindNonClosedWiresInParamDivisorResult = Option<((usize, usize), Vec<Wire>, Vec<Wire>)>;
fn non_closed_wires_in_param_divisor<C, S>(
face_index: usize,
Shell {
edges,
faces,
ref vertices,
}: &mut Shell<Point3, C, S>,
poly_edges: &[PolylineCurve<Point3>],
sp: impl SP<S>,
) -> FindNonClosedWiresInParamDivisorResult
where
C: ParametricCurve3D + BoundedCurve + TryFrom<PCurve<Line<Point2>, S>>,
S: ParametricSurface3D,
{
let Face {
boundaries,
surface,
..
} = &faces[face_index];
let param_boundaries = create_param_boundaries(boundaries, surface, poly_edges, sp)?;
let (mut open, mut closed) = (Vec::new(), Vec::new());
boundaries
.iter()
.zip(&param_boundaries)
.for_each(|(boundary, param_boundary)| {
let poly = PolylineCurve(param_boundary.iter().flatten().copied().collect());
match boundary_type(&poly) {
BoundaryType::NotClosed => open.push((boundary, param_boundary)),
_ => closed.push((boundary, param_boundary)),
}
});
if open.len() != 2 {
return None;
}
let take_front = &closure_take_front(edges);
let find_nearest_to_ends = move |i: usize, u0: f64, up: f64| {
move |(edge_index, poly): ZippedEdge<'_>| {
let u = poly[0][i];
let closure = move |i| {
let u = u + i as f64 * up;
f64::abs(u - u0)
};
let dist = (-2..=2)
.map(closure)
.min_by(|x, y| x.partial_cmp(y).unwrap())
.unwrap();
(take_front(*edge_index), dist)
}
};
let create_pre_divisor = move |(wire, poly_wire): ZippedWire<'_>| {
let (urange, vrange) = surface.try_range_tuple();
let (up, vp) = (surface.u_period(), surface.v_period());
let p = poly_wire[0][0];
let q = *poly_wire.last().unwrap().last().unwrap();
let vertices: Vec<(usize, f64)> = if p.x.near(&q.x) {
if let (Some(vp), Some((v0, _))) = (vp, vrange) {
let closure = find_nearest_to_ends(1, v0, vp);
Some(wire.iter().zip(poly_wire).map(closure).collect())
} else {
None
}
} else if p.y.near(&q.y) {
if let (Some(up), Some((u0, _))) = (up, urange) {
let closure = find_nearest_to_ends(0, u0, up);
Some(wire.iter().zip(poly_wire).map(closure).collect())
} else {
None
}
} else {
None
}?;
vertices
.into_iter()
.min_by(|(_, x), (_, y)| x.partial_cmp(y).unwrap())
.map(|(v, _)| v)
};
let pre_divisor_vec = open
.iter()
.copied()
.map(create_pre_divisor)
.collect::<Option<Vec<_>>>()?;
let open: Vec<_> = open.into_iter().map(|(w, _)| w.to_vec()).collect();
let closed: Vec<_> = closed.into_iter().map(|(w, _)| w.to_vec()).collect();
let divisor = nearest_correction(
(pre_divisor_vec[0], pre_divisor_vec[1]),
&open[0],
&open[1],
vertices,
take_front,
)?;
Some((divisor, open, closed))
}
// --- split_face_by_divisor ---
fn split_face_by_divisor<C, S>(
face_index: usize,
divisor: (usize, usize),
shell: &mut Shell<Point3, C, S>,
poly_edges: &mut Vec<PolylineCurve<Point3>>,
sp: impl SP<S>,
tol: f64,
) -> Option<Vec<Face<S>>>
where
C: ParametricCurve3D
+ BoundedCurve
+ Cut
+ ParameterDivision1D<Point = Point3>
+ SearchNearestParameter<D1, Point = Point3>
+ TryFrom<PCurve<Line<Point2>, S>>,
S: ParametricSurface3D,
{
take_vertices_to_intersections(divisor, face_index, shell, poly_edges, &sp, tol);
let face = &shell.faces[face_index];
let new_boundaries = split_boundaries_by_divisor(
face,
&face.boundaries,
divisor,
&mut shell.edges,
poly_edges,
&sp,
tol,
)?;
divide_face(
&mut shell.faces[face_index],
new_boundaries,
poly_edges,
&sp,
)
}
// --- take_vertices_to_intersections ---
fn take_vertices_to_intersections<C, S>(
divisor: (usize, usize),
face_index: usize,
Shell {
vertices,
edges,
faces,
}: &mut Shell<Point3, C, S>,
poly_edges: &mut Vec<PolylineCurve<Point3>>,
sp: impl SP<S>,
tol: f64,
) -> Option<()>
where
C: ParametricCurve3D
+ BoundedCurve
+ Cut
+ SearchNearestParameter<D1, Point = Point3>
+ ParameterDivision1D<Point = Point3>,
S: ParametricSurface3D,
{
let Face {
ref boundaries,
ref surface,
..
} = faces[face_index];
let param_boundaries = create_param_boundaries(boundaries, surface, poly_edges, &sp)?;
let (v0, v1) = divisor;
let p = zip_boundaries(boundaries, &param_boundaries)
.find_map(closure_find_vertex_parameter(v0, edges))?;
let q = zip_boundaries(boundaries, &param_boundaries)
.find_map(closure_find_vertex_parameter(v1, edges))?;
let Face {
ref boundaries,
ref surface,
..
} = faces[face_index];
let periods = (surface.u_period(), surface.v_period());
let q = periodic_iterator(q, periods)
.min_by(|q, r| p.distance2(*q).partial_cmp(&p.distance2(*r)).unwrap())?;
let pcurve = PCurve::new(Line(p, q), surface.clone());
let cut_edge = |(edge_index, param_edge): ZippedEdge<'_>| {
let index = edge_index.index;
let vec = enumerate_intersections(&edges[index], param_edge, &pcurve)?;
if vec.is_empty() {
return Some(None);
}
let erange = cut_edge_by_intersections(index, vec, edges, poly_edges, vertices, tol);
Some(Some((index, erange)))
};
let new_edges = zip_boundaries(boundaries, &param_boundaries)
.map(cut_edge)
.collect::<Option<Vec<_>>>()?;
let insert = |(index, erange): (usize, Range<usize>)| {
faces
.iter_mut()
.flat_map(|face| &mut face.boundaries)
.for_each(|wire| insert_new_edges(wire, index, erange.clone()));
};
new_edges.into_iter().flatten().for_each(insert);
Some(())
}
fn closure_find_vertex_parameter<C>(
v: usize,
edges: &[Edge<C>],
) -> impl Fn(ZippedEdge<'_>) -> Option<Point2> + '_ {
let take_front = closure_take_front(edges);
move |(edge_index, param_edge): ZippedEdge<'_>| match v == take_front(*edge_index) {
true => Some(param_edge[0]),
false => None,
}
}
fn enumerate_intersections<C, S>(
edge: &Edge<C>,
param_edge: &PolylineCurve<Point2>,
pcurve: &PCurve<Line<Point2>, S>,
) -> Option<Vec<(f64, Point3)>>
where
C: ParametricCurve3D + SearchNearestParameter<D1, Point = Point3>,
S: ParametricSurface3D,
{
let rough_intersections = intersections_between_line_polyline(*pcurve.curve(), param_edge);
let intersections = exact_intersections(rough_intersections, pcurve, &edge.curve)?;
Some(intersections)
}
fn intersections_between_line_polyline(
line: Line<Point2>,
param_edge: &PolylineCurve<Point2>,
) -> Vec<f64> {
let filter = |p: &[Point2]| {
let (s, t, _) = line.intersection(Line(p[0], p[1]))?;
let unit = 0.0..1.0;
match unit.contains(&s) && unit.contains(&t) {
true => Some(s),
false => None,
}
};
let mut vec: Vec<_> = param_edge.windows(2).filter_map(filter).collect();
if !vec.is_empty() {
let first = *param_edge.first().unwrap();
let last = *param_edge.last().unwrap();
if line.distance_to_point(first).so_small() {
vec.remove(0);
}
if line.distance_to_point(last).so_small() {
vec.pop();
}
}
vec
}
fn exact_intersections<C, S>(
naive_intersections: Vec<f64>,
pcurve: &PCurve<Line<Point2>, S>,
curve: &C,
) -> Option<Vec<(f64, Point3)>>
where
C: ParametricCurve3D + SearchNearestParameter<D1, Point = Point3>,
S: ParametricSurface3D,
{
let closure = |t| {
let (_, t, p) = search_intersection(pcurve, curve, t)?;
Some((t, p))
};
let mut res = naive_intersections
.into_iter()
.map(closure)
.collect::<Option<Vec<_>>>()?;
res.sort_by(|(s, _), (t, _)| s.partial_cmp(t).unwrap());
Some(res)
}
fn search_intersection<C, S>(
pcurve: &PCurve<Line<Point2>, S>,
curve: &C,
mut t0: f64,
) -> Option<(f64, f64, Point3)>
where
C: ParametricCurve3D + SearchNearestParameter<D1, Point = Point3>,
S: ParametricSurface3D,
{
let mut previous0 = t0;
let mut previous1 = None;
for _ in 0..100 {
let p = pcurve.subs(t0);
let t1 = curve.search_nearest_parameter(p, previous1, 100)?;
let q = curve.subs(t1);
t0 = pcurve.search_nearest_parameter(q, t0, 100)?;
if let Some(previous1) = previous1 {
if previous0.near(&t0) && previous1.near(&t1) {
return Some((t0, t1, pcurve.subs(t0)));
}
}
previous0 = t0;
previous1 = Some(t1);
}
None
}
fn cut_edge_by_intersections<C>(
edge_index: usize,
intersections: Vec<(f64, Point3)>,
edges: &mut Vec<Edge<C>>,
poly_edges: &mut Vec<PolylineCurve<Point3>>,
vertices: &mut Vec<Point3>,
tol: f64,
) -> Range<usize>
where
C: Cut<Point = Point3> + ParameterDivision1D<Point = Point3>,
{
let edge = &mut edges[edge_index];
let vfirst = vertices.len();
vertices.extend(intersections.iter().map(|(_, p)| *p));
let intersections_iter = intersections.into_iter().enumerate().rev();
let closure = |(i, (t, _))| Edge {
vertices: (vfirst + i, vfirst + i + 1),
curve: edge.curve.cut(t),
};
let mut new_edges: Vec<_> = intersections_iter.map(closure).collect();
let to_poly = closure_to_poly(tol);
poly_edges[edge_index] = to_poly(edge);
poly_edges.extend(new_edges.iter().map(to_poly));
new_edges.last_mut().unwrap().vertices.1 = edge.vertices.1;
edge.vertices.1 = vfirst;
let efirst = edges.len();
edges.extend(new_edges);
efirst..edges.len()
}
fn insert_new_edges(wire: &mut Wire, pivot_edge_index: usize, inserted_range: Range<usize>) {
let positions = wire
.iter()
.enumerate()
.filter_map(
|(i, edge_index)| match edge_index.index == pivot_edge_index {
true => Some(i),
false => None,
},
)
.collect::<Vec<usize>>();
positions.into_iter().rev().for_each(|i| {
let orientation = wire[i].orientation;
let ei = |index| EdgeIndex { index, orientation };
if orientation {
let tmp_wire = wire.split_off(i + 1);
wire.extend(inserted_range.clone().map(ei));
wire.extend(tmp_wire);
} else {
let tmp_wire = wire.split_off(i);
wire.extend(inserted_range.clone().rev().map(ei));
wire.extend(tmp_wire);
}
});
}
// --- split boundaries ---
fn split_boundaries_by_divisor<C, S>(
Face {
boundaries,
surface,
..
}: &Face<S>,
closed: &[Wire],
divisor: (usize, usize),
edges: &mut Vec<Edge<C>>,
poly_edges: &mut Vec<PolylineCurve<Point3>>,
sp: impl SP<S>,
tol: f64,
) -> Option<Vec<Wire>>
where
C: ParametricCurve3D + BoundedCurve + TryFrom<PCurve<Line<Point2>, S>>,
S: ParametricSurface3D,
{
let param_boundaries = create_param_boundaries(boundaries, surface, poly_edges, &sp)?;
let param_vertices = create_param_vertices(boundaries, &param_boundaries, edges);
let mut duplicated_edges = duplicated_edges(boundaries.iter().flatten().map(|ei| ei.index));
let vertices_on_divisor = enumerate_vertices_on_divisor(divisor, &param_vertices, surface)?;
let new_edges = create_new_edges(&vertices_on_divisor, poly_edges, surface, tol)?;
let (new_edge_range, new_edge_indices) = signup_new_edges(edges, new_edges);
duplicated_edges.extend(new_edge_range);
let edge_iter = closed.iter().flatten().copied().chain(new_edge_indices);
let vemap = create_vemap(edges, edge_iter);
Some(construct_boundaries(vemap, edges, &duplicated_edges))
}
// --- create_param_boundaries ---
fn create_param_boundaries<S: ParametricSurface3D>(
boundaries: &[Wire],
surface: &S,
poly_edges: &[PolylineCurve<Point3>],
sp: impl SP<S>,
) -> Option<Vec<Vec<PolylineCurve<Point2>>>> {
let closure = |boundary: &Wire| boundary_to_param_polys(boundary, surface, poly_edges, &sp);
boundaries.iter().map(closure).collect()
}
fn boundary_to_param_polys<S: ParametricSurface3D>(
boundary: &Wire,
surface: &S,
poly_edges: &[PolylineCurve<Point3>],
sp: impl SP<S>,
) -> Option<Vec<PolylineCurve<Point2>>> {
let get_poly = closure_get_poly(poly_edges);
let poly_boundary: Vec<_> = boundary.iter().copied().map(get_poly).collect();
let poly_wire_iter = PolyWireIter::try_new(&poly_boundary)?;
let closure = poly_project_to_uv(surface, sp);
let mut long_poly = poly_wire_iter.map(closure).collect::<Option<Vec<_>>>()?;
boundary_into_domain(&mut long_poly, surface);
let mut vec = vec![long_poly.remove(0)];
let split_into_2dpoly = move |poly: &PolylineCurve<_>| {
let latter = long_poly.split_off(poly.len() - 1);
vec.append(&mut long_poly);
let mut res = Vec::new();
res.append(&mut vec);
vec.push(*res.last().unwrap());
long_poly = latter;
PolylineCurve(res)
};
Some(poly_boundary.iter().map(split_into_2dpoly).collect())
}
struct PolyWireIter<'a, P> {
current: std::slice::Iter<'a, P>,
stock: std::vec::IntoIter<std::slice::Iter<'a, P>>,
}
impl<'a, P> PolyWireIter<'a, P> {
fn try_new<I: AsRef<[P]> + 'a>(vec: &'a [I]) -> Option<Self> {
let iters: Vec<_> = vec.iter().map(|v| v.as_ref().iter()).collect();
let mut stock = iters.into_iter();
Some(Self {
current: stock.next()?,
stock,
})
}
}
impl<'a, P: Copy> Iterator for PolyWireIter<'a, P> {
type Item = P;
fn next(&mut self) -> Option<Self::Item> {
if let Some(next) = self.current.next() {
Some(*next)
} else {
self.current = self.stock.next()?;
self.current.next()?;
self.current.next().cloned()
}
}
}
fn poly_project_to_uv<'a, S: ParametricSurface3D>(
surface: &'a S,
sp: impl SP<S> + 'a,
) -> impl FnMut(Point3) -> Option<Point2> + 'a {
let (up, vp) = (surface.u_period(), surface.v_period());
let mut previous = None;
move |pt| {
let (mut u, mut v) = sp(surface, pt, previous)?;
if let (Some(up), Some((u0, _))) = (up, previous) {
u = get_mindiff(u, u0, up);
}
if let (Some(vp), Some((_, v0))) = (vp, previous) {
v = get_mindiff(v, v0, vp);
}
previous = Some((u, v));
Some(Point2::new(u, v))
}
}
fn abs_diff(previous: f64) -> impl Fn(&f64, &f64) -> std::cmp::Ordering {
let f = move |x: &f64| f64::abs(x - previous);
move |x: &f64, y: &f64| f(x).partial_cmp(&f(y)).unwrap()
}
fn get_mindiff(u: f64, u0: f64, up: f64) -> f64 {
let closure = |i| u + i as f64 * up;
(-2..=2).map(closure).min_by(abs_diff(u0)).unwrap()
}
fn boundary_into_domain<S: ParametricSurface3D>(vec: &mut Vec<Point2>, surface: &S) {
let (up, vp) = (surface.u_period(), surface.v_period());
let (urange, vrange) = surface.try_range_tuple();
let grav = vec.iter().fold(Point2::origin(), |g, p| g + p.to_vec()) / vec.len() as f64;
if let (Some(up), Some((u0, _))) = (up, urange) {
let quot = f64::floor((grav.x - u0) / up);
vec.iter_mut().for_each(|p| p.x -= quot * up);
}
if let (Some(vp), Some((v0, _))) = (vp, vrange) {
let quot = f64::floor((grav.y - v0) / vp);
vec.iter_mut().for_each(|p| p.y -= quot * vp);
}
let last = *vec.last().unwrap();
if !vec[0].near(&last) {
let Point2 { x: u0, y: v0 } = last;
if surface.uder(u0, v0).so_small() || surface.vder(u0, v0).so_small() {
vec.push(vec[0]);
}
}
}
// --- create_param_vertices ---
fn create_param_vertices<C>(
boundaries: &[Wire],
param_boundaries: &[Vec<PolylineCurve<Point2>>],
edges: &[Edge<C>],
) -> HashMap<usize, Point2> {
let take_front = closure_take_front(edges);
zip_boundaries(boundaries, param_boundaries)
.map(|(edge_index, param_edge)| (take_front(*edge_index), param_edge[0]))
.collect()
}
// --- duplicated_edges ---
fn duplicated_edges(edges: impl Iterator<Item = usize>) -> HashSet<usize> {
let mut done = HashSet::default();
edges.filter(move |index| !done.insert(*index)).collect()
}
// --- enumerate_vertices_on_divisor ---
fn enumerate_vertices_on_divisor<S: ParametricSurface>(
divisor: (usize, usize),
param_vertices: &HashMap<usize, Point2>,
surface: &S,
) -> Option<Vec<(f64, (usize, Point2))>> {
let (v0, v1) = divisor;
let (p, q) = (*param_vertices.get(&v0)?, *param_vertices.get(&v1)?);
let periods = (surface.u_period(), surface.v_period());
let q = periodic_iterator(q, periods)
.min_by(|q, r| p.distance2(*q).partial_cmp(&p.distance2(*r)).unwrap())?;
let line = Line(p, q);
let iter = param_vertices.iter().filter_map(move |(v, uv)| {
periodic_iterator(*uv, periods)
.find(move |uv| line.distance_to_point_as_segment(*uv).so_small())
.map(move |uv| Some((line.search_nearest_parameter(uv, None, 1)?, (*v, uv))))
});
let mut vertices_on_divisor = iter.collect::<Option<Vec<_>>>()?;
vertices_on_divisor.sort_by(|(s, _), (t, _)| s.partial_cmp(t).unwrap());
Some(vertices_on_divisor)
}
// --- create_new_edges ---
fn create_new_edges<C, S>(
vertices_on_divisor: &[(f64, (usize, Point2))],
poly_edges: &mut Vec<PolylineCurve<Point3>>,
surface: &S,
tol: f64,
) -> Option<Vec<Edge<C>>>
where
C: TryFrom<PCurve<Line<Point2>, S>>,
S: ParametricSurface3D,
{
vertices_on_divisor
.iter()
.for_each(|(_, (_, uv))| print!("{:?} ", surface.subs(uv.x, uv.y)));
println!();
let make_edge = move |p: &[(f64, (usize, Point2))]| {
let ((_, (v0, uv0)), (_, (v1, uv1))) = (p[0], p[1]);
let pcurve = PCurve::new(Line(uv0, uv1), surface.clone());
poly_edges.push(PolylineCurve::from_curve(&pcurve, (0.0, 1.0), tol));
Some(Edge {
vertices: (v0, v1),
curve: C::try_from(pcurve).ok()?,
})
};
vertices_on_divisor.chunks(2).map(make_edge).collect()
}
// --- signup new edges ---
fn signup_new_edges<C>(
edges: &mut Vec<Edge<C>>,
new_edges: Vec<Edge<C>>,
) -> (Range<usize>, impl Iterator<Item = EdgeIndex>) {
let len = edges.len();
edges.extend(new_edges);
let new_edge_indices = (len..edges.len()).flat_map(move |index| {
let ei = |orientation: bool| EdgeIndex { index, orientation };
[ei(true), ei(false)]
});
(len..edges.len(), new_edge_indices)
}
// --- create_vemap ---
fn create_vemap<C>(
edges: &[Edge<C>],
edge_iter: impl Iterator<Item = EdgeIndex>,
) -> HashMap<usize, Vec<EdgeIndex>> {
let mut vemap = HashMap::<usize, Vec<EdgeIndex>>::default();
let take_front = closure_take_front(edges);
edge_iter.for_each(|edge_index| {
let v = take_front(edge_index);
vemap.entry(v).or_insert_with(Vec::new).push(edge_index);
});
vemap
}
// --- construct boundaries ---
fn construct_boundaries<C>(
mut vemap: HashMap<usize, Vec<EdgeIndex>>,
edges: &[Edge<C>],
new_edge_range: &HashSet<usize>,
) -> Vec<Wire> {
let take_back = closure_take_back(edges);
let mut new_boundaries = Vec::new();
while !vemap.is_empty() {
let (start, vec) = vemap.iter_mut().min_by_key(|(idx, _)| *idx).unwrap();
let start = *start;
let mut edge_index = vec.pop().unwrap();
if vec.is_empty() {
vemap.remove(&start);
}
let mut wire = Vec::new();
loop {
wire.push(edge_index);
let v = take_back(edge_index);
if v == start {
break;
}
let Some(vec) = vemap.get_mut(&v) else {
unreachable!();
};
match vec.len() {
1 => {
edge_index = vec.pop().unwrap();
vemap.remove(&v);
}
2 => {
let is_new0 = new_edge_range.contains(&edge_index.index);
let is_new1 = new_edge_range.contains(&vec[0].index);
let i = if is_new0 != is_new1 { 0 } else { 1 };
edge_index = vec.remove(i);
}
_ => panic!("something wrong!"),
}
}
new_boundaries.push(wire);
}
new_boundaries
}
// --- divide_face ---
fn divide_face<S: ParametricSurface3D>(
Face {
ref mut boundaries,
ref surface,
ref orientation,
}: &mut Face<S>,
new_boundaries: Vec<Wire>,
poly_edges: &[PolylineCurve<Point3>],
sp: impl SP<S>,
) -> Option<Vec<Face<S>>> {
let mut face_boundaries = assort_boundary(surface, new_boundaries, poly_edges, sp)?.into_iter();
*boundaries = face_boundaries.next().unwrap();
let create_face = |boundaries| Face {
boundaries,
surface: surface.clone(),
orientation: *orientation,
};
Some(face_boundaries.map(create_face).collect())
}
fn assort_boundary<S>(
surface: &S,
boundaries: Vec<Wire>,
poly_edges: &[PolylineCurve<Point3>],
sp: impl SP<S>,
) -> Option<Vec<Vec<Wire>>>
where
S: ParametricSurface3D,
{
let (mut positives, mut negatives) = (Vec::new(), Vec::new());
let param_boundaries = create_param_boundaries(&boundaries, surface, poly_edges, sp)?;
boundaries
.into_iter()
.zip(param_boundaries)
.try_for_each(|(boundary, param_boundary)| {
let poly_boundary = PolylineCurve(param_boundary.into_iter().flatten().collect());
match boundary_type(&poly_boundary) {
BoundaryType::Positive => positives.push((boundary, poly_boundary)),
BoundaryType::Negative => negatives.push((boundary, poly_boundary)),
_ => return None,
}
Some(())
})?;
let mut assorted = positives.into_iter().map(|x| vec![x]).collect::<Vec<_>>();
negatives
.into_iter()
.try_for_each(|(boundary, poly_boundary)| {
let p = poly_boundary[0];
let vec = assorted.iter_mut().find(|vec| vec[0].1.include(p))?;
vec.push((boundary, poly_boundary));
Some(())
})?;
let res = assorted
.into_iter()
.map(|vec| vec.into_iter().map(|(x, _)| x).collect())
.collect();
Some(res)
}
// --- connect_open_boundaries ---
fn connect_open_boundaries<C>(
boundaries: &mut Vec<Wire>,
mut new_boundaries: Vec<Wire>,
divisor: (usize, usize),
edges: &[Edge<C>],
mut open: Vec<Wire>,
) -> Option<()> {
let (v0, v1) = divisor;
let take_front = closure_take_front(edges);
let find_index = move |wire: &Wire, v: usize| {
wire.iter()
.position(|edge_index| take_front(*edge_index) == v)
};
let (i, index) = open
.iter()
.enumerate()
.find_map(|(i, wire)| find_index(wire, v0).map(|index| (i, index)))?;
if index != 0 {
open.swap(0, 1);
}
let (mut open_v1, mut open_v0) = (open.pop()?, open.pop()?);
let j = find_index(&open_v1, v1)?;
open_v0.rotate_left(i);
open_v1.rotate_left(j);
let (k, added_wire) = new_boundaries
.iter_mut()
.find_map(|wire| find_index(wire, v0).map(|index| (index, wire)))?;
added_wire.rotate_left(k);
open_v0.append(added_wire);
let l = find_index(&open_v0, v1)?;
open_v0.rotate_left(l);
open_v0.append(&mut open_v1);
*added_wire = open_v0;
*boundaries = new_boundaries;
Some(())
}
// --- common functions ---
fn closure_take_front<C>(edges: &[Edge<C>]) -> impl Fn(EdgeIndex) -> usize + '_ {
move |edge_index: EdgeIndex| {
let index = edge_index.index;
let (v0, v1) = edges[index].vertices;
match edge_index.orientation {
true => v0,
false => v1,
}
}
}
fn closure_take_back<C>(edges: &[Edge<C>]) -> impl Fn(EdgeIndex) -> usize + '_ {
move |edge_index: EdgeIndex| {
let (v0, v1) = edges[edge_index.index].vertices;
match edge_index.orientation {
true => v1,
false => v0,
}
}
}
fn closure_get_poly<P: Clone>(
poly_edges: &[PolylineCurve<P>],
) -> impl Fn(EdgeIndex) -> PolylineCurve<P> + '_ {
move |edge_index: EdgeIndex| {
let poly = &poly_edges[edge_index.index];
match edge_index.orientation {
true => poly.clone(),
false => poly.inverse(),
}
}
}
fn closure_to_poly<P, C>(tol: f64) -> impl Fn(&Edge<C>) -> PolylineCurve<P> + 'static
where C: BoundedCurve + ParameterDivision1D<Point = P> {
move |Edge { curve, .. }: &Edge<C>| PolylineCurve::from_curve(curve, curve.range_tuple(), tol)
}
type ZippedEdge<'a> = (&'a EdgeIndex, &'a PolylineCurve<Point2>);
type ZippedWire<'a> = (&'a Wire, &'a Vec<PolylineCurve<Point2>>);
fn zip_boundaries<'a>(
boundaries: &'a [Wire],
param_boundaries: &'a [Vec<PolylineCurve<Point2>>],
) -> impl Iterator<Item = ZippedEdge<'a>> {
boundaries
.iter()
.zip(param_boundaries)
.flat_map(move |(b, pb)| b.iter().zip(pb))
}
#[derive(Clone, Copy, Debug)]
enum BoundaryType {
Positive,
Negative,
NotClosed,
}
fn boundary_type(poly: &PolylineCurve<Point2>) -> BoundaryType {
if poly[0].distance2(poly[poly.len() - 1]) < 1.0e-3 {
match poly.area() > 0.0 {
true => BoundaryType::Positive,
false => BoundaryType::Negative,
}
} else {
BoundaryType::NotClosed
}
}
fn periodic_iterator(
p: Point2,
(up, vp): (Option<f64>, Option<f64>),
) -> impl Iterator<Item = Point2> {
let up_range = match up {
Some(up) => vec![-2.0 * up, -up, 0.0, up, 2.0 * up],
None => vec![0.0],
};
let vp_range = match vp {
Some(vp) => vec![-2.0 * vp, -vp, 0.0, vp, 2.0 * vp],
None => vec![0.0],
};
itertools::iproduct!(up_range, vp_range).map(move |(dx, dy)| p + Vector2::new(dx, dy))
}
fn nearest_correction(
(v0, _v1): (usize, usize),
wire0: &[EdgeIndex],
wire1: &[EdgeIndex],
vertices: &[Point3],
take_front: impl Fn(EdgeIndex) -> usize,
) -> Option<(usize, usize)> {
let p0 = vertices[v0];
let v1 = wire1.iter().copied().map(&take_front).min_by(|v, w| {
p0.distance2(vertices[*v])
.partial_cmp(&p0.distance2(vertices[*w]))
.unwrap()
})?;
let p1 = vertices[v1];
let v0 = wire0.iter().copied().map(&take_front).min_by(|v, w| {
p1.distance2(vertices[*v])
.partial_cmp(&p1.distance2(vertices[*w]))
.unwrap()
})?;
Some((v0, v1))
}
File diff suppressed because it is too large Load Diff
-43
View File
@@ -1,43 +0,0 @@
//! Crate for operationg shapes. Provides boolean operations to Solid, and shape healing for importing shapes from other CAD systems.
// SolidCraft: warnings are not errors in the vendored copy (newer compilers flag more).
#![allow(dead_code)]
#![warn(clippy::all, rust_2018_idioms)]
#![warn(
missing_docs,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications
)]
/// SolidCraft: report where an operation gives up (set `SHAPEOPS_TRACE`).
macro_rules! tr {
($e:expr, $tag:expr) => {
match $e {
Some(v) => v,
None => {
if std::env::var_os("SHAPEOPS_TRACE").is_some() {
eprintln!("shapeops: gave up at {} ({}:{})", $tag, file!(), line!());
}
return None;
}
}
};
}
pub(crate) use tr;
mod healing;
pub use healing::{RobustSplitClosedEdgesAndFaces, SplitClosedEdgesAndFaces};
mod transversal;
pub use transversal::{
and, and_with_history, and_with_history_meshed, meshes, or, or_with_history, EdgeOrigin, FaceIndex, FaceOrigin,
History, MeshedShell, ShapeOpsCurve, ShapeOpsSurface,
};
// SolidCraft: exercised by kernel regressions in the workspace CI.
#[doc(hidden)]
pub use transversal::IntersectionCurveWithParameters;
mod alternative;
-263
View File
@@ -1,263 +0,0 @@
#![allow(clippy::many_single_char_names)]
use super::faces_classification::FacesClassification;
use super::history::Lineage;
use super::loops_store::*;
use rustc_hash::FxHashMap as HashMap;
use std::ops::Deref;
use truck_meshalgo::prelude::*;
use truck_topology::*;
/// SolidCraft: `p` moved into the surface's parameter range (a point found on the boundary can lie a
/// hair outside it, and a search started from there fails).
fn clamped<P, S: ParametricSurface<Point = P>>(surface: &S, p: Point2) -> Point2 {
match surface.try_range_tuple() {
(Some((u0, u1)), Some((v0, v1))) => Point2::new(p.x.clamp(u0, u1), p.y.clamp(v0, v1)),
_ => p,
}
}
fn create_parameter_boundary<P, C, S>(
face: &Face<P, C, S>,
wire: &Wire<P, C>,
polys: &mut HashMap<EdgeID<C>, PolylineCurve<P>>,
tol: f64,
lenient: bool,
) -> Option<PolylineCurve<Point2>>
where
P: Copy,
C: BoundedCurve<Point = P> + ParameterDivision1D<Point = P>,
S: Clone + ParametricSurface<Point = P> + SearchParameter<D2, Point = P> + SearchNearestParameter<D2, Point = P>,
{
let surface = face.surface();
let pt = wire.front_vertex().unwrap().point();
// SolidCraft: intersection-curve points lie on the surface only within the tolerance; fall
// back to the nearest parameter when the exact search fails.
let p: Point2 = surface
.search_parameter(pt, None, 100)
.or_else(|| surface.search_nearest_parameter(pt, None, 100))?
.into();
let vec = wire.edge_iter().try_fold(vec![p], |mut vec, edge| {
// SolidCraft: an intersection curve with a point that projects onto neither surface
// gives this division up (`take_projection_failure`), as its panic did (#5562).
if !polys.contains_key(&edge.id()) {
let curve = edge.curve();
let _ = truck_geometry::prelude::take_projection_failure();
let div = curve.parameter_division(curve.range_tuple(), tol).1;
if truck_geometry::prelude::take_projection_failure() {
return None;
}
polys.insert(edge.id(), PolylineCurve(div));
}
let poly = polys.get(&edge.id())?;
let mut p = *vec.last().unwrap();
let closure = |q: &P| -> Option<Point2> {
// SolidCraft: the points of an intersection curve on a lofted or swept surface lie on
// the seam between its halves, a hair outside the parameter range, and Newton's
// method from the previous point's parameter then fails (or the surface bends away
// from it). `lenient`, the second try after the first gave up: start from the hint
// moved into the range, and as a last resort search the whole surface.
let hint = if lenient { clamped(&surface, p) } else { p };
let found = surface
.search_parameter(*q, Some(hint.into()), 100)
.or_else(|| surface.search_nearest_parameter(*q, Some(hint.into()), 100));
let found = if lenient {
found
.or_else(|| surface.search_parameter(*q, None, 100))
.or_else(|| surface.search_nearest_parameter(*q, None, 100))
} else {
found
};
p = found?.into();
Some(p)
};
let add: Option<Vec<Point2>> = match edge.orientation() {
true => poly.iter().skip(1).map(closure).collect(),
false => poly.iter().rev().skip(1).map(closure).collect(),
};
vec.append(&mut add?);
Some(vec)
})?;
// SolidCraft: a lenient boundary whose points jump half the parameter range or more has been
// found across a periodic surface's seam by the whole-surface search (on a cylinder the
// points of one edge landed at both ends of its angle): it would divide the face wrongly, so
// the face is left undivided as before.
if lenient {
let (u, v) = surface.try_range_tuple();
let half = |r: Option<(f64, f64)>| r.map_or(f64::INFINITY, |(a, b)| (b - a).abs() / 2.0);
let (hu, hv) = (half(u), half(v));
if vec.windows(2).any(|w| (w[1].x - w[0].x).abs() >= hu || (w[1].y - w[0].y).abs() >= hv) {
return None;
}
}
Some(PolylineCurve(vec))
}
/// SolidCraft: whether a loop's polygon in the parameter plane has no area to speak of: two
/// points (a closed loop of two edges divided at their ends only), or an area that is rounding
/// beside the polygon's extent. Its sign then tells nothing.
fn flat_polygon(poly: &PolylineCurve<Point2>) -> bool {
let (mut lo, mut hi) = (Point2::new(f64::INFINITY, f64::INFINITY), Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY));
for p in poly.iter() {
lo = Point2::new(lo.x.min(p.x), lo.y.min(p.y));
hi = Point2::new(hi.x.max(p.x), hi.y.max(p.y));
}
let diag2 = (hi.x - lo.x).powi(2) + (hi.y - lo.y).powi(2);
poly.len() < 4 || !(poly.area().abs() > 1e-9 * diag2)
}
/// SolidCraft: a loop whose polygon is flat ([`flat_polygon`]) divided again, finer, to the
/// loop's own size. A small hole's wire is two half circles; divided at a tolerance that
/// follows a much larger body (a few millimetres for a part over a metre long) each half is a
/// single chord, the polygon has no area, and its sign, which tells a hole from a face piece,
/// comes out at random: the hole became a face of its own, inside out, and the face it was cut
/// in was left whole. The polygon only places the loop; the edges are not changed.
fn finer_if_flat<C, S>(face: &Face<Point3, C, S>, wire: &Wire<Point3, C>, poly: PolylineCurve<Point2>, tol: f64) -> PolylineCurve<Point2>
where
C: BoundedCurve<Point = Point3> + ParameterDivision1D<Point = Point3>,
S: Clone + ParametricSurface3D + SearchParameter<D2, Point = Point3> + SearchNearestParameter<D2, Point = Point3>,
{
if !flat_polygon(&poly) {
return poly;
}
// The loop's extent: the diagonal of the box round its edges' ends and middles.
let mut pts = Vec::new();
for edge in wire.edge_iter() {
let curve = edge.curve();
let (t0, t1) = curve.range_tuple();
pts.extend([curve.subs(t0), curve.subs((t0 + t1) / 2.0), curve.subs(t1)]);
}
let (mut lo, mut hi) = (Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY), Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY));
for p in &pts {
lo = Point3::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
hi = Point3::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
}
let size = if pts.is_empty() { 0.0 } else { hi.distance(lo) };
let mut fine = tol;
for _ in 0..3 {
fine = (fine / 8.0).min(size * 0.01);
if !(fine > 0.0 && fine.is_finite()) {
break;
}
let mut map = HashMap::default();
let again = create_parameter_boundary(face, wire, &mut map, fine, false).or_else(|| create_parameter_boundary(face, wire, &mut map, fine, true));
if let Some(again) = again.filter(|p| !flat_polygon(p)) {
return again;
}
}
poly
}
#[derive(Clone, Debug)]
struct WireChunk<'a, C> {
poly: PolylineCurve<Point2>,
wire: &'a BoundaryWire<Point3, C>,
}
type FaceWithShapesOpStatus<C, S> = (Face<Point3, C, S>, ShapesOpStatus);
fn divide_one_face<C, S>(
face: &Face<Point3, C, S>,
loops: &Loops<Point3, C>,
tol: f64,
) -> Option<Vec<FaceWithShapesOpStatus<C, S>>>
where
C: BoundedCurve<Point = Point3> + ParameterDivision1D<Point = Point3>,
S: Clone + ParametricSurface3D + SearchParameter<D2, Point = Point3> + SearchNearestParameter<D2, Point = Point3>,
{
let (mut pre_faces, mut negative_wires) = (Vec::new(), Vec::new());
let mut map = HashMap::default();
loops.iter().try_for_each(|wire| {
let poly = crate::tr!(
create_parameter_boundary(face, wire, &mut map, tol, false).or_else(|| create_parameter_boundary(face, wire, &mut map, tol, true)),
"parameter boundary"
);
let poly = finer_if_flat(face, wire, poly, tol);
match poly.area() > 0.0 {
true => pre_faces.push(vec![WireChunk { poly, wire }]),
false => negative_wires.push(WireChunk { poly, wire }),
}
Some(())
})?;
if std::env::var_os("SHAPEOPS_TRACE").is_some() {
for c in pre_faces.iter().flatten() {
eprintln!(" positive loop: area {:.4} pts {} first {:?}", c.poly.area(), c.poly.len(), c.poly.front());
}
for c in &negative_wires {
eprintln!(" negative loop: area {:.4} pts {} first {:?} status {:?}", c.poly.area(), c.poly.len(), c.poly.front(), c.wire.status());
}
}
negative_wires.into_iter().try_for_each(|chunk| {
let pt = chunk.poly.front();
// SolidCraft: the innermost piece that holds the loop (nested intersection loops, such
// as both walls of a tube crossing one face), never the loop's own twin.
let hole = -chunk.poly.area();
let op = crate::tr!(
pre_faces
.iter_mut()
.filter(|face| face[0].poly.area() > hole * (1.0 + 1e-6) && face[0].poly.include(pt))
.min_by(|x, y| x[0].poly.area().total_cmp(&y[0].poly.area())),
"loop not inside any face piece"
);
op.push(chunk);
Some(())
})?;
let vec: Vec<_> = pre_faces
.into_iter()
.map(|pre_face| -> Option<_> {
let surface = face.surface();
let op = pre_face
.iter()
.find(|chunk| chunk.wire.status() != ShapesOpStatus::Unknown);
let status = match op {
Some(chunk) => chunk.wire.status(),
None => ShapesOpStatus::Unknown,
};
let wires: Vec<Wire<Point3, C>> = pre_face
.into_iter()
.map(|chunk| chunk.wire.deref().clone())
.collect();
// SolidCraft: no panics (wasm can't catch them): a wire visiting a vertex twice gives up.
let mut new_face = crate::tr!(Face::try_new(wires, surface).ok(), "face of a wire visiting a vertex twice");
if !face.orientation() {
new_face.invert();
}
Some((new_face, status))
})
.collect::<Option<_>>()?;
Some(vec)
}
pub fn divide_faces<C, S>(
shell: &Shell<Point3, C, S>,
loops_store: &LoopsStore<Point3, C>,
tol: f64,
lineage: &mut Lineage<C, S>,
) -> Option<FacesClassification<Point3, C, S>>
where
C: BoundedCurve<Point = Point3> + ParameterDivision1D<Point = Point3>,
S: Clone + ParametricSurface3D + SearchParameter<D2, Point = Point3> + SearchNearestParameter<D2, Point = Point3>,
{
let mut res = FacesClassification::<Point3, C, S>::default();
shell
.iter()
.zip(loops_store)
.try_for_each(|(face, loops)| {
if loops
.iter()
.all(|wire| wire.status() == ShapesOpStatus::Unknown)
{
res.push(face.clone(), ShapesOpStatus::Unknown);
} else {
let vec = crate::tr!(divide_one_face(face, loops, tol), "divide one face");
vec.into_iter().for_each(|(piece, status)| {
lineage.face_piece(face.id(), piece.id());
res.push(piece, status)
});
}
Some(())
})?;
Some(res)
}
#[cfg(test)]
mod tests;
@@ -1,226 +0,0 @@
use super::*;
use shell::ShellCondition;
use truck_geometry::prelude::*;
use truck_topology::Vertex;
const TOL: f64 = 0.05;
fn line(v0: &Vertex<Point3>, v1: &Vertex<Point3>) -> Edge<Point3, BSplineCurve<Point3>> {
let curve = BSplineCurve::new(KnotVec::bezier_knot(1), vec![v0.point(), v1.point()]);
Edge::new(v0, v1, curve)
}
fn parabola(
v0: &Vertex<Point3>,
v1: &Vertex<Point3>,
pt: Point3,
) -> Edge<Point3, BSplineCurve<Point3>> {
let curve = BSplineCurve::new(KnotVec::bezier_knot(2), vec![v0.point(), pt, v1.point()]);
Edge::new(v0, v1, curve)
}
#[test]
fn divide_plane_test() {
let v = Vertex::news([
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 4.0, 0.0),
Point3::new(-1.0, 1.0, 0.0),
Point3::new(-1.0, 3.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(1.0, 3.0, 0.0),
]);
let edge = vec![
parabola(&v[0], &v[1], Point3::new(-4.0, 2.0, 0.0)),
parabola(&v[0], &v[1], Point3::new(4.0, 2.0, 0.0)),
line(&v[0], &v[1]),
parabola(&v[2], &v[3], Point3::new(-3.0, 2.0, 0.0)),
parabola(&v[2], &v[3], Point3::new(-1.0, 2.0, 0.0)),
parabola(&v[4], &v[5], Point3::new(1.0, 2.0, 0.0)),
parabola(&v[4], &v[5], Point3::new(3.0, 2.0, 0.0)),
];
let wire: Vec<Wire<_, _>> = vec![
vec![edge[1].clone(), edge[0].inverse()].into(),
vec![edge[2].clone(), edge[0].inverse()].into(),
vec![edge[1].clone(), edge[2].inverse()].into(),
vec![edge[3].clone(), edge[4].inverse()].into(),
vec![edge[5].clone(), edge[6].inverse()].into(),
];
let face = Face::new(
vec![wire[0].clone(), wire[3].clone(), wire[4].clone()],
Plane::new(
Point3::origin(),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
),
);
let loops: Loops<_, _> = vec![
BoundaryWire::new(wire[1].clone(), ShapesOpStatus::Or),
BoundaryWire::new(wire[2].clone(), ShapesOpStatus::And),
BoundaryWire::new(wire[3].clone(), ShapesOpStatus::Unknown),
BoundaryWire::new(wire[4].clone(), ShapesOpStatus::Unknown),
]
.into_iter()
.collect();
let res = divide_one_face(&face, &loops, 0.01).unwrap();
assert_eq!(res.len(), 2);
let (mut or, mut and) = (true, true);
for (face, status) in res {
let bdd = face.absolute_boundaries();
match status {
ShapesOpStatus::Or => {
assert_eq!(bdd.len(), 2);
assert!(bdd[0] == wire[1] || bdd[0] == wire[3]);
assert!(bdd[1] == wire[1] || bdd[1] == wire[3]);
assert_ne!(bdd[0], bdd[1]);
assert!(or);
or = false;
}
ShapesOpStatus::And => {
assert_eq!(bdd.len(), 2);
assert!(bdd[0] == wire[2] || bdd[0] == wire[4]);
assert!(bdd[1] == wire[2] || bdd[1] == wire[4]);
assert_ne!(bdd[0], bdd[1]);
assert!(and);
and = false;
}
_ => panic!("There must be no unknown!"),
}
}
}
type AlternativeIntersection = crate::alternative::Alternative<
NurbsCurve<Vector4>,
IntersectionCurve<PolylineCurve<Point3>, AlternativeSurface>,
>;
type AlternativeSurface = crate::alternative::Alternative<BSplineSurface<Point3>, Plane>;
crate::impl_from!(
NurbsCurve<Vector4>,
IntersectionCurve<PolylineCurve<Point3>, AlternativeSurface>
);
crate::impl_from!(BSplineSurface<Point3>, Plane);
fn parabola_surfaces() -> (AlternativeSurface, AlternativeSurface) {
// define surfaces
#[rustfmt::skip]
let ctrl0 = vec![
vec![Point3::new(-1.0, -1.0, 3.0), Point3::new(-1.0, 0.0, -1.0), Point3::new(-1.0, 1.0, 3.0)],
vec![Point3::new(0.0, -1.0, -1.0), Point3::new(0.0, 0.0, -5.0), Point3::new(0.0, 1.0, -1.0)],
vec![Point3::new(1.0, -1.0, 3.0), Point3::new(1.0, 0.0, -1.0), Point3::new(1.0, 1.0, 3.0)],
];
#[rustfmt::skip]
let ctrl1 = vec![
vec![Point3::new(-1.0, -1.0, -3.0), Point3::new(-1.0, 0.0, 1.0), Point3::new(-1.0, 1.0, -3.0)],
vec![Point3::new(0.0, -1.0, 1.0), Point3::new(0.0, 0.0, 5.0), Point3::new(0.0, 1.0, 1.0)],
vec![Point3::new(1.0, -1.0, -3.0), Point3::new(1.0, 0.0, 1.0), Point3::new(1.0, 1.0, -3.0)],
];
(
BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl0).into(),
BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl1).into(),
)
}
#[test]
fn independent_intersection() {
// prepare geoetries
let arc00: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 0.0),
Vector4::new(-1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc01: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(-1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, -1.0, 0.0, 0.0),
Vector4::new(1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc10: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, -1.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 0.0),
Vector4::new(-1.0, 0.0, -1.0, 1.0),
],
))
.into();
let arc11: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(-1.0, 0.0, -1.0, 1.0),
Vector4::new(0.0, -1.0, 0.0, 0.0),
Vector4::new(1.0, 0.0, -1.0, 1.0),
],
))
.into();
let (surface0, surface1) = parabola_surfaces();
let plane0: AlternativeSurface = Plane::new(
Point3::new(0.0, 0.0, 1.0),
Point3::new(1.0, 0.0, 1.0),
Point3::new(0.0, 1.0, 1.0),
)
.into();
let plane1: AlternativeSurface = Plane::new(
Point3::new(0.0, 0.0, -1.0),
Point3::new(1.0, 0.0, -1.0),
Point3::new(0.0, 1.0, -1.0),
)
.into();
// prepare topologies
let v00 = Vertex::new(Point3::new(1.0, 0.0, 1.0));
let v01 = Vertex::new(Point3::new(-1.0, 0.0, 1.0));
let v10 = Vertex::new(Point3::new(1.0, 0.0, -1.0));
let v11 = Vertex::new(Point3::new(-1.0, 0.0, -1.0));
let wire0: Wire<_, _> = vec![Edge::new(&v00, &v01, arc00), Edge::new(&v01, &v00, arc01)].into();
let wire1: Wire<_, _> = vec![Edge::new(&v10, &v11, arc10), Edge::new(&v11, &v10, arc11)].into();
let shell0: Shell<_, _, _> = vec![
Face::new(vec![wire0.clone()], plane0),
Face::new(vec![wire0], surface0).inverse(),
]
.into();
assert_eq!(shell0.shell_condition(), ShellCondition::Closed);
let shell1: Shell<_, _, _> = vec![
Face::new(vec![wire1.clone()], plane1).inverse(),
Face::new(vec![wire1], surface1),
]
.into();
assert_eq!(shell1.shell_condition(), ShellCondition::Closed);
let poly_shell0 = shell0.triangulation(TOL);
let poly_shell1 = shell1.triangulation(TOL);
let LoopsStoreQuadruple {
geom_loops_store0: loops_store0,
geom_loops_store1: loops_store1,
..
} = create_loops_stores(&shell0, &poly_shell0, &shell1, &poly_shell1, TOL, &mut Default::default()).unwrap();
let [and0, or0, unknown0] = divide_faces(&shell0, &loops_store0, TOL, &mut Default::default())
.unwrap()
.and_or_unknown();
let [and1, or1, unknown1] = divide_faces(&shell1, &loops_store1, TOL, &mut Default::default())
.unwrap()
.and_or_unknown();
assert_eq!(and0.len(), 1);
assert_eq!(or0.len(), 1);
assert_eq!(unknown0.len(), 1);
assert_eq!(and1.len(), 1);
assert_eq!(or1.len(), 1);
assert_eq!(unknown1.len(), 1);
match unknown0[0].surface() {
AlternativeSurface::FirstType(_) => panic!("This face must plane!"),
AlternativeSurface::SecondType(_) => {}
}
match unknown1[0].surface() {
AlternativeSurface::FirstType(_) => panic!("This face must plane!"),
AlternativeSurface::SecondType(_) => {}
}
let and_shell: Shell<_, _, _> = vec![and0[0].clone(), and1[0].clone()].into();
assert_eq!(and_shell.shell_condition(), ShellCondition::Closed);
}
@@ -1,74 +0,0 @@
use super::loops_store::ShapesOpStatus;
use rustc_hash::FxHashMap as HashMap;
use truck_topology::*;
#[derive(Clone, Debug)]
pub struct FacesClassification<P, C, S> {
shell: Shell<P, C, S>,
status: HashMap<FaceID<S>, ShapesOpStatus>,
}
impl<P, C, S> Default for FacesClassification<P, C, S> {
fn default() -> Self {
Self {
shell: Default::default(),
status: HashMap::default(),
}
}
}
impl<P, C, S> FacesClassification<P, C, S> {
pub fn push(&mut self, face: Face<P, C, S>, status: ShapesOpStatus) {
self.status.insert(face.id(), status);
self.shell.push(face);
}
pub fn and_or_unknown(&self) -> [Shell<P, C, S>; 3] {
let [mut and, mut or, mut unknown] = <[Shell<P, C, S>; 3]>::default();
for face in &self.shell {
// SolidCraft: no panics (wasm can't catch them).
match self.status.get(&face.id()) {
Some(ShapesOpStatus::And) => and.push(face.clone()),
Some(ShapesOpStatus::Or) => or.push(face.clone()),
_ => unknown.push(face.clone()),
}
}
[and, or, unknown]
}
pub fn integrate_by_component(&mut self) {
let [and, or, unknown] = self.and_or_unknown();
let and_boundary = and.extract_boundaries();
let or_boundary = or.extract_boundaries();
// SolidCraft: `connected_components` walks a map keyed by face pointers, so each
// component's face order (and with it which boundary edge comes first) followed the
// heap addresses: the same Boolean could classify a component differently from run to
// run. Put each component's faces back in the shell's order first.
let position: HashMap<_, usize> = unknown.face_iter().enumerate().map(|(i, face)| (face.id(), i)).collect();
let components = unknown.connected_components();
for comp in components {
let mut faces: Vec<_> = comp.face_iter().cloned().collect();
faces.sort_by_key(|face| position.get(&face.id()).copied().unwrap_or(usize::MAX));
let comp: Shell<P, C, S> = faces.into_iter().collect();
let boundary = comp.extract_boundaries();
let Some(first) = boundary.first().and_then(|w| w.edge_iter().next()).map(|e| e.id()) else { continue };
let set = if and_boundary.iter().flatten().any(|edge| edge.id() == first) {
Some(ShapesOpStatus::And)
} else if or_boundary.iter().flatten().any(|edge| edge.id() == first) {
Some(ShapesOpStatus::Or)
} else {
None
};
if let Some(st) = set {
comp.iter().for_each(|face| {
if let Some(x) = self.status.get_mut(&face.id()) {
*x = st;
}
})
}
}
}
}
#[cfg(test)]
mod tests;
@@ -1,131 +0,0 @@
use super::{
super::{divide_face, loops_store},
*,
};
use truck_geometry::prelude::*;
use truck_meshalgo::prelude::*;
use truck_topology::{shell::ShellCondition, Vertex};
const TOL: f64 = 0.05;
type AlternativeIntersection = crate::alternative::Alternative<
NurbsCurve<Vector4>,
IntersectionCurve<PolylineCurve<Point3>, AlternativeSurface>,
>;
type AlternativeSurface = crate::alternative::Alternative<BSplineSurface<Point3>, Plane>;
fn parabola_surfaces() -> (AlternativeSurface, AlternativeSurface) {
// define surfaces
#[rustfmt::skip]
let ctrl0 = vec![
vec![Point3::new(-1.0, -1.0, 3.0), Point3::new(-1.0, 0.0, -1.0), Point3::new(-1.0, 1.0, 3.0)],
vec![Point3::new(0.0, -1.0, -1.0), Point3::new(0.0, 0.0, -5.0), Point3::new(0.0, 1.0, -1.0)],
vec![Point3::new(1.0, -1.0, 3.0), Point3::new(1.0, 0.0, -1.0), Point3::new(1.0, 1.0, 3.0)],
];
#[rustfmt::skip]
let ctrl1 = vec![
vec![Point3::new(-1.0, -1.0, -3.0), Point3::new(-1.0, 0.0, 1.0), Point3::new(-1.0, 1.0, -3.0)],
vec![Point3::new(0.0, -1.0, 1.0), Point3::new(0.0, 0.0, 5.0), Point3::new(0.0, 1.0, 1.0)],
vec![Point3::new(1.0, -1.0, -3.0), Point3::new(1.0, 0.0, 1.0), Point3::new(1.0, 1.0, -3.0)],
];
(
BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl0).into(),
BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl1).into(),
)
}
#[test]
fn independent_intersection() {
// prepare geoetries
let arc00: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 0.0),
Vector4::new(-1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc01: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(-1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, -1.0, 0.0, 0.0),
Vector4::new(1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc10: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, -1.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 0.0),
Vector4::new(-1.0, 0.0, -1.0, 1.0),
],
))
.into();
let arc11: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(-1.0, 0.0, -1.0, 1.0),
Vector4::new(0.0, -1.0, 0.0, 0.0),
Vector4::new(1.0, 0.0, -1.0, 1.0),
],
))
.into();
let (surface0, surface1) = parabola_surfaces();
let plane0: AlternativeSurface = Plane::new(
Point3::new(0.0, 0.0, 1.0),
Point3::new(1.0, 0.0, 1.0),
Point3::new(0.0, 1.0, 1.0),
)
.into();
let plane1: AlternativeSurface = Plane::new(
Point3::new(0.0, 0.0, -1.0),
Point3::new(1.0, 0.0, -1.0),
Point3::new(0.0, 1.0, -1.0),
)
.into();
// prepare topologies
let v00 = Vertex::new(Point3::new(1.0, 0.0, 1.0));
let v01 = Vertex::new(Point3::new(-1.0, 0.0, 1.0));
let v10 = Vertex::new(Point3::new(1.0, 0.0, -1.0));
let v11 = Vertex::new(Point3::new(-1.0, 0.0, -1.0));
let wire0: Wire<_, _> = vec![Edge::new(&v00, &v01, arc00), Edge::new(&v01, &v00, arc01)].into();
let wire1: Wire<_, _> = vec![Edge::new(&v10, &v11, arc10), Edge::new(&v11, &v10, arc11)].into();
let shell0: Shell<_, _, _> = vec![
Face::new(vec![wire0.clone()], plane0),
Face::new(vec![wire0], surface0).inverse(),
]
.into();
assert_eq!(shell0.shell_condition(), ShellCondition::Closed);
let shell1: Shell<_, _, _> = vec![
Face::new(vec![wire1.clone()], plane1).inverse(),
Face::new(vec![wire1], surface1),
]
.into();
assert_eq!(shell1.shell_condition(), ShellCondition::Closed);
let poly_shell0 = shell0.triangulation(TOL);
let poly_shell1 = shell1.triangulation(TOL);
let loops_store::LoopsStoreQuadruple {
geom_loops_store0: loops_store0,
geom_loops_store1: loops_store1,
..
} = loops_store::create_loops_stores(&shell0, &poly_shell0, &shell1, &poly_shell1, TOL, &mut Default::default())
.unwrap();
let mut cls0 = divide_face::divide_faces(&shell0, &loops_store0, TOL, &mut Default::default()).unwrap();
cls0.integrate_by_component();
let mut cls1 = divide_face::divide_faces(&shell1, &loops_store1, TOL, &mut Default::default()).unwrap();
cls1.integrate_by_component();
let [mut and, mut or, _] = cls0.and_or_unknown();
let [and1, or1, _] = cls1.and_or_unknown();
and.extend(and1);
or.extend(or1);
assert_eq!(and.len(), 2);
assert_eq!(or.len(), 4);
assert_eq!(and.shell_condition(), ShellCondition::Closed);
assert_eq!(or.shell_condition(), ShellCondition::Closed);
}
-133
View File
@@ -1,133 +0,0 @@
//! SolidCraft: where each face and edge of a boolean's result came from, recorded by the code
//! that builds them (never reconstructed from geometry).
//!
//! Topology ids are addresses of shared data, so they identify a face or an edge only while it
//! lives. [`Lineage`] therefore stores, for every face and edge an operation creates, its origin
//! in terms of the operands at the moment it is created (while its parent is alive), and never a
//! chain of ids: a dead entity's address that is reused is overwritten when the new entity is
//! recorded.
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use truck_topology::*;
/// A face of one of a boolean's two operands.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FaceIndex {
/// 0 for the first operand, 1 for the second.
pub operand: usize,
/// Position in that operand's `face_iter` order.
pub face: usize,
}
/// Where a face of a boolean's result lies. Every result face is a region of exactly one
/// operand face (a boolean makes no face of its own).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FaceOrigin {
/// The operand face it is a region of (same surface, same orientation).
pub source: FaceIndex,
/// `true` when it is that very face, passed through untouched (no intersection reached
/// it); `false` for a region rebuilt from it by the face division.
pub kept: bool,
}
/// Where an edge of a boolean's result came from.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum EdgeOrigin {
/// An operand edge or a piece of it.
Edge {
/// 0 for the first operand, 1 for the second.
operand: usize,
/// Position in that operand's edge order (see [`History`]).
edge: usize,
},
/// The intersection of two operand faces, or a piece of it (`a < b`).
Meet {
/// The lesser of the two faces.
a: FaceIndex,
/// The greater of the two faces.
b: FaceIndex,
},
}
/// History of a boolean's result: one entry per face in its `face_iter` order and one per edge
/// in its edge order. A solid's edge order lists its distinct edges in order of first use,
/// walking `face_iter` and each face's `edge_iter` (that is, `Solid::edge_iter` without repeats).
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct History {
/// Origin of each result face.
pub faces: Vec<FaceOrigin>,
/// Origin of each result edge.
pub edges: Vec<EdgeOrigin>,
}
/// The ids of `solid`'s edges in its edge order (see [`History`]).
fn edge_order<P, C, S>(solid: &Solid<P, C, S>) -> Vec<EdgeID<C>> {
let mut seen = HashSet::default();
solid.edge_iter().map(|e| e.id()).filter(|id| seen.insert(*id)).collect()
}
/// Records `origin` under `key`, or, when the origin is unknown, leaves nothing stale under the
/// new entity's address.
fn set<K: std::hash::Hash + Eq, V>(map: &mut HashMap<K, V>, key: K, origin: Option<V>) {
match origin {
Some(o) => map.insert(key, o),
None => map.remove(&key),
};
}
/// Origins, in operand terms, of the faces and edges alive during one boolean.
///
/// The operands must not share faces or edges (a shared one is attributed to the second).
#[derive(Debug)]
pub(crate) struct Lineage<C, S> {
faces: HashMap<FaceID<S>, FaceOrigin>,
edges: HashMap<EdgeID<C>, EdgeOrigin>,
}
impl<C, S> Default for Lineage<C, S> {
fn default() -> Self { Self { faces: HashMap::default(), edges: HashMap::default() } }
}
impl<C, S> Lineage<C, S> {
/// Every face and edge of the operands, as itself.
pub fn new<P>(operands: [&Solid<P, C, S>; 2]) -> Self {
let mut lineage = Self::default();
for (operand, solid) in operands.into_iter().enumerate() {
for (face, f) in solid.face_iter().enumerate() {
let source = FaceIndex { operand, face };
lineage.faces.insert(f.id(), FaceOrigin { source, kept: true });
}
for (edge, id) in edge_order(solid).into_iter().enumerate() {
lineage.edges.insert(id, EdgeOrigin::Edge { operand, edge });
}
}
lineage
}
/// `piece` is a region trimmed out of the live face `parent`.
pub fn face_piece(&mut self, parent: FaceID<S>, piece: FaceID<S>) {
let origin = self.faces.get(&parent).map(|&o| FaceOrigin { kept: false, ..o });
set(&mut self.faces, piece, origin);
}
/// `piece` is the live edge `parent` or a piece of it.
pub fn edge_piece(&mut self, parent: EdgeID<C>, piece: EdgeID<C>) {
let origin = self.edges.get(&parent).copied();
set(&mut self.edges, piece, origin);
}
/// `edge` is (part of) the intersection of the live faces `faces`.
pub fn meet(&mut self, edge: EdgeID<C>, faces: [FaceID<S>; 2]) {
let [a, b] = faces.map(|f| self.faces.get(&f).map(|o| o.source));
let origin = a.zip(b).map(|(a, b)| EdgeOrigin::Meet { a: a.min(b), b: a.max(b) });
set(&mut self.edges, edge, origin);
}
/// The history of `solid`, made from the operands by the recorded steps; `None` when one
/// of its faces or edges was not recorded.
pub fn history<P>(&self, solid: &Solid<P, C, S>) -> Option<History> {
let faces = solid.face_iter().map(|f| self.faces.get(&f.id()).copied()).collect::<Option<_>>();
let edges = edge_order(solid).into_iter().map(|id| self.edges.get(&id).copied()).collect::<Option<_>>();
Some(History { faces: crate::tr!(faces, "face history"), edges: crate::tr!(edges, "edge history") })
}
}
-243
View File
@@ -1,243 +0,0 @@
use super::history::{History, Lineage};
use super::*;
use truck_geometry::prelude::IntersectionCurve;
use truck_meshalgo::prelude::*;
use truck_topology::*;
/// Only solids consisting of faces whose surface is implemented this trait can be used for set operations.
pub trait ShapeOpsSurface:
ParametricSurface3D
+ ParameterDivision2D
+ SearchParameter<D2, Point = Point3>
+ SearchNearestParameter<D2, Point = Point3>
+ Invertible
+ Send
+ Sync {
}
impl<S> ShapeOpsSurface for S where S: ParametricSurface3D
+ ParameterDivision2D
+ SearchParameter<D2, Point = Point3>
+ SearchNearestParameter<D2, Point = Point3>
+ Invertible
+ Send
+ Sync
{
}
/// Only solids consisting of edges whose curve is implemented this trait can be used for set operations.
pub trait ShapeOpsCurve<S: ShapeOpsSurface>:
ParametricCurve3D
+ TrySubs
+ ParameterDivision1D<Point = Point3>
+ Cut
+ Invertible
+ From<IntersectionCurve<PolylineCurve<Point3>, S>>
+ SearchParameter<D1, Point = Point3>
+ SearchNearestParameter<D1, Point = Point3>
+ Send
+ Sync {
}
impl<C, S: ShapeOpsSurface> ShapeOpsCurve<S> for C where C: ParametricCurve3D
+ TrySubs
+ ParameterDivision1D<Point = Point3>
+ Cut
+ Invertible
+ From<IntersectionCurve<PolylineCurve<Point3>, S>>
+ SearchParameter<D1, Point = Point3>
+ SearchNearestParameter<D1, Point = Point3>
+ Send
+ Sync
{
}
/// SolidCraft: the meshed form of a shell that a set operation works on (see [`meshes`]).
pub type MeshedShell = Shell<Point3, PolylineCurve<Point3>, Option<PolygonMesh>>;
/// SolidCraft: the meshes of `solid`'s shells at `tol`, as a set operation makes them: an
/// operand used in many operations at one tolerance (a large body cut down to many small
/// regions) is meshed once and handed to [`and_with_history_meshed`], which is most of what
/// such an operation costs. They fit any copy of `solid` with its faces in the same order.
pub fn meshes<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(solid: &Solid<Point3, C, S>, tol: f64) -> Vec<MeshedShell> {
solid.boundaries().iter().map(|shell| shell.robust_triangulation(tol)).collect()
}
/// SolidCraft: whether `shell` faces inward (a solid's void, a cut's inverted tool), by the sign
/// of its volume with each face counted as it is oriented (a face's mesh is its surface's,
/// whatever the face's orientation).
fn faces_inward(shell: &MeshedShell) -> Option<bool> {
let mut volume = 0.0;
for face in shell.face_iter() {
let v = face.surface()?.volume();
volume += if face.orientation() { v } else { -v };
}
Some(volume < 0.0)
}
/// SolidCraft: whether `pt` is inside the closed `shell`, by the faces a ray from it crosses.
/// For an AND (`and`) each face counts as it is oriented, and a shell facing inward (`inward`,
/// see [`faces_inward`]) holds what lies outside it: the crossings count 0 there and -1 inside.
/// An OR keeps truck's count, by the faces' surfaces alone: it folds a solid's voids in as if
/// they were solids of their own, which is no union with voids whichever way they are counted.
fn inside_shell(shell: &MeshedShell, pt: Point3, and: bool, inward: bool) -> Option<bool> {
let dir = hash::take_one_unit(pt);
let mut count = 0;
for face in shell.face_iter() {
let sign = if face.orientation() || !and { 1 } else { -1 };
count += sign * face.surface()?.signed_crossing_faces(pt, dir);
}
Some(if inward { count >= 0 } else { count >= 1 })
}
fn process_one_pair_of_shells<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
shell0: &Shell<Point3, C, S>,
meshed0: Option<&MeshedShell>,
shell1: &Shell<Point3, C, S>,
tol: f64,
lineage: &mut Lineage<C, S>,
and: bool,
) -> Option<[Shell<Point3, C, S>; 2]> {
nonpositive_tolerance!(tol);
// SolidCraft: robust meshing (boundaries from earlier operations lie on surfaces only
// within tolerance). A mesh made before is copied: the loops stores cut its edges.
let poly_shell0 = match meshed0 {
Some(m) if m.len() == shell0.len() => m.mapped(|p| *p, |c| c.clone(), |s| s.clone()),
_ => shell0.robust_triangulation(tol),
};
let poly_shell1 = shell1.robust_triangulation(tol);
let loops_store::LoopsStoreQuadruple {
geom_loops_store0: loops_store0,
geom_loops_store1: loops_store1,
..
} = crate::tr!(loops_store::create_loops_stores(shell0, &poly_shell0, shell1, &poly_shell1, tol, lineage), "loops stores");
let mut cls0 = crate::tr!(divide_face::divide_faces(shell0, &loops_store0, tol, lineage), "divide faces 0");
cls0.integrate_by_component();
let mut cls1 = crate::tr!(divide_face::divide_faces(shell1, &loops_store1, tol, lineage), "divide faces 1");
cls1.integrate_by_component();
let [mut and0, mut or0, unknown0] = cls0.and_or_unknown();
let inward1 = and && !unknown0.is_empty() && faces_inward(&poly_shell1)?;
unknown0.into_iter().try_for_each(|face| {
// SolidCraft: no panics (wasm can't catch them).
let pt = face.boundaries().first()?.vertex_iter().next()?.point();
if inside_shell(&poly_shell1, pt, and, inward1)? {
and0.push(face);
} else {
or0.push(face);
}
Some(())
})?;
let [mut and1, mut or1, unknown1] = cls1.and_or_unknown();
let inward0 = and && !unknown1.is_empty() && faces_inward(&poly_shell0)?;
unknown1.into_iter().try_for_each(|face| {
// SolidCraft: no panics (wasm can't catch them).
let pt = face.boundaries().first()?.vertex_iter().next()?.point();
if inside_shell(&poly_shell0, pt, and, inward0)? {
and1.push(face);
} else {
or1.push(face);
}
Some(())
})?;
and0.append(&mut and1);
or0.append(&mut or1);
Some([and0, or0])
}
/// AND operation between two solids.
pub fn and<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid0: &Solid<Point3, C, S>,
solid1: &Solid<Point3, C, S>,
tol: f64,
) -> Option<Solid<Point3, C, S>> {
boolean(solid0, None, solid1, tol, true).map(|(solid, _)| solid)
}
/// OR operation between two solids.
pub fn or<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid0: &Solid<Point3, C, S>,
solid1: &Solid<Point3, C, S>,
tol: f64,
) -> Option<Solid<Point3, C, S>> {
boolean(solid0, None, solid1, tol, false).map(|(solid, _)| solid)
}
/// SolidCraft: AND operation between two solids, with where each face and edge of the result
/// came from (operand 0 is `solid0`, operand 1 is `solid1`): the same result as [`and`], and
/// its history, `None` when a face or edge of it was not recorded. The operands must not share
/// faces or edges.
pub fn and_with_history<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid0: &Solid<Point3, C, S>,
solid1: &Solid<Point3, C, S>,
tol: f64,
) -> Option<(Solid<Point3, C, S>, Option<History>)> {
boolean(solid0, None, solid1, tol, true)
}
/// SolidCraft: [`and_with_history`] with `solid0` meshed already ([`meshes`] of it, or of a
/// copy of it, at `tol`): the same result.
pub fn and_with_history_meshed<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid0: &Solid<Point3, C, S>,
meshed0: &[MeshedShell],
solid1: &Solid<Point3, C, S>,
tol: f64,
) -> Option<(Solid<Point3, C, S>, Option<History>)> {
boolean(solid0, Some(meshed0), solid1, tol, true)
}
/// SolidCraft: OR operation between two solids, with where each face and edge of the result
/// came from (operand 0 is `solid0`, operand 1 is `solid1`): the same result as [`or`], and
/// its history, `None` when a face or edge of it was not recorded. The operands must not share
/// faces or edges.
pub fn or_with_history<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid0: &Solid<Point3, C, S>,
solid1: &Solid<Point3, C, S>,
tol: f64,
) -> Option<(Solid<Point3, C, S>, Option<History>)> {
boolean(solid0, None, solid1, tol, false)
}
/// Combines the operands' first shells, then folds in their other shells one at a time
/// (`and` chooses the AND or the OR shell of each pass). The history is `None` when a face or
/// edge of the result was not recorded; the result stands without it.
fn boolean<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid0: &Solid<Point3, C, S>,
meshed0: Option<&[MeshedShell]>,
solid1: &Solid<Point3, C, S>,
tol: f64,
and: bool,
) -> Option<(Solid<Point3, C, S>, Option<History>)> {
let pick = |[and_shell, or_shell]: [Shell<Point3, C, S>; 2]| if and { and_shell } else { or_shell };
let mut lineage = Lineage::new([solid0, solid1]);
let mut iter0 = solid0.boundaries().iter();
let mut iter1 = solid1.boundaries().iter();
let shell0 = iter0.next()?;
let shell1 = iter1.next()?;
let meshed0 = meshed0.filter(|m| m.len() == solid0.boundaries().len()).and_then(|m| m.first());
let mut shell = pick(crate::tr!(process_one_pair_of_shells(shell0, meshed0, shell1, tol, &mut lineage, and), "first shells"));
for other in iter0.chain(iter1) {
shell = pick(crate::tr!(process_one_pair_of_shells(&shell, None, other, tol, &mut lineage, and), "another shell"));
}
let solid = crate::tr!(Solid::try_new(components_in_order(&shell)).ok(), "result solid");
let history = lineage.history(&solid);
Some((solid, history))
}
// SolidCraft: truck's connected components come out in hash order of face addresses, so the
// result's face order changed from run to run. Keep the shell's own order instead.
fn components_in_order<C, S>(shell: &Shell<Point3, C, S>) -> Vec<Shell<Point3, C, S>> {
let pos: rustc_hash::FxHashMap<_, usize> = shell.face_iter().enumerate().map(|(i, f)| (f.id(), i)).collect();
let key = |f: &Face<Point3, C, S>| pos.get(&f.id()).copied().unwrap_or(usize::MAX);
let mut comps: Vec<Vec<Face<Point3, C, S>>> = shell
.connected_components()
.into_iter()
.map(|c| {
let mut faces: Vec<_> = c.into_iter().collect();
faces.sort_by_key(|f| key(f));
faces
})
.collect();
comps.sort_by_key(|c| c.first().map(|f| key(f)).unwrap_or(usize::MAX));
comps.into_iter().map(Shell::from).collect()
}
#[cfg(test)]
mod tests;
-153
View File
@@ -1,153 +0,0 @@
use truck_meshalgo::prelude::*;
use truck_modeling::*;
#[test]
fn punched_cube() {
let v = builder::vertex(Point3::origin());
let e = builder::tsweep(&v, Vector3::unit_x());
let f = builder::tsweep(&e, Vector3::unit_y());
let cube = builder::tsweep(&f, Vector3::unit_z());
let v = builder::vertex(Point3::new(0.5, 0.25, -0.5));
let w = builder::rsweep(&v, Point3::new(0.5, 0.5, 0.0), Vector3::unit_z(), Rad(7.0));
let f = builder::try_attach_plane(&[w]).unwrap();
let mut cylinder = builder::tsweep(&f, Vector3::unit_z() * 2.0);
cylinder.not();
let and = crate::and(&cube, &cylinder, 0.05).unwrap();
let poly = and.triangulation(0.01).to_polygon();
let file = std::fs::File::create("punched-cube.obj").unwrap();
obj::write(&poly, file).unwrap();
}
// SolidCraft: boolean history.
use crate::{EdgeOrigin, FaceIndex, History};
fn cuboid(origin: Point3, size: Vector3) -> Solid {
let v = builder::vertex(origin);
let e = builder::tsweep(&v, size.x * Vector3::unit_x());
let f = builder::tsweep(&e, size.y * Vector3::unit_y());
builder::tsweep(&f, size.z * Vector3::unit_z())
}
/// Distinct edges in order of first use along the faces.
fn edges_in_order(solid: &Solid) -> Vec<Edge> {
let mut seen = std::collections::HashSet::new();
solid.face_iter().flat_map(Face::edge_iter).filter(|e| seen.insert(e.id())).collect()
}
fn on_surface(surface: &Surface, pt: Point3, tol: f64) -> bool {
surface.search_nearest_parameter(pt, None, 100).is_some_and(|(u, v)| surface.subs(u, v).distance(pt) < tol)
}
fn on_curve(curve: &Curve, pt: Point3, tol: f64) -> bool {
curve.search_nearest_parameter(pt, None, 100).is_some_and(|t| curve.subs(t).distance(pt) < tol)
}
/// Checks `history` against the geometry of `result` and the operands; returns how many result
/// faces are kept operand faces and the intersection edges.
fn check(result: &Solid, operands: [&Solid; 2], history: &History, tol: f64) -> (usize, Vec<[FaceIndex; 2]>) {
let operand_faces = operands.map(|s| s.face_iter().collect::<Vec<_>>());
let face = |f: FaceIndex| operand_faces[f.operand][f.face];
assert_eq!(history.faces.len(), result.face_iter().count());
for (out, origin) in result.face_iter().zip(&history.faces) {
let src = face(origin.source);
assert_eq!(out.orientation(), src.orientation());
assert_eq!(out.id() == src.id(), origin.kept);
assert!(out.vertex_iter().all(|v| on_surface(&src.surface(), v.point(), tol)));
}
let edges = edges_in_order(result);
assert_eq!(history.edges.len(), edges.len());
let operand_edges = operands.map(edges_in_order);
let mut meets = Vec::new();
for (out, origin) in edges.iter().zip(&history.edges) {
let ends = [out.front().point(), out.back().point()];
match *origin {
EdgeOrigin::Edge { operand, edge } => {
let src = &operand_edges[operand][edge];
assert!(ends.iter().all(|&p| on_curve(&src.curve(), p, tol)));
}
EdgeOrigin::Meet { a, b } => {
assert!(a < b);
for f in [a, b] {
assert!(ends.iter().all(|&p| on_surface(&face(f).surface(), p, tol)));
}
meets.push([a, b]);
}
}
}
(history.faces.iter().filter(|o| o.kept).count(), meets)
}
#[test]
fn box_union_box_history() {
let a = cuboid(Point3::origin(), Vector3::new(1.0, 1.0, 1.0));
let b = cuboid(Point3::new(0.5, 0.25, 0.25), Vector3::new(1.0, 0.5, 0.5));
let (or, history) = crate::or_with_history(&a, &b, 0.05).unwrap();
let history = history.unwrap();
let (kept, meets) = check(&or, [&a, &b], &history, 0.05);
// a's five faces away from b and b's far end are kept; a's face at x = 1 gets a hole and
// b's four sides are trimmed where they enter a.
assert_eq!(history.faces.len(), 11);
assert_eq!(kept, 6);
let pieces: Vec<_> = history.faces.iter().filter(|o| !o.kept).map(|o| o.source).collect();
assert_eq!(pieces.iter().filter(|f| f.operand == 0).count(), 1);
assert_eq!(pieces.iter().filter(|f| f.operand == 1).count(), 4);
// b's sides meet a's face at x = 1 along four new edges.
assert_eq!(meets.len(), 4);
assert!(meets.iter().all(|[x, y]| x.operand == 0 && y.operand == 1 && *x == pieces[0]));
// a's 12 edges, b's 4 far-end edges and the 4 trimmed edges along x.
assert_eq!(history.edges.len(), 24);
let from_b = history.edges.iter().filter(|o| matches!(o, EdgeOrigin::Edge { operand: 1, .. })).count();
assert_eq!(from_b, 8);
}
#[test]
fn box_minus_cylinder_history() {
let cube = cuboid(Point3::origin(), Vector3::new(1.0, 1.0, 1.0));
let v = builder::vertex(Point3::new(0.5, 0.25, -0.5));
let w = builder::rsweep(&v, Point3::new(0.5, 0.5, 0.0), Vector3::unit_z(), Rad(7.0));
let f = builder::try_attach_plane(&[w]).unwrap();
let mut cylinder = builder::tsweep(&f, Vector3::unit_z() * 2.0);
cylinder.not();
let (cut, history) = crate::and_with_history(&cube, &cylinder, 0.05).unwrap();
let history = history.unwrap();
let (kept, meets) = check(&cut, [&cube, &cylinder], &history, 0.05);
// The cube's four sides are kept; its top and bottom get holes; only the cylinder's wall
// remains of the tool, trimmed to the cube.
assert_eq!(kept, 4);
assert!(history.faces.iter().filter(|o| o.source.operand == 1).all(|o| !o.kept));
// The holes' rims: the wall meets the top and the bottom.
assert!(!meets.is_empty());
assert!(meets.iter().all(|[x, y]| x.operand == 0 && y.operand == 1));
let rims: std::collections::BTreeSet<_> = meets.iter().map(|[x, _]| *x).collect();
assert_eq!(rims.len(), 2);
}
/// A cube [0, 3]³ with a cubic void [1, 2]³: two shells, the void's faces after the outer ones.
fn hollow_cube() -> Solid {
let mut void = cuboid(Point3::new(1.0, 1.0, 1.0), Vector3::new(1.0, 1.0, 1.0));
void.not();
let outer = cuboid(Point3::origin(), Vector3::new(3.0, 3.0, 3.0));
Solid::new(outer.into_boundaries().into_iter().chain(void.into_boundaries()).collect())
}
#[test]
fn multi_shell_operand_history() {
let hollow = hollow_cube();
// Covers the upper half of the hollow cube: its bottom crosses both shells.
let slab = cuboid(Point3::new(-0.5, -0.5, 1.5), Vector3::new(4.0, 4.0, 2.5));
let (and, history) = crate::and_with_history(&hollow, &slab, 0.05).unwrap();
let history = history.unwrap();
let (kept, meets) = check(&and, [&hollow, &slab], &history, 0.05);
// The outer top and the void's top (numbered after the outer shell's six faces) are kept.
assert_eq!(kept, 2);
assert!(history.faces.iter().any(|o| o.kept && o.source.operand == 0 && o.source.face >= 6));
// The slab's bottom meets the four outer sides and the void's four sides.
let outer = meets.iter().filter(|[x, _]| x.operand == 0 && x.face < 6).count();
let void = meets.iter().filter(|[x, _]| x.operand == 0 && x.face >= 6).count();
assert_eq!((outer, void), (4, 4), "{meets:?}");
let slab_faces: std::collections::BTreeSet<_> = meets.iter().map(|[_, y]| *y).collect();
assert_eq!(slab_faces.len(), 1);
}
@@ -1,183 +0,0 @@
use truck_base::cgmath64::*;
use truck_geometry::prelude::*;
use truck_meshalgo::prelude::*;
/// polyline base ntersection curve with parameter
#[derive(Debug, Clone, derive_more::Deref, derive_more::DerefMut)]
pub struct IntersectionCurveWithParameters<S> {
#[deref]
#[deref_mut]
ic: IntersectionCurve<PolylineCurve<Point3>, S>,
params0: PolylineCurve<Point2>,
params1: PolylineCurve<Point2>,
}
impl<S> From<IntersectionCurveWithParameters<S>> for IntersectionCurve<PolylineCurve<Point3>, S> {
fn from(a: IntersectionCurveWithParameters<S>) -> Self { a.ic }
}
impl<S> IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
/// SolidCraft: construct a projected curve, returning `None` for degenerate input.
pub fn try_new(
surface0: S,
surface1: S,
poly: PolylineCurve<Point3>,
tol: f64,
) -> Option<Self> {
// SolidCraft: a point contact is not a curve. Reject it before indexing the
// endpoints, and reject collapsed/non-finite segments before normalizing.
if poly.len() < 2 || poly.windows(2).any(|p| {
let length2 = (p[1] - p[0]).magnitude2();
!length2.is_finite() || length2 <= 0.0
}) {
return None;
}
let mut polyline = PolylineCurve(Vec::new());
let mut params0 = PolylineCurve(Vec::new());
let mut params1 = PolylineCurve(Vec::new());
for p in poly.windows(2) {
let n = (p[1] - p[0]).normalize();
let (q, p0, p1) = double_projection(&surface0, None, &surface1, None, p[0], n, 100)?;
polyline.push(q);
params0.push(p0);
params1.push(p1);
}
let (q, p0, p1) = if poly[0].near(&poly[poly.len() - 1]) {
(polyline[0], params0[0], params1[0])
} else {
let n = (poly[poly.len() - 1] - poly[poly.len() - 2]).normalize();
double_projection(
&surface0,
None,
&surface1,
None,
poly[poly.len() - 1],
n,
100,
)?
};
polyline.push(q);
params0.push(p0);
params1.push(p1);
Some(Self {
ic: IntersectionCurve::new_unchecked(
Box::new(surface0),
Box::new(surface1),
polyline,
tol,
),
params0,
params1,
})
}
}
impl<S> ParametricCurve for IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
type Point = Point3;
type Vector = Vector3;
#[inline(always)]
fn subs(&self, t: f64) -> Point3 { self.ic.subs(t) }
#[inline(always)]
fn der(&self, t: f64) -> Vector3 { self.ic.der(t) }
#[inline(always)]
fn der2(&self, t: f64) -> Vector3 { self.ic.der2(t) }
#[inline(always)]
fn parameter_range(&self) -> ParameterRange { self.ic.parameter_range() }
}
impl<S> BoundedCurve for IntersectionCurveWithParameters<S> where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3> {}
impl<S> ParameterDivision1D for IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
type Point = Point3;
#[inline(always)]
fn parameter_division(&self, range: (f64, f64), tol: f64) -> (Vec<f64>, Vec<Point3>) {
self.ic.parameter_division(range, tol)
}
}
impl<S> Cut for IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
#[inline(always)]
fn cut(&mut self, t: f64) -> Self {
Self {
ic: self.ic.cut(t),
params0: self.params0.cut(t),
params1: self.params1.cut(t),
}
}
}
impl<S: Clone> Invertible for IntersectionCurveWithParameters<S> {
fn invert(&mut self) {
self.ic.invert();
self.params0.invert();
self.params1.invert();
}
}
impl<S> SearchParameter<D1> for IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
type Point = Point3;
fn search_parameter<H: Into<SPHint1D>>(
&self,
point: Point3,
hint: H,
trials: usize,
) -> Option<f64> {
self.ic.search_parameter(point, hint, trials)
}
}
impl<S> SearchNearestParameter<D1> for IntersectionCurveWithParameters<S>
where S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>
{
type Point = Point3;
fn search_nearest_parameter<H: Into<SPHint1D>>(
&self,
point: Point3,
hint: H,
trials: usize,
) -> Option<f64> {
self.ic.search_nearest_parameter(point, hint, trials)
}
}
pub fn intersection_curves<S>(
surface0: S,
polygon0: &PolygonMesh,
surface1: S,
polygon1: &PolygonMesh,
tol: f64,
) -> Vec<(
PolylineCurve<Point3>,
Option<IntersectionCurveWithParameters<S>>,
)>
where
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
let interferences = polygon0.extract_interference(polygon1);
let polylines = super::polyline_construction::construct_polylines(&interferences);
polylines
.into_iter()
.map(|polyline| {
let curve = IntersectionCurveWithParameters::try_new(
surface0.clone(),
surface1.clone(),
polyline.clone(),
tol,
);
(polyline, curve)
})
.collect()
}
#[cfg(test)]
mod tests;
@@ -1,173 +0,0 @@
use super::*;
use std::f64::consts::PI;
mod double_projection_tests {
use super::*;
fn create_axis(n: Vector3) -> (Vector3, Vector3) {
let idx = if n[0].abs() < n[1].abs() { 0 } else { 1 };
let idx = if n[idx].abs() < n[2].abs() { idx } else { 2 };
let mut e = Vector3::zero();
e[idx] = 1.0;
let x = n.cross(e).normalize();
(x, n.cross(x))
}
fn exec_plane_case() {
let c0 = Point3::new(
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
);
let n0 = Vector3::new(
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
)
.normalize();
let (x, y) = create_axis(n0);
let plane0 = Plane::new(c0, c0 + x, c0 + y);
let c1 = Point3::new(
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
);
let n1 = Vector3::new(
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
2.0 * rand::random::<f64>() - 1.0,
)
.normalize();
let (x, y) = create_axis(n1);
let plane1 = Plane::new(c1, c1 + x, c1 + y);
let n = n0.cross(n1).normalize();
let mut o = None;
for i in 0..10 {
let t = i as f64;
let p = Point3::origin() + t * n;
let (q, p0, p1) = double_projection(&plane0, None, &plane1, None, p, n, 100)
.unwrap_or_else(|| panic!("plane0: {plane0:?}\nplane1: {plane1:?}\n p: {p:?}"));
assert_near!(q, plane0.subs(p0.x, p0.y));
assert_near!(q, plane1.subs(p1.x, p1.y));
if let Some(o) = o {
assert_near!(q.distance2(o), t * t);
} else {
o = Some(q);
}
}
}
#[test]
fn plane_case() { (0..100).for_each(|_| exec_plane_case()); }
#[test]
fn sphere_case() {
let sphere0 = Sphere::new(Point3::new(0.0, 0.0, 1.0), f64::sqrt(2.0));
let sphere1 = Sphere::new(Point3::new(0.0, 0.0, -1.0), f64::sqrt(2.0));
for _ in 0..100 {
let t = 2.0 * PI * rand::random::<f64>();
let r = 2.0 * rand::random::<f64>();
let p = Point3::new(r * f64::cos(t), r * f64::sin(t), 0.0);
let n = Vector3::new(-f64::sin(t), f64::cos(t), 0.0);
let (q, p0, p1) = double_projection(&sphere0, None, &sphere1, None, p, n, 100)
.unwrap_or_else(|| panic!("p: {p:?}"));
assert_near!(q, sphere0.subs(p0.x, p0.y));
assert_near!(q, sphere1.subs(p1.x, p1.y));
assert_near!(q, Point3::new(f64::cos(t), f64::sin(t), 0.0));
}
}
}
#[test]
fn intersection_curve_sphere_case() {
let sphere0 = Sphere::new(Point3::new(0.0, 0.0, 1.0), f64::sqrt(2.0));
let sphere1 = Sphere::new(Point3::new(0.0, 0.0, -1.0), f64::sqrt(2.0));
const M: usize = 5;
let polyline = (0..=M)
.map(|i| {
let t = 2.0 * PI * i as f64 / M as f64;
Point3::new(0.8 * f64::cos(t), 0.8 * f64::sin(t), 0.0)
})
.collect::<PolylineCurve<_>>();
let curve: IntersectionCurve<_, _> =
IntersectionCurveWithParameters::try_new(sphere0, sphere1, polyline, 0.5)
.unwrap()
.into();
const N: usize = 100;
let mut sum = 0.0;
let (t0, t1) = curve.range_tuple();
for i in 0..N {
let t = t0 + (t1 - t0) * i as f64 / N as f64;
let pt = curve.subs(t);
assert_near!(pt.distance(Point3::origin()), 1.0);
let vec = curve.der(t);
assert!(pt.dot(vec).so_small(), "{i} {t} {vec:?}");
assert!(vec[2].so_small());
sum += vec.magnitude() * (t1 - t0) / N as f64;
}
assert!(f64::abs(sum - 2.0 * PI) < 0.1, "{}", sum);
let theta = 2.0 * PI * rand::random::<f64>();
let pt = Point3::new(f64::cos(theta), f64::sin(theta), 0.0);
let t = curve.search_parameter(pt, None, 1).unwrap();
assert!(curve.subs(t).near(&pt));
let pt = Point3::new(1.1 * f64::cos(theta), 1.1 * f64::sin(theta), 0.0);
assert!(curve.search_parameter(pt, None, 1).is_none());
let mut curve0 = curve.clone();
let curve1 = curve0.cut(2.5);
assert_near!(curve0.front(), curve.front());
assert_near!(curve0.back(), curve.subs(2.5));
assert_near!(curve1.front(), curve.subs(2.5));
assert_near!(curve1.back(), curve.back());
let mut curve0 = curve.clone();
let curve1 = curve0.cut(2.0);
assert_near!(curve0.front(), curve.front());
assert_near!(curve0.back(), curve.subs(2.0));
assert_near!(curve1.front(), curve.subs(2.0));
assert_near!(curve1.back(), curve.back());
}
#[test]
fn collide_parabola() {
const TOL: f64 = 0.05;
// define surfaces
#[rustfmt::skip]
let ctrl0 = vec![
vec![Point3::new(-1.0, -1.0, 3.0), Point3::new(-1.0, 0.0, -1.0), Point3::new(-1.0, 1.0, 3.0)],
vec![Point3::new(0.0, -1.0, -1.0), Point3::new(0.0, 0.0, -5.0), Point3::new(0.0, 1.0, -1.0)],
vec![Point3::new(1.0, -1.0, 3.0), Point3::new(1.0, 0.0, -1.0), Point3::new(1.0, 1.0, 3.0)],
];
#[rustfmt::skip]
let ctrl1 = vec![
vec![Point3::new(-1.0, -1.0, -3.0), Point3::new(-1.0, 0.0, 1.0), Point3::new(-1.0, 1.0, -3.0)],
vec![Point3::new(0.0, -1.0, 1.0), Point3::new(0.0, 0.0, 5.0), Point3::new(0.0, 1.0, 1.0)],
vec![Point3::new(1.0, -1.0, -3.0), Point3::new(1.0, 0.0, 1.0), Point3::new(1.0, 1.0, -3.0)],
];
let surface0 = BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl0);
let surface1 = BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl1);
// meshing surface
let instant = std::time::Instant::now();
let polygon0 = StructuredMesh::from_surface(&surface0, surface0.range_tuple(), TOL).destruct();
let polygon1 = StructuredMesh::from_surface(&surface1, surface1.range_tuple(), TOL).destruct();
println!("Meshing Surfaces: {}s", instant.elapsed().as_secs_f64());
// extract intersection curves
let instant = std::time::Instant::now();
let curves = intersection_curves(surface0, &polygon0, surface1, &polygon1, TOL);
println!(
"Extracting Intersection: {}s",
instant.elapsed().as_secs_f64()
);
assert_eq!(curves.len(), 1);
let curve = curves[0].1.clone().unwrap();
const N: usize = 100;
for i in 0..N {
let t1 = curve.range_tuple().1;
let t = t1 * i as f64 / N as f64;
let pt = curve.subs(t);
assert_near!(pt.distance(Point3::origin()) * 0.5, f64::sqrt(0.5) * 0.5);
}
}
-723
View File
@@ -1,723 +0,0 @@
#![allow(clippy::many_single_char_names)]
use super::*;
use super::history::Lineage;
use rustc_hash::FxHashMap as HashMap;
use truck_base::cgmath64::*;
use truck_geometry::prelude::IntersectionCurve;
use truck_meshalgo::prelude::*;
use truck_topology::{Vertex, *};
type PolylineCurve = truck_meshalgo::prelude::PolylineCurve<Point3>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShapesOpStatus {
Unknown,
And,
Or,
}
impl ShapesOpStatus {
fn not(self) -> Self {
match self {
Self::Unknown => Self::Unknown,
Self::And => Self::Or,
Self::Or => Self::And,
}
}
}
#[derive(Clone, Debug)]
pub struct BoundaryWire<P, C> {
wire: Wire<P, C>,
status: ShapesOpStatus,
}
impl<P, C> BoundaryWire<P, C> {
#[inline(always)]
pub fn new(wire: Wire<P, C>, status: ShapesOpStatus) -> Self { Self { wire, status } }
#[inline(always)]
pub fn status(&self) -> ShapesOpStatus { self.status }
#[inline(always)]
pub fn invert(&mut self) {
self.wire.invert();
self.status = self.status.not();
}
#[inline(always)]
pub fn inverse(&self) -> Self {
Self {
wire: self.wire.inverse(),
status: self.status.not(),
}
}
}
impl ShapesOpStatus {
fn from_is_curve<C, S>(curve: &IntersectionCurve<C, S>) -> Option<ShapesOpStatus>
where
C: ParametricCurve3D + BoundedCurve,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>, {
let (t0, t1) = curve.range_tuple();
let t = (t0 + t1) / 2.0;
let (_, pt0, pt1) = curve.search_triple(t)?;
let der = curve.leader().der(t);
let normal0 = curve.surface0().normal(pt0[0], pt0[1]);
let normal1 = curve.surface1().normal(pt1[0], pt1[1]);
match normal0.cross(der).dot(normal1) > 0.0 {
true => Some(ShapesOpStatus::Or),
false => Some(ShapesOpStatus::And),
}
}
}
impl<P, C> std::ops::Deref for BoundaryWire<P, C> {
type Target = Wire<P, C>;
#[inline(always)]
fn deref(&self) -> &Self::Target { &self.wire }
}
impl<P, C> std::ops::DerefMut for BoundaryWire<P, C> {
#[inline(always)]
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.wire }
}
#[derive(Clone, Debug)]
pub struct Loops<P, C>(Vec<BoundaryWire<P, C>>);
#[derive(Clone, Debug)]
pub struct LoopsStore<P, C>(Vec<Loops<P, C>>);
impl<P, C> std::ops::Deref for Loops<P, C> {
type Target = Vec<BoundaryWire<P, C>>;
#[inline(always)]
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<P, C> std::ops::DerefMut for Loops<P, C> {
#[inline(always)]
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl<P, C> std::ops::Deref for LoopsStore<P, C> {
type Target = Vec<Loops<P, C>>;
#[inline(always)]
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<P, C> std::ops::DerefMut for LoopsStore<P, C> {
#[inline(always)]
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl<P, C> FromIterator<BoundaryWire<P, C>> for Loops<P, C> {
#[inline(always)]
fn from_iter<I: IntoIterator<Item = BoundaryWire<P, C>>>(iter: I) -> Self {
Self(Vec::from_iter(iter))
}
}
impl<'a, P, C, S> From<&'a Face<P, C, S>> for Loops<P, C> {
#[inline(always)]
fn from(face: &'a Face<P, C, S>) -> Loops<P, C> {
face.absolute_boundaries()
.iter()
.map(|wire| BoundaryWire::new(wire.clone(), ShapesOpStatus::Unknown))
.collect()
}
}
impl<'a, P: 'a, C: 'a, S: 'a> FromIterator<&'a Face<P, C, S>> for LoopsStore<P, C> {
fn from_iter<I: IntoIterator<Item = &'a Face<P, C, S>>>(iter: I) -> Self {
Self(iter.into_iter().map(Loops::from).collect())
}
}
impl<'a, P, C> IntoIterator for &'a LoopsStore<P, C> {
type Item = <&'a Vec<Loops<P, C>> as IntoIterator>::Item;
type IntoIter = <&'a Vec<Loops<P, C>> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter { self.0.iter() }
}
#[derive(Clone, Debug, Copy, PartialEq)]
enum ParameterKind {
Front,
Back,
Inner(f64),
}
impl ParameterKind {
fn try_new(t: f64, (t0, t1): (f64, f64)) -> Option<ParameterKind> {
if t0.near(&t) {
Some(ParameterKind::Front)
} else if t1.near(&t) {
Some(ParameterKind::Back)
} else if t0 < t && t < t1 {
Some(ParameterKind::Inner(t))
} else {
None
}
}
}
impl<P: Copy + Tolerance, C: Clone> Loops<P, C> {
fn search_parameter(&self, pt: P) -> Option<(usize, usize, ParameterKind)>
where C: BoundedCurve<Point = P> + SearchParameter<D1, Point = P> {
self.iter()
.enumerate()
.flat_map(move |(i, wire)| wire.iter().enumerate().map(move |(j, edge)| (i, j, edge)))
.find_map(|(i, j, edge)| {
let curve = edge.curve();
curve.search_parameter(pt, None, 1).and_then(|t| {
// SolidCraft: a point at an end of the curve is that end, however far its
// parameter is from the end's. The parameter alone is no distance: on a
// polyline it counts segments, and an intersection curve ending 2e-7 mm from
// another's end (two walls of a glyph meeting a face) read 1.4e-6 before it,
// so it was cut into a sliver and the result's shell never closed (#391).
let kind = if pt.near(&curve.front()) {
ParameterKind::Front
} else if pt.near(&curve.back()) {
ParameterKind::Back
} else {
ParameterKind::try_new(t, curve.range_tuple())?
};
Some((i, j, kind))
})
})
}
// SolidCraft: `replaced(old, new)` is told of every edge rebuilt on the new vertex.
fn change_vertex(
&mut self,
old_vertex: &Vertex<P>,
new_vertex: &Vertex<P>,
emap: &mut HashMap<EdgeID<C>, Edge<P, C>>,
replaced: &mut impl FnMut(EdgeID<C>, EdgeID<C>),
) {
self.iter_mut()
.flat_map(|wire| wire.iter_mut())
.for_each(|edge| {
let mut new_edge = if edge.absolute_front() == old_vertex {
emap.entry(edge.id()).or_insert_with(|| {
Edge::new(new_vertex, edge.absolute_back(), edge.curve())
})
} else if edge.absolute_back() == old_vertex {
emap.entry(edge.id()).or_insert_with(|| {
Edge::new(edge.absolute_front(), new_vertex, edge.curve())
})
} else {
return;
}
.clone();
replaced(edge.id(), new_edge.id());
if !edge.orientation() {
new_edge.invert();
}
// Remove the edge from the HashMap when it is no longer there because ID reassignment will occur.
if edge.count() == 1 {
emap.remove(&edge.id());
}
*edge = new_edge;
})
}
fn swap_edge_into_wire(&mut self, edge_id: EdgeID<C>, new_wire: &Wire<P, C>) {
self.iter_mut().for_each(|wire| {
let mut iter = wire.iter().enumerate();
if let Some((idx, edge)) = iter.find(|(_, edge)| edge.id() == edge_id) {
if edge.orientation() {
wire.swap_edge_into_wire(idx, new_wire.clone());
} else {
wire.swap_edge_into_wire(idx, new_wire.inverse());
}
}
});
}
#[inline(always)]
fn add_independent_loop(&mut self, r#loop: BoundaryWire<P, C>) {
self.push(r#loop.inverse());
self.push(r#loop);
}
fn add_edge(
&mut self,
edge0: Edge<P, C>,
status: ShapesOpStatus,
) -> [Option<(usize, usize)>; 2] {
self.add_edge_choosing(edge0, status, &|_, _, _| None)
}
// SolidCraft: the edge added at the visits `chosen` (found for the same edge in the mirror
// store, whose wires and edges lie in the same order); the default where they are no visits
// of its ends.
fn add_edge_at(&mut self, edge0: Edge<P, C>, status: ShapesOpStatus, chosen: [Option<(usize, usize)>; 2]) -> [Option<(usize, usize)>; 2] {
self.add_edge_choosing(edge0, status, &|_, at_back, at_front| {
let held = |visits: &[(usize, usize)], c: Option<(usize, usize)>| c.is_none_or(|v| visits.contains(&v));
(held(at_back, chosen[0]) && held(at_front, chosen[1])).then_some(chosen)
})
}
// SolidCraft: `pick(loops, at_back, at_front)` may name the visits (wire, edge) the edge is
// added at, from the visits of a wire to its back and front vertices; `None` leaves it to the
// default below.
#[allow(clippy::type_complexity)]
fn add_edge_choosing(
&mut self,
edge0: Edge<P, C>,
status: ShapesOpStatus,
pick: &dyn Fn(&Self, &[(usize, usize)], &[(usize, usize)]) -> Option<[Option<(usize, usize)>; 2]>,
) -> [Option<(usize, usize)>; 2] {
// SolidCraft: a vertex several intersection curves end on (the tangent point of two equal
// cylinders, say) is visited by more than one wire. The edge belongs where both of its
// ends are: a wire holding the visit at each end. Taking the first visit of each end
// joined wires that are different regions of the face, and the face came out with a wire
// visiting a vertex twice.
let starting_at = |loops: &Self, vertex: &Vertex<P>| -> Vec<(usize, usize)> {
loops
.iter()
.enumerate()
.flat_map(|(i, wire)| wire.iter().enumerate().filter(|(_, edge)| edge.front() == vertex).map(move |(j, _)| (i, j)))
.collect()
};
let (at_back, at_front) = (starting_at(self, edge0.back()), starting_at(self, edge0.front()));
let together = at_back
.iter()
.find_map(|&(i, j)| at_front.iter().find(|&&(k, _)| k == i).map(|&(_, l)| ((i, j), (i, l))));
let (a, b) = match (pick(self, &at_back, &at_front), together) {
(Some([a, b]), _) => (a, b),
(None, Some((a, b))) => (Some(a), Some(b)),
(None, None) => (at_back.first().copied(), at_front.first().copied()),
};
if let Some((wire_index0, edge_index0)) = a {
self[wire_index0].rotate_left(edge_index0);
self[wire_index0].push_front(edge0.clone());
self[wire_index0].push_back(edge0.inverse());
}
match (a, b) {
(Some((wire_index0, edge_index0)), Some((wire_index1, edge_index1))) => {
if wire_index0 == wire_index1 {
let len = self[wire_index0].len() - 2;
let edge_index1 = (len + edge_index1 - edge_index0) % len + 1;
let new_wire = self[wire_index0].split_off(edge_index1);
self[wire_index0].status = status;
self.push(BoundaryWire::new(new_wire, status.not()));
} else {
let mut new_wire0 = self[wire_index1].clone();
let mut new_wire1 = new_wire0.split_off(edge_index1);
new_wire0.append(&mut self[wire_index0]);
new_wire0.append(&mut new_wire1);
self[wire_index0] = new_wire0;
self.swap_remove(wire_index1);
}
}
(None, Some((wire_index1, edge_index1))) => {
self[wire_index1].rotate_left(edge_index1);
self[wire_index1].push_front(edge0.inverse());
self[wire_index1].push_back(edge0);
}
(None, None) => self.push(BoundaryWire::new(
vec![edge0.inverse(), edge0].into(),
ShapesOpStatus::Unknown,
)),
_ => {}
}
[a, b]
}
}
/// SolidCraft: where an edge leaves a vertex, as a unit vector: along the polyline from its
/// front (or back) vertex to the first point a tenth of its length away, which is steadier
/// than its first segment where the mesh it was traced on is noisy.
fn leaving(edge: &Edge<Point3, PolylineCurve>, from_front: bool) -> Option<Vector3> {
let curve = edge.curve();
let n = curve.len();
if n < 2 {
return None;
}
let total: f64 = (1..n).map(|k| (curve[k] - curve[k - 1]).magnitude()).sum();
let (origin, order): (Point3, Vec<usize>) = if from_front { (curve[0], (1..n).collect()) } else { (curve[n - 1], (0..n - 1).rev().collect()) };
let mut found = None;
for k in order {
let d = curve[k] - origin;
found = Some(d);
if d.magnitude() >= 0.1 * total {
break;
}
}
found.filter(|d| d.magnitude() > 1e-12 && d.magnitude().is_finite()).map(|d| d.normalize())
}
/// SolidCraft: the counterclockwise angle about `normal` from `from` to `to`, in `[0, 2π)`.
fn sweep(from: Vector3, to: Vector3, normal: Vector3) -> Option<f64> {
let e1 = from - normal * from.dot(normal);
if e1.magnitude() < 1e-9 {
return None;
}
let e1 = e1.normalize();
let e2 = normal.cross(e1);
let angle = to.dot(e2).atan2(to.dot(e1));
Some(if angle < 0.0 { angle + 2.0 * std::f64::consts::PI } else { angle })
}
impl Loops<Point3, PolylineCurve> {
/// SolidCraft: the visit among `visits` (of the vertex `at`) whose corner holds the direction
/// `d` the new edge leaves in: the wedge swept counterclockwise about `normal` from the
/// wire's outgoing edge to the way back along its incoming one. `None` when no corner can be
/// told (a single visit is returned as it is).
fn corner_holding(&self, visits: &[(usize, usize)], d: Vector3, normal: Vector3) -> Option<(usize, usize)> {
if let [only] = visits {
return Some(*only);
}
let tau = 2.0 * std::f64::consts::PI;
visits
.iter()
.filter_map(|&(i, j)| {
let wire = self.get(i)?;
let out = leaving(wire.get(j)?, true)?;
let before = wire.get((j + wire.len() - 1) % wire.len())?;
let back = leaving(before, false)?;
let wedge = sweep(out, back, normal)?;
let wedge = if wedge < 1e-6 { tau } else { wedge };
let at = sweep(out, d, normal)?;
(at > 1e-6 && at < wedge - 1e-6).then_some(((i, j), wedge))
})
.min_by(|x, y| x.1.total_cmp(&y.1))
.map(|(visit, _)| visit)
}
/// SolidCraft: [`Loops::add_edge`], with the corner at each end of the edge told by its
/// direction there (`normals`: the face's normal at its back and front vertices) where a
/// vertex is visited more than once.
fn add_edge_by_angle(&mut self, edge0: Edge<Point3, PolylineCurve>, status: ShapesOpStatus, normals: [Option<Vector3>; 2]) -> [Option<(usize, usize)>; 2] {
let (d_back, d_front) = (leaving(&edge0, false), leaving(&edge0, true));
let pick = |loops: &Self, at_back: &[(usize, usize)], at_front: &[(usize, usize)]| {
if at_back.len() < 2 && at_front.len() < 2 {
return None;
}
let a = match (d_back, normals[0]) {
(Some(d), Some(n)) => loops.corner_holding(at_back, d, n),
_ => None,
};
let b = match (d_front, normals[1]) {
(Some(d), Some(n)) => loops.corner_holding(at_front, d, n),
_ => None,
};
// A vertex visited more than once whose corner could not be told is left to the default.
let told = |visits: &[(usize, usize)], c: Option<(usize, usize)>| visits.len() < 2 || c.is_some();
(told(at_back, a) && told(at_front, b)).then_some([a.or_else(|| at_back.first().copied()), b.or_else(|| at_front.first().copied())])
};
self.add_edge_choosing(edge0, status, &pick)
}
}
impl<P: Copy + Tolerance, C: Clone> LoopsStore<P, C> {
#[inline(always)]
fn change_vertex(
&mut self,
old_vertex: &Vertex<P>,
new_vertex: &Vertex<P>,
emap: &mut HashMap<EdgeID<C>, Edge<P, C>>,
replaced: &mut impl FnMut(EdgeID<C>, EdgeID<C>),
) {
self.iter_mut()
.for_each(|loops| loops.change_vertex(old_vertex, new_vertex, emap, replaced));
}
#[inline(always)]
fn swap_edge_into_wire(&mut self, edge_id: EdgeID<C>, new_wire: &Wire<P, C>) {
self.iter_mut()
.for_each(|loops| loops.swap_edge_into_wire(edge_id, new_wire))
}
fn add_polygon_vertex(
&mut self,
loops_index: usize,
v: &Vertex<P>,
emap: &mut HashMap<EdgeID<C>, Edge<P, C>>,
) -> Option<(usize, usize, ParameterKind)>
where
C: Cut<Point = P> + SearchParameter<D1, Point = P>,
{
let pt = v.point();
let (wire_index, edge_index, kind) = self[loops_index].search_parameter(pt)?;
match kind {
ParameterKind::Front => {
let old_vertex = self[loops_index][wire_index][edge_index]
.absolute_front()
.clone();
self.change_vertex(&old_vertex, v, emap, &mut |_, _| {});
}
ParameterKind::Back => {
let old_vertex = self[loops_index][wire_index][edge_index]
.absolute_back()
.clone();
self.change_vertex(&old_vertex, v, emap, &mut |_, _| {});
}
ParameterKind::Inner(t) => {
let edge = self[loops_index][wire_index][edge_index].absolute_clone();
let edge_id = edge.id();
let (edge0, edge1) = edge.cut_with_parameter(v, t)?;
let new_wire: Wire<_, _> = vec![edge0, edge1].into();
self.swap_edge_into_wire(edge_id, &new_wire);
}
}
Some((wire_index, edge_index, kind))
}
}
impl<C> LoopsStore<Point3, C> {
fn add_geom_vertex<S>(
&mut self,
(loops_index, wire_index, edge_index): (usize, usize, usize),
v: &Vertex<Point3>,
kind: ParameterKind,
another_surface: &S,
emap: &mut HashMap<EdgeID<C>, Edge<Point3, C>>,
lineage: &mut Lineage<C, S>,
) -> Option<()>
where
C: Cut<Point = Point3, Vector = Vector3> + SearchNearestParameter<D1, Point = Point3>,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
match kind {
ParameterKind::Front => {
let old_vertex = self[loops_index][wire_index][edge_index]
.absolute_front()
.clone();
v.set_point(old_vertex.point());
self.change_vertex(&old_vertex, v, emap, &mut |old, new| lineage.edge_piece(old, new));
}
ParameterKind::Back => {
let old_vertex = self[loops_index][wire_index][edge_index]
.absolute_back()
.clone();
v.set_point(old_vertex.point());
self.change_vertex(&old_vertex, v, emap, &mut |old, new| lineage.edge_piece(old, new));
}
ParameterKind::Inner(_) => {
let curve = self[loops_index][wire_index][edge_index].curve();
let (pt, t, _) =
curve_surface_projection(&curve, None, another_surface, None, v.point(), 100)?;
v.set_point(pt);
let edge = self[loops_index][wire_index][edge_index].absolute_clone();
let edge_id = edge.id();
let (edge0, edge1) = edge.cut_with_parameter(v, t)?;
lineage.edge_piece(edge_id, edge0.id());
lineage.edge_piece(edge_id, edge1.id());
let new_wire: Wire<_, _> = vec![edge0, edge1].into();
self.swap_edge_into_wire(edge_id, &new_wire);
}
}
Some(())
}
}
fn curve_surface_projection<C, S>(
curve: &C,
curve_hint: Option<f64>,
surface: &S,
surface_hint: Option<(f64, f64)>,
point: Point3,
trials: usize,
) -> Option<(Point3, f64, Point2)>
where
C: ParametricCurve3D + SearchNearestParameter<D1, Point = Point3>,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
if trials == 0 {
return None;
}
let t = curve.search_nearest_parameter(point, curve_hint, 10)?;
let pt0 = curve.subs(t);
let (u, v) = surface.search_nearest_parameter(point, surface_hint, 10)?;
let pt1 = surface.subs(u, v);
if point.near(&pt0) && point.near(&pt1) && pt0.near(&pt1) {
Some((point, t, Point2::new(u, v)))
} else {
let l = curve.der(t);
let n = surface.normal(u, v);
let t0 = (pt1 - pt0).dot(n) / l.dot(n);
curve_surface_projection(
curve,
Some(t),
surface,
Some((u, v)),
pt0 + t0 * l,
trials - 1,
)
}
}
fn create_independent_loop<P, C, D>(mut poly_curve0: C) -> Wire<P, D>
where
C: Cut<Point = P>,
D: From<C>, {
let (t0, t1) = poly_curve0.range_tuple();
let t = (t0 + t1) / 2.0;
let poly_curve1 = poly_curve0.cut(t);
let v0 = Vertex::new(poly_curve0.front());
let v1 = Vertex::new(poly_curve1.front());
let edge0 = Edge::new(&v0, &v1, poly_curve0.into());
let edge1 = Edge::new(&v1, &v0, poly_curve1.into());
vec![edge0, edge1].into()
}
#[allow(dead_code)]
pub struct LoopsStoreQuadruple<C> {
pub geom_loops_store0: LoopsStore<Point3, C>,
pub poly_loops_store0: LoopsStore<Point3, PolylineCurve>,
pub geom_loops_store1: LoopsStore<Point3, C>,
pub poly_loops_store1: LoopsStore<Point3, PolylineCurve>,
}
pub fn create_loops_stores<C, S>(
geom_shell0: &Shell<Point3, C, S>,
poly_shell0: &Shell<Point3, PolylineCurve, Option<PolygonMesh>>,
geom_shell1: &Shell<Point3, C, S>,
poly_shell1: &Shell<Point3, PolylineCurve, Option<PolygonMesh>>,
tol: f64,
lineage: &mut Lineage<C, S>,
) -> Option<LoopsStoreQuadruple<C>>
where
C: SearchNearestParameter<D1, Point = Point3>
+ SearchParameter<D1, Point = Point3>
+ Cut<Point = Point3, Vector = Vector3>
+ From<IntersectionCurve<PolylineCurve, S>>,
S: ParametricSurface3D + SearchNearestParameter<D2, Point = Point3>,
{
let mut geom_loops_store0: LoopsStore<_, _> = geom_shell0.face_iter().collect();
let mut poly_loops_store0: LoopsStore<_, _> = poly_shell0.face_iter().collect();
let mut geom_loops_store1: LoopsStore<_, _> = geom_shell1.face_iter().collect();
let mut poly_loops_store1: LoopsStore<_, _> = poly_shell1.face_iter().collect();
let store0_len = geom_loops_store0.len();
let store1_len = geom_loops_store1.len();
(0..store0_len)
.flat_map(move |i| (0..store1_len).map(move |j| (i, j)))
.try_for_each(|(face_index0, face_index1)| {
let ori0 = geom_shell0[face_index0].orientation();
let ori1 = geom_shell1[face_index1].orientation();
let surface0 = geom_shell0[face_index0].surface();
let surface1 = geom_shell1[face_index1].surface();
let meeting = [geom_shell0[face_index0].id(), geom_shell1[face_index1].id()];
let polygon0 = crate::tr!(poly_shell0[face_index0].surface(), "polygon0");
let polygon1 = crate::tr!(poly_shell1[face_index1].surface(), "polygon1");
intersection_curve::intersection_curves(
surface0.clone(),
&polygon0,
surface1.clone(),
&polygon1,
tol,
)
.into_iter()
.try_for_each(|(polyline, intersection_curve)| {
let mut intersection_curve = crate::tr!(intersection_curve, "intersection curve").into();
let status = crate::tr!(ShapesOpStatus::from_is_curve(&intersection_curve), "status");
let (status0, status1) = match (ori0, ori1) {
(true, true) => (status, status.not()),
(true, false) => (status.not(), status.not()),
(false, true) => (status, status),
(false, false) => (status.not(), status),
};
if polyline.front().near(&polyline.back()) {
let poly_wire = create_independent_loop(polyline);
poly_loops_store0[face_index0]
.add_independent_loop(BoundaryWire::new(poly_wire.clone(), status0));
poly_loops_store1[face_index1]
.add_independent_loop(BoundaryWire::new(poly_wire, status1));
let geom_wire = create_independent_loop(intersection_curve);
geom_wire.iter().for_each(|edge| lineage.meet(edge.id(), meeting));
geom_loops_store0[face_index0]
.add_independent_loop(BoundaryWire::new(geom_wire.clone(), status0));
geom_loops_store1[face_index1]
.add_independent_loop(BoundaryWire::new(geom_wire, status1));
} else {
let pv0 = Vertex::new(polyline.front());
let pv1 = Vertex::new(polyline.back());
let gv0 = Vertex::new(polyline.front());
let gv1 = Vertex::new(polyline.back());
let mut pemap0 = HashMap::default();
let mut pemap1 = HashMap::default();
let mut gemap0 = HashMap::default();
let mut gemap1 = HashMap::default();
let idx00 =
poly_loops_store0.add_polygon_vertex(face_index0, &pv0, &mut pemap0);
if let Some((wire_index, edge_index, kind)) = idx00 {
crate::tr!(geom_loops_store0.add_geom_vertex(
(face_index0, wire_index, edge_index),
&gv0,
kind,
&surface1,
&mut gemap0,
lineage,
), "add_geom_vertex 1");
let polyline = intersection_curve.editor().leader;
*polyline.first_mut().unwrap() = gv0.point();
}
let idx01 =
poly_loops_store0.add_polygon_vertex(face_index0, &pv1, &mut pemap1);
if let Some((wire_index, edge_index, kind)) = idx01 {
crate::tr!(geom_loops_store0.add_geom_vertex(
(face_index0, wire_index, edge_index),
&gv1,
kind,
&surface1,
&mut gemap1,
lineage,
), "add_geom_vertex 2");
let polyline = intersection_curve.editor().leader;
*polyline.last_mut().unwrap() = gv1.point();
}
let idx10 =
poly_loops_store1.add_polygon_vertex(face_index1, &pv0, &mut pemap0);
if let Some((wire_index, edge_index, kind)) = idx10 {
crate::tr!(geom_loops_store1.add_geom_vertex(
(face_index1, wire_index, edge_index),
&gv0,
kind,
&surface0,
&mut gemap0,
lineage,
), "add_geom_vertex 3");
let polyline = intersection_curve.editor().leader;
*polyline.first_mut().unwrap() = gv0.point();
}
let idx11 =
poly_loops_store1.add_polygon_vertex(face_index1, &pv1, &mut pemap1);
if let Some((wire_index, edge_index, kind)) = idx11 {
crate::tr!(geom_loops_store1.add_geom_vertex(
(face_index1, wire_index, edge_index),
&gv1,
kind,
&surface0,
&mut gemap1,
lineage,
), "add_geom_vertex 4");
let polyline = intersection_curve.editor().leader;
*polyline.last_mut().unwrap() = gv1.point();
}
let pedge = Edge::new(&pv0, &pv1, polyline);
let gedge = Edge::new(&gv0, &gv1, intersection_curve.into());
lineage.meet(gedge.id(), meeting);
let normal_at = |surface: &S, p: Point3| -> Option<Vector3> {
let (u, v) = surface.search_nearest_parameter(p, None, 100)?;
Some(surface.normal(u, v)).filter(|n| n.magnitude() > 0.0 && n.magnitude().is_finite())
};
let normals0 = [normal_at(&surface0, gv1.point()), normal_at(&surface0, gv0.point())];
let normals1 = [normal_at(&surface1, gv1.point()), normal_at(&surface1, gv0.point())];
let chosen0 = poly_loops_store0[face_index0].add_edge_by_angle(pedge.clone(), status0, normals0);
geom_loops_store0[face_index0].add_edge_at(gedge.clone(), status0, chosen0);
let chosen1 = poly_loops_store1[face_index1].add_edge_by_angle(pedge, status1, normals1);
geom_loops_store1[face_index1].add_edge_at(gedge, status1, chosen1);
}
Some(())
})
})?;
Some(LoopsStoreQuadruple {
geom_loops_store0,
poly_loops_store0,
geom_loops_store1,
poly_loops_store1,
})
}
#[cfg(test)]
mod tests;
@@ -1,776 +0,0 @@
use super::*;
use std::fmt::Debug;
use truck_geometry::prelude::*;
const TOL: f64 = 0.01;
crate::impl_from!(
NurbsCurve<Vector4>,
IntersectionCurve<PolylineCurve, BSplineSurface<Point3>>
);
type AlternativeIntersection = crate::alternative::Alternative<
NurbsCurve<Vector4>,
IntersectionCurve<PolylineCurve, BSplineSurface<Point3>>,
>;
struct DebugDisplay<'a, T, Format> {
entity: &'a T,
format: Format,
}
impl<'a, P: Debug, C: Debug> Debug for DebugDisplay<'a, Loops<P, C>, WireDisplayFormat> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Loops")
.field(
&self
.entity
.0
.iter()
.map(|wire| wire.display(self.format))
.collect::<Vec<_>>(),
)
.finish()
}
}
impl<'a, P: Debug, C: Debug> Debug for DebugDisplay<'a, LoopsStore<P, C>, WireDisplayFormat> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.entity.0.iter().map(|loops| DebugDisplay {
entity: loops,
format: self.format,
}))
.finish()
}
}
impl<P: Debug, C: Debug> LoopsStore<P, C> {
fn display(&self, format: WireDisplayFormat) -> DebugDisplay<'_, Self, WireDisplayFormat> {
DebugDisplay {
entity: self,
format,
}
}
}
fn parabola_surfaces() -> (BSplineSurface<Point3>, BSplineSurface<Point3>) {
// define surfaces
#[rustfmt::skip]
let ctrl0 = vec![
vec![Point3::new(-1.0, -1.0, 3.0), Point3::new(-1.0, 0.0, -1.0), Point3::new(-1.0, 1.0, 3.0)],
vec![Point3::new(0.0, -1.0, -1.0), Point3::new(0.0, 0.0, -5.0), Point3::new(0.0, 1.0, -1.0)],
vec![Point3::new(1.0, -1.0, 3.0), Point3::new(1.0, 0.0, -1.0), Point3::new(1.0, 1.0, 3.0)],
];
#[rustfmt::skip]
let ctrl1 = vec![
vec![Point3::new(-1.0, -1.0, -3.0), Point3::new(-1.0, 0.0, 1.0), Point3::new(-1.0, 1.0, -3.0)],
vec![Point3::new(0.0, -1.0, 1.0), Point3::new(0.0, 0.0, 5.0), Point3::new(0.0, 1.0, 1.0)],
vec![Point3::new(1.0, -1.0, -3.0), Point3::new(1.0, 0.0, 1.0), Point3::new(1.0, 1.0, -3.0)],
];
(
BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl0),
BSplineSurface::new((KnotVec::bezier_knot(2), KnotVec::bezier_knot(2)), ctrl1),
)
}
#[test]
fn independent_intersection() {
// prepare geoetries
let arc00: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 0.0),
Vector4::new(-1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc01 = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(-1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, -1.0, 0.0, 0.0),
Vector4::new(1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc10: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, -1.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 0.0),
Vector4::new(-1.0, 0.0, -1.0, 1.0),
],
))
.into();
let arc11 = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(-1.0, 0.0, -1.0, 1.0),
Vector4::new(0.0, -1.0, 0.0, 0.0),
Vector4::new(1.0, 0.0, -1.0, 1.0),
],
))
.into();
let (surface0, surface1) = parabola_surfaces();
// prepare topologies
let v00 = Vertex::new(Point3::new(1.0, 0.0, 1.0));
let v01 = Vertex::new(Point3::new(-1.0, 0.0, 1.0));
let v10 = Vertex::new(Point3::new(1.0, 0.0, -1.0));
let v11 = Vertex::new(Point3::new(-1.0, 0.0, -1.0));
let wire0: Wire<_, _> = vec![Edge::new(&v00, &v01, arc00), Edge::new(&v01, &v00, arc01)].into();
let wire1: Wire<_, _> = vec![Edge::new(&v10, &v11, arc10), Edge::new(&v11, &v10, arc11)].into();
let geom_shell0: Shell<_, _, _> = vec![Face::new(vec![wire0], surface0).inverse()].into();
let geom_shell1: Shell<_, _, _> = vec![Face::new(vec![wire1], surface1)].into();
let poly_shell0 = geom_shell0.triangulation(TOL);
let poly_shell1 = geom_shell1.triangulation(TOL);
// exec create loops stores!
let LoopsStoreQuadruple {
geom_loops_store0,
geom_loops_store1,
..
} = create_loops_stores(&geom_shell0, &poly_shell0, &geom_shell1, &poly_shell1, TOL, &mut Default::default()).unwrap();
// check the topology
let vertex_format = VertexDisplayFormat::AsPoint;
let edge_id_format = EdgeDisplayFormat::VerticesTupleAndID { vertex_format };
let wire_id_format = WireDisplayFormat::EdgesListTuple {
edge_format: edge_id_format,
};
let edge_geom_format = EdgeDisplayFormat::VerticesTupleAndCurve { vertex_format };
let wire_geom_format = WireDisplayFormat::EdgesListTuple {
edge_format: edge_geom_format,
};
assert_eq!(geom_loops_store0.len(), 1);
assert_eq!(geom_loops_store0[0].len(), 3);
assert_eq!(geom_loops_store0[0][0].len(), 2);
assert_eq!(geom_loops_store0[0][1].len(), 2);
assert_eq!(geom_loops_store0[0][2].len(), 2);
assert!(
geom_loops_store0[0][0].is_closed(),
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert!(
geom_loops_store0[0][1].is_closed(),
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert!(
geom_loops_store0[0][2].is_closed(),
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert!(
geom_loops_store0[0][0].is_geometric_consistent(),
"{:?}",
geom_loops_store0.display(wire_geom_format)
);
assert!(
geom_loops_store0[0][1].is_geometric_consistent(),
"{:?}",
geom_loops_store0.display(wire_geom_format)
);
assert!(
geom_loops_store0[0][2].is_geometric_consistent(),
"{:?}",
geom_loops_store0.display(wire_geom_format)
);
assert_eq!(geom_loops_store1.len(), 1);
assert_eq!(geom_loops_store1[0].len(), 3);
assert_eq!(geom_loops_store1[0][0].len(), 2);
assert_eq!(geom_loops_store1[0][1].len(), 2);
assert_eq!(geom_loops_store1[0][2].len(), 2);
assert!(
geom_loops_store1[0][0].is_closed(),
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert!(
geom_loops_store1[0][1].is_closed(),
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert!(
geom_loops_store1[0][2].is_closed(),
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert!(
geom_loops_store1[0][0].is_geometric_consistent(),
"{:?}",
geom_loops_store1.display(wire_geom_format)
);
assert!(
geom_loops_store1[0][1].is_geometric_consistent(),
"{:?}",
geom_loops_store1.display(wire_geom_format)
);
assert!(
geom_loops_store1[0][2].is_geometric_consistent(),
"{:?}",
geom_loops_store1.display(wire_geom_format)
);
// check the boundary status
let mut flags = [true; 3];
for i in 0..3 {
let bw = &geom_loops_store0[0][i];
match bw.status() {
ShapesOpStatus::Unknown => {
let curve = bw[0].oriented_curve();
assert_near!(curve.subs(0.0)[2], 1.0);
assert!(flags[0]);
flags[0] = false;
}
ShapesOpStatus::And => {
let curve = bw[0].oriented_curve();
let pt = curve.subs(0.5) - Point3::origin();
let der = curve.der(0.5);
assert!(pt.cross(der)[2] > 0.0);
assert!(flags[1]);
flags[1] = false;
}
ShapesOpStatus::Or => {
let curve = bw[0].oriented_curve();
let pt = curve.subs(0.5) - Point3::origin();
let der = curve.der(0.5);
assert!(pt.cross(der)[2] < 0.0);
assert!(flags[2]);
flags[2] = false;
}
}
}
let mut flags = [true; 3];
for i in 0..3 {
let bw = &geom_loops_store1[0][i];
match bw.status() {
ShapesOpStatus::Unknown => {
let curve = bw[0].oriented_curve();
assert_near!(curve.subs(0.0)[2], -1.0);
assert!(flags[0]);
flags[0] = false;
}
ShapesOpStatus::Or => {
let curve = bw[0].oriented_curve();
let pt = curve.subs(0.5) - Point3::origin();
let der = curve.der(0.5);
assert!(pt.cross(der)[2] < 0.0);
assert!(flags[1]);
flags[1] = false;
}
ShapesOpStatus::And => {
let curve = bw[0].oriented_curve();
let pt = curve.subs(0.5) - Point3::origin();
let der = curve.der(0.5);
assert!(pt.cross(der)[2] > 0.0);
assert!(flags[2]);
flags[2] = false;
}
}
}
}
#[test]
fn rotated_intersection() {
// prepare geoetries
let arc00: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 0.0),
Vector4::new(-1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc01: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(-1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, -1.0, 0.0, 0.0),
Vector4::new(1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc02: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, 0.0, -3.0, 1.0),
Vector4::new(-1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc10: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(0.0, -1.0, -1.0, 1.0),
Vector4::new(1.0, 0.0, 0.0, 0.0),
Vector4::new(0.0, 1.0, -1.0, 1.0),
],
))
.into();
let arc11: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(0.0, 1.0, -1.0, 1.0),
Vector4::new(-1.0, 0.0, 0.0, 0.0),
Vector4::new(0.0, -1.0, -1.0, 1.0),
],
))
.into();
let arc12: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(0.0, -1.0, -1.0, 1.0),
Vector4::new(0.0, 0.0, 3.0, 1.0),
Vector4::new(0.0, 1.0, -1.0, 1.0),
],
))
.into();
let (surface0, surface1) = parabola_surfaces();
let v00 = Vertex::new(Point3::new(1.0, 0.0, 1.0));
let v01 = Vertex::new(Point3::new(-1.0, 0.0, 1.0));
let edge00 = Edge::new(&v00, &v01, arc00);
let edge01 = Edge::new(&v01, &v00, arc01);
let edge02 = Edge::new(&v00, &v01, arc02);
let wire00: Wire<_, _> = vec![edge00, edge02.inverse()].into();
let wire01: Wire<_, _> = vec![edge01, edge02].into();
let face00 = Face::new(vec![wire00], surface0.clone());
let face01 = Face::new(vec![wire01], surface0);
let geom_shell0: Shell<_, _, _> = vec![face00.inverse(), face01.inverse()].into();
let v10 = Vertex::new(Point3::new(0.0, -1.0, -1.0));
let v11 = Vertex::new(Point3::new(0.0, 1.0, -1.0));
let edge10 = Edge::new(&v10, &v11, arc10);
let edge11 = Edge::new(&v11, &v10, arc11);
let edge12 = Edge::new(&v10, &v11, arc12);
let wire10: Wire<_, _> = vec![edge10, edge12.inverse()].into();
let wire11: Wire<_, _> = vec![edge12, edge11].into();
let face10 = Face::new(vec![wire10], surface1.clone());
let face11 = Face::new(vec![wire11], surface1);
let geom_shell1: Shell<_, _, _> = vec![face10, face11].into();
let poly_shell0 = geom_shell0.triangulation(TOL);
let poly_shell1 = geom_shell1.triangulation(TOL);
let file = std::fs::File::create("polyshell0.obj").unwrap();
obj::write(&poly_shell0.to_polygon(), file).unwrap();
let file = std::fs::File::create("polyshell1.obj").unwrap();
obj::write(&poly_shell1.to_polygon(), file).unwrap();
let LoopsStoreQuadruple {
geom_loops_store0,
geom_loops_store1,
..
} = create_loops_stores(&geom_shell0, &poly_shell0, &geom_shell1, &poly_shell1, TOL, &mut Default::default()).unwrap();
let vertex_format = VertexDisplayFormat::AsPoint;
let edge_id_format = EdgeDisplayFormat::VerticesTupleAndID { vertex_format };
let wire_id_format = WireDisplayFormat::EdgesListTuple {
edge_format: edge_id_format,
};
let edge_geom_format = EdgeDisplayFormat::VerticesTupleAndCurve { vertex_format };
let wire_geom_format = WireDisplayFormat::EdgesListTuple {
edge_format: edge_geom_format,
};
assert_eq!(geom_loops_store0.len(), 2);
assert_eq!(geom_loops_store0[0].len(), 2);
assert!(
geom_loops_store0[0][0].is_closed(),
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert!(
geom_loops_store0[0][1].is_closed(),
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert!(
geom_loops_store0[0][0].is_geometric_consistent(),
"{:?}",
geom_loops_store0.display(wire_geom_format)
);
assert!(
geom_loops_store0[0][1].is_geometric_consistent(),
"{:?}",
geom_loops_store0.display(wire_geom_format)
);
let (a, b) = (geom_loops_store0[0][0].len(), geom_loops_store0[0][1].len());
let compatible0 = match geom_loops_store0[0][0].status() {
ShapesOpStatus::And => a == 3,
ShapesOpStatus::Or => a == 5,
_ => false,
};
let compatible1 = match geom_loops_store0[0][1].status() {
ShapesOpStatus::And => b == 3,
ShapesOpStatus::Or => b == 5,
_ => false,
};
assert!(
compatible0 && compatible1 && a * b == 15,
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert_eq!(geom_loops_store0[1].len(), 2);
assert!(
geom_loops_store0[1][0].is_closed(),
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert!(
geom_loops_store0[1][1].is_closed(),
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert!(
geom_loops_store0[1][0].is_geometric_consistent(),
"{:?}",
geom_loops_store0.display(wire_geom_format)
);
assert!(
geom_loops_store0[1][1].is_geometric_consistent(),
"{:?}",
geom_loops_store0.display(wire_geom_format)
);
let (a, b) = (geom_loops_store0[1][0].len(), geom_loops_store0[1][1].len());
let compatible0 = match geom_loops_store0[1][0].status() {
ShapesOpStatus::And => a == 3,
ShapesOpStatus::Or => a == 5,
_ => false,
};
let compatible1 = match geom_loops_store0[1][1].status() {
ShapesOpStatus::And => b == 3,
ShapesOpStatus::Or => b == 5,
_ => false,
};
assert!(
compatible0 && compatible1 && a * b == 15,
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert_eq!(geom_loops_store1.len(), 2);
assert_eq!(geom_loops_store1[0].len(), 2);
assert!(
geom_loops_store1[0][0].is_closed(),
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert!(
geom_loops_store1[0][1].is_closed(),
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert!(
geom_loops_store1[0][0].is_geometric_consistent(),
"{:?}",
geom_loops_store1.display(wire_geom_format)
);
assert!(
geom_loops_store1[0][1].is_geometric_consistent(),
"{:?}",
geom_loops_store1.display(wire_geom_format)
);
let (a, b) = (geom_loops_store1[0][0].len(), geom_loops_store1[0][1].len());
let compatible0 = match geom_loops_store1[0][0].status() {
ShapesOpStatus::And => a == 3,
ShapesOpStatus::Or => a == 5,
_ => false,
};
let compatible1 = match geom_loops_store1[0][1].status() {
ShapesOpStatus::And => b == 3,
ShapesOpStatus::Or => b == 5,
_ => false,
};
assert!(
compatible0 && compatible1 && a * b == 15,
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert_eq!(geom_loops_store1[1].len(), 2);
assert!(
geom_loops_store1[1][0].is_closed(),
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert!(
geom_loops_store1[1][1].is_closed(),
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert!(
geom_loops_store1[1][0].is_geometric_consistent(),
"{:?}",
geom_loops_store1.display(wire_geom_format)
);
assert!(
geom_loops_store1[1][1].is_geometric_consistent(),
"{:?}",
geom_loops_store1.display(wire_geom_format)
);
let (a, b) = (geom_loops_store1[1][0].len(), geom_loops_store1[1][1].len());
let compatible0 = match geom_loops_store1[1][0].status() {
ShapesOpStatus::And => a == 3,
ShapesOpStatus::Or => a == 5,
_ => false,
};
let compatible1 = match geom_loops_store1[1][1].status() {
ShapesOpStatus::And => b == 3,
ShapesOpStatus::Or => b == 5,
_ => false,
};
assert!(
compatible0 && compatible1 && a * b == 15,
"{:?}",
geom_loops_store1.display(wire_id_format)
);
}
#[test]
fn crossing_edges() {
let arc00: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 0.0),
Vector4::new(-1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc01: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(-1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, -1.0, 0.0, 0.0),
Vector4::new(1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc02: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, 1.0, 1.0),
Vector4::new(0.0, 0.0, -3.0, 1.0),
Vector4::new(-1.0, 0.0, 1.0, 1.0),
],
))
.into();
let arc10: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, -1.0, 1.0),
Vector4::new(0.0, 1.0, 0.0, 0.0),
Vector4::new(-1.0, 0.0, -1.0, 1.0),
],
))
.into();
let arc11: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(-1.0, 0.0, -1.0, 1.0),
Vector4::new(0.0, -1.0, 0.0, 0.0),
Vector4::new(1.0, 0.0, -1.0, 1.0),
],
))
.into();
let arc12: AlternativeIntersection = NurbsCurve::new(BSplineCurve::new(
KnotVec::bezier_knot(2),
vec![
Vector4::new(1.0, 0.0, -1.0, 1.0),
Vector4::new(0.0, 0.0, 3.0, 1.0),
Vector4::new(-1.0, 0.0, -1.0, 1.0),
],
))
.into();
let (surface0, surface1) = parabola_surfaces();
let v00 = Vertex::new(Point3::new(1.0, 0.0, 1.0));
let v01 = Vertex::new(Point3::new(-1.0, 0.0, 1.0));
let edge00 = Edge::new(&v00, &v01, arc00);
let edge01 = Edge::new(&v01, &v00, arc01);
let edge02 = Edge::new(&v00, &v01, arc02);
let wire00: Wire<_, _> = vec![edge00, edge02.inverse()].into();
let wire01: Wire<_, _> = vec![edge01, edge02].into();
let face00 = Face::new(vec![wire00], surface0.clone());
let face01 = Face::new(vec![wire01], surface0);
let geom_shell0: Shell<_, _, _> = vec![face00.inverse(), face01.inverse()].into();
let v10 = Vertex::new(Point3::new(1.0, 0.0, -1.0));
let v11 = Vertex::new(Point3::new(-1.0, 0.0, -1.0));
let edge10 = Edge::new(&v10, &v11, arc10);
let edge11 = Edge::new(&v11, &v10, arc11);
let edge12 = Edge::new(&v10, &v11, arc12);
let wire10: Wire<_, _> = vec![edge10, edge12.inverse()].into();
let wire11: Wire<_, _> = vec![edge11, edge12].into();
let face10 = Face::new(vec![wire10], surface1.clone());
let face11 = Face::new(vec![wire11], surface1);
let geom_shell1: Shell<_, _, _> = vec![face10, face11].into();
let poly_shell0 = geom_shell0.triangulation(TOL);
let poly_shell1 = geom_shell1.triangulation(TOL);
let LoopsStoreQuadruple {
geom_loops_store0,
geom_loops_store1,
..
} = create_loops_stores(&geom_shell0, &poly_shell0, &geom_shell1, &poly_shell1, TOL, &mut Default::default()).unwrap();
let vertex_format = VertexDisplayFormat::AsPoint;
let edge_id_format = EdgeDisplayFormat::VerticesTupleAndID { vertex_format };
let wire_id_format = WireDisplayFormat::EdgesListTuple {
edge_format: edge_id_format,
};
let edge_geom_format = EdgeDisplayFormat::VerticesTupleAndCurve { vertex_format };
let wire_geom_format = WireDisplayFormat::EdgesListTuple {
edge_format: edge_geom_format,
};
assert_eq!(geom_loops_store0.len(), 2);
assert_eq!(geom_loops_store0[0].len(), 2);
assert!(
geom_loops_store0[0][0].is_closed(),
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert!(
geom_loops_store0[0][1].is_closed(),
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert!(
geom_loops_store0[0][0].is_geometric_consistent(),
"{:?}",
geom_loops_store0.display(wire_geom_format)
);
assert!(
geom_loops_store0[0][1].is_geometric_consistent(),
"{:?}",
geom_loops_store0.display(wire_geom_format)
);
let (a, b) = (geom_loops_store0[0][0].len(), geom_loops_store0[0][1].len());
let compatible0 = match geom_loops_store0[0][0].status() {
ShapesOpStatus::And => a == 2,
ShapesOpStatus::Or => a == 4,
_ => false,
};
let compatible1 = match geom_loops_store0[0][1].status() {
ShapesOpStatus::And => b == 2,
ShapesOpStatus::Or => b == 4,
_ => false,
};
assert!(
compatible0 && compatible1 && a * b == 8,
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert_eq!(geom_loops_store0[1].len(), 2);
assert!(
geom_loops_store0[1][0].is_closed(),
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert!(
geom_loops_store0[1][1].is_closed(),
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert!(
geom_loops_store0[1][0].is_geometric_consistent(),
"{:?}",
geom_loops_store0.display(wire_geom_format)
);
assert!(
geom_loops_store0[1][1].is_geometric_consistent(),
"{:?}",
geom_loops_store0.display(wire_geom_format)
);
let (a, b) = (geom_loops_store0[1][0].len(), geom_loops_store0[1][1].len());
let compatible0 = match geom_loops_store0[1][0].status() {
ShapesOpStatus::And => a == 2,
ShapesOpStatus::Or => a == 4,
_ => false,
};
let compatible1 = match geom_loops_store0[1][1].status() {
ShapesOpStatus::And => b == 2,
ShapesOpStatus::Or => b == 4,
_ => false,
};
assert!(
compatible0 && compatible1 && a * b == 8,
"{:?}",
geom_loops_store0.display(wire_id_format)
);
assert_eq!(geom_loops_store1.len(), 2);
assert_eq!(geom_loops_store1[0].len(), 2);
assert!(
geom_loops_store1[0][0].is_closed(),
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert!(
geom_loops_store1[0][1].is_closed(),
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert!(
geom_loops_store1[0][0].is_geometric_consistent(),
"{:?}",
geom_loops_store1.display(wire_geom_format)
);
assert!(
geom_loops_store1[0][1].is_geometric_consistent(),
"{:?}",
geom_loops_store1.display(wire_geom_format)
);
let (a, b) = (geom_loops_store1[0][0].len(), geom_loops_store1[0][1].len());
let compatible0 = match geom_loops_store1[0][0].status() {
ShapesOpStatus::And => a == 2,
ShapesOpStatus::Or => a == 4,
_ => false,
};
let compatible1 = match geom_loops_store1[0][1].status() {
ShapesOpStatus::And => b == 2,
ShapesOpStatus::Or => b == 4,
_ => false,
};
assert!(
compatible0 && compatible1 && a * b == 8,
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert_eq!(geom_loops_store1[1].len(), 2);
assert!(
geom_loops_store1[1][0].is_closed(),
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert!(
geom_loops_store1[1][1].is_closed(),
"{:?}",
geom_loops_store1.display(wire_id_format)
);
assert!(
geom_loops_store1[1][0].is_geometric_consistent(),
"{:?}",
geom_loops_store1.display(wire_geom_format)
);
assert!(
geom_loops_store1[1][1].is_geometric_consistent(),
"{:?}",
geom_loops_store1.display(wire_geom_format)
);
let (a, b) = (geom_loops_store1[1][0].len(), geom_loops_store1[1][1].len());
let compatible0 = match geom_loops_store1[1][0].status() {
ShapesOpStatus::And => a == 2,
ShapesOpStatus::Or => a == 4,
_ => false,
};
let compatible1 = match geom_loops_store1[1][1].status() {
ShapesOpStatus::And => b == 2,
ShapesOpStatus::Or => b == 4,
_ => false,
};
assert!(
compatible0 && compatible1 && a * b == 8,
"{:?}",
geom_loops_store0.display(wire_id_format)
);
}
-12
View File
@@ -1,12 +0,0 @@
mod divide_face;
mod faces_classification;
mod history;
mod integrate;
mod intersection_curve;
// SolidCraft: expose the constructor for workspace regression coverage of the vendored fix.
#[doc(hidden)]
pub use intersection_curve::IntersectionCurveWithParameters;
mod loops_store;
mod polyline_construction;
pub use history::{EdgeOrigin, FaceIndex, FaceOrigin, History};
pub use integrate::{and, and_with_history, and_with_history_meshed, meshes, or, or_with_history, MeshedShell, ShapeOpsCurve, ShapeOpsSurface};
@@ -1,117 +0,0 @@
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use std::collections::VecDeque;
use truck_base::{cgmath64::*, tolerance::*};
use truck_meshalgo::prelude::PolylineCurve;
pub fn construct_polylines(lines: &[(Point3, Point3)]) -> Vec<PolylineCurve<Point3>> {
let mut graph: Graph = lines.iter().collect();
let mut res = Vec::new();
while !graph.is_empty() {
let (mut idx, node) = graph.get_one();
let mut wire: VecDeque<_> = vec![node.coord].into();
while let Some((idx0, pt)) = graph.get_a_next_node(idx) {
idx = idx0;
wire.push_back(pt);
}
let mut idx = PointIndex::from(wire[0]);
while let Some((idx0, pt)) = graph.get_a_next_node(idx) {
idx = idx0;
wire.push_front(pt);
}
res.push(PolylineCurve(wire.into()));
}
res
}
#[derive(Clone, Debug, Copy, Hash, PartialEq, Eq)]
struct PointIndex([i64; 3]);
impl From<Point3> for PointIndex {
#[inline(always)]
fn from(pt: Point3) -> PointIndex {
let idx = pt.add_element_wise(TOLERANCE) / (2.0 * TOLERANCE);
PointIndex(idx.cast::<i64>().unwrap().into())
}
}
struct Node {
coord: Point3,
adjacency: HashSet<PointIndex>,
}
impl Node {
#[inline(always)]
fn new(coord: Point3, adjacency: HashSet<PointIndex>) -> Node { Node { coord, adjacency } }
fn pop_one_adjacency(&mut self) -> PointIndex {
let idx = *self.adjacency.iter().next().unwrap();
self.adjacency.remove(&idx);
idx
}
}
struct Graph(HashMap<PointIndex, Node>);
impl std::ops::Deref for Graph {
type Target = HashMap<PointIndex, Node>;
#[inline(always)]
fn deref(&self) -> &Self::Target { &self.0 }
}
impl std::ops::DerefMut for Graph {
#[inline(always)]
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl Graph {
fn add_half_edge(&mut self, pt0: Point3, pt1: Point3) {
let idx0 = pt0.into();
let idx1 = pt1.into();
if let Some(node) = self.get_mut(&idx0) {
node.adjacency.insert(idx1);
} else {
let mut set = HashSet::default();
set.insert(idx1);
self.insert(idx0, Node::new(pt0, set));
}
}
fn add_edge(&mut self, line: (Point3, Point3)) {
if !line.0.near(&line.1) {
self.add_half_edge(line.0, line.1);
self.add_half_edge(line.1, line.0);
}
}
#[inline(always)]
fn get_one(&self) -> (PointIndex, &Node) {
let (idx, node) = self.iter().next().unwrap();
(*idx, node)
}
fn get_a_next_node(&mut self, idx: PointIndex) -> Option<(PointIndex, Point3)> {
let node = self.get_mut(&idx)?;
let idx0 = node.pop_one_adjacency();
if node.adjacency.is_empty() {
self.remove(&idx);
}
let node = self.get_mut(&idx0)?;
node.adjacency.remove(&idx);
let pt = node.coord;
if node.adjacency.is_empty() {
self.remove(&idx0);
}
Some((idx0, pt))
}
}
impl<'a> FromIterator<&'a (Point3, Point3)> for Graph {
fn from_iter<I: IntoIterator<Item = &'a (Point3, Point3)>>(iter: I) -> Graph {
let mut res = Graph(HashMap::default());
iter.into_iter().for_each(|line| res.add_edge(*line));
res
}
}
#[cfg(test)]
mod tests;
@@ -1,113 +0,0 @@
use super::*;
#[test]
fn construct_polylines_positive0() {
let lines = vec![
(Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)),
(Point3::new(1.0, 0.0, 0.0), Point3::new(0.0, 1.0, 0.0)),
(Point3::new(1.0, 1.0, 0.0), Point3::new(0.0, 0.0, 1.0)),
(Point3::new(0.0, 1.0, 1.0), Point3::new(1.0, 1.0, 1.0)),
(Point3::new(0.0, 0.0, 1.0), Point3::new(1.0, 0.0, 1.0)),
(Point3::new(0.0, 1.0, 0.0), Point3::new(1.0, 1.0, 0.0)),
(Point3::new(1.0, 1.0, 1.0), Point3::new(0.0, 0.0, 0.0)),
(Point3::new(1.0, 0.0, 1.0), Point3::new(0.0, 1.0, 1.0)),
];
let polyline = construct_polylines(&lines);
assert_eq!(polyline.len(), 1);
assert_eq!(polyline[0].len(), 9);
let mut sign = None;
for line in polyline[0].windows(2) {
let a = line[0][0] + line[0][1] * 2.0 + line[0][2] * 4.0;
let b = line[1][0] + line[1][1] * 2.0 + line[1][2] * 4.0;
let x = b - a;
assert!(f64::abs(x) == 1.0 || f64::abs(x) == 7.0);
let s = f64::signum(x * (x - 2.0) * (x + 2.0));
if let Some(sign) = sign {
assert!(s == sign);
} else {
sign = Some(s);
}
}
}
#[test]
fn construct_polylines_positive1() {
let lines = vec![
(Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)),
(Point3::new(1.0, 0.0, 0.0), Point3::new(1.0, 1.0, 0.0)),
(Point3::new(1.0, 0.0, 1.0), Point3::new(1.0, 1.0, 1.0)),
(Point3::new(1.0, 1.0, 1.0), Point3::new(0.0, 1.0, 1.0)),
(Point3::new(1.0, 1.0, 0.0), Point3::new(0.0, 1.0, 0.0)),
(Point3::new(0.0, 1.0, 0.0), Point3::new(0.0, 0.0, 0.0)),
(Point3::new(0.0, 0.0, 1.0), Point3::new(1.0, 0.0, 1.0)),
(Point3::new(0.0, 1.0, 1.0), Point3::new(0.0, 0.0, 1.0)),
];
let polyline = construct_polylines(&lines);
assert_eq!(polyline.len(), 2);
assert_eq!(polyline[0].len(), 5);
assert_eq!(polyline[1].len(), 5);
}
#[test]
fn construct_polylines_positive2() {
let lines = vec![
(Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)),
(Point3::new(1.0, 0.0, 0.0), Point3::new(0.0, 1.0, 0.0)),
(Point3::new(1.0, 1.0, 0.0), Point3::new(0.0, 0.0, 1.0)),
(Point3::new(0.0, 1.0, 1.0), Point3::new(1.0, 1.0, 1.0)),
(Point3::new(0.0, 0.0, 1.0), Point3::new(1.0, 0.0, 1.0)),
(Point3::new(1.0, 1.0, 0.0), Point3::new(1.0, 1.0, 0.0)),
(Point3::new(0.0, 1.0, 0.0), Point3::new(1.0, 1.0, 0.0)),
(Point3::new(1.0, 1.0, 1.0), Point3::new(0.0, 0.0, 0.0)),
(Point3::new(1.0, 0.0, 1.0), Point3::new(0.0, 1.0, 1.0)),
];
let polyline = construct_polylines(&lines);
assert_eq!(polyline.len(), 1);
assert_eq!(polyline[0].len(), 9);
let mut sign = None;
for line in polyline[0].windows(2) {
let a = line[0][0] + line[0][1] * 2.0 + line[0][2] * 4.0;
let b = line[1][0] + line[1][1] * 2.0 + line[1][2] * 4.0;
let x = b - a;
assert!(f64::abs(x) == 1.0 || f64::abs(x) == 7.0);
let s = f64::signum(x * (x - 2.0) * (x + 2.0));
if let Some(sign) = sign {
assert!(s == sign);
} else {
sign = Some(s);
}
}
}
#[test]
fn construct_polylines_positive3() {
let lines = vec![
(Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)),
(Point3::new(1.0, 0.0, 0.0), Point3::new(0.0, 1.0, 0.0)),
(Point3::new(1.0, 1.0, 0.0), Point3::new(0.0, 0.0, 1.0)),
(Point3::new(0.0, 1.0, 1.0), Point3::new(1.0, 1.0, 1.0)),
(Point3::new(0.0, 0.0, 1.0), Point3::new(1.0, 0.0, 1.0)),
(Point3::new(1.0, 1.0, 0.0), Point3::new(1.0, 1.0, 0.0)),
(Point3::new(0.0, 1.0, 0.0), Point3::new(1.0, 1.0, 0.0)),
(Point3::new(1.0, 0.0, 1.0), Point3::new(0.0, 1.0, 1.0)),
];
let polyline = construct_polylines(&lines);
assert_eq!(polyline.len(), 1);
assert_eq!(polyline[0].len(), 8);
let mut sign = None;
for line in polyline[0].windows(2) {
let a = line[0][0] + line[0][1] * 2.0 + line[0][2] * 4.0;
let b = line[1][0] + line[1][1] * 2.0 + line[1][2] * 4.0;
let x = b - a;
assert!(f64::abs(x) == 1.0);
let s = f64::signum(x * (x - 2.0) * (x + 2.0));
if let Some(sign) = sign {
assert!(s == sign);
} else {
sign = Some(s);
}
}
}
-109
View File
@@ -1,109 +0,0 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "truck-stepio"
version = "0.3.0"
authors = ["Yoshinori Tanimura <tanimura@ricos.co.jp>"]
build = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Reads/writes STEP files from/to truck"
homepage = "https://github.com/ricosjp/truck"
readme = "README.md"
keywords = [
"truck",
"graphics",
]
categories = ["graphics"]
license = "Apache-2.0"
repository = "https://github.com/ricosjp/truck"
[lib]
name = "truck_stepio"
path = "src/lib.rs"
[dependencies.chrono]
version = "0.4.38"
features = ["wasmbind"]
[dependencies.derive_more]
version = "0.99.18"
optional = true
[dependencies.ruststep]
version = "0.4.0"
optional = true
[dependencies.serde]
version = "1.0.210"
features = ["derive"]
optional = true
[dependencies.truck-derivers]
version = "0.1.0"
optional = true
[dependencies.truck-geometry]
version = "0.5.0"
[dependencies.truck-geotrait]
version = "0.4.0"
optional = true
[dependencies.truck-modeling]
version = "0.6.0"
[dependencies.truck-polymesh]
version = "0.6.0"
[dependencies.truck-topology]
version = "0.6.0"
[dev-dependencies.clap]
version = "4.5.17"
features = ["derive"]
[dev-dependencies.espr-derive]
version = "0.4.0"
[dev-dependencies.itertools]
version = "0.13.0"
[dev-dependencies.proptest]
version = "1.5.0"
[dev-dependencies.serde_json]
version = "1.0.128"
[dev-dependencies.truck-meshalgo]
version = "0.4.0"
[features]
default = [
"in",
"derive",
]
derive = ["truck-derivers"]
in = [
"derive_more",
"ruststep",
"serde",
"truck-geotrait",
]
-177
View File
@@ -1,177 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
-21
View File
@@ -1,21 +0,0 @@
# truck-stepio
[![Crates.io](https://img.shields.io/crates/v/truck-stepio.svg)](https://crates.io/crates/truck-stepio) [![Docs.rs](https://docs.rs/truck-stepio/badge.svg)](https://docs.rs/truck-stepio)
Reads/writes STEP files from/to truck.
## Sample Codes
### shape-to-step
convert from truck shape json to step file.
#### usage
```bash
shape-to-step <input shape file> [output shape file]
```
### step-to-mesh
Parse STEP data, extract shape, and meshing.
-397
View File
@@ -1,397 +0,0 @@
use crate::{self as truck_stepio};
use derive_more::*;
use serde::{Deserialize, Serialize};
use truck_derivers::{DisplayByStep, StepCurve, StepLength, StepSurface};
/// re-export structs in `truck-geometry` and `truck-polymesh`.
pub mod re_exports {
pub use truck_geometry::prelude::*;
pub use truck_polymesh::*;
}
pub use re_exports::*;
/// Errors that occur when converting STEP format
pub type StepConvertingError = Box<dyn std::error::Error>;
/// `ellipse`, realized in `truck`
pub type Ellipse<P, M> = Processor<TrimmedCurve<UnitCircle<P>>, M>;
/// `hyperbola`, realized in `truck`
pub type Hyperbola<P, M> = Processor<TrimmedCurve<UnitHyperbola<P>>, M>;
/// `parabola`, realized in `truck`
pub type Parabola<P, M> = Processor<TrimmedCurve<UnitParabola<P>>, M>;
/// `spherical_surface`, realized in `truck`
pub type SphericalSurface = Processor<Sphere, Matrix4>;
/// `cylindrical_surface`, realized in `truck`
pub type CylindricalSurface = Processor<RevolutedCurve<Line<Point3>>, Matrix4>;
/// `toroidal_surface`, realized in `truck`
pub type ToroidalSurface = Processor<Torus, Matrix4>;
/// `conical_surface`, realized in `truck`
pub type ConicalSurface = Processor<RevolutedCurve<Line<Point3>>, Matrix4>;
/// `surface_of_linear_extrusion`, realized in `truck`
pub type StepExtrudedCurve = ExtrudedCurve<Curve3D, Vector3>;
/// `surface_of_revolution`, realized in `truck`
pub type StepRevolutedCurve = Processor<RevolutedCurve<Curve3D>, Matrix4>;
/// `pcurve`, realized in `truck`
pub type PCurve = truck_geometry::prelude::PCurve<Box<Curve2D>, Box<Surface>>;
/// `conic` in 2D, realized in `truck`
#[derive(
Clone,
Copy,
Debug,
PartialEq,
From,
Serialize,
Deserialize,
ParametricCurve,
BoundedCurve,
Cut,
Invertible,
ParameterDivision1D,
SearchParameterD1,
SearchNearestParameterD1,
TransformedM3,
StepLength,
DisplayByStep,
StepCurve,
)]
pub enum Conic2D {
Ellipse(Ellipse<Point2, Matrix3>),
Hyperbola(Hyperbola<Point2, Matrix3>),
Parabola(Parabola<Point2, Matrix3>),
}
/// `curve` in 2D, realized in `truck`
#[derive(
Clone,
Debug,
PartialEq,
From,
Serialize,
Deserialize,
ParametricCurve,
BoundedCurve,
Cut,
Invertible,
ParameterDivision1D,
SearchParameterD1,
SearchNearestParameterD1,
TransformedM3,
StepLength,
DisplayByStep,
StepCurve,
)]
pub enum Curve2D {
Line(Line<Point2>),
Polyline(PolylineCurve<Point2>),
Conic(Conic2D),
BSplineCurve(BSplineCurve<Point2>),
NurbsCurve(NurbsCurve<Vector3>),
}
/// `conic` in 3D, realized in `truck`
#[derive(
Clone,
Copy,
Debug,
PartialEq,
From,
Serialize,
Deserialize,
ParametricCurve,
BoundedCurve,
Cut,
Invertible,
ParameterDivision1D,
SearchParameterD1,
SearchNearestParameterD1,
TransformedM4,
StepLength,
DisplayByStep,
StepCurve,
)]
pub enum Conic3D {
Ellipse(Ellipse<Point3, Matrix4>),
Hyperbola(Hyperbola<Point3, Matrix4>),
Parabola(Parabola<Point3, Matrix4>),
}
/// `curve` in 3D, realized in `truck`
#[derive(
Clone,
Debug,
PartialEq,
From,
Serialize,
Deserialize,
ParametricCurve,
BoundedCurve,
Cut,
Invertible,
ParameterDivision1D,
SearchParameterD1,
SearchNearestParameterD1,
TransformedM4,
StepLength,
DisplayByStep,
StepCurve,
)]
pub enum Curve3D {
Line(Line<Point3>),
Polyline(PolylineCurve<Point3>),
Conic(Conic3D),
BSplineCurve(BSplineCurve<Point3>),
PCurve(PCurve),
NurbsCurve(NurbsCurve<Vector4>),
}
/// `elementary_surface`, realized in `truck`
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Serialize,
Deserialize,
ParametricSurface3D,
ParameterDivision2D,
SearchParameterD2,
SearchNearestParameterD2,
Invertible,
TransformedM4,
StepLength,
DisplayByStep,
StepSurface,
)]
pub enum ElementarySurface {
Plane(Plane),
Sphere(SphericalSurface),
CylindricalSurface(CylindricalSurface),
ToroidalSurface(ToroidalSurface),
ConicalSurface(ConicalSurface),
}
/// `swept_surface`, realized in `truck`
#[derive(
Clone,
Debug,
PartialEq,
Serialize,
Deserialize,
ParametricSurface3D,
ParameterDivision2D,
SearchParameterD2,
SearchNearestParameterD2,
Invertible,
TransformedM4,
StepLength,
DisplayByStep,
StepSurface,
)]
pub enum SweptCurve {
ExtrudedCurve(StepExtrudedCurve),
RevolutedCurve(StepRevolutedCurve),
}
/// `surface`, realized in `truck`
#[derive(
Clone,
Debug,
PartialEq,
Serialize,
Deserialize,
ParametricSurface3D,
ParameterDivision2D,
SearchParameterD2,
SearchNearestParameterD2,
Invertible,
TransformedM4,
StepLength,
StepSurface,
)]
pub enum Surface {
ElementarySurface(Box<ElementarySurface>),
SweptCurve(Box<SweptCurve>),
BSplineSurface(Box<BSplineSurface<Point3>>),
NurbsSurface(Box<NurbsSurface<Vector4>>),
}
impl truck_stepio::out::DisplayByStep for Surface {
fn fmt(&self, idx: usize, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use Surface::*;
match self {
ElementarySurface(x) => x.fmt(idx, f),
SweptCurve(x) => x.fmt(idx, f),
BSplineSurface(x) => x.fmt(idx, f),
NurbsSurface(x) => x.fmt(idx, f),
}
}
}
/// `spherical_surface`, realized in `truck`
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, StepSurface)]
pub struct Sphere(pub truck_geometry::prelude::Sphere);
impl truck_stepio::out::StepSurface for Processor<Sphere, Matrix4> {
#[inline(always)]
fn same_sense(&self) -> bool { self.orientation() }
}
mod sphere {
use super::*;
use std::f64::consts::PI;
use std::ops::Bound;
impl ParametricSurface for Sphere {
type Point = Point3;
type Vector = Vector3;
#[inline]
fn subs(&self, u: f64, v: f64) -> Point3 { self.0.subs(PI / 2.0 - v, u) }
#[inline]
fn uder(&self, u: f64, v: f64) -> Vector3 { self.0.vder(PI / 2.0 - v, u) }
#[inline]
fn vder(&self, u: f64, v: f64) -> Vector3 { -self.0.uder(PI / 2.0 - v, u) }
#[inline]
fn uuder(&self, u: f64, v: f64) -> Vector3 { self.0.vvder(PI / 2.0 - v, u) }
#[inline]
fn uvder(&self, u: f64, v: f64) -> Vector3 { -self.0.uvder(PI / 2.0 - v, u) }
#[inline]
fn vvder(&self, u: f64, v: f64) -> Vector3 { self.0.uuder(PI / 2.0 - v, u) }
#[inline]
fn parameter_range(&self) -> (ParameterRange, ParameterRange) {
(
(Bound::Included(0.0), Bound::Excluded(2.0 * PI)),
(Bound::Included(-PI / 2.0), Bound::Excluded(PI / 2.0)),
)
}
#[inline]
fn u_period(&self) -> Option<f64> { Some(2.0 * PI) }
}
impl ParametricSurface3D for Sphere {
#[inline]
fn normal(&self, u: f64, v: f64) -> Vector3 { self.0.normal(PI / 2.0 - v, u) }
}
impl SearchNearestParameter<D2> for Sphere {
type Point = Point3;
#[inline]
fn search_nearest_parameter<H: Into<SPHint2D>>(
&self,
point: Self::Point,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
self.0
.search_nearest_parameter(point, hint, trials)
.map(|(u, v)| (v, PI / 2.0 - u))
}
}
impl SearchParameter<D2> for Sphere {
type Point = Point3;
#[inline]
fn search_parameter<H: Into<SPHint2D>>(
&self,
point: Self::Point,
hint: H,
trials: usize,
) -> Option<(f64, f64)> {
self.0
.search_parameter(point, hint, trials)
.map(|(u, v)| (v, PI / 2.0 - u))
}
}
impl ParameterDivision2D for Sphere {
#[inline]
fn parameter_division(
&self,
((u0, u1), (v0, v1)): ((f64, f64), (f64, f64)),
tol: f64,
) -> (Vec<f64>, Vec<f64>) {
let range = ((PI / 2.0 - v1, PI / 2.0 - v0), (u0, u1));
let (udiv0, vdiv0) = self.0.parameter_division(range, tol);
let vdiv = udiv0.into_iter().map(|u| PI / 2.0 - u).collect();
(vdiv0, vdiv)
}
}
#[cfg(test)]
proptest::proptest! {
#[test]
fn surface(
center in proptest::array::uniform3(-100.0f64..100.0f64),
radius in 0.1f64..100.0f64,
(u, v) in (0.0..=2.0 * PI, -PI / 2.0..=PI / 2.0),
) {
const EPS: f64 = 1.0e-3;
let sphere = Sphere(truck_geometry::prelude::Sphere::new(center.into(), radius));
let uder0 = sphere.uder(u, v);
let uder1 = (sphere.subs(u + EPS, v) - sphere.subs(u - EPS, v)) / (2.0 * EPS);
assert!(
(uder0 - uder1).magnitude2() < EPS,
"uder failed: {uder0:?}, {uder1:?}"
);
let vder0 = sphere.vder(u, v);
let vder1 = (sphere.subs(u, v + EPS) - sphere.subs(u, v - EPS)) / (2.0 * EPS);
assert!(
(vder0 - vder1).magnitude2() < EPS,
"vder failed: {vder0:?}, {vder1:?}"
);
let uuder0 = sphere.uuder(u, v);
let uuder1 = (sphere.uder(u + EPS, v) - sphere.uder(u - EPS, v)) / (2.0 * EPS);
assert!(
(uuder0 - uuder1).magnitude2() < EPS,
"uuder failed: {uuder0:?}, {uuder1:?}"
);
let uvder0 = sphere.uvder(u, v);
let uvder1 = (sphere.uder(u, v + EPS) - sphere.uder(u, v - EPS)) / (2.0 * EPS);
assert!(
(uvder0 - uvder1).magnitude2() < EPS,
"uvder failed: {uvder0:?}, {uvder1:?}"
);
let vvder0 = sphere.vvder(u, v);
let vvder1 = (sphere.vder(u, v + EPS) - sphere.vder(u, v - EPS)) / (2.0 * EPS);
assert!(
(vvder0 - vvder1).magnitude2() < EPS,
"vvder failed: {vvder0:?}, {vvder1:?}"
);
let n0 = sphere.normal(u, v);
let n1 = sphere.uder(u, v).cross(sphere.vder(u, v)).normalize();
assert!(
(n0 - n1).magnitude2() < EPS,
"normal failed: {n0:?}, {n1:?}"
);
}
}
}
impl truck_stepio::out::ConstStepLength for Processor<Sphere, Matrix4> {
const LENGTH: usize = Processor::<truck_geometry::prelude::Sphere, Matrix4>::LENGTH;
}
impl truck_stepio::out::StepLength for Processor<Sphere, Matrix4> {
fn step_length(&self) -> usize { <Self as truck_stepio::out::ConstStepLength>::LENGTH }
}
impl truck_stepio::out::DisplayByStep for Processor<Sphere, Matrix4> {
fn fmt(&self, idx: usize, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Processor::new(self.entity().0)
.transformed(*self.transform())
.fmt(idx, f)
}
}
/// Implementation required to apply a closed surface division to a shape parsed from a STEP file.
mod from_pcurve {
use super::{Curve2D, Curve3D, Surface};
use truck_geometry::prelude::*;
impl From<PCurve<Line<Point2>, Surface>> for Curve3D {
fn from(value: PCurve<Line<Point2>, Surface>) -> Self {
let (line, surface) = value.decompose();
Curve3D::PCurve(PCurve::new(Curve2D::Line(line).into(), surface.into()))
}
}
}
-2784
View File
File diff suppressed because it is too large Load Diff
-78
View File
@@ -1,78 +0,0 @@
//! Reads/writes STEP files from/to truck.
//!
//! # Current Status
//!
//! It is possible to output data modeled by truck-modeling.
//! Shapes created by set operations cannot be output yet.
//! Input will come further down the road.
// SolidCraft: compiler warnings no longer fail the build.
#![deny(clippy::all, rust_2018_idioms)]
#![warn(
missing_docs,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications
)]
/// STEP input module
/// # Example
/// ```
/// use truck_stepio::r#in::{*, alias::*};
/// use ruststep::tables::EntityTable;
/// // read file
/// let step_string = include_str!(concat!(
/// env!("CARGO_MANIFEST_DIR"),
/// "/../resources/step/occt-cube.step",
/// ));
/// // parse step file
/// let exchange = ruststep::parser::parse(&step_string).unwrap();
/// // convert the parsing results to a Rust struct
/// let table = Table::from_data_section(&exchange.data[0]);
/// // get `CartesianPoint` registered in #102
/// let step_point = EntityTable::<CartesianPointHolder>::get_owned(&table, 102).unwrap();
/// // convert `CartesianPoint` in STEP to `Point3` in cgmath
/// let cgmath_point = Point3::from(&step_point);
/// // check parse result
/// assert_eq!(cgmath_point, Point3::new(0.0, 10.0, 0.0));
/// ```
#[cfg(feature = "in")]
pub mod r#in;
/// STEP output module
pub mod out;
#[doc(hidden)]
#[macro_export]
macro_rules! impl_from {
($(impl From<&$refed: ty> for $converted: ty {
$from_func: item
})*) => {
$(impl From<&$refed> for $converted {
$from_func
}
impl From<$refed> for $converted {
fn from(x: $refed) -> Self { Self::from(&x) }
})*
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! impl_try_from {
($(impl TryFrom<&$refed: ty> for $converted: ty {
$try_from_func: item
})*) => {
$(impl TryFrom<&$refed> for $converted {
type Error = ExpressParseError;
$try_from_func
}
impl TryFrom<$refed> for $converted {
type Error = ExpressParseError;
fn try_from(x: $refed) -> Result<Self, ExpressParseError> { Self::try_from(&x) }
})*
};
}
-834
View File
@@ -1,834 +0,0 @@
use super::{Result, *};
use truck_geometry::prelude::*;
use truck_modeling::{Curve as ModelingCurve, Leader, Surface as ModelingSurface};
use truck_polymesh::PolylineCurve;
impl DisplayByStep for Point2 {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"#{idx} = CARTESIAN_POINT('', {coordinates});\n",
coordinates = SliceDisplay(AsRef::<[f64; 2]>::as_ref(self)),
))
}
}
impl_const_step_length!(Point2, 1);
impl DisplayByStep for Point3 {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"#{idx} = CARTESIAN_POINT('', {coordinates});\n",
coordinates = SliceDisplay(AsRef::<[f64; 3]>::as_ref(self)),
))
}
}
impl_const_step_length!(Point3, 1);
/// class for display `DIRECTION`.
#[derive(Clone, Debug, Copy)]
pub struct VectorAsDirection<V>(pub V);
impl DisplayByStep for VectorAsDirection<Vector2> {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"#{idx} = DIRECTION('', {direction_ratios});\n",
direction_ratios = SliceDisplay(AsRef::<[f64; 2]>::as_ref(&self.0)),
))
}
}
impl DisplayByStep for VectorAsDirection<Vector3> {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"#{idx} = DIRECTION('', {direction_ratios});\n",
direction_ratios = SliceDisplay(AsRef::<[f64; 3]>::as_ref(&self.0)),
))
}
}
impl_const_step_length!(VectorAsDirection<V>, 1, <V>);
impl<V> DisplayByStep for V
where
V: InnerSpace<Scalar = f64>,
VectorAsDirection<V>: DisplayByStep,
{
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let magnitude = self.magnitude();
let direction_idx = idx + 1;
f.write_fmt(format_args!(
"#{idx} = VECTOR('', #{direction_idx}, {magnitude});\n{direction}",
direction = StepDisplay::new(VectorAsDirection(*self / magnitude), direction_idx),
magnitude = FloatDisplay(magnitude),
))
}
}
impl_const_step_length!(Vector2, 2);
impl_const_step_length!(Vector3, 2);
impl<P> DisplayByStep for Line<P>
where
P: EuclideanSpace + ConstStepLength + DisplayByStep,
P::Diff: DisplayByStep,
{
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let pnt_idx = idx + 1;
let dir_idx = idx + 1 + P::LENGTH;
f.write_fmt(format_args!(
"#{idx} = LINE('', #{pnt_idx}, #{dir_idx});\n{pnt}{dir}",
pnt = StepDisplay::new(self.0, pnt_idx),
dir = StepDisplay::new(self.1 - self.0, dir_idx),
))
}
}
impl<P> StepLength for Line<P>
where
P: EuclideanSpace + ConstStepLength,
P::Diff: ConstStepLength,
{
#[inline(always)]
fn step_length(&self) -> usize { <Self as ConstStepLength>::LENGTH }
}
impl<P> ConstStepLength for Line<P>
where
P: EuclideanSpace + ConstStepLength,
P::Diff: ConstStepLength,
{
const LENGTH: usize = 1 + P::LENGTH + P::Diff::LENGTH;
}
impl<P> StepCurve for Line<P> {}
impl<P> DisplayByStep for PolylineCurve<P>
where P: Copy + ConstStepLength + DisplayByStep
{
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"#{idx} = POLYLINE('', {range});\n",
range = IndexSliceDisplay(idx + 1..=idx + self.0.len())
))?;
let closure = |(i, p): (usize, &P)| p.fmt(idx + 1 + i * P::LENGTH, f);
self.0.iter().enumerate().try_for_each(closure)
}
}
impl<P: ConstStepLength> StepLength for PolylineCurve<P> {
#[inline(always)]
fn step_length(&self) -> usize { 1 + self.0.len() * P::LENGTH }
}
impl<P> StepCurve for PolylineCurve<P> {}
impl<P> DisplayByStep for BSplineCurve<P>
where P: Copy + ConstStepLength + DisplayByStep
{
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let (knots, multi) = self.knot_vec().to_single_multi();
let control_points_instances = self
.control_points()
.iter()
.enumerate()
.map(|(i, p)| StepDisplay::new(*p, idx + 1 + i * P::LENGTH))
.collect::<Vec<_>>();
f.write_fmt(format_args!(
"#{idx} = B_SPLINE_CURVE_WITH_KNOTS('', {degree}, {control_points_list}, .UNSPECIFIED., .U., .U., {knot_multiplicities}, {knots}, .UNSPECIFIED.);\n{control_points_instances}",
degree = self.degree(),
control_points_list = IndexSliceDisplay((idx + 1..=idx + self.control_points().len() * P::LENGTH).step_by(P::LENGTH)),
knot_multiplicities = SliceDisplay(&multi),
knots = SliceDisplay(&knots),
control_points_instances = SliceDisplay(&control_points_instances),
))
}
}
impl<P> StepLength for BSplineCurve<P> {
#[inline(always)]
fn step_length(&self) -> usize { self.control_points().len() + 1 }
}
impl<P> StepCurve for BSplineCurve<P> {}
impl<V> DisplayByStep for NurbsCurve<V>
where
V: Homogeneous<f64>,
V::Point: ConstStepLength + DisplayByStep,
{
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let (knots, multi) = self.knot_vec().to_single_multi();
let control_points_instances = self
.control_points()
.iter()
.enumerate()
.map(|(i, v)| StepDisplay::new(v.to_point(), idx + 1 + i * V::Point::LENGTH))
.collect::<Vec<_>>();
let weights = self
.control_points()
.iter()
.map(|v| v.weight())
.collect::<Vec<_>>();
f.write_fmt(format_args!(
"#{idx} = (
BOUNDED_CURVE()
B_SPLINE_CURVE({degree}, {control_points_list}, .UNSPECIFIED., .U., .U.)
B_SPLINE_CURVE_WITH_KNOTS({knot_multiplicities}, {knots}, .UNSPECIFIED.)
CURVE()
GEOMETRIC_REPRESENTATION_ITEM()
RATIONAL_B_SPLINE_CURVE({weights})
REPRESENTATION_ITEM('')
);\n{control_points_instances}",
degree = self.degree(),
control_points_list = IndexSliceDisplay(
(idx + 1..=idx + self.control_points().len() * V::Point::LENGTH)
.step_by(V::Point::LENGTH)
),
knot_multiplicities = SliceDisplay(&multi),
knots = SliceDisplay(&knots),
weights = SliceDisplay(&weights),
control_points_instances = SliceDisplay(&control_points_instances),
))
}
}
impl<V> StepLength for NurbsCurve<V> {
#[inline(always)]
fn step_length(&self) -> usize { self.control_points().len() + 1 }
}
impl<V> StepCurve for NurbsCurve<V> {}
impl DisplayByStep for Processor<TrimmedCurve<UnitCircle<Point2>>, Matrix3> {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let transform = *self.transform();
let position_idx = idx + 1;
let location_idx = idx + 2;
let ref_direction_idx = idx + 3;
let r0 = transform[0].magnitude();
let r1 = transform[1].magnitude();
let ref_direction = VectorAsDirection(transform[0].truncate() / r0);
let location = transform[2].to_point();
if r0.near(&r1) {
let r = FloatDisplay(r0);
f.write_fmt(format_args!("#{idx} = CIRCLE('', #{position_idx}, {r});\n"))?;
} else {
let (r0, r1) = (FloatDisplay(r0), FloatDisplay(r1));
f.write_fmt(format_args!(
"#{idx} = ELLIPSE('', #{position_idx}, {r0}, {r1});\n"
))?;
}
f.write_fmt(format_args!(
"#{position_idx} = AXIS2_PLACEMENT_2D('', #{location_idx}, #{ref_direction_idx});\n",
))?;
DisplayByStep::fmt(&location, location_idx, f)?;
DisplayByStep::fmt(&ref_direction, ref_direction_idx, f)
}
}
impl_const_step_length!(Processor<TrimmedCurve<UnitCircle<Point2>>, Matrix3>, 4);
impl DisplayByStep for Processor<TrimmedCurve<UnitCircle<Point3>>, Matrix4> {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let transform = self.transform();
let position_idx = idx + 1;
let location_idx = idx + 2;
let axis_idx = idx + 3;
let ref_direction_idx = idx + 4;
let location = transform[3].to_point();
let axis = VectorAsDirection(transform[2].truncate().normalize());
let r0 = transform[0].magnitude();
let r1 = transform[1].magnitude();
let ref_direction = VectorAsDirection(transform[0].truncate() / r0);
if r0.near(&r1) {
let r = FloatDisplay(r0);
f.write_fmt(format_args!("#{idx} = CIRCLE('', #{position_idx}, {r});\n"))?;
} else {
let (r0, r1) = (FloatDisplay(r0), FloatDisplay(r1));
f.write_fmt(format_args!(
"#{idx} = ELLIPSE('', #{position_idx}, {r0}, {r1});\n"
))?;
}
f.write_fmt(format_args!(
"#{position_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{axis_idx}, #{ref_direction_idx});\n",
))?;
DisplayByStep::fmt(&location, location_idx, f)?;
DisplayByStep::fmt(&axis, axis_idx, f)?;
DisplayByStep::fmt(&ref_direction, ref_direction_idx, f)
}
}
impl_const_step_length!(Processor<TrimmedCurve<UnitCircle<Point3>>, Matrix4>, 5);
impl DisplayByStep for Processor<TrimmedCurve<UnitHyperbola<Point2>>, Matrix3> {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let transform = *self.transform();
let position_idx = idx + 1;
let location_idx = idx + 2;
let ref_direction_idx = idx + 3;
let r0 = transform[0].magnitude();
let r1 = transform[1].magnitude();
let ref_direction_raw = VectorAsDirection(transform[0].truncate() / r0);
let ref_direction = StepDisplay::new(ref_direction_raw, ref_direction_idx);
let location = StepDisplay::new(transform[2].to_point(), location_idx);
let (r0, r1) = (FloatDisplay(r0), FloatDisplay(r1));
f.write_fmt(format_args!(
"#{idx} = HYPERBOLA('', #{position_idx}, {r0}, {r1});
#{position_idx} = AXIS2_PLACEMENT_2D('', #{location_idx}, #{ref_direction_idx});
{location}{ref_direction}"
))
}
}
impl_const_step_length!(Processor<TrimmedCurve<UnitHyperbola<Point2>>, Matrix3>, 4);
impl DisplayByStep for Processor<TrimmedCurve<UnitHyperbola<Point3>>, Matrix4> {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let transform = self.transform();
let position_idx = idx + 1;
let location_idx = idx + 2;
let axis_idx = idx + 3;
let ref_direction_idx = idx + 4;
let location = StepDisplay::new(transform[3].to_point(), location_idx);
let axis_raw = VectorAsDirection(transform[2].truncate().normalize());
let axis = StepDisplay::new(axis_raw, axis_idx);
let r0 = transform[0].magnitude();
let r1 = transform[1].magnitude();
let ref_direction_raw = VectorAsDirection(transform[0].truncate() / r0);
let ref_direction = StepDisplay::new(ref_direction_raw, ref_direction_idx);
let (r0, r1) = (FloatDisplay(r0), FloatDisplay(r1));
f.write_fmt(format_args!(
"#{idx} = HYPERBOLA('', #{position_idx}, {r0}, {r1});
#{position_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{axis_idx}, #{ref_direction_idx});
{location}{axis}{ref_direction}"
))
}
}
impl_const_step_length!(Processor<TrimmedCurve<UnitHyperbola<Point3>>, Matrix4>, 5);
impl DisplayByStep for Processor<TrimmedCurve<UnitParabola<Point2>>, Matrix3> {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let transform = *self.transform();
let position_idx = idx + 1;
let location_idx = idx + 2;
let ref_direction_idx = idx + 3;
let r0 = transform[0].magnitude();
let r1 = transform[1].magnitude();
let focal_dist = FloatDisplay(r1 * r1 / r0);
let ref_direction_raw = VectorAsDirection(transform[0].truncate() / r0);
let ref_direction = StepDisplay::new(ref_direction_raw, ref_direction_idx);
let location = StepDisplay::new(transform[2].to_point(), location_idx);
f.write_fmt(format_args!(
"#{idx} = PARABOLA('', #{position_idx}, {focal_dist});
#{position_idx} = AXIS2_PLACEMENT_2D('', #{location_idx}, #{ref_direction_idx});
{location}{ref_direction}"
))
}
}
impl_const_step_length!(Processor<TrimmedCurve<UnitParabola<Point2>>, Matrix3>, 4);
impl DisplayByStep for Processor<TrimmedCurve<UnitParabola<Point3>>, Matrix4> {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let transform = self.transform();
let position_idx = idx + 1;
let location_idx = idx + 2;
let axis_idx = idx + 3;
let ref_direction_idx = idx + 4;
let location = StepDisplay::new(transform[3].to_point(), location_idx);
let axis_raw = VectorAsDirection(transform[2].truncate().normalize());
let axis = StepDisplay::new(axis_raw, axis_idx);
let r0 = transform[0].magnitude();
let r1 = transform[1].magnitude();
let focal_dist = FloatDisplay(r1 * r1 / r0);
let ref_direction_raw = VectorAsDirection(transform[0].truncate() / r0);
let ref_direction = StepDisplay::new(ref_direction_raw, ref_direction_idx);
f.write_fmt(format_args!(
"#{idx} = PARABOLA('', #{position_idx}, {focal_dist});
#{position_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{axis_idx}, #{ref_direction_idx});
{location}{axis}{ref_direction}"
))
}
}
impl_const_step_length!(Processor<TrimmedCurve<UnitParabola<Point3>>, Matrix4>, 5);
impl<C, M: One> StepCurve for Processor<C, M> {
#[inline(always)]
fn same_sense(&self) -> bool { self.orientation() }
}
impl<C, S> DisplayByStep for IntersectionCurve<C, S>
where
C: StepLength + DisplayByStep,
S: StepLength + DisplayByStep,
{
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let curve_idx = idx + 1;
let surface0_idx = curve_idx + self.leader().step_length();
let surface1_idx = surface0_idx + self.surface0().step_length();
f.write_fmt(format_args!(
"#{idx} = INTERSECTION_CURVE('', #{curve_idx}, (#{surface0_idx}, #{surface1_idx}), .CURVE_3D.);\n"
))?;
self.leader().fmt(curve_idx, f)?;
self.surface0().fmt(surface0_idx, f)?;
// SolidCraft: the second surface was written at the first one's index (duplicate ids).
self.surface1().fmt(surface1_idx, f)
}
}
impl<C: StepLength, S: StepLength> StepLength for IntersectionCurve<C, S> {
#[inline(always)]
fn step_length(&self) -> usize {
1 + self.leader().step_length()
+ self.surface0().step_length()
+ self.surface1().step_length()
}
}
impl<C, S> ConstStepLength for IntersectionCurve<C, S>
where
C: ConstStepLength,
S: ConstStepLength,
{
const LENGTH: usize = 1 + C::LENGTH + S::LENGTH + S::LENGTH;
}
impl<C: StepCurve, S> StepCurve for IntersectionCurve<C, S> {
#[inline(always)]
fn same_sense(&self) -> bool { self.leader().same_sense() }
}
impl<C, S> DisplayByStep for PCurve<C, S>
where
C: DisplayByStep,
S: DisplayByStep + StepLength,
{
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let surface_idx = idx + 1;
let repr_idx = surface_idx + self.surface().step_length();
let context_idx = repr_idx + 1;
let curve_idx = repr_idx + 2;
let curve = StepDisplay::new(self.curve(), curve_idx);
let surface = StepDisplay::new(self.surface(), surface_idx);
f.write_fmt(format_args!(
"#{idx} = PCURVE('', #{surface_idx}, #{repr_idx});
{surface}#{repr_idx} = DEFINITIONAL_REPRESENTATION('', (#{curve_idx}), #{context_idx});
#{context_idx} = (
GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT()
REPRESENTATION_CONTEXT('2D SPACE', '')
);
{curve}"
))
}
}
impl<C: StepLength, S: StepLength> StepLength for PCurve<C, S> {
fn step_length(&self) -> usize { 3 + self.curve().step_length() + self.surface().step_length() }
}
impl<C, S> ConstStepLength for PCurve<C, S>
where
C: ConstStepLength,
S: ConstStepLength,
{
const LENGTH: usize = 3 + C::LENGTH + S::LENGTH;
}
impl<C: StepCurve, S> StepCurve for PCurve<C, S> {
#[inline(always)]
fn same_sense(&self) -> bool { self.curve().same_sense() }
}
impl DisplayByStep for Leader {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
match self {
Leader::Polyline(x) => DisplayByStep::fmt(x, idx, f),
Leader::BSpline(x) => DisplayByStep::fmt(x, idx, f),
}
}
}
impl StepLength for Leader {
fn step_length(&self) -> usize {
match self {
Leader::Polyline(x) => x.step_length(),
Leader::BSpline(x) => x.step_length(),
}
}
}
impl StepCurve for Leader {}
impl DisplayByStep for ModelingCurve {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
match self {
ModelingCurve::Line(x) => DisplayByStep::fmt(x, idx, f),
ModelingCurve::BSplineCurve(x) => DisplayByStep::fmt(x, idx, f),
ModelingCurve::NurbsCurve(x) => DisplayByStep::fmt(x, idx, f),
ModelingCurve::IntersectionCurve(x) => DisplayByStep::fmt(x, idx, f),
}
}
}
impl StepLength for ModelingCurve {
fn step_length(&self) -> usize {
match self {
ModelingCurve::Line(_) => Line::<Point3>::LENGTH,
ModelingCurve::BSplineCurve(x) => x.step_length(),
ModelingCurve::NurbsCurve(x) => x.step_length(),
ModelingCurve::IntersectionCurve(x) => x.step_length(),
}
}
}
impl StepCurve for ModelingCurve {}
impl DisplayByStep for Plane {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let axis2_placement_idx = idx + 1;
let location_idx = idx + 2;
let z_axis_idx = idx + 3;
let x_axis_idx = idx + 4;
f.write_fmt(format_args!(
"#{idx} = PLANE('', #{axis2_placement_idx});
#{axis2_placement_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{z_axis_idx}, #{x_axis_idx});
{location}{z_axis}{x_axis}",
location = StepDisplay::new(self.origin(), location_idx),
z_axis = StepDisplay::new(VectorAsDirection(self.normal()), z_axis_idx),
x_axis = StepDisplay::new(VectorAsDirection(self.u_axis().normalize()), x_axis_idx)
))
}
}
impl_const_step_length!(Plane, 5);
impl StepSurface for Plane {}
impl DisplayByStep for Processor<Sphere, Matrix4> {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let sphere = *self.entity();
let transform = self.transform();
let position_idx = idx + 1;
let location_idx = idx + 2;
let axis_idx = idx + 3;
let ref_direction_idx = idx + 4;
let location = transform[3].to_point() + sphere.center().to_vec();
let axis = VectorAsDirection(transform[2].truncate().normalize());
let r0 = transform[0].magnitude();
let r1 = transform[1].magnitude();
if !r0.near(&r1) {
f.write_str("The transform of sphere includes non-uniform scale.")?;
return ERR;
}
let ref_direction = VectorAsDirection(transform[0].truncate() / r0);
let r = FloatDisplay(r0 * sphere.radius());
f.write_fmt(format_args!(
"#{idx} = SPHERICAL_SURFACE('', #{position_idx}, {r});
#{position_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{axis_idx}, #{ref_direction_idx});\n"
))?;
DisplayByStep::fmt(&location, location_idx, f)?;
DisplayByStep::fmt(&axis, axis_idx, f)?;
DisplayByStep::fmt(&ref_direction, ref_direction_idx, f)
}
}
impl_const_step_length!(Processor<Sphere, Matrix4>, 5);
impl DisplayByStep for Sphere {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
DisplayByStep::fmt(&Processor::new(*self), idx, f)
}
}
impl_const_step_length!(Sphere, 5);
impl StepSurface for Sphere {}
impl DisplayByStep for Processor<Torus, Matrix4> {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let torus = *self.entity();
let transform = self.transform();
let position_idx = idx + 1;
let location_idx = idx + 2;
let axis_idx = idx + 3;
let ref_direction_idx = idx + 4;
let location = transform[3].to_point() + torus.center().to_vec();
let axis = VectorAsDirection(transform[2].truncate().normalize());
let r0 = transform[0].magnitude();
let r1 = transform[1].magnitude();
if !r0.near(&r1) {
f.write_str("The transform of sphere includes non-uniform scale.")?;
return ERR;
}
let ref_direction = VectorAsDirection(transform[0].truncate() / r0);
let greater = FloatDisplay(r0 * torus.large_radius());
let lesser = FloatDisplay(r0 * torus.small_radius());
f.write_fmt(format_args!(
"#{idx} = TOROIDAL_SURFACE('', #{position_idx}, {greater}, {lesser});
#{position_idx} = AXIS2_PLACEMENT_3D('', #{location_idx}, #{axis_idx}, #{ref_direction_idx});\n",
))?;
DisplayByStep::fmt(&location, location_idx, f)?;
DisplayByStep::fmt(&axis, axis_idx, f)?;
DisplayByStep::fmt(&ref_direction, ref_direction_idx, f)
}
}
impl_const_step_length!(Processor<Torus, Matrix4>, 5);
impl StepSurface for Processor<Torus, Matrix4> {
#[inline(always)]
fn same_sense(&self) -> bool { self.orientation() }
}
impl DisplayByStep for Torus {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
DisplayByStep::fmt(&Processor::new(*self), idx, f)
}
}
impl_const_step_length!(Torus, 5);
impl StepSurface for Torus {}
impl<P> DisplayByStep for BSplineSurface<P>
where P: Copy + DisplayByStep
{
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let control_points = self.control_points();
let control_points_instances = self
.control_points()
.iter()
.flatten()
.enumerate()
.map(|(i, p)| StepDisplay::new(*p, idx + i + 1))
.collect::<Vec<_>>();
let mut counter = 0;
let control_points_list = control_points
.iter()
.map(|slice| {
counter += slice.len();
IndexSliceDisplay(idx + counter - slice.len() + 1..=idx + counter)
})
.collect::<Vec<_>>();
let (uknots, umulti) = self.uknot_vec().to_single_multi();
let (vknots, vmulti) = self.vknot_vec().to_single_multi();
f.write_fmt(format_args!(
"#{idx} = B_SPLINE_SURFACE_WITH_KNOTS('', {u_degree}, {v_degree}, {control_points_list}, .UNSPECIFIED., .U., .U., .U., \
{u_multiplicities}, {v_multiplicities}, {u_knots}, {v_knots}, .UNSPECIFIED.);\n{control_points_instances}",
u_degree = self.udegree(),
v_degree = self.vdegree(),
control_points_list = SliceDisplay(&control_points_list),
u_multiplicities = SliceDisplay(&umulti),
v_multiplicities = SliceDisplay(&vmulti),
u_knots = SliceDisplay(&uknots),
v_knots = SliceDisplay(&vknots),
control_points_instances = SliceDisplay(&control_points_instances),
))
}
}
impl<P> StepLength for BSplineSurface<P> {
#[inline(always)]
fn step_length(&self) -> usize { 1 + self.control_points().iter().map(Vec::len).sum::<usize>() }
}
impl<P> StepSurface for BSplineSurface<P> {}
impl<V> DisplayByStep for NurbsSurface<V>
where
V: Homogeneous<f64>,
V::Point: Copy + DisplayByStep,
{
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let control_points_instances = self
.control_points()
.iter()
.flatten()
.enumerate()
.map(|(i, v)| StepDisplay::new(v.to_point(), idx + i + 1))
.collect::<Vec<_>>();
let mut counter = 0;
let control_points_list = self
.control_points()
.iter()
.map(|slice| {
counter += slice.len();
IndexSliceDisplay(idx + counter - slice.len() + 1..=idx + counter)
})
.collect::<Vec<_>>();
let weights = self
.control_points()
.iter()
.map(|slice| slice.iter().map(|v| v.weight()).collect::<Vec<_>>())
.collect::<Vec<_>>();
let weights = weights
.iter()
.map(|slice| SliceDisplay(slice))
.collect::<Vec<_>>();
let (uknots, umulti) = self.uknot_vec().to_single_multi();
let (vknots, vmulti) = self.vknot_vec().to_single_multi();
f.write_fmt(format_args!(
"#{idx} = (
BOUNDED_SURFACE()
B_SPLINE_SURFACE({u_degree}, {v_degree}, {control_points_list}, .UNSPECIFIED., .U., .U., .U.)
B_SPLINE_SURFACE_WITH_KNOTS({u_multiplicities}, {v_multiplicities}, {u_knots}, {v_knots}, .UNSPECIFIED.)
GEOMETRIC_REPRESENTATION_ITEM()
RATIONAL_B_SPLINE_SURFACE({weights})
REPRESENTATION_ITEM('')
SURFACE()
);\n{control_points_instances}",
u_degree = self.udegree(),
v_degree = self.vdegree(),
control_points_list = SliceDisplay(&control_points_list),
u_multiplicities = SliceDisplay(&umulti),
v_multiplicities = SliceDisplay(&vmulti),
u_knots = SliceDisplay(&uknots),
v_knots = SliceDisplay(&vknots),
control_points_instances = SliceDisplay(&control_points_instances),
weights = SliceDisplay(&weights),
))
}
}
impl<V> StepLength for NurbsSurface<V> {
#[inline(always)]
fn step_length(&self) -> usize { 1 + self.control_points().iter().map(Vec::len).sum::<usize>() }
}
impl<V> StepSurface for NurbsSurface<V> {}
impl<C> DisplayByStep for ExtrudedCurve<C, Vector3>
where C: StepLength + DisplayByStep
{
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let curve = self.entity_curve();
let curve_idx = idx + 1;
let vector_idx = idx + 1 + curve.step_length();
let vector = self.extruding_vector();
f.write_fmt(format_args!(
"#{idx} = SURFACE_OF_LINEAR_EXTRUSION('', #{curve_idx}, #{vector_idx});{}{}",
StepDisplay::new(curve, curve_idx),
StepDisplay::new(vector, vector_idx),
))
}
}
impl<C: StepLength> StepLength for ExtrudedCurve<C, Vector3> {
fn step_length(&self) -> usize { 1 + self.entity_curve().step_length() + Vector3::LENGTH }
}
impl<C: ConstStepLength> ConstStepLength for ExtrudedCurve<C, Vector3> {
const LENGTH: usize = 1 + C::LENGTH + Vector3::LENGTH;
}
impl<C> StepSurface for ExtrudedCurve<C, Vector3> {}
impl<C, T: One> StepSurface for Processor<ExtrudedCurve<C, Vector3>, T> {
#[inline(always)]
fn same_sense(&self) -> bool { self.orientation() }
}
impl<C> DisplayByStep for RevolutedCurve<C>
where C: StepLength + DisplayByStep
{
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let curve = self.entity_curve();
let curve_idx = idx + 1;
let axis_idx = curve_idx + curve.step_length();
let location_idx = axis_idx + 1;
let dir_idx = location_idx + 1;
f.write_fmt(format_args!(
"#{idx} = SURFACE_OF_REVOLUTION('', #{curve_idx}, #{axis_idx});
{curve}#{axis_idx} = AXIS1_PLACEMENT('', #{location_idx}, #{dir_idx});\n{location}{dir}",
curve = StepDisplay::new(curve, curve_idx),
location = StepDisplay::new(self.origin(), location_idx),
dir = StepDisplay::new(VectorAsDirection(self.axis()), dir_idx),
))
}
}
impl<C: StepLength> StepLength for RevolutedCurve<C> {
#[inline(always)]
fn step_length(&self) -> usize { 4 + self.entity_curve().step_length() }
}
impl<C: ConstStepLength> ConstStepLength for RevolutedCurve<C> {
const LENGTH: usize = 4 + C::LENGTH;
}
// ISO 10303-42 parameterises surface_of_revolution by (angle, profile); truck by (profile,
// angle), so the normals are opposite.
impl<C> StepSurface for RevolutedCurve<C> {
#[inline(always)]
fn same_sense(&self) -> bool { false }
}
impl<C> DisplayByStep for Processor<RevolutedCurve<C>, Matrix4>
where C: StepLength + Transformed<Matrix4> + DisplayByStep
{
// SolidCraft: a transform that commutes with turning about the axis keeps the surface one of
// revolution: the transformed profile revolved about the transformed axis. That holds for
// uniform scales (the only case taken before) and also for a scale across the axis that
// differs from the scale along it, which is how a shell or an offset moves a revolved
// cylinder or cone face. Any other transform is still refused.
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
let surface = self.entity();
let transform = *self.transform();
if transform.determinant().so_small() {
f.write_str("Transform is not regular")?;
return ERR;
}
let a = surface.axis().normalize();
let la = transform.transform_vector(a);
if la.so_small() {
f.write_str("Transform is not regular")?;
return ERR;
}
let axis = la.normalize();
// Two directions across the axis, and their images.
let seed = if a.x.abs() < 0.9 { Vector3::unit_x() } else { Vector3::unit_y() };
let u = (seed - a * a.dot(seed)).normalize();
let v = a.cross(u);
let (lu, lv) = (transform.transform_vector(u), transform.transform_vector(v));
let s = lu.magnitude();
// Across the axis: a similarity onto the plane across the image axis.
let commutes = !s.so_small()
&& (lu.dot(axis) / s).so_small()
&& (lv.dot(axis) / s).so_small()
&& (lu.dot(lv) / (s * s)).so_small()
&& (lv.magnitude() / s).near(&1.0);
if !commutes {
f.write_str("Transform does not keep the surface one of revolution.")?;
return ERR;
}
let curve = surface.entity_curve().transformed(transform);
let origin = transform.transform_point(surface.origin());
let surface = RevolutedCurve::by_revolution(curve, origin, axis);
DisplayByStep::fmt(&surface, idx, f)
}
}
impl<C: StepLength> StepLength for Processor<RevolutedCurve<C>, Matrix4> {
fn step_length(&self) -> usize { self.entity().step_length() }
}
// SolidCraft: the written surface revolves the transformed profile about the transformed
// axis; a mirroring transform reverses the turn, and with it the normal (was `!orientation`,
// right only without mirroring). The STEP normal is the opposite of truck's (see above).
impl<C> StepSurface for Processor<RevolutedCurve<C>, Matrix4> {
#[inline(always)]
fn same_sense(&self) -> bool { self.orientation() != (self.transform().determinant() >= 0.0) }
}
impl DisplayByStep for ModelingSurface {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
match self {
ModelingSurface::Plane(x) => DisplayByStep::fmt(x, idx, f),
ModelingSurface::BSplineSurface(x) => DisplayByStep::fmt(x, idx, f),
ModelingSurface::NurbsSurface(x) => DisplayByStep::fmt(x, idx, f),
ModelingSurface::RevolutedCurve(x) => DisplayByStep::fmt(x, idx, f),
}
}
}
impl StepLength for ModelingSurface {
fn step_length(&self) -> usize {
match self {
ModelingSurface::Plane(_) => Plane::LENGTH,
ModelingSurface::BSplineSurface(x) => x.step_length(),
ModelingSurface::NurbsSurface(x) => x.step_length(),
ModelingSurface::RevolutedCurve(x) => x.entity().step_length(),
}
}
}
// SolidCraft: was the default (`true`), which dropped an inverted revolution's orientation.
impl StepSurface for ModelingSurface {
fn same_sense(&self) -> bool {
match self {
ModelingSurface::RevolutedCurve(x) => StepSurface::same_sense(x),
_ => true,
}
}
}
-388
View File
@@ -1,388 +0,0 @@
use std::fmt::{Debug, Display, Formatter, Result};
use truck_topology::compress::*;
use self::topology::PreStepModel;
const ERR: Result = Err(std::fmt::Error);
#[cfg(feature = "derive")]
pub use truck_derivers::{DisplayByStep, StepLength};
/// display boolean number to step file
#[derive(Clone, Copy, Debug)]
pub struct BooleanDisplay(pub bool);
impl Display for BooleanDisplay {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self.0 {
true => f.write_str(".T."),
false => f.write_str(".F."),
}
}
}
/// display float number to step file
#[derive(Clone, Copy, Debug)]
pub struct FloatDisplay(pub f64);
impl Display for FloatDisplay {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let FloatDisplay(x) = *self;
if f64::abs(x) < 1.0e-2 && x != 0.0 {
f.write_fmt(format_args!("{x:.10E}"))
} else {
f.write_fmt(format_args!("{x:?}"))
}
}
}
/// display step slice
/// # Examples
/// ```
/// use truck_stepio::out::SliceDisplay;
/// let slice = &[1.0, 2.0, 3.0, 4.0];
/// let display = SliceDisplay(slice);
/// let step_string = display.to_string();
/// assert_eq!(step_string, "(1.0, 2.0, 3.0, 4.0)");
/// ```
#[derive(Clone, Debug)]
pub struct SliceDisplay<'a, T>(pub &'a [T]);
impl<'a> Display for SliceDisplay<'a, f64> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_str("(")?;
self.0.iter().enumerate().try_for_each(|(i, x)| {
if i != 0 {
f.write_str(", ")?;
}
Display::fmt(&FloatDisplay(*x), f)
})?;
f.write_str(")")
}
}
impl<'a> Display for SliceDisplay<'a, usize> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_str("(")?;
self.0.iter().enumerate().try_for_each(|(i, x)| {
if i != 0 {
f.write_str(", ")?;
}
Display::fmt(x, f)
})?;
f.write_str(")")
}
}
impl<'a> Display for SliceDisplay<'a, String> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_str("(")?;
self.0.iter().enumerate().try_for_each(|(i, x)| {
if i != 0 {
f.write_str(", ")?;
}
f.write_fmt(format_args!("'{x}'"))
})?;
f.write_str(")")
}
}
impl<'a> Display for SliceDisplay<'a, SliceDisplay<'a, f64>> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_str("(")?;
self.0.iter().enumerate().try_for_each(|(i, x)| {
if i != 0 {
f.write_str(", ")?;
}
Display::fmt(x, f)
})?;
f.write_str(")")
}
}
/// display index slice
/// # Examples
/// ```
/// use truck_stepio::out::*;
/// let indices = [1, 10, 100, 1000, 10000];
/// let display = IndexSliceDisplay(indices.into_iter());
/// let step_string = display.to_string();
/// assert_eq!(step_string, "(#1, #10, #100, #1000, #10000)");
/// ```
#[derive(Clone, Debug)]
pub struct IndexSliceDisplay<I>(pub I);
impl<I: Clone + IntoIterator<Item = usize>> Display for IndexSliceDisplay<I> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_str("(")?;
self.0
.clone()
.into_iter()
.enumerate()
.try_for_each(|(i, idx)| {
if i != 0 {
f.write_fmt(format_args!(", #{idx}"))
} else {
f.write_fmt(format_args!("#{idx}"))
}
})?;
f.write_str(")")
}
}
impl<'a, I: Clone + IntoIterator<Item = usize>> Display for SliceDisplay<'a, IndexSliceDisplay<I>> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_str("(")?;
self.0.iter().enumerate().try_for_each(|(i, x)| {
if i != 0 {
f.write_str(", ")?;
}
Display::fmt(x, f)
})?;
f.write_str(")")
}
}
/// trait for outputting by STEP file format.
pub trait DisplayByStep {
/// formatter
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result;
}
impl<T: DisplayByStep> DisplayByStep for &T {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result { DisplayByStep::fmt(*self, idx, f) }
}
impl<T: DisplayByStep> DisplayByStep for Box<T> {
fn fmt(&self, idx: usize, f: &mut Formatter<'_>) -> Result {
DisplayByStep::fmt(self.as_ref(), idx, f)
}
}
/// Display struct for outputting some objects to STEP file format.
#[derive(Clone, Debug)]
pub struct StepDisplay<T> {
entity: T,
idx: usize,
}
impl<'a, T> Display for SliceDisplay<'a, StepDisplay<T>>
where StepDisplay<T>: Display
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.0.iter().try_for_each(|x| Display::fmt(x, f))
}
}
impl<T> StepDisplay<T> {
/// constructor
#[inline]
pub const fn new(entity: T, idx: usize) -> Self { Self { entity, idx } }
/// return entity
#[inline]
pub const fn entity(&self) -> &T { &self.entity }
/// return index
#[inline]
pub const fn index(&self) -> usize { self.idx }
}
impl<T: DisplayByStep> Display for StepDisplay<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result { DisplayByStep::fmt(&self.entity, self.idx, f) }
}
/// Calculate how many lines are used in outputting an object to a STEP file
pub trait StepLength {
/// Calculate how many lines are used in outputting an object to a STEP file
fn step_length(&self) -> usize;
}
impl<T: StepLength> StepLength for &T {
#[inline(always)]
fn step_length(&self) -> usize { StepLength::step_length(*self) }
}
impl<T: StepLength> StepLength for Box<T> {
#[inline(always)]
fn step_length(&self) -> usize { self.as_ref().step_length() }
}
/// Constant numbers of lines for outputting an object to a STEP file.
/// `x.step_length() == X::LENGTH` must always hold.
pub trait ConstStepLength: StepLength {
/// the number of line
const LENGTH: usize;
}
impl<T: ConstStepLength> ConstStepLength for &T {
const LENGTH: usize = T::LENGTH;
}
impl<T: ConstStepLength> ConstStepLength for Box<T> {
const LENGTH: usize = T::LENGTH;
}
macro_rules! impl_const_step_length {
($type: ty, $len: expr $(,<$($gen: ident),*>)?) => {
impl$(<$($gen),*>)? ConstStepLength for $type {
const LENGTH: usize = $len;
}
impl$(<$($gen),*>)? StepLength for $type {
#[inline(always)]
fn step_length(&self) -> usize { <Self as ConstStepLength>::LENGTH }
}
};
}
/// Additional information for output to `edge_curve`.
pub trait StepCurve {
/// the parameter `same_sense`.
#[inline(always)]
fn same_sense(&self) -> bool { true }
}
impl<T: StepCurve> StepCurve for &T {
#[inline(always)]
fn same_sense(&self) -> bool { (*self).same_sense() }
}
impl<T: StepCurve> StepCurve for Box<T> {
#[inline(always)]
fn same_sense(&self) -> bool { self.as_ref().same_sense() }
}
/// Additional information for output to `face_surface`.
pub trait StepSurface {
/// the parameter `same_sense`.
#[inline(always)]
fn same_sense(&self) -> bool { true }
}
impl<T: StepSurface> StepSurface for &T {
#[inline(always)]
fn same_sense(&self) -> bool { (*self).same_sense() }
}
impl<T: StepSurface> StepSurface for Box<T> {
#[inline(always)]
fn same_sense(&self) -> bool { self.as_ref().same_sense() }
}
/// Describe STEP file header
#[derive(Clone, Debug)]
pub struct StepHeaderDescriptor {
/// file name
pub file_name: String,
/// time stamp
pub time_stamp: String,
/// authors
pub authors: Vec<String>,
/// organization
pub organization: Vec<String>,
/// organization system
pub organization_system: String,
/// authorization
pub authorization: String,
}
#[derive(Clone, Debug)]
struct StepHeader {
file_name: String,
time_stamp: String,
authors: Vec<String>,
organization: Vec<String>,
origination_system: String,
authorization: String,
schema: String,
}
impl Default for StepHeaderDescriptor {
fn default() -> Self {
Self {
file_name: Default::default(),
time_stamp: chrono::Utc::now().naive_local().to_string(),
authors: Default::default(),
organization: Default::default(),
organization_system: Default::default(),
authorization: Default::default(),
}
}
}
impl Display for StepHeader {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let empty_string = [String::new()];
f.write_fmt(format_args!(
"HEADER;
FILE_DESCRIPTION(('Shape Data from Truck'), '2;1');
FILE_NAME('{}', '{}', ({}), ({}), 'truck', '{}', '{}');
FILE_SCHEMA(('{}'));
ENDSEC;\n",
self.file_name,
self.time_stamp,
if self.authors.is_empty() {
SliceDisplay(&empty_string)
} else {
SliceDisplay(&self.authors)
},
if self.organization.is_empty() {
SliceDisplay(&empty_string)
} else {
SliceDisplay(&self.organization)
},
self.origination_system,
self.authorization,
self.schema,
))
}
}
/// Display model with configuations
#[derive(Clone, Debug)]
pub struct StepModel<'a, P, C, S>(PreStepModel<'a, P, C, S>);
/// Display models with configuations
#[derive(Clone, Debug)]
pub struct StepModels<'a, P, C, S> {
models: Vec<PreStepModel<'a, P, C, S>>,
next_idx: usize,
}
/// Display struct for outputting STEP file format with header.
#[derive(Clone, Debug)]
pub struct CompleteStepDisplay<T> {
display: T,
header: StepHeader,
}
impl<T: Display> Display for CompleteStepDisplay<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"ISO-10303-21;\n{}DATA;\n{}ENDSEC;\nEND-ISO-10303-21;\n",
self.header, self.display,
))
}
}
impl<T> CompleteStepDisplay<T> {
/// constructor
#[inline]
pub fn new(display: T, header: StepHeaderDescriptor) -> Self {
CompleteStepDisplay {
display,
header: StepHeader {
file_name: header.file_name,
time_stamp: header.time_stamp,
authors: header.authors,
organization: header.organization,
origination_system: header.organization_system,
authorization: header.authorization,
schema: "ISO-10303-042".to_string(),
},
}
}
}
mod geometry;
mod topology;
pub use geometry::VectorAsDirection;
-493
View File
@@ -1,493 +0,0 @@
use super::{Result, *};
#[derive(Clone, Debug)]
pub(super) struct StepShell<'a, P, C, S> {
entity: &'a CompressedShell<P, C, S>,
idx: usize,
face_indices: Vec<usize>,
ep_edges: usize,
ep_vertices: usize,
surface_indices: Vec<usize>,
curve_indices: Vec<usize>,
ep_points: usize,
is_open: bool,
}
impl<'a, P, C, S> StepShell<'a, P, C, S>
where
P: Copy,
C: StepLength,
S: StepLength,
{
fn new(shell: &'a CompressedShell<P, C, S>, idx: usize, is_open: bool) -> Self {
let faces = &shell.faces;
let edges = &shell.edges;
let vertices = &shell.vertices;
let mut cursor = idx + 1;
let face_indices = faces
.iter()
.map(|f| {
let res = cursor;
cursor += match f.boundaries.is_empty() {
true => 5,
false => 1 + f.boundaries.iter().map(|b| 2 + b.len()).sum::<usize>(),
};
res
})
.collect::<Vec<_>>();
let ep_edges = cursor;
let ep_vertices = ep_edges + edges.len();
cursor = ep_vertices + vertices.len();
let surface_indices = faces
.iter()
.map(|f| {
let res = cursor;
cursor += f.surface.step_length();
res
})
.collect::<Vec<_>>();
let curve_indices = edges
.iter()
.map(|e| {
let res = cursor;
cursor += e.curve.step_length();
res
})
.collect::<Vec<_>>();
let ep_points = cursor;
StepShell {
entity: shell,
idx,
face_indices,
ep_edges,
ep_vertices,
surface_indices,
curve_indices,
ep_points,
is_open,
}
}
}
impl<'a, P, C, S> Display for StepShell<'a, P, C, S>
where
P: DisplayByStep + Copy,
C: DisplayByStep + StepCurve,
S: DisplayByStep + StepSurface,
{
fn fmt(&self, formatter: &mut Formatter<'_>) -> Result {
let StepShell {
entity,
idx,
face_indices,
ep_edges,
ep_vertices,
surface_indices,
curve_indices,
ep_points,
is_open,
} = self;
let faces = &entity.faces;
let edges = &entity.edges;
let vertices = &entity.vertices;
let shell_kind = match is_open {
true => "OPEN_SHELL",
false => "CLOSED_SHELL",
};
formatter.write_fmt(format_args!(
"#{idx} = {shell_kind}('', {face_indices});\n",
face_indices = IndexSliceDisplay(self.face_indices.clone()),
))?;
faces.iter().enumerate().try_for_each(|(i, f)| {
let idx = face_indices[i];
let mut cursor = idx + 1;
let face_geometry = surface_indices[i];
let face_bounds = match f.boundaries.is_empty() {
true => vec![cursor],
false => {
let closure = |b: &Vec<CompressedEdgeIndex>| {
let res = cursor;
cursor += 2 + b.len();
res
};
f.boundaries.iter().map(closure).collect()
}
};
formatter.write_fmt(format_args!(
"#{idx} = FACE_SURFACE('', {face_bound}, #{face_geometry}, {same_sense});\n",
same_sense = BooleanDisplay(f.orientation == f.surface.same_sense()),
face_bound = IndexSliceDisplay(face_bounds.clone()),
))?;
cursor = idx + 1;
if f.boundaries.is_empty() {
let face_bound_idx = cursor;
let vertex_loop_idx = cursor + 1;
let vertex_idx = cursor + 2;
let vertex_geometry = cursor + 3;
formatter.write_fmt(format_args!(
"#{face_bound_idx} = FACE_BOUND('', #{vertex_loop_idx}, .T.);
#{vertex_loop_idx} = VERTEX_LOOP('', #{vertex_idx});
#{vertex_idx} = VERTEX_POINT('', #{vertex_geometry});
#{vertex_geometry} = POINT_ON_SURFACE('', #{face_geometry}, 0.0, 0.0);\n"
))?;
}
f.boundaries.iter().try_for_each(|b| {
let face_bound_idx = cursor;
let edge_loop_idx = cursor + 1;
let ep_oriented_edges = cursor + 2;
cursor += 2 + b.len();
formatter.write_fmt(format_args!(
"#{face_bound_idx} = FACE_BOUND('', #{edge_loop_idx}, {orientation});
#{edge_loop_idx} = EDGE_LOOP('', {oriented_edge_indices});\n",
orientation = BooleanDisplay(f.orientation),
oriented_edge_indices =
IndexSliceDisplay(ep_oriented_edges..ep_oriented_edges + b.len()),
))?;
b.iter().enumerate().try_for_each(|(j, ce)| {
formatter.write_fmt(format_args!(
"#{idx} = ORIENTED_EDGE('', *, *, #{edge_element}, {orientation});\n",
idx = ep_oriented_edges + j,
edge_element = ep_edges + ce.index,
orientation = if ce.orientation { ".T." } else { ".F." },
))
})
})
})?;
edges.iter().enumerate().try_for_each(|(i, e)| {
let same_sense = if e.curve.same_sense() { ".T." } else { ".F." };
formatter.write_fmt(format_args!(
"#{idx} = EDGE_CURVE('', #{edge_start}, #{edge_end}, #{edge_geometry}, {same_sense});\n",
idx = ep_edges + i,
edge_start = ep_vertices + e.vertices.0,
edge_end = ep_vertices + e.vertices.1,
edge_geometry = curve_indices[i],
))
})?;
(0..vertices.len()).try_for_each(|i| {
formatter.write_fmt(format_args!(
"#{idx} = VERTEX_POINT('', #{vertex_geometry});\n",
idx = ep_vertices + i,
vertex_geometry = ep_points + i,
))
})?;
faces.iter().zip(surface_indices).try_for_each(|(f, idx)| {
Display::fmt(&StepDisplay::new(&f.surface, *idx), formatter)
})?;
edges
.iter()
.zip(curve_indices)
.try_for_each(|(e, idx)| Display::fmt(&StepDisplay::new(&e.curve, *idx), formatter))?;
vertices
.iter()
.enumerate()
.try_for_each(|(i, v)| Display::fmt(&StepDisplay::new(*v, ep_points + i), formatter))
}
}
impl<'a, P, C, S> StepLength for StepShell<'a, P, C, S> {
fn step_length(&self) -> usize {
1 + self.ep_points + self.entity.vertices.len() - self.face_indices[0]
}
}
#[derive(Clone, Debug)]
pub(super) struct StepSolid<'a, P, C, S> {
idx: usize,
boundaries: Vec<StepShell<'a, P, C, S>>,
}
impl<'a, P, C, S> StepSolid<'a, P, C, S>
where
P: Copy,
C: StepLength,
S: StepLength,
{
fn new(solid: &'a CompressedSolid<P, C, S>, idx: usize) -> Self {
let mut cursor = idx + 1;
let boundaries = solid
.boundaries
.iter()
.map(|shell| {
let res = StepShell::new(shell, cursor, false);
cursor += 1 + res.step_length();
res
})
.collect::<Vec<_>>();
StepSolid { idx, boundaries }
}
}
impl<'a, P, C, S> Display for StepSolid<'a, P, C, S>
where
P: DisplayByStep + Copy,
C: DisplayByStep + StepLength + StepCurve,
S: DisplayByStep + StepLength + StepSurface,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let StepSolid { idx, boundaries } = self;
match boundaries.len() {
0 => {
f.pad("empty solid!")?;
Err(std::fmt::Error)
}
1 => {
let shell_idx = idx + 1;
let step_shell = &boundaries[0];
f.write_fmt(format_args!(
"#{idx} = MANIFOLD_SOLID_BREP('', #{shell_idx});\n"
))?;
Display::fmt(step_shell, f)
}
_ => {
let first_shell_idx = boundaries[0].face_indices[0] - 1;
f.write_fmt(format_args!(
"#{idx} = BREP_WITH_VOIDS('', #{first_shell_idx}, {other_shells});\n",
other_shells = IndexSliceDisplay(
boundaries[1..]
.iter()
.map(|step_shell| step_shell.face_indices[0] - 2)
),
))?;
Display::fmt(&boundaries[0], f)?;
boundaries[1..].iter().try_for_each(|step_shell| {
let oriented_shell_idx = step_shell.face_indices[0] - 2;
let shell_idx = step_shell.face_indices[0] - 1;
f.write_fmt(format_args!(
"#{oriented_shell_idx} = ORIENTED_CLOSED_SHELL('', *, #{shell_idx}, .T.);\n",
))?;
Display::fmt(step_shell, f)
})
}
}
}
}
impl<'a, P, C, S> StepLength for StepSolid<'a, P, C, S> {
fn step_length(&self) -> usize {
let b = &self.boundaries;
match b.len() {
0 => 0,
1 => 1 + b[0].step_length(),
_ => b.len() + b.iter().map(StepLength::step_length).sum::<usize>(),
}
}
}
#[derive(Clone, Debug)]
pub(super) enum PreStepModel<'a, P, C, S> {
/// shell based surface model
Shell(StepShell<'a, P, C, S>),
/// solid model
Solid(StepSolid<'a, P, C, S>),
}
impl<'a, P, C, S> From<&'a CompressedShell<P, C, S>> for PreStepModel<'a, P, C, S>
where
P: Copy,
C: StepLength,
S: StepLength,
{
fn from(shell: &'a CompressedShell<P, C, S>) -> Self {
Self::Shell(StepShell::new(shell, 17, true))
}
}
impl<'a, P, C, S> From<&'a CompressedSolid<P, C, S>> for PreStepModel<'a, P, C, S>
where
P: Copy,
C: StepLength,
S: StepLength,
{
fn from(solid: &'a CompressedSolid<P, C, S>) -> Self { Self::Solid(StepSolid::new(solid, 16)) }
}
impl<'a, P, C, S> Display for PreStepModel<'a, P, C, S>
where
P: DisplayByStep + Copy,
C: DisplayByStep + StepLength + StepCurve,
S: DisplayByStep + StepLength + StepSurface,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
Self::Shell(x) => {
f.write_fmt(format_args!(
"#{idx} = SHELL_BASED_SURFACE_MODEL('', (#{shell_idx}));\n",
idx = x.idx - 1,
shell_idx = x.idx
))?;
Display::fmt(&x, f)
}
Self::Solid(x) => Display::fmt(x, f),
}
}
}
impl<'a, P, C, S> StepLength for PreStepModel<'a, P, C, S> {
fn step_length(&self) -> usize {
match self {
Self::Shell(x) => 1 + x.step_length(),
Self::Solid(x) => x.step_length(),
}
}
}
impl<'a, P, C, S> From<&'a CompressedShell<P, C, S>> for StepModel<'a, P, C, S>
where
P: Copy,
C: StepLength,
S: StepLength,
{
fn from(shell: &'a CompressedShell<P, C, S>) -> Self { Self(shell.into()) }
}
impl<'a, P, C, S> From<&'a CompressedSolid<P, C, S>> for StepModel<'a, P, C, S>
where
P: Copy,
C: StepLength,
S: StepLength,
{
fn from(solid: &'a CompressedSolid<P, C, S>) -> Self { Self(solid.into()) }
}
impl<'a, P, C, S> Display for StepModel<'a, P, C, S>
where
P: DisplayByStep + Copy,
C: DisplayByStep + StepLength + StepCurve,
S: DisplayByStep + StepLength + StepSurface,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.pad(
"#1 = APPLICATION_PROTOCOL_DEFINITION('international standard', 'automotive_design', 2000, #2);
#2 = APPLICATION_CONTEXT('core data for automotive mechanical design processes');
#3 = SHAPE_DEFINITION_REPRESENTATION(#4, #10);
#4 = PRODUCT_DEFINITION_SHAPE('','', #5);
#5 = PRODUCT_DEFINITION('design','', #6, #9);
#6 = PRODUCT_DEFINITION_FORMATION('','', #7);
#7 = PRODUCT('','','', (#8));
#8 = PRODUCT_CONTEXT('', #2, 'mechanical');
#9 = PRODUCT_DEFINITION_CONTEXT('part definition', #2, 'design');
#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('', (#16), #11);
#11 = (
GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#15))
GLOBAL_UNIT_ASSIGNED_CONTEXT((#12, #13, #14))
REPRESENTATION_CONTEXT('Context #1', '3D Context with UNIT and UNCERTAINTY')
);
#12 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
#13 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
#14 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
#15 = UNCERTAINTY_MEASURE_WITH_UNIT(1.0E-6, #12, 'distance_accuracy_value','confusion accuracy');\n"
)?;
Display::fmt(&self.0, f)
}
}
impl<'a, P, C, S> Default for StepModels<'a, P, C, S> {
fn default() -> Self {
Self {
models: Vec::new(),
next_idx: 16,
}
}
}
impl<'a, P, C, S> StepModels<'a, P, C, S>
where
P: Copy,
C: StepLength,
S: StepLength,
{
/// push a shell to step models
pub fn push_shell(&mut self, shell: &'a CompressedShell<P, C, S>) {
let model = PreStepModel::Shell(StepShell::new(shell, self.next_idx + 1, true));
self.next_idx += model.step_length();
self.models.push(model)
}
/// push a solid to step models
pub fn push_solid(&mut self, solid: &'a CompressedSolid<P, C, S>) {
let model = PreStepModel::Solid(StepSolid::new(solid, self.next_idx));
self.next_idx += model.step_length();
self.models.push(model)
}
}
impl<'a, P, C, S> FromIterator<&'a CompressedShell<P, C, S>> for StepModels<'a, P, C, S>
where
P: Copy,
C: StepLength,
S: StepLength,
{
fn from_iter<T: IntoIterator<Item = &'a CompressedShell<P, C, S>>>(iter: T) -> Self {
let mut next_idx = 16;
let models = iter
.into_iter()
.map(|shell| {
let model = PreStepModel::Shell(StepShell::new(shell, next_idx + 1, true));
next_idx += model.step_length();
model
})
.collect();
Self { models, next_idx }
}
}
impl<'a, P, C, S> FromIterator<&'a CompressedSolid<P, C, S>> for StepModels<'a, P, C, S>
where
P: Copy,
C: StepLength,
S: StepLength,
{
fn from_iter<T: IntoIterator<Item = &'a CompressedSolid<P, C, S>>>(iter: T) -> Self {
let mut next_idx = 16;
let models = iter
.into_iter()
.map(|solid| {
let model = PreStepModel::Solid(StepSolid::new(solid, next_idx));
next_idx += model.step_length();
model
})
.collect();
Self { models, next_idx }
}
}
impl<'a, P, C, S> Display for StepModels<'a, P, C, S>
where
P: DisplayByStep + Copy,
C: DisplayByStep + StepLength + StepCurve,
S: DisplayByStep + StepLength + StepSurface,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.pad(
"#1 = APPLICATION_PROTOCOL_DEFINITION('international standard', 'automotive_design', 2000, #2);
#2 = APPLICATION_CONTEXT('core data for automotive mechanical design processes');
#3 = SHAPE_DEFINITION_REPRESENTATION(#4, #10);
#4 = PRODUCT_DEFINITION_SHAPE('','', #5);
#5 = PRODUCT_DEFINITION('design','', #6, #9);
#6 = PRODUCT_DEFINITION_FORMATION('','', #7);
#7 = PRODUCT('','','', (#8));
#8 = PRODUCT_CONTEXT('', #2, 'mechanical');
#9 = PRODUCT_DEFINITION_CONTEXT('part definition', #2, 'design');\n")?;
let models_slice = IndexSliceDisplay(self.models.iter().map(|model| match model {
PreStepModel::Shell(x) => x.idx - 1,
PreStepModel::Solid(x) => x.idx,
}));
f.write_fmt(format_args!(
"#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('', {models_slice}, #11);\n"
))?;
f.pad("#11 = (
GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#15))
GLOBAL_UNIT_ASSIGNED_CONTEXT((#12, #13, #14))
REPRESENTATION_CONTEXT('Context #1', '3D Context with UNIT and UNCERTAINTY')
);
#12 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
#13 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
#14 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
#15 = UNCERTAINTY_MEASURE_WITH_UNIT(1.0E-6, #12, 'distance_accuracy_value','confusion accuracy');\n"
)?;
self.models
.iter()
.try_for_each(|model| Display::fmt(model, f))
}
}