Keep selected enabled features on model updates instead of resetting them, better list of objects support and recover/disable previous selection on scene reloads.

This commit is contained in:
Yeicor
2024-03-30 17:26:06 +01:00
parent e42d6be074
commit 86180c424e
13 changed files with 144 additions and 85 deletions

View File

@@ -2439,7 +2439,7 @@ THE SOFTWARE.
The following npm package may be included in this product: The following npm package may be included in this product:
- vuetify@3.5.11 - vuetify@3.5.13
This package contains the following license and notice below: This package contains the following license and notice below:

View File

@@ -13,7 +13,7 @@ from yacv_server import show, export_all # Check out other exported methods for
# Create a simple object # Create a simple object
with BuildPart() as example: with BuildPart() as example:
Box(10, 10, 5) Box(10, 10, 5)
Cylinder(4, 5, mode=Mode.SUBTRACT) Cylinder(3, 5, mode=Mode.SUBTRACT)
# Show it in the frontend with hot-reloading # Show it in the frontend with hot-reloading
show(example) show(example)

View File

@@ -7,7 +7,7 @@ import Tools from "./tools/Tools.vue";
import Models from "./models/Models.vue"; import Models from "./models/Models.vue";
import {VBtn, VLayout, VMain, VToolbarTitle} from "vuetify/lib/components/index.mjs"; import {VBtn, VLayout, VMain, VToolbarTitle} from "vuetify/lib/components/index.mjs";
import {settings} from "./misc/settings"; import {settings} from "./misc/settings";
import {NetworkManager, NetworkUpdateEvent} from "./misc/network"; import {NetworkManager, NetworkUpdateEvent, NetworkUpdateEventModel} from "./misc/network";
import {SceneMgr} from "./misc/scene"; import {SceneMgr} from "./misc/scene";
import {Document} from "@gltf-transform/core"; import {Document} from "@gltf-transform/core";
import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue"; import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue";
@@ -28,6 +28,7 @@ const viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
const sceneDocument = shallowRef(new Document()); const sceneDocument = shallowRef(new Document());
provide('sceneDocument', {sceneDocument}); provide('sceneDocument', {sceneDocument});
const models: Ref<InstanceType<typeof Models> | null> = ref(null) const models: Ref<InstanceType<typeof Models> | null> = ref(null)
const tools: Ref<InstanceType<typeof Tools> | null> = ref(null)
const disableTap = ref(false); const disableTap = ref(false);
const setDisableTap = (val: boolean) => disableTap.value = val; const setDisableTap = (val: boolean) => disableTap.value = val;
provide('disableTap', {disableTap, setDisableTap}); provide('disableTap', {disableTap, setDisableTap});
@@ -49,6 +50,7 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
for (let modelIndex in event.models) { for (let modelIndex in event.models) {
let isLast = parseInt(modelIndex) === event.models.length - 1; let isLast = parseInt(modelIndex) === event.models.length - 1;
let model = event.models[modelIndex]; let model = event.models[modelIndex];
tools.value?.removeObjectSelections(model.name);
try { try {
if (!model.isRemove) { if (!model.isRemove) {
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && settings.loadHelpers, isLast); doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && settings.loadHelpers, isLast);
@@ -68,8 +70,8 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
} }
async function onModelRemoveRequest(name: string) { async function onModelRemoveRequest(name: string) {
sceneDocument.value = await SceneMgr.removeModel(sceneUrl, sceneDocument.value, name); await onModelUpdateRequest(new NetworkUpdateEvent([new NetworkUpdateEventModel(name, "", null, true)], () => {
triggerRef(sceneDocument); // Why not triggered automatically? }));
} }
// Set up the load model event listener // Set up the load model event listener
@@ -114,7 +116,7 @@ async function loadModelManual() {
<template #toolbar> <template #toolbar>
<v-toolbar-title>Tools</v-toolbar-title> <v-toolbar-title>Tools</v-toolbar-title>
</template> </template>
<tools :viewer="viewer" @findModel="(name) => models?.findModel(name)"/> <tools ref="tools" :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
</sidebar> </sidebar>
</v-layout> </v-layout>

View File

@@ -8,7 +8,6 @@ function getCenterAndVertexList(selInfo: SelectionInfo, scene: ModelScene): {
center: Vector3, center: Vector3,
vertices: Array<Vector3> vertices: Array<Vector3>
} { } {
selInfo.object.updateMatrixWorld();
let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position'); let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position');
let ind: BufferAttribute | null = selInfo.object.geometry.index; let ind: BufferAttribute | null = selInfo.object.geometry.index;
if (ind === null) { if (ind === null) {

View File

@@ -2,7 +2,7 @@ import {settings} from "./settings";
const batchTimeout = 250; // ms const batchTimeout = 250; // ms
class NetworkUpdateEventModel { export class NetworkUpdateEventModel {
name: string; name: string;
url: string; url: string;
// TODO: Detect and manage instances of the same object (same hash, different name) // TODO: Detect and manage instances of the same object (same hash, different name)

View File

@@ -12,8 +12,8 @@ import {
VTooltip, VTooltip,
} from "vuetify/lib/components/index.mjs"; } from "vuetify/lib/components/index.mjs";
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf"; import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
import {Document, Mesh} from "@gltf-transform/core"; import {Mesh} from "@gltf-transform/core";
import {inject, ref, type ShallowRef, watch} from "vue"; import {ref, watch} from "vue";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue"; import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import { import {
mdiCircleOpacity, mdiCircleOpacity,
@@ -85,7 +85,7 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
scene.queueRender() scene.queueRender()
} }
watch(enabledFeatures, onEnabledFeaturesChange); watch(enabledFeatures, onEnabledFeaturesChange, {deep: true});
function onOpacityChange(newOpacity: number) { function onOpacityChange(newOpacity: number) {
let scene = props.viewer?.scene; let scene = props.viewer?.scene;
@@ -122,8 +122,6 @@ function onWireframeChange(newWireframe: boolean) {
watch(wireframe, onWireframeChange); watch(wireframe, onWireframeChange);
let {sceneDocument} = inject<{ sceneDocument: ShallowRef<Document> }>('sceneDocument')!!;
function onClipPlanesChange() { function onClipPlanesChange() {
let scene = props.viewer?.scene; let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model; let sceneModel = (scene as any)?._model;
@@ -245,9 +243,35 @@ function onModelLoad() {
let sceneModel = (scene as any)?._model; let sceneModel = (scene as any)?._model;
if (!scene || !sceneModel) return; if (!scene || !sceneModel) return;
// Count the number of faces, edges and vertices
const isFirstLoad = faceCount.value === -1;
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)
// First time: set the enabled features to all provided features
if (isFirstLoad) {
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)
}
// Add darkened back faces for all face objects to improve cutting planes // Add darkened back faces for all face objects to improve cutting planes
let childrenToAdd: Array<MObject3D> = []; let childrenToAdd: Array<MObject3D> = [];
sceneModel.traverse((child: MObject3D) => { sceneModel.traverse((child: MObject3D) => {
child.updateMatrixWorld(); // Objects are mostly static, so ensure updated matrices
if (child.userData[extrasNameKey] === modelName) { if (child.userData[extrasNameKey] === modelName) {
if (child.type == 'Mesh' || child.type == 'SkinnedMesh') { if (child.type == 'Mesh' || child.type == 'SkinnedMesh') {
// Compute a BVH for faster raycasting (MUCH faster selection) // Compute a BVH for faster raycasting (MUCH faster selection)
@@ -278,28 +302,6 @@ function onModelLoad() {
}); });
childrenToAdd.forEach((child: MObject3D) => sceneModel.add(child)); 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... // Furthermore...
// Enabled features may have been reset after a reload // Enabled features may have been reset after a reload
onEnabledFeaturesChange(enabledFeatures.value) onEnabledFeaturesChange(enabledFeatures.value)

View File

@@ -33,21 +33,23 @@ let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start
let showDistances = ref<Boolean>(true); let showDistances = ref<Boolean>(true);
let mouseDownAt: [number, number] | null = null; let mouseDownAt: [number, number] | null = null;
let mouseDownTime = 0;
let selectFilter = ref('Any (S)'); let selectFilter = ref('Any (S)');
const raycaster = new Raycaster(); const raycaster = new Raycaster();
let selectionMoveListener = (event: MouseEvent) => { let mouseDownListener = (event: MouseEvent) => {
mouseDownAt = [event.clientX, event.clientY]; mouseDownAt = [event.clientX, event.clientY];
mouseDownTime = performance.now();
if (!selectionEnabled.value) return; if (!selectionEnabled.value) return;
}; };
let selectionListener = (event: MouseEvent) => { let mouseUpListener = (event: MouseEvent) => {
// If the mouse moved while clicked (dragging), avoid selection logic // If the mouse moved while clicked (dragging), avoid selection logic
if (mouseDownAt) { if (mouseDownAt) {
let [x, y] = mouseDownAt; let [x, y] = mouseDownAt;
mouseDownAt = null; mouseDownAt = null;
if (Math.abs(event.clientX - x) > 5 || Math.abs(event.clientY - y) > 5) { if (Math.abs(event.clientX - x) > 5 || Math.abs(event.clientY - y) > 5 || performance.now() - mouseDownTime > 500) {
return; return;
} }
} }
@@ -254,14 +256,29 @@ let onViewerReady = (viewer: typeof ModelViewerWrapperT) => {
viewer.onElemReady((elem: ModelViewerElement) => { viewer.onElemReady((elem: ModelViewerElement) => {
if (hasListeners) return; if (hasListeners) return;
hasListeners = true; hasListeners = true;
elem.addEventListener('mouseup', selectionListener); elem.addEventListener('mousedown', mouseDownListener); // Avoid clicking when dragging
elem.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging elem.addEventListener('mouseup', mouseUpListener);
elem.addEventListener('load', () => { elem.addEventListener('load', () => {
// After a reload of the scene, we need to recover object references and highlight them again
for (let sel of selected.value) {
let scene = props.viewer?.scene;
if (!scene) continue;
let foundObject = null;
scene.traverse((obj: MObject3D) => {
if (sel.matches(obj)) {
foundObject = obj as MObject3D;
}
});
if (foundObject) {
sel.object = foundObject;
highlight(sel);
} else {
selected.value = selected.value.filter((m) => m.getKey() !== sel.getKey());
}
}
if (firstLoad) { if (firstLoad) {
toggleShowBoundingBox(); toggleShowBoundingBox();
firstLoad = false; firstLoad = false;
} else {
updateBoundingBox();
} }
}); });
elem.addEventListener('camera-change', onCameraChange); elem.addEventListener('camera-change', onCameraChange);
@@ -406,6 +423,8 @@ function updateDistances() {
return; return;
} }
defineExpose({deselect, updateBoundingBox, updateDistances});
// Add keyboard shortcuts // Add keyboard shortcuts
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
if (event.key === 's') { if (event.key === 's') {

View File

@@ -19,7 +19,7 @@ import type {ModelViewerElement} from '@google/model-viewer';
import type {MObject3D} from "./Selection.vue"; import type {MObject3D} from "./Selection.vue";
import Loading from "../misc/Loading.vue"; import Loading from "../misc/Loading.vue";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue"; import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import {defineAsyncComponent, type Ref, ref} from "vue"; import {defineAsyncComponent, ref, type Ref} from "vue";
import type {SelectionInfo} from "./selection"; import type {SelectionInfo} from "./selection";
const SelectionComponent = defineAsyncComponent({ const SelectionComponent = defineAsyncComponent({
@@ -107,6 +107,15 @@ async function openGithub() {
window.open('https://github.com/yeicor-3d/yet-another-cad-viewer', '_blank') window.open('https://github.com/yeicor-3d/yet-another-cad-viewer', '_blank')
} }
function removeObjectSelections(objName: string) {
for (let selInfo of selection.value.filter((s) => s.getObjectName() === objName)) {
selectionComp.value?.deselect(selInfo);
}
selectionComp.value?.updateBoundingBox();
selectionComp.value?.updateDistances();
}
defineExpose({removeObjectSelections});
// Add keyboard shortcuts // Add keyboard shortcuts
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
@@ -133,7 +142,7 @@ window.addEventListener('keydown', (event) => {
</v-btn> </v-btn>
<v-divider/> <v-divider/>
<h5>Selection ({{ selectionFaceCount() }}F {{ selectionEdgeCount() }}E {{ selectionVertexCount() }}V)</h5> <h5>Selection ({{ selectionFaceCount() }}F {{ selectionEdgeCount() }}E {{ selectionVertexCount() }}V)</h5>
<selection-component :ref="selectionComp as any" :viewer="props.viewer as any" v-model="selection" <selection-component ref="selectionComp" :viewer="props.viewer as any" v-model="selection"
@findModel="(name) => emit('findModel', name)"/> @findModel="(name) => emit('findModel', name)"/>
<v-divider/> <v-divider/>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@@ -187,4 +196,8 @@ window.addEventListener('keydown', (event) => {
position: relative; position: relative;
top: 5px; top: 5px;
} }
h5 {
font-size: 14px;
}
</style> </style>

View File

@@ -3,6 +3,7 @@
import type {MObject3D} from "./Selection.vue"; import type {MObject3D} from "./Selection.vue";
import type {Intersection} from "three"; import type {Intersection} from "three";
import {Box3} from "three"; import {Box3} from "three";
import {extrasNameKey} from "../misc/gltf";
/** Information about a single item in the selection */ /** Information about a single item in the selection */
export class SelectionInfo { export class SelectionInfo {
@@ -19,6 +20,17 @@ export class SelectionInfo {
this.indices = indices; this.indices = indices;
} }
public getObjectName() {
return this.object.userData[extrasNameKey];
}
public matches(object: MObject3D) {
return this.getObjectName() === object.userData[extrasNameKey] &&
(this.kind === 'face' && (object.type === 'Mesh' || object.type === 'SkinnedMesh') ||
this.kind === 'edge' && (object.type === 'Line' || object.type === 'LineSegments') ||
this.kind === 'vertex' && object.type === 'Points')
}
public getKey() { public getKey() {
return this.object.uuid + this.kind + this.indices[0].toFixed() + this.indices[1].toFixed(); return this.object.uuid + this.kind + this.indices[0].toFixed() + this.indices[1].toFixed();
} }

View File

@@ -26,9 +26,10 @@ export default defineConfig({
} }
}, },
build: { build: {
assetsDir: '.', assetsDir: '.', // Support deploying to a subdirectory using relative URLs
cssCodeSplit: false, // Small enough to inline cssCodeSplit: false, // Small enough to inline
chunkSizeWarningLimit: 550, // Three.js is big. Draco is even bigger but not likely to be used. chunkSizeWarningLimit: 550, // Three.js is big. Draco is even bigger but not likely to be used.
sourcemap: true, // For debugging production
}, },
define: { define: {
__APP_NAME__: JSON.stringify(name), __APP_NAME__: JSON.stringify(name),

View File

@@ -2,9 +2,13 @@
Utilities to work with CAD objects Utilities to work with CAD objects
""" """
import hashlib import hashlib
import io
import re
from typing import Optional, Union, Tuple from typing import Optional, Union, Tuple
from OCP.TopExp import TopExp
from OCP.TopLoc import TopLoc_Location from OCP.TopLoc import TopLoc_Location
from OCP.TopTools import TopTools_IndexedMapOfShape
from OCP.TopoDS import TopoDS_Shape from OCP.TopoDS import TopoDS_Shape
from build123d import Compound, Shape from build123d import Compound, Shape
@@ -14,7 +18,7 @@ CADCoreLike = Union[TopoDS_Shape, TopLoc_Location] # Faces, Edges, Vertices and
CADLike = Union[CADCoreLike, any] # build123d and cadquery types CADLike = Union[CADCoreLike, any] # build123d and cadquery types
def get_shape(obj: CADLike, error: bool = True, in_iter: bool = False) -> Optional[CADCoreLike]: def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]:
""" Get the shape of a CAD-like object """ """ Get the shape of a CAD-like object """
# Try to grab a shape if a different type of object was passed # Try to grab a shape if a different type of object was passed
@@ -42,13 +46,22 @@ def get_shape(obj: CADLike, error: bool = True, in_iter: bool = False) -> Option
return obj return obj
# Handle iterables like Build123d ShapeList by extracting all sub-shapes and making a compound # Handle iterables like Build123d ShapeList by extracting all sub-shapes and making a compound
if not in_iter: if isinstance(obj, list) or isinstance(obj, tuple) or isinstance(obj, set) or isinstance(obj, dict):
try: try:
obj_iter = iter(obj) if isinstance(obj, dict):
obj_iter = iter(obj.values())
else:
obj_iter = iter(obj)
# print(obj, ' -> ', obj_iter) # print(obj, ' -> ', obj_iter)
shapes_raw = [get_shape(sub_obj, error=False, in_iter=True) for sub_obj in obj_iter] shapes_raw = [get_shape(sub_obj, error=False) for sub_obj in obj_iter]
shapes_bd = [Shape(shape) for shape in shapes_raw if shape is not None] # Silently drop non-shapes
return get_shape(Compound(shapes_bd), error) shapes_raw_filtered = [shape for shape in shapes_raw if shape is not None]
if len(shapes_raw_filtered) > 0: # Continue if we found at least one shape
# Sorting is required to improve hashcode consistency
shapes_raw_filtered_sorted = sorted(shapes_raw_filtered, key=lambda x: _hashcode(x))
# Build a single compound shape
shapes_bd = [Shape(shape) for shape in shapes_raw_filtered_sorted if shape is not None]
return get_shape(Compound(shapes_bd), error)
except TypeError: except TypeError:
pass pass
@@ -149,3 +162,32 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
# Return the GLTF binary blob and the suggested name of the image # Return the GLTF binary blob and the suggested name of the image
return b''.join(mgr.build().save_to_bytes()), name return b''.join(mgr.build().save_to_bytes()), name
def _hashcode(obj: Union[bytes, CADCoreLike], **extras) -> str:
"""Utility to compute the STABLE hash code of a shape"""
# NOTE: obj.HashCode(MAX_HASH_CODE) is not stable across different runs of the same program
# This is best-effort and not guaranteed to be unique
hasher = hashlib.md5(usedforsecurity=False)
for k, v in extras.items():
hasher.update(str(k).encode())
hasher.update(str(v).encode())
if isinstance(obj, bytes):
hasher.update(obj)
elif isinstance(obj, TopLoc_Location):
sub_data = io.BytesIO()
obj.DumpJson(sub_data)
hasher.update(sub_data.getvalue())
elif isinstance(obj, TopoDS_Shape):
map_of_shapes = TopTools_IndexedMapOfShape()
TopExp.MapShapes_s(obj, map_of_shapes)
for i in range(1, map_of_shapes.Extent() + 1):
sub_shape = map_of_shapes.FindKey(i)
sub_data = io.BytesIO()
TopoDS_Shape.DumpJson(sub_shape, sub_data)
val = sub_data.getvalue()
val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address
hasher.update(val)
else:
raise ValueError(f'Cannot hash object of type {type(obj)}')
return hasher.hexdigest()

View File

@@ -1,14 +1,9 @@
import hashlib from typing import List, Dict, Tuple
import io
import re
from typing import List, Dict, Tuple, Union
from OCP.BRep import BRep_Tool from OCP.BRep import BRep_Tool
from OCP.BRepAdaptor import BRepAdaptor_Curve from OCP.BRepAdaptor import BRepAdaptor_Curve
from OCP.GCPnts import GCPnts_TangentialDeflection from OCP.GCPnts import GCPnts_TangentialDeflection
from OCP.TopExp import TopExp
from OCP.TopLoc import TopLoc_Location from OCP.TopLoc import TopLoc_Location
from OCP.TopTools import TopTools_IndexedMapOfShape
from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
from build123d import Shape, Vertex, Face, Location from build123d import Shape, Vertex, Face, Location
from pygltflib import GLTF2 from pygltflib import GLTF2
@@ -130,30 +125,3 @@ def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[Topo
mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces)) mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces))
def _hashcode(obj: Union[bytes, TopoDS_Shape], **extras) -> str:
"""Utility to compute the hash code of a shape recursively without the need to tessellate it"""
# NOTE: obj.HashCode(MAX_HASH_CODE) is not stable across different runs of the same program
# This is best-effort and not guaranteed to be unique
hasher = hashlib.md5(usedforsecurity=False)
for k, v in extras.items():
hasher.update(str(k).encode())
hasher.update(str(v).encode())
if isinstance(obj, bytes):
hasher.update(obj)
elif isinstance(obj, TopLoc_Location):
sub_data = io.BytesIO()
obj.DumpJson(sub_data)
hasher.update(sub_data.getvalue())
elif isinstance(obj, TopoDS_Shape):
map_of_shapes = TopTools_IndexedMapOfShape()
TopExp.MapShapes_s(obj, map_of_shapes)
for i in range(1, map_of_shapes.Extent() + 1):
sub_shape = map_of_shapes.FindKey(i)
sub_data = io.BytesIO()
TopoDS_Shape.DumpJson(sub_shape, sub_data)
val = sub_data.getvalue()
val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address
hasher.update(val)
else:
raise ValueError(f'Cannot hash object of type {type(obj)}')
return hasher.hexdigest()

View File

@@ -23,7 +23,8 @@ from yacv_server.myhttp import HTTPHandler
from yacv_server.mylogger import logger from yacv_server.mylogger import logger
from yacv_server.pubsub import BufferedPubSub from yacv_server.pubsub import BufferedPubSub
from yacv_server.rwlock import RWLock from yacv_server.rwlock import RWLock
from yacv_server.tessellate import _hashcode, tessellate from yacv_server.tessellate import tessellate
from cad import _hashcode
@dataclass_json @dataclass_json