Compare commits

...

5 Commits

Author SHA1 Message Date
Yeicor
064d9aeb35 Automatically update version to 0.7.1 2024-03-26 20:30:21 +00:00
Yeicor
eed0baccac fix automatic naming of objects 2024-03-26 21:25:28 +01:00
Yeicor
72480d82c8 strong performance optimizations for the backend 2024-03-26 21:22:48 +01:00
Yeicor
3de710c8b5 Merge remote-tracking branch 'origin/master' 2024-03-26 20:43:31 +01:00
Yeicor
8ebf48cb36 configurable edge and vertex widths 2024-03-26 20:43:21 +01:00
10 changed files with 108 additions and 54 deletions

View File

@@ -18,6 +18,7 @@ with BuildPart() as example:
# Show it in the frontend with hot-reloading
show(example)
# %%
# If running on CI, export the objects to .glb files for a static deployment

View File

@@ -66,8 +66,14 @@ export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
...(AxesColors.y[0]), 255, ...(AxesColors.y[1]), 255,
...(AxesColors.z[0]), 255, ...(AxesColors.z[1]), 255
].map(x => x / 255.0);
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), '__helper_axes'); // Axes at (0,0,0)!
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], transform, '__helper_axes', WebGL2RenderingContext.POINTS);
// Axes at (0, 0, 0)
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), '__helper_axes');
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], new Matrix4(), '__helper_axes', WebGL2RenderingContext.POINTS);
// Axes at center
if (new Matrix4() != transform) {
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, transform, '__helper_axes_center');
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], transform, '__helper_axes_center', WebGL2RenderingContext.POINTS);
}
}
/**

View File

@@ -9,13 +9,13 @@ const LineSegments2Import = import('three/examples/jsm/lines/LineSegments2.js');
const LineMaterialImport = import('three/examples/jsm/lines/LineMaterial.js');
const LineSegmentsGeometryImport = import('three/examples/jsm/lines/LineSegmentsGeometry.js');
export async function toLineSegments(bufferGeometry: BufferGeometry) {
export async function toLineSegments(bufferGeometry: BufferGeometry, lineWidth: number = 0.1) {
const LineSegments2 = (await LineSegments2Import).LineSegments2;
const LineMaterial = (await LineMaterialImport).LineMaterial;
return new LineSegments2(await toLineSegmentsGeometry(bufferGeometry), new LineMaterial({
color: 0xffffffff,
vertexColors: true,
linewidth: 0.1, // mm
linewidth: lineWidth, // mm
worldUnits: true,
resolution: new Vector2(1, 1), // Update resolution on resize!!!
}));

View File

@@ -13,6 +13,7 @@ export const settings = {
"dev+http://127.0.0.1:32323/"
],
loadHelpers: true,
edgeWidth: 0, /* The default line size for edges, set to 0 to use basic gl.LINEs */
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
monitorEveryMs: 100,
monitorOpenTimeoutMs: 1000,

View File

