mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-20 22:47:04 +01:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ba0e18479 | ||
|
|
eca2bbfa7c | ||
|
|
86180c424e | ||
|
|
e42d6be074 | ||
|
|
e2d6a3cb00 | ||
|
|
9e453b7890 | ||
|
|
0b8faa9e8b | ||
|
|
00bc2a15e0 | ||
|
|
432abcf85c | ||
|
|
4b6d3f6266 | ||
|
|
255ae72ed2 | ||
|
|
77dd9fb43e | ||
|
|
5dc2ae2f8d | ||
|
|
58440723bd | ||
|
|
bfdd656316 | ||
|
|
7408823c02 | ||
|
|
856ffbc4c5 | ||
|
|
d0f8463bbf | ||
|
|
162d3e22a2 | ||
|
|
4b06559ab8 | ||
|
|
9afa2e5786 | ||
|
|
7196fb2f32 | ||
|
|
8ec60faa04 | ||
|
|
13bbdd5956 |
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'
|
||||||
|
|||||||
@@ -2225,13 +2225,13 @@ THE SOFTWARE.
|
|||||||
|
|
||||||
The following npm package may be included in this product:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- three@0.162.0
|
- three@0.160.1
|
||||||
|
|
||||||
This package contains the following license and notice below:
|
This package contains the following license and notice below:
|
||||||
|
|
||||||
The MIT License
|
The MIT License
|
||||||
|
|
||||||
Copyright © 2010-2024 three.js authors
|
Copyright © 2010-2023 three.js authors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -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,11 +28,16 @@ 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});
|
||||||
|
|
||||||
async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
||||||
|
// Trigger progress bar as soon as possible (also triggered earlier for each raw notification)
|
||||||
|
if (viewer.value && event.models.length > 0) {
|
||||||
|
viewer.value.onProgress(0.10);
|
||||||
|
}
|
||||||
// Load/unload a new batch of models to optimize rendering time
|
// Load/unload a new batch of models to optimize rendering time
|
||||||
console.log("Received model update request", event.models);
|
console.log("Received model update request", event.models);
|
||||||
let shutdownRequestIndex = event.models.findIndex((model) => model.isRemove == null);
|
let shutdownRequestIndex = event.models.findIndex((model) => model.isRemove == null);
|
||||||
@@ -45,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);
|
||||||
@@ -64,12 +70,14 @@ 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
|
||||||
let networkMgr = new NetworkManager();
|
let networkMgr = new NetworkManager();
|
||||||
|
networkMgr.addEventListener('update-early',
|
||||||
|
(e) => viewer.value?.onProgress((e as CustomEvent<Array<any>>).detail.length * 0.01));
|
||||||
networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent));
|
networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent));
|
||||||
// Start loading all configured models ASAP
|
// Start loading all configured models ASAP
|
||||||
for (let model of settings.preload) {
|
for (let model of settings.preload) {
|
||||||
@@ -108,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) {
|
||||||
|
|||||||
@@ -21,7 +21,32 @@ export async function mergePartial(url: string, name: string, document: Document
|
|||||||
networkFinished();
|
networkFinished();
|
||||||
|
|
||||||
// Load the new document
|
// Load the new document
|
||||||
let newDoc = await io.readBinary(new Uint8Array(buffer));
|
let newDoc = null;
|
||||||
|
let alreadyTried: { [name: string]: boolean } = {}
|
||||||
|
while (newDoc == null) { // Retry adding extensions as required until the document is loaded
|
||||||
|
try { // Try to load fast if no extensions are used
|
||||||
|
newDoc = await io.readBinary(new Uint8Array(buffer));
|
||||||
|
} catch (e) { // Fallback to wait for download and register big extensions
|
||||||
|
if (e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
|
||||||
|
if (alreadyTried["draco"]) throw e; else alreadyTried["draco"] = true;
|
||||||
|
// WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?)
|
||||||
|
let {KHRDracoMeshCompression} = await import("@gltf-transform/extensions")
|
||||||
|
let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
|
||||||
|
let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js");
|
||||||
|
io.registerExtensions([KHRDracoMeshCompression])
|
||||||
|
.registerDependencies({
|
||||||
|
'draco3d.decoder': await dracoDecoderWeb.default({}),
|
||||||
|
'draco3d.encoder': await dracoEncoderWeb.default({})
|
||||||
|
});
|
||||||
|
} else if (e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
|
||||||
|
if (alreadyTried["webp"]) throw e; else alreadyTried["webp"] = true;
|
||||||
|
let {EXTTextureWebP} = await import("@gltf-transform/extensions")
|
||||||
|
io.registerExtensions([EXTTextureWebP]);
|
||||||
|
} else { // TODO: Add more extensions as required
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove any previous model with the same name
|
// Remove any previous model with the same name
|
||||||
await document.transform(dropByName(name));
|
await document.transform(dropByName(name));
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -93,30 +93,49 @@ export class NetworkManager extends EventTarget {
|
|||||||
|
|
||||||
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean | null, disconnect: () => void = () => {
|
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean | null, disconnect: () => void = () => {
|
||||||
}) {
|
}) {
|
||||||
let prevHash = this.knownObjectHashes[name];
|
// console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);
|
||||||
// console.debug("Found model", name, "with hash", hash, "and previous hash", prevHash);
|
|
||||||
if (!hash || hash !== prevHash || isRemove) {
|
// We only care about the latest update per model name
|
||||||
// Update known hashes
|
|
||||||
if (isRemove == false) {
|
|
||||||
this.knownObjectHashes[name] = hash;
|
|
||||||
} else if (isRemove == true) {
|
|
||||||
if (!(name in this.knownObjectHashes)) return; // Nothing to remove...
|
|
||||||
delete this.knownObjectHashes[name];
|
|
||||||
// Also update buffered updates if the model is removed
|
|
||||||
this.bufferedUpdates = this.bufferedUpdates.filter(m => m.name !== name);
|
this.bufferedUpdates = this.bufferedUpdates.filter(m => m.name !== name);
|
||||||
}
|
|
||||||
let newModel = new NetworkUpdateEventModel(name, url, hash, isRemove);
|
// Add the new model to the list of updates and dispatch the early update
|
||||||
this.bufferedUpdates.push(newModel);
|
let upd = new NetworkUpdateEventModel(name, url, hash, isRemove);
|
||||||
|
this.bufferedUpdates.push(upd);
|
||||||
|
this.dispatchEvent(new CustomEvent("update-early", {detail: this.bufferedUpdates}));
|
||||||
|
|
||||||
// Optimization: try to batch updates automatically for faster rendering
|
// Optimization: try to batch updates automatically for faster rendering
|
||||||
if (this.batchTimeout !== null) clearTimeout(this.batchTimeout);
|
if (this.batchTimeout !== null) clearTimeout(this.batchTimeout);
|
||||||
this.batchTimeout = setTimeout(() => {
|
this.batchTimeout = setTimeout(() => {
|
||||||
|
// Update known hashes for minimal updates
|
||||||
|
for (let model of this.bufferedUpdates) {
|
||||||
|
if (model.isRemove == false && model.hash && model.hash === this.knownObjectHashes[model.name]) {
|
||||||
|
// Delete this useless update
|
||||||
|
let foundFirst = false;
|
||||||
|
this.bufferedUpdates = this.bufferedUpdates.filter(m => {
|
||||||
|
if (m === model) {
|
||||||
|
if (!foundFirst) { // Remove only first full match
|
||||||
|
foundFirst = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Keep this update and update the last known hash
|
||||||
|
if (model.isRemove == true) {
|
||||||
|
if (model.name in this.knownObjectHashes) delete this.knownObjectHashes[model.name];
|
||||||
|
} else if (model.isRemove == false) {
|
||||||
|
this.knownObjectHashes[model.name] = model.hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch the event to actually update the models
|
||||||
this.dispatchEvent(new NetworkUpdateEvent(this.bufferedUpdates, disconnect));
|
this.dispatchEvent(new NetworkUpdateEvent(this.bufferedUpdates, disconnect));
|
||||||
this.bufferedUpdates = [];
|
this.bufferedUpdates = [];
|
||||||
}, batchTimeout);
|
}, batchTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function* readLinesStreamings(reader: ReadableStreamDefaultReader<Uint8Array>) {
|
async function* readLinesStreamings(reader: ReadableStreamDefaultReader<Uint8Array>) {
|
||||||
let decoder = new TextDecoder();
|
let decoder = new TextDecoder();
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ export class SceneMgr {
|
|||||||
let loadStart = performance.now();
|
let loadStart = performance.now();
|
||||||
let loadNetworkEnd: number;
|
let loadNetworkEnd: number;
|
||||||
|
|
||||||
|
try {
|
||||||
// Start merging into the current document, replacing or adding as needed
|
// Start merging into the current document, replacing or adding as needed
|
||||||
document = await mergePartial(url, name, document, () => loadNetworkEnd = performance.now());
|
document = await mergePartial(url, name, document, () => loadNetworkEnd = performance.now());
|
||||||
|
|
||||||
console.log("Model", name, "loaded in", performance.now() - loadNetworkEnd!, "ms after",
|
console.log("Model", name, "loaded in", performance.now() - loadNetworkEnd!, "ms after",
|
||||||
loadNetworkEnd! - loadStart, "ms of transferring data (maybe building the object on the server)");
|
loadNetworkEnd! - loadStart, "ms of transferring data (maybe building the object on the server)");
|
||||||
|
} finally {
|
||||||
if (updateHelpers) {
|
if (updateHelpers) {
|
||||||
// Reload the helpers to fit the new model
|
// Reload the helpers to fit the new model
|
||||||
await this.reloadHelpers(sceneUrl, document, reloadScene);
|
await this.reloadHelpers(sceneUrl, document, reloadScene);
|
||||||
@@ -31,6 +32,7 @@ export class SceneMgr {
|
|||||||
document = await this.showCurrentDoc(sceneUrl, document);
|
document = await this.showCurrentDoc(sceneUrl, document);
|
||||||
console.log("Scene displayed in", performance.now() - displayStart, "ms");
|
console.log("Scene displayed in", performance.now() - displayStart, "ms");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
@@ -97,7 +99,7 @@ export class SceneMgr {
|
|||||||
// Serialize the document into a GLB and update the viewerSrc
|
// Serialize the document into a GLB and update the viewerSrc
|
||||||
let buffer = await toBuffer(document);
|
let buffer = await toBuffer(document);
|
||||||
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
|
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
|
||||||
console.debug("Showing current doc", document, "as", Array.from(buffer));
|
console.debug("Showing current doc", document, "with", buffer.length, "total bytes");
|
||||||
sceneUrl.value = URL.createObjectURL(blob);
|
sceneUrl.value = URL.createObjectURL(blob);
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -43,10 +43,11 @@ const emit = defineEmits<{ remove: [] }>()
|
|||||||
|
|
||||||
let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
|
let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
|
||||||
|
|
||||||
// Reactive properties
|
// Count the number of faces, edges and vertices
|
||||||
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
let faceCount = ref(-1);
|
||||||
const opacity = defineModel<number>("opacity", {default: 1});
|
let edgeCount = ref(-1);
|
||||||
const wireframe = ref(false);
|
let vertexCount = ref(-1);
|
||||||
|
|
||||||
// Clipping planes are handled in y-up space (swapped on interface, Z inverted later)
|
// Clipping planes are handled in y-up space (swapped on interface, Z inverted later)
|
||||||
const clipPlaneX = ref(1);
|
const clipPlaneX = ref(1);
|
||||||
const clipPlaneSwappedX = ref(false);
|
const clipPlaneSwappedX = ref(false);
|
||||||
@@ -56,24 +57,10 @@ const clipPlaneZ = ref(1);
|
|||||||
const clipPlaneSwappedZ = ref(false);
|
const clipPlaneSwappedZ = ref(false);
|
||||||
const edgeWidth = ref(settings.edgeWidth);
|
const edgeWidth = ref(settings.edgeWidth);
|
||||||
|
|
||||||
// Count the number of faces, edges and vertices
|
// Misc properties
|
||||||
let faceCount = props.meshes
|
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
||||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES))
|
const opacity = defineModel<number>("opacity", {default: 1});
|
||||||
.map(p => (p.getExtras()?.face_triangles_end as any)?.length ?? 1)
|
const wireframe = ref(false);
|
||||||
.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)
|
|
||||||
|
|
||||||
// Listeners for changes in the properties (or viewer reloads)
|
// Listeners for changes in the properties (or viewer reloads)
|
||||||
function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
||||||
@@ -81,9 +68,6 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
|||||||
let scene = props.viewer?.scene;
|
let scene = props.viewer?.scene;
|
||||||
let sceneModel = (scene as any)?._model;
|
let sceneModel = (scene as any)?._model;
|
||||||
if (!scene || !sceneModel) return;
|
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) => {
|
sceneModel.traverse((child: MObject3D) => {
|
||||||
if (child.userData[extrasNameKey] === modelName) {
|
if (child.userData[extrasNameKey] === modelName) {
|
||||||
let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh'
|
let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh'
|
||||||
@@ -101,16 +85,12 @@ 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;
|
||||||
let sceneModel = (scene as any)?._model;
|
let sceneModel = (scene as any)?._model;
|
||||||
if (!scene || !sceneModel) return;
|
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) => {
|
sceneModel.traverse((child: MObject3D) => {
|
||||||
if (child.userData[extrasNameKey] === modelName) {
|
if (child.userData[extrasNameKey] === modelName) {
|
||||||
if (child.material && child.material.opacity !== newOpacity) {
|
if (child.material && child.material.opacity !== newOpacity) {
|
||||||
@@ -129,10 +109,6 @@ function onWireframeChange(newWireframe: boolean) {
|
|||||||
let scene = props.viewer?.scene;
|
let scene = props.viewer?.scene;
|
||||||
let sceneModel = (scene as any)?._model;
|
let sceneModel = (scene as any)?._model;
|
||||||
if (!scene || !sceneModel) return;
|
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) => {
|
sceneModel.traverse((child: MObject3D) => {
|
||||||
if (child.userData[extrasNameKey] === modelName) {
|
if (child.userData[extrasNameKey] === modelName) {
|
||||||
if (child.material && child.material.wireframe !== newWireframe) {
|
if (child.material && child.material.wireframe !== newWireframe) {
|
||||||
@@ -146,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;
|
||||||
@@ -268,11 +242,36 @@ function onModelLoad() {
|
|||||||
let scene = props.viewer?.scene;
|
let scene = props.viewer?.scene;
|
||||||
let sceneModel = (scene as any)?._model;
|
let sceneModel = (scene as any)?._model;
|
||||||
if (!scene || !sceneModel) return;
|
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
|
// Count the number of faces, edges and vertices
|
||||||
// of not actually removing the primitives from the scene graph
|
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> = [];
|
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)
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ function onRemove(mesh: Mesh) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findModel(name: string) {
|
function findModel(name: string) {
|
||||||
console.log('Find model', name);
|
|
||||||
if (!expandedNames.value.includes(name)) expandedNames.value.push(name);
|
if (!expandedNames.value.includes(name)) expandedNames.value.push(name);
|
||||||
console.log('Expanded', expandedNames.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({findModel})
|
defineExpose({findModel})
|
||||||
|
|||||||
2
frontend/shims.d.ts
vendored
2
frontend/shims.d.ts
vendored
@@ -1,3 +1,5 @@
|
|||||||
// Avoids typescript error when importing some files
|
// Avoids typescript error when importing some files
|
||||||
declare module '@jamescoyle/vue-icon'
|
declare module '@jamescoyle/vue-icon'
|
||||||
declare module 'three-orientation-gizmo/src/OrientationGizmo'
|
declare module 'three-orientation-gizmo/src/OrientationGizmo'
|
||||||
|
declare module 'three/examples/jsm/libs/draco/draco_decoder.js'
|
||||||
|
declare module 'three/examples/jsm/libs/draco/draco_encoder.js'
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelSc
|
|||||||
import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot";
|
import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot";
|
||||||
import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer";
|
import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer";
|
||||||
import type {Vector3} from "three";
|
import type {Vector3} from "three";
|
||||||
import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh';
|
|
||||||
import {BufferGeometry, Mesh} from "three";
|
import {BufferGeometry, Mesh} from "three";
|
||||||
|
import {acceleratedRaycast, computeBoundsTree, disposeBoundsTree} from 'three-mesh-bvh';
|
||||||
|
|
||||||
ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
|
ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@@ -41,8 +41,31 @@ onMounted(() => {
|
|||||||
emit('load')
|
emit('load')
|
||||||
});
|
});
|
||||||
elem.value.addEventListener('camera-change', onCameraChange);
|
elem.value.addEventListener('camera-change', onCameraChange);
|
||||||
|
elem.value.addEventListener('progress', (ev) => onProgress((ev as any).detail.totalProgress));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handles loading the events for <model-viewer>'s slotted progress bar
|
||||||
|
const progressBar = ref<HTMLElement | null>(null);
|
||||||
|
const updateBar = ref<HTMLElement | null>(null);
|
||||||
|
let onProgressHideTimeout: number | null = null;
|
||||||
|
const onProgress = (totalProgress: number) => {
|
||||||
|
if (!progressBar.value || !updateBar.value) return;
|
||||||
|
// Update the progress bar and ensure it's visible
|
||||||
|
progressBar.value.style.display = 'block';
|
||||||
|
progressBar.value.style.opacity = '1'; // Fade in
|
||||||
|
updateBar.value.style.width = `${totalProgress * 100}%`;
|
||||||
|
// Auto-hide smoothly when no progress is made for a while
|
||||||
|
if (onProgressHideTimeout) clearTimeout(onProgressHideTimeout);
|
||||||
|
onProgressHideTimeout = setTimeout(() => {
|
||||||
|
if (!progressBar.value) return;
|
||||||
|
progressBar.value.style.opacity = '0'; // Fade out
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!progressBar.value) return;
|
||||||
|
progressBar.value.style.display = 'none'; // Actually hide
|
||||||
|
}, 300); // 0.3s fade out
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
class Line3DData {
|
class Line3DData {
|
||||||
startHotspot: HTMLElement = document.body
|
startHotspot: HTMLElement = document.body
|
||||||
endHotspot: HTMLElement = document.body
|
endHotspot: HTMLElement = document.body
|
||||||
@@ -137,7 +160,7 @@ function entries(lines: { [id: number]: Line3DData }): [string, Line3DData][] {
|
|||||||
return Object.entries(lines);
|
return Object.entries(lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D});
|
defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D, onProgress});
|
||||||
|
|
||||||
let {disableTap} = inject<{ disableTap: Ref<boolean> }>('disableTap')!!;
|
let {disableTap} = inject<{ disableTap: Ref<boolean> }>('disableTap')!!;
|
||||||
watch(disableTap, (value) => {
|
watch(disableTap, (value) => {
|
||||||
@@ -155,7 +178,8 @@ watch(disableTap, (value) => {
|
|||||||
:shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay"
|
:shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay"
|
||||||
:ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background"
|
:ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background"
|
||||||
:environment-image="settings.background">
|
:environment-image="settings.background">
|
||||||
<slot></slot> <!-- Controls, annotations, etc. -->
|
<slot></slot>
|
||||||
|
<!-- Display some information during initial load -->
|
||||||
<div class="annotation initial-load-banner">
|
<div class="annotation initial-load-banner">
|
||||||
Trying to load models from...
|
Trying to load models from...
|
||||||
<v-list v-for="src in settings.preload" :key="src">
|
<v-list v-for="src in settings.preload" :key="src">
|
||||||
@@ -163,6 +187,11 @@ watch(disableTap, (value) => {
|
|||||||
</v-list>
|
</v-list>
|
||||||
<!-- Too much idle CPU usage: <loading></loading> -->
|
<!-- Too much idle CPU usage: <loading></loading> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Customize the progress bar -->
|
||||||
|
<div class="progress-bar" slot="progress-bar" ref="progressBar">
|
||||||
|
<div class="update-bar" ref="updateBar"/>
|
||||||
|
</div>
|
||||||
</model-viewer>
|
</model-viewer>
|
||||||
|
|
||||||
<!-- The SVG overlay for fake 3D lines attached to the model -->
|
<!-- The SVG overlay for fake 3D lines attached to the model -->
|
||||||
@@ -223,4 +252,30 @@ watch(disableTap, (value) => {
|
|||||||
.initial-load-banner .v-list-item {
|
.initial-load-banner .v-list-item {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 10%;
|
||||||
|
max-height: 2%;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 0;
|
||||||
|
transform: translate3d(-50%, 0%, 0);
|
||||||
|
border-radius: 25px;
|
||||||
|
box-shadow: 0 3px 10px 3px rgba(0, 0, 0, 0.5), 0 0 5px 1px rgba(0, 0, 0, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-bar {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 25px;
|
||||||
|
float: left;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yet-another-cad-viewer",
|
"name": "yet-another-cad-viewer",
|
||||||
"version": "0.8.1",
|
"version": "0.8.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"update-licenses": "generate-license-file --input package.json --output assets/licenses.txt --overwrite"
|
"update-licenses": "generate-license-file --input package.json --output assets/licenses.txt --overwrite"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gltf-transform/core": "^3.10.0",
|
"@gltf-transform/core": "^3.10.1",
|
||||||
|
"@gltf-transform/extensions": "^3.10.1",
|
||||||
"@gltf-transform/functions": "^3.10.1",
|
"@gltf-transform/functions": "^3.10.1",
|
||||||
"@google/model-viewer": "^3.4.0",
|
"@google/model-viewer": "^3.4.0",
|
||||||
"@jamescoyle/vue-icon": "^0.1.2",
|
"@jamescoyle/vue-icon": "^0.1.2",
|
||||||
@@ -25,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",
|
||||||
@@ -38,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.1"
|
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 huge
|
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,10 +2,15 @@
|
|||||||
Utilities to work with CAD objects
|
Utilities to work with CAD objects
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import Optional, Union, List, Tuple
|
import io
|
||||||
|
import re
|
||||||
|
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 yacv_server.gltf import GLTFMgr
|
from yacv_server.gltf import GLTFMgr
|
||||||
|
|
||||||
@@ -40,22 +45,42 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]:
|
|||||||
if isinstance(obj, TopoDS_Shape):
|
if isinstance(obj, TopoDS_Shape):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
# Handle iterables like Build123d ShapeList by extracting all sub-shapes and making a compound
|
||||||
|
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) 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
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)')
|
raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)')
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def grab_all_cad() -> List[Tuple[str, CADCoreLike]]:
|
def grab_all_cad() -> set[Tuple[str, CADCoreLike]]:
|
||||||
""" Grab all shapes by inspecting the stack """
|
""" Grab all shapes by inspecting the stack """
|
||||||
import inspect
|
import inspect
|
||||||
stack = inspect.stack()
|
stack = inspect.stack()
|
||||||
shapes = []
|
shapes = set()
|
||||||
for frame in stack:
|
for frame in stack:
|
||||||
for key, value in frame.frame.f_locals.items():
|
for key, value in frame.frame.f_locals.items():
|
||||||
shape = get_shape(value, error=False)
|
shape = get_shape(value, error=False)
|
||||||
if shape:
|
if shape and shape not in shapes:
|
||||||
shapes.append((key, shape))
|
shapes.add((key, shape))
|
||||||
return shapes
|
return shapes
|
||||||
|
|
||||||
|
|
||||||
@@ -137,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
|
||||||
@@ -194,7 +195,7 @@ class YACV:
|
|||||||
|
|
||||||
def show_cad_all(self, **kwargs):
|
def show_cad_all(self, **kwargs):
|
||||||
"""Publishes all CAD objects in the current scope to the server"""
|
"""Publishes all CAD objects in the current scope to the server"""
|
||||||
all_cad = grab_all_cad()
|
all_cad = list(grab_all_cad()) # List for reproducible iteration order
|
||||||
self.show(*[cad for _, cad in all_cad], names=[name for name, _ in all_cad], **kwargs)
|
self.show(*[cad for _, cad in all_cad], names=[name for name, _ in all_cad], **kwargs)
|
||||||
|
|
||||||
def remove(self, name: str):
|
def remove(self, name: str):
|
||||||
@@ -325,7 +326,7 @@ _find_var_name_count = 0
|
|||||||
def _find_var_name(obj: any, avoid_levels: int = 2) -> str:
|
def _find_var_name(obj: any, avoid_levels: int = 2) -> str:
|
||||||
"""A hacky way to get a stable name for an object that may change over time"""
|
"""A hacky way to get a stable name for an object that may change over time"""
|
||||||
global _find_var_name_count
|
global _find_var_name_count
|
||||||
obj_shape = get_shape(obj)
|
obj_shape = get_shape(obj, error=False) or obj
|
||||||
for frame in inspect.stack()[avoid_levels:]:
|
for frame in inspect.stack()[avoid_levels:]:
|
||||||
for key, value in frame.frame.f_locals.items():
|
for key, value in frame.frame.f_locals.items():
|
||||||
if get_shape(value, error=False) is obj_shape:
|
if get_shape(value, error=False) is obj_shape:
|
||||||
|
|||||||
46
yarn.lock
46
yarn.lock
@@ -390,7 +390,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
|
||||||
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
|
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
|
||||||
|
|
||||||
"@gltf-transform/core@^3.10.0", "@gltf-transform/core@^3.10.1":
|
"@gltf-transform/core@^3.10.1":
|
||||||
version "3.10.1"
|
version "3.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/@gltf-transform/core/-/core-3.10.1.tgz#d99c060b499482ed2c3304466405bf4c10939831"
|
resolved "https://registry.yarnpkg.com/@gltf-transform/core/-/core-3.10.1.tgz#d99c060b499482ed2c3304466405bf4c10939831"
|
||||||
integrity sha512-50OYemknGNxjBmiOM6iJp04JAu0bl9jvXJfN/gFt9QdJO02cPDcoXlTfSPJG6TVWDcfl0xPlsx1vybcbPVGFcQ==
|
integrity sha512-50OYemknGNxjBmiOM6iJp04JAu0bl9jvXJfN/gFt9QdJO02cPDcoXlTfSPJG6TVWDcfl0xPlsx1vybcbPVGFcQ==
|
||||||
@@ -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