diff --git a/frontend/models/Model.vue b/frontend/models/Model.vue index 15c53a0..6d94114 100644 --- a/frontend/models/Model.vue +++ b/frontend/models/Model.vue @@ -43,10 +43,11 @@ const emit = defineEmits<{ remove: [] }>() let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah" -// Reactive properties -const enabledFeatures = defineModel>("enabledFeatures", {default: [0, 1, 2]}); -const opacity = defineModel("opacity", {default: 1}); -const wireframe = ref(false); +// Count the number of faces, edges and vertices +let faceCount = ref(-1); +let edgeCount = ref(-1); +let vertexCount = ref(-1); + // Clipping planes are handled in y-up space (swapped on interface, Z inverted later) const clipPlaneX = ref(1); const clipPlaneSwappedX = ref(false); @@ -56,24 +57,10 @@ const clipPlaneZ = ref(1); const clipPlaneSwappedZ = ref(false); const edgeWidth = ref(settings.edgeWidth); -// Count the number of faces, edges and vertices -let faceCount = props.meshes - .flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES)) - .map(p => (p.getExtras()?.face_triangles_end as any)?.length ?? 1) - .reduce((a, b) => a + b, 0) -let edgeCount = props.meshes - .flatMap((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES])) - .map(p => (p.getExtras()?.edge_points_end as any)?.length ?? 0) - .reduce((a, b) => a + b, 0) -let vertexCount = props.meshes - .flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS)) - .map(p => (p.getAttribute("POSITION")?.getCount() ?? 0)) - .reduce((a, b) => a + b, 0) - -// Set initial defaults for the enabled features -if (faceCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0) -if (edgeCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 1) -if (vertexCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 2) +// Misc properties +const enabledFeatures = defineModel>("enabledFeatures", {default: [0, 1, 2]}); +const opacity = defineModel("opacity", {default: 1}); +const wireframe = ref(false); // Listeners for changes in the properties (or viewer reloads) function onEnabledFeaturesChange(newEnabledFeatures: Array) { @@ -81,9 +68,6 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array) { let scene = props.viewer?.scene; let sceneModel = (scene as any)?._model; if (!scene || !sceneModel) return; - // Iterate all primitives of the mesh and set their visibility based on the enabled features - // Use the scene graph instead of the document to avoid reloading the same model, at the cost - // of not actually removing the primitives from the scene graph sceneModel.traverse((child: MObject3D) => { if (child.userData[extrasNameKey] === modelName) { let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh' @@ -107,10 +91,6 @@ function onOpacityChange(newOpacity: number) { let scene = props.viewer?.scene; let sceneModel = (scene as any)?._model; if (!scene || !sceneModel) return; - // Iterate all primitives of the mesh and set their opacity based on the enabled features - // Use the scene graph instead of the document to avoid reloading the same model, at the cost - // of not actually removing the primitives from the scene graph - // console.log('Opacity may have changed', newOpacity) sceneModel.traverse((child: MObject3D) => { if (child.userData[extrasNameKey] === modelName) { if (child.material && child.material.opacity !== newOpacity) { @@ -129,10 +109,6 @@ function onWireframeChange(newWireframe: boolean) { let scene = props.viewer?.scene; let sceneModel = (scene as any)?._model; if (!scene || !sceneModel) return; - // Iterate all primitives of the mesh and set their wireframe based on the enabled features - // Use the scene graph instead of the document to avoid reloading the same model, at the cost - // of not actually removing the primitives from the scene graph - // console.log('Wireframe may have changed', newWireframe) sceneModel.traverse((child: MObject3D) => { if (child.userData[extrasNameKey] === modelName) { if (child.material && child.material.wireframe !== newWireframe) { @@ -268,9 +244,8 @@ function onModelLoad() { let scene = props.viewer?.scene; let sceneModel = (scene as any)?._model; if (!scene || !sceneModel) return; - // Iterate all primitives of the mesh and set their visibility based on the enabled features - // Use the scene graph instead of the document to avoid reloading the same model, at the cost - // of not actually removing the primitives from the scene graph + + // Add darkened back faces for all face objects to improve cutting planes let childrenToAdd: Array = []; sceneModel.traverse((child: MObject3D) => { if (child.userData[extrasNameKey] === modelName) { @@ -303,6 +278,28 @@ function onModelLoad() { }); childrenToAdd.forEach((child: MObject3D) => sceneModel.add(child)); + // Count the number of faces, edges and vertices + faceCount.value = props.meshes + .flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES)) + .map(p => (p.getExtras()?.face_triangles_end as any)?.length ?? 1) + .reduce((a, b) => a + b, 0) + edgeCount.value = props.meshes + .flatMap((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES])) + .map(p => (p.getExtras()?.edge_points_end as any)?.length ?? 0) + .reduce((a, b) => a + b, 0) + vertexCount.value = props.meshes + .flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS)) + .map(p => (p.getAttribute("POSITION")?.getCount() ?? 0)) + .reduce((a, b) => a + b, 0) + + // Set the enabled features to all provided features + if (faceCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0) + else if (!enabledFeatures.value.includes(0)) enabledFeatures.value.push(0) + if (edgeCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 1) + else if (!enabledFeatures.value.includes(1)) enabledFeatures.value.push(1) + if (vertexCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 2) + else if (!enabledFeatures.value.includes(2)) enabledFeatures.value.push(2) + // Furthermore... // Enabled features may have been reset after a reload onEnabledFeaturesChange(enabledFeatures.value) diff --git a/yacv_server/cad.py b/yacv_server/cad.py index c72d95f..9529f2f 100644 --- a/yacv_server/cad.py +++ b/yacv_server/cad.py @@ -2,10 +2,11 @@ Utilities to work with CAD objects """ import hashlib -from typing import Optional, Union, List, Tuple +from typing import Optional, Union, Tuple from OCP.TopLoc import TopLoc_Location from OCP.TopoDS import TopoDS_Shape +from build123d import Compound, Shape from yacv_server.gltf import GLTFMgr @@ -13,7 +14,7 @@ CADCoreLike = Union[TopoDS_Shape, TopLoc_Location] # Faces, Edges, Vertices and CADLike = Union[CADCoreLike, any] # build123d and cadquery types -def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]: +def get_shape(obj: CADLike, error: bool = True, in_iter: bool = False) -> Optional[CADCoreLike]: """ Get the shape of a CAD-like object """ # Try to grab a shape if a different type of object was passed @@ -40,6 +41,17 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]: if isinstance(obj, TopoDS_Shape): return obj + # Handle iterables like Build123d ShapeList by extracting all sub-shapes and making a compound + if not in_iter: + try: + obj_iter = iter(obj) + # print(obj, ' -> ', obj_iter) + shapes_raw = [get_shape(sub_obj, error=False, in_iter=True) for sub_obj in obj_iter] + shapes_bd = [Shape(shape) for shape in shapes_raw if shape is not None] + return get_shape(Compound(shapes_bd), error) + except TypeError: + pass + if error: raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)') else: