Skip to content
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

Manifold metadata #425

Closed
mikesimons opened this issue May 6, 2023 · 20 comments · Fixed by #430
Closed

Manifold metadata #425

mikesimons opened this issue May 6, 2023 · 20 comments · Fixed by #430

Comments

@mikesimons
Copy link

Would there be a possibility of being able to attach metadata to Manifolds?

I'm trying to use ManifoldCAD to create designs for a woodworking project but ideally I'd be able to attach labels, material, fastener sizes etc to manifolds.

I'm hoping that I will then be able to generate cut and part lists from decomposed manifolds that make up the finished design.
It may also allow me to isolate specific manifolds by name in code.

An alternative (though slightly more cumbersome method) would be if a unique id can be exposed for every manifold. This way I could maintain an independent dictionary of metadata keyed by id... though I would definitely prefer being able to attach metadata directly.

Wouldn't need to be much more than a dictionary of strings; more complex types can be serialized in and out of this if needs be.

Finally, thank you for such a great project 🙇

@pca006132
Copy link
Collaborator

You can attach properties to vertices, which will be preserved after operations. Alternatively, there is also a originalID, exposed in runOriginalID, that can be made unique for every single manifold by calling AsOriginal.

@mikesimons
Copy link
Author

Ah sweet. Thanks @pca006132.
I will try out your suggestions and report back.

@mikesimons
Copy link
Author

I've had a quick play and it looks like originalId and a separate dictionary should work. 🙇‍♂️
What're the implications for liberal use of asOriginal? It basically means that parts which would otherwise have shared a mesh now do not? If so I guess that has performance implications for assemblies with many of the same part?

@pca006132
Copy link
Collaborator

After boolean operations, we will try to simplify the mesh by removing some vertices, which can make later operations more efficient. However, if the vertices have different original IDs, we cannot remove them, so the mesh will be more complicated. But normally we don't expect too much of a performance problem even if you use AsOriginal everywhere.

@mikesimons
Copy link
Author

Right, got it.

Unfortunately after a bit more testing I don't think either approach will work after all. In my initial testing I used lists of manifolds and that worked fine. However, after trying it with an actual assembly the manifolds that go in to the compose call do not seem to be the same as the ones that come out; they're missing their ids.

Being able to survive a compose/decompose would be ideal as then I can just work with the final assembly.

I've also tried using the vert properties which feels a bit sketchy (I think it locked up the tab at one point) but also I couldn't make work. I can assign a new value in the vert property array but it doesn't stick. I tried mutating existing entries as well as creating a new expanded Float32Array and assigning it back. The values were not present after retrieving them with manifold.getMesh().vertProperties.

I will take a different approach for now and console.log or concat part / cut lists as the assemblies are made rather than involving the manifolds but it's a much less desirable solution for my use case.

@mikesimons
Copy link
Author

Here is a simple repro example for both cases in case I'm doing something silly:

vertProperties:

const manifold = cube();
const vp = manifold.getMesh().vertProperties;
vp[0] = 999999;
const newVp = new Float32Array(vp.length + 1);
newVp.set(vp);
newVp[vp.length] = 99999;
manifold.getMesh().vertProperties = newVp;

// console.log: Float32Array(25) [999999, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 99999, buffer: ArrayBuffer(100), byteLength: 100, byteOffset: 0, length: 25, Symbol(Symbol.toStringTag): 'Float32Array']
console.log(newVp)

// console.log: Float32Array(24) [0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, buffer: ArrayBuffer(96), byteLength: 96, byteOffset: 0, length: 24, Symbol(Symbol.toStringTag): 'Float32Array']
console.log(manifold.getMesh().vertProperties)

OriginalID

const manifolds = [cube().asOriginal(), cube().asOriginal()];
// console.log: (2) [151, 154]
console.log(manifolds.map((v) => v.originalID()))
const composed = compose(manifolds);
const decomposed = composed.decompose();
// console.log: (2) [-1, -1]
console.log(decomposed.map((v) => v.originalID()));

@pca006132
Copy link
Collaborator

Interesting, will have a look at it later this week.

@elalish
Copy link
Owner

elalish commented May 8, 2023

I see that after your compose, decompose steps the originals IDs have been lost - that does feel like a bug, and should be fixable. Meanwhile, have you tried union instead of compose? It's basically the same but safer.

@mikesimons
Copy link
Author

Good to know I'm not doing something dumb 😅.

I did try union but it seemed it was a one way operation.
Is there a way of determining what went in to a union from the result?

Since my main focus is a cut / part list it's pretty important to be able to work back to the constituent parts from the final assembly. I had compose / decompose down as a kind of grouping mechanism which seems perfect for this use case.

Thanks for your time on this 🙇

@pca006132
Copy link
Collaborator

Compose can only be used for parts that are non-intersecting, it is just some low level optimization exposed to the users. If you want to preserve the different parts, the way I would do it is to translate them to the correct location and apply all other operations to each of them, without doing union. And then you can export each component individually.

