mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-20 14:37:03 +01:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
345636e478 | ||
|
|
9a0fb03526 | ||
|
|
2037621afc | ||
|
|
2ba0e18479 | ||
|
|
eca2bbfa7c | ||
|
|
86180c424e | ||
|
|
e42d6be074 | ||
|
|
e2d6a3cb00 | ||
|
|
9e453b7890 | ||
|
|
0b8faa9e8b | ||
|
|
00bc2a15e0 | ||
|
|
432abcf85c |
2
.github/workflows/deploy2.yml
vendored
2
.github/workflows/deploy2.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
mv "$dir/"* public/
|
||||
rmdir "$dir"
|
||||
done
|
||||
- uses: "actions/configure-pages@v4"
|
||||
- uses: "actions/configure-pages@v5"
|
||||
- uses: "actions/upload-pages-artifact@v3"
|
||||
with:
|
||||
path: 'public'
|
||||
|
||||
@@ -2439,7 +2439,7 @@ THE SOFTWARE.
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from yacv_server import show, export_all # Check out other exported methods for
|
||||
# Create a simple object
|
||||
with BuildPart() as example:
|
||||
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(example)
|
||||
|
||||
@@ -7,7 +7,7 @@ import Tools from "./tools/Tools.vue";
|
||||
import Models from "./models/Models.vue";
|
||||
import {VBtn, VLayout, VMain, VToolbarTitle} from "vuetify/lib/components/index.mjs";
|
||||
import {settings} from "./misc/settings";
|
||||
import {NetworkManager, NetworkUpdateEvent} from "./misc/network";
|
||||
import {NetworkManager, NetworkUpdateEvent, NetworkUpdateEventModel} from "./misc/network";
|
||||
import {SceneMgr} from "./misc/scene";
|
||||
import {Document} from "@gltf-transform/core";
|
||||
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());
|
||||
provide('sceneDocument', {sceneDocument});
|
||||
const models: Ref<InstanceType<typeof Models> | null> = ref(null)
|
||||
const tools: Ref<InstanceType<typeof Tools> | null> = ref(null)
|
||||
const disableTap = ref(false);
|
||||
const setDisableTap = (val: boolean) => disableTap.value = val;
|
||||
provide('disableTap', {disableTap, setDisableTap});
|
||||
@@ -49,6 +50,7 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
||||
for (let modelIndex in event.models) {
|
||||
let isLast = parseInt(modelIndex) === event.models.length - 1;
|
||||
let model = event.models[modelIndex];
|
||||
tools.value?.removeObjectSelections(model.name);
|
||||
try {
|
||||
if (!model.isRemove) {
|
||||
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) {
|
||||
sceneDocument.value = await SceneMgr.removeModel(sceneUrl, sceneDocument.value, name);
|
||||
triggerRef(sceneDocument); // Why not triggered automatically?
|
||||
await onModelUpdateRequest(new NetworkUpdateEvent([new NetworkUpdateEventModel(name, "", null, true)], () => {
|
||||
}));
|
||||
}
|
||||
|
||||
// Set up the load model event listener
|
||||
@@ -114,7 +116,7 @@ async function loadModelManual() {
|
||||
<template #toolbar>
|
||||
<v-toolbar-title>Tools</v-toolbar-title>
|
||||
</template>
|
||||
<tools :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
|
||||
<tools ref="tools" :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
|
||||
</sidebar>
|
||||
|
||||
</v-layout>
|
||||
|
||||
@@ -8,7 +8,6 @@ function getCenterAndVertexList(selInfo: SelectionInfo, scene: ModelScene): {
|
||||
center: Vector3,
|
||||
vertices: Array<Vector3>
|
||||
} {
|
||||
selInfo.object.updateMatrixWorld();
|
||||
let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position');
|
||||
let ind: BufferAttribute | null = selInfo.object.geometry.index;
|
||||
if (ind === null) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import {settings} from "./settings";
|
||||
|
||||
const batchTimeout = 250; // ms
|
||||
|
||||
class NetworkUpdateEventModel {
|
||||
export class NetworkUpdateEventModel {
|
||||
name: string;
|
||||
url: string;
|
||||
// TODO: Detect and manage instances of the same object (same hash, different name)
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
VTooltip,
|
||||
} from "vuetify/lib/components/index.mjs";
|
||||
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
|
||||
import {Document, Mesh} from "@gltf-transform/core";
|
||||
import {inject, ref, type ShallowRef, watch} from "vue";
|
||||
import {Mesh} from "@gltf-transform/core";
|
||||
import {ref, watch} from "vue";
|
||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||
import {
|
||||
mdiCircleOpacity,
|
||||
@@ -85,7 +85,7 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
||||
scene.queueRender()
|
||||
}
|
||||
|
||||
watch(enabledFeatures, onEnabledFeaturesChange);
|
||||
watch(enabledFeatures, onEnabledFeaturesChange, {deep: true});
|
||||
|
||||
function onOpacityChange(newOpacity: number) {
|
||||
let scene = props.viewer?.scene;
|
||||
@@ -122,8 +122,6 @@ function onWireframeChange(newWireframe: boolean) {
|
||||
|
||||
watch(wireframe, onWireframeChange);
|
||||
|
||||
let {sceneDocument} = inject<{ sceneDocument: ShallowRef<Document> }>('sceneDocument')!!;
|
||||
|
||||
function onClipPlanesChange() {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
@@ -245,9 +243,35 @@ function onModelLoad() {
|
||||
let sceneModel = (scene as any)?._model;
|
||||
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
|
||||
let childrenToAdd: Array<MObject3D> = [];
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
child.updateMatrixWorld(); // Objects are mostly static, so ensure updated matrices
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if (child.type == 'Mesh' || child.type == 'SkinnedMesh') {
|
||||
// Compute a BVH for faster raycasting (MUCH faster selection)
|
||||
@@ -278,28 +302,6 @@ 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)
|
||||
|
||||
@@ -33,21 +33,23 @@ let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start
|
||||
let showDistances = ref<Boolean>(true);
|
||||
|
||||
let mouseDownAt: [number, number] | null = null;
|
||||
let mouseDownTime = 0;
|
||||
let selectFilter = ref('Any (S)');
|
||||
const raycaster = new Raycaster();
|
||||
|
||||
|
||||
let selectionMoveListener = (event: MouseEvent) => {
|
||||
let mouseDownListener = (event: MouseEvent) => {
|
||||
mouseDownAt = [event.clientX, event.clientY];
|
||||
mouseDownTime = performance.now();
|
||||
if (!selectionEnabled.value) return;
|
||||
};
|
||||
|
||||
let selectionListener = (event: MouseEvent) => {
|
||||
let mouseUpListener = (event: MouseEvent) => {
|
||||
// If the mouse moved while clicked (dragging), avoid selection logic
|
||||
if (mouseDownAt) {
|
||||
let [x, y] = mouseDownAt;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -254,14 +256,29 @@ let onViewerReady = (viewer: typeof ModelViewerWrapperT) => {
|
||||
viewer.onElemReady((elem: ModelViewerElement) => {
|
||||
if (hasListeners) return;
|
||||
hasListeners = true;
|
||||
elem.addEventListener('mouseup', selectionListener);
|
||||
elem.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging
|
||||
elem.addEventListener('mousedown', mouseDownListener); // Avoid clicking when dragging
|
||||
elem.addEventListener('mouseup', mouseUpListener);
|
||||
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) {
|
||||
toggleShowBoundingBox();
|
||||
firstLoad = false;
|
||||
} else {
|
||||
updateBoundingBox();
|
||||
}
|
||||
});
|
||||
elem.addEventListener('camera-change', onCameraChange);
|
||||
@@ -406,6 +423,8 @@ function updateDistances() {
|
||||
return;
|
||||
}
|
||||
|
||||
defineExpose({deselect, updateBoundingBox, updateDistances});
|
||||
|
||||
// Add keyboard shortcuts
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === 's') {
|
||||
@@ -443,7 +462,6 @@ window.addEventListener('keydown', (event) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="select-parent">
|
||||
<v-btn icon @click="toggleSelection" :color="selectionEnabled ? 'surface-light' : ''">
|
||||
<v-tooltip activator="parent">{{ selectionEnabled ? 'Disable (s)election mode' : 'Enable (s)election mode' }}
|
||||
</v-tooltip>
|
||||
@@ -456,7 +474,6 @@ window.addEventListener('keydown', (event) => {
|
||||
v-model="selectFilter"/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<v-btn icon @click="toggleHighlightNextSelection" :color="highlightNextSelection[0] ? 'surface-light' : ''">
|
||||
<v-tooltip activator="parent">(H)ighlight the next clicked element in the models list</v-tooltip>
|
||||
<svg-icon type="mdi" :path="mdiFeatureSearch"/>
|
||||
@@ -475,20 +492,11 @@ window.addEventListener('keydown', (event) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Very hacky styling... */
|
||||
.select-parent {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.select-parent .v-btn {
|
||||
position: relative;
|
||||
top: -20px;
|
||||
}
|
||||
|
||||
.select-only {
|
||||
display: inline-block;
|
||||
width: calc(100% - 48px);
|
||||
float: right;
|
||||
height: 36px;
|
||||
position: relative;
|
||||
top: -12px;
|
||||
width: calc(100% - 48px);
|
||||
}
|
||||
</style>
|
||||
@@ -19,7 +19,7 @@ import type {ModelViewerElement} from '@google/model-viewer';
|
||||
import type {MObject3D} from "./Selection.vue";
|
||||
import Loading from "../misc/Loading.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";
|
||||
|
||||
const SelectionComponent = defineAsyncComponent({
|
||||
@@ -107,6 +107,15 @@ async function openGithub() {
|
||||
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
|
||||
window.addEventListener('keydown', (event) => {
|
||||
@@ -133,7 +142,7 @@ window.addEventListener('keydown', (event) => {
|
||||
</v-btn>
|
||||
<v-divider/>
|
||||
<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)"/>
|
||||
<v-divider/>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -187,4 +196,8 @@ window.addEventListener('keydown', (event) => {
|
||||
position: relative;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
import type {MObject3D} from "./Selection.vue";
|
||||
import type {Intersection} from "three";
|
||||
import {Box3} from "three";
|
||||
import {extrasNameKey} from "../misc/gltf";
|
||||
|
||||
/** Information about a single item in the selection */
|
||||
export class SelectionInfo {
|
||||
@@ -19,6 +20,17 @@ export class SelectionInfo {
|
||||
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() {
|
||||
return this.object.uuid + this.kind + this.indices[0].toFixed() + this.indices[1].toFixed();
|
||||
}
|
||||
|
||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yet-another-cad-viewer",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.7",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -26,11 +26,11 @@
|
||||
"three-mesh-bvh": "^0.7.3",
|
||||
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
|
||||
"vue": "^3.4.21",
|
||||
"vuetify": "^3.5.11"
|
||||
"vuetify": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.3",
|
||||
"@types/node": "^20.11.30",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^20.12.2",
|
||||
"@types/three": "^0.160.0",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
@@ -39,9 +39,9 @@
|
||||
"commander": "^12.0.0",
|
||||
"generate-license-file": "^3.0.1",
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"terser": "^5.29.2",
|
||||
"terser": "^5.30.0",
|
||||
"typescript": "~5.4.3",
|
||||
"vite": "^5.2.6",
|
||||
"vite": "^5.2.7",
|
||||
"vue-tsc": "^2.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "yacv-server"
|
||||
version = "0.8.5"
|
||||
version = "0.8.7"
|
||||
description = "Yet Another CAD Viewer (server)"
|
||||
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -26,9 +26,10 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
build: {
|
||||
assetsDir: '.',
|
||||
assetsDir: '.', // Support deploying to a subdirectory using relative URLs
|
||||
cssCodeSplit: false, // Small enough to inline
|
||||
chunkSizeWarningLimit: 550, // Three.js is big. Draco is even bigger but not likely to be used.
|
||||
sourcemap: true, // For debugging production
|
||||
},
|
||||
define: {
|
||||
__APP_NAME__: JSON.stringify(name),
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
Utilities to work with CAD objects
|
||||
"""
|
||||
import hashlib
|
||||
import io
|
||||
import re
|
||||
from typing import Optional, Union, Tuple
|
||||
|
||||
from OCP.TopExp import TopExp
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopTools import TopTools_IndexedMapOfShape
|
||||
from OCP.TopoDS import TopoDS_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
|
||||
|
||||
|
||||
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 """
|
||||
|
||||
# Try to grab a shape if a different type of object was passed
|
||||
@@ -42,12 +46,21 @@ def get_shape(obj: CADLike, error: bool = True, in_iter: bool = False) -> Option
|
||||
return obj
|
||||
|
||||
# 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:
|
||||
if isinstance(obj, dict):
|
||||
obj_iter = iter(obj.values())
|
||||
else:
|
||||
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]
|
||||
shapes_raw = [get_shape(sub_obj, error=False) for sub_obj in obj_iter]
|
||||
# Silently drop non-shapes
|
||||
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:
|
||||
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 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()
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import hashlib
|
||||
import io
|
||||
import re
|
||||
from typing import List, Dict, Tuple, Union
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
from OCP.BRep import BRep_Tool
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Curve
|
||||
from OCP.GCPnts import GCPnts_TangentialDeflection
|
||||
from OCP.TopExp import TopExp
|
||||
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 build123d import Shape, Vertex, Face, Location
|
||||
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))
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -23,7 +23,8 @@ from yacv_server.myhttp import HTTPHandler
|
||||
from yacv_server.mylogger import logger
|
||||
from yacv_server.pubsub import BufferedPubSub
|
||||
from yacv_server.rwlock import RWLock
|
||||
from yacv_server.tessellate import _hashcode, tessellate
|
||||
from yacv_server.tessellate import tessellate
|
||||
from yacv_server.cad import _hashcode
|
||||
|
||||
|
||||
@dataclass_json
|
||||
|
||||
44
yarn.lock
44
yarn.lock
@@ -777,10 +777,10 @@
|
||||
"@sigstore/core" "^1.0.0"
|
||||
"@sigstore/protobuf-specs" "^0.3.0"
|
||||
|
||||
"@tsconfig/node20@^20.1.3":
|
||||
version "20.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.3.tgz#b3b4cf785e1b390a6ab48a68aa594a25960d2fe8"
|
||||
integrity sha512-XeWn6Gms5MaQWdj+C4fuxuo/Icy8ckh+BwAIijhX2LKRHHt1OuctLLLlB0F4EPi55m2IUJNTnv8FH9kSBI7Ogw==
|
||||
"@tsconfig/node20@^20.1.4":
|
||||
version "20.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928"
|
||||
integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==
|
||||
|
||||
"@tufjs/canonical-json@2.0.0":
|
||||
version "2.0.0"
|
||||
@@ -805,10 +805,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23"
|
||||
integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==
|
||||
|
||||
"@types/node@^20.11.30":
|
||||
version "20.11.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f"
|
||||
integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==
|
||||
"@types/node@^20.12.2":
|
||||
version "20.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e"
|
||||
integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
@@ -2410,7 +2410,7 @@ postcss-selector-parser@^6.0.10:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss@^8.4.35, postcss@^8.4.36:
|
||||
postcss@^8.4.35, postcss@^8.4.38:
|
||||
version "8.4.38"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
||||
@@ -2883,10 +2883,10 @@ tar@^6.1.11, tar@^6.1.2:
|
||||
mkdirp "^1.0.3"
|
||||
yallist "^4.0.0"
|
||||
|
||||
terser@^5.29.2:
|
||||
version "5.29.2"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.29.2.tgz#c17d573ce1da1b30f21a877bffd5655dd86fdb35"
|
||||
integrity sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==
|
||||
terser@^5.30.0:
|
||||
version "5.30.0"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.0.tgz#64cb2af71e16ea3d32153f84d990f9be0cdc22bf"
|
||||
integrity sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.3"
|
||||
acorn "^8.8.2"
|
||||
@@ -3002,13 +3002,13 @@ validate-npm-package-name@^5.0.0:
|
||||
dependencies:
|
||||
builtins "^5.0.0"
|
||||
|
||||
vite@^5.2.6:
|
||||
version "5.2.6"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.6.tgz#fc2ce309e0b4871e938cb0aca3b96c422c01f222"
|
||||
integrity sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==
|
||||
vite@^5.2.7:
|
||||
version "5.2.7"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.7.tgz#e1b8a985eb54fcb9467d7f7f009d87485016df6e"
|
||||
integrity sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==
|
||||
dependencies:
|
||||
esbuild "^0.20.1"
|
||||
postcss "^8.4.36"
|
||||
postcss "^8.4.38"
|
||||
rollup "^4.13.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
@@ -3041,10 +3041,10 @@ vue@^3.4.21:
|
||||
"@vue/server-renderer" "3.4.21"
|
||||
"@vue/shared" "3.4.21"
|
||||
|
||||
vuetify@^3.5.11:
|
||||
version "3.5.11"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.11.tgz#9e5b628544e736de0b7f236b704539d544588152"
|
||||
integrity sha512-us5I0jyFwIQYG4v41PFmVMkoc/oJddVT4C2RFjJTI99ttigbQ92gsTeG5SB8BPfmfnUS4paR5BedZwk6W3KlJw==
|
||||
vuetify@^3.5.13:
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.13.tgz#24a45d19ce5dcf71b2653f0bcf3ea91edf1f406c"
|
||||
integrity sha512-3ZyIoHgB2GR87ojIpqNwkkRXlUNTEKh83fjUuQ1hOKdTXzEuZXBgtfUt9kp4WOVnYILGdZKWTJ6gv8nXOa/tZA==
|
||||
|
||||
walk-up-path@^3.0.1:
|
||||
version "3.0.1"
|
||||
|
||||
Reference in New Issue
Block a user