t view/hide shape
diff --git a/src/Display/WebGl/threejs_renderer.py b/src/Display/WebGl/threejs_renderer.py
index 82a5c234c..167905b82 100644
--- a/src/Display/WebGl/threejs_renderer.py
+++ b/src/Display/WebGl/threejs_renderer.py
@@ -1,4 +1,4 @@
-##Copyright 2011-2019 Thomas Paviot (tpaviot@gmail.com)
+##Copyright 2011-2024 Thomas Paviot (tpaviot@gmail.com)
##
##This file is part of pythonOCC.
##
@@ -15,31 +15,39 @@
##You should have received a copy of the GNU Lesser General Public License
##along with pythonOCC. If not, see
.
+import json
import os
+from string import Template
import sys
import tempfile
import uuid
-import json
+from typing import Any, Dict, Generator, List, Optional, Tuple
from OCC.Core.gp import gp_Vec
from OCC.Core.Tesselator import ShapeTesselator
-from OCC import VERSION as OCC_VERSION
+from OCC import VERSION
from OCC.Extend.TopologyUtils import is_edge, is_wire, discretize_edge, discretize_wire
from OCC.Display.WebGl.simple_server import start_server
-THREEJS_RELEASE = "r143"
-
-def spinning_cursor():
+def spinning_cursor() -> Generator[str, None, None]:
+ """
+ A spinning cursor generator.
+ """
while True:
- for cursor in "|/-\\":
- yield cursor
+ yield from "|/-\\"
-def color_to_hex(rgb_color):
- """Takes a tuple with 3 floats between 0 and 1.
- Returns a hex. Useful to convert occ colors to web color code
+def color_to_hex(rgb_color: Tuple[float, float, float]) -> str:
+ """
+ Converts a color from RGB to a hex string.
+
+ Args:
+ rgb_color (tuple): A tuple of 3 floats (R, G, B) between 0 and 1.
+
+ Returns:
+ str: The color as a hex string.
"""
r, g, b = rgb_color
if not (0 <= r <= 1.0 and 0 <= g <= 1.0 and 0 <= b <= 1.0):
@@ -50,15 +58,23 @@ def color_to_hex(rgb_color):
return "0x%.02x%.02x%.02x" % (rh, gh, bh)
-def export_edgedata_to_json(edge_hash, point_set):
- """Export a set of points to a LineSegment buffergeometry"""
+def export_edgedata_to_json(edge_hash: str, point_set: List[List[float]]) -> str:
+ """
+ Exports a set of points to a LineSegment buffergeometry.
+
+ Args:
+ edge_hash (str): The hash of the edge.
+ point_set (list): A list of points.
+
+ Returns:
+ str: The JSON string.
+ """
# first build the array of point coordinates
# edges are built as follows:
# points_coordinates =[P0x, P0y, P0z, P1x, P1y, P1z, P2x, P2y, etc.]
points_coordinates = []
for point in point_set:
- for coord in point:
- points_coordinates.append(coord)
+ points_coordinates.extend(iter(point))
# then build the dictionary exported to json
edges_data = {
"metadata": {
@@ -78,18 +94,19 @@ def export_edgedata_to_json(edge_hash, point_set):
}
},
}
- return json.dumps(edges_data)
+ return json.dumps(edges_data, indent=4)
-HEADER = """
+HEADER_TEMPLATE = Template(
+ """
-
pythonocc @VERSION@ webgl renderer
+
pythonocc $VERSION webgl renderer
"""
-BODY_PART0 = """
-
+)
+
+BODY_TEMPLATE = Template(
+ """
+
t view/hide shape
@@ -147,316 +167,326 @@ def export_edgedata_to_json(edge_hash, point_set):
g view/hide grid
a view/hide axis
-
-
-
-
-""" % (
- THREEJS_RELEASE,
- THREEJS_RELEASE,
- THREEJS_RELEASE,
- THREEJS_RELEASE,
+ $VertexShaderDefinition
+ $FragmentShaderDefinition
+
+
+
+
+"""
)
-BODY_PART1 = """
-
- @VertexShaderDefinition@
- @FragmentShaderDefinition@
-
-
+ }
+
+ maxRad = maxRad * 0.7; // otherwise the scene seems to be too far away
+ camera.lookAt(center);
+ var direction = new THREE.Vector3().copy(camera.position).sub(controls.target);
+ var len = direction.length();
+ direction.normalize();
+
+ // compute new distance of camera to middle of scene to fit the object to screen
+ var lnew = maxRad / Math.sin(camera.fov/180. * Math.PI / 2.);
+ direction.multiplyScalar(lnew);
+
+ var pnew = new THREE.Vector3().copy(center).add(direction);
+ // change near far values to avoid culling of objects
+ camera.position.set(pnew.x, pnew.y, pnew.z);
+ camera.far = lnew * 50;
+ camera.near = lnew * 50 * 0.001;
+ camera.updateProjectionMatrix();
+ controls.target = center;
+ controls.update();
+
+ // adds and adjust a grid helper if needed
+ gridHelper = new THREE.GridHelper(maxRad*4, 10)
+ scene.add(gridHelper);
+
+ // axisHelper
+ axisHelper = new THREE.AxesHelper(maxRad);
+ scene.add(axisHelper);
+}
+
+function render() {
+ //@IncrementTime@ TODO UNCOMMENT
+ update_lights();
+ renderer.render(scene, camera);
+}
"""
+)
class HTMLHeader:
- def __init__(self, bg_gradient_color1="#ced7de", bg_gradient_color2="#808080"):
+ """
+ A class to generate the HTML header.
+ """
+
+ def __init__(
+ self, bg_gradient_color1: str = "#ced7de", bg_gradient_color2: str = "#808080"
+ ) -> None:
+ """
+ Initializes the HTMLHeader.
+
+ Args:
+ bg_gradient_color1 (str, optional): The first color of the background gradient.
+ bg_gradient_color2 (str, optional): The second color of the background gradient.
+ """
self._bg_gradient_color1 = bg_gradient_color1
self._bg_gradient_color2 = bg_gradient_color2
- def get_str(self):
- header_str = HEADER.replace(
- "@bg_gradient_color1@", "%s" % self._bg_gradient_color1
- )
- header_str = header_str.replace(
- "@bg_gradient_color2@", "%s" % self._bg_gradient_color2
+ def get_str(self) -> str:
+ """
+ Returns the HTML header as a string.
+ """
+ return HEADER_TEMPLATE.substitute(
+ {
+ "bg_gradient_color1": f"{self._bg_gradient_color1}",
+ "bg_gradient_color2": f"{self._bg_gradient_color2}",
+ "VERSION": VERSION,
+ }
)
- header_str = header_str.replace("@VERSION@", OCC_VERSION)
- return header_str
-
-
-class HTMLBody_Part1:
- def __init__(self, vertex_shader=None, fragment_shader=None, uniforms=None):
- self._vertex_shader = vertex_shader
- self._fragment_shader = fragment_shader
- self._uniforms = uniforms
-
- def get_str(self):
- global BODY_PART2
- # get the location where pythonocc is running from
- body_str = BODY_PART1.replace("@VERSION@", OCC_VERSION)
- if (self._fragment_shader is not None) and (self._fragment_shader is not None):
- vertex_shader_string_definition = (
- ''
- % self._vertex_shader
- )
- fragment_shader_string_definition = (
- ''
- % self._fragment_shader
- )
- shader_material_definition = """
- var vertexShader = document.getElementById('vertexShader').textContent;
- var fragmentShader = document.getElementById('fragmentShader').textContent;
- var shader_material = new THREE.ShaderMaterial({uniforms: uniforms,
- vertexShader: vertexShader,
- fragmentShader: fragmentShader});
- """
- if self._uniforms is None:
- body_str = body_str.replace("@Uniforms@", "uniforms ={};\n")
- BODY_PART2 = BODY_PART2.replace("@IncrementTime@", "")
- else:
- body_str = body_str.replace("@Uniforms@", self._uniforms)
- if "time" in self._uniforms:
- BODY_PART2 = BODY_PART2.replace(
- "@IncrementTime@", "uniforms.time.value += 0.05;"
- )
- else:
- BODY_PART2 = BODY_PART2.replace("@IncrementTime@", "")
- body_str = body_str.replace(
- "@VertexShaderDefinition@", vertex_shader_string_definition
- )
- body_str = body_str.replace(
- "@FragmentShaderDefinition@", fragment_shader_string_definition
- )
- body_str = body_str.replace(
- "@ShaderMaterialDefinition@", shader_material_definition
- )
- body_str = body_str.replace("@ShapeMaterial@", "shader_material")
- else:
- body_str = body_str.replace("@Uniforms@", "")
- body_str = body_str.replace("@VertexShaderDefinition@", "")
- body_str = body_str.replace("@FragmentShaderDefinition@", "")
- body_str = body_str.replace("@ShaderMaterialDefinition@", "")
- body_str = body_str.replace("@ShapeMaterial@", "phong_material")
- body_str = body_str.replace("@IncrementTime@", "")
- return body_str
class ThreejsRenderer:
- def __init__(self, path=None):
- if not path:
- self._path = tempfile.mkdtemp()
- else:
- self._path = path
+ """
+ A renderer that uses three.js to display shapes in a web browser.
+ """
+
+ def __init__(self, path: Optional[str] = None) -> None:
+ """
+ Initializes the ThreejsRenderer.
+
+ Args:
+ path (str, optional): The path to the directory where the HTML
+ and JavaScript files will be created. If not specified, a
+ temporary directory will be created.
+ """
+ self._path = tempfile.mkdtemp() if not path else path
self._html_filename = os.path.join(self._path, "index.html")
- self._3js_shapes = {}
- self._3js_edges = {}
+ self._main_js_filename = os.path.join(self._path, "main.js")
+ self._3js_shapes: Dict[str, Any] = {}
+ self._3js_edges: Dict[str, Any] = {}
self.spinning_cursor = spinning_cursor()
- print("## threejs %s webgl renderer" % THREEJS_RELEASE)
+ print("## threejs renderer")
def DisplayShape(
self,
- shape,
- export_edges=False,
- color=(0.65, 0.65, 0.7),
- specular_color=(0.2, 0.2, 0.2),
- shininess=0.9,
- transparency=0.0,
- line_color=(0, 0.0, 0.0),
- line_width=1.0,
- mesh_quality=1.0,
- ):
+ shape: Any,
+ export_edges: bool = False,
+ color: Tuple[float, float, float] = (0.65, 0.65, 0.7),
+ specular_color: Tuple[float, float, float] = (0.2, 0.2, 0.2),
+ shininess: float = 0.9,
+ transparency: float = 0.0,
+ line_color: Tuple[float, float, float] = (0, 0.0, 0.0),
+ line_width: float = 1.0,
+ mesh_quality: float = 1.0,
+ ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
+ """
+ Displays a shape.
+
+ Args:
+ shape: The shape to display.
+ export_edges (bool, optional): Whether to export the edges of the shape.
+ color (tuple, optional): The color of the shape.
+ specular_color (tuple, optional): The specular color of the shape.
+ shininess (float, optional): The shininess of the shape.
+ transparency (float, optional): The transparency of the shape.
+ line_color (tuple, optional): The color of the lines.
+ line_width (float, optional): The width of the lines.
+ mesh_quality (float, optional): The quality of the mesh.
+
+ Returns:
+ A tuple containing the shapes and edges.
+ """
# if the shape is an edge or a wire, use the related functions
if is_edge(shape):
print("discretize an edge")
pnts = discretize_edge(shape)
- edge_hash = "edg%s" % uuid.uuid4().hex
+ edge_hash = f"edg{uuid.uuid4().hex}"
str_to_write = export_edgedata_to_json(edge_hash, pnts)
- edge_full_path = os.path.join(self._path, edge_hash + ".json")
+ edge_full_path = os.path.join(self._path, f"{edge_hash}.json")
with open(edge_full_path, "w") as edge_file:
edge_file.write(str_to_write)
# store this edge hash
@@ -465,16 +495,16 @@ def DisplayShape(
elif is_wire(shape):
print("discretize a wire")
pnts = discretize_wire(shape)
- wire_hash = "wir%s" % uuid.uuid4().hex
+ wire_hash = f"wir{uuid.uuid4().hex}"
str_to_write = export_edgedata_to_json(wire_hash, pnts)
- wire_full_path = os.path.join(self._path, wire_hash + ".json")
+ wire_full_path = os.path.join(self._path, f"{wire_hash}.json")
with open(wire_full_path, "w") as wire_file:
wire_file.write(str_to_write)
# store this edge hash
self._3js_edges[wire_hash] = [color, line_width]
return self._3js_shapes, self._3js_edges
shape_uuid = uuid.uuid4().hex
- shape_hash = "shp%s" % shape_uuid
+ shape_hash = f"shp{shape_uuid}"
# tesselatte
tess = ShapeTesselator(shape)
tess.Compute(
@@ -487,7 +517,7 @@ def DisplayShape(
)
sys.stdout.flush()
# export to 3JS
- shape_full_path = os.path.join(self._path, shape_hash + ".json")
+ shape_full_path = os.path.join(self._path, f"{shape_hash}.json")
# add this shape to the shape dict, sotres everything related to it
self._3js_shapes[shape_hash] = [
export_edges,
@@ -499,7 +529,6 @@ def DisplayShape(
line_width,
]
# generate the mesh
- # tess.ExportShapeToThreejs(shape_hash, shape_full_path)
# and also to JSON
with open(shape_full_path, "w") as json_file:
json_file.write(tess.ExportShapeToThreejsJSONString(shape_uuid))
@@ -511,31 +540,31 @@ def DisplayShape(
for i_edge in range(nbr_edges):
# after that, the file can be appended
str_to_write = ""
- edge_point_set = []
nbr_vertices = tess.ObjEdgeGetVertexCount(i_edge)
- for i_vert in range(nbr_vertices):
- edge_point_set.append(tess.GetEdgeVertex(i_edge, i_vert))
+ edge_point_set = [
+ tess.GetEdgeVertex(i_edge, i_vert) for i_vert in range(nbr_vertices)
+ ]
# write to file
- edge_hash = "edg%s" % uuid.uuid4().hex
+ edge_hash = f"edg{uuid.uuid4().hex}"
str_to_write += export_edgedata_to_json(edge_hash, edge_point_set)
# create the file
- edge_full_path = os.path.join(self._path, edge_hash + ".json")
+ edge_full_path = os.path.join(self._path, f"{edge_hash}.json")
with open(edge_full_path, "w") as edge_file:
edge_file.write(str_to_write)
# store this edge hash, with black color
self._3js_edges[edge_hash] = [(0, 0, 0), line_width]
return self._3js_shapes, self._3js_edges
- def generate_html_file(self):
- """Generate the HTML file to be rendered by the web browser"""
- global BODY_PART0
+ def generate_html_file(self) -> None:
+ """
+ Generates the HTML file to be rendered by the web browser.
+ """
+ global BODY_TEMPLATE
# loop over shapes to generate html shapes stuff
# the following line is a list that will help generating the string
# using "".join()
- shape_string_list = []
- shape_string_list.append("loader = new THREE.BufferGeometryLoader();\n")
- shape_idx = 0
- for shape_hash in self._3js_shapes:
+ shape_string_list = ["var loader = new THREE.BufferGeometryLoader();\n"]
+ for shape_idx, shape_hash in enumerate(self._3js_shapes):
# get properties for this shape
(
export_edges,
@@ -546,76 +575,95 @@ def generate_html_file(self):
line_color,
line_width,
) = self._3js_shapes[shape_hash]
- # creates a material for the shape
- shape_string_list.append(
- "\t\t\t%s_phong_material = new THREE.MeshPhongMaterial({" % shape_hash
+ shape_string_list.extend(
+ (
+ "\t\t\tvar %s_phong_material = new THREE.MeshPhongMaterial({"
+ % shape_hash,
+ f"color:{color_to_hex(color)},",
+ f"specular:{color_to_hex(specular_color)},",
+ "shininess:%g," % shininess,
+ "side: THREE.DoubleSide,",
+ "flatShading:false,",
+ )
)
- shape_string_list.append("color:%s," % color_to_hex(color))
- shape_string_list.append("specular:%s," % color_to_hex(specular_color))
- shape_string_list.append("shininess:%g," % shininess)
- # force double side rendering, see issue #645
- shape_string_list.append("side: THREE.DoubleSide,")
if transparency > 0.0:
shape_string_list.append(
"transparent: true, premultipliedAlpha: true, opacity:%g,"
% transparency
)
- # var line_material = new THREE.LineBasicMaterial({color: 0x000000, linewidth: 2});
- shape_string_list.append("});\n")
- # load json geometry files
- shape_string_list.append(
- "\t\t\tloader.load('%s.json', function(geometry) {\n" % shape_hash
- )
- shape_string_list.append(
- "\t\t\t\tmesh = new THREE.Mesh(geometry, %s_phong_material);\n"
- % shape_hash
+ shape_string_list.extend(
+ (
+ "});\n",
+ "\t\t\tloader.load('%s.json', function(geometry) {\n" % shape_hash,
+ "\t\t\t\tvar mesh = new THREE.Mesh(geometry, %s_phong_material);\n"
+ % shape_hash,
+ "\t\t\t\tmesh.castShadow = true;\n",
+ "\t\t\t\tmesh.receiveShadow = true;\n",
+ "\t\t\t\tscene.add(mesh);\n",
+ )
)
- # enable shadows for object
- shape_string_list.append("\t\t\t\tmesh.castShadow = true;\n")
- shape_string_list.append("\t\t\t\tmesh.receiveShadow = true;\n")
- # add mesh to scene
- shape_string_list.append("\t\t\t\tscene.add(mesh);\n")
# last shape, we request for a fit_to_scene
if shape_idx == len(self._3js_shapes) - 1:
shape_string_list.append("\tfit_to_scene();});\n")
else:
shape_string_list.append("\t\t\t});\n\n")
- shape_idx += 1
# Process edges
edge_string_list = []
for edge_hash in self._3js_edges:
color, line_width = self._3js_edges[edge_hash]
- edge_string_list.append(
- "\tloader.load('%s.json', function(geometry) {\n" % edge_hash
- )
- edge_string_list.append(
- "\tline_material = new THREE.LineBasicMaterial({color: %s, linewidth: %s});\n"
- % ((color_to_hex(color), line_width))
+ edge_string_list.extend(
+ (
+ "\tloader.load('%s.json', function(geometry) {\n" % edge_hash,
+ "\tvar line_material = new THREE.LineBasicMaterial({color: %s, linewidth: %s});\n"
+ % ((color_to_hex(color), line_width)),
+ "\tvar line = new THREE.Line(geometry, line_material);\n",
+ "\tscene.add(line);\n",
+ "\t});\n",
+ )
)
- edge_string_list.append(
- "\tline = new THREE.Line(geometry, line_material);\n"
+ # write the main.js file
+ with open(self._main_js_filename, "w") as fp:
+ main_js = MAIN_JS_TEMPLATE.substitute(
+ {
+ "ShapeList": "".join(shape_string_list),
+ "EdgeList": "".join(edge_string_list),
+ "Uniforms": "",
+ "ShaderMaterialDefinition": "",
+ }
)
- # add mesh to scene
- edge_string_list.append("\tscene.add(line);\n")
- edge_string_list.append("\t});\n")
- # write the string for the shape
+ fp.write(main_js)
+
+ # write the index.html file
with open(self._html_filename, "w") as fp:
fp.write("\n")
fp.write("")
# header
fp.write(HTMLHeader().get_str())
# body
- BODY_PART0 = BODY_PART0.replace("@VERSION@", OCC_VERSION)
- fp.write(BODY_PART0)
- fp.write(HTMLBody_Part1().get_str())
- fp.write("".join(shape_string_list))
- fp.write("".join(edge_string_list))
- # then write header part 2
- fp.write(BODY_PART2)
+ body = BODY_TEMPLATE.substitute(
+ {
+ "VERSION": VERSION,
+ "VertexShaderDefinition": "",
+ "FragmentShaderDefinition": "",
+ }
+ ) # = BODY_TEMPLATE_PART0.replace("@VERSION@", VERSION)
+ fp.write(body)
fp.write("\n")
- def render(self, addr="localhost", server_port=8080, open_webbrowser=False):
- """render the scene into the browser."""
+ def render(
+ self,
+ addr: str = "localhost",
+ server_port: int = 8080,
+ open_webbrowser: bool = False,
+ ) -> None:
+ """
+ Renders the scene in the browser.
+
+ Args:
+ addr (str, optional): The address to bind the server to.
+ server_port (int, optional): The port to use for the server.
+ open_webbrowser (bool, optional): Whether to open a web browser.
+ """
# generate HTML file
self.generate_html_file()
# then create a simple web server
@@ -625,10 +673,13 @@ def render(self, addr="localhost", server_port=8080, open_webbrowser=False):
if __name__ == "__main__":
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeTorus
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
- from OCC.Core.gp import gp_Trsf
+ from OCC.Core.gp import gp_Trsf, gp_Vec
+ from OCC.Core.TopoDS import TopoDS_Shape
import time
- def translate_shp(shp, vec, copy=False):
+ def translate_shp(
+ shp: TopoDS_Shape, vec: gp_Vec, copy: bool = False
+ ) -> TopoDS_Shape:
trns = gp_Trsf()
trns.SetTranslation(vec)
brep_trns = BRepBuilderAPI_Transform(shp, trns, copy)
diff --git a/src/Display/WebGl/threejs_renderer.pyi b/src/Display/WebGl/threejs_renderer.pyi
new file mode 100644
index 000000000..16b2d4ad4
--- /dev/null
+++ b/src/Display/WebGl/threejs_renderer.pyi
@@ -0,0 +1,33 @@
+from typing import Any, Dict, Generator, List, Optional, Tuple
+
+def spinning_cursor() -> Generator[str, None, None]: ...
+def color_to_hex(rgb_color: Tuple[float, float, float]) -> str: ...
+def export_edgedata_to_json(edge_hash: str, point_set: List[List[float]]) -> str: ...
+
+class HTMLHeader:
+ def __init__(
+ self, bg_gradient_color1: str = "#ced7de", bg_gradient_color2: str = "#808080"
+ ) -> None: ...
+ def get_str(self) -> str: ...
+
+class ThreejsRenderer:
+ def __init__(self, path: Optional[str] = None) -> None: ...
+ def DisplayShape(
+ self,
+ shape: Any,
+ export_edges: bool = False,
+ color: Tuple[float, float, float] = ...,
+ specular_color: Tuple[float, float, float] = ...,
+ shininess: float = 0.9,
+ transparency: float = 0.0,
+ line_color: Tuple[float, float, float] = ...,
+ line_width: float = 1.0,
+ mesh_quality: float = 1.0,
+ ) -> Tuple[Dict[str, Any], Dict[str, Any]]: ...
+ def generate_html_file(self) -> None: ...
+ def render(
+ self,
+ addr: str = "localhost",
+ server_port: int = 8080,
+ open_webbrowser: bool = False,
+ ) -> None: ...
diff --git a/src/Display/WebGl/x3dom_renderer.py b/src/Display/WebGl/x3dom_renderer.py
index c9c74d7f3..09d0e9199 100644
--- a/src/Display/WebGl/x3dom_renderer.py
+++ b/src/Display/WebGl/x3dom_renderer.py
@@ -17,43 +17,45 @@
import os
import sys
+from string import Template
import tempfile
import uuid
+from typing import Any, Dict, Generator, List, Optional, Tuple
from xml.etree import ElementTree
from OCC.Core.Tesselator import ShapeTesselator
-from OCC import VERSION as OCC_VERSION
+from OCC import VERSION
from OCC.Extend.TopologyUtils import is_edge, is_wire, discretize_edge, discretize_wire
from OCC.Display.WebGl.simple_server import start_server
-X3DOM_RELEASE = "1.8.2"
-def spinning_cursor():
+def spinning_cursor() -> Generator[str, None, None]:
+ """
+ A spinning cursor generator.
+ """
while True:
- for cursor in "|/-\\":
- yield cursor
+ yield from "|/-\\"
-X3DFILE_HEADER = """
+X3DFILE_HEADER_TEMPLATE = Template(
+ """
-
-
-
-
+
+
+
+
-""" % (
- OCC_VERSION,
- OCC_VERSION,
- OCC_VERSION,
+"""
)
-HEADER = """
+HEADER_TEMPLATE = Template(
+ """
- pythonOCC @VERSION@ x3dom renderer
+ pythonOCC $VERSION x3dom renderer
@@ -61,7 +63,7 @@ def spinning_cursor():
"""
+)
-BODY = """
+BODY_TEMPLATE = Template(
+ """
- @X3DSCENE@
+ $X3DSCENE
t view/hide shape
@@ -144,7 +148,7 @@ def spinning_cursor():
current_mat = mat;
console.log(mat);
selected_target_color = mat.diffuseColor;
- mat.diffuseColor = "1, 0.65, 0";
+ mat.diffuseColor = "1. 0.65 0.";
//console.log(the_shape.getElementsByTagName("Appearance"));//.getAttribute('diffuseColor'));
}
function onDocumentKeyPress(event) {
@@ -153,9 +157,11 @@ def spinning_cursor():
if (current_selected_shape) {
if (current_selected_shape.render == "true") {
current_selected_shape.render = "false";
+ console.log("hide ", current_selected_shape);
}
else {
current_selected_shape.render = "true";
+ console.log("show ", current_selected_shape)
}
}
}
@@ -166,38 +172,56 @@ def spinning_cursor():
"""
+)
+
+def export_edge_to_indexed_lineset(edge_point_set: List[List[float]]) -> str:
+ """
+ Exports an edge to an IndexedLineSet string.
-def export_edge_to_indexed_lineset(edge_point_set):
- str_x3d_to_return = "\t
" % len(edge_point_set)
+ Args:
+ edge_point_set (list): A list of points.
+
+ Returns:
+ str: The IndexedLineSet string.
+ """
+ str_x3d_to_return = f"\t"
str_x3d_to_return += "\n"
return str_x3d_to_return
-def indexed_lineset_to_x3d_string(str_linesets, header=True, footer=True, ils_id=0):
- """takes an str_lineset, coming for instance from export_curve_to_ils,
- and export to an X3D string"""
- if header:
- x3dfile_str = X3DFILE_HEADER
- else:
- x3dfile_str = ""
+def indexed_lineset_to_x3d_string(
+ str_linesets: List[str], header: bool = True, footer: bool = True, ils_id: int = 0
+) -> str:
+ """
+ Converts an IndexedLineSet string to an X3D string.
+
+ Args:
+ str_linesets (list): A list of IndexedLineSet strings.
+ header (bool, optional): Whether to include the X3D header.
+ footer (bool, optional): Whether to include the X3D footer.
+ ils_id (int, optional): The ID of the IndexedLineSet.
+
+ Returns:
+ str: The X3D string.
+ """
+ x3dfile_str = (
+ X3DFILE_HEADER_TEMPLATE.substitute({"VERSION": f"{VERSION}"}) if header else ""
+ )
x3dfile_str += "\n"
x3dfile_str += "\t\n"
- ils_id = 0
- for str_lineset in str_linesets:
- x3dfile_str += "\t\t\n" % ils_id
+ for ils_id, str_lineset in enumerate(str_linesets):
+ x3dfile_str += f"\t\t\n"
# empty appearance, but the x3d validator complains if nothing set
x3dfile_str += (
"\t\t\t\n\t\t"
)
x3dfile_str += str_lineset
x3dfile_str += "\t\t\n"
- ils_id += 1
-
x3dfile_str += "\t\n"
x3dfile_str += "\n"
if footer:
@@ -207,70 +231,95 @@ def indexed_lineset_to_x3d_string(str_linesets, header=True, footer=True, ils_id
class HTMLHeader:
- def __init__(self, bg_gradient_color1="#ced7de", bg_gradient_color2="#808080"):
+ """
+ A class to generate the HTML header.
+ """
+
+ def __init__(
+ self, bg_gradient_color1: str = "#ced7de", bg_gradient_color2: str = "#808080"
+ ) -> None:
+ """
+ Initializes the HTMLHeader.
+
+ Args:
+ bg_gradient_color1 (str, optional): The first color of the background gradient.
+ bg_gradient_color2 (str, optional): The second color of the background gradient.
+ """
self._bg_gradient_color1 = bg_gradient_color1
self._bg_gradient_color2 = bg_gradient_color2
- def get_str(self):
- header_str = HEADER.replace(
- "@bg_gradient_color1@", "%s" % self._bg_gradient_color1
- )
- header_str = header_str.replace(
- "@bg_gradient_color2@", "%s" % self._bg_gradient_color2
+ def get_str(self) -> str:
+ """
+ Returns the HTML header as a string.
+ """
+ return HEADER_TEMPLATE.substitute(
+ {
+ "bg_gradient_color1": f"{self._bg_gradient_color1}",
+ "bg_gradient_color2": f"{self._bg_gradient_color2}",
+ "VERSION": f"{VERSION}",
+ }
)
- header_str = header_str.replace("@VERSION@", OCC_VERSION)
- return header_str
class HTMLBody:
- def __init__(self, x3d_shapes, axes_plane, axes_plane_zoom_factor=1.0):
- """x3d_shapes is a list that contains uid for each shape"""
+ """
+ A class to generate the HTML body.
+ """
+
+ def __init__(
+ self,
+ x3d_shapes: List[str],
+ axes_plane: bool,
+ axes_plane_zoom_factor: float = 1.0,
+ ) -> None:
+ """
+ Initializes the HTMLBody.
+
+ Args:
+ x3d_shapes (list): A list of shape UIDs.
+ axes_plane (bool): Whether to display the axes plane.
+ axes_plane_zoom_factor (float, optional): The zoom factor for the axes plane.
+ """
self._x3d_shapes = x3d_shapes
self.spinning_cursor = spinning_cursor()
self._display_axes_plane = axes_plane
self._axis_plane_zoom_factor = axes_plane_zoom_factor
- def get_str(self):
+ def get_str(self) -> str:
+ """
+ Returns the HTML body as a string.
+ """
# get the location where pythonocc is running from
- body_str = BODY.replace("@VERSION@", OCC_VERSION)
- x3dcontent = '\n\t\n\t\t\n'
+ x3dcontent = "\n\t\n\t\t\n"
nb_shape = len(self._x3d_shapes)
- cur_shp = 1
if self._display_axes_plane:
- x3dcontent += """
-
-
+ x3dcontent += f"""
+
+
- """ % (
- self._axis_plane_zoom_factor,
- self._axis_plane_zoom_factor,
- self._axis_plane_zoom_factor,
- )
- # global rotateso that z is align properly
- x3dcontent += (
- ''
- )
- for shp_uid in self._x3d_shapes:
+ """
+ # global rotate so that z is properly aligned
+ x3dcontent += '\n'
+ for cur_shp, shp_uid in enumerate(self._x3d_shapes, start=1):
sys.stdout.write(
"\r%s meshing shapes... %i%%"
% (next(self.spinning_cursor), round(cur_shp / nb_shape * 100))
)
sys.stdout.flush()
-
- x3dcontent += (
- '\t\t\t\n'
- % shp_uid
- )
- cur_shp += 1
- x3dcontent += ""
- x3dcontent += "\t\t\n\t\n"
- body_str = body_str.replace("@X3DSCENE@", x3dcontent)
- body_str = body_str.replace("@X3DOMVERSION@", X3DOM_RELEASE)
- return body_str
+ # only the last downloaded shape raises a fitCamera event
+ x3dcontent += "\t\t\t\n'
+ x3dcontent += "\t\t\t\n\t\t\n\t\n"
+
+ return BODY_TEMPLATE.substitute(
+ {"VERSION": f"{VERSION}", "X3DSCENE": f"{x3dcontent}"}
+ )
class X3DExporter:
@@ -278,18 +327,34 @@ class X3DExporter:
def __init__(
self,
- shape, # the TopoDS shape to mesh
- vertex_shader, # the vertex_shader, passed as a string
- fragment_shader, # the fragment shader, passed as a string
- export_edges, # if yes, edges are exported to IndexedLineSet (might be SLOWW)
- color, # the default shape color
- specular_color, # shape specular color (white by default)
- shininess, # shape shininess
- transparency, # shape transparency
- line_color, # edge color
- line_width, # edge liewidth,
- mesh_quality, # mesh quality default is 1., good is <1, bad is >1
- ):
+ shape: Any,
+ vertex_shader: Optional[str],
+ fragment_shader: Optional[str],
+ export_edges: bool,
+ color: Tuple[float, float, float],
+ specular_color: Tuple[float, float, float],
+ shininess: float,
+ transparency: float,
+ line_color: Tuple[float, float, float],
+ line_width: float,
+ mesh_quality: float,
+ ) -> None:
+ """
+ Initializes the X3DExporter.
+
+ Args:
+ shape: The shape to export.
+ vertex_shader: The vertex shader to use.
+ fragment_shader: The fragment shader to use.
+ export_edges: Whether to export edges.
+ color: The color of the shape.
+ specular_color: The specular color of the shape.
+ shininess: The shininess of the shape.
+ transparency: The transparency of the shape.
+ line_color: The color of the lines.
+ line_width: The width of the lines.
+ mesh_quality: The quality of the mesh.
+ """
self._shape = shape
self._vs = vertex_shader
self._fs = fragment_shader
@@ -302,12 +367,19 @@ def __init__(
# the list of indexed face sets that compose the shape
# if ever the map_faces_to_mesh option is enabled, this list
# maybe composed of dozains of TriangleSet
- self._triangle_sets = []
- self._line_sets = []
+ self._triangle_sets: List[str] = []
+ self._line_sets: List[str] = []
self._x3d_string = "" # the string that contains the x3d description
- def compute(self):
+ def compute(self) -> None:
+ """
+ Computes the tessellation of the shape.
+ """
shape_tesselator = ShapeTesselator(self._shape)
+
+ if shape_tesselator.GetDeviation() <= 0:
+ raise ValueError("The deviation is <= 0.")
+
shape_tesselator.Compute(
compute_edges=self._export_edges,
mesh_quality=self._mesh_quality,
@@ -319,44 +391,38 @@ def compute(self):
# get number of edges
nbr_edges = shape_tesselator.ObjGetEdgeCount()
for i_edge in range(nbr_edges):
- edge_point_set = []
nbr_vertices = shape_tesselator.ObjEdgeGetVertexCount(i_edge)
- for i_vert in range(nbr_vertices):
- edge_point_set.append(
- shape_tesselator.GetEdgeVertex(i_edge, i_vert)
- )
+ edge_point_set = [
+ shape_tesselator.GetEdgeVertex(i_edge, i_vert)
+ for i_vert in range(nbr_vertices)
+ ]
ils = export_edge_to_indexed_lineset(edge_point_set)
self._line_sets.append(ils)
- def to_x3dfile_string(self, shape_id):
- x3dfile_str = X3DFILE_HEADER
+ def to_x3dfile_string(self, shape_id: int) -> str:
+ """
+ Converts the shape to an X3D string.
+
+ Args:
+ shape_id (int): The ID of the shape.
+
+ Returns:
+ str: The X3D string.
+ """
+ x3dfile_str = X3DFILE_HEADER_TEMPLATE.substitute({"VERSION": f"{VERSION}"})
for triangle_set in self._triangle_sets:
- x3dfile_str += (
- "\n"
+ x3dfile_str += ""
+ x3dfile_str += f"\n\n"
+ x3dfile_str += "\n"
#
# set Material or shader
#
if self._vs is None and self._fs is None:
- x3dfile_str += "\n" % self._transparency
- x3dfile_str += "\n"
+ x3dfile_str += f"\n"
else: # set shaders
x3dfile_str += (
'\n'
@@ -385,59 +451,99 @@ def to_x3dfile_string(self, shape_id):
# use ElementTree to ensure xml file quality
#
xml_et = ElementTree.fromstring(x3dfile_str)
- clean_x3d_str = ElementTree.tostring(xml_et, encoding="utf8").decode("utf8")
+ return ElementTree.tostring(xml_et, encoding="utf8").decode("utf8")
- return clean_x3d_str
+ def write_to_file(self, filename: str, shape_id: int) -> None:
+ """
+ Writes the X3D string to a file.
- def write_to_file(self, filename, shape_id):
+ Args:
+ filename (str): The name of the file to write to.
+ shape_id (int): The ID of the shape.
+ """
with open(filename, "w") as f:
f.write(self.to_x3dfile_string(shape_id))
class X3DomRenderer:
- def __init__(self, path=None, display_axes_plane=True, axes_plane_zoom_factor=1.0):
- if not path: # by default, write to a temp directory
- self._path = tempfile.mkdtemp()
- else:
- self._path = path
+ """
+ A renderer that uses x3dom to display shapes in a web browser.
+ """
+
+ def __init__(
+ self,
+ path: Optional[str] = None,
+ display_axes_plane: bool = True,
+ axes_plane_zoom_factor: float = 1.0,
+ ) -> None:
+ """
+ Initializes the X3DomRenderer.
+
+ Args:
+ path (str, optional): The path to the directory where the HTML
+ and JavaScript files will be created. If not specified, a
+ temporary directory will be created.
+ display_axes_plane (bool, optional): Whether to display the axes plane.
+ axes_plane_zoom_factor (float, optional): The zoom factor for the axes plane.
+ """
+ self._path = tempfile.mkdtemp() if not path else path
self._html_filename = os.path.join(self._path, "index.html")
- self._x3d_shapes = {}
- self._x3d_edges = {}
+ self._x3d_shapes: Dict[str, Any] = {}
+ self._x3d_edges: Dict[str, Any] = {}
self._axes_plane = (
display_axes_plane # display the small RVB axes and the plane
)
self._axes_plane_zoom_factor = axes_plane_zoom_factor
print(
- "## x3dom webgl renderer - render axes/planes : %r - axes/plane zoom factor : %g"
- % (self._axes_plane, self._axes_plane_zoom_factor)
+ f"## x3dom webgl renderer - render axes/planes : {self._axes_plane} - axes/plane zoom factor : {self._axes_plane_zoom_factor}"
)
def DisplayShape(
self,
- shape,
- vertex_shader=None,
- fragment_shader=None,
- export_edges=False,
- color=(0.65, 0.65, 0.7),
- specular_color=(0.2, 0.2, 0.2),
- shininess=0.9,
- transparency=0.0,
- line_color=(0, 0.0, 0.0),
- line_width=2.0,
- mesh_quality=1.0,
- ):
- """Adds a shape to the rendering buffer. This class computes the x3d file"""
+ shape: Any,
+ vertex_shader: Optional[str] = None,
+ fragment_shader: Optional[str] = None,
+ export_edges: bool = False,
+ color: Tuple[float, float, float] = (0.65, 0.65, 0.7),
+ specular_color: Tuple[float, float, float] = (0.2, 0.2, 0.2),
+ shininess: float = 0.9,
+ transparency: float = 0.0,
+ line_color: Tuple[float, float, float] = (0.0, 0.0, 0.0),
+ line_width: float = 2.0,
+ mesh_quality: float = 1.0,
+ ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
+ """
+ Adds a shape to the rendering buffer.
+
+ This class computes the x3d file.
+
+ Args:
+ shape: The shape to display.
+ vertex_shader (str, optional): The vertex shader to use.
+ fragment_shader (str, optional): The fragment shader to use.
+ export_edges (bool, optional): Whether to export the edges of the shape.
+ color (tuple, optional): The color of the shape.
+ specular_color (tuple, optional): The specular color of the shape.
+ shininess (float, optional): The shininess of the shape.
+ transparency (float, optional): The transparency of the shape.
+ line_color (tuple, optional): The color of the lines.
+ line_width (float, optional): The width of the lines.
+ mesh_quality (float, optional): The quality of the mesh.
+
+ Returns:
+ A tuple containing the shapes and edges.
+ """
# if the shape is an edge or a wire, use the related functions
if is_edge(shape):
print("X3D exporter, discretize an edge")
pnts = discretize_edge(shape)
- edge_hash = "edg%s" % uuid.uuid4().hex
+ edge_hash = f"edg{uuid.uuid4().hex}"
line_set = export_edge_to_indexed_lineset(pnts)
x3dfile_content = indexed_lineset_to_x3d_string(
[line_set], ils_id=edge_hash
)
- edge_full_path = os.path.join(self._path, edge_hash + ".x3d")
+ edge_full_path = os.path.join(self._path, f"{edge_hash}.x3d")
with open(edge_full_path, "w") as edge_file:
edge_file.write(x3dfile_content)
# store this edge hash
@@ -447,12 +553,12 @@ def DisplayShape(
if is_wire(shape):
print("X3D exporter, discretize a wire")
pnts = discretize_wire(shape)
- wire_hash = "wir%s" % uuid.uuid4().hex
+ wire_hash = f"wir{uuid.uuid4().hex}"
line_set = export_edge_to_indexed_lineset(pnts)
x3dfile_content = indexed_lineset_to_x3d_string(
[line_set], ils_id=wire_hash
)
- wire_full_path = os.path.join(self._path, wire_hash + ".x3d")
+ wire_full_path = os.path.join(self._path, f"{wire_hash}.x3d")
with open(wire_full_path, "w") as wire_file:
wire_file.write(x3dfile_content)
# store this edge hash
@@ -460,7 +566,7 @@ def DisplayShape(
return self._x3d_shapes, self._x3d_edges
shape_uuid = uuid.uuid4().hex
- shape_hash = "shp%s" % shape_uuid
+ shape_hash = f"shp{shape_uuid}"
x3d_exporter = X3DExporter(
shape,
vertex_shader,
@@ -475,7 +581,7 @@ def DisplayShape(
mesh_quality,
)
x3d_exporter.compute()
- x3d_filename = os.path.join(self._path, "%s.x3d" % shape_hash)
+ x3d_filename = os.path.join(self._path, f"{shape_hash}.x3d")
# the x3d filename is computed from the shape hash
shape_id = len(self._x3d_shapes)
x3d_exporter.write_to_file(x3d_filename, shape_id)
@@ -491,16 +597,29 @@ def DisplayShape(
]
return self._x3d_shapes, self._x3d_edges
- def render(self, addr="localhost", server_port=8080, open_webbrowser=False):
- """Call the render() method to display the X3D scene."""
+ def render(
+ self,
+ addr: str = "localhost",
+ server_port: int = 8080,
+ open_webbrowser: bool = False,
+ ) -> None:
+ """
+ Renders the scene in the browser.
+ """
# first generate the HTML root file
self.generate_html_file(self._axes_plane, self._axes_plane_zoom_factor)
# then create a simple web server
start_server(addr, server_port, self._path, open_webbrowser)
- def generate_html_file(self, axes_plane, axes_plane_zoom_factor):
- """Generate the HTML file to be rendered wy the web browser
- axes_plane: a boolean, tells whether or not display axes
+ def generate_html_file(
+ self, axes_plane: bool, axes_plane_zoom_factor: float
+ ) -> None:
+ """
+ Generates the HTML file to be rendered by the web browser.
+
+ Args:
+ axes_plane (bool): Whether to display the axes plane.
+ axes_plane_zoom_factor (float): The zoom factor for the axes plane.
"""
with open(self._html_filename, "w") as html_file:
html_file.write("\n")
diff --git a/src/Display/WebGl/x3dom_renderer.pyi b/src/Display/WebGl/x3dom_renderer.pyi
new file mode 100644
index 000000000..683099822
--- /dev/null
+++ b/src/Display/WebGl/x3dom_renderer.pyi
@@ -0,0 +1,75 @@
+from typing import Any, Dict, Generator, List, Optional, Tuple
+
+def spinning_cursor() -> Generator[str, None, None]: ...
+def export_edge_to_indexed_lineset(edge_point_set: List[List[float]]) -> str: ...
+def indexed_lineset_to_x3d_string(
+ str_linesets: List[str],
+ header: bool = True,
+ footer: bool = True,
+ ils_id: int = 0,
+) -> str: ...
+
+class HTMLHeader:
+ def __init__(
+ self, bg_gradient_color1: str = "#ced7de", bg_gradient_color2: str = "#808080"
+ ) -> None: ...
+ def get_str(self) -> str: ...
+
+class HTMLBody:
+ def __init__(
+ self,
+ x3d_shapes: List[str],
+ axes_plane: bool,
+ axes_plane_zoom_factor: float = 1.0,
+ ) -> None: ...
+ def get_str(self) -> str: ...
+
+class X3DExporter:
+ def __init__(
+ self,
+ shape: Any,
+ vertex_shader: Optional[str],
+ fragment_shader: Optional[str],
+ export_edges: bool,
+ color: Tuple[float, float, float],
+ specular_color: Tuple[float, float, float],
+ shininess: float,
+ transparency: float,
+ line_color: Tuple[float, float, float],
+ line_width: float,
+ mesh_quality: float,
+ ) -> None: ...
+ def compute(self) -> None: ...
+ def to_x3dfile_string(self, shape_id: int) -> str: ...
+ def write_to_file(self, filename: str, shape_id: int) -> None: ...
+
+class X3DomRenderer:
+ def __init__(
+ self,
+ path: Optional[str] = None,
+ display_axes_plane: bool = True,
+ axes_plane_zoom_factor: float = 1.0,
+ ) -> None: ...
+ def DisplayShape(
+ self,
+ shape: Any,
+ vertex_shader: Optional[str] = None,
+ fragment_shader: Optional[str] = None,
+ export_edges: bool = False,
+ color: Tuple[float, float, float] = (0.65, 0.65, 0.7),
+ specular_color: Tuple[float, float, float] = (0.2, 0.2, 0.2),
+ shininess: float = 0.9,
+ transparency: float = 0.0,
+ line_color: Tuple[float, float, float] = (0.0, 0.0, 0.0),
+ line_width: float = 2.0,
+ mesh_quality: float = 1.0,
+ ) -> Tuple[Dict[str, Any], Dict[str, Any]]: ...
+ def render(
+ self,
+ addr: str = "localhost",
+ server_port: int = 8080,
+ open_webbrowser: bool = False,
+ ) -> None: ...
+ def generate_html_file(
+ self, axes_plane: bool, axes_plane_zoom_factor: float
+ ) -> None: ...
diff --git a/src/Display/backend.py b/src/Display/backend.py
index 96f12a87d..30b128d50 100644
--- a/src/Display/backend.py
+++ b/src/Display/backend.py
@@ -16,14 +16,26 @@
##along with pythonOCC. If not, see .
import logging
+import os
+import sys
+from typing import Any, Optional, Tuple
# backend constants
WX = "wx"
-PYSIDE2 = "qt-pyside2"
-PYQT5 = "qt-pyqt5"
+PYQT5 = "pyqt5"
+PYSIDE2 = "pyside2"
+PYQT6 = "pyqt6"
+PYSIDE6 = "pyside6"
+TK = "tk"
# backend module
-HAVE_PYQT5, HAVE_PYSIDE2, HAVE_WX = False, False, False
+HAVE_PYQT5, HAVE_PYSIDE2, HAVE_PYQT6, HAVE_PYSIDE6, HAVE_WX = (
+ False,
+ False,
+ False,
+ False,
+ False,
+)
# is any backend imported?
HAVE_BACKEND = False
@@ -33,8 +45,25 @@
log.setLevel(logging.DEBUG)
-def load_pyqt5():
- """Return True is PyQt5 found, else False"""
+def qt6_force_xcb_on_linux() -> None:
+ """
+ Force QT_QPA_PLATFORM to 'xcb' on Linux for Qt6.
+
+ Wayland implementation prevents winId to provide with the correct x11
+ windows id.
+ """
+ if sys.platform == "linux" and "XDG_SESSION_TYPE" in os.environ:
+ if os.environ["XDG_SESSION_TYPE"] == "wayland":
+ os.environ["QT_QPA_PLATFORM"] = "xcb"
+
+
+def load_pyqt5() -> bool:
+ """
+ Loads the PyQt5 backend.
+
+ Returns:
+ bool: True if PyQt5 is found, False otherwise.
+ """
global HAVE_PYQT5, QtCore, QtGui, QtWidgets, QtOpenGL
# backend already loaded, dont load another one
@@ -49,8 +78,13 @@ def load_pyqt5():
return HAVE_PYQT5
-def load_pyside2():
- """Return True is PySide2 found, else False"""
+def load_pyside2() -> bool:
+ """
+ Loads the PySide2 backend.
+
+ Returns:
+ bool: True if PySide2 is found, False otherwise.
+ """
global HAVE_PYSIDE2, QtCore, QtGui, QtWidgets, QtOpenGL
# backend already loaded, dont load another one
@@ -65,8 +99,57 @@ def load_pyside2():
return HAVE_PYSIDE2
-def load_wx():
- """Return True is wxPython found, else False"""
+def load_pyqt6() -> bool:
+ """
+ Loads the PyQt6 backend.
+
+ Returns:
+ bool: True if PyQt6 is found, False otherwise.
+ """
+ global HAVE_PYQT6, QtCore, QtGui, QtWidgets, QtOpenGL
+
+ # backend already loaded, dont load another one
+ if loaded_backend():
+ return False
+ try:
+ qt6_force_xcb_on_linux()
+ from PyQt6 import QtCore, QtGui, QtOpenGL, QtWidgets
+
+ HAVE_PYQT6 = True
+ except ImportError:
+ HAVE_PYQT6 = False
+ return HAVE_PYQT6
+
+
+def load_pyside6() -> bool:
+ """
+ Loads the PySide6 backend.
+
+ Returns:
+ bool: True if PySide6 is found, False otherwise.
+ """
+ global HAVE_PYSIDE6, QtCore, QtGui, QtWidgets, QtOpenGL
+
+ # backend already loaded, dont load another one
+ if loaded_backend():
+ return False
+ try:
+ qt6_force_xcb_on_linux()
+ from PySide6 import QtCore, QtGui, QtOpenGL, QtWidgets
+
+ HAVE_PYSIDE6 = True
+ except ImportError:
+ HAVE_PYSIDE6 = False
+ return HAVE_PYSIDE6
+
+
+def load_wx() -> bool:
+ """
+ Loads the wxPython backend.
+
+ Returns:
+ bool: True if wxPython is found, False otherwise.
+ """
# backend already loaded, dont load another one
if loaded_backend():
@@ -81,35 +164,49 @@ def load_wx():
return HAVE_WX
-def loaded_backend():
+def loaded_backend() -> bool:
+ """
+ Returns True if a backend is loaded, False otherwise.
+ """
return HAVE_BACKEND
-def get_loaded_backend():
+def get_loaded_backend() -> str:
+ """
+ Returns the name of the loaded backend.
+ """
return BACKEND_MODULE
-def load_any_qt_backend():
- """Load any qt based backend. First try to load
- PyQt5, then PySide2. Raise an exception if none of them are available
+def load_any_qt_backend() -> bool:
+ """
+ Loads any Qt-based backend.
+
+ It first tries to load PyQt5, then PyQt6.
+
+ Returns:
+ bool: True if a Qt backend was loaded, False otherwise.
+
+ Raises:
+ AssertionError: If no Qt backend can be loaded.
"""
pyqt5_loaded = False
# by default, load PyQt5
pyqt5_loaded = load_backend(PYQT5)
if not pyqt5_loaded:
- pyside2_loaded = load_backend(PYSIDE2)
- if not (pyqt5_loaded or pyside2_loaded):
- raise AssertionError("None of the PyQt5 or PySide2 can be loaded")
+ pyqt6_loaded = load_backend(PYQT6)
+ if not (pyqt5_loaded or pyqt6_loaded):
+ raise AssertionError("None of the PyQt5 or PyQt6 can be loaded")
return True
-def load_backend(backend_str=None):
+def load_backend(backend_str: Optional[str] = None) -> str:
"""Load a GUI backend
If no Qt backend is found (PyQt5 or PySide), wx is loaded
The search order for pythonocc compatible gui modules is:
- PyQt5, PySide2, wx
+ PyQt5, PySide2, PyQt6, PySide6, wx
Note
----
@@ -121,7 +218,7 @@ def load_backend(backend_str=None):
specifies which backend to load
- backend_str is one of ( "qt-pyqt5", "qt-pyside2", "wx" )
+ backend_str is one of ( "pyqt5", "pyqt6", "pyside2", "pyside6", "wx" )
if no value has been set, load the first module in gui module search
order
@@ -130,7 +227,7 @@ def load_backend(backend_str=None):
-------
str
the name of the loaded backend
- one of ( "qt-pyqt5", "qt-pyside2", "wx" )
+ one of ( "pyqt5", "pyqt6", "pyside2", "pyside6", "wx" )
Raises
------
@@ -154,8 +251,8 @@ def load_backend(backend_str=None):
return BACKEND_MODULE
if backend_str is not None:
- compatible_backends = (PYQT5, PYSIDE2, WX)
- if not backend_str in compatible_backends:
+ compatible_backends = (PYQT5, PYQT6, PYSIDE2, PYSIDE6, WX, TK)
+ if backend_str not in compatible_backends:
msg = (
f"incompatible backend_str specified: {backend_str}\n"
f"backend is one of : {compatible_backends}"
@@ -166,18 +263,18 @@ def load_backend(backend_str=None):
if backend_str == PYQT5 or backend_str is None:
if load_pyqt5():
HAVE_BACKEND = True
- BACKEND_MODULE = "qt-pyqt5"
+ BACKEND_MODULE = "pyqt5"
log.info("backend loaded: %s", BACKEND_MODULE)
return BACKEND_MODULE
- if backend_str == PYQT5 and not HAVE_BACKEND:
- msg = f"{backend_str} backend could not be loaded"
- log.exception(msg)
- raise ValueError(msg)
+ if backend_str == PYQT5 and not HAVE_BACKEND:
+ msg = f"{backend_str} backend could not be loaded"
+ log.exception(msg)
+ raise ValueError(msg)
if backend_str == PYSIDE2 or (backend_str is None and not HAVE_BACKEND):
if load_pyside2():
HAVE_BACKEND = True
- BACKEND_MODULE = "qt-pyside2"
+ BACKEND_MODULE = "pyside2"
log.info("backend loaded: %s", BACKEND_MODULE)
return BACKEND_MODULE
elif backend_str == PYSIDE2 and not HAVE_BACKEND:
@@ -185,6 +282,28 @@ def load_backend(backend_str=None):
log.exception(msg)
raise ValueError(msg)
+ if backend_str == PYQT6 or backend_str is None:
+ if load_pyqt6():
+ HAVE_BACKEND = True
+ BACKEND_MODULE = "pyqt6"
+ log.info("backend loaded: %s", BACKEND_MODULE)
+ return BACKEND_MODULE
+ if backend_str == PYQT6 and not HAVE_BACKEND:
+ msg = f"{backend_str} backend could not be loaded"
+ log.exception(msg)
+ raise ValueError(msg)
+
+ if backend_str == PYSIDE6 or backend_str is None:
+ if load_pyside6():
+ HAVE_BACKEND = True
+ BACKEND_MODULE = "pyside6"
+ log.info("backend loaded: %s", BACKEND_MODULE)
+ return BACKEND_MODULE
+ if backend_str == PYSIDE6 and not HAVE_BACKEND:
+ msg = f"{backend_str} backend could not be loaded"
+ log.exception(msg)
+ raise ValueError(msg)
+
if backend_str == WX or (backend_str is None and not HAVE_BACKEND):
if load_wx():
HAVE_BACKEND = True
@@ -196,14 +315,11 @@ def load_backend(backend_str=None):
log.exception("%s backend could not be loaded", backend_str)
raise ValueError(msg)
- if not HAVE_BACKEND:
- raise ImportError(
- "No compliant GUI library could be imported.\n"
- "Either PyQt5, PPySide2 or wxPython is required"
- )
+ # finally, return a tk backend, available on all machines
+ return "tk"
-def get_qt_modules():
+def get_qt_modules() -> Tuple[Any, Any, Any, Any]:
"""
Returns
@@ -218,21 +334,20 @@ def get_qt_modules():
ValueError
when no Qt backend has been yet loaded
informs the user to call `load_backend` or that no Qt python module
- ( PyQt5, PySide ) is found
+ (PyQt5, PySide) is found
"""
if not HAVE_BACKEND:
raise ValueError(
"no backend has been imported yet with " "``load_backend``... "
)
-
- if HAVE_PYQT5 or HAVE_PYSIDE2:
+ if HAVE_PYQT5 or HAVE_PYQT6 or HAVE_PYSIDE2 or HAVE_PYSIDE6:
return QtCore, QtGui, QtWidgets, QtOpenGL
if HAVE_WX:
- raise ValueError("the Wx backend is already loaded")
+ raise ValueError("the wx backend is already loaded")
msg = (
"no Qt backend is loaded, hence cannot return any modules\n"
- "either you haven't got PyQt5 or PySide2 installed\n"
+ "either you haven't got PyQt5, PyQt6, PySide2 or PySide6 installed\n"
"or you haven't yet loaded a backend with the "
"`OCC.Display.backend.load_backend` function"
)
diff --git a/src/Display/backend.pyi b/src/Display/backend.pyi
new file mode 100644
index 000000000..a636a8303
--- /dev/null
+++ b/src/Display/backend.pyi
@@ -0,0 +1,13 @@
+from typing import Any, Optional, Tuple
+
+def qt6_force_xcb_on_linux() -> None: ...
+def load_pyqt5() -> bool: ...
+def load_pyside2() -> bool: ...
+def load_pyqt6() -> bool: ...
+def load_pyside6() -> bool: ...
+def load_wx() -> bool: ...
+def loaded_backend() -> bool: ...
+def get_loaded_backend() -> str: ...
+def load_any_qt_backend() -> bool: ...
+def load_backend(backend_str: Optional[str] = None) -> str: ...
+def get_qt_modules() -> Tuple[Any, Any, Any, Any]: ...
diff --git a/src/Display/qtDisplay.py b/src/Display/qtDisplay.py
index cdb9334a8..f91ed4fef 100644
--- a/src/Display/qtDisplay.py
+++ b/src/Display/qtDisplay.py
@@ -19,21 +19,31 @@
import logging
import os
-import sys
+from typing import Any, Callable, Dict, List, Optional
+from OCC.Core.AIS import AIS_Manipulator
+from OCC.Core.gp import gp_Trsf
from OCC.Display import OCCViewer
from OCC.Display.backend import get_qt_modules
QtCore, QtGui, QtWidgets, QtOpenGL = get_qt_modules()
-logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
log = logging.getLogger(__name__)
+log.setLevel(logging.DEBUG)
class qtBaseViewer(QtWidgets.QWidget):
- """The base Qt Widget for an OCC viewer"""
+ """
+ The base Qt Widget for an OCC viewer.
+ """
- def __init__(self, parent=None):
+ def __init__(self, parent: Optional[Any] = None) -> None:
+ """
+ Initializes the qtBaseViewer.
+
+ Args:
+ parent (QWidget, optional): The parent widget.
+ """
super(qtBaseViewer, self).__init__(parent)
self._display = OCCViewer.Viewer3d()
self._inited = False
@@ -42,35 +52,46 @@ def __init__(self, parent=None):
self.setMouseTracking(True)
# Strong focus
- self.setFocusPolicy(QtCore.Qt.WheelFocus)
+ self.setFocusPolicy(QtCore.Qt.FocusPolicy.WheelFocus)
- self.setAttribute(QtCore.Qt.WA_NativeWindow)
- self.setAttribute(QtCore.Qt.WA_PaintOnScreen)
- self.setAttribute(QtCore.Qt.WA_NoSystemBackground)
+ self.setAttribute(QtCore.Qt.WidgetAttribute.WA_NativeWindow)
+ self.setAttribute(QtCore.Qt.WidgetAttribute.WA_PaintOnScreen)
+ self.setAttribute(QtCore.Qt.WidgetAttribute.WA_NoSystemBackground)
self.setAutoFillBackground(False)
- def resizeEvent(self, event):
+ def resizeEvent(self, event: Any) -> None:
+ """
+ Called when the widget is resized.
+ """
super(qtBaseViewer, self).resizeEvent(event)
self._display.View.MustBeResized()
- def paintEngine(self):
+ def paintEngine(self) -> None:
+ """
+ Returns the paint engine.
+ """
return None
class qtViewer3d(qtBaseViewer):
+ """
+ A Qt Widget for an OCC viewer.
+ """
# emit signal when selection is changed
# is a list of TopoDS_*
- HAVE_PYQT_SIGNAL = False
- if hasattr(QtCore, "pyqtSignal"): # PyQt
+ if hasattr(QtCore, "pyqtSignal"): # PyQt5
sig_topods_selected = QtCore.pyqtSignal(list)
- HAVE_PYQT_SIGNAL = True
elif hasattr(QtCore, "Signal"): # PySide2
sig_topods_selected = QtCore.Signal(list)
- HAVE_PYQT_SIGNAL = True
+ else:
+ raise IOError("no signal")
- def __init__(self, *kargs):
+ def __init__(self, *kargs: Any) -> None:
+ """
+ Initializes the qtViewer3d.
+ """
qtBaseViewer.__init__(self, *kargs)
self.setObjectName("qt_viewer_3d")
@@ -85,20 +106,25 @@ def __init__(self, *kargs):
self._selection = None
self._drawtext = True
self._qApp = QtWidgets.QApplication.instance()
- self._key_map = {}
+ self._key_map: Dict[int, Callable] = {}
self._current_cursor = "arrow"
- self._available_cursors = {}
+ self._available_cursors: Dict[str, QtGui.QCursor] = {}
@property
- def qApp(self):
- # reference to QApplication instance
+ def qApp(self) -> Any:
+ """
+ A reference to the QApplication instance.
+ """
return self._qApp
@qApp.setter
- def qApp(self, value):
+ def qApp(self, value: Any) -> None:
self._qApp = value
- def InitDriver(self):
+ def InitDriver(self) -> None:
+ """
+ Initializes the driver.
+ """
self._display.Create(window_handle=int(self.winId()), parent=self)
# background gradient
self._display.SetModeShaded()
@@ -115,7 +141,10 @@ def InitDriver(self):
}
self.createCursors()
- def createCursors(self):
+ def createCursors(self) -> None:
+ """
+ Creates the cursors.
+ """
module_pth = os.path.abspath(os.path.dirname(__file__))
icon_pth = os.path.join(module_pth, "icons")
@@ -127,7 +156,7 @@ def createCursors(self):
)
self._available_cursors = {
- "arrow": QtGui.QCursor(QtCore.Qt.ArrowCursor), # default
+ "arrow": QtGui.QCursor(QtCore.Qt.CursorShape.ArrowCursor), # default
"pan": QtGui.QCursor(_CURSOR_PIX_PAN),
"rotate": QtGui.QCursor(_CURSOR_PIX_ROT),
"zoom": QtGui.QCursor(_CURSOR_PIX_ZOOM),
@@ -136,7 +165,10 @@ def createCursors(self):
self._current_cursor = "arrow"
- def keyPressEvent(self, event):
+ def keyPressEvent(self, event: Any) -> None:
+ """
+ Called when a key is pressed.
+ """
super(qtViewer3d, self).keyPressEvent(event)
code = event.key()
if code in self._key_map:
@@ -148,15 +180,24 @@ def keyPressEvent(self, event):
else:
log.info("key: code %i not mapped to any function" % code)
- def focusInEvent(self, event):
+ def focusInEvent(self, event: Any) -> None:
+ """
+ Called when the widget gains focus.
+ """
if self._inited:
self._display.Repaint()
- def focusOutEvent(self, event):
+ def focusOutEvent(self, event: Any) -> None:
+ """
+ Called when the widget loses focus.
+ """
if self._inited:
self._display.Repaint()
- def paintEvent(self, event):
+ def paintEvent(self, event: Any) -> None:
+ """
+ Called when the widget is painted.
+ """
if not self._inited:
self.InitDriver()
@@ -168,59 +209,62 @@ def paintEvent(self, event):
rect = QtCore.QRect(*self._drawbox)
painter.drawRect(rect)
- def wheelEvent(self, event):
+ def wheelEvent(self, event: Any) -> None:
+ """
+ Called when the mouse wheel is scrolled.
+ """
delta = event.angleDelta().y()
- if delta > 0:
- zoom_factor = 2.0
- else:
- zoom_factor = 0.5
+ zoom_factor = 2.0 if delta > 0 else 0.5
self._display.ZoomFactor(zoom_factor)
@property
- def cursor(self):
+ def cursor(self) -> str:
+ """
+ The current cursor.
+ """
return self._current_cursor
@cursor.setter
- def cursor(self, value):
- if not self._current_cursor == value:
-
+ def cursor(self, value: str) -> None:
+ if self._current_cursor != value:
self._current_cursor = value
- cursor = self._available_cursors.get(value)
-
- if cursor:
+ if cursor := self._available_cursors.get(value):
self.qApp.setOverrideCursor(cursor)
else:
self.qApp.restoreOverrideCursor()
- def mousePressEvent(self, event):
+ def mousePressEvent(self, event: Any) -> None:
+ """
+ Called when a mouse button is pressed.
+ """
self.setFocus()
ev = event.pos()
self.dragStartPosX = ev.x()
self.dragStartPosY = ev.y()
self._display.StartRotation(self.dragStartPosX, self.dragStartPosY)
- def mouseReleaseEvent(self, event):
+ def mouseReleaseEvent(self, event: Any) -> None:
+ """
+ Called when a mouse button is released.
+ """
pt = event.pos()
modifiers = event.modifiers()
- if event.button() == QtCore.Qt.LeftButton:
+ if event.button() == QtCore.Qt.MouseButton.LeftButton:
if self._select_area:
[Xmin, Ymin, dx, dy] = self._drawbox
self._display.SelectArea(Xmin, Ymin, Xmin + dx, Ymin + dy)
self._select_area = False
+ elif modifiers == QtCore.Qt.Modifier.SHIFT:
+ self._display.ShiftSelect(pt.x(), pt.y())
else:
- # multiple select if shift is pressed
- if modifiers == QtCore.Qt.ShiftModifier:
- self._display.ShiftSelect(pt.x(), pt.y())
- else:
- # single select otherwise
- self._display.Select(pt.x(), pt.y())
-
- if (self._display.selected_shapes is not None) and self.HAVE_PYQT_SIGNAL:
+ # single select otherwise
+ self._display.Select(pt.x(), pt.y())
- self.sig_topods_selected.emit(self._display.selected_shapes)
+ if self._display.selected_shapes is not None:
+ self.sig_topods_selected.emit(self._display.selected_shapes)
- elif event.button() == QtCore.Qt.RightButton:
+ elif event.button() == QtCore.Qt.MouseButton.RightButton:
if self._zoom_area:
[Xmin, Ymin, dx, dy] = self._drawbox
self._display.ZoomArea(Xmin, Ymin, Xmin + dx, Ymin + dy)
@@ -228,7 +272,10 @@ def mouseReleaseEvent(self, event):
self.cursor = "arrow"
- def DrawBox(self, event):
+ def DrawBox(self, event: Any) -> None:
+ """
+ Draws a selection box.
+ """
tolerance = 2
pt = event.pos()
dx = pt.x() - self.dragStartPosX
@@ -237,19 +284,25 @@ def DrawBox(self, event):
return
self._drawbox = [self.dragStartPosX, self.dragStartPosY, dx, dy]
- def mouseMoveEvent(self, evt):
+ def mouseMoveEvent(self, evt: Any) -> None:
+ """
+ Called when the mouse is moved.
+ """
pt = evt.pos()
- buttons = int(evt.buttons())
+ # buttons = int(evt.buttons())
+ buttons = evt.buttons()
modifiers = evt.modifiers()
# ROTATE
- if buttons == QtCore.Qt.LeftButton and not modifiers == QtCore.Qt.ShiftModifier:
+ if (
+ buttons == QtCore.Qt.MouseButton.LeftButton
+ and modifiers != QtCore.Qt.Modifier.SHIFT
+ ):
self.cursor = "rotate"
self._display.Rotation(pt.x(), pt.y())
self._drawbox = False
- # DYNAMIC ZOOM
elif (
- buttons == QtCore.Qt.RightButton
- and not modifiers == QtCore.Qt.ShiftModifier
+ buttons == QtCore.Qt.MouseButton.RightButton
+ and modifiers != QtCore.Qt.Modifier.SHIFT
):
self.cursor = "zoom"
self._display.Repaint()
@@ -262,8 +315,7 @@ def mouseMoveEvent(self, evt):
self.dragStartPosX = pt.x()
self.dragStartPosY = pt.y()
self._drawbox = False
- # PAN
- elif buttons == QtCore.Qt.MidButton:
+ elif buttons == QtCore.Qt.MouseButton.MiddleButton:
dx = pt.x() - self.dragStartPosX
dy = pt.y() - self.dragStartPosY
self.dragStartPosX = pt.x()
@@ -271,15 +323,12 @@ def mouseMoveEvent(self, evt):
self.cursor = "pan"
self._display.Pan(dx, -dy)
self._drawbox = False
- # DRAW BOX
- # ZOOM WINDOW
- elif buttons == QtCore.Qt.RightButton and modifiers == QtCore.Qt.ShiftModifier:
+ elif buttons == QtCore.Qt.MouseButton.RightButton:
self._zoom_area = True
self.cursor = "zoom-area"
self.DrawBox(evt)
self.update()
- # SELECT AREA
- elif buttons == QtCore.Qt.LeftButton and modifiers == QtCore.Qt.ShiftModifier:
+ elif buttons == QtCore.Qt.MouseButton.LeftButton:
self._select_area = True
self.DrawBox(evt)
self.update()
@@ -287,3 +336,168 @@ def mouseMoveEvent(self, evt):
self._drawbox = False
self._display.MoveTo(pt.x(), pt.y())
self.cursor = "arrow"
+
+
+class qtViewer3dWithManipulator(qtViewer3d):
+ """
+ A Qt Widget for an OCC viewer with a manipulator.
+ """
+
+ # emit signal when selection is changed
+ # is a list of TopoDS_*
+ if hasattr(QtCore, "pyqtSignal"): # PyQt5
+ sig_topods_selected = QtCore.pyqtSignal(list)
+ elif hasattr(QtCore, "Signal"):
+ sig_topods_selected = QtCore.Signal(list)
+
+ def __init__(self, *kargs: Any) -> None:
+ """
+ Initializes the qtViewer3dWithManipulator.
+ """
+ qtBaseViewer.__init__(self, *kargs)
+
+ self.setObjectName("qt_viewer_3d")
+
+ self._drawbox = False
+ self._zoom_area = False
+ self._select_area = False
+ self._inited = False
+ self._leftisdown = False
+ self._middleisdown = False
+ self._rightisdown = False
+ self._selection = None
+ self._drawtext = True
+ self._qApp = QtWidgets.QApplication.instance()
+ self._key_map: Dict[int, Callable] = {}
+ self._current_cursor = "arrow"
+ self._available_cursors: Dict[str, QtGui.QCursor] = {}
+
+ # create empty manipulator
+ self.manipulator = AIS_Manipulator()
+ self.trsf_manip: List[gp_Trsf] = []
+ self.manip_moved = False
+
+ def set_manipulator(self, manipulator: AIS_Manipulator) -> None:
+ """
+ Sets the manipulator to use.
+
+ Args:
+ manipulator: The manipulator to use.
+ """
+ self.trsf_manip = []
+ self.manipulator = manipulator
+ self.manip_moved = False
+
+ def mousePressEvent(self, event: Any) -> None:
+ """
+ Called when a mouse button is pressed.
+ """
+ self.setFocus()
+ ev = event.pos()
+ self.dragStartPosX = ev.x()
+ self.dragStartPosY = ev.y()
+ if self.manipulator.HasActiveMode():
+ self.manipulator.StartTransform(
+ self.dragStartPosX, self.dragStartPosY, self._display.GetView()
+ )
+ else:
+ self._display.StartRotation(self.dragStartPosX, self.dragStartPosY)
+
+ def mouseMoveEvent(self, evt: Any) -> None:
+ """
+ Called when the mouse is moved.
+ """
+ pt = evt.pos()
+ buttons = int(evt.buttons())
+ modifiers = evt.modifiers()
+ # TRANSFORM via MANIPULATOR or ROTATE
+ if (
+ buttons == QtCore.Qt.MouseButton.LeftButton
+ and modifiers != QtCore.Qt.Modifier.SHIFT
+ ):
+ if self.manipulator.HasActiveMode():
+ self.trsf = self.manipulator.Transform(
+ pt.x(), pt.y(), self._display.GetView()
+ )
+ self.manip_moved = True
+ self._display.View.Redraw()
+ else:
+ self.cursor = "rotate"
+ self._display.Rotation(pt.x(), pt.y())
+ self._drawbox = False
+ elif (
+ buttons == QtCore.Qt.MouseButton.RightButton
+ and modifiers != QtCore.Qt.Modifier.SHIFT
+ ):
+ self.cursor = "zoom"
+ self._display.Repaint()
+ self._display.DynamicZoom(
+ abs(self.dragStartPosX),
+ abs(self.dragStartPosY),
+ abs(pt.x()),
+ abs(pt.y()),
+ )
+ self.dragStartPosX = pt.x()
+ self.dragStartPosY = pt.y()
+ self._drawbox = False
+ elif buttons == QtCore.Qt.MouseButton.MidButton:
+ dx = pt.x() - self.dragStartPosX
+ dy = pt.y() - self.dragStartPosY
+ self.dragStartPosX = pt.x()
+ self.dragStartPosY = pt.y()
+ self.cursor = "pan"
+ self._display.Pan(dx, -dy)
+ self._drawbox = False
+ elif buttons == QtCore.Qt.MouseButton.RightButton:
+ self._zoom_area = True
+ self.cursor = "zoom-area"
+ self.DrawBox(evt)
+ self.update()
+ elif buttons == QtCore.Qt.MouseButton.LeftButton:
+ self._select_area = True
+ self.DrawBox(evt)
+ self.update()
+ else:
+ self._drawbox = False
+ self._display.MoveTo(pt.x(), pt.y())
+ self.cursor = "arrow"
+
+ def get_trsf_from_manip(self) -> gp_Trsf:
+ """
+ Returns the transformation from the manipulator.
+ """
+ trsf = gp_Trsf()
+ for t in self.trsf_manip:
+ trsf.Multiply(t)
+ return trsf
+
+ def mouseReleaseEvent(self, event: Any) -> None:
+ """
+ Called when a mouse button is released.
+ """
+ pt = event.pos()
+ modifiers = event.modifiers()
+ if event.button() == QtCore.Qt.MouseButton.LeftButton:
+ if self.manip_moved:
+ self.trsf_manip.append(self.trsf)
+ self.manip_moved = False
+ if self._select_area:
+ [Xmin, Ymin, dx, dy] = self._drawbox
+ self._display.SelectArea(Xmin, Ymin, Xmin + dx, Ymin + dy)
+ self._select_area = False
+ elif modifiers == QtCore.Qt.Modifier.SHIFT:
+ self._display.ShiftSelect(pt.x(), pt.y())
+ else:
+ # single select otherwise
+ self._display.Select(pt.x(), pt.y())
+
+ if self._display.selected_shapes is not None:
+ self.sig_topods_selected.emit(self._display.selected_shapes)
+
+ elif event.button() == QtCore.Qt.MouseButton.RightButton:
+ if self._zoom_area:
+ [Xmin, Ymin, dx, dy] = self._drawbox
+ self._display.ZoomArea(Xmin, Ymin, Xmin + dx, Ymin + dy)
+ self._zoom_area = False
+
+ self.cursor = "arrow"
diff --git a/src/Display/qtDisplay.pyi b/src/Display/qtDisplay.pyi
new file mode 100644
index 000000000..dbe76dcbf
--- /dev/null
+++ b/src/Display/qtDisplay.pyi
@@ -0,0 +1,46 @@
+from typing import Any, Optional
+
+from OCC.Core.AIS import AIS_Manipulator
+from OCC.Core.gp import gp_Trsf
+from OCC.Display.backend import get_qt_modules
+
+QtCore, QtGui, QtWidgets, QtOpenGL = get_qt_modules()
+
+class qtBaseViewer(QtWidgets.QWidget):
+ def __init__(self, parent: Optional[Any] = None) -> None: ...
+ def resizeEvent(self, event: Any) -> None: ...
+ def paintEngine(self) -> None: ...
+
+class qtViewer3d(qtBaseViewer):
+ sig_topods_selected: Any
+
+ def __init__(self, *kargs: Any) -> None: ...
+ @property
+ def qApp(self) -> Any: ...
+ @qApp.setter
+ def qApp(self, value: Any) -> None: ...
+ def InitDriver(self) -> None: ...
+ def createCursors(self) -> None: ...
+ def keyPressEvent(self, event: Any) -> None: ...
+ def focusInEvent(self, event: Any) -> None: ...
+ def focusOutEvent(self, event: Any) -> None: ...
+ def paintEvent(self, event: Any) -> None: ...
+ def wheelEvent(self, event: Any) -> None: ...
+ @property
+ def cursor(self) -> str: ...
+ @cursor.setter
+ def cursor(self, value: str) -> None: ...
+ def mousePressEvent(self, event: Any) -> None: ...
+ def mouseReleaseEvent(self, event: Any) -> None: ...
+ def DrawBox(self, event: Any) -> None: ...
+ def mouseMoveEvent(self, evt: Any) -> None: ...
+
+class qtViewer3dWithManipulator(qtViewer3d):
+ sig_topods_selected: Any
+
+ def __init__(self, *kargs: Any) -> None: ...
+ def set_manipulator(self, manipulator: AIS_Manipulator) -> None: ...
+ def mousePressEvent(self, event: Any) -> None: ...
+ def mouseMoveEvent(self, evt: Any) -> None: ...
+ def get_trsf_from_manip(self) -> gp_Trsf: ...
+ def mouseReleaseEvent(self, event: Any) -> None: ...
diff --git a/src/Display/tkDisplay.py b/src/Display/tkDisplay.py
new file mode 100644
index 000000000..b01b77981
--- /dev/null
+++ b/src/Display/tkDisplay.py
@@ -0,0 +1,108 @@
+##Copyright 2023 Thomas Paviot (tpaviot@gmail.com)
+##
+##This file is part of pythonOCC.
+##
+##pythonOCC is free software: you can redistribute it and/or modify
+##it under the terms of the GNU Lesser General Public License as published by
+##the Free Software Foundation, either version 3 of the License, or
+##(at your option) any later version.
+##
+##pythonOCC is distributed in the hope that it will be useful,
+##but WITHOUT ANY WARRANTY; without even the implied warranty of
+##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+##GNU Lesser General Public License for more details.
+##
+##You should have received a copy of the GNU Lesser General Public License
+##along with pythonOCC. If not, see .
+
+import tkinter as tk
+
+from OCC.Display import OCCViewer
+
+
+class tkViewer3d(tk.Frame):
+ """
+ A Tkinter widget for an OCC viewer.
+ """
+
+ def __init__(self, parent: "tk.Widget", default: str = "") -> None:
+ """
+ Initializes the tkViewer3d.
+
+ Args:
+ parent: The parent widget.
+ default (str, optional): The default value.
+ """
+ tk.Frame.__init__(self, parent, width=1024, height=768)
+ self.bind("