@elalish
Copy link
Owner

elalish commented May 8, 2023

Yes, I agree with @pca006132, though I'm still a bit confused about the use case. Can you describe a simple end-to-end example of what you're trying to accomplish? Parts and cuts are pretty different concepts, so it would be helpful to understand your workflow.

@mikesimons
Copy link
Author

Umm, maybe it's best explained with what I'm playing with atm.
So as I said before it's woodworking; based on the design I'd like to produce a list of cuts I'd need to make grouped by lumber dimensions and a list of hardware... maybe that's where the confusion comes in. When I say "part list" I mean hardware like fasteners, rails etc.

Here is what it currently looks like:

const metadata = {} as Record<string, any>;
const cuts = [];
const parts = [];

const bench = {
  width: 1450,
  height: 890,
  depth: 550
};

const lumberDimensions = {
  2: {
    4: { width: 90, thickness: 38 },
    6: { width: 140, thickness: 38 }
  }
};

const lumberDim = (a: number, b: number) => lumberDimensions[a][b];
const lumber = (a: number, b: number, length: number) => {
  const dim = lumberDimensions[a][b];
  return cube([length, dim.width, dim.thickness]);
};

const legAssembly = (assembly: string, legHeight: number) => {
  const lowerFaceLength = 100;
  const stretcherDim = lumberDim(2,4);
  const legDim = lumberDim(2,6);

  // Carriage bolt
  const carriageBoltDiameter = 10;
  const carriageBoltLength = legDim.thickness*2+20;
  const carriageBoltRecess = cylinder(20, 20).translate([300, legDim.width/2, legDim.thickness-20]);
  const carriageBoltHole = cylinder(legDim.thickness*2, carriageBoltDiameter).translate([300, legDim.width/2, -legDim.thickness]);
  const carriageBolt = cylinder(carriageBoltLength, carriageBoltDiameter).translate([300, legDim.width/2, -carriageBoltLength+30]);

  // Lower face
  const lowerFace = lumber(2, 6, lowerFaceLength);

  // Leg
  let leg = lumber(2, 6, legHeight).translate([0, 0, -stretcherDim.thickness]);
  leg = leg.subtract(carriageBoltHole);

  // Upper face
  const upperFaceStart = lowerFaceLength + stretcherDim.width;
  const upperFaceLength = legHeight - upperFaceStart;
  let upperFace = lumber(2, 6, upperFaceLength).translate([upperFaceStart ,0, 0]);
  upperFace = upperFace.subtract(carriageBoltRecess);
  upperFace = upperFace.subtract(carriageBoltHole);

  // cut/part list
  cuts.push({ assembly, label: 'upper face', lumber: legDim, length: upperFaceLength });
  cuts.push({ assembly, label: 'lower face', lumber: legDim, length: lowerFaceLength });
  cuts.push({ assembly, label: 'leg', lumber: legDim, length: legHeight });
  parts.push({ assembly, label: 'leg bolt', type: 'carriage bolt', diameter: carriageBoltDiameter, length: carriageBoltLength });

  const result = compose([
    lowerFace.asOriginal(),
    leg.asOriginal(),
    upperFace.asOriginal(),
    carriageBolt.asOriginal()
  ]).rotate([0, -90, 0]);

  return result;
}

const finalAssembly = [
  legAssembly("right front leg", bench.height),
  legAssembly("left front leg", bench.height).translate([0, bench.width, 0]),
  legAssembly("right rear leg", bench.height).mirror([1,0,0]).translate([bench.depth, 0 ,0]),
  legAssembly("left rear leg", bench.height).mirror([1,0,0]).translate([bench.depth, bench.width, 0])
];

const result = union(finalAssembly);
cuts.map((c) => console.log(`cut: ${JSON.stringify(c)}`));
parts.map((c) => console.log(`part: ${JSON.stringify(c)}`));

This produces:

cut: {"assembly":"right front leg","label":"upper face","lumber":{"width":140,"thickness":38},"length":700}
cut: {"assembly":"right front leg","label":"lower face","lumber":{"width":140,"thickness":38},"length":100}
cut: {"assembly":"right front leg","label":"leg","lumber":{"width":140,"thickness":38},"length":890}
cut: {"assembly":"left front leg","label":"upper face","lumber":{"width":140,"thickness":38},"length":700}
cut: {"assembly":"left front leg","label":"lower face","lumber":{"width":140,"thickness":38},"length":100}
cut: {"assembly":"left front leg","label":"leg","lumber":{"width":140,"thickness":38},"length":890}
cut: {"assembly":"right rear leg","label":"upper face","lumber":{"width":140,"thickness":38},"length":700}
cut: {"assembly":"right rear leg","label":"lower face","lumber":{"width":140,"thickness":38},"length":100}
cut: {"assembly":"right rear leg","label":"leg","lumber":{"width":140,"thickness":38},"length":890}
cut: {"assembly":"left rear leg","label":"upper face","lumber":{"width":140,"thickness":38},"length":700}
cut: {"assembly":"left rear leg","label":"lower face","lumber":{"width":140,"thickness":38},"length":100}
cut: {"assembly":"left rear leg","label":"leg","lumber":{"width":140,"thickness":38},"length":890}
part: {"assembly":"right front leg","label":"leg bolt","type":"carriage bolt","diameter":10,"length":96}
part: {"assembly":"left front leg","label":"leg bolt","type":"carriage bolt","diameter":10,"length":96}
part: {"assembly":"right rear leg","label":"leg bolt","type":"carriage bolt","diameter":10,"length":96}
part: {"assembly":"left rear leg","label":"leg bolt","type":"carriage bolt","diameter":10,"length":96}

