2013-04-20 20:33:35 -04:00
|
|
|
"""
|
|
|
|
Tests basic workplane functionality
|
|
|
|
"""
|
2017-09-17 00:57:12 +02:00
|
|
|
# core modules
|
2022-05-18 09:36:43 -04:00
|
|
|
import os
|
2018-02-02 16:34:37 +01:00
|
|
|
import io
|
2022-05-04 14:43:43 -04:00
|
|
|
from pathlib import Path
|
|
|
|
import re
|
2022-09-09 18:46:47 +02:00
|
|
|
import sys
|
2022-06-17 16:17:22 -04:00
|
|
|
import pytest
|
2023-02-20 23:10:44 -08:00
|
|
|
import ezdxf
|
|
|
|
|
|
|
|
from pytest import approx
|
2017-09-17 00:57:12 +02:00
|
|
|
|
|
|
|
# my modules
|
2023-02-20 23:10:44 -08:00
|
|
|
from cadquery import (
|
|
|
|
exporters,
|
|
|
|
importers,
|
|
|
|
Workplane,
|
|
|
|
Edge,
|
|
|
|
Vertex,
|
|
|
|
Assembly,
|
|
|
|
Plane,
|
|
|
|
Location,
|
|
|
|
Vector,
|
|
|
|
)
|
2013-04-20 20:33:35 -04:00
|
|
|
from tests import BaseTest
|
2021-08-31 08:58:51 +02:00
|
|
|
from OCP.GeomConvert import GeomConvert
|
|
|
|
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge
|
2013-04-20 20:33:35 -04:00
|
|
|
|
2017-09-17 00:57:12 +02:00
|
|
|
|
2022-06-17 16:17:22 -04:00
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
def tmpdir(tmp_path_factory):
|
|
|
|
return tmp_path_factory.mktemp("out")
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
def box123():
|
|
|
|
return Workplane().box(1, 2, 3)
|
|
|
|
|
|
|
|
|
|
|
|
def test_step_options(tmpdir):
|
2022-05-18 09:36:43 -04:00
|
|
|
"""
|
|
|
|
Exports a box using the options to decrease STEP file size and
|
|
|
|
then imports that STEP to validate it.
|
|
|
|
"""
|
|
|
|
# Use a temporary directory
|
|
|
|
box_path = os.path.join(tmpdir, "out.step")
|
|
|
|
|
|
|
|
# Simple object to export
|
|
|
|
box = Workplane().box(1, 1, 1)
|
|
|
|
|
|
|
|
# Export the STEP with the size-saving options and then import it back in
|
|
|
|
box.val().exportStep(box_path, write_pcurves=False, precision_mode=0)
|
|
|
|
w = importers.importStep(box_path)
|
|
|
|
|
|
|
|
# Make sure there was a valid box in the exported file
|
|
|
|
assert w.solids().size() == 1
|
|
|
|
assert w.faces().size() == 6
|
|
|
|
|
|
|
|
|
2013-04-20 20:33:35 -04:00
|
|
|
class TestExporters(BaseTest):
|
2020-08-07 08:50:53 +02:00
|
|
|
def _exportBox(self, eType, stringsToFind, tolerance=0.1, angularTolerance=0.1):
|
2013-04-20 20:33:35 -04:00
|
|
|
"""
|
2020-09-27 06:33:59 -07:00
|
|
|
Exports a test object, and then looks for
|
|
|
|
all of the supplied strings to be in the result
|
|
|
|
returns the result in case the case wants to do more checks also
|
2013-04-20 20:33:35 -04:00
|
|
|
"""
|
2017-09-17 00:57:12 +02:00
|
|
|
p = Workplane("XY").box(1, 2, 3)
|
2018-02-02 16:34:37 +01:00
|
|
|
|
2022-09-09 18:46:47 +02:00
|
|
|
if eType in (exporters.ExportTypes.AMF, exporters.ExportTypes.THREEMF):
|
2018-02-02 16:34:37 +01:00
|
|
|
s = io.BytesIO()
|
|
|
|
else:
|
|
|
|
s = io.StringIO()
|
2020-01-20 20:52:12 +01:00
|
|
|
|
2020-08-06 14:34:22 -07:00
|
|
|
exporters.exportShape(
|
2020-08-07 08:50:53 +02:00
|
|
|
p, eType, s, tolerance=tolerance, angularTolerance=angularTolerance
|
2020-08-06 14:34:22 -07:00
|
|
|
)
|
2014-08-18 14:45:02 -04:00
|
|
|
|
2020-01-20 20:52:12 +01:00
|
|
|
result = "{}".format(s.getvalue())
|
2017-09-17 00:57:12 +02:00
|
|
|
|
2013-04-20 20:33:35 -04:00
|
|
|
for q in stringsToFind:
|
2017-09-17 00:57:12 +02:00
|
|
|
self.assertTrue(result.find(q) > -1)
|
2013-04-20 20:33:35 -04:00
|
|
|
return result
|
2014-08-18 14:45:02 -04:00
|
|
|
|
2020-07-29 08:52:41 +02:00
|
|
|
def _box(self):
|
|
|
|
|
|
|
|
return Workplane().box(1, 1, 1)
|
|
|
|
|
2022-06-18 08:51:23 -04:00
|
|
|
def testSTL(self):
|
|
|
|
# New STL tests have been added; Keep this to test deprecated exportShape
|
|
|
|
self._exportBox(exporters.ExportTypes.STL, ["facet normal"])
|
|
|
|
|
2013-04-20 20:33:35 -04:00
|
|
|
def testSVG(self):
|
2020-01-20 20:52:12 +01:00
|
|
|
self._exportBox(exporters.ExportTypes.SVG, ["<svg", "<g transform"])
|
2013-04-20 20:33:35 -04:00
|
|
|
|
2020-07-29 08:52:41 +02:00
|
|
|
exporters.export(self._box(), "out.svg")
|
|
|
|
|
2021-01-28 16:04:35 -05:00
|
|
|
def testSVGOptions(self):
|
|
|
|
self._exportBox(exporters.ExportTypes.SVG, ["<svg", "<g transform"])
|
|
|
|
|
|
|
|
exporters.export(
|
|
|
|
self._box(),
|
|
|
|
"out.svg",
|
|
|
|
opt={
|
|
|
|
"width": 100,
|
|
|
|
"height": 100,
|
|
|
|
"marginLeft": 10,
|
|
|
|
"marginTop": 10,
|
|
|
|
"showAxes": False,
|
|
|
|
"projectionDir": (0, 0, 1),
|
|
|
|
"strokeWidth": 0.25,
|
|
|
|
"strokeColor": (255, 0, 0),
|
|
|
|
"hiddenColor": (0, 0, 255),
|
|
|
|
"showHidden": True,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2013-04-20 20:33:35 -04:00
|
|
|
def testAMF(self):
|
2020-01-20 20:52:12 +01:00
|
|
|
self._exportBox(exporters.ExportTypes.AMF, ["<amf units", "</object>"])
|
2013-04-20 20:33:35 -04:00
|
|
|
|
2020-07-29 08:52:41 +02:00
|
|
|
exporters.export(self._box(), "out.amf")
|
|
|
|
|
2013-04-20 20:33:35 -04:00
|
|
|
def testSTEP(self):
|
2020-01-20 20:52:12 +01:00
|
|
|
self._exportBox(exporters.ExportTypes.STEP, ["FILE_SCHEMA"])
|
2013-04-20 20:33:35 -04:00
|
|
|
|
2020-07-29 08:52:41 +02:00
|
|
|
exporters.export(self._box(), "out.step")
|
|
|
|
|
2022-09-09 18:46:47 +02:00
|
|
|
def test3MF(self):
|
|
|
|
self._exportBox(
|
|
|
|
exporters.ExportTypes.THREEMF,
|
|
|
|
["3D/3dmodel.model", "[Content_Types].xml", "_rels/.rels"],
|
|
|
|
)
|
|
|
|
exporters.export(self._box(), "out1.3mf") # Compound
|
|
|
|
exporters.export(self._box().val(), "out2.3mf") # Solid
|
|
|
|
|
|
|
|
# No zlib support
|
|
|
|
import zlib
|
|
|
|
|
|
|
|
sys.modules["zlib"] = None
|
|
|
|
exporters.export(self._box(), "out3.3mf")
|
|
|
|
sys.modules["zlib"] = zlib
|
|
|
|
|
2013-04-20 20:33:35 -04:00
|
|
|
def testTJS(self):
|
2020-01-20 20:52:12 +01:00
|
|
|
self._exportBox(
|
|
|
|
exporters.ExportTypes.TJS, ["vertices", "formatVersion", "faces"]
|
|
|
|
)
|
2020-07-28 22:23:13 +02:00
|
|
|
|
2020-07-29 08:52:41 +02:00
|
|
|
exporters.export(self._box(), "out.tjs")
|
|
|
|
|
2020-10-14 19:07:47 +02:00
|
|
|
def testVRML(self):
|
|
|
|
|
|
|
|
exporters.export(self._box(), "out.vrml")
|
|
|
|
|
|
|
|
with open("out.vrml") as f:
|
|
|
|
res = f.read(10)
|
|
|
|
|
|
|
|
assert res.startswith("#VRML V2.0")
|
|
|
|
|
2020-11-19 08:22:35 +01:00
|
|
|
# export again to trigger all paths in the code
|
|
|
|
exporters.export(self._box(), "out.vrml")
|
|
|
|
|
2021-06-22 10:06:50 +02:00
|
|
|
def testVTP(self):
|
|
|
|
|
|
|
|
exporters.export(self._box(), "out.vtp")
|
|
|
|
|
|
|
|
with open("out.vtp") as f:
|
|
|
|
res = f.read(100)
|
|
|
|
|
|
|
|
assert res.startswith('<?xml version="1.0"?>\n<VTKFile')
|
|
|
|
|
2020-07-28 22:23:13 +02:00
|
|
|
def testDXF(self):
|
|
|
|
|
2020-07-29 08:52:41 +02:00
|
|
|
exporters.export(self._box().section(), "out.dxf")
|
|
|
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
exporters.export(self._box().val(), "out.dxf")
|
|
|
|
|
2020-07-28 22:23:13 +02:00
|
|
|
s1 = (
|
|
|
|
Workplane("XZ")
|
|
|
|
.polygon(10, 10)
|
|
|
|
.ellipse(1, 2)
|
|
|
|
.extrude(1)
|
|
|
|
.edges("|Y")
|
|
|
|
.fillet(1)
|
|
|
|
.section()
|
|
|
|
)
|
|
|
|
exporters.dxf.exportDXF(s1, "res1.dxf")
|
|
|
|
|
|
|
|
s1_i = importers.importDXF("res1.dxf")
|
|
|
|
|
2020-08-06 11:53:20 -07:00
|
|
|
self.assertAlmostEqual(s1.val().Area(), s1_i.val().Area(), 6)
|
|
|
|
self.assertAlmostEqual(s1.edges().size(), s1_i.edges().size())
|
2020-07-28 22:23:13 +02:00
|
|
|
|
|
|
|
pts = [(0, 0), (0, 0.5), (1, 1)]
|
|
|
|
s2 = (
|
|
|
|
Workplane().spline(pts).close().extrude(1).edges("|Z").fillet(0.1).section()
|
|
|
|
)
|
|
|
|
exporters.dxf.exportDXF(s2, "res2.dxf")
|
|
|
|
|
|
|
|
s2_i = importers.importDXF("res2.dxf")
|
|
|
|
|
2020-08-06 11:53:20 -07:00
|
|
|
self.assertAlmostEqual(s2.val().Area(), s2_i.val().Area(), 6)
|
|
|
|
self.assertAlmostEqual(s2.edges().size(), s2_i.edges().size())
|
2020-07-28 22:23:13 +02:00
|
|
|
|
|
|
|
s3 = (
|
|
|
|
Workplane("XY")
|
|
|
|
.ellipseArc(1, 2, 0, 180)
|
|
|
|
.close()
|
|
|
|
.extrude(1)
|
|
|
|
.edges("|Z")
|
|
|
|
.fillet(0.1)
|
|
|
|
.section()
|
|
|
|
)
|
|
|
|
exporters.dxf.exportDXF(s3, "res3.dxf")
|
|
|
|
|
|
|
|
s3_i = importers.importDXF("res3.dxf")
|
|
|
|
|
2020-08-06 11:53:20 -07:00
|
|
|
self.assertAlmostEqual(s3.val().Area(), s3_i.val().Area(), 6)
|
|
|
|
self.assertAlmostEqual(s3.edges().size(), s3_i.edges().size())
|
2020-07-29 08:52:41 +02:00
|
|
|
|
2020-09-27 06:33:59 -07:00
|
|
|
cyl = Workplane("XY").circle(22).extrude(10, both=True).translate((-50, 0, 0))
|
|
|
|
|
|
|
|
s4 = Workplane("XY").box(80, 60, 5).cut(cyl).section()
|
|
|
|
|
|
|
|
exporters.dxf.exportDXF(s4, "res4.dxf")
|
|
|
|
|
|
|
|
s4_i = importers.importDXF("res4.dxf")
|
|
|
|
|
|
|
|
self.assertAlmostEqual(s4.val().Area(), s4_i.val().Area(), 6)
|
|
|
|
self.assertAlmostEqual(s4.edges().size(), s4_i.edges().size())
|
|
|
|
|
2021-08-31 08:58:51 +02:00
|
|
|
# test periodic spline
|
2021-08-31 18:35:01 +02:00
|
|
|
w = Workplane().spline([(1, 1), (2, 2), (3, 2), (3, 1)], periodic=True)
|
|
|
|
exporters.dxf.exportDXF(w, "res5.dxf")
|
2021-08-31 08:58:51 +02:00
|
|
|
|
2021-08-31 18:35:01 +02:00
|
|
|
w_i = importers.importDXF("res5.dxf")
|
2021-08-31 08:58:51 +02:00
|
|
|
|
2021-08-31 18:35:01 +02:00
|
|
|
self.assertAlmostEqual(w.val().Length(), w_i.wires().val().Length(), 6)
|
2021-08-31 08:58:51 +02:00
|
|
|
|
|
|
|
# test rational spline
|
|
|
|
c = Edge.makeCircle(1)
|
|
|
|
adaptor = c._geomAdaptor()
|
|
|
|
curve = GeomConvert.CurveToBSplineCurve_s(adaptor.Curve().Curve())
|
|
|
|
|
|
|
|
e = Workplane().add(Edge(BRepBuilderAPI_MakeEdge(curve).Shape()))
|
2021-08-31 18:35:01 +02:00
|
|
|
exporters.dxf.exportDXF(e, "res6.dxf")
|
|
|
|
|
|
|
|
e_i = importers.importDXF("res6.dxf")
|
|
|
|
|
|
|
|
self.assertAlmostEqual(e.val().Length(), e_i.wires().val().Length(), 6)
|
|
|
|
|
|
|
|
# test non-planar section
|
|
|
|
s5 = (
|
|
|
|
Workplane()
|
|
|
|
.spline([(0, 0), (1, 0), (1, 1), (0, 1)])
|
|
|
|
.close()
|
|
|
|
.extrude(1, both=True)
|
|
|
|
.translate((-3, -4, 0))
|
|
|
|
)
|
|
|
|
|
|
|
|
s5.plane = Plane(origin=(0, 0.1, 0.5), normal=(0.05, 0.05, 1))
|
|
|
|
s5 = s5.section()
|
|
|
|
exporters.dxf.exportDXF(s5, "res7.dxf")
|
2021-08-31 08:58:51 +02:00
|
|
|
|
2021-08-31 18:35:01 +02:00
|
|
|
s5_i = importers.importDXF("res7.dxf")
|
2021-08-31 08:58:51 +02:00
|
|
|
|
2021-08-31 18:35:01 +02:00
|
|
|
self.assertAlmostEqual(s5.val().Area(), s5_i.val().Area(), 4)
|
2021-08-31 08:58:51 +02:00
|
|
|
|
2020-07-29 08:52:41 +02:00
|
|
|
def testTypeHandling(self):
|
|
|
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
exporters.export(self._box(), "out.random")
|
|
|
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
exporters.export(self._box(), "out.stl", "STP")
|
2022-05-04 14:43:43 -04:00
|
|
|
|
|
|
|
|
2022-06-17 16:17:22 -04:00
|
|
|
@pytest.mark.parametrize(
|
2022-06-18 08:51:23 -04:00
|
|
|
"id, opt, matchvals",
|
2022-06-17 16:17:22 -04:00
|
|
|
[
|
2022-08-12 12:04:46 -04:00
|
|
|
(0, {"ascii": True}, ["solid", "facet normal"]),
|
|
|
|
(1, {"ASCII": True}, ["solid", "facet normal"]),
|
2022-06-18 08:51:23 -04:00
|
|
|
(2, {"unknown_opt": 1, "ascii": True}, ["solid", "facet normal"]),
|
2022-08-12 12:04:46 -04:00
|
|
|
(3, {"ASCII": False, "ascii": True}, ["solid", "facet normal"]),
|
2022-06-17 16:17:22 -04:00
|
|
|
],
|
|
|
|
)
|
2022-06-18 08:51:23 -04:00
|
|
|
def test_stl_ascii(tmpdir, box123, id, opt, matchvals):
|
2022-06-17 16:17:22 -04:00
|
|
|
"""
|
2022-06-18 08:51:23 -04:00
|
|
|
:param tmpdir: temporary directory fixture
|
2022-06-17 16:17:22 -04:00
|
|
|
:param box123: box fixture
|
|
|
|
:param id: The index or id; output filename is <test name>_<id>.stl
|
|
|
|
:param opt: The export opt dict
|
2022-06-18 08:51:23 -04:00
|
|
|
:param matchval: List of strings to match at start of file
|
2022-06-17 16:17:22 -04:00
|
|
|
"""
|
|
|
|
|
2022-06-18 08:51:23 -04:00
|
|
|
fpath = tmpdir.joinpath(f"stl_ascii_{id}.stl").resolve()
|
2022-06-17 16:17:22 -04:00
|
|
|
assert not fpath.exists()
|
|
|
|
|
2022-06-18 08:51:23 -04:00
|
|
|
assert matchvals
|
2022-06-17 16:17:22 -04:00
|
|
|
|
|
|
|
exporters.export(box123, str(fpath), None, 0.1, 0.1, opt)
|
|
|
|
|
2022-06-18 08:51:23 -04:00
|
|
|
with open(fpath, "r") as f:
|
|
|
|
for i, line in enumerate(f):
|
|
|
|
if i > len(matchvals) - 1:
|
|
|
|
break
|
|
|
|
assert line.find(matchvals[i]) > -1
|
2022-06-17 16:17:22 -04:00
|
|
|
|
|
|
|
|
2022-06-18 08:51:23 -04:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
"id, opt, matchval",
|
|
|
|
[
|
2023-01-01 00:08:40 +01:00
|
|
|
(0, None, b"STL Exported by Open CASCADE"),
|
|
|
|
(1, {"ascii": False}, b"STL Exported by Open CASCADE"),
|
|
|
|
(2, {"ASCII": False}, b"STL Exported by Open CASCADE"),
|
|
|
|
(3, {"unknown_opt": 1}, b"STL Exported by Open CASCADE"),
|
|
|
|
(4, {"unknown_opt": 1, "ascii": False}, b"STL Exported by Open CASCADE"),
|
2022-06-18 08:51:23 -04:00
|
|
|
],
|
|
|
|
)
|
|
|
|
def test_stl_binary(tmpdir, box123, id, opt, matchval):
|
|
|
|
"""
|
|
|
|
:param tmpdir: temporary directory fixture
|
|
|
|
:param box123: box fixture
|
|
|
|
:param id: The index or id; output filename is <test name>_<id>.stl
|
|
|
|
:param opt: The export opt dict
|
|
|
|
:param matchval: Check that the file starts with the specified value
|
|
|
|
"""
|
|
|
|
|
2022-08-12 12:04:46 -04:00
|
|
|
fpath = tmpdir.joinpath(f"stl_binary_{id}.stl").resolve()
|
2022-06-17 16:17:22 -04:00
|
|
|
assert not fpath.exists()
|
|
|
|
|
2022-06-18 08:51:23 -04:00
|
|
|
assert matchval
|
|
|
|
|
|
|
|
exporters.export(box123, str(fpath), None, 0.1, 0.1, opt)
|
|
|
|
|
2022-06-17 16:17:22 -04:00
|
|
|
with open(fpath, "rb") as f:
|
|
|
|
r = f.read(len(matchval))
|
|
|
|
assert r == matchval
|
|
|
|
|
|
|
|
|
|
|
|
def test_assy_vtk_rotation(tmpdir):
|
2022-05-04 14:43:43 -04:00
|
|
|
|
|
|
|
v0 = Vertex.makeVertex(1, 0, 0)
|
|
|
|
|
|
|
|
assy = Assembly()
|
|
|
|
assy.add(
|
|
|
|
v0, name="v0", loc=Location(Vector(0, 0, 0), Vector(1, 0, 0), 90),
|
|
|
|
)
|
|
|
|
|
|
|
|
fwrl = Path(tmpdir, "v0.wrl")
|
|
|
|
assert not fwrl.exists()
|
|
|
|
assy.save(str(fwrl), "VRML")
|
|
|
|
assert fwrl.exists()
|
|
|
|
|
|
|
|
matched_rot = False
|
|
|
|
with open(fwrl) as f:
|
|
|
|
pat_rot = re.compile("""rotation 1 0 0 1.5707963267""")
|
|
|
|
for line in f:
|
|
|
|
if m := re.search(pat_rot, line):
|
|
|
|
matched_rot = True
|
|
|
|
break
|
|
|
|
|
|
|
|
assert matched_rot
|
2022-10-04 19:48:48 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_tessellate(box123):
|
|
|
|
|
|
|
|
verts, triangles = box123.val().tessellate(1e-6)
|
|
|
|
assert len(verts) == 24
|
|
|
|
assert len(triangles) == 12
|
2023-02-20 23:10:44 -08:00
|
|
|
|
|
|
|
|
|
|
|
def _dxf_spline_max_degree(fname):
|
|
|
|
|
|
|
|
dxf = ezdxf.readfile(fname)
|
|
|
|
msp = dxf.modelspace()
|
|
|
|
|
|
|
|
rv = 0
|
|
|
|
|
|
|
|
for el in msp:
|
|
|
|
if isinstance(el, ezdxf.entities.Spline):
|
|
|
|
rv = el.dxf.degree if el.dxf.degree > rv else rv
|
|
|
|
|
|
|
|
return rv
|
|
|
|
|
|
|
|
|
|
|
|
def _check_dxf_no_spline(fname):
|
|
|
|
|
|
|
|
dxf = ezdxf.readfile(fname)
|
|
|
|
msp = dxf.modelspace()
|
|
|
|
|
|
|
|
for el in msp:
|
|
|
|
if isinstance(el, ezdxf.entities.Spline):
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def test_dxf_approx():
|
|
|
|
|
|
|
|
pts = [(0, 0), (0, 0.5), (1, 1)]
|
|
|
|
w1 = Workplane().spline(pts).close().extrude(1).edges("|Z").fillet(0.1).section()
|
|
|
|
exporters.exportDXF(w1, "orig.dxf")
|
|
|
|
|
|
|
|
assert _dxf_spline_max_degree("orig.dxf") == 6
|
|
|
|
|
|
|
|
exporters.exportDXF(w1, "limit1.dxf", approx="spline")
|
|
|
|
w1_i1 = importers.importDXF("limit1.dxf")
|
|
|
|
|
|
|
|
assert _dxf_spline_max_degree("limit1.dxf") == 3
|
|
|
|
|
|
|
|
assert w1.val().Area() == approx(w1_i1.val().Area(), 1e-3)
|
|
|
|
assert w1.edges().size() == w1_i1.edges().size()
|
|
|
|
|
|
|
|
exporters.exportDXF(w1, "limit2.dxf", approx="arc")
|
|
|
|
w1_i2 = importers.importDXF("limit2.dxf")
|
|
|
|
|
|
|
|
assert _check_dxf_no_spline("limit2.dxf")
|
|
|
|
|
|
|
|
assert w1.val().Area() == approx(w1_i2.val().Area(), 1e-3)
|