8000 Shapeops OR function not behaving as expected when solids touch but do not overlap · Issue #57 · ricosjp/truck · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Shapeops OR function not behaving as expected when solids touch but do not overlap #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
MattFerraro opened this issue Feb 6, 2024 · 3 comments

Comments

@MattFerraro
Copy link
MattFerraro commented Feb 6, 2024

Here is a minimal example:

use truck_modeling::builder::{tsweep, vertex};
use truck_modeling::{Point3, Vector3};
use truck_shapeops::or;

fn main() {
    let point_a = vertex(Point3::new(0.0, 0.0, 0.0));
    let line_a = tsweep(&point_a, Vector3::unit_x());
    let square_a = tsweep(&line_a, Vector3::unit_y());
    let cube_a = tsweep(&square_a, Vector3::unit_z());

    let z_offset = 1.0;
    let x_offset = 0.1;
    let y_offset = 0.1;
    let point_b = vertex(Point3::new(x_offset, y_offset, z_offset));
    let line_b = tsweep(&point_b, Vector3::unit_x());
    let square_b = tsweep(&line_b, Vector3::unit_y());
    let cube_b = tsweep(&square_b, Vector3::unit_z());

    let combined_cube = or(&cube_a, &cube_b, 0.01);
    match combined_cube {
        Some(c) => {
            println!(
                "combined_cube has {:?} shell boundaries",
                c.boundaries().len()
            );
        }
        None => {
            println!("combined_cube: None");
        }
    }
}

When I run this, the output I see is:

combined_cube: None

I expected the output to be:

combined_cube has 1 shell boundaries

If I change the value to: let z_offset = 0.9 then I get the correct behavior:

combined_cube has 1 shell boundaries

If I change the value to: let z_offset = 1.1 then I get different behavior which is also correct:

combined_cube has 2 shell boundaries

It is only when I choose let z_offset = 1.0 when the None value is returned. Is this the intended behavior? How can I merge two solids which are touching but not overlapping?

@MattFerraro
Copy link
Author

Maybe I'm misunderstanding the purpose of the or function. Is there some other way to join two cubes together that share a face?

@tsukimizake
Copy link

I tried to debug the boolean operations with coplanar faces on https://github.com/tsukimizake/truck/tree/fix-coplanar-boolean-operations-2 . Now I gave up fixing it, but at least I could make or operation of adjacent cubes work.
I'll write up what I did. I hope it helps him fix it.

I can use english to some extent, but as the author of this project seems to be also Japanese, I 8000 would like to write it in Japanese. If there is anybody interested, please use a llm to translate it.

最も単純なテストケースとして以下のようなz=1で接し、xy軸をずらすことで他の面は同一平面上にない2つのboxのorで実験しました。

#[test]
fn adjacent_cubes_or() {
    let v = builder::vertex(Point3::origin());
    let e = builder::tsweep(&v, Vector3::unit_x());
    let f = builder::tsweep(&e, Vector3::unit_y());
    let cube: Solid = builder::tsweep(&f, Vector3::unit_z());

    let v = builder::vertex(Point3::new(0.5, 0.5, 1.0));
    let w = builder::tsweep(&v, Vector3::unit_x());
    let f = builder::tsweep(&w, Vector3::unit_y());
    let cube2: Solid = builder::tsweep(&f, Vector3::unit_z());

    let result = crate::or(&cube, &cube2, 0.05);
    match &result {
        Some(_) => eprintln!("Result is Some"),
        None => eprintln!("Result is None"),
    }

    assert!(
        result.is_some(),
        "Boolean OR of adjacent cubes should succeed"
    );
}

これを実行したときまず失敗していたのはdivide_facesの

        let op = pre_faces.iter_mut().find(|face| face[0].poly.include(pt))?;

がNoneを返すというものでした。

この原因はcreate_loops_storeにおいて上のケースでのz=1のような共有面の隣で、相手Shellの辺が交線と見なされてLoopsStoreに追加されてしまうというものでした。
追加したadjacent_cubes_or testにおいては以下のような形のLoopsStoreが発生していたので、finalize_adjacent_to_coplanar_facesで(0.5, 0.5, 1) -> (1, 0.5, 1)(1, 0.5, 1) -> (0.5, 0.5, 1)のような自明なループを削除するというパッチアップをまずは試しています。

LoopsStore[1]:
  Loop[0] with status: Or
  Edges in loop:
  Edge: (0.5, 0.5, 1) -> (1, 0.5, 1)
  Edge: (1, 0.5, 1) -> (1.5, 0.5, 1)
  Edge: (1.5, 0.5, 1) -> (1.5, 0.5, 2)
  Edge: (1.5, 0.5, 2) -> (0.5, 0.5, 2)
  Edge: (0.5, 0.5, 2) -> (0.5, 0.5, 1)
  Loop[1] with status: And
  Edges in loop:
  Edge: (0.5, 0.5, 1) -> (1, 0.5, 1)
  Edge: (1, 0.5, 1) -> (0.5, 0.5, 1)

ただ、この対処は全く完全ではなく、例えばcoplanar_faces_or testでは以下のようなLoopsStoreが発生してwire is not simpleとなります。

LoopsStore[4]:
  Loop[0] with status: Or
  Edges in loop:
  Edge: (0, 0.5, 0) -> (0, 0.5, 1)
  Edge: (0, 0.5, 1) -> (0, 0, 1)
  Edge: (0, 0, 1) -> (0, 0, 0)
  Edge: (0, 0, 0) -> (0, 0.5, 0)
  Edge: (0, 0.5, 0) -> (0, 1, 0)
  Edge: (0, 1, 0) -> (0, 1, 1)
  Edge: (0, 1, 1) -> (0, 0.5, 1)
  Edge: (0, 0.5, 1) -> (0, 0.5, 0)
  Edge: (0, 0.5, 0) -> (0, 0, 0)
  Edge: (0, 0, 0) -> (0, 0.5, 0)

また、現状のfinalize_adjacent_to_coplanar_facesでは共有面がカーブしている場合にも捕捉できないと思われます。

add_edgeによる面の分割やShapeOpsStatus選択のロジックが読み解ききれず手を付けられていませんが、そのあたりで面上に辺があるケースを弾く方が正道かもしれません。

@ovo-Tim
Copy link
ovo-Tim commented May 11, 2025

Trying to solve this, I followed what @tsukimizake said(translated):

The root cause was found in create_loops_store, where in cases like the shared face at z=1, edges from the other shell were being mistakenly recognized as intersection curves and added to the LoopsStore.

So I wrote this function to prevent edges from the coplanar face being recognized as intersection curves and added to the LoopsStore.

fn is_coplanar<C>(face0: &C, face1: &C) -> bool
    where
        C: ParametricSurface3D + Clone,
    {

    let n0 = face0.normal(0.0, 0.0).normalize();
    let n1  = face1.normal(0.0, 0.0).normalize();

    if !(n0.dot(n1).abs() - 1.0).so_small() {
        return false;
    }

    let pt0 = face0.subs(0.0, 0.0);
    let pt1 = face1.subs(0.0, 0.0);
    if !(pt0 - pt1).dot(n0).abs().so_small() {
        return false;
    }
    true
}

Image

The change doesn’t seem to break anything (it passes all the tests), but the issue is still there. 😿

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants
0