The intention is to be able to pack cut lists on to boards and produce a shopping list for hardware.

To be honest I think this approach will work well enough for now but metadata on the manifolds was the first place my head went for this.

Interestingly, if you change the final union in this code for a compose it also errors out with RuntimeError: memory access out of bounds.

@mikesimons
Copy link
Author

mikesimons commented May 8, 2023

Since the current approach is working well enough please feel free to close this.

That said if originalID is supposed to survive a compose/decompose then that may be a bug and I'm fairly sure the memory access out of bounds is a bug. That one seems to be related to the number of things going on... if you comment out any one leg assembly it works but with 4 it dies.

@elalish
Copy link
Owner

elalish commented May 8, 2023

Yes, those are both valid issues - let's keep this open to track. And thanks for the detail! That's a very interesting use case that I'll have to think over a bit.

@elalish
Copy link
Owner

elalish commented May 9, 2023

In playing with your example, I think your current approach is the right one. In fact I would avoid any union and decompose between the separates "cuts" because the way you're modeling doesn't actually create any intersections in the first place, except where you're drilling the bolt hole in the leg.

If you made this into a tool outside of manifoldCAD.org, you might also consider simply putting each cut and part into its own glTF node, so you can name them and keep them separate more easily downstream. Manifold is meant for mesh modification - tracking assemblies is probably easier done at a higher level. You might look at my new gltf-io.js example: https://github.com/elalish/manifold/blob/master/bindings/wasm/examples/model-viewer.html - it only writes a single mesh right now, but gltf-transform makes it very easy to add more following the same pattern. Try it live here: https://manifoldcad.org/model-viewer.html

@elalish
Copy link
Owner

elalish commented May 9, 2023

I've also been thinking it would be cool to support OpenSCAD-style animations in ManifoldCAD.org, which would also require a concept of assemblies. I'm trying to figure out the right level of API to expose for this - I'll keep your example in mind.

@mikesimons
Copy link
Author

Sounds awesome 😁. I'll take a look at your model-viewer example and be sure to let you know if I come up with anything fun.

For now I'll be focusing on building the workbench and working with ManifoldCAD ... otherwise I'll never get it built 🙊 . The simplicity in ManifoldCAD is nice for that to be honest; no crazy distractions or mad complex interfaces to learn. Just a small API.

I might see about implementing a grid or an axis indicator or maybe even just shortcut keys for front / side / top / perspective elevations (a little like blender keypad shortcuts). It can be quite confusing to determine camera orientation after you've been working a while. 🤔

Anyway, thanks again for your time and consideration 🙇

@elalish
Copy link
Owner

elalish commented May 9, 2023

Yes, some help with UI would be excellent! Glad you like it.

@elalish
Copy link
Owner

elalish commented May 9, 2023

Regarding the bugs: I realized the decompose issue isn't actually a bug - you were using originalID(), which gives -1 because that's not an original (input) mesh, but something that's been operated on. You'd need to use getMesh().runOriginalID, since it might be composed of multiple operations (like the bolt hole).

As for the OOM, I have a minimal repro:

const legAssembly = () => {
  const lowerFace = cube([100, 140, 38]);
  return compose([
    lowerFace
  ]);
};

const finalAssembly = [
  legAssembly(),
  legAssembly().translate([0, 300, 0]),
  legAssembly().mirror([1,0,0]).translate([300, 0 ,0]),
  legAssembly().mirror([1,0,0]).translate([300, 300, 0])
];

const result = compose(finalAssembly);

It seems we have some problem when two compose functions are used sequentially, since if you remove either it works fine. Also, it seems to be somewhat random about failing depending on how many of the four legAssemblys you comment out. @pca006132 would you be interested in taking a look?

@pca006132
Copy link
Collaborator

I can have a look but I don't have a lot of time recently, so I will probably be a bit slow on this.

pca006132 added a commit to pca006132/manifold that referenced this issue May 12, 2023
pca006132 added a commit to pca006132/manifold that referenced this issue May 12, 2023
pca006132 added a commit that referenced this issue May 12, 2023
* fix compose index error

#425 (comment)

* simplify test case
cartesian-theatrics pushed a commit to SovereignShop/manifold that referenced this issue Mar 11, 2024
* fix compose index error

elalish#425 (comment)

* simplify test case
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

Successfully merging a pull request may close this issue.

3 participants