@@ -22,6 +22,7 @@ import {
mdiRectangle,
mdiRectangleOutline,
mdiSwapHorizontal,
mdiVectorLine,
mdiVectorRectangle
} from '@mdi/js'
import SvgIcon from '@jamescoyle/vue-icon';
@@ -30,9 +31,9 @@ import {Box3} from "three/src/math/Box3.js";
import {Color} from "three/src/math/Color.js";
import {Plane} from "three/src/math/Plane.js";
import {Vector3} from "three/src/math/Vector3.js";
import {Vector2} from "three/src/math/Vector2.js";
import type {MObject3D} from "../tools/Selection.vue";
import {toLineSegments} from "../misc/lines.js";
import {settings} from "../misc/settings.js";
const props = defineProps<{
meshes: Array<Mesh>,
@@ -53,6 +54,7 @@ const clipPlaneY = ref(1);
const clipPlaneSwappedY = ref(false);
const clipPlaneZ = ref(1);
const clipPlaneSwappedZ = ref(false);
const edgeWidth = ref(settings.edgeWidth);
// Count the number of faces, edges and vertices
let faceCount = props.meshes
@@ -205,6 +207,63 @@ watch(clipPlaneSwappedZ, onClipPlanesChange);
// Clip planes are also affected by the camera position, so we need to listen to camera changes
props.viewer!!.onElemReady((elem) => elem.addEventListener('camera-change', onClipPlanesChange))
let edgeWidthChangeCleanup = [] as Array<() => void>;
function onEdgeWidthChange(newEdgeWidth: number) {
let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model;
if (!scene || !sceneModel) return;
edgeWidthChangeCleanup.forEach((f) => f());
edgeWidthChangeCleanup = [];
let linesToImprove: Array<MObject3D> = [];
sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) {
if (child.type == 'Line' || child.type == 'LineSegments') {
// child.material.linewidth = 3; // Not supported in WebGL2
// Swap geometry with LineGeometry to support widths
// https://threejs.org/examples/?q=line#webgl_lines_fat
if (newEdgeWidth > 0) linesToImprove.push(child);
}
if (child.type == 'Points') {
(child.material as any).size = newEdgeWidth > 0 ? newEdgeWidth * 50 : 5;
child.material.needsUpdate = true;
}
}
});
linesToImprove.forEach(async (line: MObject3D) => {
let line2 = await toLineSegments(line.geometry, newEdgeWidth);
// Update resolution on resize
let resizeListener = (elem: HTMLElement) => {
line2.material.resolution.set(elem.clientWidth, elem.clientHeight);
line2.material.needsUpdate = true;
};
props.viewer!!.onElemReady((elem) => {
elem.addEventListener('resize', () => resizeListener(elem));
resizeListener(elem);
});
// Copy the transform of the original line
line2.position.copy(line.position);
line2.computeLineDistances();
line2.userData = Object.assign({}, line.userData);
line.parent!.add(line2);
line.children.forEach((o) => line2.add(o));
line.visible = false;
line.userData.niceLine = line2;
// line.parent!.remove(line); // Keep it for better raycast and selection!
line2.userData.noHit = true;
edgeWidthChangeCleanup.push(() => {
line2.parent!.remove(line2);
line.visible = true;
props.viewer!!.onElemReady((elem) => {
elem.removeEventListener('resize', () => resizeListener(elem));
});
});
});
scene.queueRender()
}
watch(edgeWidth, onEdgeWidthChange);
function onModelLoad() {
let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model;
@@ -213,7 +272,6 @@ function onModelLoad() {
// 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
let childrenToAdd: Array<MObject3D> = [];
let linesToImprove: Array<MObject3D> = [];
sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) {
if (child.type == 'Mesh' || child.type == 'SkinnedMesh') {
@@ -236,44 +294,14 @@ function onModelLoad() {
backChild.material = child.material.clone();
backChild.material.side = BackSide;
backChild.material.color = new Color(0.25, 0.25, 0.25)
child.userData.backChild = backChild;
backChild.userData.noHit = true;
child.userData.backChild = backChild;
childrenToAdd.push(backChild as MObject3D);
}
}
if (child.type == 'Line' || child.type == 'LineSegments') {
// child.material.linewidth = 3; // Not supported in WebGL2
// Swap geometry with LineGeometry to support widths
// https://threejs.org/examples/?q=line#webgl_lines_fat
linesToImprove.push(child);
}
if (child.type == 'Points') {
(child.material as any).size = 7;
child.material.needsUpdate = true;
}
}
});
childrenToAdd.forEach((child: MObject3D) => sceneModel.add(child));
linesToImprove.forEach(async (line: MObject3D) => {
let line2 = await toLineSegments(line.geometry);
// Update resolution on resize
props.viewer!!.onElemReady((elem) => {
let l = () => {
line2.material.resolution.set(elem.clientWidth, elem.clientHeight);
line2.material.needsUpdate = true;
};
elem.addEventListener('resize', l); // TODO: Remove listener when line is replaced
l();
});
line2.computeLineDistances();
line2.userData = Object.assign({}, line.userData);
line.parent!.add(line2);
line.children.forEach((o) => line2.add(o));
line.visible = false;
line.userData.niceLine = line2;
// line.parent!.remove(line); // Keep it for better raycast and selection!
line2.userData.noHit = true;
});
// Furthermore...
// Enabled features may have been reset after a reload
@@ -284,6 +312,8 @@ function onModelLoad() {
onWireframeChange(wireframe.value)
// Clip planes may have been reset after a reload
onClipPlanesChange()
// Edge width may have been reset after a reload
onEdgeWidthChange(edgeWidth.value)
scene.queueRender()
}
@@ -327,6 +357,12 @@ props.viewer!!.onElemReady((elem) => elem.addEventListener('load', onModelLoad))
<v-checkbox-btn trueIcon="mdi-triangle-outline" falseIcon="mdi-triangle" v-model="wireframe"></v-checkbox-btn>
</template>
</v-slider>
<v-slider v-if="edgeCount > 0 || vertexCount > 0" v-model="edgeWidth" hide-details min="0" max="1">
<template v-slot:prepend>
<v-tooltip activator="parent">Edge and vertex sizes</v-tooltip>
<svg-icon type="mdi" :path="mdiVectorLine"></svg-icon>
</template>
</v-slider>
<v-divider></v-divider>
<v-slider v-model="clipPlaneX" hide-details min="0" max="1">
<template v-slot:prepend>

