mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-20 14:37:03 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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/
|
mv "$dir/"* public/
|
||||||
rmdir "$dir"
|
rmdir "$dir"
|
||||||
done
|
done
|
||||||
- uses: "actions/configure-pages@v4"
|
- uses: "actions/configure-pages@v5"
|
||||||
- uses: "actions/upload-pages-artifact@v3"
|
- uses: "actions/upload-pages-artifact@v3"
|
||||||
with:
|
with:
|
||||||
path: 'public'
|
path: 'public'
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yet-another-cad-viewer",
|
"name": "yet-another-cad-viewer",
|
||||||
"version": "0.8.5",
|
"version": "0.8.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -26,11 +26,11 @@
|
|||||||
"three-mesh-bvh": "^0.7.3",
|
"three-mesh-bvh": "^0.7.3",
|
||||||
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
|
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vuetify": "^3.5.11"
|
"vuetify": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node20": "^20.1.3",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.12.2",
|
||||||
"@types/three": "^0.160.0",
|
"@types/three": "^0.160.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
"commander": "^12.0.0",
|
"commander": "^12.0.0",
|
||||||
"generate-license-file": "^3.0.1",
|
"generate-license-file": "^3.0.1",
|
||||||
"npm-run-all2": "^6.1.1",
|
"npm-run-all2": "^6.1.1",
|
||||||
"terser": "^5.29.2",
|
"terser": "^5.30.0",
|
||||||
"typescript": "~5.4.3",
|
"typescript": "~5.4.3",
|
||||||
"vite": "^5.2.6",
|
"vite": "^5.2.7",
|
||||||
"vue-tsc": "^2.0.7"
|
"vue-tsc": "^2.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "yacv-server"
|
name = "yacv-server"
|
||||||
version = "0.8.5"
|
version = "0.8.6"
|
||||||
description = "Yet Another CAD Viewer (server)"
|
description = "Yet Another CAD Viewer (server)"
|
||||||
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
|
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,12 +46,21 @@ 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:
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
obj_iter = iter(obj.values())
|
||||||
|
else:
|
||||||
obj_iter = iter(obj)
|
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
|
||||||
|
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)
|
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()
|
||||||
|
|||||||
@@ -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()
|
|
||||||
|
|||||||
@@ -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 yacv_server.cad import _hashcode
|
||||||
|
|
||||||
|
|
||||||
@dataclass_json
|
@dataclass_json
|
||||||
|
|||||||
44
yarn.lock
44
yarn.lock
@@ -777,10 +777,10 @@
|
|||||||
"@sigstore/core" "^1.0.0"
|
"@sigstore/core" "^1.0.0"
|
||||||
"@sigstore/protobuf-specs" "^0.3.0"
|
"@sigstore/protobuf-specs" "^0.3.0"
|
||||||
|
|
||||||
"@tsconfig/node20@^20.1.3":
|
"@tsconfig/node20@^20.1.4":
|
||||||
version "20.1.3"
|
version "20.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.3.tgz#b3b4cf785e1b390a6ab48a68aa594a25960d2fe8"
|
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928"
|
||||||
integrity sha512-XeWn6Gms5MaQWdj+C4fuxuo/Icy8ckh+BwAIijhX2LKRHHt1OuctLLLlB0F4EPi55m2IUJNTnv8FH9kSBI7Ogw==
|
integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==
|
||||||
|
|
||||||
"@tufjs/canonical-json@2.0.0":
|
"@tufjs/canonical-json@2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@@ -805,10 +805,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23"
|
resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23"
|
||||||
integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==
|
integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==
|
||||||
|
|
||||||
"@types/node@^20.11.30":
|
"@types/node@^20.12.2":
|
||||||
version "20.11.30"
|
version "20.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e"
|
||||||
integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==
|
integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~5.26.4"
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
@@ -2410,7 +2410,7 @@ postcss-selector-parser@^6.0.10:
|
|||||||
cssesc "^3.0.0"
|
cssesc "^3.0.0"
|
||||||
util-deprecate "^1.0.2"
|
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"
|
version "8.4.38"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
||||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
||||||
@@ -2883,10 +2883,10 @@ tar@^6.1.11, tar@^6.1.2:
|
|||||||
mkdirp "^1.0.3"
|
mkdirp "^1.0.3"
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
terser@^5.29.2:
|
terser@^5.30.0:
|
||||||
version "5.29.2"
|
version "5.30.0"
|
||||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.29.2.tgz#c17d573ce1da1b30f21a877bffd5655dd86fdb35"
|
resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.0.tgz#64cb2af71e16ea3d32153f84d990f9be0cdc22bf"
|
||||||
integrity sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==
|
integrity sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/source-map" "^0.3.3"
|
"@jridgewell/source-map" "^0.3.3"
|
||||||
acorn "^8.8.2"
|
acorn "^8.8.2"
|
||||||
@@ -3002,13 +3002,13 @@ validate-npm-package-name@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
builtins "^5.0.0"
|
builtins "^5.0.0"
|
||||||
|
|
||||||
vite@^5.2.6:
|
vite@^5.2.7:
|
||||||
version "5.2.6"
|
version "5.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.6.tgz#fc2ce309e0b4871e938cb0aca3b96c422c01f222"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.7.tgz#e1b8a985eb54fcb9467d7f7f009d87485016df6e"
|
||||||
integrity sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==
|
integrity sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild "^0.20.1"
|
esbuild "^0.20.1"
|
||||||
postcss "^8.4.36"
|
postcss "^8.4.38"
|
||||||
rollup "^4.13.0"
|
rollup "^4.13.0"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.3"
|
fsevents "~2.3.3"
|
||||||
@@ -3041,10 +3041,10 @@ vue@^3.4.21:
|
|||||||
"@vue/server-renderer" "3.4.21"
|
"@vue/server-renderer" "3.4.21"
|
||||||
"@vue/shared" "3.4.21"
|
"@vue/shared" "3.4.21"
|
||||||
|
|
||||||
vuetify@^3.5.11:
|
vuetify@^3.5.13:
|
||||||
version "3.5.11"
|
version "3.5.13"
|
||||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.11.tgz#9e5b628544e736de0b7f236b704539d544588152"
|
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.13.tgz#24a45d19ce5dcf71b2653f0bcf3ea91edf1f406c"
|
||||||
integrity sha512-us5I0jyFwIQYG4v41PFmVMkoc/oJddVT4C2RFjJTI99ttigbQ92gsTeG5SB8BPfmfnUS4paR5BedZwk6W3KlJw==
|
integrity sha512-3ZyIoHgB2GR87ojIpqNwkkRXlUNTEKh83fjUuQ1hOKdTXzEuZXBgtfUt9kp4WOVnYILGdZKWTJ6gv8nXOa/tZA==
|
||||||
|
|
||||||
walk-up-path@^3.0.1:
|
walk-up-path@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user