View File

@@ -1,6 +1,6 @@
{
"name": "yet-another-cad-viewer",
"version": "0.7.0",
"version": "0.7.1",
"description": "",
"license": "MIT",
"private": true,

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "yacv-server"
version = "0.7.0"
version = "0.7.1"
description = "Yet Another CAD Viewer (server)"
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
license = "MIT"

View File

@@ -70,7 +70,7 @@ class GLTFMgr:
def _vertices_primitive(self) -> Primitive:
return [p for p in self.gltf.meshes[0].primitives if p.mode == POINTS][0]
def add_face(self, vertices_raw: List[Tuple[float, float, float]], indices_raw: List[Tuple[int, int, int]],
def add_face(self, vertices_raw: List[Vector], indices_raw: List[Tuple[int, int, int]],
tex_coord_raw: List[Tuple[float, float]],
color: Tuple[float, float, float, float] = (1.0, 0.75, 0.0, 1.0)):
"""Add a face to the GLTF mesh"""
@@ -85,7 +85,7 @@ class GLTFMgr:
self._faces_primitive.extras["face_triangles_end"].append(len(self.face_indices))
def add_edge(self, vertices_raw: List[Tuple[Tuple[float, float, float], Tuple[float, float, float]]],
color: Tuple[float, float, float, float] = (0.1, 0.1, 0.4, 1.0)):
color: Tuple[float, float, float, float] = (0.1, 0.1, 1.0, 1.0)):
"""Add an edge to the GLTF mesh"""
vertices_flat = [v for t in vertices_raw for v in t] # Line from 0 to 1, 2 to 3, 4 to 5, etc.
base_index = len(self.edge_positions) // 3
@@ -95,7 +95,7 @@ class GLTFMgr:
self._edges_primitive.extras["edge_points_end"].append(len(self.edge_indices))
def add_vertex(self, vertex: Tuple[float, float, float],
color: Tuple[float, float, float, float] = (0.1, 0.4, 0.1, 1.0)):
color: Tuple[float, float, float, float] = (0.1, 0.1, 0.1, 1.0)):
"""Add a vertex to the GLTF mesh"""
base_index = len(self.vertex_positions) // 3
self.vertex_indices.append(base_index)

View File

@@ -43,17 +43,17 @@ def tessellate(
_tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance)
if edges:
for edge in face.edges():
edge_to_faces[_hashcode(edge.wrapped)] = edge_to_faces.get(_hashcode(edge.wrapped), []) + [face.wrapped]
edge_to_faces[(edge.wrapped)] = edge_to_faces.get((edge.wrapped), []) + [face.wrapped]
if vertices:
for vertex in face.vertices():
vertex_to_faces[_hashcode(vertex.wrapped)] = vertex_to_faces.get(_hashcode(vertex.wrapped), []) + [face.wrapped]
vertex_to_faces[(vertex.wrapped)] = vertex_to_faces.get((vertex.wrapped), []) + [face.wrapped]
if edges:
for edge in shape.edges():
_tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(_hashcode(edge.wrapped), []), angular_tolerance,
_tessellate_edge(mgr, edge.wrapped, edge_to_faces.get((edge.wrapped), []), angular_tolerance,
angular_tolerance)
if vertices:
for vertex in shape.vertices():
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(_hashcode(vertex.wrapped), []))
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get((vertex.wrapped), []))
return mgr.build()
@@ -65,12 +65,12 @@ def _tessellate_face(
angular_tolerance: float = 0.1
):
face = Shape(ocp_face)
face.mesh(tolerance, angular_tolerance)
# face.mesh(tolerance, angular_tolerance)
tri_mesh = face.tessellate(tolerance, angular_tolerance)
poly = BRep_Tool.Triangulation_s(face.wrapped, TopLoc_Location())
if poly is None:
logger.warn("No triangulation found for face")
return GLTF2()
tri_mesh = face.tessellate(tolerance, angular_tolerance)
# Get UV of each face from the parameters
uv = [
@@ -78,7 +78,7 @@ def _tessellate_face(
for v in (poly.UVNode(i) for i in range(1, poly.NbNodes() + 1))
]
vertices = [(v.X, v.Y, v.Z) for v in tri_mesh[0]]
vertices = tri_mesh[0]
indices = tri_mesh[1]
mgr.add_face(vertices, indices, uv)

View File

@@ -18,11 +18,11 @@ from OCP.TopoDS import TopoDS_Shape
from build123d import Shape, Axis, Location, Vector
from dataclasses_json import dataclass_json
from yacv_server.rwlock import RWLock
from yacv_server.cad import get_shape, grab_all_cad, CADCoreLike, CADLike
from yacv_server.myhttp import HTTPHandler
from yacv_server.mylogger import logger
from yacv_server.pubsub import BufferedPubSub
from yacv_server.rwlock import RWLock
from yacv_server.tessellate import _hashcode, tessellate
@@ -278,9 +278,10 @@ class YACV:
edges=event.kwargs.get('edges', True),
vertices=event.kwargs.get('vertices', True))
glb_list_of_bytes = gltf.save_to_bytes()
publish_to.publish(b''.join(glb_list_of_bytes))
logger.info('export(%s) took %.3f seconds, %d parts', name, time.time() - start,
len(gltf.meshes[0].primitives))
glb_bytes = b''.join(glb_list_of_bytes)
publish_to.publish(glb_bytes)
logger.info('export(%s) took %.3f seconds, %s', name, time.time() - start,
sizeof_fmt(len(glb_bytes)))
# In either case return the elements of a subscription to the async generator
subscription = self.build_events[name].subscribe()
@@ -324,9 +325,18 @@ _find_var_name_count = 0
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"""
global _find_var_name_count
obj_shape = get_shape(obj)
for frame in inspect.stack()[avoid_levels:]:
for key, value in frame.frame.f_locals.items():
if value is obj:
if get_shape(value, error=False) is obj_shape:
return key
_find_var_name_count += 1
return 'unknown_var_' + str(_find_var_name_count)
def sizeof_fmt(num, suffix="B"):
for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
if abs(num) < 1024.0:
return f"{num:3.1f}{unit}{suffix}"
num /= 1024.0
return f"{num:.1f}Yi{suffix}"