Compare commits

...
14 Commits
Author SHA1 Message Date
Clifford Yapp ae128b65e3 Annotation raytracing as overlay
This was the initial attempt at non-scene-geometry annotation raytracing.
Squashed down to one commit for simpler diff review.

Clear Werror=subobject-linkage issue
use bu_strcmp
Make annotation intersections scale independent
Use relative parallel and determinant tests so valid annotations are not rejected after small model transforms.  Carry the caller's model tolerance into visibility decisions and cover both behaviors in the focused annotation semantics test.
Use a strict annotation hit ordering
Compare hit distances exactly before using the path as a tie breaker.  Approximate equality in a std::sort predicate is not transitive and can violate the strict weak ordering required by the C++ standard library.
Preserve hit distance in simple rt lightmodels
Record the first partition distance in viewit, matching colorview.  Annotation compositing uses this value to keep model-space marks behind foreground geometry for lightmodels 1, 2, 4, 5, and 6.
Defer NIRT annotation reporting
Remove the provisional useannot mode instead of emitting records outside NIRT's fmt system.  This preserves existing structured output contracts and avoids extending the public nirt_opt_vals layout until annotation records have a complete formatter design.
Use annotation line styles for render width
Remove the redundant object-wide render_width field, ANR2 payload, and --stroke-width option.  Ray renderers now rely on the established per-segment line_width style over an automatic base width, preserving the public primitive layout and avoiding another database extension.  Document and test the ray coverage behavior.
Name the ADRT model coordinate scale
Replace repeated 1000-to-1 load conversions with ADRT_MODEL_UNITS_PER_TIE_UNIT so geometry and annotation compositing share one documented coordinate convention.
Keep annotations out of the librender camera ABI
Replace camera-resident annotation fields with an additive render_camera_render_annotations entry point.  Per-render private state now carries the scene, coordinate scale, and derived view while the public render_camera_t and thread-data layouts remain unchanged.  Preserve primary rays before shaders mutate them so annotation compositing evaluates the intended sample.
Define ISST annotation scene ownership
Make GFile release its annotation scene and TIE data, destroy the render window before its GFile owner, and clear the renderer under its mutex before reloading.  Queued renders now reject cleared scenes after acquiring the context, preventing use-after-free during reload and shutdown.
Cache rt annotation preparation state
Keep annotation scene and view state private to do.c, expose read-only accessors to workers, and reuse prepared coverage when the database, selected paths, and tolerances are unchanged.  Explicitly invalidate on animation changes and centralize cleanup.
Add a NIRT annotation output format
Add opt-in NIRT annotation reporting
Document NIRT annotation diagnostics
Document annotation and view-object roles
Remove the unused ISST application meta-object
ISSTApp declares no signals, slots, properties, or invokable methods.  Avoid generating a redundant derived meta-object, which also prevents Qt 6 from rejecting the member main-window construction during application startup.
Use Qt 6 OpenGL dependencies for ISST
Qt6::OpenGL and Qt6::OpenGLWidgets already carry the platform OpenGL implementation.  Do not also link the independently discovered host OpenGL target, which can select a different GL ABI and bypass Qt's configured backend.
Keep ISST annotations out of NMG tessellation
ISST renders annotations through its dedicated annotation scene.  Exclude annotation-only roots from the geometry walk and replace annotation leaves in mixed trees with empty nodes so the independent NMG path does not report unsupported tessellation.
Share ADRT annotation tessellation filtering
Frame ISST views using annotation bounds
Gate ART annotation frame processing
Honor inherited colors in annotation renders
Make RTree ray queries const-safe
Composite ordered annotation coverage with RTree indices
Blend all annotation layers in image renderers
Test ordered annotation coverage and alpha blending
Test ADRT annotation filtering and scene framing
Document annotation layering and lookup design
Tighten annotation scene preparation
Test annotation combination color inheritance
2026-09-18 18:22:49 +00:00
Clifford Yapp 21e034ee34 Rename so we don't end up conflicting with an MSVC header 2026-09-18 16:13:27 +00:00
Clifford Yapp 8f81f27d92 librt: correct winding for explicit DSP B-C cell cuts
The explicit ULlr permutation inverted both terrain triangle normals.
The cell intersection state machine uses normal direction to classify
entry and exit, so top-to-side rays could disappear.

An independent triangle mesh and the DSP received the same 500,000
Crofton chords. The mesh returned 3,689 hit rays, while DSP returned
3,463; the DSP surface estimate was 10.35% below the exact area.
Using the adaptive B-C permutation for explicit cuts preserves the
selected diagonal and restores outward top normals. DSP then returned
3,688 hit rays and the area error fell to 4.53%.

Add a fixed top-to-side ray that checks entry and exit distances plus
the top normal.
2026-09-18 15:45:52 +00:00
Clifford Yapp d729a7d3f2 librt: make Crofton sampling deterministic and reusable
Retain ordinary Monte Carlo chord sampling while making its stream
explicit and portable. Draw independent sphere endpoints from a
fixed-seed mt19937_64 generator so every setup is reproducible
without changing the estimator's statistical convergence model.

Convert the generator's high 53 bits explicitly to avoid library-
specific real-number conversions. Consume four values per ray so
ray offsets reproduce disjoint continuations of the same stream.

Reuse a bounded C++ worker pool and stop only after windowed area
estimates remain stable. This avoids repeated thread construction
while retaining the existing stopping criteria.

Add optional collection of ray identifiers, paired entry and exit
hit points, normals, and partition thickness. This gives
reconstruction callers structured source evidence while preserving
lightweight area and volume estimation. Link librt to the thread
target and extend the Crofton tests to verify that offset calls
exactly reproduce the uninterrupted stream.
2026-09-18 14:48:05 +00:00
Clifford Yapp 17601916f4 Use static diagonal transformation in IGES test 2026-09-18 14:02:22 +00:00
Clifford Yapp 90e5ede2b3 Add opt-in forward parity shots for solid BoTs
When LIBRT_BOT_PARITY_SHOT is enabled at prep, solid
BoTs and ARS conversions traverse from the current shot
origin. Discard hits behind that origin and use an odd
forward hit count to construct an interior entry at the
origin. Mark the entry with a dedicated surface number
and a normal opposite the ray. The option assumes a
closed mesh without self intersections; box backout
remains the default because stray exits on bad meshes
can become long segments and origin entries can change
rendering behavior.

Exercise scalar and packet shots for ARS and BoT, plus
composite scene shots, in both modes. With cameras
inside Generic_Twin meshes, median shot time fell 10.6%
for an isolated solid, 1.4% for the full scene from the
same view, and 6.0% for another full-scene inside view.
The isolated render matched, while the full-scene views
changed 173 of 1048576 and 507 of 65536 pixels,
respectively.
2026-09-18 00:30:10 +00:00
Clifford Yapp 72454757ec Add accelerated triangle mesh self-intersection check
Expose an early-exit libbg query for mesh self intersections
and use a BVH to keep candidate pair checks practical on large
meshes. Distinguish contact at shared indexed features from
crossing or overlap, and handle near-contact cases observed
in thin wing meshes.

Make strict hole-fill validation reject self intersections
when requested while preserving the permissive repair default.
Cover shared features, overlapping meshes, malformed input,
and thin wing fixtures. A clean 32768-face grid takes about
58 ms with the BVH, versus about 4 s with the prior pairwise
check.
2026-09-18 00:30:10 +00:00
Clifford Yapp 6e9e1975b8 Distcheck fixes 2026-09-17 19:52:47 -04:00
Clifford Yapp afedaa95bd Enable RPP tests for seven bounded primitives
Particle entered the function table with ft_use_rpp=0 in 1990
(b85ddfc9ff); RPC, RHC, EPA, and EHY did so in 1992
(16dc889cfb).  CLINE arrived with zero in 2000 (4a69d5ab3c).
ANNOT was a zero-flagged stub in 2012 (f31b27d992) and retained the
value when its callbacks arrived in 2017 (cb83958bf5).  The later
table moves and callback rewrites preserved these values.  The
inspected commits record no per-primitive correctness or performance
reason for choosing zero.

The finite shot geometry is enclosed by the prepared boxes: particle
uses its two end radii, CLINE includes its radius and beam expansion,
and RPC, RHC, EPA, and EHY use conservative profile envelopes.
Model-space ANNOT prep produces a BoT and uses its bounds; screen-space
ANNOT has no shot geometry.  Enable the solid RPP check for these seven.

Leave HALF at zero because it is infinite and SKETCH because it never
shoots.  HF computes its box from the maximum height but omits a
negative minimum, so its box can be too small.  ARB8 and ARBN already
clip rays against their faces or planes, and SPH has a cheap quadratic
shot; an added box test needs its own tolerance and performance case.
BoT uses a BVH and origin backout, so changing its RPP behavior needs
separate validation.  The remaining zero entries do not shoot rays.

Five deterministic 1024-square primitives.g renders averaged 1.222
seconds of shot time before and 1.166 after, with 178,709 fewer solid
shot calls and identical pixels.  Two other camera views also matched,
as did NIRT results for rays starting inside each of the six finite
solid types.  The annotation raytrace regression passes.
2026-09-17 21:35:11 +00:00
Clifford Yapp 5c8092b4e6 Use accurate PNTS bounds for ray box tests
PNTS shot uses each point's scale when the point type carries one,
but its bounding box used only the collection scale.  A larger point
could therefore lie outside the reported box and be missed by the RPP
test.  Share the effective scale calculation with prep and include
each rendered sphere in the box.  Reject empty or invalid point sets
instead of returning an unusable box.

Enable ft_use_rpp for PNTS once its box encloses the shot geometry.
Test a ray that hits a point larger than the collection scale and
check that empty point sets do not report usable bounds.
2026-09-17 21:21:43 +00:00
Clifford Yapp de09372b73 Fix BoT shots with origins inside mesh bounds
Commit 7376385fd2 backed the HLBVH traversal origin by twice the
soltab bounding radius to preserve entrance triangles.  Commit
1d75918492 replaced that conservative distance with max(0, -r_min),
assuming r_min reaches the solid boundary.  During rt_shootray,
however, r_min can describe the current spatial partition cell.  A
ray starting inside a BoT can enter that cell after entering the mesh.
BVH culling then drops the entrance triangles, and segment assembly
discards the exit hit without an entrance.  In an overlapping scene,
ARS and native BoT differ because the ARS RPP test refreshes r_min.

For origins inside the BoT BVH root box, move the traversal origin
just beyond the box's entry face.  This retains the entrance facets
without the excess traversal from a bounding radius backout.  Rays
originating outside the box need no backout.  Share the calculation
between scalar and vector shots, while keeping triangle intersections
relative to the original ray origin.

Five deterministic, single processor Generic_Twin.g renders per view
had identical pixels with both backout methods.  The box method cut
shot time by 3.5% from inside the flight deck console, 24.5% from
inside a main strut, and 5.9% from inside a tire.  An exterior view
differed by about 0.2% from the radius method.  Avoiding unnecessary BVH
traversal more than offsets the extra box backout arithmetic for
these interior views.

Test direct ARS and BoT shots whose origins are inside the mesh,
including vector shots, and an overlapping composite scene that
reproduces the failure through rt_shootray without setting r_min
manually.
2026-09-17 21:20:03 +00:00
Clifford Yapp bcfb8669c1 Revert "Add Windows GUI session probe"
This reverts commit 066ec0bc8e.
2026-09-17 20:59:30 +00:00
Clifford Yapp 066ec0bc8e Add Windows GUI session probe
Testing whether we can actually do GUI applcation testing on a
Github CI runner.
2026-09-17 16:48:59 +00:00
Clifford Yapp fa3822b8f8 Modularize GUI regression helpers and isolate Xmin backend 2026-09-17 16:35:48 +00:00
108 changed files with 7118 additions and 3056 deletions
+246
View File
@@ -0,0 +1,246 @@
Annotation and view-object rendering notes
==========================================
Scope
-----
BRL-CAD has two related, but deliberately distinct, ways to represent visual
markup:
* An annotation is an ID_ANNOT database primitive. It is persistent model
content with enough semantic and presentation information to be reproduced
outside the application that created it.
* A view object is application-owned scene state. It is useful for interactive
construction and manipulation, but is intentionally not backed by a database
object.
They share portions of the interactive display infrastructure, but they do not
currently have interchangeable ownership, lifetime, or rendering semantics.
Annotation capabilities
-----------------------
An rt_annot_internal stores a model-space anchor, a set of 2D vertices, and
either a model plane basis or screen-space placement. Its segment types cover
lines, circular arcs, NURBS, Beziers, text, and filled polygons. Segment style
records include a semantic role, line pattern and width, RGBA color, font or
symbol, text scaling, and decorative flags.
The libged annotate command can create styled text, leaders, linear/angular/
radius/diameter/ordinate dimensions, and bounding-box autodim groups. It can
regenerate derived annotations from saved attributes and associate them with
source model objects without making them part of those objects' CSG trees.
Annotations therefore have the properties needed for persistent markup:
* database naming, paths, transforms, and instances;
* survival across save/reopen and transfer to another application;
* semantic segment roles and per-segment presentation data;
* model-space and view-dependent screen-space placement; and
* high-level generation metadata and source-object associations.
An annotation is informational coverage, not a CSG solid. The raytracing
implementation preserves that distinction. It prepares annotation coverage
from active database paths and composites it on primary samples after ordinary
geometry has established color and depth. Model-space annotation coverage is
occluded by nearer solid geometry. Screen-space annotation coverage uses the
render view and image dimensions. Annotations do not acquire artificial
volume, alter model bounds, cast shadows, or participate in reflection and
refraction rays.
Visible coverage is returned in back-to-front layers. Model-space layers are
ordered by ray distance, followed by view-space layers ordered by projected
anchor depth; segment draw order breaks ties within an annotation. This lets
the image renderers apply source-over alpha consistently while retaining the
single-frontmost-hit query for compatibility. NIRT continues to report one
frontmost segment per model-space annotation rather than exposing these
presentation layers as solid partitions.
Color follows normal database tree state. An annotation segment with an
explicit RGBA style wins; otherwise it inherits the resolved combination-path
color; otherwise it is white. The legacy region-ID color table is not applied
to annotation display or coverage, since annotations are informational leaves
rather than regions.
Prepared coverage uses the existing libbg RTree implementation. A 3D RTree
culls model-space annotation instances and a 2D local RTree culls segments
inside every annotation. Screen-space instance extents cannot be placed in a
static world-space index because they change with the view, so those instances
remain in a compact list while still using their local segment RTree. This
avoids a new spatial data structure and keeps prepared scenes immutable and
safe for concurrent sample queries.
The rt, ART, and ADRT/ISST integrations follow this overlay model. ERT invokes
rt with the database, displayed database paths, and view, so database-backed
annotations can follow that route as well. ERT does not currently serialize
or pass application-owned view objects to its subprocess.
NIRT keeps annotation diagnostics opt-in. `useannot 1` enables model-space
annotation intersections, and the `fmt a` action reports them. Default NIRT
output is unchanged. Screen-space annotations are deliberately omitted
because a NIRT ray does not define an image size, pixel footprint, or complete
model-to-view mapping.
View-object capabilities
------------------------
The core representation is bv_scene_obj. A view set owns shared view objects,
and a particular bview can also own local objects. Independent views force
local ownership. bv_obj_put recycles objects when their application lifetime
ends; the objects are not written to the .g database.
The `view obj` command exposes the principal markup types:
* Lines are model-coordinate polylines represented by vlists. Generic scene
rendering can add arrowheads to segment ends.
* Labels have a model anchor and optional model-space target/leader. Their text
is projected and drawn as screen-facing HUD text. If the command omits Z,
screen X/Y coordinates are projected onto the current view plane at creation
time.
* Polygons include general polygons, circles, ellipses, rectangles, and
squares. Creation and editing operate on a saved view plane with a view-Z
offset. Selection, movement, open/close, area, overlap, and 2D boolean
operations are supported. A "fill" is currently a set of hatch lines, not
an opaque surface. Polygons can be explicitly imported from or exported to
persistent sketch primitives.
Scene objects also provide object color, transparency, pixel line width, dash
style, and arrow dimensions. Not all of these properties have a uniform
command interface for every object type.
There are other visual layers that should not be conflated with these objects.
Legacy gv_tcl data/sdata arrows, axes, lines, labels, and polygons have a
separate representation. Faceplate/HUD elements are screen UI. Database
objects temporarily cloned for editing are also transient, but retain database
primitive semantics and live in the database-object scene containers.
Comparison
----------
Both annotations and view objects eventually produce display-manager pixels,
often through bv_scene_obj and vlists. That common final path does not make
their source representations equivalent.
Annotations are the canonical choice when markup must be saved, named in a
database, instanced, shared, raytraced from a .g file, or understood by another
program. View objects are the right choice for application/session state that
must be created and manipulated without changing the database.
Some view objects have a direct annotation analogue, but conversion is not
lossless without type-aware logic:
* A view polyline maps naturally to line segments, including arrowhead style.
* A polygon outline and its hatch lines can map to annotation coverage, but the
existing view "fill" must not silently become an opaque filled polygon.
* A label is hybrid. Its target and leader are model-space geometry while its
glyphs remain screen-facing at a projected model anchor.
* Arbitrary vlist extraction is insufficient because display-matrix commands,
point and line sizes, child objects, glyph behavior, and semantic roles are
otherwise lost or ambiguous.
View-object line width and annotation line width also need an explicit unit
policy. View-object widths are display pixels. Screen-space annotation widths
are view-dependent coverage widths. A model-space annotation's line_width is
used as a scale on the automatically selected model-space stroke width. In
other words, removing a separate raytracer "stroke-width" setting does not
remove width control: the annotation's existing line_width/`--line-width`
style remains the single user-facing control. The renderer interprets it in
the appropriate coordinate system rather than exposing a second competing
knob.
Recommended transient rendering design
--------------------------------------
Do not make librt or the raytracers depend on bv_scene_obj, libbv application
containers, or libged. Do not manufacture tubes, TGCs, meshes, or geometry
just in front of the view plane. Artificial geometry would affect bounds,
depth, shadows, and secondary rays, and would make antialiasing and perspective
behavior depend on a fabricated distance from the eye.
If transient view objects need to appear in raytraced output, use an explicit
application-to-renderer snapshot boundary:
1. The GED/application layer snapshots selected live view objects when a render
is launched. This is the only layer that owns their lifetime and semantics.
2. A type-aware adapter in libged or libbv translates supported objects into a
renderer-neutral overlay/coverage description. It should reuse annotation
coverage preparation through an in-memory annotation-instance interface,
rather than duplicate line, glyph, and arrow intersection algorithms.
3. The snapshot travels with the exact view state: model-to-view transform,
perspective, image width and height, and sampling convention. In-process
renderers can receive it directly; subprocess rt/ERT needs an explicit
serialized side channel.
4. Raytracers composite this coverage only on primary image samples, after
geometry color and depth are known. It remains outside CSG, model bounds,
shadows, reflections, and refractions.
5. Initial support should be deliberately finite: polyline strokes and
arrowheads, label glyphs and leaders, polygon outlines, and polygon hatches.
Faceplate elements and legacy gv_tcl overlays should be separately specified
opt-in layers rather than being included accidentally.
6. Snapshot records should preserve object name and type, RGBA, visibility,
local/shared ownership, presentation units, and source identity. Unsupported
object types should produce a deliberate diagnostic or documented omission.
Application-side compositing of the interactive display-manager layer over an
ERT framebuffer is a viable smaller feature if only ERT screen captures need
view objects. It would closely reproduce the interactive view, but it would
not make the objects available to standalone rt, ART, or ISST output and should
not be presented as the general renderer solution.
Conversion between the two systems should remain explicit. Automatically
turning view objects into database annotations would violate their deliberately
transient contract. A user command to promote selected view markup to a
persistent annotation would be reasonable; polygon-to-sketch export is an
existing precedent. Conversely, a view-object editor may be used as a front
end for constructing an annotation, provided the persistence action is clear.
Maintenance findings before implementing the adapter
----------------------------------------------------
Several representation issues should be corrected before type-driven snapshot
conversion is relied upon:
* Container-selection flags (BV_DB_OBJS, BV_VIEW_OBJS, BV_LOCAL_OBJS, and
BV_CHILD_OBJS) reuse numeric bits also used for semantic s_type_flags
(BV_DBOBJ_BASED, BV_VIEWONLY, BV_LINES, and others). bv_obj_create currently
assigns its type argument to s_type_flags. A local view object consequently
acquires BV_LINES incidentally, while the line command does not explicitly
tag lines. The namespaces should be separated and every object explicitly
typed.
* Lines, labels, and polygons should have unambiguous type tags and per-type
lifecycle/extraction callbacks. Snapshot conversion must inspect typed data,
not infer semantics from a rendered vlist.
* Label construction should initialize every bv_label field deliberately,
including size, line_flag, anchor, arrow, and target. Current construction
does not establish all of those values on every path.
* The status of legacy gv_tcl overlay data should be decided explicitly:
migrate it, support it through a separate adapter, or document its exclusion.
* Screen-space annotation display and raytrace units need a parity test. The
legacy display-matrix vlist path and the ray coverage path currently express
screen offsets through different mechanisms; their apparent agreement must
be verified rather than assumed.
* Per-segment annotation colors exceed the legacy interactive vlist object's
single-color model. Uniform annotate-command colors work today, but true
multicolor interactive parity may require split scene objects or a richer
display representation.
Tests for a future adapter should cover shared and local ownership, cleanup,
label anchors and leaders, polygon holes/hatches/view-Z, perspective, pixel
width, model-space occlusion, screen-space overlay, and serialization across
the ERT subprocess boundary.
Conclusion
----------
Persistent annotations and transient view objects should continue to coexist.
The annotation coverage scene is the correct renderer-side abstraction for
both saved annotations and a future snapshot of transient markup. The
application should perform the semantic conversion and transfer a view-complete
snapshot; the renderer should treat it as primary-ray coverage, never as
invented scene geometry.
+53 -1
View File
@@ -132,6 +132,10 @@ Command to set the backout flag. With no option, prints the current value. When
Sets the _useair_ member of the __rt_i__ structure to the integer _n_. If _n_ is 0, then *nirt* ignores any air in the geometry. If this command is invoked with no arguments, *nirt* prints the current value of _useair_. Default is 0.
*useannot [* _n_*]*::
Enables (_n_=1) or disables (_n_=0) diagnostic annotation intersections. When enabled, *nirt* evaluates all model-space annotations below the active database paths and invokes the *a* output action for each crossing. Screen-space annotations are not evaluated because an individual NIRT ray does not supply a pixel size or model-to-view transform. If this command is invoked with no arguments, *nirt* prints the current value. Default is 0.
*overlap_claims [* _n_*]*::
Specifies how to handle multiple regions' claims to segments of a ray. If _n_ is 0 or "resolve", then all overlaps are resolved in favor of a single region and any other claimants are ignored. If _n_ is 2 or "rebuild_all", then all overlaps are rebuilt, so any overlapping regions along the ray create individual (geometrically intersecting) partitions. If _n_ is 3 or "retain", then all overlaps are retained. In this case, the resulting partitions are always geometrically disjoint, each one is owned by a single region according to the current overlap resolution strategy, but every claimant is recorded. If _n_ is 1 or "rebuild_fastgen", then *nirt* takes on _FASTGEN_ behavior, so overlaps of plate-mode primitives are rebuilt, but other overlaps are retained. This command is useful with the __claimant_count__, __claimant_list__, and __claimant_listn__ output items. Default is "resolve".
@@ -197,7 +201,11 @@ Quits *nirt*.
[[output_statements]]
== OUTPUT STATEMENTS
*nirt* allows the user a high degree of control, via the _fmt_ command, over what information gets printed out for each ray and in what format. There are six types of output statements, each of which is executed under appropriate circumstances. The types and their use are:
*nirt* allows the user a high degree of control, via the _fmt_ command, over what information gets printed out for each ray and in what format. There are eight types of output statements, each of which is executed under appropriate circumstances. The types and their use are:
*a*::
Annotation. Executed once for each model-space annotation crossing when *useannot* is enabled. Crossings are reported in ray-distance order after the ordinary solid shotline output. The default format is empty.
*r*::
@@ -278,6 +286,41 @@ azimuth of view (i.e., of ray direction).
elevation of view (i.e., of ray direction).
Annotation Information
*annot_path*::
full database path of the annotation primitive.
*annot_segment*::
zero-based annotation segment index.
*annot_role*::
semantic role of the annotation segment, such as `text`, `leader`, `dimension`, or `arrowhead`.
*annot_x*::
_x_ coordinate of the annotation hit point.
*annot_y*::
_y_ coordinate of the annotation hit point.
*annot_z*::
_z_ coordinate of the annotation hit point.
*annot_d*::
_d_ grid-plane coordinate of the annotation hit point.
*annot_visible*::
1 if no non-air solid surface is closer to the ray origin, otherwise 0. Occluded annotation crossings are still reported so they can be inspected during debugging.
Partition Information
@@ -521,6 +564,15 @@ nirt -A material_id -A los_density model.g all
Each region the ray passes through is reported together with its _material_id_ and _los_density_ attribute values via the *attributes* output item, which is useful for vulnerability and mass-properties analyses.
====
.Reporting annotation crossings for debugging
====
[source,bash]
----
nirt -s -e 'useannot 1;fmt a "%s %d (%g %g %g) visible=%d\n" annot_path annot_segment annot_x annot_y annot_z annot_visible;s' model.g component dimensions
----
This opt-in format reports every model-space annotation crossing and whether it is in front of the first solid surface. Ordinary NIRT output is unchanged unless *useannot* is enabled and an *a* format is supplied.
====
[[hints]]
== HINTS
+32 -2
View File
@@ -25,7 +25,7 @@ where the "char" argument is replaced by a single character identifying the cate
[[nirt_format_categories]]
== Output Categories
NIRT offers seven categories of output reporting:
NIRT offers eight categories of output reporting:
[cols="3*"]
[%noheader]
@@ -33,6 +33,9 @@ NIRT offers seven categories of output reporting:
|*Category Name*
|*Char*
|*Description*
|*Annotation*
|*a*
|Output for each model-space annotation crossing when annotation reporting is enabled with *useannot 1*. The default annotation format is empty.
|*Ray*
|*r*
|Report of ray information. The formatting associated with r will print once per ray, regardless of whether a region is encountered.
@@ -62,7 +65,7 @@ NIRT offers seven categories of output reporting:
[[nirt_format_str]]
== Format Strings
NIRT's format specifications are a subset of those found in printf implementations. Specifically, the d, i, and f types are supported, as well as width and precision specifiers.
NIRT's format specifications are a subset of those found in printf implementations. The d, i, f, e, E, g, G, and s types are supported, as well as width and precision specifiers.
[[nirt_format_keys]]
@@ -100,6 +103,33 @@ Unlike the full C/C++ printf, NIRT accepts only pre-defined string keys that ide
Footnote and Miss categories have no particular keys unique to them, although they (like all categories) can use the Ray keys if desired. Partition, Overlap, and Gap, on the other hand, have keys that are unique to their contexts and valid only within those contexts:
.Annotation Variables
[cols="2*"]
[%noheader]
|===
|annot_path
|Full database path of the annotation primitive.
|annot_segment
|Zero-based annotation segment index.
|annot_role
|Semantic role of the annotation segment, such as `text`, `leader`, `dimension`, or `arrowhead`.
|annot_x
|x coordinate of the annotation hit point.
|annot_y
|y coordinate of the annotation hit point.
|annot_z
|z coordinate of the annotation hit point.
|annot_d
|d grid-plane coordinate of the annotation hit point.
|annot_visible
|1 if no non-air solid surface is closer to the ray origin, otherwise 0.
|===
Annotation reporting is diagnostic and opt-in. It returns all model-space
annotation crossings in ray-distance order, including occluded crossings.
Screen-space annotations are not reported because a NIRT shot does not define
the pixel dimensions and model-to-view transform needed to evaluate them.
.Partition Variables
[cols="2*"]
[%noheader]
+8 -9
View File
@@ -40,12 +40,11 @@ Coordinates and explicit sizes use the database's current modeling units.
Creation commands draw the new annotation immediately. Use *--no-draw* to
create it without changing the display. Names must not already exist.
Model-space annotations also participate in raytracing. Librt expands their
strokes and filled areas into transient mesh geometry during preparation; the
generated geometry is not written to the database. It uses the normal region
material and color resolved from the containing combination tree. Screen-space
annotations remain display-only because primitive preparation has no render
view or image dimensions with which to place them.
Ray-traced annotations are zero-thickness informational coverage rather than
solid regions. Model-space annotations are depth-tested against ordinary
geometry. Screen-space annotations are generated for the current camera and
composited in the foreground; they do not participate in shadows, reflections,
line-of-sight thickness, or Boolean evaluation.
The supported dimensions follow the general presentation conventions of ISO
129-1. Lettering defaults to OSIFont, an ISO 3098-style technical font shipped
@@ -209,9 +208,9 @@ Set the persistent annotation color using integer red, green, and blue values
from 0 through 255.
*--line-width width*::
Set the display line width. For a model-space raytrace, this value scales the
physical stroke width inferred from the annotation's text height or overall
extent.
Set the presentation line width. Display and screen-space ray rendering use
the value as a pixel width. Model-space ray rendering uses it to scale the
automatic stroke width derived from the annotation's text height or extent.
*--line-style style*::
Set semantic line-pattern metadata to *continuous*, *dashed*, *dotted*,
+17
View File
@@ -120,6 +120,23 @@ BG_EXPORT extern int bg_trimesh_unmatched_edges(int num_edges, struct bg_trimesh
BG_EXPORT extern int bg_trimesh_misoriented_edges(int num_edges, struct bg_trimesh_halfedge *edge_list, bg_edge_error_funct_t error_edge_func, void *data);
BG_EXPORT extern int bg_trimesh_excess_edges(int num_edges, struct bg_trimesh_halfedge *edge_list, bg_edge_error_funct_t error_edge_func, void *data);
BG_EXPORT extern int bg_trimesh_solid2(int vcnt, int fcnt, fastf_t *v, int *f, struct bg_trimesh_solid_errors *errors);
/**
* Report geometric intersections between faces of one triangle mesh.
* Contact confined to a shared indexed vertex or edge is allowed; overlap
* beyond that shared feature and contact between unconnected faces count as
* self intersections. This test does not check manifoldness or closure.
*
* @param[in] faces flat array of three vertex indices per triangle
* @param[in] num_faces number of triangles
* @param[in] points array of vertex positions
* @param[in] num_points number of vertices
*
* @return 1 if self intersecting, 0 if not, -1 for malformed input
* (invalid indices, non-finite vertices, or degenerate faces) or a failed check.
*/
BG_EXPORT extern int bg_trimesh_self_isect(const int *faces, size_t num_faces,
const point_t *points, size_t num_points);
BG_EXPORT extern int bg_trimesh_hanging_nodes(int num_vertices, int num_faces, fastf_t *vertices, int *faces, struct bg_trimesh_solid_errors *errors);
BG_EXPORT extern struct bg_trimesh_halfedge * bg_trimesh_generate_edge_list(int fcnt, int *f);
+4 -1
View File
@@ -31,7 +31,10 @@
__BEGIN_DECLS
struct rt_comb_internal; /* forward declaration */
struct bu_external;
struct db_i;
struct directory;
struct rt_comb_internal;
/* db5_attr.c */
+46
View File
@@ -227,6 +227,32 @@ struct rt_crofton_params {
double time_ms; /**< wall-clock time budget (ms); 0 = disabled */
};
/** One solid partition observed by a Crofton ray. */
struct rt_crofton_segment {
point_t in_point;
vect_t in_normal;
point_t out_point;
vect_t out_normal;
fastf_t thickness;
size_t ray_id;
};
/**
* Structured samples and estimates produced by rt_crofton_collect().
* Release @p segments with rt_crofton_result_free().
*/
struct rt_crofton_result {
struct rt_crofton_segment *segments;
size_t segment_count;
size_t ray_count;
size_t crossing_count;
double surface_area;
double volume;
};
/** Initialize an rt_crofton_result before first use. */
#define RT_CROFTON_RESULT_INIT { NULL, 0, 0, 0, 0.0, 0.0 }
/**
* Run the Cauchy-Crofton ray-sampling estimator on an already-prepared
@@ -337,6 +363,26 @@ struct rt_crofton_params {
*/
RT_EXPORT extern int rt_crofton_shoot(double *out_surf_area, double *out_volume, point_t *out_aabb_min, point_t *out_aabb_max, point_t out_obb[8], point_t **out_points, size_t *out_point_count, struct rt_i *rtip, const struct rt_crofton_params *params, const fastf_t *bbox_min, const fastf_t *bbox_max);
/**
* Run Crofton sampling while preserving oriented entry/exit pairs.
*
* @p result must be initialized with RT_CROFTON_RESULT_INIT. On success it
* owns the returned segment array. @p rtip must already be prepared.
* @p ray_offset skips that many samples in the deterministic sequence and
* labels the returned rays starting at that value. This lets successive
* calls use disjoint validation streams.
*/
RT_EXPORT extern int rt_crofton_collect(struct rt_crofton_result *result,
struct rt_i *rtip,
const struct rt_crofton_params *params,
size_t ray_offset,
const fastf_t *bbox_min,
const fastf_t *bbox_max);
/** Release storage owned by a structured Crofton result. */
RT_EXPORT extern void rt_crofton_result_free(
struct rt_crofton_result *result);
/**
* Cauchy-Crofton surface-area and/or volume estimator for a primitive.
+85
View File
@@ -29,10 +29,19 @@
#include "bu/list.h"
#include "bu/vls.h"
#include "bn/tol.h"
#include "rt/geom.h"
#include "rt/defines.h"
__BEGIN_DECLS
struct db_i;
struct xray;
#define RT_ANNOT_SCREEN_DPI 96.0
#define RT_ANNOT_MILLIMETERS_PER_INCH 25.4
#define RT_ANNOT_SCREEN_PIXELS_PER_MM \
(RT_ANNOT_SCREEN_DPI / RT_ANNOT_MILLIMETERS_PER_INCH)
RT_EXPORT extern struct rt_annot_internal *rt_copy_annot(const struct rt_annot_internal *annot_ip);
/** Validate annotation topology, model-space placement, and optional segment
@@ -40,6 +49,82 @@ RT_EXPORT extern struct rt_annot_internal *rt_copy_annot(const struct rt_annot_i
RT_EXPORT extern int rt_annot_validate(const struct rt_annot_internal *annot_ip,
struct bu_vls *messages);
/** Opaque, immutable collection of prepared annotation instances. */
struct rt_annot_scene;
/** Camera state needed to evaluate view-space annotations. Pixel sample
* coordinates use a lower-left origin. */
struct rt_annot_view {
mat_t model2view;
size_t width;
size_t height;
fastf_t perspective;
};
/** A zero-thickness annotation coverage event. path is owned by the scene. */
struct rt_annot_hit {
const char *path;
size_t segment;
uint32_t role;
point_t point;
fastf_t distance;
unsigned char color[4];
int screen_space;
int visible;
};
/** Prepare annotations referenced by the supplied top-level database paths.
* Boolean operations are intentionally ignored: annotations are
* informational leaves, not solids. */
RT_EXPORT extern struct rt_annot_scene *rt_annot_scene_create(
struct db_i *dbip, int path_count, const char * const *paths,
const struct bg_tess_tol *ttol, const struct bn_tol *tol);
RT_EXPORT extern void rt_annot_scene_destroy(struct rt_annot_scene *scene);
RT_EXPORT extern size_t rt_annot_scene_count(const struct rt_annot_scene *scene);
/** Return model-space bounds for prepared annotations. Screen-space geometry
* contributes its model-space anchor because its extent is view-dependent. */
RT_EXPORT extern int rt_annot_scene_bounds(const struct rt_annot_scene *scene,
point_t minimum, point_t maximum);
/** Return the top visible coverage event for a primary sample. */
RT_EXPORT extern int rt_annot_scene_query(const struct rt_annot_scene *scene,
const struct rt_annot_view *view, const struct xray *ray,
fastf_t sample_x, fastf_t sample_y, fastf_t scene_distance,
struct rt_annot_hit *hit);
/** Return all visible primary-sample coverage events in back-to-front order.
* Model-space events precede screen-space events. When hits is NULL or
* capacity is zero, only the required result count is returned. */
RT_EXPORT extern size_t rt_annot_scene_query_layers(
const struct rt_annot_scene *scene, const struct rt_annot_view *view,
const struct xray *ray, fastf_t sample_x, fastf_t sample_y,
fastf_t scene_distance, struct rt_annot_hit *hits, size_t capacity);
/** Return all model-space coverage events, sorted by ray distance. When hits
* is NULL or capacity is zero, only the required result count is returned. */
RT_EXPORT extern size_t rt_annot_scene_query_model(const struct rt_annot_scene *scene,
const struct xray *ray, fastf_t scene_distance,
struct rt_annot_hit *hits, size_t capacity);
/** Alpha-composite a coverage result over a linear RGB sample. */
RT_EXPORT extern void rt_annot_hit_blend(fastf_t color[3],
const struct rt_annot_hit *hit);
/** Alpha-composite all visible primary-sample coverage over color. If
* front_hit is non-NULL, it receives the frontmost event. */
RT_EXPORT extern int rt_annot_scene_composite(const struct rt_annot_scene *scene,
const struct rt_annot_view *view, const struct xray *ray,
fastf_t sample_x, fastf_t sample_y, fastf_t scene_distance,
fastf_t color[3], struct rt_annot_hit *front_hit);
/** Generate one annotation segment in annotation-local XY coordinates. This
* is shared by plotting and the coverage preparer to keep curve and font
* expansion identical. */
RT_EXPORT extern int rt_annot_segment_vlist(struct bu_list *vlfree,
struct bu_list *vhead, const struct bg_tess_tol *ttol,
const struct rt_annot_internal *annot_ip, size_t segment);
__END_DECLS
/** @} */
+3
View File
@@ -55,11 +55,14 @@ struct clt_tri_specific {
};
#endif
#define RT_BOT_SURFNO_INTERIOR (-1)
/* Shared between bot and ars at the moment */
struct bot_specific {
unsigned char bot_mode;
unsigned char bot_orientation;
unsigned char bot_flags;
unsigned char bot_parity_shot;
size_t bot_ntri;
fastf_t *bot_thickness;
struct bu_bitv *bot_facemode;
+7 -5
View File
@@ -119,14 +119,16 @@ mark_as_advanced(SYS_INCLUDE_PATTERNS)
# ${BRLCAD_EXT_DIR}/install copies, so wrap logic to do so into a
# function.
function(strip_excluded RDIR EXPATTERNS)
file(GLOB_RECURSE staged_files LIST_DIRECTORIES false RELATIVE "${RDIR}" "${RDIR}/*")
foreach(ep ${${EXPATTERNS}})
file(GLOB_RECURSE MATCHING_FILES LIST_DIRECTORIES false RELATIVE "${RDIR}" "${RDIR}/${ep}")
foreach(rf ${MATCHING_FILES})
set(matching_files ${staged_files})
list(FILTER matching_files INCLUDE REGEX "${ep}")
foreach(rf ${matching_files})
file(REMOVE "${RDIR}/${rf}")
if(EXISTS ${RDIR}/${rf})
if(EXISTS "${RDIR}/${rf}")
message(FATAL_ERROR "Removing ${RDIR}/${rf} failed")
endif(EXISTS ${RDIR}/${rf})
endforeach(rf ${MATCHING_FILES})
endif(EXISTS "${RDIR}/${rf}")
endforeach(rf ${matching_files})
endforeach(ep ${${EXPATTERNS}})
endfunction(strip_excluded)
+36 -27
View File
@@ -169,23 +169,26 @@ function(BRLCAD_ADD_TEST NAME test_name COMMAND test_prog)
add_dependencies(check ${_test_dependencies})
endfunction(BRLCAD_ADD_TEST)
# Register a test whose command needs a private X server. Keeping Xmin's
# process setup here lets GUI fixtures concentrate on application behavior and
# gives every caller the same display geometry, timeout, and dependency
# handling. Tests are registered only when the build selected Xmin as its X11
# provider.
function(BRLCAD_ADD_XMIN_TEST)
if(NOT BUILD_TESTING OR NOT BRLCAD_X11_PROVIDER_RESOLVED STREQUAL "XMIN")
# Register a GUI test using the selected display backend. Xmin is currently
# the only backend; its server and control paths stay here so fixtures only
# depend on the GUI test controller contract.
function(BRLCAD_ADD_GUI_TEST)
if(NOT BUILD_TESTING)
return()
endif()
cmake_parse_arguments(PARSE_ARGV 0 XMIN "" "NAME;SCREEN;TIMEOUT"
"COMMAND;ENVIRONMENT;SERVER_ARGS")
if(NOT XMIN_NAME)
message(FATAL_ERROR "BRLCAD_ADD_XMIN_TEST requires NAME")
if(BRLCAD_X11_PROVIDER_RESOLVED STREQUAL "XMIN")
_brlcad_add_xmin_gui_test(${ARGV})
endif()
if(NOT XMIN_COMMAND)
message(FATAL_ERROR "BRLCAD_ADD_XMIN_TEST requires COMMAND")
endfunction()
function(_brlcad_add_xmin_gui_test)
cmake_parse_arguments(PARSE_ARGV 0 GUI "" "NAME;SCREEN;TIMEOUT"
"COMMAND;ENVIRONMENT;SERVER_ARGS")
if(NOT GUI_NAME)
message(FATAL_ERROR "BRLCAD_ADD_GUI_TEST requires NAME")
endif()
if(NOT GUI_COMMAND)
message(FATAL_ERROR "BRLCAD_ADD_GUI_TEST requires COMMAND")
endif()
set(_xmin_server "${Xmin_SERVER_EXECUTABLE}")
@@ -199,36 +202,42 @@ function(BRLCAD_ADD_XMIN_TEST)
find_program(_xmin_run NAMES xmin-run HINTS "${_xmin_bin_dir}" NO_DEFAULT_PATH NO_CACHE)
endif()
if(NOT _xmin_server OR NOT _xmin_run)
message(WARNING "Skipping ${XMIN_NAME}: the selected Xmin SDK does not provide its test runner")
message(WARNING "Skipping ${GUI_NAME}: the selected Xmin SDK does not provide its test runner")
return()
endif()
if(NOT XMIN_SCREEN)
set(XMIN_SCREEN "1280x960x24")
if(NOT GUI_SCREEN)
set(GUI_SCREEN "1280x960x24")
endif()
if(NOT _xmin_bin_dir)
get_filename_component(_xmin_bin_dir "${_xmin_server}" DIRECTORY)
endif()
find_program(_xmin_ctl NAMES xminctl HINTS "${_xmin_bin_dir}"
NO_DEFAULT_PATH NO_CACHE)
set(_xmin_environment "XMIN_SCREEN=${XMIN_SCREEN}" ${XMIN_ENVIRONMENT})
set(_gui_environment
"GUI_TEST_SCREEN=${GUI_SCREEN}"
"XMIN_SCREEN=${GUI_SCREEN}"
${GUI_ENVIRONMENT}
)
if(_xmin_ctl)
list(PREPEND _xmin_environment "XMIN_CTL=${_xmin_ctl}")
list(PREPEND _gui_environment
"GUI_TEST_CTL=${CMAKE_SOURCE_DIR}/regress/xmin/xmin_gui_ctl.sh"
"XMIN_CTL=${_xmin_ctl}"
)
endif()
set(_xmin_command
"${CMAKE_COMMAND}" -E env ${_xmin_environment} ${XMIN_COMMAND}
set(_gui_command
"${CMAKE_COMMAND}" -E env ${_gui_environment} ${GUI_COMMAND}
)
brlcad_add_test(
NAME ${XMIN_NAME}
NAME ${GUI_NAME}
COMMAND "${_xmin_run}" --server "${_xmin_server}"
--screen "${XMIN_SCREEN}" ${XMIN_SERVER_ARGS} -- ${_xmin_command}
--screen "${GUI_SCREEN}" ${GUI_SERVER_ARGS} -- ${_gui_command}
)
if(XMIN_TIMEOUT)
set_tests_properties(${XMIN_NAME} PROPERTIES TIMEOUT "${XMIN_TIMEOUT}")
if(GUI_TIMEOUT)
set_tests_properties(${GUI_NAME} PROPERTIES TIMEOUT "${GUI_TIMEOUT}")
endif()
endfunction(BRLCAD_ADD_XMIN_TEST)
endfunction()
# Local Variables:
# mode: cmake
+1
View File
@@ -1,5 +1,6 @@
# default.nrt
# Description: default - Standard NIRT Reporting Format
fmt a ""
fmt r "Origin (x y z) = (%.8f %.8f %.8f) (h v d) = (%.4f %.4f %.4f)\nDirection (x y z) = (%.8f %.8f %.8f) (az el) = (%.8f %.8f)\n" x_orig y_orig z_orig h v d_orig x_dir y_dir z_dir a e
fmt h " Region Name Entry (x y z) LOS Obliq_in Attrib\n"
fmt p "%-20s (%9.4f %9.4f %9.4f) %8.4f %8.4f%s\n" reg_name x_in y_in z_in los obliq_in attributes
+2 -1
View File
@@ -110,7 +110,8 @@ add_subdirectory(rtedge)
# RtWizard Image Generation Regression Tests
add_subdirectory(rtwizard)
# Shared Xmin GUI test support
# Shared GUI test support and Xmin fixtures
add_subdirectory(gui)
add_subdirectory(xmin)
# per-primitive validity
+17
View File
@@ -0,0 +1,17 @@
cmakefiles(
CMakeLists.txt
README.md
gui_test.sh
gui_smoke.sh
gui_tcl_test.sh
gui_test.tcl
gui_test_core.tcl
sketch_test.tcl
)
# Local Variables:
# mode: cmake
# tab-width: 2
# indent-tabs-mode: nil
# End:
# ex: shiftwidth=2 tabstop=8
+39
View File
@@ -0,0 +1,39 @@
# GUI regression support
The shared support has three layers:
1. `brlcad_add_gui_test` registers a test and delegates session setup to the
selected backend.
2. `GUI_TEST_CTL` is the window-control interface supplied by the backend.
3. `gui_test.sh`, `gui_smoke.sh`, `gui_tcl_test.sh`, and `gui_test.tcl` provide
shell and Tcl helpers for application lifecycle, artifacts, window input,
screen comparisons, widgets, menus, and asynchronous Tcl errors.
Application fixtures own their semantic oracles. `sketch_test.tcl` contains
shared sketch assertions used by Archer and MGED. The core helpers do not
encode application menus, object models, or screen layouts.
## Controller contract
The controller takes one command followed by arguments:
| Command | Result |
| --- | --- |
| `wait-window --timeout milliseconds title` | Return an opaque window token. |
| `geometry token` | Print `x y width height`. |
| `activate token`, `close token` | Control the identified window. |
| `click [--delay milliseconds] token x y button`, `mouse-move token x y`, `button button down|up` | Send pointer input. |
| `key name`, `type text` | Send keyboard input. |
| `wait-stable --quiet milliseconds --timeout milliseconds token` | Wait for visual quiescence. |
| `capture-screen path` | Write a binary PPM of the composed screen. |
| `screen-geometry` | Print `width height`. |
`root` denotes screen coordinates. Tk fixtures publish `winfo id` values as
window tokens and widget-local points as coordinates. The backend resolves
the tokens; fixtures do not interpret their representation. Capture and
screen geometry must describe the same screen so root-coordinate cropping is
consistent.
The only implemented backend is Xmin. See [its fixtures](../xmin/README.md)
for setup and test coverage. A native backend will also need a CMake session
provider and working display/input permissions on its runner.
+88
View File
@@ -0,0 +1,88 @@
#!/bin/sh
set -eu
: "${GUI_TEST_SHELL:?GUI_TEST_SHELL is required}"
. "$GUI_TEST_SHELL"
gui_stop_app()
{
if [ -n "${GUI_APP_PID:-}" ] && kill -0 "$GUI_APP_PID" 2>/dev/null; then
kill "$GUI_APP_PID" >/dev/null 2>&1 || true
fi
}
if [ "$#" -lt 2 ]; then
echo "Usage: $0 expected-window executable [argument ...]" >&2
exit 2
fi
: "${GUI_TEST_CTL:?GUI_TEST_CTL is required}"
expected_window=$1
shift
application=$1
shift
gui_require_executable "$GUI_TEST_CTL"
gui_require_executable "$application"
GUI_ARTIFACT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/brlcad-gui-smoke.XXXXXX")
export GUI_ARTIFACT_DIR
GUI_APP_LOG="$GUI_ARTIFACT_DIR/application.log"
export GUI_APP_LOG
trap gui_stop_app EXIT INT TERM
"$application" "$@" >"$GUI_APP_LOG" 2>&1 &
GUI_APP_PID=$!
export GUI_APP_PID
if ! window_id=$("$GUI_TEST_CTL" wait-window --timeout "$GUI_WAIT_TIMEOUT_MS" \
"$expected_window"); then
gui_fail "window did not appear: $expected_window"
fi
if ! kill -0 "$GUI_APP_PID" 2>/dev/null; then
"$GUI_TEST_CTL" wait-stable --quiet "$GUI_STABLE_QUIET_MS" \
--timeout "$GUI_STABLE_TIMEOUT_MS" "$window_id" >/dev/null 2>&1 || true
gui_fail "application exited before its window could be tested"
fi
"$GUI_TEST_CTL" activate "$window_id"
if ! geometry=$(gui_wait_for_minimum_geometry "$window_id" \
"$GUI_MIN_WINDOW_WIDTH" "$GUI_MIN_WINDOW_HEIGHT"); then
gui_fail "window did not reach a usable geometry: $expected_window ($geometry)"
fi
gui_capture_screen "$window_id" "$GUI_ARTIFACT_DIR/before.ppm"
if [ -n "${GUI_SMOKE_ACTION:-}" ]; then
gui_require_executable "$GUI_SMOKE_ACTION"
"$GUI_SMOKE_ACTION" "$window_id" "$GUI_ARTIFACT_DIR"
if [ "${GUI_MIN_CHANGED_BYTES:-0}" -gt 0 ]; then
gui_wait_for_image_difference "$GUI_ARTIFACT_DIR/before.ppm" \
"$GUI_ARTIFACT_DIR/after.ppm" "$GUI_MIN_CHANGED_BYTES" \
"$window_id"
else
gui_capture_screen "$window_id" "$GUI_ARTIFACT_DIR/after.ppm"
fi
fi
"$GUI_TEST_CTL" close "$window_id"
wait "$GUI_APP_PID"
GUI_APP_PID=
if [ "${GUI_KEEP_ARTIFACTS:-0}" -eq 1 ]; then
echo "GUI smoke artifacts: $GUI_ARTIFACT_DIR"
else
"${CMAKE_COMMAND:-cmake}" -E remove_directory "$GUI_ARTIFACT_DIR"
fi
echo "PASS: $expected_window launched, rendered, and closed"
# Local Variables:
# tab-width: 8
# mode: sh
# sh-indentation: 4
# sh-basic-offset: 4
# indent-tabs-mode: t
# End:
# ex: shiftwidth=4 tabstop=8 cino=N-s
+60
View File
@@ -0,0 +1,60 @@
#!/bin/sh
set -eu
: "${GUI_TEST_SHELL:?GUI_TEST_SHELL is required}"
. "$GUI_TEST_SHELL"
if [ "$#" -lt 2 ]; then
echo "Usage: $0 test-name executable [argument ...]" >&2
exit 2
fi
test_name=$1
shift
test_program=$1
gui_require_executable "$test_program"
GUI_TEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/brlcad-gui-${test_name}.XXXXXX")
export GUI_TEST_DIR
gui_tcl_test_cleanup()
{
if [ "${GUI_KEEP_ARTIFACTS:-0}" -eq 0 ]; then
"${CMAKE_COMMAND:-cmake}" -E remove_directory "$GUI_TEST_DIR"
fi
}
trap 'exit 1' INT TERM
if ! "$@"; then
echo "GUI test artifacts: $GUI_TEST_DIR" >&2
exit 1
fi
if [ -s "$GUI_TEST_DIR/result" ]; then
test_result=$(sed -n '1p' "$GUI_TEST_DIR/result")
case "$test_result" in
PASS:*) ;;
*)
echo "$test_result" >&2
echo "GUI test artifacts: $GUI_TEST_DIR" >&2
exit 1
;;
esac
fi
if [ "${GUI_KEEP_ARTIFACTS:-0}" -eq 1 ]; then
echo "GUI test artifacts: $GUI_TEST_DIR"
else
gui_tcl_test_cleanup
fi
# Local Variables:
# tab-width: 8
# mode: sh
# sh-indentation: 4
# sh-basic-offset: 4
# indent-tabs-mode: t
# End:
# ex: shiftwidth=4 tabstop=8 cino=N-s
+156
View File
@@ -0,0 +1,156 @@
#!/bin/sh
GUI_WAIT_TIMEOUT_MS=${GUI_WAIT_TIMEOUT_MS:-30000}
GUI_STABLE_QUIET_MS=${GUI_STABLE_QUIET_MS:-200}
GUI_STABLE_TIMEOUT_MS=${GUI_STABLE_TIMEOUT_MS:-5000}
GUI_POLL_SECONDS=${GUI_POLL_SECONDS:-0.05}
GUI_MIN_WINDOW_WIDTH=${GUI_MIN_WINDOW_WIDTH:-200}
GUI_MIN_WINDOW_HEIGHT=${GUI_MIN_WINDOW_HEIGHT:-150}
gui_fail()
{
echo "FAIL: $1" >&2
if [ -n "${GUI_APP_LOG:-}" ] && [ -f "$GUI_APP_LOG" ]; then
sed -n '1,240p' "$GUI_APP_LOG" >&2
fi
exit 1
}
gui_require_executable()
{
if [ ! -x "$1" ]; then
gui_fail "required executable is unavailable: $1"
fi
}
gui_wait_for_minimum_geometry()
{
window=$1
minimum_width=$2
minimum_height=$3
timeout_seconds=$(((GUI_WAIT_TIMEOUT_MS + 999) / 1000))
start_time=$(date +%s)
while :; do
geometry=$("$GUI_TEST_CTL" geometry "$window") || return 1
set -- $geometry
if [ "$#" -ge 4 ]; then
width=$3
height=$4
if [ "$width" -ge "$minimum_width" ] &&
[ "$height" -ge "$minimum_height" ]; then
echo "$geometry"
return 0
fi
fi
current_time=$(date +%s)
if [ $((current_time - start_time)) -ge "$timeout_seconds" ]; then
echo "$geometry"
return 1
fi
sleep "$GUI_POLL_SECONDS"
done
}
gui_capture_screen()
{
window=$1
output=$2
"$GUI_TEST_CTL" wait-stable --quiet "$GUI_STABLE_QUIET_MS" \
--timeout "$GUI_STABLE_TIMEOUT_MS" "$window" >/dev/null 2>&1 || true
# Tk and Qt compose child windows into the root; a top-level-only capture
# can therefore contain just the parent's cleared backing surface.
"$GUI_TEST_CTL" capture-screen "$output" ||
gui_fail "could not capture the GUI test screen"
if [ "$(sed -n '1p' "$output")" != "P6" ]; then
gui_fail "screen capture is not a binary PPM image: $output"
fi
nonzero_bytes=$(LC_ALL=C tr -d '\000' < "$output" | wc -c | tr -d ' ')
if [ "$nonzero_bytes" -lt 1024 ]; then
gui_fail "screen capture contains no meaningful rendered content"
fi
}
gui_count_changed_bytes()
{
first=$1
second=$2
minimum=$3
# Stop once the threshold is reached. Full root captures are large, and
# their exact total difference is immaterial to this binary assertion.
changed_bytes=$(cmp -l "$first" "$second" | awk -v minimum="$minimum" \
'NR >= minimum { print NR; exit }
END { if (NR < minimum) print NR }')
echo "$changed_bytes"
}
gui_wait_for_image_difference()
{
first=$1
second=$2
minimum=$3
window=$4
timeout_seconds=$(((GUI_WAIT_TIMEOUT_MS + 999) / 1000))
start_time=$(date +%s)
if [ ! -s "$first" ]; then
gui_fail "cannot compare a missing or empty GUI capture: $first"
fi
while :; do
gui_capture_screen "$window" "$second"
changed_bytes=$(gui_count_changed_bytes "$first" "$second" "$minimum")
if [ "$changed_bytes" -ge "$minimum" ]; then
return 0
fi
current_time=$(date +%s)
if [ $((current_time - start_time)) -ge "$timeout_seconds" ]; then
gui_fail "GUI captures changed by only $changed_bytes bytes; expected at least $minimum"
fi
sleep "$GUI_POLL_SECONDS"
done
}
# Capture a root-coordinate region of the screen as PPM. The conversion
# commands preserve the image comparison format used by the MGED fixture.
gui_capture_region()
(
output=$1
x=$2
y=$3
width=$4
height=$5
root_capture=${output%.ppm}-root.ppm
root_pixels=${output%.ppm}-root.pix
cropped_pixels=${output%.ppm}-cropped.pix
geometry=$("$GUI_TEST_CTL" screen-geometry) || exit 1
set -- $geometry
screen_width=$1
screen_height=$2
lower_y=$((screen_height - y - height))
upper_y=$((lower_y + height - 1))
right_x=$((x + width - 1))
"$GUI_TEST_CTL" wait-stable --quiet "$GUI_STABLE_QUIET_MS" \
--timeout "$GUI_STABLE_TIMEOUT_MS" root >/dev/null 2>&1 || true
"$GUI_TEST_CTL" capture-screen "$root_capture" || exit 1
"$ICV_BIN" "$root_capture" "$root_pixels" >/dev/null 2>&1 || exit 1
"$PIXCROP_BIN" "$root_pixels" "$cropped_pixels" \
"$screen_width" "$width" "$height" \
"$x" "$upper_y" "$right_x" "$upper_y" \
"$right_x" "$lower_y" "$x" "$lower_y" >/dev/null 2>&1 || exit 1
"$ICV_BIN" -w "$width" -n "$height" \
"$cropped_pixels" "$output" >/dev/null 2>&1 || exit 1
rm -f "$root_capture" "$root_pixels" "$cropped_pixels"
)
# Local Variables:
# tab-width: 8
# mode: sh
# sh-indentation: 4
# sh-basic-offset: 4
# indent-tabs-mode: t
# End:
# ex: shiftwidth=4 tabstop=8 cino=N-s
@@ -1,4 +1,4 @@
# X M I N _ G U I _ T E S T . T C L
# G U I _ T E S T . T C L
# BRL-CAD
#
# Copyright (c) 2026 United States Government as represented by
@@ -18,14 +18,14 @@
# information.
#
###
source [file join [file dirname [info script]] xmin_test.tcl]
source [file join [file dirname [info script]] gui_test_core.tcl]
namespace eval ::xmin::test {
namespace eval ::gui::test {
variable background_errors {}
variable numeric_tolerance 1.0e-12
}
proc ::xmin::test::record_background_error {message} {
proc ::gui::test::record_background_error {message} {
variable background_errors
set error_info ""
set error_code ""
@@ -38,15 +38,15 @@ proc ::xmin::test::record_background_error {message} {
lappend background_errors [list $message $error_info $error_code]
}
proc ::xmin::test::capture_background_errors {} {
proc ::gui::test::capture_background_errors {} {
variable background_errors
set background_errors {}
proc ::bgerror {message} {
::xmin::test::record_background_error $message
::gui::test::record_background_error $message
}
}
proc ::xmin::test::require_no_background_errors {} {
proc ::gui::test::require_no_background_errors {} {
variable background_errors
if {[llength $background_errors] == 0} {
return
@@ -66,7 +66,7 @@ proc ::xmin::test::require_no_background_errors {} {
error "background Tcl error(s):\n[join $report \n]"
}
proc ::xmin::test::check_background_errors {status message} {
proc ::gui::test::check_background_errors {status message} {
if {$status == 0 &&
[catch {require_no_background_errors} background_error]} {
return [list 1 "FAIL: $background_error"]
@@ -74,13 +74,13 @@ proc ::xmin::test::check_background_errors {status message} {
return [list $status $message]
}
proc ::xmin::test::require {condition message} {
proc ::gui::test::require {condition message} {
if {![uplevel 1 [list expr $condition]]} {
error $message
}
}
proc ::xmin::test::equivalent_values {first second} {
proc ::gui::test::equivalent_values {first second} {
variable numeric_tolerance
if {$first eq $second} {
return 1
@@ -103,7 +103,7 @@ proc ::xmin::test::equivalent_values {first second} {
return 1
}
proc ::xmin::test::widget_inventory {root} {
proc ::gui::test::widget_inventory {root} {
set counts {}
foreach widget [linsert [descendants $root] 0 $root] {
set class [winfo class $widget]
@@ -118,7 +118,7 @@ proc ::xmin::test::widget_inventory {root} {
return [join [lsort -dictionary $inventory] "\n"]
}
proc ::xmin::test::menu_inventory_walk {menu path inventory_name visited_name} {
proc ::gui::test::menu_inventory_walk {menu path inventory_name visited_name} {
upvar 1 $inventory_name inventory $visited_name visited
if {[dict exists $visited $menu]} {
return
@@ -147,7 +147,7 @@ proc ::xmin::test::menu_inventory_walk {menu path inventory_name visited_name} {
}
}
proc ::xmin::test::menu_roots {root} {
proc ::gui::test::menu_roots {root} {
set roots {}
if {![catch {set menu [$root cget -menu]}] &&
$menu ne "" && [winfo exists $menu]} {
@@ -177,7 +177,7 @@ proc ::xmin::test::menu_roots {root} {
return $roots
}
proc ::xmin::test::menu_inventory {root} {
proc ::gui::test::menu_inventory {root} {
set inventory {}
set visited {}
foreach menu_root [menu_roots $root] {
@@ -191,7 +191,7 @@ proc ::xmin::test::menu_inventory {root} {
return [join [lsort -dictionary -unique $inventory] "\n"]
}
proc ::xmin::test::find_menu_entry {root labels} {
proc ::gui::test::find_menu_entry {root labels} {
set menu ""
set remaining [lrange $labels 1 end]
foreach menu_root [menu_roots $root] {
@@ -237,7 +237,7 @@ proc ::xmin::test::find_menu_entry {root labels} {
return $found
}
proc ::xmin::test::invoke_menu_entry {root labels} {
proc ::gui::test::invoke_menu_entry {root labels} {
set entry [find_menu_entry $root $labels]
if {$entry eq ""} {
error "menu entry not found: [join $labels { > }]"
@@ -249,75 +249,6 @@ proc ::xmin::test::invoke_menu_entry {root labels} {
$menu invoke $index
}
proc ::xmin::test::sketch_editor_stub {command args} {
switch -- $command {
get_scale {
return 1.0
}
get_vlist {
return {{0 0} {10 0} {5 5}}
}
set_radius {
return
}
default {
error "unexpected sketch editor command: $command"
}
}
}
proc ::xmin::test::require_unit_vector {vector description} {
lassign $vector x y
set magnitude [expr {sqrt($x * $x + $y * $y)}]
require {abs($magnitude - 1.0) < 1.0e-9} "$description is not a unit vector: $vector"
}
proc ::xmin::test::exercise_sketch_segments {
arc_class bezier_class application object_namespace
} {
set canvas .xmin_sketch_segments
catch {destroy $canvas}
canvas $canvas
set editor ::xmin::test::sketch_editor_stub
set arc ${object_namespace}::arc_probe
$arc_class $arc $editor $canvas {S 0 E 1 R 10 L 0 O 0}
set start_tangent [$arc get_tangent_at_vertex 0]
set end_tangent [$arc get_tangent_at_vertex 1]
require_unit_vector $start_tangent "$application sketch arc start tangent"
require_unit_vector $end_tangent "$application sketch arc end tangent"
require {[$arc get_tangent_at_vertex 2] eq "0 0"} "$application sketch arc returned a tangent for an unrelated vertex"
$arc draw {}
require {[llength [$canvas find all]] == 1} "$application sketch arc did not draw on its canvas"
$arc reverse_orientation
set reversed_tangent [$arc get_tangent_at_vertex 0]
lassign $start_tangent sx sy
lassign $reversed_tangent rx ry
require {
abs($sx + $rx) < 1.0e-9 && abs($sy + $ry) < 1.0e-9
} "$application sketch arc orientation did not reverse its tangent"
::itcl::delete object $arc
set circle ${object_namespace}::circle_probe
$arc_class $circle $editor $canvas {S 0 E 1 R -1 L 0 O 0}
require_unit_vector [$circle get_tangent_at_vertex 0] "$application full-circle tangent"
require {[$circle get_tangent_at_vertex 1] eq "0 0"} "$application full circle reported a tangent at its center"
::itcl::delete object $circle
set bezier ${object_namespace}::bezier_probe
set item_count [llength [$canvas find all]]
$bezier_class $bezier $editor $canvas {D 2 P {0 2 1}}
$bezier draw {}
require {
[llength [$canvas find all]] == $item_count + 1
} "$application sketch Bezier did not draw on its canvas"
require {
[string match {*bezier D 2 P*} [$bezier serialize 1.0]]
} "$application sketch Bezier did not serialize its control points"
::itcl::delete object $bezier
destroy $canvas
}
# Local Variables:
# mode: Tcl
# tab-width: 8
@@ -1,4 +1,4 @@
# X M I N _ T E S T . T C L
# G U I _ T E S T _ C O R E . T C L
# BRL-CAD
#
# Copyright (c) 2026 United States Government as represented by
@@ -18,15 +18,15 @@
# information.
#
###
namespace eval ::xmin::test {
namespace eval ::gui::test {
variable directory
if {![info exists ::env(XMIN_TEST_DIR)]} {
error "XMIN_TEST_DIR is required"
if {![info exists ::env(GUI_TEST_DIR)]} {
error "GUI_TEST_DIR is required"
}
set directory $::env(XMIN_TEST_DIR)
set directory $::env(GUI_TEST_DIR)
}
proc ::xmin::test::write {name contents} {
proc ::gui::test::write {name contents} {
variable directory
set destination [file join $directory $name]
set temporary ${destination}.tmp
@@ -36,14 +36,14 @@ proc ::xmin::test::write {name contents} {
file rename -force $temporary $destination
}
proc ::xmin::test::publish_target {name widget} {
proc ::gui::test::publish_target {name widget} {
write ${name}_window [winfo id $widget]
write $name [list \
[expr {[winfo width $widget] / 2}] \
[expr {[winfo height $widget] / 2}]]
}
proc ::xmin::test::descendants {widget} {
proc ::gui::test::descendants {widget} {
set descendants {}
foreach child [winfo children $widget] {
lappend descendants $child
@@ -52,7 +52,7 @@ proc ::xmin::test::descendants {widget} {
return $descendants
}
proc ::xmin::test::find_widget {root class text} {
proc ::gui::test::find_widget {root class text} {
foreach widget [descendants $root] {
if {[winfo class $widget] ne $class ||
[catch {set widget_text [$widget cget -text]}] ||
+96
View File
@@ -0,0 +1,96 @@
# S K E T C H _ T E S T . T C L
# BRL-CAD
#
# Copyright (c) 2026 United States Government as represented by
# the U.S. Army Research Laboratory.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# version 2.1 as published by the Free Software Foundation.
#
# This library 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 this file; see the file named COPYING for more
# information.
#
###
# Shared sketch segment checks used by Archer and MGED.
proc ::gui::test::sketch_editor_stub {command args} {
switch -- $command {
get_scale {
return 1.0
}
get_vlist {
return {{0 0} {10 0} {5 5}}
}
set_radius {
return
}
default {
error "unexpected sketch editor command: $command"
}
}
}
proc ::gui::test::require_unit_vector {vector description} {
lassign $vector x y
set magnitude [expr {sqrt($x * $x + $y * $y)}]
require {abs($magnitude - 1.0) < 1.0e-9} "$description is not a unit vector: $vector"
}
proc ::gui::test::exercise_sketch_segments {
arc_class bezier_class application object_namespace
} {
set canvas .gui_sketch_segments
catch {destroy $canvas}
canvas $canvas
set editor ::gui::test::sketch_editor_stub
set arc ${object_namespace}::arc_probe
$arc_class $arc $editor $canvas {S 0 E 1 R 10 L 0 O 0}
set start_tangent [$arc get_tangent_at_vertex 0]
set end_tangent [$arc get_tangent_at_vertex 1]
require_unit_vector $start_tangent "$application sketch arc start tangent"
require_unit_vector $end_tangent "$application sketch arc end tangent"
require {[$arc get_tangent_at_vertex 2] eq "0 0"} "$application sketch arc returned a tangent for an unrelated vertex"
$arc draw {}
require {[llength [$canvas find all]] == 1} "$application sketch arc did not draw on its canvas"
$arc reverse_orientation
set reversed_tangent [$arc get_tangent_at_vertex 0]
lassign $start_tangent sx sy
lassign $reversed_tangent rx ry
require {
abs($sx + $rx) < 1.0e-9 && abs($sy + $ry) < 1.0e-9
} "$application sketch arc orientation did not reverse its tangent"
::itcl::delete object $arc
set circle ${object_namespace}::circle_probe
$arc_class $circle $editor $canvas {S 0 E 1 R -1 L 0 O 0}
require_unit_vector [$circle get_tangent_at_vertex 0] "$application full-circle tangent"
require {[$circle get_tangent_at_vertex 1] eq "0 0"} "$application full circle reported a tangent at its center"
::itcl::delete object $circle
set bezier ${object_namespace}::bezier_probe
set item_count [llength [$canvas find all]]
$bezier_class $bezier $editor $canvas {D 2 P {0 2 1}}
$bezier draw {}
require {
[llength [$canvas find all]] == $item_count + 1
} "$application sketch Bezier did not draw on its canvas"
require {
[string match {*bezier D 2 P*} [$bezier serialize 1.0]]
} "$application sketch Bezier did not serialize its control points"
::itcl::delete object $bezier
destroy $canvas
}
# Local Variables:
# mode: Tcl
# tab-width: 8
# indent-tabs-mode: t
# End:
+59 -64
View File
@@ -1,18 +1,18 @@
if(BRLCAD_X11_PROVIDER_RESOLVED STREQUAL "XMIN" AND
Xmin_RUN_EXECUTABLE AND SH_EXEC AND TARGET m35.g)
set(_xmin_smoke "${CMAKE_CURRENT_SOURCE_DIR}/xmin_gui_smoke.sh")
set(_xmin_smoke_environment
set(_gui_smoke "${CMAKE_SOURCE_DIR}/regress/gui/gui_smoke.sh")
set(_gui_smoke_environment
"CMAKE_COMMAND=${CMAKE_COMMAND}"
"LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/${LIB_DIR}"
"XMIN_TEST_SHELL=${CMAKE_CURRENT_SOURCE_DIR}/xmin_test.sh"
"GUI_TEST_SHELL=${CMAKE_SOURCE_DIR}/regress/gui/gui_test.sh"
)
set(_xmin_test_db "${CMAKE_BINARY_DIR}/${DATA_DIR}/db/m35.g")
set(_xmin_gui_library "${CMAKE_CURRENT_SOURCE_DIR}/xmin_gui_test.tcl")
set(_xmin_tcl_runner "${CMAKE_CURRENT_SOURCE_DIR}/xmin_tcl_gui_test.sh")
set(_gui_test_db "${CMAKE_BINARY_DIR}/${DATA_DIR}/db/m35.g")
set(_gui_test_library "${CMAKE_SOURCE_DIR}/regress/gui/gui_test.tcl")
set(_gui_tcl_runner "${CMAKE_SOURCE_DIR}/regress/gui/gui_tcl_test.sh")
if(TARGET mged AND TARGET dm-tkswrast AND TARGET dm-X AND TARGET dm-ogl
AND TARGET icv AND TARGET pixcrop AND TARGET rt)
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME regress-mged-xmin-gui-edit
TIMEOUT 600
ENVIRONMENT
@@ -25,11 +25,10 @@ if(BRLCAD_X11_PROVIDER_RESOLVED STREQUAL "XMIN" AND
"MGED_GUI_MANIFEST=${CMAKE_CURRENT_SOURCE_DIR}/mged/mged_gui_menu_manifest.txt"
"MGED_PIPE_ORACLE=${CMAKE_CURRENT_SOURCE_DIR}/mged/xmin_gui_pipe_oracle.tcl"
"MGED_RT_BIN=$<TARGET_FILE:rt>"
"MGED_XMIN_ACTIVE_SESSION=1"
"MGED_TEST_SHELL=${SH_EXEC}"
"MGED_XMIN_RC=${CMAKE_CURRENT_SOURCE_DIR}/mged/xmin_gui_edit.tcl"
"GUI_TEST_SHELL=${CMAKE_SOURCE_DIR}/regress/gui/gui_test.sh"
"MGED_GUI_RC=${CMAKE_CURRENT_SOURCE_DIR}/mged/xmin_gui_edit.tcl"
"PIXCROP_BIN=$<TARGET_FILE:pixcrop>"
"XMIN_GUI_LIBRARY=${CMAKE_SOURCE_DIR}/regress/xmin/xmin_gui_test.tcl"
"GUI_TEST_LIBRARY=${_gui_test_library}"
COMMAND "${SH_EXEC}" "${CMAKE_CURRENT_SOURCE_DIR}/mged/mged_xmin_gui.sh"
)
brlcad_regression_test(regress-mged-xmin-gui-edit
@@ -38,48 +37,48 @@ if(BRLCAD_X11_PROVIDER_RESOLVED STREQUAL "XMIN" AND
endif()
if(TARGET mged AND TARGET dm-tkswrast)
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME regress-mged-xmin-gui-controls
TIMEOUT 180
ENVIRONMENT ${_xmin_smoke_environment}
"XMIN_GUI_LIBRARY=${_xmin_gui_library}"
COMMAND "${SH_EXEC}" "${_xmin_tcl_runner}"
ENVIRONMENT ${_gui_smoke_environment}
"GUI_TEST_LIBRARY=${_gui_test_library}"
COMMAND "${SH_EXEC}" "${_gui_tcl_runner}"
mged-controls $<TARGET_FILE:mged> --gui --dm-type tkswrast
--rcfile "${CMAKE_CURRENT_SOURCE_DIR}/mged/xmin_gui_controls.tcl"
--geom 680x520+0+0 --ggeom 512x512+700+0
"${_xmin_test_db}"
"${_gui_test_db}"
)
brlcad_regression_test(regress-mged-xmin-gui-controls
"mged;dm-tkswrast;m35.g" STAND_ALONE TEST_DEFINED)
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME regress-mged-xmin-gui-workflows
TIMEOUT 300
ENVIRONMENT ${_xmin_smoke_environment}
"XMIN_GUI_LIBRARY=${_xmin_gui_library}"
COMMAND "${SH_EXEC}" "${_xmin_tcl_runner}"
ENVIRONMENT ${_gui_smoke_environment}
"GUI_TEST_LIBRARY=${_gui_test_library}"
COMMAND "${SH_EXEC}" "${_gui_tcl_runner}"
mged-workflows $<TARGET_FILE:mged> --gui --dm-type tkswrast
--rcfile "${CMAKE_CURRENT_SOURCE_DIR}/mged/xmin_gui_workflows.tcl"
--geom 680x520+0+0 --ggeom 512x512+700+0
"${_xmin_test_db}"
"${_gui_test_db}"
)
brlcad_regression_test(regress-mged-xmin-gui-workflows
"mged;dm-tkswrast;m35.g" STAND_ALONE TEST_DEFINED)
function(mged_add_xmin_gui_fixture test_name dependencies)
string(REPLACE "-" "_" script_name "${test_name}")
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME regress-mged-xmin-gui-${test_name}
TIMEOUT 180
ENVIRONMENT ${_xmin_smoke_environment}
"MGED_XMIN_GUI_LIBRARY=${CMAKE_CURRENT_SOURCE_DIR}/mged/xmin_gui_test.tcl"
"XMIN_GUI_LIBRARY=${_xmin_gui_library}"
ENVIRONMENT ${_gui_smoke_environment}
"MGED_GUI_TEST_LIBRARY=${CMAKE_CURRENT_SOURCE_DIR}/mged/xmin_gui_test.tcl"
"GUI_TEST_LIBRARY=${_gui_test_library}"
${ARGN}
COMMAND "${SH_EXEC}" "${_xmin_tcl_runner}"
COMMAND "${SH_EXEC}" "${_gui_tcl_runner}"
mged-${test_name} $<TARGET_FILE:mged> --gui --dm-type tkswrast
--rcfile "${CMAKE_CURRENT_SOURCE_DIR}/mged/xmin_gui_${script_name}.tcl"
--geom 680x520+0+0 --ggeom 512x512+700+0
"${_xmin_test_db}"
"${_gui_test_db}"
)
brlcad_regression_test(regress-mged-xmin-gui-${test_name}
"${dependencies}" STAND_ALONE TEST_DEFINED)
@@ -105,40 +104,40 @@ if(BRLCAD_X11_PROVIDER_RESOLVED STREQUAL "XMIN" AND
endif()
if(TARGET archer)
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME regress-archer-xmin-smoke
TIMEOUT 180
ENVIRONMENT ${_xmin_smoke_environment}
"XMIN_SMOKE_ACTION=${CMAKE_CURRENT_SOURCE_DIR}/archer_action.sh"
"XMIN_MIN_CHANGED_BYTES=10000"
COMMAND "${SH_EXEC}" "${_xmin_smoke}" "Archer ${BRLCAD_VERSION}"
ENVIRONMENT ${_gui_smoke_environment}
"GUI_SMOKE_ACTION=${CMAKE_CURRENT_SOURCE_DIR}/archer_action.sh"
"GUI_MIN_CHANGED_BYTES=10000"
COMMAND "${SH_EXEC}" "${_gui_smoke}" "Archer ${BRLCAD_VERSION}"
"${CMAKE_CURRENT_SOURCE_DIR}/archer_smoke.sh"
$<TARGET_FILE:archer> "${_xmin_test_db}"
$<TARGET_FILE:archer> "${_gui_test_db}"
)
brlcad_regression_test(regress-archer-xmin-smoke
"archer;m35.g" STAND_ALONE TEST_DEFINED)
if(TARGET bwish)
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME regress-archer-xmin-gui
TIMEOUT 180
ENVIRONMENT ${_xmin_smoke_environment}
"XMIN_GUI_LIBRARY=${_xmin_gui_library}"
ENVIRONMENT ${_gui_smoke_environment}
"GUI_TEST_LIBRARY=${_gui_test_library}"
"ARCHER_LAUNCH=${CMAKE_BINARY_DIR}/${DATA_DIR}/tclscripts/archer/init/archer_launch.tcl"
"ARCHER_TEST_DATABASE=${_xmin_test_db}"
"ARCHER_TEST_DATABASE=${_gui_test_db}"
"ARCHER_MENU_MANIFEST=${CMAKE_CURRENT_SOURCE_DIR}/archer_menu_manifest.txt"
COMMAND "${SH_EXEC}" "${_xmin_tcl_runner}" archer
COMMAND "${SH_EXEC}" "${_gui_tcl_runner}" archer
$<TARGET_FILE:bwish> "${CMAKE_CURRENT_SOURCE_DIR}/archer_xmin_gui.tcl"
)
brlcad_regression_test(regress-archer-xmin-gui
"archer;bwish;m35.g" STAND_ALONE TEST_DEFINED)
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME regress-archer-embedded-xmin-gui
TIMEOUT 180
ENVIRONMENT ${_xmin_smoke_environment}
"XMIN_GUI_LIBRARY=${_xmin_gui_library}"
COMMAND "${SH_EXEC}" "${_xmin_tcl_runner}" archer-embedded
ENVIRONMENT ${_gui_smoke_environment}
"GUI_TEST_LIBRARY=${_gui_test_library}"
COMMAND "${SH_EXEC}" "${_gui_tcl_runner}" archer-embedded
$<TARGET_FILE:bwish>
"${CMAKE_CURRENT_SOURCE_DIR}/archer_embedded_xmin_gui.tcl"
)
@@ -148,27 +147,27 @@ if(BRLCAD_X11_PROVIDER_RESOLVED STREQUAL "XMIN" AND
endif()
if(TARGET rtwizard)
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME regress-rtwizard-xmin-smoke
TIMEOUT 180
ENVIRONMENT ${_xmin_smoke_environment}
"XMIN_SMOKE_ACTION=${CMAKE_CURRENT_SOURCE_DIR}/rtwizard_action.sh"
"XMIN_MIN_CHANGED_BYTES=5000"
COMMAND "${SH_EXEC}" "${_xmin_smoke}" RtWizard
$<TARGET_FILE:rtwizard> --gui -i "${_xmin_test_db}" -t A
ENVIRONMENT ${_gui_smoke_environment}
"GUI_SMOKE_ACTION=${CMAKE_CURRENT_SOURCE_DIR}/rtwizard_action.sh"
"GUI_MIN_CHANGED_BYTES=5000"
COMMAND "${SH_EXEC}" "${_gui_smoke}" RtWizard
$<TARGET_FILE:rtwizard> --gui -i "${_gui_test_db}" -t A
)
brlcad_regression_test(regress-rtwizard-xmin-smoke
"rtwizard;m35.g" STAND_ALONE TEST_DEFINED)
if(TARGET bwish)
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME regress-rtwizard-xmin-gui
TIMEOUT 180
ENVIRONMENT ${_xmin_smoke_environment}
"XMIN_GUI_LIBRARY=${_xmin_gui_library}"
"RTWIZARD_TEST_DATABASE=${_xmin_test_db}"
ENVIRONMENT ${_gui_smoke_environment}
"GUI_TEST_LIBRARY=${_gui_test_library}"
"RTWIZARD_TEST_DATABASE=${_gui_test_db}"
"RTWIZARD_MENU_MANIFEST=${CMAKE_CURRENT_SOURCE_DIR}/rtwizard_menu_manifest.txt"
COMMAND "${SH_EXEC}" "${_xmin_tcl_runner}" rtwizard
COMMAND "${SH_EXEC}" "${_gui_tcl_runner}" rtwizard
$<TARGET_FILE:bwish> "${CMAKE_CURRENT_SOURCE_DIR}/rtwizard_xmin_gui.tcl"
)
brlcad_regression_test(regress-rtwizard-xmin-gui
@@ -179,15 +178,15 @@ if(BRLCAD_X11_PROVIDER_RESOLVED STREQUAL "XMIN" AND
# qged is still evolving, so keep this fixture deliberately narrow: prove
# that Qt starts, the console accepts a command, and OpenGL redraws.
if(TARGET qged)
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME regress-qged-xmin-smoke
TIMEOUT 180
ENVIRONMENT ${_xmin_smoke_environment}
"XMIN_SMOKE_ACTION=${CMAKE_CURRENT_SOURCE_DIR}/qged_action.sh"
"XMIN_MIN_CHANGED_BYTES=5000"
COMMAND "${SH_EXEC}" "${_xmin_smoke}" QGED
ENVIRONMENT ${_gui_smoke_environment}
"GUI_SMOKE_ACTION=${CMAKE_CURRENT_SOURCE_DIR}/qged_action.sh"
"GUI_MIN_CHANGED_BYTES=5000"
COMMAND "${SH_EXEC}" "${_gui_smoke}" QGED
"${CMAKE_CURRENT_SOURCE_DIR}/qged_smoke.sh"
$<TARGET_FILE:qged> "${_xmin_test_db}"
$<TARGET_FILE:qged> "${_gui_test_db}"
)
brlcad_regression_test(regress-qged-xmin-smoke
"qged;m35.g" STAND_ALONE TEST_DEFINED)
@@ -197,7 +196,7 @@ endif()
cmakefiles(
CMakeLists.txt
README.md
xmin_gui_smoke.sh
xmin_gui_ctl.sh
archer_action.sh
archer_smoke.sh
archer_embedded_xmin_gui.tcl
@@ -208,10 +207,6 @@ cmakefiles(
rtwizard_action.sh
rtwizard_menu_manifest.txt
rtwizard_xmin_gui.tcl
xmin_gui_test.tcl
xmin_test.sh
xmin_test.tcl
xmin_tcl_gui_test.sh
mged/MGED_GUI_TEST_COVERAGE.md
mged/mged_gui_menu_manifest.txt
mged/mged_xmin_gui.sh
+11 -14
View File
@@ -13,16 +13,13 @@ and toolkit code must interpret consistently.
## Shared architecture
The support has three layers:
1. `brlcad_add_xmin_test` owns the private-server lifecycle, fixed screen,
environment, timeout, and CTest registration.
2. `xmin_gui_smoke.sh` owns the application lifecycle: launch, resolve the
mapped top-level to a stable window ID, wait for rendering to settle,
capture, run an optional action, compare captures, and close cleanly.
3. Application fixtures own their semantic oracle. Tk applications source
`xmin_gui_test.tcl` for widget and menu discovery. XTEST action scripts
drive paths that must traverse the actual X input protocol.
The platform-neutral helpers and controller contract are documented in
[GUI regression support](../gui/README.md). `brlcad_add_gui_test` currently
selects the Xmin backend, which starts a private server at a fixed screen
size. `xmin_gui_ctl.sh` translates controller commands to `xminctl`.
Application fixtures supply their own semantic checks; XTEST action scripts
exercise real input dispatch. MGED's deep fixture includes X11 and GLX
display managers and remains specific to this backend.
Resolving a window name once is important for Qt, which may create mapped and
unmapped helper windows with the same title. Repeating a title lookup can
@@ -114,13 +111,13 @@ widget-state assertion.
For a conventional executable:
```
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME regress-example-xmin-smoke
TIMEOUT 180
ENVIRONMENT
"XMIN_TEST_SHELL=${CMAKE_CURRENT_SOURCE_DIR}/xmin_test.sh"
"XMIN_SMOKE_ACTION=${CMAKE_CURRENT_SOURCE_DIR}/example_action.sh"
COMMAND "${SH_EXEC}" "${CMAKE_CURRENT_SOURCE_DIR}/xmin_gui_smoke.sh"
"GUI_TEST_SHELL=${CMAKE_SOURCE_DIR}/regress/gui/gui_test.sh"
"GUI_SMOKE_ACTION=${CMAKE_CURRENT_SOURCE_DIR}/example_action.sh"
COMMAND "${SH_EXEC}" "${CMAKE_SOURCE_DIR}/regress/gui/gui_smoke.sh"
"Expected title" $<TARGET_FILE:example>
)
```
+7 -7
View File
@@ -2,12 +2,12 @@
set -eu
: "${XMIN_TEST_SHELL:?XMIN_TEST_SHELL is required}"
. "$XMIN_TEST_SHELL"
: "${GUI_TEST_SHELL:?GUI_TEST_SHELL is required}"
. "$GUI_TEST_SHELL"
window=$1
geometry=$("$XMIN_CTL" geometry "$window") ||
xmin_fail "could not query Archer window geometry"
geometry=$("$GUI_TEST_CTL" geometry "$window") ||
gui_fail "could not query Archer window geometry"
set -- $geometry
width=$3
height=$4
@@ -16,9 +16,9 @@ height=$4
# relative to the live geometry so the action is independent of screen size.
command_x=$((width / 4))
command_y=$((height - 27))
"$XMIN_CTL" click "$window" "$command_x" "$command_y" 1
"$XMIN_CTL" type "draw all.g"
"$XMIN_CTL" key enter
"$GUI_TEST_CTL" click "$window" "$command_x" "$command_y" 1
"$GUI_TEST_CTL" type "draw all.g"
"$GUI_TEST_CTL" key enter
ARCHER_DRAW_SETTLE_SECONDS=${ARCHER_DRAW_SETTLE_SECONDS:-1}
sleep "$ARCHER_DRAW_SETTLE_SECONDS"
+13 -13
View File
@@ -18,12 +18,12 @@
# information.
#
###
if {![info exists ::env(XMIN_GUI_LIBRARY)]} {
puts stderr "XMIN_GUI_LIBRARY is required"
if {![info exists ::env(GUI_TEST_LIBRARY)]} {
puts stderr "GUI_TEST_LIBRARY is required"
exit 2
}
source $::env(XMIN_GUI_LIBRARY)
set ::env(ARCHER_PREFS_FILE) [file join $::env(XMIN_TEST_DIR) .archerrc]
source $::env(GUI_TEST_LIBRARY)
set ::env(ARCHER_PREFS_FILE) [file join $::env(GUI_TEST_DIR) .archerrc]
# ArcherCore supports embedding in another Itk widget. Its constructor
# currently performs a synchronous database load after one update, so inspect
@@ -41,16 +41,16 @@ if {[catch {package require Archer 1.0} message options]} {
}
exit 1
}
::xmin::test::capture_background_errors
::gui::test::capture_background_errors
proc ::archer_embedded_check {} {
if {[catch {
set application .embedded
::xmin::test::require {[winfo exists $application]} \
::gui::test::require {[winfo exists $application]} \
"embedded Archer widget was not created"
set inventory [::xmin::test::menu_inventory $application]
::xmin::test::write embedded_menu_inventory $inventory
set inventory [::gui::test::menu_inventory $application]
::gui::test::write embedded_menu_inventory $inventory
foreach labels {
{File Save}
@@ -66,8 +66,8 @@ proc ::archer_embedded_check {} {
{Raytrace rt 512x512}
{Help {About Archer...}}
} {
::xmin::test::require {
[::xmin::test::find_menu_entry $application $labels] ne ""
::gui::test::require {
[::gui::test::find_menu_entry $application $labels] ne ""
} "missing embedded Archer menu entry: [join $labels { > }]"
}
@@ -76,8 +76,8 @@ proc ::archer_embedded_check {} {
{File Revert}
{File Raytrace}
} {
::xmin::test::require {
[::xmin::test::find_menu_entry $application $labels] eq ""
::gui::test::require {
[::gui::test::find_menu_entry $application $labels] eq ""
} "unexpected embedded Archer menu entry: [join $labels { > }]"
}
} message options]} {
@@ -90,7 +90,7 @@ proc ::archer_embedded_check {} {
}
catch {update}
lassign [::xmin::test::check_background_errors 0 \
lassign [::gui::test::check_background_errors 0 \
"PASS: embedded Archer menu construction"] status result
puts $result
if {$status != 0} {
+2 -2
View File
@@ -2,14 +2,14 @@
set -eu
: "${XMIN_ARTIFACT_DIR:?XMIN_ARTIFACT_DIR is required}"
: "${GUI_ARTIFACT_DIR:?GUI_ARTIFACT_DIR is required}"
if [ "$#" -lt 1 ]; then
echo "Usage: $0 archer [argument ...]" >&2
exit 2
fi
ARCHER_PREFS_FILE="$XMIN_ARTIFACT_DIR/.archerrc"
ARCHER_PREFS_FILE="$GUI_ARTIFACT_DIR/.archerrc"
export ARCHER_PREFS_FILE
exec "$@"
+63 -62
View File
@@ -18,14 +18,15 @@
# information.
#
###
if {![info exists ::env(XMIN_GUI_LIBRARY)] ||
if {![info exists ::env(GUI_TEST_LIBRARY)] ||
![info exists ::env(ARCHER_LAUNCH)] ||
![info exists ::env(ARCHER_TEST_DATABASE)]} {
puts stderr "XMIN_GUI_LIBRARY, ARCHER_LAUNCH, and ARCHER_TEST_DATABASE are required"
puts stderr "GUI_TEST_LIBRARY, ARCHER_LAUNCH, and ARCHER_TEST_DATABASE are required"
exit 2
}
source $::env(XMIN_GUI_LIBRARY)
set ::env(ARCHER_PREFS_FILE) [file join $::env(XMIN_TEST_DIR) .archerrc]
source $::env(GUI_TEST_LIBRARY)
source [file join [file dirname $::env(GUI_TEST_LIBRARY)] sketch_test.tcl]
set ::env(ARCHER_PREFS_FILE) [file join $::env(GUI_TEST_DIR) .archerrc]
namespace eval ::archer::xmin {
variable application ""
@@ -44,7 +45,7 @@ proc ::archer::xmin::finish {status message} {
}
catch {destroy .}
catch {update}
lassign [::xmin::test::check_background_errors $status $message] \
lassign [::gui::test::check_background_errors $status $message] \
status message
puts $message
if {$status != 0} {
@@ -55,7 +56,7 @@ proc ::archer::xmin::finish {status message} {
proc ::archer::xmin::toplevels {} {
set toplevels {}
foreach widget [linsert [::xmin::test::descendants .] 0 .] {
foreach widget [linsert [::gui::test::descendants .] 0 .] {
if {[winfo toplevel $widget] eq $widget} {
lappend toplevels $widget
}
@@ -88,18 +89,18 @@ proc ::archer::xmin::exercise_dialog {root labels {expected_title ""}} {
variable dialog_timeout_ms
set dialog_seen ""
after $dialog_poll_delay_ms ::archer::xmin::dismiss_dialog
::xmin::test::invoke_menu_entry $root $labels
::gui::test::invoke_menu_entry $root $labels
if {$dialog_seen eq ""} {
set timeout [after $dialog_timeout_ms \
[list set ::archer::xmin::dialog_seen $dialog_timeout_marker]]
vwait ::archer::xmin::dialog_seen
after cancel $timeout
}
::xmin::test::require {
::gui::test::require {
$dialog_seen ne "" && $dialog_seen ne $dialog_timeout_marker} \
"menu entry did not display a dialog: [join $labels { > }]"
if {$expected_title ne ""} {
::xmin::test::require {$dialog_seen eq $expected_title} \
::gui::test::require {$dialog_seen eq $expected_title} \
"unexpected dialog title '$dialog_seen', expected '$expected_title'"
}
}
@@ -112,7 +113,7 @@ proc ::archer::xmin::compare_manifest {actual} {
set expected [string trim [read $channel]]
close $channel
if {$actual ne $expected} {
::xmin::test::write menu_inventory_actual $actual
::gui::test::write menu_inventory_actual $actual
error "live Archer menu inventory differs from its manifest"
}
}
@@ -138,7 +139,7 @@ proc ::archer::xmin::exercise_object_lifecycle {
error "$class $cycle initialization failed: $message"
}
update
::xmin::test::require {
::gui::test::require {
[llength [info commands $object]] == 1 &&
[winfo exists $object]
} "$class $cycle construction did not retain its object and window"
@@ -149,7 +150,7 @@ proc ::archer::xmin::exercise_object_lifecycle {
error "$class $cycle destruction failed: $message"
}
update
::xmin::test::require {
::gui::test::require {
[llength [info commands $object]] == 0 &&
![winfo exists $object]
} "$class $cycle destruction retained its object or window"
@@ -182,8 +183,8 @@ proc ::archer::xmin::require_arb_standard_storage {ged geometry_name primitive}
set expected [$ged get $geometry_name [lindex $group 0]]
foreach attribute [lrange $group 1 end] {
set actual [$ged get $geometry_name $attribute]
::xmin::test::require {
[::xmin::test::equivalent_values $actual $expected]
::gui::test::require {
[::gui::test::equivalent_values $actual $expected]
} "$primitive storage is nonstandard: [lindex $group 0]=$expected, $attribute=$actual"
}
}
@@ -208,8 +209,8 @@ proc ::archer::xmin::exercise_arb_editor_fields {
set storage_attribute [arb_storage_attribute $primitive $logical_vertex]
set actual [lindex [$ged get $geometry_name $storage_attribute] $component_index]
::xmin::test::require {
[::xmin::test::equivalent_values $actual $requested]
::gui::test::require {
[::gui::test::equivalent_values $actual $requested]
} "$primitive $component_name wrote $actual instead of $requested to $storage_attribute"
require_arb_standard_storage $ged $geometry_name $primitive
@@ -228,8 +229,8 @@ proc ::archer::xmin::exercise_editor_update {
set original [$ged get $geometry_name]
$object updateGeometry
set updated [$ged get $geometry_name]
::xmin::test::require \
[::xmin::test::equivalent_values $updated $original] \
::gui::test::require \
[::gui::test::equivalent_values $updated $original] \
"$class changed geometry during an unmodified update: original={$original}, updated={$updated}"
if {[regexp {^Arb([4-8])EditFrame$} $class unused arb_type]} {
exercise_arb_editor_fields $object $ged $geometry_name \
@@ -308,29 +309,29 @@ proc ::archer::xmin::exercise_wizard_build {
error "$class geometry generation failed: $result"
}
::xmin::test::require {$result eq $top} "$class returned '$result' instead of its top object '$top'"
::xmin::test::require {[$ged exists $top]} "$class did not create its top database object '$top'"
::xmin::test::require {
::gui::test::require {$result eq $top} "$class returned '$result' instead of its top object '$top'"
::gui::test::require {[$ged exists $top]} "$class did not create its top database object '$top'"
::gui::test::require {
[$ged attr get $top WizardClass] eq $class
} "$class did not identify its generated database object"
::xmin::test::require {
::gui::test::require {
[llength [$object getWizardState]] > 0
} "$class did not retain its generation parameters"
::xmin::test::require {
::gui::test::require {
[lsearch -exact [$ged who] $top] >= 0
} "$class did not draw its generated database object"
if {$class eq "TankWizard"} {
::xmin::test::require {
::gui::test::require {
[$ged exists ${top}_gun_tube.r]
} "TankWizard omitted its transformed gun-barrel region"
set xml [$object buildTankXML]
::xmin::test::require {
::gui::test::require {
[string first "<Name>$top</Name>" $xml] >= 0 &&
[string first "<Name>Gun Barrel</Name>" $xml] >= 0
} "TankWizard XML omitted its top system or gun component"
set open_systems [llength [regexp -all -inline {<System>} $xml]]
set close_systems [llength [regexp -all -inline {</System>} $xml]]
::xmin::test::require {
::gui::test::require {
$open_systems > 10 && $open_systems == $close_systems
} "TankWizard XML has unbalanced system elements"
}
@@ -385,22 +386,22 @@ proc ::archer::xmin::exercise_attribute_groups_utility {application} {
AttrGroupsDisplayUtilityP $object $application
update
set group_list [$object component glist]
::xmin::test::require {
::gui::test::require {
[$group_list get 0 end] eq "Xmin"
} "attribute-groups utility did not load its database definition"
$group_list selection set 0
uplevel #0 [$group_list cget -selectioncommand]
::xmin::test::require {
::gui::test::require {
[$object getCurrentGroup] eq "Xmin"
} "attribute-groups utility did not select its group"
set attribute_list [$object component alist]
::xmin::test::require {
::gui::test::require {
[$attribute_list get 0 end] eq "alpha beta"
} "attribute-groups utility did not load the grouped values"
$attribute_list selection set 0
set highlighted [$object highlightSelectedAttr]
::xmin::test::require {
::gui::test::require {
[lsearch -exact [lindex $highlighted 0] xmin-attr-alpha.s] >= 0
} "attribute-groups utility did not resolve and highlight the selected value"
::itcl::delete object $object
@@ -416,7 +417,7 @@ proc ::archer::xmin::exercise_lod_utility {application} {
update
set dialog "$object.lodDialog"
::xmin::test::require {
::gui::test::require {
[llength [info commands $dialog]] == 1 && [winfo exists $dialog]
} "LOD utility did not create its configuration dialog"
set frame "$dialog.lodFrame"
@@ -428,7 +429,7 @@ proc ::archer::xmin::exercise_lod_utility {application} {
update
set updated_points [$ged lod scale points]
set updated_curves [$ged lod scale curves]
::xmin::test::require {
::gui::test::require {
abs($updated_points - 0.7) < 1.0e-9 &&
abs($updated_curves - 9.0) < 1.0e-9
} "LOD utility controls set points=$updated_points and curves=$updated_curves"
@@ -457,7 +458,7 @@ proc ::archer::xmin::exercise_bot_utility {application} {
BotUtilityP $object $application
update
set selector [$object component combo]
::xmin::test::require {
::gui::test::require {
[lsearch -exact [$selector cget -values] $bot] >= 0
} "BoT utility did not list its available mesh"
$object configure -selectedbot $bot
@@ -471,7 +472,7 @@ proc ::archer::xmin::exercise_bot_utility {application} {
lappend editors $editor
}
}
::xmin::test::require {
::gui::test::require {
[llength $editors] == 1 && [$ged exists "$bot.edit"]
} "BoT utility did not launch an Archer-backed editor with a working copy"
set editor [lindex $editors 0]
@@ -507,11 +508,11 @@ proc ::archer::xmin::exercise_combination_edit {application} {
set combination [lrange [$ged get xmin-tree.c] 1 end]
set original_tree [bu_get_value_by_keyword tree $combination]
set flattened_tree [ArcherCore::unpackTree $original_tree]
::xmin::test::require {
::gui::test::require {
[ArcherCore::packTree $flattened_tree] ne $original_tree
} "Archer combination fixture does not exercise a lossy tree grouping"
set spaced_leaf [list l [lindex $members 1]]
::xmin::test::require {
::gui::test::require {
[ArcherCore::packTree [ArcherCore::unpackTree $spaced_leaf]] eq $spaced_leaf
} "Archer tree editor did not preserve a member name containing spaces"
@@ -527,9 +528,9 @@ proc ::archer::xmin::exercise_combination_edit {application} {
set updated [lrange [$ged get xmin-tree.c] 1 end]
set updated_tree [bu_get_value_by_keyword tree $updated]
set updated_rgb [bu_get_value_by_keyword rgb $updated]
::xmin::test::require {$updated_tree eq $original_tree} \
::gui::test::require {$updated_tree eq $original_tree} \
"editing a combination attribute changed its Boolean tree"
::xmin::test::require {$updated_rgb eq "17 34 51"} \
::gui::test::require {$updated_rgb eq "17 34 51"} \
"Archer did not apply the requested combination color"
::itcl::delete object $editor
exercise_object_lifecycle CombEditFrame $editor \
@@ -547,7 +548,7 @@ proc ::archer::xmin::exercise_combination_edit {application} {
break
}
}
::xmin::test::require {$combination_node ne ""} \
::gui::test::require {$combination_node ne ""} \
"Archer hierarchy omitted the test combination"
$tree focus $combination_node
$tree item $combination_node -open true
@@ -557,7 +558,7 @@ proc ::archer::xmin::exercise_combination_edit {application} {
foreach child [$tree children $combination_node] {
lappend child_names [$tree item $child -text]
}
::xmin::test::require {[lsort $child_names] eq [lsort $members]} \
::gui::test::require {[lsort $child_names] eq [lsort $members]} \
"Archer hierarchy reported the wrong combination members: $child_names"
}
@@ -586,7 +587,7 @@ proc ::archer::xmin::exercise_raytrace_abort {application} {
set raytrace_button [$toolbar component raytrace]
set launch_command [$toolbar itemcget raytrace -command]
::xmin::test::require {
::gui::test::require {
[string first raytracePlus $launch_command] >= 0
} "Archer raytrace toolbar button does not start a raytrace"
@@ -597,14 +598,14 @@ proc ::archer::xmin::exercise_raytrace_abort {application} {
$raytrace_button invoke
set abort_command [$toolbar itemcget raytrace -command]
::xmin::test::require {
::gui::test::require {
$abort_command ne $launch_command &&
[string first abort $abort_command] >= 0
} "Archer did not turn its raytrace toolbar button into Abort"
$raytrace_button invoke
wait_for {$raytrace_aborted ne ""} "RtWizard abort callback"
::xmin::test::require {$raytrace_aborted} \
::gui::test::require {$raytrace_aborted} \
"Archer reported normal completion after the Abort action"
wait_for {
[string first raytracePlus [$toolbar itemcget raytrace -command]] >= 0
@@ -614,22 +615,22 @@ proc ::archer::xmin::exercise_raytrace_abort {application} {
proc ::archer::xmin::run {} {
variable application
set application $::ArcherCore::application
::xmin::test::require {[llength [info commands $application]] == 1} \
::gui::test::require {[llength [info commands $application]] == 1} \
"Archer application object was not created"
::xmin::test::require {[winfo ismapped $application]} \
::gui::test::require {[winfo ismapped $application]} \
"Archer main window is not mapped"
::xmin::test::require {[string match {Archer *} [wm title $application]]} \
::gui::test::require {[string match {Archer *} [wm title $application]]} \
"unexpected Archer main-window title"
set opened_database [$application WhatsOpen]
::xmin::test::require {
::gui::test::require {
[file normalize $opened_database] eq [file normalize $::env(ARCHER_WORK_DATABASE)]
} "Archer did not open the relative database argument"
set widget_inventory [::xmin::test::widget_inventory $application]
set menu_inventory [::xmin::test::menu_inventory $application]
::xmin::test::write widget_inventory $widget_inventory
::xmin::test::write menu_inventory $menu_inventory
set widget_inventory [::gui::test::widget_inventory $application]
set menu_inventory [::gui::test::menu_inventory $application]
::gui::test::write widget_inventory $widget_inventory
::gui::test::write menu_inventory $menu_inventory
compare_manifest $menu_inventory
foreach labels {
@@ -641,27 +642,27 @@ proc ::archer::xmin::run {} {
{Help {Archer Help...}}
{Help {About Archer...}}
} {
::xmin::test::require {
[::xmin::test::find_menu_entry $application $labels] ne ""
::gui::test::require {
[::gui::test::find_menu_entry $application $labels] ne ""
} "missing Archer menu entry: [join $labels { > }]"
}
$application draw all.g
update
::xmin::test::require {
::gui::test::require {
[lsearch -exact [$application gedCmd who] all.g] >= 0
} "Archer did not draw all.g"
::xmin::test::invoke_menu_entry $application \
::gui::test::invoke_menu_entry $application \
{Display {Standard Views} Front}
set aet [$application gedCmd aet]
::xmin::test::require {
::gui::test::require {
abs([lindex $aet 0]) < 0.001 && abs([lindex $aet 1]) < 0.001
} "Front menu entry did not set the expected view: $aet"
::xmin::test::invoke_menu_entry $application \
::gui::test::invoke_menu_entry $application \
{Display {Background Color} Black}
::xmin::test::require {
::gui::test::require {
[$application gedCmd bg] eq "0 0 0"
} "Black background menu entry did not update the display"
@@ -672,7 +673,7 @@ proc ::archer::xmin::run {} {
exercise_dialog $application {Help {About Archer...}} {About Archer}
exercise_combination_edit $application
::xmin::test::exercise_sketch_segments SketchCArc SketchBezier Archer ::archer::xmin
::gui::test::exercise_sketch_segments SketchCArc SketchBezier Archer ::archer::xmin
exercise_wizard_builds $application
exercise_editor_lifecycles $application
exercise_plugin_lifecycles $application
@@ -692,10 +693,10 @@ proc ::archer::xmin::run_checked {} {
}
}
set work_database [file join $::env(XMIN_TEST_DIR) archer-relative.g]
set work_database [file join $::env(GUI_TEST_DIR) archer-relative.g]
file copy -force $::env(ARCHER_TEST_DATABASE) $work_database
set ::env(ARCHER_WORK_DATABASE) $work_database
cd $::env(XMIN_TEST_DIR)
cd $::env(GUI_TEST_DIR)
set argv [list [file tail $work_database]]
set argc 1
set argv0 [info nameofexecutable]
@@ -707,7 +708,7 @@ if {[catch {source $::env(ARCHER_LAUNCH)} message options]} {
}
exit 1
}
::xmin::test::capture_background_errors
::gui::test::capture_background_errors
# Let the splash timer finish so it cannot obscure dialog discovery.
after 1800 ::archer::xmin::run_checked
+231 -264
View File
@@ -17,25 +17,16 @@ MIN_RENDER_CHANGED_CHANNELS=100
MIN_REFERENCE_EXACT_PERCENT=90
MAX_RAYTRACE_ELAPSED_MS=5000
RGB_CHANNELS=3
SCREEN_GEOMETRY=1280x960x24
SCREEN_WIDTH=${SCREEN_GEOMETRY%%x*}
screen_height_depth=${SCREEN_GEOMETRY#*x}
SCREEN_HEIGHT=${screen_height_depth%%x*}
COMMAND_GEOMETRY=680x520+0+0
GRAPHICS_GEOMETRY=512x512+700+0
MGED_TEST_SHELL=${MGED_TEST_SHELL:-sh}
: "${GUI_TEST_SHELL:?GUI_TEST_SHELL is required}"
. "$GUI_TEST_SHELL"
GUI_STABLE_QUIET_MS=$STABLE_QUIET
GUI_STABLE_TIMEOUT_MS=$STABLE_TIMEOUT
TOP_LEVEL_MENUS="file:File edit:Edit create:Create view:View viewring:ViewRing settings:Settings modes:Modes misc:Misc tools:Tools help:Help"
MGED_DM_TYPES=${MGED_DM_TYPES:-tkswrast}
MGED_GUI_APNG_DIR=${MGED_GUI_APNG_DIR:-.}
MGED_PIPE_ORACLE=${MGED_PIPE_ORACLE:-${MGED_XMIN_RC%/*}/xmin_gui_pipe_oracle.tcl}
require_executable()
{
if [ ! -x "$1" ]; then
echo "FAIL: required executable is unavailable: $1" >&2
exit 1
fi
}
MGED_PIPE_ORACLE=${MGED_PIPE_ORACLE:-${MGED_GUI_RC%/*}/xmin_gui_pipe_oracle.tcl}
wait_for_file()
{
@@ -51,7 +42,7 @@ wait_for_file_with_limit()
if [ -s "$path" ]; then
return 0
fi
if [ -s "$MGED_XMIN_TEST_DIR/result" ]; then
if [ -s "$MGED_GUI_TEST_DIR/result" ]; then
return 1
fi
if [ -n "${MGED_PID:-}" ] && ! kill -0 "$MGED_PID" 2>/dev/null; then
@@ -70,7 +61,7 @@ wait_for_value()
if [ -f "$1" ] && [ "$(sed -n '1p' "$1")" = "$2" ]; then
return 0
fi
if [ -s "$MGED_XMIN_TEST_DIR/result" ]; then
if [ -s "$MGED_GUI_TEST_DIR/result" ]; then
return 1
fi
sleep "$WAIT_DELAY"
@@ -86,7 +77,7 @@ wait_for_not_value()
if [ -s "$1" ] && [ "$(sed -n '1p' "$1")" != "$2" ]; then
return 0
fi
if [ -s "$MGED_XMIN_TEST_DIR/result" ]; then
if [ -s "$MGED_GUI_TEST_DIR/result" ]; then
return 1
fi
sleep "$WAIT_DELAY"
@@ -100,17 +91,17 @@ start_raytrace()
# xminctl can return before Tk handles the click. Wait for the button
# callback before enabling the monitor's MGED command polling.
click_target raytrace_button
if ! wait_for_value "$MGED_XMIN_TEST_DIR/raytrace_invocations" "$1"; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/raytrace_invocations" "$1"; then
fail "Raytrace Control Panel did not process button click $1"
fi
printf '%s\n' "$1" > "$MGED_XMIN_TEST_DIR/raytrace_request"
printf '%s\n' "$1" > "$MGED_GUI_TEST_DIR/raytrace_request"
}
query_database()
{
database=$1
query=$2
error_file="$MGED_XMIN_TEST_DIR/database_query_error"
error_file="$MGED_GUI_TEST_DIR/database_query_error"
count=0
while [ "$count" -lt "$DATABASE_QUERY_LIMIT" ]; do
if DATABASE_VALUE=$("$MGED_BIN" -c "$database" "$query" 2>"$error_file"); then
@@ -137,24 +128,24 @@ read_point()
read_target()
{
TARGET_WINDOW=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/$1_window")
read_point "$MGED_XMIN_TEST_DIR/$1"
TARGET_WINDOW=$(sed -n '1p' "$MGED_GUI_TEST_DIR/$1_window")
read_point "$MGED_GUI_TEST_DIR/$1"
}
click_target()
{
read_target "$1"
"$XMIN_CTL" click --delay "$CLICK_DELAY" "$TARGET_WINDOW" "$POINT_X" "$POINT_Y" "${2:-1}"
"$GUI_TEST_CTL" click --delay "$CLICK_DELAY" "$TARGET_WINDOW" "$POINT_X" "$POINT_Y" "${2:-1}"
}
activate_menu_target()
{
read_target "$1"
"$XMIN_CTL" mouse-move "$TARGET_WINDOW" "$POINT_X" "$POINT_Y"
"$XMIN_CTL" wait-stable --quiet "$STABLE_QUIET" \
"$GUI_TEST_CTL" mouse-move "$TARGET_WINDOW" "$POINT_X" "$POINT_Y"
"$GUI_TEST_CTL" wait-stable --quiet "$STABLE_QUIET" \
--timeout "$STABLE_TIMEOUT" root >/dev/null 2>&1 || true
"$XMIN_CTL" button 1 down
"$XMIN_CTL" button 1 up
"$GUI_TEST_CTL" button 1 down
"$GUI_TEST_CTL" button 1 up
}
replace_entry_text()
@@ -165,14 +156,14 @@ replace_entry_text()
expected_value=$4
click_target "$target"
"$XMIN_CTL" key end
"$GUI_TEST_CTL" key end
remaining=${#initial_value}
while [ "$remaining" -gt 0 ]; do
"$XMIN_CTL" key backspace
"$GUI_TEST_CTL" key backspace
remaining=$((remaining - 1))
done
wait_for_value "$current_file" "" || return 1
"$XMIN_CTL" type "$expected_value"
"$GUI_TEST_CTL" type "$expected_value"
wait_for_value "$current_file" "$expected_value"
}
@@ -183,8 +174,8 @@ capture_failure()
capture_frame()
{
frames_dir="$MGED_XMIN_RUN_DIR/frames"
counter_file="$MGED_XMIN_RUN_DIR/frame_counter"
frames_dir="$MGED_GUI_RUN_DIR/frames"
counter_file="$MGED_GUI_RUN_DIR/frame_counter"
"$CMAKE_COMMAND" -E make_directory "$frames_dir"
frame_number=0
@@ -194,14 +185,14 @@ capture_frame()
frame_number=$((frame_number + 1))
frame_name=$(printf '%06d.ppm' "$frame_number")
"$XMIN_CTL" wait-stable --quiet "$STABLE_QUIET" \
"$GUI_TEST_CTL" wait-stable --quiet "$STABLE_QUIET" \
--timeout "$STABLE_TIMEOUT" root >/dev/null 2>&1 || true
if ! "$XMIN_CTL" capture-root "$frames_dir/$frame_name" >/dev/null 2>&1; then
if ! "$GUI_TEST_CTL" capture-screen "$frames_dir/$frame_name" >/dev/null 2>&1; then
return 1
fi
printf '%s\n' "$frame_number" > "$counter_file"
printf '%06d %s %s\n' "$frame_number" "$MGED_DM_TYPE" "$1" \
>> "$MGED_XMIN_RUN_DIR/frames.log"
>> "$MGED_GUI_RUN_DIR/frames.log"
}
record_state()
@@ -213,29 +204,13 @@ record_state()
capture_dm_snapshot()
{
snapshot="$MGED_XMIN_TEST_DIR/$1.ppm"
root_snapshot="$MGED_XMIN_TEST_DIR/$1-root.ppm"
root_pixels="$MGED_XMIN_TEST_DIR/$1-root.pix"
cropped_pixels="$MGED_XMIN_TEST_DIR/$1-cropped.pix"
snapshot="$MGED_GUI_TEST_DIR/$1.ppm"
read -r dm_x dm_y dm_width dm_height \
< "$MGED_XMIN_TEST_DIR/active_dm_geometry"
lower_y=$((SCREEN_HEIGHT - dm_y - dm_height))
upper_y=$((lower_y + dm_height - 1))
right_x=$((dm_x + dm_width - 1))
"$XMIN_CTL" wait-stable --quiet "$STABLE_QUIET" \
--timeout "$STABLE_TIMEOUT" root >/dev/null 2>&1 || true
if ! "$XMIN_CTL" capture-root "$root_snapshot" >/dev/null 2>&1 ||
! "$ICV_BIN" "$root_snapshot" "$root_pixels" >/dev/null 2>&1 ||
! "$PIXCROP_BIN" "$root_pixels" "$cropped_pixels" \
"$SCREEN_WIDTH" "$dm_width" "$dm_height" \
"$dm_x" "$upper_y" "$right_x" "$upper_y" \
"$right_x" "$lower_y" "$dm_x" "$lower_y" >/dev/null 2>&1 ||
! "$ICV_BIN" -w "$dm_width" -n "$dm_height" \
"$cropped_pixels" "$snapshot" >/dev/null 2>&1; then
< "$MGED_GUI_TEST_DIR/active_dm_geometry"
if ! gui_capture_region "$snapshot" "$dm_x" "$dm_y" \
"$dm_width" "$dm_height"; then
fail "could not capture display-manager snapshot '$1'"
fi
"$CMAKE_COMMAND" -E rm -f "$root_snapshot" "$root_pixels" "$cropped_pixels"
}
assert_raw_images_equal()
@@ -315,7 +290,7 @@ wait_for_changed_dm_snapshot()
description=$1
baseline=$2
snapshot_name=$3
snapshot="$MGED_XMIN_TEST_DIR/$snapshot_name.ppm"
snapshot="$MGED_GUI_TEST_DIR/$snapshot_name.ppm"
count=0
changed_channels=0
@@ -341,7 +316,7 @@ assert_raytrace_duration()
{
request=$1
description=$2
elapsed_file="$MGED_XMIN_TEST_DIR/raytrace_elapsed_${request}_ms"
elapsed_file="$MGED_GUI_TEST_DIR/raytrace_elapsed_${request}_ms"
if ! wait_for_file "$elapsed_file"; then
fail "$description did not report its elapsed time"
fi
@@ -358,8 +333,8 @@ assert_raytrace_duration()
toggle_renderer_setting()
{
setting=$1
initial=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/renderer_${setting}_initial")
mutable=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/renderer_${setting}_mutable")
initial=$(sed -n '1p' "$MGED_GUI_TEST_DIR/renderer_${setting}_initial")
mutable=$(sed -n '1p' "$MGED_GUI_TEST_DIR/renderer_${setting}_mutable")
if [ "$initial" -eq 0 ]; then
expected=1
else
@@ -367,13 +342,13 @@ toggle_renderer_setting()
fi
click_target main_misc_menu
if ! wait_for_file "$MGED_XMIN_TEST_DIR/renderer_${setting}" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/renderer_${setting}_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/renderer_${setting}" ||
! wait_for_file "$MGED_GUI_TEST_DIR/renderer_${setting}_window"; then
fail "Misc menu did not expose $setting"
fi
record_state "Misc menu $setting"
activate_menu_target "renderer_${setting}"
if ! wait_for_value "$MGED_XMIN_TEST_DIR/renderer_${setting}_gui_current" "$expected"; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/renderer_${setting}_gui_current" "$expected"; then
fail "$setting menu state did not change to $expected"
fi
backend_expected=$expected
@@ -382,7 +357,7 @@ toggle_renderer_setting()
backend_expected=$initial
state_description="requested state $expected (backend unavailable)"
fi
if ! wait_for_value "$MGED_XMIN_TEST_DIR/renderer_${setting}_current" \
if ! wait_for_value "$MGED_GUI_TEST_DIR/renderer_${setting}_current" \
"$backend_expected"; then
fail "$setting backend state did not become $backend_expected"
fi
@@ -391,17 +366,17 @@ toggle_renderer_setting()
click_target main_misc_menu
record_state "Misc menu $setting restore"
activate_menu_target "renderer_${setting}"
if ! wait_for_value "$MGED_XMIN_TEST_DIR/renderer_${setting}_gui_current" "$initial"; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/renderer_${setting}_gui_current" "$initial"; then
fail "$setting menu state did not return to $initial"
fi
if ! wait_for_value "$MGED_XMIN_TEST_DIR/renderer_${setting}_current" "$initial"; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/renderer_${setting}_current" "$initial"; then
fail "$setting did not return to $initial"
fi
}
create_recording()
{
frames_dir="$MGED_XMIN_RUN_DIR/frames"
frames_dir="$MGED_GUI_RUN_DIR/frames"
set -- "$frames_dir"/*.ppm
if [ ! -f "$1" ]; then
echo "FAIL: MGED GUI run did not produce recording frames" >&2
@@ -417,14 +392,14 @@ create_recording()
return 1
fi
validation_dir="$MGED_XMIN_RUN_DIR/apng_validation"
validation_dir="$MGED_GUI_RUN_DIR/apng_validation"
"$CMAKE_COMMAND" -E make_directory "$validation_dir"
if ! "$ICV_BIN" anim extract "$apng" "$validation_dir/frame_" \
>/dev/null 2>&1; then
echo "FAIL: could not reopen MGED GUI recording $apng" >&2
return 1
fi
expected_frames=$(sed -n '1p' "$MGED_XMIN_RUN_DIR/frame_counter")
expected_frames=$(sed -n '1p' "$MGED_GUI_RUN_DIR/frame_counter")
actual_frames=$(find "$validation_dir" -type f -name '*.png' | wc -l | tr -d ' ')
if [ "$actual_frames" -ne "$expected_frames" ]; then
echo "FAIL: APNG has $actual_frames frames; expected $expected_frames" >&2
@@ -448,32 +423,28 @@ fail()
MGED_PID=""
echo "MGED exited with status $mged_status" >&2
fi
if [ -f "$MGED_XMIN_TEST_DIR/result" ]; then
sed -n '1,20p' "$MGED_XMIN_TEST_DIR/result" >&2
if [ -f "$MGED_GUI_TEST_DIR/result" ]; then
sed -n '1,20p' "$MGED_GUI_TEST_DIR/result" >&2
fi
for diagnostic in "$MGED_XMIN_TEST_DIR"/*_debug; do
for diagnostic in "$MGED_GUI_TEST_DIR"/*_debug; do
if [ -f "$diagnostic" ]; then
echo "$(basename "$diagnostic"): $(sed -n '1,20p' "$diagnostic")" >&2
fi
done
if [ -f "$MGED_XMIN_TEST_DIR/mged.log" ]; then
if [ -f "$MGED_GUI_TEST_DIR/mged.log" ]; then
echo "MGED log follows:" >&2
sed -n '1,240p' "$MGED_XMIN_TEST_DIR/mged.log" >&2
sed -n '1,240p' "$MGED_GUI_TEST_DIR/mged.log" >&2
fi
exit 1
}
if [ "${MGED_XMIN_INNER:-0}" -eq 0 ]; then
require_executable "$MGED_BIN"
require_executable "$XMIN_CTL"
if [ "${MGED_XMIN_ACTIVE_SESSION:-0}" -eq 0 ]; then
require_executable "$XMIN_RUN"
require_executable "$XMIN_SERVER"
fi
require_executable "$ICV_BIN"
require_executable "$PIXCROP_BIN"
if [ ! -f "$MGED_XMIN_RC" ]; then
echo "FAIL: MGED Xmin rc file is unavailable: $MGED_XMIN_RC" >&2
if [ "${MGED_GUI_INNER:-0}" -eq 0 ]; then
gui_require_executable "$MGED_BIN"
gui_require_executable "$GUI_TEST_CTL"
gui_require_executable "$ICV_BIN"
gui_require_executable "$PIXCROP_BIN"
if [ ! -f "$MGED_GUI_RC" ]; then
echo "FAIL: MGED GUI rc file is unavailable: $MGED_GUI_RC" >&2
exit 1
fi
if [ ! -f "$MGED_PIPE_ORACLE" ]; then
@@ -481,40 +452,36 @@ if [ "${MGED_XMIN_INNER:-0}" -eq 0 ]; then
exit 1
fi
MGED_XMIN_RUN_DIR=$(mktemp -d /tmp/mged-xmin-gui.XXXXXX)
export MGED_XMIN_RUN_DIR
MGED_GUI_RUN_DIR=$(mktemp -d "${TMPDIR:-/tmp}/mged-gui.XXXXXX")
export MGED_GUI_RUN_DIR
run_status=0
cleanup()
{
if [ -n "${MGED_PID:-}" ]; then
kill "$MGED_PID" >/dev/null 2>&1 || true
fi
if [ "${MGED_XMIN_KEEP:-0}" -eq 1 ]; then
echo "MGED Xmin test artifacts: $MGED_XMIN_RUN_DIR" >&2
if [ "${MGED_GUI_KEEP:-0}" -eq 1 ]; then
echo "MGED GUI test artifacts: $MGED_GUI_RUN_DIR" >&2
else
"$CMAKE_COMMAND" -E remove_directory "$MGED_XMIN_RUN_DIR"
"$CMAKE_COMMAND" -E remove_directory "$MGED_GUI_RUN_DIR"
fi
}
trap cleanup EXIT INT TERM
MGED_XMIN_INNER=1
export MGED_XMIN_INNER
MGED_GUI_INNER=1
export MGED_GUI_INNER
for MGED_DM_TYPE in $MGED_DM_TYPES; do
export MGED_DM_TYPE
MGED_XMIN_TEST_DIR="$MGED_XMIN_RUN_DIR/cases/$MGED_DM_TYPE"
export MGED_XMIN_TEST_DIR
"$CMAKE_COMMAND" -E make_directory "$MGED_XMIN_TEST_DIR"
XMIN_TEST_DIR=$MGED_XMIN_TEST_DIR
export XMIN_TEST_DIR
test_db="$MGED_XMIN_TEST_DIR/gui-edit.g"
MGED_GUI_TEST_DIR="$MGED_GUI_RUN_DIR/cases/$MGED_DM_TYPE"
export MGED_GUI_TEST_DIR
"$CMAKE_COMMAND" -E make_directory "$MGED_GUI_TEST_DIR"
GUI_TEST_DIR=$MGED_GUI_TEST_DIR
export GUI_TEST_DIR
test_db="$MGED_GUI_TEST_DIR/gui-edit.g"
"$MGED_BIN" -c "$test_db" "in gui.s sph 0 0 0 10" \
>"$MGED_XMIN_TEST_DIR/create.log" 2>&1
>"$MGED_GUI_TEST_DIR/create.log" 2>&1
set +e
if [ "${MGED_XMIN_ACTIVE_SESSION:-0}" -eq 1 ]; then
"$MGED_TEST_SHELL" "$0"
else
"$XMIN_RUN" --server "$XMIN_SERVER" --screen "$SCREEN_GEOMETRY" -- "$0"
fi
sh "$0"
dm_status=$?
set -e
if [ "$dm_status" -ne 0 ]; then
@@ -528,7 +495,7 @@ if [ "${MGED_XMIN_INNER:-0}" -eq 0 ]; then
fi
if [ "$sphere_vertex" != "5 0 0" ]; then
echo "FAIL: $MGED_DM_TYPE primitive edit stored V {$sphere_vertex}, expected {5 0 0}" >&2
sed -n '1,20p' "$MGED_XMIN_TEST_DIR/database_query_error" >&2
sed -n '1,20p' "$MGED_GUI_TEST_DIR/database_query_error" >&2
run_status=1
break
fi
@@ -540,7 +507,7 @@ if [ "${MGED_XMIN_INNER:-0}" -eq 0 ]; then
run_status=1
break
fi
read -r matrix_x matrix_y < "$MGED_XMIN_TEST_DIR/matrix_expected"
read -r matrix_x matrix_y < "$MGED_GUI_TEST_DIR/matrix_expected"
if query_database "$test_db" "get matrix.c tree"; then
matrix_tree=$DATABASE_VALUE
else
@@ -559,13 +526,13 @@ if [ "${MGED_XMIN_INNER:-0}" -eq 0 ]; then
recording_status=$?
set -e
if [ "$run_status" -ne 0 ] || [ "$recording_status" -ne 0 ]; then
MGED_XMIN_KEEP=1
MGED_GUI_KEEP=1
exit 1
fi
exit 0
fi
test_db="$MGED_XMIN_TEST_DIR/gui-edit.g"
test_db="$MGED_GUI_TEST_DIR/gui-edit.g"
inner_cleanup()
{
if [ -n "${MGED_PID:-}" ]; then
@@ -574,213 +541,213 @@ inner_cleanup()
}
trap inner_cleanup EXIT INT TERM
"$MGED_BIN" --gui --dm-type "$MGED_DM_TYPE" --rcfile "$MGED_XMIN_RC" \
"$MGED_BIN" --gui --dm-type "$MGED_DM_TYPE" --rcfile "$MGED_GUI_RC" \
--geom "$COMMAND_GEOMETRY" --ggeom "$GRAPHICS_GEOMETRY" "$test_db" \
>"$MGED_XMIN_TEST_DIR/mged.log" 2>&1 &
>"$MGED_GUI_TEST_DIR/mged.log" 2>&1 &
MGED_PID=$!
if ! wait_for_file "$MGED_XMIN_TEST_DIR/main_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/main_window"; then
fail "MGED main window did not become ready"
fi
main_window=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/main_window")
"$XMIN_CTL" activate "$main_window"
main_window=$(sed -n '1p' "$MGED_GUI_TEST_DIR/main_window")
"$GUI_TEST_CTL" activate "$main_window"
record_state "MGED ready"
for menu in $TOP_LEVEL_MENUS; do
menu_name=${menu%%:*}
menu_label=${menu#*:}
if ! wait_for_file "$MGED_XMIN_TEST_DIR/main_${menu_name}_menu" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/main_${menu_name}_menu_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/main_${menu_name}_menu" ||
! wait_for_file "$MGED_GUI_TEST_DIR/main_${menu_name}_menu_window"; then
fail "MGED $menu_label menu target did not become ready"
fi
click_target "main_${menu_name}_menu"
record_state "$menu_label menu"
if [ "$menu_name" = misc ]; then
if ! wait_for_file "$MGED_XMIN_TEST_DIR/faceplate_toggle" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/faceplate_toggle_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/orig_gui_toggle" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/orig_gui_toggle_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/faceplate_toggle" ||
! wait_for_file "$MGED_GUI_TEST_DIR/faceplate_toggle_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/orig_gui_toggle" ||
! wait_for_file "$MGED_GUI_TEST_DIR/orig_gui_toggle_window"; then
fail "Misc menu Faceplate targets did not become ready"
fi
fi
"$XMIN_CTL" key escape
"$GUI_TEST_CTL" key escape
done
"$XMIN_CTL" click --delay "$CLICK_DELAY" "$main_window" 100 100 1
"$XMIN_CTL" activate "$main_window"
"$GUI_TEST_CTL" click --delay "$CLICK_DELAY" "$main_window" 100 100 1
"$GUI_TEST_CTL" activate "$main_window"
if [ "$MGED_DM_TYPE" = "tkswrast" ] && [ -n "${MGED_GUI_MANIFEST:-}" ]; then
if ! wait_for_file "$MGED_XMIN_TEST_DIR/menu_inventory"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/menu_inventory"; then
fail "MGED menu inventory did not become ready"
fi
if ! "$CMAKE_COMMAND" -E compare_files \
"$MGED_XMIN_TEST_DIR/menu_inventory" "$MGED_GUI_MANIFEST"; then
"$MGED_GUI_TEST_DIR/menu_inventory" "$MGED_GUI_MANIFEST"; then
fail "MGED menu inventory differs from $MGED_GUI_MANIFEST"
fi
fi
for setting in faceplate orig_gui; do
if ! wait_for_file "$MGED_XMIN_TEST_DIR/${setting}_current"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/${setting}_current"; then
fail "MGED did not report the current $setting state"
fi
if [ "$(sed -n '1p' "$MGED_XMIN_TEST_DIR/${setting}_current")" -ne 1 ]; then
if [ "$(sed -n '1p' "$MGED_GUI_TEST_DIR/${setting}_current")" -ne 1 ]; then
click_target main_misc_menu
record_state "Misc menu $setting"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/${setting}_toggle" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/${setting}_toggle_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/${setting}_toggle" ||
! wait_for_file "$MGED_GUI_TEST_DIR/${setting}_toggle_window"; then
fail "Misc menu did not expose $setting"
fi
activate_menu_target "${setting}_toggle"
if ! wait_for_value "$MGED_XMIN_TEST_DIR/${setting}_current" 1; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/${setting}_current" 1; then
fail "$setting did not enable the in-scene Faceplate"
fi
record_state "$setting enabled"
fi
done
if ! wait_for_file "$MGED_XMIN_TEST_DIR/faceplate_header" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/faceplate_header_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/faceplate_3525" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/faceplate_3525_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/faceplate_view_initial"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/faceplate_header" ||
! wait_for_file "$MGED_GUI_TEST_DIR/faceplate_header_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/faceplate_3525" ||
! wait_for_file "$MGED_GUI_TEST_DIR/faceplate_3525_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/faceplate_view_initial"; then
fail "Faceplate targets did not become ready"
fi
faceplate_view_initial=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/faceplate_view_initial")
faceplate_view_initial=$(sed -n '1p' "$MGED_GUI_TEST_DIR/faceplate_view_initial")
click_target faceplate_header 2
if ! wait_for_file "$MGED_XMIN_TEST_DIR/faceplate_general_ready"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/faceplate_general_ready"; then
fail "Faceplate general menu did not expand"
fi
record_state "Faceplate general menu"
click_target faceplate_3525 2
if ! wait_for_not_value "$MGED_XMIN_TEST_DIR/faceplate_view_current" \
if ! wait_for_not_value "$MGED_GUI_TEST_DIR/faceplate_view_current" \
"$faceplate_view_initial"; then
fail "Faceplate 35,25 did not change the view"
fi
record_state "Faceplate 35,25 view"
click_target faceplate_header 2
if ! wait_for_file "$MGED_XMIN_TEST_DIR/renderer_capabilities" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/main_misc_menu" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/main_misc_menu_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/renderer_capabilities" ||
! wait_for_file "$MGED_GUI_TEST_DIR/main_misc_menu" ||
! wait_for_file "$MGED_GUI_TEST_DIR/main_misc_menu_window"; then
fail "renderer capability inventory did not become ready"
fi
renderer_capabilities=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/renderer_capabilities")
renderer_capabilities=$(sed -n '1p' "$MGED_GUI_TEST_DIR/renderer_capabilities")
if [ "$renderer_capabilities" != "none" ]; then
for setting in $renderer_capabilities; do
if ! wait_for_file "$MGED_XMIN_TEST_DIR/renderer_${setting}_initial"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/renderer_${setting}_initial"; then
fail "renderer did not report initial $setting state"
fi
if ! wait_for_file "$MGED_XMIN_TEST_DIR/renderer_${setting}_mutable"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/renderer_${setting}_mutable"; then
fail "renderer did not report $setting mutability"
fi
toggle_renderer_setting "$setting"
done
fi
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/stop_general_monitors"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/renderer_monitor_stopped" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/faceplate_monitor_stopped"; then
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/stop_general_monitors"
if ! wait_for_file "$MGED_GUI_TEST_DIR/renderer_monitor_stopped" ||
! wait_for_file "$MGED_GUI_TEST_DIR/faceplate_monitor_stopped"; then
fail "MGED general-interface monitors did not stop"
fi
if ! wait_for_file "$MGED_XMIN_TEST_DIR/main_edit_menu" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/main_edit_menu_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/main_edit_menu" ||
! wait_for_file "$MGED_GUI_TEST_DIR/main_edit_menu_window"; then
fail "MGED Edit menu did not become ready"
fi
click_target main_edit_menu
record_state "Edit menu"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/primitive_editor_menu" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/primitive_editor_menu_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/primitive_editor_menu" ||
! wait_for_file "$MGED_GUI_TEST_DIR/primitive_editor_menu_window"; then
fail "MGED Primitive Editor menu entry did not become ready"
fi
activate_menu_target primitive_editor_menu
if ! wait_for_file "$MGED_XMIN_TEST_DIR/editor_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/editor_window"; then
fail "Primitive Editor did not open"
fi
record_state "Primitive Editor"
editor_window=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/editor_window")
"$XMIN_CTL" activate "$editor_window"
editor_window=$(sed -n '1p' "$MGED_GUI_TEST_DIR/editor_window")
"$GUI_TEST_CTL" activate "$editor_window"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/editor_name" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/editor_name_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/editor_name_initial" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/editor_reset" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/editor_reset_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/editor_name" ||
! wait_for_file "$MGED_GUI_TEST_DIR/editor_name_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/editor_name_initial" ||
! wait_for_file "$MGED_GUI_TEST_DIR/editor_reset" ||
! wait_for_file "$MGED_GUI_TEST_DIR/editor_reset_window"; then
fail "Primitive Editor controls did not become ready"
fi
editor_name=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/editor_name_initial")
editor_name=$(sed -n '1p' "$MGED_GUI_TEST_DIR/editor_name_initial")
if ! replace_entry_text editor_name "$editor_name" \
"$MGED_XMIN_TEST_DIR/editor_name_current" gui.s; then
"$MGED_GUI_TEST_DIR/editor_name_current" gui.s; then
fail "Primitive Editor name entry did not receive gui.s"
fi
record_state "primitive name entered"
click_target editor_reset
record_state "primitive load dispatched"
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/editor_loaded"
if ! wait_for_file_with_limit "$MGED_XMIN_TEST_DIR/sphere_vx" \
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/editor_loaded"
if ! wait_for_file_with_limit "$MGED_GUI_TEST_DIR/sphere_vx" \
"$PRIMITIVE_LOAD_RETRY_LIMIT"; then
click_target editor_name
"$XMIN_CTL" key enter
"$GUI_TEST_CTL" key enter
record_state "primitive load keyboard retry"
fi
if ! wait_for_file "$MGED_XMIN_TEST_DIR/sphere_vx" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sphere_vx_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sphere_vx_initial" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/apply_button_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/apply_button"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/sphere_vx" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sphere_vx_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sphere_vx_initial" ||
! wait_for_file "$MGED_GUI_TEST_DIR/apply_button_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/apply_button"; then
fail "Primitive Editor did not load gui.s"
fi
initial_value=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/sphere_vx_initial")
initial_value=$(sed -n '1p' "$MGED_GUI_TEST_DIR/sphere_vx_initial")
if [ "$initial_value" != "0.0000" ]; then
fail "unexpected initial sphere V-x value '$initial_value'"
fi
record_state "sphere loaded"
if ! replace_entry_text sphere_vx "$initial_value" \
"$MGED_XMIN_TEST_DIR/sphere_vx_current" 5; then
"$MGED_GUI_TEST_DIR/sphere_vx_current" 5; then
fail "sphere V-x entry did not receive the expected value"
fi
record_state "sphere vertex edited"
click_target apply_button
record_state "sphere Apply dispatched"
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/primitive_applied"
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/primitive_applied"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/primitive_result"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/primitive_result"; then
fail "MGED did not report a primitive edit result"
fi
primitive_result=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/primitive_result")
primitive_result=$(sed -n '1p' "$MGED_GUI_TEST_DIR/primitive_result")
record_state "sphere edit applied"
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/begin_pipe"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_ready" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_select" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_select_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_next" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_next_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_split" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_split_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_split_point" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_split_point_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_point" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_point_window"; then
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/begin_pipe"
if ! wait_for_file "$MGED_GUI_TEST_DIR/pipe_ready" ||
! wait_for_file "$MGED_GUI_TEST_DIR/pipe_select" ||
! wait_for_file "$MGED_GUI_TEST_DIR/pipe_select_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/pipe_next" ||
! wait_for_file "$MGED_GUI_TEST_DIR/pipe_next_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/pipe_split" ||
! wait_for_file "$MGED_GUI_TEST_DIR/pipe_split_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/pipe_split_point" ||
! wait_for_file "$MGED_GUI_TEST_DIR/pipe_split_point_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/pipe_point" ||
! wait_for_file "$MGED_GUI_TEST_DIR/pipe_point_window"; then
fail "pipe Faceplate edit did not become ready"
fi
record_state "pipe contextual Faceplate"
click_target pipe_select 2
record_state "pipe Select Point mode"
click_target pipe_point 2
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/pipe_point_clicked"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_selected"; then
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/pipe_point_clicked"
if ! wait_for_file "$MGED_GUI_TEST_DIR/pipe_selected"; then
fail "pipe point was not selected through the viewport"
fi
pipe_keypoint=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/pipe_keypoint_current")
pipe_keypoint=$(sed -n '1p' "$MGED_GUI_TEST_DIR/pipe_keypoint_current")
record_state "pipe point selected"
click_target pipe_next 2
if ! wait_for_not_value "$MGED_XMIN_TEST_DIR/pipe_keypoint_current" \
if ! wait_for_not_value "$MGED_GUI_TEST_DIR/pipe_keypoint_current" \
"$pipe_keypoint"; then
fail "pipe Next Point did not change the edit keypoint"
fi
@@ -789,75 +756,75 @@ click_target pipe_split 2
record_state "pipe Split Segment mode"
click_target pipe_split_point 2
record_state "pipe segment split"
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/pipe_finish"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/pipe_done"; then
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/pipe_finish"
if ! wait_for_file "$MGED_GUI_TEST_DIR/pipe_done"; then
fail "pipe edit did not accept cleanly"
fi
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/begin_sketch"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_ready" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_create_line" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_create_line_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_zoom_in" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_zoom_in_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_reset" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_reset_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_dismiss" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_dismiss_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_canvas_start" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_canvas_start_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_canvas_end" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_canvas_end_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_scale_current" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_vertex_count_current"; then
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/begin_sketch"
if ! wait_for_file "$MGED_GUI_TEST_DIR/sketch_ready" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_create_line" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_create_line_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_zoom_in" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_zoom_in_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_reset" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_reset_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_dismiss" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_dismiss_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_canvas_start" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_canvas_start_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_canvas_end" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_canvas_end_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_scale_current" ||
! wait_for_file "$MGED_GUI_TEST_DIR/sketch_vertex_count_current"; then
fail "Sketch Editor controls did not become ready"
fi
record_state "Sketch Editor ready"
sketch_scale=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/sketch_scale_current")
sketch_scale=$(sed -n '1p' "$MGED_GUI_TEST_DIR/sketch_scale_current")
click_target sketch_zoom_in
if ! wait_for_not_value "$MGED_XMIN_TEST_DIR/sketch_scale_current" \
if ! wait_for_not_value "$MGED_GUI_TEST_DIR/sketch_scale_current" \
"$sketch_scale"; then
fail "Sketch Editor Zoom In did not change the canvas scale"
fi
record_state "Sketch Editor zoomed"
sketch_vertex_count=$(sed -n '1p' \
"$MGED_XMIN_TEST_DIR/sketch_vertex_count_current")
"$MGED_GUI_TEST_DIR/sketch_vertex_count_current")
click_target sketch_create_line
record_state "Sketch Editor Create Line mode"
click_target sketch_canvas_start
record_state "Sketch Editor line start"
click_target sketch_canvas_end
if ! wait_for_not_value "$MGED_XMIN_TEST_DIR/sketch_vertex_count_current" \
if ! wait_for_not_value "$MGED_GUI_TEST_DIR/sketch_vertex_count_current" \
"$sketch_vertex_count"; then
fail "Sketch Editor did not add vertices for the new line"
fi
record_state "Sketch Editor line created"
click_target sketch_reset
if ! wait_for_value "$MGED_XMIN_TEST_DIR/sketch_vertex_count_current" \
if ! wait_for_value "$MGED_GUI_TEST_DIR/sketch_vertex_count_current" \
"$sketch_vertex_count"; then
fail "Sketch Editor Reset Sketch did not restore the fixture"
fi
record_state "Sketch Editor reset"
click_target sketch_dismiss
if ! wait_for_file "$MGED_XMIN_TEST_DIR/sketch_closed"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/sketch_closed"; then
fail "Sketch Editor did not dismiss cleanly"
fi
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/begin_matrix"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/matrix_dm_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/matrix_dm"; then
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/begin_matrix"
if ! wait_for_file "$MGED_GUI_TEST_DIR/matrix_dm_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/matrix_dm"; then
fail "MGED did not enter matrix edit mode"
fi
record_state "matrix edit ready"
click_target matrix_dm 2
record_state "matrix edit moved"
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/matrix_clicked"
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/matrix_clicked"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/matrix_result"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/matrix_result"; then
fail "MGED did not report a matrix edit result"
fi
matrix_result=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/matrix_result")
matrix_result=$(sed -n '1p' "$MGED_GUI_TEST_DIR/matrix_result")
case "$matrix_result" in
PASS:*) ;;
*)
@@ -870,39 +837,39 @@ capture_dm_snapshot raytrace_baseline
click_target main_tools_menu
record_state "Tools menu embedded raytrace"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_control_menu" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_control_menu_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_control_menu" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_control_menu_window"; then
fail "Tools menu did not expose the Raytrace Control Panel"
fi
activate_menu_target raytrace_control_menu
if ! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_ready" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_size" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_size_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_destination" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_destination_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_button" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_button_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_active" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_active_window" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_dismiss" ||
! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_dismiss_window"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_ready" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_size" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_size_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_destination" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_destination_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_button" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_button_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_active" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_active_window" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_dismiss" ||
! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_dismiss_window"; then
fail "Raytrace Control Panel controls did not become ready"
fi
record_state "Raytrace Control Panel"
raytrace_size=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/raytrace_size_initial")
raytrace_size=$(sed -n '1p' "$MGED_GUI_TEST_DIR/raytrace_size_initial")
if ! replace_entry_text raytrace_size "$raytrace_size" \
"$MGED_XMIN_TEST_DIR/raytrace_size_current" "$RAYTRACE_SIZE"; then
"$MGED_GUI_TEST_DIR/raytrace_size_current" "$RAYTRACE_SIZE"; then
fail "Raytrace Control Panel size entry did not accept $RAYTRACE_SIZE"
fi
record_state "embedded raytrace size $RAYTRACE_SIZE"
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/raytrace_prepare_reference"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_reference_ready"; then
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/raytrace_prepare_reference"
if ! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_reference_ready"; then
fail "MGED did not prepare the standalone raytrace reference"
fi
reference_script=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/raytrace_reference_script")
reference_image=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/raytrace_reference_image")
reference_script=$(sed -n '1p' "$MGED_GUI_TEST_DIR/raytrace_reference_script")
reference_image=$(sed -n '1p' "$MGED_GUI_TEST_DIR/raytrace_reference_image")
if ! /bin/sh "$reference_script"; then
fail "standalone reference raytrace failed"
fi
@@ -912,7 +879,7 @@ fi
record_state "standalone raytrace reference complete"
start_raytrace 1
record_state "embedded raytrace started"
if ! wait_for_value "$MGED_XMIN_TEST_DIR/raytrace_complete" 1; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/raytrace_complete" 1; then
fail "embedded raytrace did not complete"
fi
assert_raytrace_duration 1 "embedded raytrace"
@@ -927,7 +894,7 @@ for mode in overlay interlay underlay; do
click_target raytrace_framebuffer_menu
record_state "Raytrace Framebuffer menu $mode"
activate_menu_target "raytrace_$mode"
if ! wait_for_value "$MGED_XMIN_TEST_DIR/raytrace_overlay_current" "$layer"; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/raytrace_overlay_current" "$layer"; then
fail "embedded framebuffer did not enter $mode mode"
fi
record_state "embedded framebuffer $mode"
@@ -935,14 +902,14 @@ for mode in overlay interlay underlay; do
done
assert_images_differ "embedded raytrace overlay" \
"$MGED_XMIN_TEST_DIR/raytrace_baseline.ppm" \
"$MGED_XMIN_TEST_DIR/raytrace_overlay.ppm"
"$MGED_GUI_TEST_DIR/raytrace_baseline.ppm" \
"$MGED_GUI_TEST_DIR/raytrace_overlay.ppm"
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/raytrace_export_requested"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_export_ready"; then
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/raytrace_export_requested"
if ! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_export_ready"; then
fail "MGED did not export the embedded framebuffer"
fi
embedded_image=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/raytrace_export_image")
embedded_image=$(sed -n '1p' "$MGED_GUI_TEST_DIR/raytrace_export_image")
if [ ! -s "$embedded_image" ]; then
fail "MGED exported an empty embedded framebuffer image"
fi
@@ -955,42 +922,42 @@ record_state "embedded framebuffer matches standalone rt"
# visible framebuffer image to remove.
click_target raytrace_framebuffer_menu
activate_menu_target raytrace_overlay
if ! wait_for_value "$MGED_XMIN_TEST_DIR/raytrace_overlay_current" 2; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/raytrace_overlay_current" 2; then
fail "embedded framebuffer did not return to overlay mode"
fi
capture_dm_snapshot raytrace_active
if ! wait_for_value "$MGED_XMIN_TEST_DIR/raytrace_fb_current" 1; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/raytrace_fb_current" 1; then
fail "embedded framebuffer was not active after raytracing"
fi
click_target raytrace_active
if ! wait_for_value "$MGED_XMIN_TEST_DIR/raytrace_fb_current" 0; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/raytrace_fb_current" 0; then
fail "Raytrace Control Panel did not deactivate the framebuffer"
fi
record_state "embedded framebuffer inactive"
wait_for_changed_dm_snapshot "framebuffer active toggle" \
"$MGED_XMIN_TEST_DIR/raytrace_active.ppm" raytrace_inactive
"$MGED_GUI_TEST_DIR/raytrace_active.ppm" raytrace_inactive
click_target raytrace_active
if ! wait_for_value "$MGED_XMIN_TEST_DIR/raytrace_fb_current" 1; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/raytrace_fb_current" 1; then
fail "Raytrace Control Panel did not reactivate the framebuffer"
fi
record_state "embedded framebuffer reactivated"
file_destination=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/raytrace_file_destination")
embedded_destination=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/raytrace_destination_initial")
file_destination=$(sed -n '1p' "$MGED_GUI_TEST_DIR/raytrace_file_destination")
embedded_destination=$(sed -n '1p' "$MGED_GUI_TEST_DIR/raytrace_destination_initial")
if ! replace_entry_text raytrace_destination "$embedded_destination" \
"$MGED_XMIN_TEST_DIR/raytrace_destination_current" "$file_destination"; then
"$MGED_GUI_TEST_DIR/raytrace_destination_current" "$file_destination"; then
fail "Raytrace Control Panel did not accept a file destination"
fi
record_state "raytrace file destination"
file_render_size=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/raytrace_size_current")
file_render_size=$(sed -n '1p' "$MGED_GUI_TEST_DIR/raytrace_size_current")
if ! replace_entry_text raytrace_size "$file_render_size" \
"$MGED_XMIN_TEST_DIR/raytrace_size_current" "$RAYTRACE_SIZE"; then
"$MGED_GUI_TEST_DIR/raytrace_size_current" "$RAYTRACE_SIZE"; then
fail "Raytrace Control Panel did not restore size $RAYTRACE_SIZE for the file render"
fi
record_state "raytrace file size $RAYTRACE_SIZE"
start_raytrace 2
record_state "GUI file raytrace started"
if ! wait_for_value "$MGED_XMIN_TEST_DIR/raytrace_complete" 2; then
if ! wait_for_value "$MGED_GUI_TEST_DIR/raytrace_complete" 2; then
fail "GUI file raytrace did not complete"
fi
assert_raytrace_duration 2 "GUI file raytrace"
@@ -1000,25 +967,25 @@ fi
assert_raw_images_equal "GUI file raytrace" "$reference_image" "$file_destination"
record_state "GUI raytrace matches standalone rt"
if ! replace_entry_text raytrace_destination "$file_destination" \
"$MGED_XMIN_TEST_DIR/raytrace_destination_current" "$embedded_destination"; then
"$MGED_GUI_TEST_DIR/raytrace_destination_current" "$embedded_destination"; then
fail "Raytrace Control Panel did not restore the embedded destination"
fi
record_state "embedded raytrace destination restored"
click_target raytrace_dismiss
if ! wait_for_file "$MGED_XMIN_TEST_DIR/raytrace_closed"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/raytrace_closed"; then
fail "Raytrace Control Panel did not dismiss cleanly"
fi
"$XMIN_CTL" activate "$main_window"
"$GUI_TEST_CTL" activate "$main_window"
click_target main_view_menu
record_state "post-raytrace View menu responsiveness"
"$XMIN_CTL" key escape
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/raytrace_responsive"
"$GUI_TEST_CTL" key escape
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/raytrace_responsive"
if ! wait_for_file "$MGED_XMIN_TEST_DIR/result"; then
if ! wait_for_file "$MGED_GUI_TEST_DIR/result"; then
fail "MGED did not remain responsive after embedded raytrace"
fi
result=$(sed -n '1p' "$MGED_XMIN_TEST_DIR/result")
result=$(sed -n '1p' "$MGED_GUI_TEST_DIR/result")
case "$result" in
PASS:*) ;;
*)
@@ -1026,7 +993,7 @@ case "$result" in
;;
esac
"$CMAKE_COMMAND" -E touch "$MGED_XMIN_TEST_DIR/finish"
"$CMAKE_COMMAND" -E touch "$MGED_GUI_TEST_DIR/finish"
wait "$MGED_PID"
MGED_PID=""
+24 -24
View File
@@ -20,35 +20,35 @@
###
# Exercise MGED's BoT editor through its live controls.
source $::env(MGED_XMIN_GUI_LIBRARY)
source $::env(MGED_GUI_TEST_LIBRARY)
namespace eval ::mged::xmin::bot {}
proc ::mged::xmin::bot::find_combobox {root first_value} {
foreach widget [::xmin::test::descendants $root] {
foreach widget [::gui::test::descendants $root] {
if {[winfo class $widget] eq "TCombobox" &&
[lindex [$widget cget -values] 0] eq $first_value} {
return $widget
}
}
::mged::xmin::test::fail \
::mged::gui::test::fail \
"$root has no combobox beginning with '$first_value'"
}
proc ::mged::xmin::bot::confirm_action {editor button_text} {
set button [::mged::xmin::test::find_widget_by_text $editor $button_text]
set button [::mged::gui::test::find_widget_by_text $editor $button_text]
$button invoke
::mged::xmin::test::settle
::mged::gui::test::settle
set confirmation $editor.confirmDialog
::mged::xmin::test::require_mapped $confirmation \
::mged::gui::test::require_mapped $confirmation \
"$button_text confirmation"
[::mged::xmin::test::find_widget_by_text $confirmation Yes] invoke
::mged::xmin::test::settle
[::mged::gui::test::find_widget_by_text $confirmation Yes] invoke
::mged::gui::test::settle
}
proc ::mged::xmin::bot::require_counts {bot vertices faces description} {
::xmin::test::require {
::gui::test::require {
[llength [get $bot V]] == $vertices &&
[llength [get $bot F]] == $faces
} "$description does not have $vertices vertices and $faces faces"
@@ -59,17 +59,17 @@ proc ::mged::xmin::bot::exercise_editor {id top} {
{0 0 0} {100 0 0} {0 100 0} {0 0 0} {999 999 999}
} F {{0 1 2} {3 1 2}}
::mged::xmin::test::with_dialog_answer .$id.botname "BoT to Edit" \
::mged::gui::test::with_dialog_answer .$id.botname "BoT to Edit" \
.$id.botname.mid.ent xmin_bot.s .$id.botname.bot.button0 \
[list ::mged::xmin::test::invoke $top {Tools {BoT Edit Tool}}]
[list ::mged::gui::test::invoke $top {Tools {BoT Edit Tool}}]
set editor .botedit
::mged::xmin::test::require_mapped $editor "BoT Edit Tool"
::mged::gui::test::require_mapped $editor "BoT Edit Tool"
require_counts xmin_bot.s.edit 5 2 "initial working BoT"
set mode [find_combobox $editor Surface]
set orientation [find_combobox $editor Unoriented]
::xmin::test::require {
::gui::test::require {
[$mode current] == 0 && [$orientation current] == 0
} "BoT Editor did not display the initial mode and orientation"
@@ -77,18 +77,18 @@ proc ::mged::xmin::bot::exercise_editor {id top} {
event generate $mode <<ComboboxSelected>>
$orientation current 1
event generate $orientation <<ComboboxSelected>>
::xmin::test::require {
::gui::test::require {
[bot get type xmin_bot.s.edit] eq "solid" &&
[bot get orientation xmin_bot.s.edit] eq "ccw"
} "BoT Editor did not apply mode and orientation changes"
[::mged::xmin::test::find_widget_by_text $editor {Remove Selected}] invoke
::mged::xmin::test::settle
[::mged::gui::test::find_widget_by_text $editor {Remove Selected}] invoke
::mged::gui::test::settle
require_counts xmin_bot.s.edit 3 1 "simplified working BoT"
confirm_action $editor {Start Over}
require_counts xmin_bot.s.edit 5 2 "reverted working BoT"
::xmin::test::require {
::gui::test::require {
[bot get type xmin_bot.s.edit] eq "surface" &&
[bot get orientation xmin_bot.s.edit] eq "none" &&
[$mode current] == 0 && [$orientation current] == 0
@@ -102,13 +102,13 @@ proc ::mged::xmin::bot::exercise_editor {id top} {
{Remove Unused Vertices} {Remove Duplicate Vertices}
{Remove Duplicate Faces}
} {
[::mged::xmin::test::find_widget_by_text $editor $option] invoke
[::mged::gui::test::find_widget_by_text $editor $option] invoke
}
[::mged::xmin::test::find_widget_by_text $editor {Remove Selected}] invoke
::mged::xmin::test::settle
[::mged::gui::test::find_widget_by_text $editor {Remove Selected}] invoke
::mged::gui::test::settle
confirm_action $editor Accept
::xmin::test::require {
::gui::test::require {
![winfo exists $editor] && ![exists xmin_bot.s.edit] &&
[bot get type xmin_bot.s] eq "solid" &&
[bot get orientation xmin_bot.s] eq "ccw"
@@ -117,8 +117,8 @@ proc ::mged::xmin::bot::exercise_editor {id top} {
}
proc ::mged::xmin::bot::run {id top} {
set database [file join $::env(XMIN_TEST_DIR) bot.g]
cd $::env(XMIN_TEST_DIR)
set database [file join $::env(GUI_TEST_DIR) bot.g]
cd $::env(GUI_TEST_DIR)
file delete -force $database
opendb $database y
title {Xmin MGED BoT editor regression}
@@ -126,7 +126,7 @@ proc ::mged::xmin::bot::run {id top} {
exercise_editor $id $top
}
::mged::xmin::test::start ::mged::xmin::bot::run \
::mged::gui::test::start ::mged::xmin::bot::run \
{MGED BoT editor} {MGED BoT editor regression}
# Local Variables:
+36 -36
View File
@@ -20,13 +20,13 @@
###
# Exercise stateful MGED control panels through their live Tk widgets.
if {![info exists ::env(XMIN_GUI_LIBRARY)] ||
![info exists ::env(XMIN_TEST_DIR)]} {
puts stderr "XMIN_GUI_LIBRARY and XMIN_TEST_DIR are required"
if {![info exists ::env(GUI_TEST_LIBRARY)] ||
![info exists ::env(GUI_TEST_DIR)]} {
puts stderr "GUI_TEST_LIBRARY and GUI_TEST_DIR are required"
exit 2
}
source $::env(XMIN_GUI_LIBRARY)
::xmin::test::capture_background_errors
source $::env(GUI_TEST_LIBRARY)
::gui::test::capture_background_errors
namespace eval ::mged::xmin {
variable adc_distance_tolerance 0.1
@@ -84,10 +84,10 @@ proc ::mged::xmin::set_view {azimuth elevation center_point view_size} {
}
proc ::mged::xmin::invoke {root labels} {
::xmin::test::write progress "invoke: [join $labels { > }]"
::xmin::test::invoke_menu_entry $root $labels
::gui::test::write progress "invoke: [join $labels { > }]"
::gui::test::invoke_menu_entry $root $labels
settle
::xmin::test::write progress "invoked: [join $labels { > }]"
::gui::test::write progress "invoked: [join $labels { > }]"
}
proc ::mged::xmin::exercise_view_ring {id top} {
@@ -99,7 +99,7 @@ proc ::mged::xmin::exercise_view_ring {id top} {
set second [set_view 40 30 {-4 5 6} 200]
invoke $top {ViewRing {Add View}}
set second_id $view_ring($id)
::xmin::test::require {[llength $mged_gui($id,views)] == 2} \
::gui::test::require {[llength $mged_gui($id,views)] == 2} \
"ViewRing did not store two views"
set_view 0 0 {0 0 0} 50
@@ -118,9 +118,9 @@ proc ::mged::xmin::exercise_view_ring {id top} {
set delete_menu $top.menubar.viewring.delete
$delete_menu invoke 0
::xmin::test::require {[llength $mged_gui($id,views)] == 1} \
::gui::test::require {[llength $mged_gui($id,views)] == 1} \
"ViewRing did not delete the selected stored view"
::xmin::test::require {$view_ring($id) == $second_id} \
::gui::test::require {$view_ring($id) == $second_id} \
"ViewRing deletion left its selection on a deleted view"
invoke $top {ViewRing {Next View}}
require_near [view_state] $second "ViewRing traversal after deletion"
@@ -131,9 +131,9 @@ proc ::mged::xmin::exercise_grid {id top} {
invoke $top {Tools {Grid Control Panel}}
set panel $top.grid_control
::xmin::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
::gui::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
"Grid Control Panel did not open"
::xmin::test::require {[wm title $panel] eq "Grid Control Panel ($id)"} \
::gui::test::require {[wm title $panel] eq "Grid Control Panel ($id)"} \
"Grid Control Panel has an unexpected title"
foreach setting {anchor rh rv mrh mrv draw snap} {
@@ -154,7 +154,7 @@ proc ::mged::xmin::exercise_grid {id top} {
require_near [rset grid rv] {3.5} "vertical grid spacing"
require_near [rset grid mrh] {7} "horizontal major-grid spacing"
require_near [rset grid mrv] {9} "vertical major-grid spacing"
::xmin::test::require {
::gui::test::require {
[rset grid draw] == 1 && [rset grid snap] == 1 &&
$mged_gui($id,grid_draw) == 1 && $mged_gui($id,grid_snap) == 1
} "Grid Control Panel did not synchronize its render and GUI state"
@@ -169,7 +169,7 @@ proc ::mged::xmin::exercise_grid {id top} {
$panel.applyB invoke
$panel.resetB invoke
$panel.dismissB invoke
::xmin::test::require {![winfo exists $panel]} \
::gui::test::require {![winfo exists $panel]} \
"Grid Control Panel did not dismiss"
}
@@ -179,9 +179,9 @@ proc ::mged::xmin::exercise_adc {id top} {
invoke $top {Tools {ADC Control Panel}}
set panel $top.adc_control
::xmin::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
::gui::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
"ADC Control Panel did not open"
::xmin::test::require {[wm title $panel] eq "ADC Control Panel ($id)"} \
::gui::test::require {[wm title $panel] eq "ADC Control Panel ($id)"} \
"ADC Control Panel has an unexpected title"
set reset_position [adc xyz]
@@ -205,18 +205,18 @@ proc ::mged::xmin::exercise_adc {id top} {
$adc_distance_tolerance
require_near [adc a1] {15} "ADC first angle"
require_near [adc a2] {75} "ADC second angle"
::xmin::test::require {[adc draw] == 1} \
::gui::test::require {[adc draw] == 1} \
"ADC Control Panel did not enable the cursor"
$panel.resetB invoke
require_near [adc xyz] $reset_position "reset ADC position"
::xmin::test::require {
::gui::test::require {
[adc draw] == 1 && $mged_adc_control($id,draw) == 1
} "ADC Control Panel reset did not preserve and reload the draw setting"
set mged_adc_control($id,draw) 0
$panel.applyB invoke
$panel.dismissB invoke
::xmin::test::require {![winfo exists $panel]} \
::gui::test::require {![winfo exists $panel]} \
"ADC Control Panel did not dismiss"
}
@@ -225,42 +225,42 @@ proc ::mged::xmin::exercise_font_persistence {id top} {
invoke $top {File Preferences Fonts}
set panel $top.font_scheme
::xmin::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
::gui::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
"Fonts panel did not open"
::xmin::test::require {[wm title $panel] eq "Fonts"} \
::gui::test::require {[wm title $panel] eq "Fonts"} \
"Fonts panel has an unexpected title"
set text_menu $panel._TextMB.menu
set font_index [$text_menu index {courier 18}]
::xmin::test::require {$font_index ne "none"} \
::gui::test::require {$font_index ne "none"} \
"Fonts panel does not offer courier 18"
$text_menu invoke $font_index
$panel.applyB invoke
::xmin::test::require {[font configure text_font -size] == 18} \
::gui::test::require {[font configure text_font -size] == 18} \
"Fonts panel did not apply the text-font size"
::xmin::test::require {[dict get $mged_default(text_font) -size] == 18} \
::gui::test::require {[dict get $mged_default(text_font) -size] == 18} \
"Fonts panel did not update the persisted text-font setting"
invoke $top {File {Create/Update .mgedrc}}
set rcfile [file join $::env(XMIN_TEST_DIR) .mgedrc]
::xmin::test::require {[file exists $rcfile]} \
set rcfile [file join $::env(GUI_TEST_DIR) .mgedrc]
::gui::test::require {[file exists $rcfile]} \
"Create/Update .mgedrc did not create the isolated preferences file"
set channel [open $rcfile r]
set contents [read $channel]
close $channel
set expected [list set mged_default(text_font) $mged_default(text_font)]
::xmin::test::require {[lsearch -exact [split $contents \n] $expected] >= 0} \
::gui::test::require {[lsearch -exact [split $contents \n] $expected] >= 0} \
".mgedrc did not contain the applied text-font setting"
$panel.dismissB invoke
::xmin::test::require {![winfo exists $panel]} \
::gui::test::require {![winfo exists $panel]} \
"Fonts panel did not dismiss"
}
proc ::mged::xmin::dismiss_about {expected_title} {
variable about_seen
foreach widget [linsert [::xmin::test::descendants .] 0 .] {
foreach widget [linsert [::gui::test::descendants .] 0 .] {
if {[winfo toplevel $widget] ne $widget ||
![winfo ismapped $widget] || [wm title $widget] ne $expected_title} {
continue
@@ -281,7 +281,7 @@ proc ::mged::xmin::exercise_about {top} {
set about_seen ""
after 25 [list ::mged::xmin::dismiss_about $expected_title]
invoke $top {Help {About MGED}}
::xmin::test::require {$about_seen eq $expected_title} \
::gui::test::require {$about_seen eq $expected_title} \
"About MGED did not display its dialog"
}
@@ -292,10 +292,10 @@ proc ::mged::xmin::finish {status message} {
}
set finished 1
catch {update}
lassign [::xmin::test::check_background_errors $status $message] \
lassign [::gui::test::check_background_errors $status $message] \
status message
puts $message
::xmin::test::write result $message
::gui::test::write result $message
if {$status != 0} {
puts stderr $message
}
@@ -331,8 +331,8 @@ proc ::mged::xmin::run {} {
set version [package require $package $minimum]
lappend versions "$package $version"
}
::xmin::test::write package_versions [join $versions \n]
set ::env(HOME) $::env(XMIN_TEST_DIR)
::gui::test::write package_versions [join $versions \n]
set ::env(HOME) $::env(GUI_TEST_DIR)
exercise_view_ring $id $top
exercise_grid $id $top
@@ -348,7 +348,7 @@ proc ::mged::xmin::run_checked {} {
if {[dict exists $options -errorinfo]} {
set error_info [dict get $options -errorinfo]
puts stderr $error_info
::xmin::test::write tcl_error_debug $error_info
::gui::test::write tcl_error_debug $error_info
}
finish 1 "FAIL: MGED control-panel regression: $message"
}
+6 -6
View File
@@ -23,8 +23,8 @@
# any GUI action or database edit under test: menu, entry, button, and display
# actions arrive through the X server.
set xmin_test_dir $::env(MGED_XMIN_TEST_DIR)
source $::env(XMIN_GUI_LIBRARY)
set xmin_test_dir $::env(MGED_GUI_TEST_DIR)
source $::env(GUI_TEST_LIBRARY)
set xmin_deadline_ms 240000
set xmin_poll_ms 25
@@ -45,11 +45,11 @@ set xmin_pipe_split_y 948.0
set xmin_raytrace_completion_settle_ms 500
proc xmin_write {name contents} {
::xmin::test::write $name $contents
::gui::test::write $name $contents
}
proc xmin_publish_target {name widget} {
::xmin::test::publish_target $name $widget
::gui::test::publish_target $name $widget
}
proc xmin_publish_faceplate_target {name widget bv_x bv_y} {
@@ -75,11 +75,11 @@ proc xmin_publish_model_point_target {name widget model_point} {
}
proc xmin_descendants {widget} {
::xmin::test::descendants $widget
::gui::test::descendants $widget
}
proc xmin_find_widget {root class text} {
::xmin::test::find_widget $root $class $text
::gui::test::find_widget $root $class $text
}
proc xmin_menu_inventory_walk {menu path inventory_name visited_name} {
+40 -39
View File
@@ -20,11 +20,12 @@
###
# Exercise MGED's Combination and Attribute editors through live widgets.
source $::env(MGED_XMIN_GUI_LIBRARY)
source $::env(MGED_GUI_TEST_LIBRARY)
source [file join [file dirname $::env(GUI_TEST_LIBRARY)] sketch_test.tcl]
namespace eval ::mged::xmin::editors {}
proc ::mged::xmin::editors::confirm_combination_overwrite {button} {
::mged::xmin::test::with_dialog_answer .mged_dialog Warning! "" "" \
::mged::gui::test::with_dialog_answer .mged_dialog Warning! "" "" \
.mged_dialog.bot.button0 [list $button invoke]
}
@@ -32,18 +33,18 @@ proc ::mged::xmin::editors::confirm_combination_overwrite {button} {
proc ::mged::xmin::editors::exercise_combination {id top} {
global comb_control
::mged::xmin::test::invoke $top {Edit {Combination Editor}}
::mged::gui::test::invoke $top {Edit {Combination Editor}}
set editor .$id.comb
::xmin::test::require {
::gui::test::require {
[winfo exists $editor] && [winfo ismapped $editor]
} "Combination Editor did not open"
::mged::xmin::test::set_entry $editor.nameE xmin_group.c
::mged::gui::test::set_entry $editor.nameE xmin_group.c
focus $editor.nameE
event generate $editor.nameE <KeyPress> -keysym Return
::mged::xmin::test::settle
::mged::gui::test::settle
set original_expression [string trim [$editor.combT get 1.0 end]]
::xmin::test::require {
::gui::test::require {
[string first "xmin_left.s" $original_expression] >= 0 &&
[string first "xmin_right.s" $original_expression] >= 0
} "Combination Editor did not load the selected combination tree"
@@ -56,10 +57,10 @@ proc ::mged::xmin::editors::exercise_combination {id top} {
set comb_control($id,inherit) Yes
$editor.shaderMB.m invoke plastic
confirm_combination_overwrite $editor.applyB
::mged::xmin::test::settle
::mged::gui::test::settle
set values [get_comb xmin_group.c]
::xmin::test::require {
::gui::test::require {
[lindex $values 1] eq "10 20 30" &&
[lindex [lindex $values 2] 0] eq "plastic" &&
[lindex $values 3] eq "Yes" &&
@@ -69,8 +70,8 @@ proc ::mged::xmin::editors::exercise_combination {id top} {
$editor.combT delete 1.0 end
$editor.combT insert end {u xmin_left.s}
$editor.resetB invoke
::mged::xmin::test::settle
::xmin::test::require {
::mged::gui::test::settle
::gui::test::require {
[string first "- xmin_right.s" [$editor.combT get 1.0 end]] >= 0
} "Combination Editor Reset did not reload database state"
@@ -81,7 +82,7 @@ proc ::mged::xmin::editors::exercise_combination {id top} {
set comb_control($id,los) 80
confirm_combination_overwrite $editor.applyB
set values [get_comb xmin_group.c]
::xmin::test::require {
::gui::test::require {
[lindex $values 5] eq "Yes" &&
[lindex $values 6] == 1201 &&
[lindex $values 7] == 0 &&
@@ -90,27 +91,27 @@ proc ::mged::xmin::editors::exercise_combination {id top} {
} "Combination Editor did not persist region properties"
confirm_combination_overwrite $editor.okB
::xmin::test::require {![winfo exists $editor]} \
::gui::test::require {![winfo exists $editor]} \
"Combination Editor did not close after OK"
}
proc ::mged::xmin::editors::load_attribute_object {editor object_name} {
set object_entry $editor.fr_obj.obj_e
::mged::xmin::test::set_entry $object_entry $object_name
::mged::gui::test::set_entry $object_entry $object_name
focus $object_entry
event generate $object_entry <KeyPress> -keysym Return
::mged::xmin::test::settle
::mged::gui::test::settle
}
proc ::mged::xmin::editors::select_attribute {listbox name} {
set index [lsearch -exact [$listbox get 0 end] $name]
if {$index < 0} {
::mged::xmin::test::fail "Attribute Editor did not list '$name'"
::mged::gui::test::fail "Attribute Editor did not list '$name'"
}
$listbox selection clear 0 end
$listbox selection set $index
event generate $listbox <<ListboxSelect>>
::mged::xmin::test::settle
::mged::gui::test::settle
}
proc ::mged::xmin::editors::set_attribute_value {text value} {
@@ -118,83 +119,83 @@ proc ::mged::xmin::editors::set_attribute_value {text value} {
$text delete 1.0 end
$text insert end $value
event generate $text <KeyRelease> -keysym a
::mged::xmin::test::settle
::mged::gui::test::settle
}
proc ::mged::xmin::editors::exercise_attributes {id top} {
::mged::xmin::test::invoke $top {Edit {Attribute Editor}}
::mged::gui::test::invoke $top {Edit {Attribute Editor}}
set editor .${id}_attr_edit
::xmin::test::require {
::gui::test::require {
[llength [info commands $editor]] == 1 &&
[winfo exists $editor] && [winfo ismapped $editor]
} "Attribute Editor did not open"
load_attribute_object $editor xmin_attr.s
set listbox [::mged::xmin::test::find_descendant $editor Listbox]
set value_text [::mged::xmin::test::find_descendant $editor Text]
set listbox [::mged::gui::test::find_descendant $editor Listbox]
set value_text [::mged::gui::test::find_descendant $editor Text]
select_attribute $listbox existing
::xmin::test::require {
::gui::test::require {
[string trim [$value_text get 1.0 end]] eq "original"
} "Attribute Editor did not load the selected attribute value"
set_attribute_value $value_text pending
$editor.fr_attr.frc1.reset_sel invoke
::xmin::test::require {
::gui::test::require {
[string trim [$value_text get 1.0 end]] eq "original"
} "Attribute Editor reset selected did not restore database state"
set_attribute_value $value_text changed
$editor.frc.apply invoke
::xmin::test::require {[attr get xmin_attr.s existing] eq "changed"} \
::gui::test::require {[attr get xmin_attr.s existing] eq "changed"} \
"Attribute Editor Apply did not persist an edited value"
$editor.fr_attr.fr_new.new invoke
set name_entry $editor.fr_attr.fr_new.attr_e
::mged::xmin::test::set_entry $name_entry added
::mged::gui::test::set_entry $name_entry added
focus $name_entry
event generate $name_entry <KeyPress> -keysym Return
::mged::xmin::test::settle
::mged::gui::test::settle
set_attribute_value $value_text {new value}
$editor.frc.apply invoke
::xmin::test::require {[attr get xmin_attr.s added] eq "new value"} \
::gui::test::require {[attr get xmin_attr.s added] eq "new value"} \
"Attribute Editor did not create a new attribute"
select_attribute $listbox added
$editor.fr_attr.frc1.delete_selected invoke
$editor.frc.apply invoke
::xmin::test::require {[catch {attr get xmin_attr.s added}]} \
::gui::test::require {[catch {attr get xmin_attr.s added}]} \
"Attribute Editor did not delete the selected attribute"
select_attribute $listbox existing
set_attribute_value $value_text discarded
$editor.fr_attr.frc1.reset_all invoke
select_attribute $listbox existing
::xmin::test::require {
::gui::test::require {
[string trim [$value_text get 1.0 end]] eq "changed"
} "Attribute Editor reset all did not discard pending changes"
set_attribute_value $value_text final
$editor.frc.ok invoke
::xmin::test::require {
::gui::test::require {
![winfo exists $editor] && [attr get xmin_attr.s existing] eq "final"
} "Attribute Editor OK did not persist and close"
::mged::xmin::test::invoke $top {Edit {Attribute Editor}}
::mged::gui::test::invoke $top {Edit {Attribute Editor}}
load_attribute_object $editor xmin_attr.s
set listbox [::mged::xmin::test::find_descendant $editor Listbox]
set listbox [::mged::gui::test::find_descendant $editor Listbox]
$editor do_new_obj
set value_text [::mged::xmin::test::find_descendant $editor Text]
set value_text [::mged::gui::test::find_descendant $editor Text]
select_attribute $listbox existing
set_attribute_value $value_text unsaved
$editor.frc.dismiss invoke
::xmin::test::require {
::gui::test::require {
![winfo exists $editor] && [attr get xmin_attr.s existing] eq "final"
} "Attribute Editor Dismiss saved a pending edit or failed to close"
}
proc ::mged::xmin::editors::run {id top} {
set database [file join $::env(XMIN_TEST_DIR) editors.g]
cd $::env(XMIN_TEST_DIR)
set database [file join $::env(GUI_TEST_DIR) editors.g]
cd $::env(GUI_TEST_DIR)
file delete -force $database
opendb $database y
title {Xmin MGED editor regression}
@@ -204,12 +205,12 @@ proc ::mged::xmin::editors::run {id top} {
make xmin_attr.s arb8
attr set xmin_attr.s existing original
::xmin::test::exercise_sketch_segments Sketch_carc Sketch_bezier MGED ::mged::xmin::editors
::gui::test::exercise_sketch_segments Sketch_carc Sketch_bezier MGED ::mged::xmin::editors
exercise_combination $id $top
exercise_attributes $id $top
}
::mged::xmin::test::start ::mged::xmin::editors::run \
::mged::gui::test::start ::mged::xmin::editors::run \
{MGED Combination and Attribute editors} {MGED editor regression}
# Local Variables:
+31 -31
View File
@@ -20,7 +20,7 @@
###
# Exercise MGED command manuals, searches, and help dialogs.
source $::env(MGED_XMIN_GUI_LIBRARY)
source $::env(MGED_GUI_TEST_LIBRARY)
namespace eval ::mged::xmin::help {
variable exec_arguments {}
@@ -32,94 +32,94 @@ proc ::mged::xmin::help::record_exec {args} {
}
proc ::mged::xmin::help::exercise_command_manual {top} {
::mged::xmin::test::invoke $top {Help {Command Manual Pages}}
::mged::gui::test::invoke $top {Help {Command Manual Pages}}
set browser .mgedMan
::xmin::test::require {
::gui::test::require {
[llength [info commands $browser]] == 1 &&
[winfo exists $browser] && [winfo ismapped $browser]
} "Command Manual Pages did not open the internal browser"
set toc [$browser component manpagelistbox]
::xmin::test::require {
::gui::test::require {
[winfo exists $toc] && [$toc size] > 100 &&
[lsearch -exact [$toc get 0 end] rt] >= 0
} "manual browser did not populate the program-page table of contents"
man rt
set selection [$toc curselection]
::xmin::test::require {
::gui::test::require {
[llength $selection] == 1 && [$toc get $selection] eq "rt"
} "man rt did not select and load the rt manual page"
::xmin::test::require {
::gui::test::require {
[catch {man xmin_page_that_does_not_exist} message] &&
[string first "couldn't find manual page" $message] >= 0
} "missing manual page lookup did not report a useful error"
man -k ray
::xmin::test::require {[$toc size] > 0} \
::gui::test::require {[$toc size] > 0} \
"short manual search returned no results"
$browser search "ray tracing" full
::xmin::test::require {[$toc size] > 0} \
::gui::test::require {[$toc size] > 0} \
"full-text manual search returned no results"
set search_entry [$browser component search_frame].entry
$search_entry delete 0 end
$search_entry insert 0 geometry
event generate $search_entry <KeyRelease>
::mged::xmin::test::settle
::xmin::test::require {[$toc size] > 0} \
::mged::gui::test::settle
::gui::test::require {[$toc size] > 0} \
"manual browser search widget did not refresh results"
[$browser component search_frame].clear invoke
::xmin::test::require {
::gui::test::require {
[$toc size] > 100 && [lsearch -exact [$toc get 0 end] rt] >= 0
} "manual browser Clear did not restore the table of contents"
$browser deactivate
}
proc ::mged::xmin::help::exercise_manual_search {top} {
::mged::xmin::test::with_dialog_answer $top.mansearch {Manual Search} \
::mged::gui::test::with_dialog_answer $top.mansearch {Manual Search} \
$top.mansearch.mid.ent {ray tracing} $top.mansearch.bot.button0 \
[list ::mged::xmin::test::invoke $top {Help {Manual Search}}]
[list ::mged::gui::test::invoke $top {Help {Manual Search}}]
set browser .mgedMan
set toc [$browser component manpagelistbox]
::xmin::test::require {
::gui::test::require {
[winfo ismapped $browser] && [$toc size] > 0
} "Manual Search did not display ranked internal-browser results"
$browser deactivate
}
proc ::mged::xmin::help::exercise_apropos {top} {
::mged::xmin::test::with_dialog_answer $top.apropos Apropos \
::mged::gui::test::with_dialog_answer $top.apropos Apropos \
$top.apropos.mid.ent view $top.apropos.bot.button0 \
[list ::mged::xmin::test::invoke $top {Help Apropos}]
[list ::mged::gui::test::invoke $top {Help Apropos}]
set help_window $top.help
::xmin::test::require {
::gui::test::require {
[winfo exists $help_window] && [winfo ismapped $help_window] &&
[$help_window.l size] > 0
} "Apropos did not display matching MGED commands"
$help_window.l selection set 0
focus $help_window.l
::mged::xmin::test::with_dialog_answer .mged_dialog Usage "" "" \
::mged::gui::test::with_dialog_answer .mged_dialog Usage "" "" \
.mged_dialog.bot.button0 \
[list event generate $help_window.l <KeyPress> -keysym Return]
$help_window.cancel invoke
::xmin::test::require {![winfo exists $help_window]} \
::gui::test::require {![winfo exists $help_window]} \
"Apropos results did not dismiss"
}
proc ::mged::xmin::help::exercise_static_help {id top} {
::mged::xmin::test::with_dialog_answer .mged_dialog {Shift Grips} \
::mged::gui::test::with_dialog_answer .mged_dialog {Shift Grips} \
"" "" .mged_dialog.bot.button0 \
[list ::mged::xmin::test::invoke $top {Help {Shift Grips}}]
[list ::mged::gui::test::invoke $top {Help {Shift Grips}}]
::mged::xmin::test::invoke $top {Help Dedication}
::mged::gui::test::invoke $top {Help Dedication}
set dedication .$id\_mike
::xmin::test::require {
::gui::test::require {
[winfo exists $dedication] && [winfo ismapped $dedication] &&
[wm title $dedication] eq "Dedication" &&
[string first "Michael John Muuss" [$dedication.dates cget -text]] >= 0
} "Dedication did not display its expected content"
$dedication.dismiss invoke
::xmin::test::require {![winfo exists $dedication]} \
::gui::test::require {![winfo exists $dedication]} \
"Dedication did not dismiss"
}
@@ -128,7 +128,7 @@ proc ::mged::xmin::help::exercise_html_manual {top} {
variable exec_arguments
set manual_path [file normalize [file join $mged_html_dir index.html]]
::xmin::test::require {[file readable $manual_path]} \
::gui::test::require {[file readable $manual_path]} \
"built MGED HTML manual is unavailable"
set browser [auto_execok true]
@@ -137,21 +137,21 @@ proc ::mged::xmin::help::exercise_html_manual {top} {
rename ::exec ::mged::xmin::help::saved_exec
rename ::mged::xmin::help::record_exec ::exec
set status [catch {
::mged::xmin::test::invoke $top {Help Manual}
::mged::gui::test::invoke $top {Help Manual}
} message options]
rename ::exec ::mged::xmin::help::record_exec
rename ::mged::xmin::help::saved_exec ::exec
if {$status} {
return -options $options $message
}
::xmin::test::require {
::gui::test::require {
$exec_arguments eq [list -- {*}$browser $manual_path &]
} "Manual did not dispatch the configured browser with its index page"
set mged_browser [file join $::env(XMIN_TEST_DIR) missing-browser]
::mged::xmin::test::invoke $top {Help Manual}
set mged_browser [file join $::env(GUI_TEST_DIR) missing-browser]
::mged::gui::test::invoke $top {Help Manual}
set fallback $top.man
::xmin::test::require {
::gui::test::require {
[winfo exists $fallback] && [winfo ismapped $fallback] &&
[string length [string trim [$fallback.text get 1.0 end]]] > 100
} "Manual did not load MGED's internal HTML viewer"
@@ -166,7 +166,7 @@ proc ::mged::xmin::help::run {id top} {
exercise_html_manual $top
}
::mged::xmin::test::start ::mged::xmin::help::run \
::mged::gui::test::start ::mged::xmin::help::run \
{MGED manual and help lookup} {MGED help regression}
# Local Variables:
+21 -21
View File
@@ -20,7 +20,7 @@
###
# Exercise the MGED LOD Configuration dialog and its CSG/VDS render paths.
source $::env(MGED_XMIN_GUI_LIBRARY)
source $::env(MGED_GUI_TEST_LIBRARY)
namespace eval ::mged::xmin::lod {
variable dense_point_scale 1.5
@@ -33,7 +33,7 @@ proc ::mged::xmin::lod::require_near {actual expected description} {
variable tolerance
if {![string is double -strict $actual] ||
abs($actual - $expected) > $tolerance} {
::mged::xmin::test::fail \
::mged::gui::test::fail \
"$description is '$actual', expected '$expected'"
}
}
@@ -69,10 +69,10 @@ proc ::mged::xmin::lod::create_adaptive_bot {name} {
}
proc ::mged::xmin::lod::plot_segment_count {tag} {
set plot_path [file join $::env(XMIN_TEST_DIR) "$tag.plot3"]
set plot_path [file join $::env(GUI_TEST_DIR) "$tag.plot3"]
file delete -force $plot_path
_mged_plot $plot_path
::xmin::test::require {
::gui::test::require {
[file exists $plot_path] && [file size $plot_path] > 0
} "LOD $tag plot was not produced"
@@ -85,21 +85,21 @@ proc ::mged::xmin::lod::exercise_dialog {top} {
lod scale points 0.6
lod scale curves 7
::mged::xmin::test::invoke $top {Tools {LOD Configuration}}
::mged::gui::test::invoke $top {Tools {LOD Configuration}}
set dialog .loddialog
set contents $dialog.contents
set frame $contents.lodFrame
::xmin::test::require {
::gui::test::require {
[winfo exists $dialog] && [winfo ismapped $dialog]
} "LOD Configuration did not open"
::xmin::test::require {[wm title $dialog] eq "LOD Configuration"} \
::gui::test::require {[wm title $dialog] eq "LOD Configuration"} \
"LOD Configuration has an unexpected title"
require_near [$frame.pointsScale get] 0.6 \
"LOD point scale initial widget value"
require_near [$frame.curvesScale get] 7 \
"LOD curve scale initial widget value"
::xmin::test::require {
::gui::test::require {
[lod enabled] == 0 &&
[$frame.pointsScale instate disabled] &&
[$frame.pointsValueLabel instate disabled] &&
@@ -109,8 +109,8 @@ proc ::mged::xmin::lod::exercise_dialog {top} {
} "disabled LOD state was not reflected by every dependent widget"
$frame.lodonCheckbutton invoke
::mged::xmin::test::settle
::xmin::test::require {
::mged::gui::test::settle
::gui::test::require {
[lod enabled] == 1 &&
![$frame.pointsScale instate disabled] &&
![$frame.curvesScale instate disabled] &&
@@ -119,22 +119,22 @@ proc ::mged::xmin::lod::exercise_dialog {top} {
$frame.pointsScale set 1.3
$frame.curvesScale set 11
::mged::xmin::test::settle
::mged::gui::test::settle
require_near [lod scale points] 1.3 "LOD point scale command value"
require_near [lod scale curves] 11 "LOD curve scale command value"
$frame.liveUpdateCheckbutton invoke
::xmin::test::require {
::gui::test::require {
[$frame.liveUpdateCheckbutton instate selected]
} "LOD Live Update control did not select"
$frame.pointsScale set 0.8
::mged::xmin::test::settle
::mged::gui::test::settle
require_near [lod scale points] 0.8 "live LOD point scale value"
$frame.updateButton invoke
::mged::xmin::test::settle
::mged::gui::test::settle
destroy $dialog
::xmin::test::require {![winfo exists $dialog]} \
::gui::test::require {![winfo exists $dialog]} \
"LOD Configuration did not close"
}
@@ -144,7 +144,7 @@ proc ::mged::xmin::lod::exercise_render_paths {} {
make xmin_lod_ell.s ell
set face_count [create_adaptive_bot xmin_lod_bot.s]
::xmin::test::require {
::gui::test::require {
[exists xmin_lod_bot.s] && [lindex [get xmin_lod_bot.s] 0] eq "bot"
} "LOD fixture did not produce a BoT"
@@ -169,7 +169,7 @@ proc ::mged::xmin::lod::exercise_render_paths {} {
refresh
set dense_segments [expr {[plot_segment_count dense-detail] - $background_segments}]
::xmin::test::require {
::gui::test::require {
$full_segments == $face_count * 3 &&
$sparse_segments > 0 &&
$sparse_segments < $dense_segments &&
@@ -178,7 +178,7 @@ proc ::mged::xmin::lod::exercise_render_paths {} {
draw xmin_lod_ell.s
refresh
::xmin::test::require {
::gui::test::require {
[lod enabled] == 1 &&
[lsearch -exact [who] xmin_lod_ell.s] >= 0
} "adaptive CSG drawing did not remain active"
@@ -189,8 +189,8 @@ proc ::mged::xmin::lod::exercise_render_paths {} {
}
proc ::mged::xmin::lod::run {id top} {
set database [file join $::env(XMIN_TEST_DIR) lod.g]
cd $::env(XMIN_TEST_DIR)
set database [file join $::env(GUI_TEST_DIR) lod.g]
cd $::env(GUI_TEST_DIR)
file delete -force $database
opendb $database y
title {Xmin MGED LOD regression}
@@ -199,7 +199,7 @@ proc ::mged::xmin::lod::run {id top} {
exercise_dialog $top
}
::mged::xmin::test::start ::mged::xmin::lod::run \
::mged::gui::test::start ::mged::xmin::lod::run \
{MGED LOD dialog and adaptive rendering} \
{MGED LOD regression}
+20 -20
View File
@@ -20,7 +20,7 @@
###
# Exercise MGED's overlap-generation workflow through live widgets.
source $::env(MGED_XMIN_GUI_LIBRARY)
source $::env(MGED_GUI_TEST_LIBRARY)
namespace eval ::mged::xmin::overlaps {}
@@ -43,51 +43,51 @@ proc ::mged::xmin::overlaps::exercise_overlap_tool {top database} {
make_overlap_group second 100
Z
::mged::xmin::test::invoke $top {Tools {Overlaps Tool}}
::mged::gui::test::invoke $top {Tools {Overlaps Tool}}
set menu .overlapmenu.overlapmenu
::mged::xmin::test::require_mapped $menu "Overlap Menu"
::mged::gui::test::require_mapped $menu "Overlap Menu"
$menu.buttonsFrame.newFileFrame.buttonRunOvFileGen invoke
::mged::xmin::test::settle
::mged::gui::test::settle
set tool .overlapmenu.overlapfiletool.overlapfiletool
::mged::xmin::test::require_mapped $tool "Overlap File Tool"
::mged::gui::test::require_mapped $tool "Overlap File Tool"
$tool clearSelection
::mged::xmin::test::set_entry $tool.ovFrame.objectsEntry \
::mged::gui::test::set_entry $tool.ovFrame.objectsEntry \
{first.g second.g}
$tool.ovFrame.buttonAdd invoke
set selected [$tool.objFrame.objectsList get 0 end]
::xmin::test::require {
::gui::test::require {
[llength $selected] == 2 &&
[lsearch -glob $selected */first.g] >= 0 &&
[lsearch -glob $selected */second.g] >= 0
} "Overlap File Tool did not retain two separately selected objects"
$tool.ovButtonFrame.buttonGo invoke
::mged::xmin::test::settle
::mged::gui::test::settle
set checker .checker.ck
::mged::xmin::test::require_mapped $checker "Geometry Checker"
::mged::gui::test::require_mapped $checker "Geometry Checker"
set tree $checker.checkFrame.checkList
set rows [$tree children {}]
::xmin::test::require {[llength $rows] == 2} \
::gui::test::require {[llength $rows] == 2} \
"generated overlap file did not contain both selected assemblies"
$checker.headerFrame.fullPathDisplayCheckButton invoke
set first_row [lindex $rows 0]
::xmin::test::require {
::gui::test::require {
[string index [$tree set $first_row Left] 0] eq "/" &&
[string index [$tree set $first_row Right] 0] eq "/"
} "Geometry Checker did not display full paths"
$tree selection set $first_row
event generate $tree <<TreeviewSelect>>
::mged::xmin::test::settle
::xmin::test::require {[llength [who]] >= 2} \
::mged::gui::test::settle
::gui::test::require {[llength [who]] >= 2} \
"selecting an overlap did not draw its objects"
$tree.checkMenu invoke 2
set copied [clipboard get]
::xmin::test::require {
::gui::test::require {
[string first "/" $copied] >= 0 && [llength $copied] == 2
} "Geometry Checker did not copy both full paths"
@@ -98,26 +98,26 @@ proc ::mged::xmin::overlaps::exercise_overlap_tool {top database} {
set mark_file [file join "${database}.ck" \
"ck.[file tail $database].marked"]
::xmin::test::require {
::gui::test::require {
[file exists $mark_file] && [$tree tag has marked $first_row]
} "Geometry Checker did not persist a resolved mark"
set mark_channel [open $mark_file r]
set marked_paths [gets $mark_channel]
close $mark_channel
::xmin::test::require {
::gui::test::require {
[llength $marked_paths] == 2
} "Geometry Checker did not write both marked overlap paths"
$tree selection set $first_row
$tree.checkMenu invoke 1
::xmin::test::require {![$tree tag has marked $first_row]} \
::gui::test::require {![$tree tag has marked $first_row]} \
"Geometry Checker did not clear a resolved mark"
destroy .checker
}
proc ::mged::xmin::overlaps::run {id top} {
set database [file join $::env(XMIN_TEST_DIR) overlaps.g]
cd $::env(XMIN_TEST_DIR)
set database [file join $::env(GUI_TEST_DIR) overlaps.g]
cd $::env(GUI_TEST_DIR)
file delete -force $database "${database}.ck"
opendb $database y
title {Xmin MGED overlap regression}
@@ -125,7 +125,7 @@ proc ::mged::xmin::overlaps::run {id top} {
exercise_overlap_tool $top $database
}
::mged::xmin::test::start ::mged::xmin::overlaps::run \
::mged::gui::test::start ::mged::xmin::overlaps::run \
{MGED overlap tools} {MGED overlap regression}
# Local Variables:
+25 -25
View File
@@ -20,22 +20,22 @@
###
# Exercise MGED's Query Ray and Build Pattern tools through live widgets.
source $::env(MGED_XMIN_GUI_LIBRARY)
source $::env(MGED_GUI_TEST_LIBRARY)
namespace eval ::mged::xmin::query_pattern {}
proc ::mged::xmin::query_pattern::set_components {dialog values} {
foreach {component value} $values {
::mged::xmin::test::set_entry $dialog.$component $value
::mged::gui::test::set_entry $dialog.$component $value
}
}
proc ::mged::xmin::query_pattern::require_pattern {
group first_clone second_clone description
} {
::xmin::test::require {[exists $group]} "$description group was not created"
::gui::test::require {[exists $group]} "$description group was not created"
set definition [get $group]
::xmin::test::require {
::gui::test::require {
[string first $first_clone $definition] >= 0 &&
[string first $second_clone $definition] >= 0
} "$description group did not contain both requested clones"
@@ -50,21 +50,21 @@ proc ::mged::xmin::query_pattern::exercise_query_ray {id top} {
qray effects t
qray basename query_ray
::mged::xmin::test::invoke $top {Tools {Query Ray Control Panel}}
::mged::gui::test::invoke $top {Tools {Query Ray Control Panel}}
set dialog .$id.qray_control
::mged::xmin::test::require_mapped $dialog "Query Ray Control Panel"
::mged::gui::test::require_mapped $dialog "Query Ray Control Panel"
$dialog.activeCB invoke
$dialog.use_airCB invoke
$dialog.cmd_echoCB invoke
$dialog.effectsMB.m invoke 2
::mged::xmin::test::set_entry $dialog.bnameE xmin_qray
::mged::gui::test::set_entry $dialog.bnameE xmin_qray
set qray_control($id,oddcolor) {12 34 56}
color_entry_update $dialog oddColor qray_control($id,oddcolor) \
$qray_control($id,oddcolor)
$dialog.applyB invoke
::xmin::test::require {
::gui::test::require {
$mouse_behavior eq "q" &&
$use_air == 1 &&
[_mged_qray echo] == 1 &&
@@ -74,31 +74,31 @@ proc ::mged::xmin::query_pattern::exercise_query_ray {id top} {
} "Query Ray Apply did not persist mouse, air, effect, name, or color settings"
$dialog.advB invoke
::mged::xmin::test::settle
::mged::gui::test::settle
set advanced .$id.qray_adv
::mged::xmin::test::require_mapped $advanced \
::mged::gui::test::require_mapped $advanced \
"Query Ray Advanced Settings"
set applied_format {XMIN RAY FORMAT}
::mged::xmin::test::set_entry $advanced.rayE $applied_format
::mged::gui::test::set_entry $advanced.rayE $applied_format
$advanced.applyB invoke
::xmin::test::require {[_mged_qray fmt r] eq $applied_format} \
::gui::test::require {[_mged_qray fmt r] eq $applied_format} \
"Query Ray advanced Apply did not persist the ray format"
::mged::xmin::test::set_entry $advanced.rayE {discarded format}
::mged::gui::test::set_entry $advanced.rayE {discarded format}
$advanced.resetB invoke
::xmin::test::require {$qray_control($id,fmt_ray) eq $applied_format} \
::gui::test::require {$qray_control($id,fmt_ray) eq $applied_format} \
"Query Ray advanced Reset did not restore the applied format"
$advanced.okB invoke
::xmin::test::require {![winfo exists $advanced]} \
::gui::test::require {![winfo exists $advanced]} \
"Query Ray Advanced Settings did not close after OK"
::mged::xmin::test::set_entry $dialog.bnameE discarded_name
::mged::gui::test::set_entry $dialog.bnameE discarded_name
$dialog.resetB invoke
::xmin::test::require {$qray_control($id,basename) eq "xmin_qray"} \
::gui::test::require {$qray_control($id,basename) eq "xmin_qray"} \
"Query Ray Reset did not restore the applied base name"
$dialog.okB invoke
::xmin::test::require {![winfo exists $dialog]} \
::gui::test::require {![winfo exists $dialog]} \
"Query Ray Control Panel did not close after OK"
}
@@ -107,9 +107,9 @@ proc ::mged::xmin::query_pattern::exercise_patterns {top} {
make sph_source.s sph
make cyl_source.s sph
::mged::xmin::test::invoke $top {Tools {Build Pattern Tool}}
set dialog [::mged::xmin::test::find_toplevel_by_title "Pattern Control"]
::mged::xmin::test::require_mapped $dialog "Build Pattern Tool"
::mged::gui::test::invoke $top {Tools {Build Pattern Tool}}
set dialog [::mged::gui::test::find_toplevel_by_title "Pattern Control"]
::mged::gui::test::require_mapped $dialog "Build Pattern Tool"
set_components $dialog {
e_group_r rect_pattern.g
@@ -167,13 +167,13 @@ proc ::mged::xmin::query_pattern::exercise_patterns {top} {
"cylindrical pattern"
$dialog.b_dismiss_c invoke
::xmin::test::require {![winfo exists $dialog]} \
::gui::test::require {![winfo exists $dialog]} \
"Build Pattern Tool did not dismiss"
}
proc ::mged::xmin::query_pattern::run {id top} {
set database [file join $::env(XMIN_TEST_DIR) query-pattern.g]
cd $::env(XMIN_TEST_DIR)
set database [file join $::env(GUI_TEST_DIR) query-pattern.g]
cd $::env(GUI_TEST_DIR)
file delete -force $database
opendb $database y
title {Xmin MGED specialized-tool regression}
@@ -187,7 +187,7 @@ proc ::mged::xmin::query_pattern::run {id top} {
exercise_patterns $top
}
::mged::xmin::test::start ::mged::xmin::query_pattern::run \
::mged::gui::test::start ::mged::xmin::query_pattern::run \
{MGED Query Ray and Build Pattern tools} \
{MGED Query Ray/Build Pattern regression}
+33 -33
View File
@@ -20,7 +20,7 @@
###
# Exercise MGED's Render View output dialogs through their live controls.
source $::env(MGED_XMIN_GUI_LIBRARY)
source $::env(MGED_GUI_TEST_LIBRARY)
namespace eval ::mged::xmin::render {
variable raytrace_image_size 32
@@ -36,7 +36,7 @@ proc ::mged::xmin::render::read_file {path {translation auto}} {
}
proc ::mged::xmin::render::require_file {path description} {
::xmin::test::require {
::gui::test::require {
[file exists $path] && [file size $path] > 0
} "$description did not produce a nonempty file"
}
@@ -55,17 +55,17 @@ proc ::mged::xmin::render::wait_for_file_size {path minimum_size timeout_ms} {
if {[file exists $path]} {
set actual_size [file size $path]
}
::mged::xmin::test::fail \
::mged::gui::test::fail \
"raytrace output has $actual_size bytes, expected at least $minimum_size"
}
proc ::mged::xmin::render::exercise_rt_script {id top} {
global rts_control
set output [file join $::env(XMIN_TEST_DIR) saved-view.sh]
::mged::xmin::test::invoke $top {File {Render View} {RT Script...}}
set output [file join $::env(GUI_TEST_DIR) saved-view.sh]
::mged::gui::test::invoke $top {File {Render View} {RT Script...}}
set dialog .$id.do_rtScript
::xmin::test::require {
::gui::test::require {
[winfo exists $dialog] && [winfo ismapped $dialog] &&
[wm title $dialog] eq "RT Script Tool"
} "RT Script did not open its tool"
@@ -73,33 +73,33 @@ proc ::mged::xmin::render::exercise_rt_script {id top} {
set rts_control($id,file) $output
set rts_control($id,args) {-A 0.25}
$dialog.createB invoke
::mged::xmin::test::settle
::mged::gui::test::settle
require_file $output "RT Script"
set contents [read_file $output]
::xmin::test::require {
::gui::test::require {
[string first "-A 0.25" $contents] >= 0 &&
[string first "xmin_render.r" $contents] >= 0
} "RT Script omitted its requested options or displayed object"
set initial_size [file size $output]
::mged::xmin::test::with_dialog_answer .mged_dialog \
::mged::gui::test::with_dialog_answer .mged_dialog \
"Append $output?" "" "" .mged_dialog.bot.button0 \
[list $dialog.createB invoke]
::xmin::test::require {[file size $output] > $initial_size} \
::gui::test::require {[file size $output] > $initial_size} \
"RT Script overwrite confirmation did not append another view"
$dialog.dismissB invoke
::xmin::test::require {![winfo exists $dialog]} \
::gui::test::require {![winfo exists $dialog]} \
"RT Script tool did not dismiss"
}
proc ::mged::xmin::render::exercise_plot {id top} {
global pl_control
set output [file join $::env(XMIN_TEST_DIR) rendered.plot3]
::mged::xmin::test::invoke $top {File {Render View} {Plot...}}
set output [file join $::env(GUI_TEST_DIR) rendered.plot3]
::mged::gui::test::invoke $top {File {Render View} {Plot...}}
set dialog .$id.do_plot
::xmin::test::require {
::gui::test::require {
[winfo exists $dialog] && [winfo ismapped $dialog]
} "Plot did not open its tool"
@@ -108,37 +108,37 @@ proc ::mged::xmin::render::exercise_plot {id top} {
set pl_control($id,2d) 0
set pl_control($id,float) 0
$dialog.createB invoke
::mged::xmin::test::settle
::mged::gui::test::settle
require_file $output "Plot"
set plot_text [exec $::env(PLOT3_ASC_BIN) $output]
::xmin::test::require {
::gui::test::require {
[regexp -line {^L[ \t]} $plot_text] &&
[llength [split $plot_text \n]] > 10
} "Plot output could not be decoded into line records"
$dialog.filterRB invoke
::xmin::test::require {
::gui::test::require {
[$dialog.fileE cget -state] eq "disabled" &&
[$dialog.filterE cget -state] eq "normal"
} "Plot filter selection did not update entry states"
$dialog.fileRB invoke
::xmin::test::require {
::gui::test::require {
[$dialog.fileE cget -state] eq "normal" &&
[$dialog.filterE cget -state] eq "disabled"
} "Plot file selection did not restore entry states"
$dialog.dismissB invoke
::xmin::test::require {![winfo exists $dialog]} \
::gui::test::require {![winfo exists $dialog]} \
"Plot tool did not dismiss"
}
proc ::mged::xmin::render::exercise_postscript {id top} {
global ps_control
set output [file join $::env(XMIN_TEST_DIR) rendered.ps]
::mged::xmin::test::invoke $top {File {Render View} {PostScript...}}
set output [file join $::env(GUI_TEST_DIR) rendered.ps]
::mged::gui::test::invoke $top {File {Render View} {PostScript...}}
set dialog .$id.do_ps
::xmin::test::require {
::gui::test::require {
[winfo exists $dialog] && [winfo ismapped $dialog]
} "PostScript did not open its tool"
@@ -149,14 +149,14 @@ proc ::mged::xmin::render::exercise_postscript {id top} {
set ps_control($id,linewidth) 2
set ps_control($id,zclip) 1
$dialog.fontMB.fontM.helveticaM invoke Bold
::xmin::test::require {$ps_control($id,font) eq "Helvetica-Bold"} \
::gui::test::require {$ps_control($id,font) eq "Helvetica-Bold"} \
"PostScript font menu did not update the selected font"
$dialog.createB invoke
::mged::xmin::test::settle
::mged::gui::test::settle
require_file $output "PostScript"
set contents [read_file $output]
::xmin::test::require {
::gui::test::require {
[string first "%!PS-Adobe" $contents] == 0 &&
[string first "Xmin Render Output" $contents] >= 0 &&
[string first "MGED regression" $contents] >= 0 &&
@@ -164,7 +164,7 @@ proc ::mged::xmin::render::exercise_postscript {id top} {
} "PostScript output omitted its header or configured metadata"
$dialog.dismissB invoke
::xmin::test::require {![winfo exists $dialog]} \
::gui::test::require {![winfo exists $dialog]} \
"PostScript tool did not dismiss"
}
@@ -173,10 +173,10 @@ proc ::mged::xmin::render::exercise_raytrace {id top} {
variable raytrace_image_size
variable raytrace_timeout_ms
set output [file join $::env(XMIN_TEST_DIR) rendered.pix]
::mged::xmin::test::invoke $top {File Raytrace}
set output [file join $::env(GUI_TEST_DIR) rendered.pix]
::mged::gui::test::invoke $top {File Raytrace}
set dialog .$id.rt
::xmin::test::require {
::gui::test::require {
[winfo exists $dialog] && [winfo ismapped $dialog]
} "File > Raytrace did not open the Raytrace Control Panel"
@@ -197,13 +197,13 @@ proc ::mged::xmin::render::exercise_raytrace {id top} {
$raytrace_timeout_ms
$dialog.dismissB invoke
::xmin::test::require {![winfo exists $dialog]} \
::gui::test::require {![winfo exists $dialog]} \
"Raytrace Control Panel did not dismiss"
}
proc ::mged::xmin::render::run {id top} {
set database [file join $::env(XMIN_TEST_DIR) render.g]
cd $::env(XMIN_TEST_DIR)
set database [file join $::env(GUI_TEST_DIR) render.g]
cd $::env(GUI_TEST_DIR)
file delete -force $database
opendb $database y
title {Xmin MGED render-output regression}
@@ -220,7 +220,7 @@ proc ::mged::xmin::render::run {id top} {
exercise_raytrace $id $top
}
::mged::xmin::test::start ::mged::xmin::render::run \
::mged::gui::test::start ::mged::xmin::render::run \
{MGED Render View output dialogs} {MGED render-output regression}
# Local Variables:
+26 -26
View File
@@ -20,13 +20,13 @@
###
# Exercise asynchronous search -exec drawing, serialization, and interruption.
if {![info exists ::env(XMIN_GUI_LIBRARY)] ||
![info exists ::env(XMIN_TEST_DIR)]} {
puts stderr "XMIN_GUI_LIBRARY and XMIN_TEST_DIR are required"
if {![info exists ::env(GUI_TEST_LIBRARY)] ||
![info exists ::env(GUI_TEST_DIR)]} {
puts stderr "GUI_TEST_LIBRARY and GUI_TEST_DIR are required"
exit 2
}
source $::env(XMIN_GUI_LIBRARY)
::xmin::test::capture_background_errors
source $::env(GUI_TEST_LIBRARY)
::gui::test::capture_background_errors
namespace eval ::mged::xmin::search_exec {
variable command_widget ""
@@ -85,7 +85,7 @@ proc ::mged::xmin::search_exec::exercise {} {
variable nested_status
set paths [_mged_search / -type tgc]
::xmin::test::require {[llength $paths] >= $minimum_tgc_paths} \
::gui::test::require {[llength $paths] >= $minimum_tgc_paths} \
"m35.g supplied too few TGC paths for the scaling regression"
set nested_done 0
@@ -99,11 +99,11 @@ proc ::mged::xmin::search_exec::exercise {} {
} draw_message]
set draw_elapsed [expr {[clock milliseconds] - $started}]
::xmin::test::require {$draw_status == 0} \
::gui::test::require {$draw_status == 0} \
"search -exec draw failed: $draw_message"
::xmin::test::require {$nested_done} \
::gui::test::require {$nested_done} \
"the event loop did not service the nested search probe"
::xmin::test::require {
::gui::test::require {
$nested_status == 1 &&
$nested_message eq "another MGED command is already running" &&
$nested_error_code eq {BRLCAD MGED COMMAND_BUSY}
@@ -111,9 +111,9 @@ proc ::mged::xmin::search_exec::exercise {} {
set draw_limit_message [format \
"search -exec draw took %dms; expected less than %dms" \
$draw_elapsed $draw_time_limit_ms]
::xmin::test::require {$draw_elapsed < $draw_time_limit_ms} \
::gui::test::require {$draw_elapsed < $draw_time_limit_ms} \
$draw_limit_message
::xmin::test::require {[llength [_mged_who]] >= $minimum_tgc_paths} \
::gui::test::require {[llength [_mged_who]] >= $minimum_tgc_paths} \
"search -exec draw did not populate the display list"
set redraw_started [clock milliseconds]
@@ -121,12 +121,12 @@ proc ::mged::xmin::search_exec::exercise {} {
_mged_search / -type tgc -exec draw "{}" ";"
} redraw_message]
set redraw_elapsed [expr {[clock milliseconds] - $redraw_started}]
::xmin::test::require {$redraw_status == 0} \
::gui::test::require {$redraw_status == 0} \
"repeated search -exec draw failed: $redraw_message"
set redraw_limit_message [format \
"repeated search -exec draw took %dms; expected less than %dms" \
$redraw_elapsed $draw_time_limit_ms]
::xmin::test::require {$redraw_elapsed < $draw_time_limit_ms} \
::gui::test::require {$redraw_elapsed < $draw_time_limit_ms} \
$redraw_limit_message
_mged_Z
@@ -136,9 +136,9 @@ proc ::mged::xmin::search_exec::exercise {} {
_mged_search / -type tgc -exec draw "{}" ";"
} interrupt_message]
::xmin::test::require {$interrupt_requested == 1} \
::gui::test::require {$interrupt_requested == 1} \
"the running search did not accept an interrupt request"
::xmin::test::require {
::gui::test::require {
$interrupt_status == 1 && $interrupt_message eq "Command interrupted."
} "interrupted search returned an unexpected result: $interrupt_message"
@@ -148,14 +148,14 @@ proc ::mged::xmin::search_exec::exercise {} {
set followup_status [catch {
_mged_search $followup_path -type tgc -exec ls "{}" ";"
} followup_message]
::xmin::test::require {$followup_status == 0} \
::gui::test::require {$followup_status == 0} \
"search -exec failed after interruption: $followup_message"
_mged_Z
_mged_draw $ancestor
set ancestor_display [_mged_who]
_mged_draw $followup_path
::xmin::test::require {[_mged_who] eq $ancestor_display} \
::gui::test::require {[_mged_who] eq $ancestor_display} \
"drawing a child path duplicated its displayed ancestor"
_mged_Z
@@ -163,13 +163,13 @@ proc ::mged::xmin::search_exec::exercise {} {
_mged_draw [lindex $paths 1]
_mged_draw $ancestor
set consolidated_display [_mged_who]
::xmin::test::require {
::gui::test::require {
[llength $consolidated_display] == 1 &&
[lindex $consolidated_display 0] eq $ancestor
} "drawing an ancestor did not replace its displayed descendants"
set component_paths [_mged_search /component -type tgc]
::xmin::test::require {
::gui::test::require {
[llength $component_paths] >= $minimum_tgc_paths
} "component supplied too few TGC paths for the overlap regression"
@@ -180,15 +180,15 @@ proc ::mged::xmin::search_exec::exercise {} {
_mged_search /component -type tgc -exec draw "{}" ";"
} covered_message]
set covered_elapsed [expr {[clock milliseconds] - $covered_started}]
::xmin::test::require {$covered_status == 0} \
::gui::test::require {$covered_status == 0} \
"drawing paths under component failed: $covered_message"
set covered_limit_message [format \
"drawing paths under component took %dms; expected less than %dms" \
$covered_elapsed $covered_draw_time_limit_ms]
::xmin::test::require {$covered_elapsed < $covered_draw_time_limit_ms} \
::gui::test::require {$covered_elapsed < $covered_draw_time_limit_ms} \
$covered_limit_message
set covered_display [_mged_who]
::xmin::test::require {
::gui::test::require {
[llength $covered_display] == 1 &&
[lindex $covered_display 0] eq "component"
} "drawing covered paths changed the component display root"
@@ -198,7 +198,7 @@ proc ::mged::xmin::search_exec::exercise {} {
append metrics "\ncovered_elapsed_ms $covered_elapsed"
puts "MGED search-exec: [llength $paths] paths, first $draw_elapsed ms, \
redraw $redraw_elapsed ms"
::xmin::test::write search_exec_metrics $metrics
::gui::test::write search_exec_metrics $metrics
}
proc ::mged::xmin::search_exec::finish {status message} {
@@ -208,10 +208,10 @@ proc ::mged::xmin::search_exec::finish {status message} {
}
set finished 1
catch {update}
lassign [::xmin::test::check_background_errors $status $message] \
lassign [::gui::test::check_background_errors $status $message] \
status message
puts $message
::xmin::test::write result $message
::gui::test::write result $message
if {$status != 0} {
puts stderr $message
}
@@ -265,7 +265,7 @@ proc ::mged::xmin::search_exec::run {} {
proc ::mged::xmin::search_exec::run_checked {} {
if {[catch {run} message options]} {
if {[dict exists $options -errorinfo]} {
::xmin::test::write tcl_error_debug [dict get $options -errorinfo]
::gui::test::write tcl_error_debug [dict get $options -errorinfo]
}
finish 1 "FAIL: MGED search -exec regression: $message"
}
+22 -22
View File
@@ -20,7 +20,7 @@
###
# Exercise ShotVis construction, interactive editing, persistence, and reload.
source $::env(MGED_XMIN_GUI_LIBRARY)
source $::env(MGED_GUI_TEST_LIBRARY)
namespace eval ::mged::xmin::shotvis {
variable basename xmin_shot
@@ -31,7 +31,7 @@ proc ::mged::xmin::shotvis::require_near {actual expected description} {
variable tolerance
if {![string is double -strict $actual] ||
abs($actual - $expected) > $tolerance} {
::mged::xmin::test::fail \
::mged::gui::test::fail \
"$description is '$actual', expected '$expected'"
}
}
@@ -39,10 +39,10 @@ proc ::mged::xmin::shotvis::require_near {actual expected description} {
proc ::mged::xmin::shotvis::root_combination {} {
set title [wm title .shotvis]
set prefix {Edit Shotline Visualization - }
::xmin::test::require {[string first $prefix $title] == 0} \
::gui::test::require {[string first $prefix $title] == 0} \
"ShotVis window title does not identify its active root"
set root [string range $title [string length $prefix] end]
::xmin::test::require {[exists $root]} \
::gui::test::require {[exists $root]} \
"ShotVis active root '$root' does not exist"
return $root
}
@@ -50,7 +50,7 @@ proc ::mged::xmin::shotvis::root_combination {} {
proc ::mged::xmin::shotvis::threat_combination {} {
set root [root_combination]
set threats [search $root -attr shotvis_threat]
::xmin::test::require {[llength $threats] == 1} \
::gui::test::require {[llength $threats] == 1} \
"ShotVis root does not contain exactly one threat combination"
return [lindex $threats 0]
}
@@ -58,18 +58,18 @@ proc ::mged::xmin::shotvis::threat_combination {} {
proc ::mged::xmin::shotvis::threat_cylinder {} {
set threat [threat_combination]
set cylinders [search $threat -type tgc -name \*.cyl]
::xmin::test::require {[llength $cylinders] == 1} \
::gui::test::require {[llength $cylinders] == 1} \
"ShotVis threat does not contain exactly one cylinder"
return [lindex $cylinders 0]
}
proc ::mged::xmin::shotvis::set_validated_entry {entry value} {
::xmin::test::require {[winfo exists $entry]} \
::gui::test::require {[winfo exists $entry]} \
"ShotVis entry '$entry' is unavailable"
$entry delete 0 end
$entry insert 0 $value
::mged::xmin::test::settle
::xmin::test::require {[$entry get] eq $value} \
::mged::gui::test::settle
::gui::test::require {[$entry get] eq $value} \
"ShotVis entry '$entry' rejected '$value'"
}
@@ -90,14 +90,14 @@ proc ::mged::xmin::shotvis::exercise_editor {} {
variable basename
shotvis $basename
::mged::xmin::test::require_mapped .shotvis ShotVis
::xmin::test::require {
::mged::gui::test::require_mapped .shotvis ShotVis
::gui::test::require {
[wm title .shotvis] eq "Edit Shotline Visualization - $basename" &&
[exists $basename]
} "ShotVis did not create its window and root combination"
set threat [threat_combination]
::xmin::test::require {
::gui::test::require {
[attr get $threat shotvis_threat] == 1
} "ShotVis threat combination lacks its identifying attribute"
@@ -120,15 +120,15 @@ proc ::mged::xmin::shotvis::exercise_editor {} {
set notebook $main.notebook
$notebook select $notebook.nirt
::mged::xmin::test::settle
::mged::gui::test::settle
$notebook select $notebook.threat
::mged::xmin::test::settle
::xmin::test::require {
::mged::gui::test::settle
::gui::test::require {
[lsearch -exact [who] [threat_combination]] >= 0
} "ShotVis did not redraw its threat after changing tabs"
::itcl::delete object .shotvis
::xmin::test::require {
::gui::test::require {
![winfo exists .shotvis] && [exists $basename]
} "ShotVis did not preserve its edited root when closed"
}
@@ -137,23 +137,23 @@ proc ::mged::xmin::shotvis::exercise_reload {} {
variable basename
shotvis $basename
::mged::xmin::test::require_mapped .shotvis {reloaded ShotVis}
::mged::gui::test::require_mapped .shotvis {reloaded ShotVis}
set cylinder [threat_cylinder]
lassign [get $cylinder V] start_x start_y start_z
require_near $start_x 25.0 "reloaded ShotVis start X"
require_near [magnitude [get $cylinder H]] 250.0 \
"reloaded ShotVis draw length"
.shotvis update_shot
::mged::xmin::test::settle
::mged::gui::test::settle
::itcl::delete object .shotvis
::xmin::test::require {[exists $basename]} \
::gui::test::require {[exists $basename]} \
"reloaded ShotVis did not preserve its updated root"
}
proc ::mged::xmin::shotvis::run {id top} {
variable basename
set database [file join $::env(XMIN_TEST_DIR) shotvis.g]
cd $::env(XMIN_TEST_DIR)
set database [file join $::env(GUI_TEST_DIR) shotvis.g]
cd $::env(GUI_TEST_DIR)
file delete -force $database
opendb $database y
title {Xmin MGED ShotVis regression}
@@ -163,7 +163,7 @@ proc ::mged::xmin::shotvis::run {id top} {
exercise_math
}
::mged::xmin::test::start ::mged::xmin::shotvis::run \
::mged::gui::test::start ::mged::xmin::shotvis::run \
{MGED ShotVis editing and persistence} {MGED ShotVis regression}
# Local Variables:
+36 -36
View File
@@ -20,15 +20,15 @@
###
# Shared lifecycle and interaction helpers for focused MGED Xmin fixtures.
if {![info exists ::env(XMIN_GUI_LIBRARY)] ||
![info exists ::env(XMIN_TEST_DIR)]} {
puts stderr "XMIN_GUI_LIBRARY and XMIN_TEST_DIR are required"
if {![info exists ::env(GUI_TEST_LIBRARY)] ||
![info exists ::env(GUI_TEST_DIR)]} {
puts stderr "GUI_TEST_LIBRARY and GUI_TEST_DIR are required"
exit 2
}
source $::env(XMIN_GUI_LIBRARY)
::xmin::test::capture_background_errors
source $::env(GUI_TEST_LIBRARY)
::gui::test::capture_background_errors
namespace eval ::mged::xmin::test {
namespace eval ::mged::gui::test {
variable dialog_error ""
variable dialog_seen 0
variable dialog_serial 0
@@ -40,31 +40,31 @@ namespace eval ::mged::xmin::test {
variable settle_serial 0
}
proc ::mged::xmin::test::fail {message} {
proc ::mged::gui::test::fail {message} {
error $message
}
proc ::mged::xmin::test::settle {} {
proc ::mged::gui::test::settle {} {
variable settle_delay_ms
variable settle_serial
set serial [incr settle_serial]
after $settle_delay_ms [list set ::mged::xmin::test::settle_serial $serial]
vwait ::mged::xmin::test::settle_serial
after $settle_delay_ms [list set ::mged::gui::test::settle_serial $serial]
vwait ::mged::gui::test::settle_serial
}
proc ::mged::xmin::test::invoke {root labels} {
::xmin::test::write progress "invoke: [join $labels { > }]"
::xmin::test::invoke_menu_entry $root $labels
proc ::mged::gui::test::invoke {root labels} {
::gui::test::write progress "invoke: [join $labels { > }]"
::gui::test::invoke_menu_entry $root $labels
settle
}
proc ::mged::xmin::test::set_entry {entry value} {
proc ::mged::gui::test::set_entry {entry value} {
$entry delete 0 end
$entry insert 0 $value
}
proc ::mged::xmin::test::find_descendant {root class} {
foreach widget [::xmin::test::descendants $root] {
proc ::mged::gui::test::find_descendant {root class} {
foreach widget [::gui::test::descendants $root] {
if {[winfo class $widget] eq $class} {
return $widget
}
@@ -72,8 +72,8 @@ proc ::mged::xmin::test::find_descendant {root class} {
fail "$root has no $class descendant"
}
proc ::mged::xmin::test::find_widget_by_text {root text} {
foreach widget [::xmin::test::descendants $root] {
proc ::mged::gui::test::find_widget_by_text {root text} {
foreach widget [::gui::test::descendants $root] {
if {![catch {$widget cget -text} label] && $label eq $text} {
return $widget
}
@@ -81,7 +81,7 @@ proc ::mged::xmin::test::find_widget_by_text {root text} {
fail "$root has no widget labeled '$text'"
}
proc ::mged::xmin::test::find_toplevel_by_title {title} {
proc ::mged::gui::test::find_toplevel_by_title {title} {
foreach widget [winfo children .] {
if {![catch {wm title $widget} widget_title] &&
$widget_title eq $title} {
@@ -91,7 +91,7 @@ proc ::mged::xmin::test::find_toplevel_by_title {title} {
fail "no toplevel has title '$title'"
}
proc ::mged::xmin::test::require_mapped {widget description} {
proc ::mged::gui::test::require_mapped {widget description} {
variable ready_retry_limit
for {set attempt 0} {$attempt < $ready_retry_limit} {incr attempt} {
@@ -104,15 +104,15 @@ proc ::mged::xmin::test::require_mapped {widget description} {
fail "$description is not mapped"
}
proc ::mged::xmin::test::return_file_dialog_path {path args} {
proc ::mged::gui::test::return_file_dialog_path {path args} {
return $path
}
proc ::mged::xmin::test::with_file_dialog_path {command path script} {
proc ::mged::gui::test::with_file_dialog_path {command path script} {
set saved_command ${command}_xmin_saved
rename $command $saved_command
interp alias {} $command {} \
::mged::xmin::test::return_file_dialog_path $path
::mged::gui::test::return_file_dialog_path $path
set status [catch {uplevel 1 $script} result options]
rename $command {}
@@ -123,7 +123,7 @@ proc ::mged::xmin::test::with_file_dialog_path {command path script} {
return $result
}
proc ::mged::xmin::test::answer_dialog {
proc ::mged::gui::test::answer_dialog {
serial path expected_title entry value button
} {
variable dialog_error
@@ -134,7 +134,7 @@ proc ::mged::xmin::test::answer_dialog {
return
}
if {![winfo exists $path] || ![winfo ismapped $path]} {
after 25 [list ::mged::xmin::test::answer_dialog $serial $path \
after 25 [list ::mged::gui::test::answer_dialog $serial $path \
$expected_title $entry $value $button]
return
}
@@ -155,7 +155,7 @@ proc ::mged::xmin::test::answer_dialog {
$button invoke
}
proc ::mged::xmin::test::with_dialog_answer {
proc ::mged::gui::test::with_dialog_answer {
path expected_title entry value button script
} {
variable dialog_error
@@ -165,7 +165,7 @@ proc ::mged::xmin::test::with_dialog_answer {
set dialog_error ""
set dialog_seen 0
set serial [incr dialog_serial]
after 25 [list ::mged::xmin::test::answer_dialog $serial $path \
after 25 [list ::mged::gui::test::answer_dialog $serial $path \
$expected_title $entry $value $button]
set status [catch {uplevel 1 $script} result options]
incr dialog_serial
@@ -183,24 +183,24 @@ proc ::mged::xmin::test::with_dialog_answer {
return $result
}
proc ::mged::xmin::test::finish {status message} {
proc ::mged::gui::test::finish {status message} {
variable finished
if {$finished} {
return
}
set finished 1
catch {update}
lassign [::xmin::test::check_background_errors $status $message] \
lassign [::gui::test::check_background_errors $status $message] \
status message
puts $message
::xmin::test::write result $message
::gui::test::write result $message
if {$status != 0} {
puts stderr $message
}
_mged_quit
}
proc ::mged::xmin::test::run_checked {body pass_message failure_prefix} {
proc ::mged::gui::test::run_checked {body pass_message failure_prefix} {
global mged_gui mged_players
variable ready_retries
variable ready_retry_limit
@@ -211,7 +211,7 @@ proc ::mged::xmin::test::run_checked {body pass_message failure_prefix} {
finish 1 "FAIL: $failure_prefix: MGED did not create a GUI player"
return
}
after $retry_delay_ms [list ::mged::xmin::test::run_checked \
after $retry_delay_ms [list ::mged::gui::test::run_checked \
$body $pass_message $failure_prefix]
return
}
@@ -224,7 +224,7 @@ proc ::mged::xmin::test::run_checked {body pass_message failure_prefix} {
finish 1 "FAIL: $failure_prefix: MGED did not finish mapping its GUI"
return
}
after $retry_delay_ms [list ::mged::xmin::test::run_checked \
after $retry_delay_ms [list ::mged::gui::test::run_checked \
$body $pass_message $failure_prefix]
return
}
@@ -234,7 +234,7 @@ proc ::mged::xmin::test::run_checked {body pass_message failure_prefix} {
if {[dict exists $options -errorinfo]} {
set error_info [dict get $options -errorinfo]
puts stderr $error_info
::xmin::test::write tcl_error_debug $error_info
::gui::test::write tcl_error_debug $error_info
}
finish 1 "FAIL: $failure_prefix: $message"
return
@@ -242,8 +242,8 @@ proc ::mged::xmin::test::run_checked {body pass_message failure_prefix} {
finish 0 "PASS: $pass_message"
}
proc ::mged::xmin::test::start {body pass_message failure_prefix} {
after 25 [list ::mged::xmin::test::run_checked \
proc ::mged::gui::test::start {body pass_message failure_prefix} {
after 25 [list ::mged::gui::test::run_checked \
$body $pass_message $failure_prefix]
}
+51 -51
View File
@@ -21,13 +21,13 @@
# Exercise MGED creation, browsing, and collaboration workflows through live
# Tk controls and verify their database or display-state results.
if {![info exists ::env(XMIN_GUI_LIBRARY)] ||
![info exists ::env(XMIN_TEST_DIR)]} {
puts stderr "XMIN_GUI_LIBRARY and XMIN_TEST_DIR are required"
if {![info exists ::env(GUI_TEST_LIBRARY)] ||
![info exists ::env(GUI_TEST_DIR)]} {
puts stderr "GUI_TEST_LIBRARY and GUI_TEST_DIR are required"
exit 2
}
source $::env(XMIN_GUI_LIBRARY)
::xmin::test::capture_background_errors
source $::env(GUI_TEST_LIBRARY)
::gui::test::capture_background_errors
namespace eval ::mged::xmin::workflows {
variable dialog_seen 0
@@ -69,8 +69,8 @@ proc ::mged::xmin::workflows::require_near {actual expected description} {
}
proc ::mged::xmin::workflows::invoke {root labels} {
::xmin::test::write progress "invoke: [join $labels { > }]"
::xmin::test::invoke_menu_entry $root $labels
::gui::test::write progress "invoke: [join $labels { > }]"
::gui::test::invoke_menu_entry $root $labels
settle
}
@@ -193,10 +193,10 @@ proc ::mged::xmin::workflows::reject_edit {} {
}
proc ::mged::xmin::workflows::assert_created {name panel context} {
::xmin::test::require {[exists $name]} "$context did not create $name"
::xmin::test::require {![winfo exists $panel]} \
::gui::test::require {[exists $name]} "$context did not create $name"
::gui::test::require {![winfo exists $panel]} \
"$context did not dismiss its creation dialog"
::xmin::test::require {[llength [get $name]] > 1} \
::gui::test::require {[llength [get $name]] > 1} \
"$context created an unreadable database object"
reject_edit
}
@@ -207,9 +207,9 @@ proc ::mged::xmin::workflows::create_generic_primitive {id top type menu_path} {
set name xmin_$type.s
invoke $top [concat $menu_path [list [format "%s..." $type]]]
set panel .$id.make_solid
::xmin::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
::gui::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
"Create $type did not open the generic primitive dialog"
::xmin::test::require {
::gui::test::require {
[string first $type [$panel.nameL cget -text]] >= 0
} "Create $type opened a dialog for the wrong primitive type"
@@ -272,42 +272,42 @@ proc ::mged::xmin::workflows::write_text_file {path contents} {
proc ::mged::xmin::workflows::exercise_file_operations {id top database} {
global ex_control
set extracted_database [file join $::env(XMIN_TEST_DIR) extracted.g]
set extracted_database [file join $::env(GUI_TEST_DIR) extracted.g]
invoke $top {File Export {Database Objects}}
set extract_panel .$id.do_extract
::xmin::test::require {
::gui::test::require {
[winfo exists $extract_panel] && [winfo ismapped $extract_panel]
} "Database Objects export did not open the Extract Objects dialog"
set ex_control($id,file) $extracted_database
set ex_control($id,objects) xmin_arb8.s
$extract_panel.okB invoke
settle
::xmin::test::require {
::gui::test::require {
[file exists $extracted_database] && [file size $extracted_database] > 0
} "Database Objects export did not write a database"
set ascii_database [file join $::env(XMIN_TEST_DIR) workflows.asc]
set ascii_database [file join $::env(GUI_TEST_DIR) workflows.asc]
with_file_dialog_path ::tk_getSaveFile $ascii_database \
[list invoke $top {File Export {Ascii Database}}]
::xmin::test::require {
::gui::test::require {
[file exists $ascii_database] && [file size $ascii_database] > 0
} "Ascii Database export did not write a file"
set script [file join $::env(XMIN_TEST_DIR) loaded-script.tcl]
set script [file join $::env(GUI_TEST_DIR) loaded-script.tcl]
write_text_file $script \
{set ::mged::xmin::workflows::script_loaded 1}
set ::mged::xmin::workflows::script_loaded 0
with_file_dialog_path ::tk_getOpenFile $script \
[list with_dialog_answer .mged_dialog {Script loaded} "" "" \
[list invoke $top {File {Load Script...}}]]
::xmin::test::require {$::mged::xmin::workflows::script_loaded} \
::gui::test::require {$::mged::xmin::workflows::script_loaded} \
"Load Script did not evaluate the selected Tcl file"
set new_database [file join $::env(XMIN_TEST_DIR) new-database.g]
set new_database [file join $::env(GUI_TEST_DIR) new-database.g]
with_file_dialog_path ::tk_getSaveFile $new_database \
[list with_dialog_answer .mged_dialog {File created} "" "" \
[list invoke $top {File {New...}}]]
::xmin::test::require {
::gui::test::require {
[file exists $new_database] &&
[file normalize [_mged_opendb]] eq [file normalize $new_database]
} "New did not create and open the selected database"
@@ -315,7 +315,7 @@ proc ::mged::xmin::workflows::exercise_file_operations {id top database} {
with_file_dialog_path ::tk_getOpenFile $database \
[list with_dialog_answer .mged_dialog {File loaded} "" "" \
[list invoke $top {File {Open...}}]]
::xmin::test::require {
::gui::test::require {
[file normalize [_mged_opendb]] eq [file normalize $database] &&
[exists xmin_arb8.s]
} "Open did not restore the selected database"
@@ -323,25 +323,25 @@ proc ::mged::xmin::workflows::exercise_file_operations {id top database} {
with_file_dialog_path ::tk_getOpenFile $extracted_database \
[list with_dialog_answer .$id.prefix Prefix .$id.prefix.mid.ent bin_ \
[list invoke $top {File Import {Binary Database}}]]
::xmin::test::require {[exists bin_xmin_arb8.s]} \
::gui::test::require {[exists bin_xmin_arb8.s]} \
"Binary Database import did not apply its prefix"
with_file_dialog_path ::tk_getOpenFile $ascii_database \
[list with_dialog_answer .$id.prefix Prefix .$id.prefix.mid.ent asc_ \
[list invoke $top {File Import {Ascii Database}}]]
::xmin::test::require {[exists asc_xmin_arb8.s]} \
::gui::test::require {[exists asc_xmin_arb8.s]} \
"Ascii Database import did not apply its prefix"
}
proc ::mged::xmin::workflows::exercise_dsp_creation {id top} {
global mged_gui
set data_file [file join $::env(XMIN_TEST_DIR) dsp-data.bw]
set data_file [file join $::env(GUI_TEST_DIR) dsp-data.bw]
write_binary_file $data_file [binary format S* {0 1 2 3}]
invoke $top {Create dsp...}
set panel .$id.make_dsp
::xmin::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
::gui::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
"Create dsp did not open its specialized dialog"
set name xmin_dsp.s
@@ -357,12 +357,12 @@ proc ::mged::xmin::workflows::exercise_dsp_creation {id top} {
}
proc ::mged::xmin::workflows::exercise_binunif_creation {id top} {
set data_file [file join $::env(XMIN_TEST_DIR) binunif-data.bin]
set data_file [file join $::env(GUI_TEST_DIR) binunif-data.bin]
write_binary_file $data_file [binary format c* {1 2 3 4}]
invoke $top {Create binunif...}
set panel .$id.make_binunif
::xmin::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
::gui::test::require {[winfo exists $panel] && [winfo ismapped $panel]} \
"Create binunif did not open its specialized dialog"
set name xmin_binunif
@@ -378,11 +378,11 @@ proc ::mged::xmin::workflows::exercise_binunif_creation {id top} {
proc ::mged::xmin::workflows::exercise_geometry_browser {id top} {
invoke $top {Tools {Geometry Browser}}
set browser .$id.geometree
::xmin::test::require {
::gui::test::require {
[llength [info commands $browser]] == 1 &&
[winfo exists $browser] && [winfo ismapped $browser]
} "Geometry Browser did not open"
::xmin::test::require {[wm title $browser] eq "Geometry Browser"} \
::gui::test::require {[wm title $browser] eq "Geometry Browser"} \
"Geometry Browser has an unexpected title"
set children [$browser getNodeChildren "" yes]
@@ -390,55 +390,55 @@ proc ::mged::xmin::workflows::exercise_geometry_browser {id top} {
foreach child $children {
lappend roots [lindex $child 0]
}
::xmin::test::require {
::gui::test::require {
[lsearch -exact $roots /xmin_arb8.s] >= 0 &&
[lsearch -exact $roots /xmin_binunif] >= 0
} "Geometry Browser did not populate the isolated database"
::xmin::test::write progress "browser: display"
::gui::test::write progress "browser: display"
$browser displayNode /xmin_arb8.s alone
settle
::xmin::test::require {[lsearch -exact [who] xmin_arb8.s] >= 0} \
::gui::test::require {[lsearch -exact [who] xmin_arb8.s] >= 0} \
"Geometry Browser did not display the selected primitive"
::xmin::test::write progress "browser: autosize"
::gui::test::write progress "browser: autosize"
$browser autosizeDisplay
::xmin::test::write progress "browser: zoom"
::gui::test::write progress "browser: zoom"
$browser zoomDisplay in
$browser zoomDisplay out
::xmin::test::write progress "browser: clear"
::gui::test::write progress "browser: clear"
$browser clearDisplay
settle
::xmin::test::require {[who] eq ""} \
::gui::test::require {[who] eq ""} \
"Geometry Browser did not clear the display"
::xmin::test::write progress "browser: close"
::gui::test::write progress "browser: close"
set file_menu [$browser.menubar entrycget 0 -menu]
::xmin::test::require {[$file_menu entrycget 0 -label] eq "Close"} \
::gui::test::require {[$file_menu entrycget 0 -label] eq "Close"} \
"Geometry Browser does not expose its Close command"
$file_menu invoke 0
settle
::xmin::test::require {![winfo exists $browser]} "Geometry Browser did not close"
::gui::test::require {![winfo exists $browser]} "Geometry Browser did not close"
}
proc ::mged::xmin::workflows::exercise_collaboration {id} {
global mged_collaborators mged_gui mged_players
::xmin::test::write progress "collaboration: join primary"
::gui::test::write progress "collaboration: join primary"
if {[lsearch -exact $mged_collaborators $id] < 0} {
collaborate join $id
}
set peer xmin_peer
::xmin::test::write progress "collaboration: create peer"
::gui::test::write progress "collaboration: create peer"
set result [gui -config b -dt tkswrast -id $peer -join]
if {$result ne ""} {
fail "could not create collaborating MGED player: $result"
}
settle
::xmin::test::write progress "collaboration: peer created"
::gui::test::write progress "collaboration: peer created"
::xmin::test::require {
::gui::test::require {
[lsearch -exact $mged_players $peer] >= 0 &&
[lsearch -exact $mged_collaborators $peer] >= 0 &&
$mged_gui($peer,collaborate)
@@ -453,7 +453,7 @@ proc ::mged::xmin::workflows::exercise_collaboration {id} {
require_near [size] {123} "collaborative view size"
collaborate quit $peer
::xmin::test::require {
::gui::test::require {
[lsearch -exact $mged_collaborators $peer] < 0 &&
!$mged_gui($peer,collaborate)
} "second MGED player did not leave the collaborative session"
@@ -461,7 +461,7 @@ proc ::mged::xmin::workflows::exercise_collaboration {id} {
set mged_gui($peer,show_cmd) 0
set mged_gui($peer,show_dm) 0
gui_destroy $peer
::xmin::test::require {[lsearch -exact $mged_players $peer] < 0} \
::gui::test::require {[lsearch -exact $mged_players $peer] < 0} \
"second MGED player did not shut down cleanly"
collaborate quit $id
@@ -475,10 +475,10 @@ proc ::mged::xmin::workflows::finish {status message} {
}
set finished 1
catch {update}
lassign [::xmin::test::check_background_errors $status $message] \
lassign [::gui::test::check_background_errors $status $message] \
status message
puts $message
::xmin::test::write result $message
::gui::test::write result $message
if {$status != 0} {
puts stderr $message
}
@@ -509,8 +509,8 @@ proc ::mged::xmin::workflows::run {} {
return
}
set database [file join $::env(XMIN_TEST_DIR) workflows.g]
cd $::env(XMIN_TEST_DIR)
set database [file join $::env(GUI_TEST_DIR) workflows.g]
cd $::env(GUI_TEST_DIR)
file delete -force $database
opendb $database y
title {Xmin MGED workflow regression}
@@ -530,7 +530,7 @@ proc ::mged::xmin::workflows::run_checked {} {
if {[dict exists $options -errorinfo]} {
set error_info [dict get $options -errorinfo]
puts stderr $error_info
::xmin::test::write tcl_error_debug $error_info
::gui::test::write tcl_error_debug $error_info
}
finish 1 "FAIL: MGED workflow regression: $message"
}
+7 -7
View File
@@ -2,12 +2,12 @@
set -eu
: "${XMIN_TEST_SHELL:?XMIN_TEST_SHELL is required}"
. "$XMIN_TEST_SHELL"
: "${GUI_TEST_SHELL:?GUI_TEST_SHELL is required}"
. "$GUI_TEST_SHELL"
window=$1
geometry=$("$XMIN_CTL" geometry "$window") ||
xmin_fail "could not query qged window geometry"
geometry=$("$GUI_TEST_CTL" geometry "$window") ||
gui_fail "could not query qged window geometry"
set -- $geometry
width=$3
height=$4
@@ -17,9 +17,9 @@ height=$4
# that changes the OpenGL view without depending on evolving qged controls.
console_x=$((width / 2))
console_y=$((height * 94 / 100))
"$XMIN_CTL" click "$window" "$console_x" "$console_y" 1
"$XMIN_CTL" type "draw all.g"
"$XMIN_CTL" key enter
"$GUI_TEST_CTL" click "$window" "$console_x" "$console_y" 1
"$GUI_TEST_CTL" type "draw all.g"
"$GUI_TEST_CTL" key enter
QGED_DRAW_SETTLE_SECONDS=${QGED_DRAW_SETTLE_SECONDS:-2}
sleep "$QGED_DRAW_SETTLE_SECONDS"
+2 -2
View File
@@ -2,14 +2,14 @@
set -eu
: "${XMIN_ARTIFACT_DIR:?XMIN_ARTIFACT_DIR is required}"
: "${GUI_ARTIFACT_DIR:?GUI_ARTIFACT_DIR is required}"
if [ "$#" -lt 1 ]; then
echo "Usage: $0 qged [argument ...]" >&2
exit 2
fi
XDG_CONFIG_HOME="$XMIN_ARTIFACT_DIR/config"
XDG_CONFIG_HOME="$GUI_ARTIFACT_DIR/config"
export XDG_CONFIG_HOME
exec "$@"
+6 -6
View File
@@ -2,12 +2,12 @@
set -eu
: "${XMIN_TEST_SHELL:?XMIN_TEST_SHELL is required}"
. "$XMIN_TEST_SHELL"
: "${GUI_TEST_SHELL:?GUI_TEST_SHELL is required}"
. "$GUI_TEST_SHELL"
window=$1
geometry=$("$XMIN_CTL" geometry "$window") ||
xmin_fail "could not query rtwizard window geometry"
geometry=$("$GUI_TEST_CTL" geometry "$window") ||
gui_fail "could not query rtwizard window geometry"
set -- $geometry
width=$3
height=$4
@@ -18,8 +18,8 @@ selector_x=$((width * 28 / 100))
selector_y=$((height * 30 / 100))
choice_x=$((width * 15 / 100))
choice_y=$((height * 39 / 100))
"$XMIN_CTL" click "$window" "$selector_x" "$selector_y" 1
"$XMIN_CTL" click "$window" "$choice_x" "$choice_y" 1
"$GUI_TEST_CTL" click "$window" "$selector_x" "$selector_y" 1
"$GUI_TEST_CTL" click "$window" "$choice_x" "$choice_y" 1
RTWIZARD_PAGE_SETTLE_SECONDS=${RTWIZARD_PAGE_SETTLE_SECONDS:-1}
sleep "$RTWIZARD_PAGE_SETTLE_SECONDS"
+43 -43
View File
@@ -18,12 +18,12 @@
# information.
#
###
if {![info exists ::env(XMIN_GUI_LIBRARY)] ||
if {![info exists ::env(GUI_TEST_LIBRARY)] ||
![info exists ::env(RTWIZARD_TEST_DATABASE)]} {
puts stderr "XMIN_GUI_LIBRARY and RTWIZARD_TEST_DATABASE are required"
puts stderr "GUI_TEST_LIBRARY and RTWIZARD_TEST_DATABASE are required"
exit 2
}
source $::env(XMIN_GUI_LIBRARY)
source $::env(GUI_TEST_LIBRARY)
namespace eval ::rtwizard::xmin {
variable dialog_seen ""
@@ -38,7 +38,7 @@ namespace eval ::rtwizard::xmin {
proc ::rtwizard::xmin::finish {status message} {
catch {destroy .}
catch {update}
lassign [::xmin::test::check_background_errors $status $message] \
lassign [::gui::test::check_background_errors $status $message] \
status message
puts $message
if {$status != 0} {
@@ -55,14 +55,14 @@ proc ::rtwizard::xmin::compare_manifest {actual} {
set expected [string trim [read $channel]]
close $channel
if {$actual ne $expected} {
::xmin::test::write menu_inventory_actual $actual
::gui::test::write menu_inventory_actual $actual
error "live rtwizard menu inventory differs from its manifest"
}
}
proc ::rtwizard::xmin::toplevels {} {
set toplevels {}
foreach widget [linsert [::xmin::test::descendants .] 0 .] {
foreach widget [linsert [::gui::test::descendants .] 0 .] {
if {[winfo toplevel $widget] eq $widget} {
lappend toplevels $widget
}
@@ -89,18 +89,18 @@ proc ::rtwizard::xmin::exercise_dialog {labels expected_title} {
variable dialog_poll_delay_ms
set dialog_seen ""
after $dialog_poll_delay_ms ::rtwizard::xmin::dismiss_dialog
::xmin::test::invoke_menu_entry . $labels
::xmin::test::require {$dialog_seen ne ""} \
::gui::test::invoke_menu_entry . $labels
::gui::test::require {$dialog_seen ne ""} \
"menu entry did not display a dialog: [join $labels { > }]"
if {$expected_title ne ""} {
::xmin::test::require {$dialog_seen eq $expected_title} \
::gui::test::require {$dialog_seen eq $expected_title} \
"unexpected dialog title '$dialog_seen', expected '$expected_title'"
}
}
proc ::rtwizard::xmin::step_labels {} {
set labels {}
foreach entry [split [::xmin::test::menu_inventory .] "\n"] {
foreach entry [split [::gui::test::menu_inventory .] "\n"] {
if {[lindex $entry 0] eq "Steps" &&
[lindex $entry end-1] ne "separator"} {
lappend labels [lindex $entry 1]
@@ -111,12 +111,12 @@ proc ::rtwizard::xmin::step_labels {} {
proc ::rtwizard::xmin::set_framebuffer_entry {component variable value} {
set control [$::fbp component $component]
::xmin::test::require {[winfo exists $control]} \
::gui::test::require {[winfo exists $control]} \
"missing framebuffer-page component: $component"
$control clear
$control insert 0 $value
update idletasks
::xmin::test::require {
::gui::test::require {
$::RtWizard::wizard_state($variable) eq $value
} "framebuffer control $component did not update $variable"
}
@@ -129,12 +129,12 @@ proc ::rtwizard::xmin::exercise_framebuffer_controls {} {
set height 72
foreach {component value} [list width $width height $height] {
set control [$::fbp component $component]
::xmin::test::require {[winfo exists $control]} \
::gui::test::require {[winfo exists $control]} \
"missing framebuffer-page component: $component"
$control clear
$control insert 0 $value
}
::xmin::test::require {
::gui::test::require {
[$::fbp getWidth] == $width && [$::fbp getHeight] == $height
} "framebuffer dimension controls did not retain their values"
@@ -149,23 +149,23 @@ proc ::rtwizard::xmin::exercise_framebuffer_controls {} {
}
set animation [$::fbp component cutAnimation]
::xmin::test::require {[winfo exists $animation]} \
::gui::test::require {[winfo exists $animation]} \
"missing framebuffer-page component: cutAnimation"
set ::RtWizard::wizard_state(make_animation) 0
$animation invoke
::xmin::test::require {$::RtWizard::wizard_state(make_animation) == 1} \
::gui::test::require {$::RtWizard::wizard_state(make_animation) == 1} \
"cut-animation control did not update its state"
set radiobox [$::fbp component radBox]
set filename [$::fbp component fileName]
set browse [$::fbp component browse]
$radiobox select toScreen
::xmin::test::require {
::gui::test::require {
[$filename cget -state] eq "disabled" &&
[$browse cget -state] eq "disabled"
} "screen output did not disable file controls"
$radiobox select toFile
::xmin::test::require {
::gui::test::require {
[$filename cget -state] eq "normal" &&
[$browse cget -state] eq "normal"
} "file output did not enable file controls"
@@ -183,12 +183,12 @@ proc ::rtwizard::xmin::invoke_render_with_port_contention {} {
return [::rtwizard::xmin::actual_port_occupied $logical_port]
}
try {
::xmin::test::invoke_menu_entry . {Render Full-Size}
::gui::test::invoke_menu_entry . {Render Full-Size}
} finally {
rename ::rtwiz_port_occupied {}
rename ::rtwizard::xmin::actual_port_occupied ::rtwiz_port_occupied
}
::xmin::test::require {$forced_port_probe} \
::gui::test::require {$forced_port_probe} \
"rtwizard did not exercise its occupied framebuffer-port retry"
}
@@ -207,13 +207,13 @@ proc ::rtwizard::xmin::run {} {
}
set expected_title "RtWizard - [file tail $::env(RTWIZARD_TEST_DATABASE)]"
::xmin::test::require {[wm title .] eq $expected_title} \
::gui::test::require {[wm title .] eq $expected_title} \
"unexpected rtwizard main-window title"
set widget_inventory [::xmin::test::widget_inventory .]
set menu_inventory [::xmin::test::menu_inventory .]
::xmin::test::write widget_inventory $widget_inventory
::xmin::test::write menu_inventory $menu_inventory
set widget_inventory [::gui::test::widget_inventory .]
set menu_inventory [::gui::test::menu_inventory .]
::gui::test::write widget_inventory $widget_inventory
::gui::test::write menu_inventory $menu_inventory
compare_manifest $menu_inventory
foreach labels {
@@ -224,8 +224,8 @@ proc ::rtwizard::xmin::run {} {
{Help Help...}
{Help About...}
} {
::xmin::test::require {
[::xmin::test::find_menu_entry . $labels] ne ""
::gui::test::require {
[::gui::test::find_menu_entry . $labels] ne ""
} "missing rtwizard menu entry: [join $labels { > }]"
}
@@ -241,8 +241,8 @@ proc ::rtwizard::xmin::run {} {
{Configure Ghost Elements} {Configure Full-Color Elements}
{Configure Line-Drawing Elements}}
} {
::xmin::test::invoke_menu_entry . [list Image $title]
::xmin::test::require {[$::exp getImageType] eq $title} \
::gui::test::invoke_menu_entry . [list Image $title]
::gui::test::require {[$::exp getImageType] eq $title} \
"rtwizard did not select image type: $title"
foreach page [concat {dbp fbp intro help exp} $pages] {
$::wizardInstance select $page
@@ -251,7 +251,7 @@ proc ::rtwizard::xmin::run {} {
set expected_steps [concat $expected_steps \
{Greeting {Configure Framebuffer}}]
set actual_steps [step_labels]
::xmin::test::require {
::gui::test::require {
$actual_steps eq [lsort -dictionary $expected_steps]
} "unexpected Steps menu for '$title': $actual_steps"
}
@@ -262,13 +262,13 @@ proc ::rtwizard::xmin::run {} {
set config_file $::env(RTWIZARD_RCFILE)
exercise_dialog {File {Write .rtwizardrc}} ""
::xmin::test::require {[file exists $config_file]} \
::gui::test::require {[file exists $config_file]} \
"rtwizard did not create the requested configuration file"
set channel [open $config_file r]
set config [read $channel]
close $channel
foreach setting {wizard_width wizard_height gpane} {
::xmin::test::require {
::gui::test::require {
[string first "set ::$setting " $config] >= 0
} "rtwizard configuration omitted $setting"
}
@@ -283,7 +283,7 @@ proc ::rtwizard::xmin::run {} {
set ::RtWizard::wizard_state(make_animation) 0
set rgb_bytes_per_pixel 3
set expected_size [expr {$render_dimension * $render_dimension * $rgb_bytes_per_pixel}]
::xmin::test::require {
::gui::test::require {
[info exists ::RtWizard::wizard_state(verbose)]
} "RaytraceWizard did not initialize its optional verbose state"
set force_port_contention true
@@ -295,18 +295,18 @@ proc ::rtwizard::xmin::run {} {
{Ghost Image with Insert} ghost
{Ghost Image with Insert and Lines} ghost-line
} {
::xmin::test::invoke_menu_entry . [list Image $title]
set render_file [file join $::env(XMIN_TEST_DIR) rtwizard-$basename.pix]
::gui::test::invoke_menu_entry . [list Image $title]
set render_file [file join $::env(GUI_TEST_DIR) rtwizard-$basename.pix]
set ::RtWizard::wizard_state(output_filename) $render_file
if {$force_port_contention} {
invoke_render_with_port_contention
set force_port_contention false
} else {
::xmin::test::invoke_menu_entry . {Render Full-Size}
::gui::test::invoke_menu_entry . {Render Full-Size}
}
::xmin::test::require {[file exists $render_file]} \
::gui::test::require {[file exists $render_file]} \
"rtwizard '$title' render did not create its output file"
::xmin::test::require {[file size $render_file] == $expected_size} \
::gui::test::require {[file size $render_file] == $expected_size} \
"rtwizard '$title' render has an unexpected byte count"
}
@@ -314,10 +314,10 @@ proc ::rtwizard::xmin::run {} {
set expected_preview_size [expr {
$preview_dimension * $preview_dimension * $rgb_bytes_per_pixel
}]
set preview_file [file join $::env(XMIN_TEST_DIR) rtwizard-preview.pix]
set preview_file [file join $::env(GUI_TEST_DIR) rtwizard-preview.pix]
set ::RtWizard::wizard_state(output_filename) $preview_file
::xmin::test::invoke_menu_entry . {Render Preview}
::xmin::test::require {
::gui::test::invoke_menu_entry . {Render Preview}
::gui::test::require {
[file exists $preview_file] && [file size $preview_file] == $expected_preview_size
} "rtwizard preview did not create the expected half-size image"
@@ -337,14 +337,14 @@ proc ::rtwizard::xmin::run_checked {} {
}
namespace eval ::RtWizard {}
set ::env(RTWIZARD_RCFILE) [file join $::env(XMIN_TEST_DIR) .rtwizardrc]
set ::env(RTWIZARD_RCFILE) [file join $::env(GUI_TEST_DIR) .rtwizardrc]
set ::RtWizard::wizard_state(dbFile) $::env(RTWIZARD_TEST_DATABASE)
foreach object_list {color_objlist ghost_objlist line_objlist} {
set ::RtWizard::wizard_state($object_list) {all.g}
}
set argv {}
set argc 0
::xmin::test::capture_background_errors
::gui::test::capture_background_errors
after $::rtwizard::xmin::startup_delay_ms ::rtwizard::xmin::run_checked
package require RaytraceWizard
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
set -eu
: "${XMIN_CTL:?XMIN_CTL is required}"
if [ "$#" -lt 1 ]; then
echo "Usage: $0 command [argument ...]" >&2
exit 2
fi
command=$1
shift
case "$command" in
capture-screen)
exec "$XMIN_CTL" capture-root "$@"
;;
screen-geometry)
: "${GUI_TEST_SCREEN:?GUI_TEST_SCREEN is required}"
width=${GUI_TEST_SCREEN%%x*}
height_depth=${GUI_TEST_SCREEN#*x}
height=${height_depth%%x*}
printf '%s %s\n' "$width" "$height"
;;
*)
exec "$XMIN_CTL" "$command" "$@"
;;
esac
# Local Variables:
# tab-width: 8
# mode: sh
# sh-indentation: 4
# sh-basic-offset: 4
# indent-tabs-mode: t
# End:
# ex: shiftwidth=4 tabstop=8 cino=N-s
-88
View File
@@ -1,88 +0,0 @@
#!/bin/sh
set -eu
: "${XMIN_TEST_SHELL:?XMIN_TEST_SHELL is required}"
. "$XMIN_TEST_SHELL"
xmin_stop_app()
{
if [ -n "${XMIN_APP_PID:-}" ] && kill -0 "$XMIN_APP_PID" 2>/dev/null; then
kill "$XMIN_APP_PID" >/dev/null 2>&1 || true
fi
}
if [ "$#" -lt 2 ]; then
echo "Usage: $0 expected-window executable [argument ...]" >&2
exit 2
fi
: "${XMIN_CTL:?XMIN_CTL is required}"
expected_window=$1
shift
application=$1
shift
xmin_require_executable "$XMIN_CTL"
xmin_require_executable "$application"
XMIN_ARTIFACT_DIR=$(mktemp -d /tmp/brlcad-xmin-smoke.XXXXXX)
export XMIN_ARTIFACT_DIR
XMIN_APP_LOG="$XMIN_ARTIFACT_DIR/application.log"
export XMIN_APP_LOG
trap xmin_stop_app EXIT INT TERM
"$application" "$@" >"$XMIN_APP_LOG" 2>&1 &
XMIN_APP_PID=$!
export XMIN_APP_PID
if ! window_id=$("$XMIN_CTL" wait-window --timeout "$XMIN_WAIT_TIMEOUT_MS" \
"$expected_window"); then
xmin_fail "window did not appear: $expected_window"
fi
if ! kill -0 "$XMIN_APP_PID" 2>/dev/null; then
"$XMIN_CTL" wait-stable --quiet "$XMIN_STABLE_QUIET_MS" \
--timeout "$XMIN_STABLE_TIMEOUT_MS" "$window_id" >/dev/null 2>&1 || true
xmin_fail "application exited before its window could be tested"
fi
"$XMIN_CTL" activate "$window_id"
if ! geometry=$(xmin_wait_for_minimum_geometry "$window_id" \
"$XMIN_MIN_WINDOW_WIDTH" "$XMIN_MIN_WINDOW_HEIGHT"); then
xmin_fail "window did not reach a usable geometry: $expected_window ($geometry)"
fi
xmin_capture_root "$window_id" "$XMIN_ARTIFACT_DIR/before.ppm"
if [ -n "${XMIN_SMOKE_ACTION:-}" ]; then
xmin_require_executable "$XMIN_SMOKE_ACTION"
"$XMIN_SMOKE_ACTION" "$window_id" "$XMIN_ARTIFACT_DIR"
if [ "${XMIN_MIN_CHANGED_BYTES:-0}" -gt 0 ]; then
xmin_wait_for_image_difference "$XMIN_ARTIFACT_DIR/before.ppm" \
"$XMIN_ARTIFACT_DIR/after.ppm" "$XMIN_MIN_CHANGED_BYTES" \
"$window_id"
else
xmin_capture_root "$window_id" "$XMIN_ARTIFACT_DIR/after.ppm"
fi
fi
"$XMIN_CTL" close "$window_id"
wait "$XMIN_APP_PID"
XMIN_APP_PID=
if [ "${XMIN_KEEP_ARTIFACTS:-0}" -eq 1 ]; then
echo "Xmin GUI smoke artifacts: $XMIN_ARTIFACT_DIR"
else
"${CMAKE_COMMAND:-cmake}" -E remove_directory "$XMIN_ARTIFACT_DIR"
fi
echo "PASS: $expected_window launched, rendered, and closed under Xmin"
# Local Variables:
# tab-width: 8
# mode: sh
# sh-indentation: 4
# sh-basic-offset: 4
# indent-tabs-mode: t
# End:
# ex: shiftwidth=4 tabstop=8 cino=N-s
-60
View File
@@ -1,60 +0,0 @@
#!/bin/sh
set -eu
: "${XMIN_TEST_SHELL:?XMIN_TEST_SHELL is required}"
. "$XMIN_TEST_SHELL"
if [ "$#" -lt 2 ]; then
echo "Usage: $0 test-name executable [argument ...]" >&2
exit 2
fi
test_name=$1
shift
test_program=$1
xmin_require_executable "$test_program"
XMIN_TEST_DIR=$(mktemp -d "/tmp/brlcad-xmin-${test_name}.XXXXXX")
export XMIN_TEST_DIR
xmin_tcl_test_cleanup()
{
if [ "${XMIN_KEEP_ARTIFACTS:-0}" -eq 0 ]; then
"${CMAKE_COMMAND:-cmake}" -E remove_directory "$XMIN_TEST_DIR"
fi
}
trap 'exit 1' INT TERM
if ! "$@"; then
echo "Xmin GUI test artifacts: $XMIN_TEST_DIR" >&2
exit 1
fi
if [ -s "$XMIN_TEST_DIR/result" ]; then
test_result=$(sed -n '1p' "$XMIN_TEST_DIR/result")
case "$test_result" in
PASS:*) ;;
*)
echo "$test_result" >&2
echo "Xmin GUI test artifacts: $XMIN_TEST_DIR" >&2
exit 1
;;
esac
fi
if [ "${XMIN_KEEP_ARTIFACTS:-0}" -eq 1 ]; then
echo "Xmin GUI test artifacts: $XMIN_TEST_DIR"
else
xmin_tcl_test_cleanup
fi
# Local Variables:
# tab-width: 8
# mode: sh
# sh-indentation: 4
# sh-basic-offset: 4
# indent-tabs-mode: t
# End:
# ex: shiftwidth=4 tabstop=8 cino=N-s
-123
View File
@@ -1,123 +0,0 @@
#!/bin/sh
XMIN_WAIT_TIMEOUT_MS=${XMIN_WAIT_TIMEOUT_MS:-30000}
XMIN_STABLE_QUIET_MS=${XMIN_STABLE_QUIET_MS:-200}
XMIN_STABLE_TIMEOUT_MS=${XMIN_STABLE_TIMEOUT_MS:-5000}
XMIN_POLL_SECONDS=${XMIN_POLL_SECONDS:-0.05}
XMIN_MIN_WINDOW_WIDTH=${XMIN_MIN_WINDOW_WIDTH:-200}
XMIN_MIN_WINDOW_HEIGHT=${XMIN_MIN_WINDOW_HEIGHT:-150}
xmin_fail()
{
echo "FAIL: $1" >&2
if [ -n "${XMIN_APP_LOG:-}" ] && [ -f "$XMIN_APP_LOG" ]; then
sed -n '1,240p' "$XMIN_APP_LOG" >&2
fi
exit 1
}
xmin_require_executable()
{
if [ ! -x "$1" ]; then
xmin_fail "required executable is unavailable: $1"
fi
}
xmin_wait_for_minimum_geometry()
{
window=$1
minimum_width=$2
minimum_height=$3
timeout_seconds=$(((XMIN_WAIT_TIMEOUT_MS + 999) / 1000))
start_time=$(date +%s)
while :; do
geometry=$("$XMIN_CTL" geometry "$window") || return 1
set -- $geometry
if [ "$#" -ge 4 ]; then
width=$3
height=$4
if [ "$width" -ge "$minimum_width" ] &&
[ "$height" -ge "$minimum_height" ]; then
echo "$geometry"
return 0
fi
fi
current_time=$(date +%s)
if [ $((current_time - start_time)) -ge "$timeout_seconds" ]; then
echo "$geometry"
return 1
fi
sleep "$XMIN_POLL_SECONDS"
done
}
xmin_capture_root()
{
window=$1
output=$2
"$XMIN_CTL" wait-stable --quiet "$XMIN_STABLE_QUIET_MS" \
--timeout "$XMIN_STABLE_TIMEOUT_MS" "$window" >/dev/null 2>&1 || true
# Tk and Qt compose child windows into the root; a top-level-only capture
# can therefore contain just the parent's cleared backing surface.
"$XMIN_CTL" capture-root "$output" ||
xmin_fail "could not capture the Xmin root window"
if [ "$(sed -n '1p' "$output")" != "P6" ]; then
xmin_fail "window capture is not a binary PPM image: $output"
fi
nonzero_bytes=$(LC_ALL=C tr -d '\000' < "$output" | wc -c | tr -d ' ')
if [ "$nonzero_bytes" -lt 1024 ]; then
xmin_fail "window capture contains no meaningful rendered content"
fi
}
xmin_count_changed_bytes()
{
first=$1
second=$2
minimum=$3
# Stop once the threshold is reached. Full root captures are large, and
# their exact total difference is immaterial to this binary assertion.
changed_bytes=$(cmp -l "$first" "$second" | awk -v minimum="$minimum" \
'NR >= minimum { print NR; exit }
END { if (NR < minimum) print NR }')
echo "$changed_bytes"
}
xmin_wait_for_image_difference()
{
first=$1
second=$2
minimum=$3
window=$4
timeout_seconds=$(((XMIN_WAIT_TIMEOUT_MS + 999) / 1000))
start_time=$(date +%s)
if [ ! -s "$first" ]; then
xmin_fail "cannot compare a missing or empty Xmin capture: $first"
fi
while :; do
xmin_capture_root "$window" "$second"
changed_bytes=$(xmin_count_changed_bytes "$first" "$second" "$minimum")
if [ "$changed_bytes" -ge "$minimum" ]; then
return 0
fi
current_time=$(date +%s)
if [ $((current_time - start_time)) -ge "$timeout_seconds" ]; then
xmin_fail "Xmin captures changed by only $changed_bytes bytes; expected at least $minimum"
fi
sleep "$XMIN_POLL_SECONDS"
done
}
# Local Variables:
# tab-width: 8
# mode: sh
# sh-indentation: 4
# sh-basic-offset: 4
# indent-tabs-mode: t
# End:
# ex: shiftwidth=4 tabstop=8 cino=N-s
+3 -2
View File
@@ -53,7 +53,7 @@ set(
)
brlcad_addlib(librender "${LIBRENDER_SRCS}" "" "${ADRT_LOCAL_INCLUDE_DIRS}"
PRIVATE_LIBS libgcv ZLIB::ZLIB ${M_LIBRARY}
PRIVATE_LIBS libgcv librt ZLIB::ZLIB ${M_LIBRARY}
)
set_target_properties(librender PROPERTIES VERSION 20.0.1 SOVERSION 20)
add_dependencies(librender gcv_plugins)
@@ -109,8 +109,9 @@ cmakefiles(isst.c)
add_subdirectory(master)
add_subdirectory(slave)
add_subdirectory(tests)
set(adrt_ignore_files load.h tienet.h)
set(adrt_ignore_files load.h load_g.h tienet.h)
cmakefiles(${adrt_ignore_files})
cmakefiles(CMakeLists.txt)
+8
View File
@@ -90,11 +90,19 @@ enum
#define ADRT_NAME_SIZE 256
/* TIE geometry is normalized from millimeters to meters while loading. */
#define ADRT_MODEL_UNITS_PER_TIE_UNIT 1000.0
#define ADRT_LOAD_FORMAT_G 42
#define ADRT_LOAD_FORMAT_REG 43
#define ADRT_LOAD_FORMAT_KDTREE 44
struct rt_annot_scene;
RENDER_EXPORT extern int load_g(struct tie_s *tie, const char *db, int argc, const char **argv, struct adrt_mesh_s **);
RENDER_EXPORT extern int load_g_annotations(struct tie_s *tie, const char *db,
int argc, const char **argv, struct adrt_mesh_s **,
struct rt_annot_scene **annotations);
#endif
+23 -38
View File
@@ -59,6 +59,7 @@ struct isst_s {
struct render_camera_s camera;
struct camera_tile_s tile;
struct adrt_mesh_s *meshes;
struct rt_annot_scene *annotations;
tienet_buffer_t buffer_image;
int ogl, sflags, w, h, gs, ui;
double dt, fps, uic;
@@ -67,6 +68,7 @@ struct isst_s {
void *texdata;
vect_t camera_pos_init;
vect_t camera_focus_init;
fastf_t scene_radius;
int64_t t1;
int64_t t2;
int dirty;
@@ -119,13 +121,7 @@ resize_isst(struct isst_s *isstp)
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
{
/* realloc through a temporary so a failure does not leak (and null out)
* the existing buffer. */
void *tmp = realloc(isstp->texdata, isstp->camera.w * isstp->camera.h * 3);
if (tmp)
isstp->texdata = tmp;
}
isstp->texdata = realloc(isstp->texdata, isstp->camera.w * isstp->camera.h * 3);
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGB, isstp->camera.w, isstp->camera.h, 0, GL_RGB, GL_UNSIGNED_BYTE, isstp->texdata);
glDisable(GL_LIGHTING);
@@ -158,11 +154,14 @@ isst_load_g(ClientData UNUSED(clientData), Tcl_Interp *interp, int objc,
argv = (char **)malloc(sizeof(char *) * (strlen(Tcl_GetString(objv[3])) + 1)); /* allocate way too much. */
argc = (int)bu_argv_from_string(argv, strlen(Tcl_GetString(objv[3])), Tcl_GetString(objv[3]));
load_g(isst->tie, Tcl_GetString(objv[2]), argc, (const char **)argv, &(isst->meshes));
rt_annot_scene_destroy(isst->annotations);
isst->annotations = NULL;
load_g_annotations(isst->tie, Tcl_GetString(objv[2]), argc,
(const char **)argv, &(isst->meshes), &isst->annotations);
free(argv);
VSETALL(isst->camera.pos, isst->tie->radius);
VMOVE(isst->camera.focus, isst->tie->mid);
isst->scene_radius = render_camera_fit_scene(&isst->camera, isst->tie,
isst->annotations, ADRT_MODEL_UNITS_PER_TIE_UNIT);
VMOVE(isst->camera_pos_init, isst->camera.pos);
VMOVE(isst->camera_focus_init, isst->camera.focus);
@@ -239,7 +238,9 @@ paint_window(ClientData UNUSED(clientData), Tcl_Interp *interp, int objc, Tcl_Ob
isst->buffer_image.ind = 0;
render_camera_prep(&isst->camera);
render_camera_render(&isst->camera, isst->tie, &isst->tile, &isst->buffer_image);
render_camera_render_annotations(&isst->camera, isst->tie, &isst->tile,
&isst->buffer_image, isst->annotations,
ADRT_MODEL_UNITS_PER_TIE_UNIT);
isst->t1 = bu_gettime();
@@ -282,7 +283,6 @@ set_resolution(ClientData UNUSED(clientData), Tcl_Interp *interp, int objc, Tcl_
}
CLAMP(resolution, 1, 20);
/* gs is the render grid size; 0 means render at full window resolution */
if (resolution == 20)
isst->gs = 0;
else
@@ -326,13 +326,10 @@ isst_zap(ClientData UNUSED(clientData), Tcl_Interp *interp, int objc, Tcl_Obj *c
return TCL_ERROR;
}
/* Release the buffers owned directly by the workspace before the struct
* itself. (The tie and mesh list have a more involved lifecycle and are
* left for a dedicated teardown.) */
TIENET_BUFFER_FREE(isst->buffer_image);
if (isst->texdata)
free(isst->texdata);
rt_annot_scene_destroy(isst->annotations);
bu_free(isst, "isst free");
isst = NULL;
@@ -354,7 +351,7 @@ render_mode(ClientData UNUSED(clientData), Tcl_Interp *interp, int objc, Tcl_Obj
if (objc == 4)
buf = Tcl_GetString(objv[3]);
/* pack the 'rest' into buf - probably should use a vls for this */
/* pack the 'rest' into buf - probably should use a vls for this*/
if ( strlen(mode) == 3 && bu_strncmp("cut", mode, 3) == 0 ) {
struct adrt_mesh_s *mesh;
@@ -391,26 +388,22 @@ zero_view(ClientData UNUSED(clientData), Tcl_Interp *UNUSED(interp), int UNUSED(
static int
move_walk(ClientData UNUSED(clientData), Tcl_Interp *interp, int objc, Tcl_Obj *const *objv)
move_walk(ClientData UNUSED(clientData), Tcl_Interp *interp, int UNUSED(objc), Tcl_Obj *const *objv)
{
vect_t vec;
int flag;
if (objc < 3) {
Tcl_WrongNumArgs(interp, 1, objv, "pathName flag");
return TCL_ERROR;
}
if (Tcl_GetIntFromObj(interp, objv[2], &flag) != TCL_OK)
return TCL_ERROR;
if (flag >= 0) {
VSUB2(vec, isst->camera.focus, isst->camera.pos);
VSCALE(vec, vec, 0.1 * isst->tie->radius);
VSCALE(vec, vec, 0.1 * isst->scene_radius);
VADD2(isst->camera.pos, isst->camera.pos, vec);
VADD2(isst->camera.focus, isst->camera.focus, vec);
} else {
VSUB2(vec, isst->camera.pos, isst->camera.focus);
VSCALE(vec, vec, 0.1 * isst->tie->radius);
VSCALE(vec, vec, 0.1 * isst->scene_radius);
VADD2(isst->camera.pos, isst->camera.pos, vec);
VADD2(isst->camera.focus, isst->camera.focus, vec);
}
@@ -419,16 +412,12 @@ move_walk(ClientData UNUSED(clientData), Tcl_Interp *interp, int objc, Tcl_Obj *
}
static int
move_strafe(ClientData UNUSED(clientData), Tcl_Interp *interp, int objc, Tcl_Obj *const *objv)
move_strafe(ClientData UNUSED(clientData), Tcl_Interp *interp, int UNUSED(objc), Tcl_Obj *const *objv)
{
vect_t vec, dir, up;
int flag;
if (objc < 3) {
Tcl_WrongNumArgs(interp, 1, objv, "pathName flag");
return TCL_ERROR;
}
if (Tcl_GetIntFromObj(interp, objv[2], &flag) != TCL_OK)
return TCL_ERROR;
@@ -437,13 +426,13 @@ move_strafe(ClientData UNUSED(clientData), Tcl_Interp *interp, int objc, Tcl_Obj
if (flag >= 0) {
VSUB2(dir, isst->camera.focus, isst->camera.pos);
VCROSS(vec, dir, up);
VSCALE(vec, vec, 0.1 * isst->tie->radius);
VSCALE(vec, vec, 0.1 * isst->scene_radius);
VADD2(isst->camera.pos, isst->camera.pos, vec);
VADD2(isst->camera.focus, isst->camera.pos, dir);
} else {
VSUB2(dir, isst->camera.focus, isst->camera.pos);
VCROSS(vec, dir, up);
VSCALE(vec, vec, -0.1 * isst->tie->radius);
VSCALE(vec, vec, -0.1 * isst->scene_radius);
VADD2(isst->camera.pos, isst->camera.pos, vec);
VADD2(isst->camera.focus, isst->camera.pos, dir);
}
@@ -551,14 +540,10 @@ aerotate(ClientData UNUSED(clientData), Tcl_Interp *interp, int objc, Tcl_Obj *c
VSCALE(vecdfoc, vecdfoc, mag_focus);
VADD2(isst->camera.focus, isst->camera_focus_init, vecdfoc);
}
/* Update the tcl copies of the az/el vars. Store them in the same units
* as isst_load_g (radians via -DEG2RAD); previously aerotate wrote the raw
* degrees from bn_ae_vec, leaving the Tcl vars inconsistent after a drag. */
/* Update the tcl copies of the az/el vars */
VSUB2(vec, isst->camera.focus, isst->camera.pos);
VUNITIZE(vec);
bn_ae_vec(&az, &el, vec);
az = az * -DEG2RAD;
el = el * -DEG2RAD;
bu_vls_sprintf(&tclstr, "%f", az);
Tcl_SetVar(interp, "az", bu_vls_addr(&tclstr), 0);
bu_vls_sprintf(&tclstr, "%f", el);
@@ -576,7 +561,7 @@ open_dm(ClientData UNUSED(cdata), Tcl_Interp *interp, int UNUSED(objc), Tcl_Obj
dmp = dm_open(NULL, (void *)interp, dm_default_type(), sizeof(av)/sizeof(void*)-1, (const char **)av);
if (dmp == DM_NULL) {
printf("Failed to open display manager\n");
printf("dm failed?\n");
return TCL_ERROR;
}
@@ -654,7 +639,7 @@ const char *fullname;
argv = __argv;
#endif
/* initialize progname for run-time resource finding */
/* initialize progname for run-tim resource finding */
bu_setprogname(argv[0]);
#ifdef HAVE_WINDOWS_H
+225 -21
View File
@@ -29,6 +29,7 @@
#include "bu/dylib.h"
#include "bu/log.h"
#include "bu/str.h"
#include "rt/primitives/annot.h"
#include "./camera.h"
@@ -43,13 +44,27 @@ struct render_shader_s {
static struct render_shader_s *shaders = NULL;
/* number of leading characters compared when matching a shader by name */
#define RENDER_SHADER_NAME_CMP_LEN 8
struct render_camera_annotation_data {
const struct rt_annot_scene *scene;
fastf_t model_units_per_tie_unit;
struct rt_annot_view view;
};
struct render_camera_parallel_data {
render_camera_thread_data_t thread;
struct render_camera_annotation_data annotation;
};
static const fastf_t render_camera_default_scene_radius = 1.0;
void render_camera_render_thread(int cpu, void *ptr); /* for bu_parallel */
static void render_camera_prep_ortho(render_camera_t *camera);
static void render_camera_prep_persp(render_camera_t *camera);
static void render_camera_prep_persp_dof(render_camera_t *camera);
static void render_camera_annotation_view(const render_camera_t *camera,
fastf_t model_units_per_tie_unit, struct rt_annot_view *view);
static struct render_shader_s *render_shader_register (const char *name, int (*init)(render_t *, const char *));
@@ -393,20 +408,147 @@ render_camera_prep(render_camera_t *camera)
}
void
render_camera_render_thread(int UNUSED(cpu), void *ptr)
fastf_t
render_camera_fit_scene(render_camera_t *camera, const struct tie_s *tie,
const struct rt_annot_scene *annotations,
fastf_t model_units_per_tie_unit)
{
point_t annotation_min, annotation_max;
point_t scene_min, scene_max;
vect_t offset;
fastf_t scene_radius;
if (!camera || !tie)
return 0.0;
if (!annotations || !rt_annot_scene_bounds(annotations,
annotation_min, annotation_max)) {
VSETALL(camera->pos, tie->radius);
VMOVE(camera->focus, tie->mid);
return tie->radius;
}
if (!isfinite(model_units_per_tie_unit) ||
model_units_per_tie_unit <= 0.0)
model_units_per_tie_unit = 1.0;
VSCALE(annotation_min, annotation_min,
1.0 / model_units_per_tie_unit);
VSCALE(annotation_max, annotation_max,
1.0 / model_units_per_tie_unit);
VMOVE(scene_min, annotation_min);
VMOVE(scene_max, annotation_max);
if (tie->tri_num) {
VMINMAX(scene_min, scene_max, tie->amin);
VMINMAX(scene_min, scene_max, tie->amax);
}
VADD2SCALE(camera->focus, scene_min, scene_max, 0.5);
VSUB2(offset, scene_max, scene_min);
scene_radius = 0.5 * MAGNITUDE(offset);
if (!isfinite(scene_radius) || scene_radius <= SMALL_FASTF)
scene_radius = render_camera_default_scene_radius;
/* Keep the established isometric initial direction while positioning it
* relative to the combined scene center. */
VSETALL(offset, scene_radius);
VADD2(camera->pos, camera->focus, offset);
return scene_radius;
}
static void
render_camera_annotation_view(const render_camera_t *camera,
fastf_t model_units_per_tie_unit, struct rt_annot_view *view)
{
vect_t look, up, side, temp, right;
point_t model_position;
fastf_t angle, sine, cosine, xy_scale = 1.0;
mat_t *matrix = &view->model2view;
VSUB2(look, camera->focus, camera->pos);
VUNITIZE(look);
VSET(up, 0.0, 0.0, 1.0);
VMOVE(temp, look);
angle = VDOT(up, temp);
VSCALE(temp, temp, angle);
VSUB2(up, up, temp);
VUNITIZE(up);
VCROSS(side, up, look);
sine = sin(-camera->tilt * DEG2RAD);
cosine = cos(-camera->tilt * DEG2RAD);
VSCALE(up, up, cosine);
VSCALE(side, side, sine);
VADD2(up, up, side);
VCROSS(side, up, look);
VREVERSE(right, side);
MAT_IDN(*matrix);
if (camera->type == RENDER_CAMERA_ORTHOGRAPHIC &&
camera->aspect > 0.0 && camera->gridsize > 0.0) {
xy_scale = 2.0 / (camera->aspect * camera->gridsize *
model_units_per_tie_unit);
} else if (camera->aspect > 0.0) {
xy_scale = 1.0 / camera->aspect;
}
for (size_t axis = 0; axis < 3; ++axis) {
(*matrix)[axis] = right[axis] * xy_scale;
(*matrix)[4 + axis] = up[axis] * xy_scale;
(*matrix)[8 + axis] = -look[axis];
}
VSCALE(model_position, camera->pos, model_units_per_tie_unit);
(*matrix)[3] = -VDOT(&(*matrix)[0], model_position);
(*matrix)[7] = -VDOT(&(*matrix)[4], model_position);
(*matrix)[11] = -VDOT(&(*matrix)[8], model_position);
view->width = camera->w;
view->height = camera->h;
view->perspective =
camera->type == RENDER_CAMERA_PERSPECTIVE ? 2.0 * camera->fov : 0.0;
}
static void
render_camera_composite_annotation(render_camera_thread_data_t *td,
const struct render_camera_annotation_data *annotation,
const struct tie_ray_s *tie_ray, fastf_t sample_x, fastf_t sample_y,
vect_t *pixel)
{
struct rt_annot_hit back_layer = {0};
struct xray ray;
fastf_t scene_distance = INFINITY;
if (!annotation || !annotation->scene)
return;
VSCALE(ray.r_pt, tie_ray->pos, annotation->model_units_per_tie_unit);
VMOVE(ray.r_dir, tie_ray->dir);
if (!rt_annot_scene_query_layers(annotation->scene, &annotation->view,
&ray, sample_x, sample_y, INFINITY, &back_layer, 1))
return;
if (!back_layer.screen_space) {
struct tie_id_s id;
struct tie_ray_s depth_ray = *tie_ray;
if (TIE_WORK(td->tie, &depth_ray, &id, render_hit, NULL))
scene_distance = id.dist * annotation->model_units_per_tie_unit;
}
(void)rt_annot_scene_composite(annotation->scene, &annotation->view,
&ray, sample_x, sample_y, scene_distance, *pixel, NULL);
}
static void
render_camera_render_thread_impl(int UNUSED(cpu), render_camera_thread_data_t *td,
const struct render_camera_annotation_data *annotation)
{
render_camera_thread_data_t *td;
int d, n, res_ind, scanline, v_scanline;
vect_t pixel, accum;
vect_t v1 = VINIT_ZERO;
vect_t v2 = VINIT_ZERO;
struct tie_ray_s ray;
struct tie_ray_s annotation_ray;
fastf_t view_inv;
VSETALL(v1, 0);
td = (render_camera_thread_data_t *)ptr;
view_inv = 1.0 / td->camera->view_num;
td->camera->render.tie = td->tie;
@@ -457,9 +599,15 @@ render_camera_render_thread(int UNUSED(cpu), void *ptr)
VMOVE(ray.pos, td->camera->view_list[d].pos);
ray.depth = 0;
VUNITIZE(ray.dir);
annotation_ray = ray;
/* Compute pixel value using this ray */
td->camera->render.work(&td->camera->render, td->tie, &ray, &pixel);
render_camera_composite_annotation(td, annotation,
&annotation_ray,
(fastf_t)n + 0.5,
(fastf_t)td->camera->h - (fastf_t)v_scanline - 0.5,
&pixel);
VADD2(accum, accum, pixel);
}
@@ -476,6 +624,7 @@ render_camera_render_thread(int UNUSED(cpu), void *ptr)
VMOVE(ray.pos, td->camera->view_list[0].pos);
ray.depth = 0;
VUNITIZE(ray.dir);
annotation_ray = ray;
/* Compute pixel value using this ray */
td->camera->render.work(&td->camera->render, td->tie, &ray, &pixel);
@@ -490,12 +639,19 @@ render_camera_render_thread(int UNUSED(cpu), void *ptr)
VSET(pixel, (TFLOAT)RENDER_CAMERA_BGR, (TFLOAT)RENDER_CAMERA_BGG, (TFLOAT)RENDER_CAMERA_BGB);
ray.depth = 0;
annotation_ray = ray;
/* Compute pixel value using this ray */
td->camera->render.work(&td->camera->render, td->tie, &ray, &pixel);
}
}
if (td->camera->view_num == 1)
render_camera_composite_annotation(td, annotation,
&annotation_ray, (fastf_t)n + 0.5,
(fastf_t)td->camera->h - (fastf_t)v_scanline - 0.5,
&pixel);
if (td->tile->format == RENDER_CAMERA_BIT_DEPTH_24) {
V_MIN(pixel[0], 1);
@@ -523,9 +679,30 @@ render_camera_render_thread(int UNUSED(cpu), void *ptr)
void
render_camera_render(render_camera_t *camera, struct tie_s *tie, camera_tile_t *tile, tienet_buffer_t *result)
render_camera_render_thread(int cpu, void *ptr)
{
render_camera_thread_data_t td;
render_camera_render_thread_impl(cpu, (render_camera_thread_data_t *)ptr,
NULL);
}
static void
render_camera_render_annotations_thread(int cpu, void *ptr)
{
struct render_camera_parallel_data *data =
(struct render_camera_parallel_data *)ptr;
render_camera_render_thread_impl(cpu, &data->thread, &data->annotation);
}
static void
render_camera_render_internal(render_camera_t *camera, struct tie_s *tie,
camera_tile_t *tile, tienet_buffer_t *result,
const struct rt_annot_scene *annotations,
fastf_t model_units_per_tie_unit)
{
struct render_camera_parallel_data data = {0};
unsigned int scanline;
uint32_t ind;
@@ -543,19 +720,47 @@ render_camera_render(render_camera_t *camera, struct tie_s *tie, camera_tile_t *
TCOPY(camera_tile_t, tile, 0, result->data, result->ind);
result->ind += sizeof(camera_tile_t);
td.tie = tie;
td.camera = camera;
td.tile = tile;
td.res_buf = &((char *)result->data)[result->ind];
data.thread.tie = tie;
data.thread.camera = camera;
data.thread.tile = tile;
data.thread.res_buf = &((char *)result->data)[result->ind];
scanline = 0;
td.scanline = &scanline;
td.sem_tie_worker = bu_semaphore_register("sem_tie_worker");
data.thread.scanline = &scanline;
data.thread.sem_tie_worker = bu_semaphore_register("sem_tie_worker");
data.annotation.scene = annotations;
data.annotation.model_units_per_tie_unit =
model_units_per_tie_unit > 0.0 ? model_units_per_tie_unit : 1.0;
if (annotations)
render_camera_annotation_view(camera,
data.annotation.model_units_per_tie_unit, &data.annotation.view);
bu_parallel(render_camera_render_thread, camera->thread_num, &td);
if (annotations)
bu_parallel(render_camera_render_annotations_thread,
camera->thread_num, &data);
else
bu_parallel(render_camera_render_thread, camera->thread_num,
&data.thread);
result->ind = ind;
}
return;
void
render_camera_render(render_camera_t *camera, struct tie_s *tie,
camera_tile_t *tile, tienet_buffer_t *result)
{
render_camera_render_internal(camera, tie, tile, result, NULL, 1.0);
}
void
render_camera_render_annotations(render_camera_t *camera, struct tie_s *tie,
camera_tile_t *tile, tienet_buffer_t *result,
const struct rt_annot_scene *annotations,
fastf_t model_units_per_tie_unit)
{
render_camera_render_internal(camera, tie, tile, result, annotations,
model_units_per_tie_unit);
}
@@ -620,9 +825,9 @@ render_shader_unload_plugin(render_t *r, const char *name)
{
#ifdef HAVE_DLFCN_H
struct render_shader_s *t, *s = shaders, *meh;
if (!bu_strncmp(s->name, name, RENDER_SHADER_NAME_CMP_LEN)) {
if (!bu_strncmp(s->name, name, 8)) {
t = s->next;
if (r && r->shader && !bu_strncmp(r->shader, name, RENDER_SHADER_NAME_CMP_LEN)) {
if (r && r->shader && !bu_strncmp(r->shader, name, 8)) {
meh = s->next;
while (meh) {
if (render_shader_init(r, meh->name, NULL) != -1)
@@ -641,7 +846,7 @@ LOADED:
}
while (s->next) {
if (!bu_strncmp(s->next->name, name, RENDER_SHADER_NAME_CMP_LEN)) {
if (!bu_strncmp(s->next->name, name, 8)) {
if (r)
render_shader_init(r, s->name, NULL);
if (s->next->dlh)
@@ -651,7 +856,6 @@ LOADED:
bu_free(t, "unload shader");
return 0;
}
s = s->next;
}
bu_log("Could not find shader \"%s\"\n", name);
@@ -667,7 +871,7 @@ render_shader_init(render_t *r, const char *name, const char *buf)
{
struct render_shader_s *s = shaders;
while (s) {
if (!bu_strncmp(s->name, name, RENDER_SHADER_NAME_CMP_LEN)) {
if (!bu_strncmp(s->name, name, 8)) {
s->init(r, buf);
r->shader = s->name;
return 0;
+11
View File
@@ -99,7 +99,18 @@ __BEGIN_DECLS
RENDER_EXPORT extern void render_camera_init(render_camera_t *camera, size_t threads);
RENDER_EXPORT extern void render_camera_free(render_camera_t *camera);
RENDER_EXPORT extern void render_camera_prep(render_camera_t *camera);
/** Center a camera on TIE geometry and any prepared annotations. Returns the
* effective scene radius in TIE coordinates. */
RENDER_EXPORT extern fastf_t render_camera_fit_scene(render_camera_t *camera,
const struct tie_s *tie, const struct rt_annot_scene *annotations,
fastf_t model_units_per_tie_unit);
RENDER_EXPORT extern void render_camera_render(render_camera_t *camera, struct tie_s *tie, camera_tile_t *tile, tienet_buffer_t *result);
/** Render with an externally owned annotation scene. The coordinate scale
* converts TIE coordinates to database model coordinates. */
RENDER_EXPORT extern void render_camera_render_annotations(render_camera_t *camera,
struct tie_s *tie, camera_tile_t *tile, tienet_buffer_t *result,
const struct rt_annot_scene *annotations,
fastf_t model_units_per_tie_unit);
RENDER_EXPORT extern int render_shader_init(render_t *, const char *name, const char *buf);
RENDER_EXPORT extern const char *render_shader_load_plugin(const char *filename);
+86 -10
View File
@@ -40,11 +40,13 @@
#include "vmath.h"
#include "nmg.h"
#include "rt/geom.h"
#include "rt/search.h"
#include "raytrace.h"
#include "rt/tie.h"
#include "adrt.h"
#include "adrt_struct.h"
#include "load_g.h"
static struct bn_tol tol; /* calculation tolerance */
@@ -104,6 +106,42 @@ struct gcv_data {
static struct gcv_data gcvwriter = {{load_nmg_to_adrt_gcvwrite, NULL, NULL}, NULL};
int
adrt_path_has_geometry(struct db_i *database, const char *path)
{
static const char *geometry_search = "! -type comb ! -type annot";
struct directory *root;
if (!database || !path)
return 0;
root = db_lookup(database, path, LOOKUP_QUIET);
/* Let db_walk_tree report a missing path rather than hiding the error. */
if (!root)
return 1;
return db_search(NULL, DB_SEARCH_QUIET, geometry_search, 1, &root,
database, NULL, NULL, NULL) != 0;
}
union tree *
adrt_leaf_tess(struct db_tree_state *tsp, const struct db_full_path *pathp,
struct rt_db_internal *ip, void *client_data)
{
union tree *result;
if (!ip || ip->idb_type != ID_ANNOT)
return rt_booltree_leaf_tess(tsp, pathp, ip, client_data);
/* Annotations are rendered directly and have no NMG representation. */
BU_GET(result, union tree);
RT_TREE_INIT(result);
result->tr_op = OP_NOP;
return result;
}
/* load the region into the tie image */
static void
load_nmg_to_adrt_internal(struct adrt_mesh_s *mesh, struct nmgregion *r)
@@ -153,7 +191,8 @@ load_nmg_to_adrt_internal(struct adrt_mesh_s *mesh, struct nmgregion *r)
NMG_CK_VERTEX(v);
/* convert mm to m */
VSCALE((*tribuf[vert_count]).v, v->vg_p->coord, 1.0/1000.0);
VSCALE((*tribuf[vert_count]).v, v->vg_p->coord,
1.0 / ADRT_MODEL_UNITS_PER_TIE_UNIT);
vert_count++;
}
if (vert_count > 3)
@@ -248,9 +287,12 @@ load_nmg_to_adrt_regstart(struct db_tree_state *ts, const struct db_full_path *p
for (i=0;i<bot->num_faces;i++)
{
VSCALE((*tribuf[0]).v, (bot->vertices+3*bot->faces[3*i+0]), 1.0/1000.0);
VSCALE((*tribuf[1]).v, (bot->vertices+3*bot->faces[3*i+1]), 1.0/1000.0);
VSCALE((*tribuf[2]).v, (bot->vertices+3*bot->faces[3*i+2]), 1.0/1000.0);
VSCALE((*tribuf[0]).v, (bot->vertices+3*bot->faces[3*i+0]),
1.0 / ADRT_MODEL_UNITS_PER_TIE_UNIT);
VSCALE((*tribuf[1]).v, (bot->vertices+3*bot->faces[3*i+1]),
1.0 / ADRT_MODEL_UNITS_PER_TIE_UNIT);
VSCALE((*tribuf[2]).v, (bot->vertices+3*bot->faces[3*i+2]),
1.0 / ADRT_MODEL_UNITS_PER_TIE_UNIT);
TIE_VAL(tie_push)(cur_tie, tribuf, 1, mesh, 0);
}
@@ -298,11 +340,16 @@ load_nmg_to_adrt_gcvwrite(struct nmgregion *r, const struct db_full_path *pathp,
int
load_g(struct tie_s *tie, const char *db, int argc, const char **argv, struct adrt_mesh_s **meshes)
load_g_annotations(struct tie_s *tie, const char *db, int argc,
const char **argv, struct adrt_mesh_s **meshes,
struct rt_annot_scene **annotations)
{
struct model *the_model;
struct bg_tess_tol ttol; /* tessellation tolerance in mm */
struct db_tree_state tree_state; /* includes tol & model */
const char **geometry_paths = NULL;
int geometry_count = 0;
int walk_result = 0;
cur_tie = tie; /* NOTE: cur_tie is a file-scope global; this is not
* reentrant and would need locking to be thread-safe. */
@@ -338,13 +385,21 @@ load_g(struct tie_s *tie, const char *db, int argc, const char **argv, struct ad
if ((dbip = db_open(db, DB_OPEN_READONLY)) == DBI_NULL) {
perror(db);
bu_log("Unable to open geometry database file (%s)\n", db);
nmg_km(the_model);
return -1;
}
if (db_dirbuild(dbip)) {
bu_log("ERROR: db_dirbuild failed\n");
db_close(dbip);
dbip = DBI_NULL;
nmg_km(the_model);
return -1;
}
if (annotations) {
*annotations = rt_annot_scene_create(dbip, argc, argv, &ttol, &tol);
}
BN_CK_TOL(tree_state.ts_tol);
BG_CK_TESS_TOL(tree_state.ts_ttol);
@@ -362,15 +417,26 @@ load_g(struct tie_s *tie, const char *db, int argc, const char **argv, struct ad
tribuf[1] = (TIE_3 *)bu_malloc(sizeof(TIE_3) * 3, "triangle tribuffer");
tribuf[2] = (TIE_3 *)bu_malloc(sizeof(TIE_3) * 3, "triangle tribuffer");
(void) db_walk_tree(dbip,
argc, /* number of toplevel regions */
argv, /* region names */
if (argc > 0) {
geometry_paths = (const char **)bu_calloc((size_t)argc,
sizeof(const char *), "ADRT geometry paths");
for (int i = 0; i < argc; ++i) {
if (adrt_path_has_geometry(dbip, argv[i]))
geometry_paths[geometry_count++] = argv[i];
}
}
if (geometry_count) {
walk_result = db_walk_tree(dbip,
geometry_count, /* number of toplevel regions */
geometry_paths, /* region names */
1, /* ncpu */
&tree_state, /* initial tree state */
load_nmg_to_adrt_regstart, /* region start function */
gcv_region_end, /* region end function */
rt_booltree_leaf_tess, /* leaf func */
adrt_leaf_tess, /* leaf func */
(void *)&gcvwriter); /* client data */
}
/* Release dynamic storage */
nmg_km(the_model);
@@ -380,10 +446,20 @@ load_g(struct tie_s *tie, const char *db, int argc, const char **argv, struct ad
bu_free(tribuf[1], "vert");
bu_free(tribuf[2], "vert");
bu_free(tribuf, "tri");
if (geometry_paths)
bu_free(geometry_paths, "ADRT geometry paths");
TIE_VAL(tie_prep)(cur_tie);
return 0;
return walk_result;
}
int
load_g(struct tie_s *tie, const char *db, int argc, const char **argv,
struct adrt_mesh_s **meshes)
{
return load_g_annotations(tie, db, argc, argv, meshes, NULL);
}
/*
+43
View File
@@ -0,0 +1,43 @@
/* L O A D _ G . H
* BRL-CAD / ADRT
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef ADRT_LOAD_G_H
#define ADRT_LOAD_G_H
#include "common.h"
#include "adrt.h"
struct db_i;
struct db_tree_state;
struct db_full_path;
struct rt_db_internal;
union tree;
__BEGIN_DECLS
RENDER_EXPORT extern int adrt_path_has_geometry(struct db_i *dbip,
const char *path);
RENDER_EXPORT extern union tree *adrt_leaf_tess(struct db_tree_state *tsp,
const struct db_full_path *pathp, struct rt_db_internal *ip,
void *client_data);
__END_DECLS
#endif /* ADRT_LOAD_G_H */
/*
* Local Variables:
* tab-width: 8
* mode: C
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+16
View File
@@ -0,0 +1,16 @@
brlcad_addexec(adrt_annotation_test annotation.c
"librender;libwdb;librt;libbu;${M_LIBRARY}" TEST)
target_include_directories(adrt_annotation_test PRIVATE
${BRLCAD_SOURCE_DIR}/src/adrt
${BRLCAD_SOURCE_DIR}/src/adrt/librender
)
brlcad_add_test(NAME adrt_annotation_test COMMAND adrt_annotation_test)
cmakefiles(annotation.c CMakeLists.txt)
# Local Variables:
# tab-width: 8
# mode: cmake
# indent-tabs-mode: t
# End:
# ex: shiftwidth=2 tabstop=8
+179
View File
@@ -0,0 +1,179 @@
/* A N N O T A T I O N . C
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "common.h"
#include <math.h>
#include <stdio.h>
#include "bu/app.h"
#include "bu/file.h"
#include "raytrace.h"
#include "rt/primitives/annot.h"
#include "wdb.h"
#include "load_g.h"
#include "librender/camera.h"
static void
write_test_database(struct db_i *dbip)
{
struct rt_wdb *wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_DISK);
struct rt_annot_internal annot = {0};
point2d_t vertices[2];
struct line_seg line = {0};
void *segments[1];
int reverse[1] = {0};
point_t center = VINIT_ZERO;
struct wmember members;
if (wdbp == RT_WDB_NULL)
bu_exit(1, "unable to open ADRT annotation test database\n");
annot.magic = RT_ANNOT_INTERNAL_MAGIC;
VSET(annot.V, 10.0, 0.0, 0.0);
annot.flags = RT_ANNOT_MODEL_SPACE;
VSET(annot.u_vec, 0.0, 1.0, 0.0);
VSET(annot.v_vec, 0.0, 0.0, 1.0);
annot.vert_count = 2;
annot.verts = vertices;
V2SET(vertices[0], -1.0, 0.0);
V2SET(vertices[1], 1.0, 0.0);
annot.ant.count = 1;
annot.ant.reverse = reverse;
annot.ant.segments = segments;
line.magic = CURVE_LSEG_MAGIC;
line.start = 0;
line.end = 1;
segments[0] = &line;
if (rt_annot_validate(&annot, NULL) ||
mk_annot(wdbp, "test.annot", &annot) < 0 ||
mk_sph(wdbp, "test.sph", center, 1.0) < 0)
bu_exit(1, "unable to write ADRT annotation test objects\n");
BU_LIST_INIT(&members.l);
if (!mk_addmember("test.annot", &members.l, NULL, WMOP_UNION) ||
mk_lcomb(wdbp, "annotation-only.c", &members, 0, NULL, NULL,
NULL, 0))
bu_exit(1, "unable to write annotation-only test combination\n");
BU_LIST_INIT(&members.l);
if (!mk_addmember("test.annot", &members.l, NULL, WMOP_UNION) ||
!mk_addmember("test.sph", &members.l, NULL, WMOP_UNION) ||
mk_lcomb(wdbp, "mixed.c", &members, 0, NULL, NULL, NULL, 0))
bu_exit(1, "unable to write mixed test combination\n");
}
static void
check_geometry_filter(struct db_i *dbip)
{
struct rt_db_internal intern;
union tree *leaf;
if (adrt_path_has_geometry(dbip, "test.annot") ||
adrt_path_has_geometry(dbip, "annotation-only.c") ||
!adrt_path_has_geometry(dbip, "test.sph") ||
!adrt_path_has_geometry(dbip, "mixed.c"))
bu_exit(1, "ADRT annotation geometry filtering failed\n");
RT_DB_INTERNAL_INIT(&intern);
intern.idb_type = ID_ANNOT;
leaf = adrt_leaf_tess(NULL, NULL, &intern, NULL);
if (!leaf || leaf->tr_op != OP_NOP)
bu_exit(1, "ADRT annotation leaf did not become a no-op\n");
db_free_tree(leaf);
}
static void
check_camera_fit(struct db_i *dbip)
{
const char *paths[] = {"test.annot"};
struct bg_tess_tol ttol = BG_TESS_TOL_INIT_TOL;
struct bn_tol tol = BN_TOL_INIT_TOL;
struct rt_annot_scene *scene = rt_annot_scene_create(dbip, 1, paths,
&ttol, &tol);
render_camera_t camera = {0};
struct tie_s tie = {0};
fastf_t radius;
const fastf_t combined_radius = 0.5 * sqrt(41.0);
if (!scene || rt_annot_scene_count(scene) != 1)
bu_exit(1, "unable to prepare ADRT annotation test scene\n");
tie.radius = 2.0;
VSET(tie.mid, 1.0, 2.0, 3.0);
radius = render_camera_fit_scene(&camera, &tie, NULL, 1.0);
if (!NEAR_EQUAL(radius, 2.0, BN_TOL_DIST) ||
!VNEAR_EQUAL(camera.focus, tie.mid, BN_TOL_DIST) ||
!NEAR_EQUAL(camera.pos[X], 2.0, BN_TOL_DIST) ||
!NEAR_EQUAL(camera.pos[Y], 2.0, BN_TOL_DIST) ||
!NEAR_EQUAL(camera.pos[Z], 2.0, BN_TOL_DIST))
bu_exit(1, "ADRT geometry-only camera framing changed\n");
radius = render_camera_fit_scene(&camera, &tie, scene, 10.0);
if (radius <= SMALL_FASTF ||
!NEAR_EQUAL(camera.focus[X], 1.0, BN_TOL_DIST) ||
!NEAR_ZERO(camera.focus[Y], BN_TOL_DIST) ||
!NEAR_ZERO(camera.focus[Z], BN_TOL_DIST) ||
!NEAR_EQUAL(camera.pos[X] - camera.focus[X], radius, BN_TOL_DIST) ||
!NEAR_EQUAL(camera.pos[Y] - camera.focus[Y], radius, BN_TOL_DIST) ||
!NEAR_EQUAL(camera.pos[Z] - camera.focus[Z], radius, BN_TOL_DIST))
bu_exit(1, "ADRT annotation-only camera framing failed: radius=%g "
"focus=(%g %g %g) pos=(%g %g %g)\n", radius,
V3ARGS(camera.focus), V3ARGS(camera.pos));
tie.tri_num = 1;
VSET(tie.amin, -2.0, -2.0, -2.0);
VSET(tie.amax, 0.0, 2.0, 2.0);
radius = render_camera_fit_scene(&camera, &tie, scene, 10.0);
if (!NEAR_EQUAL(radius, combined_radius, BN_TOL_DIST) ||
!NEAR_EQUAL(camera.focus[X], -0.5, BN_TOL_DIST) ||
!NEAR_ZERO(camera.focus[Y], BN_TOL_DIST) ||
!NEAR_ZERO(camera.focus[Z], BN_TOL_DIST))
bu_exit(1, "ADRT combined geometry and annotation framing failed\n");
rt_annot_scene_destroy(scene);
}
int
main(int argc, char **argv)
{
char path[MAXPATHLEN];
struct db_i *dbip;
bu_setprogname(argv[0]);
if (argc != 1)
return 1;
{
FILE *fp = bu_temp_file(path, MAXPATHLEN);
if (!fp)
bu_exit(1, "unable to create ADRT annotation test path\n");
fclose(fp);
}
dbip = db_create(path, 5);
if (dbip == DBI_NULL)
bu_exit(1, "unable to create ADRT annotation test database\n");
write_test_database(dbip);
check_geometry_filter(dbip);
check_camera_fit(dbip);
db_close(dbip);
bu_file_delete(path);
return 0;
}
/*
* Local Variables:
* tab-width: 8
* mode: C
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+399 -480
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -64,16 +64,17 @@ ArtTileCallback::on_tile_end(const renderer::Frame* frame, const size_t tile_x,
{
foundation::Tile& t = frame->image().tile(tile_x, tile_y);
const foundation::Tile rgb(t, PixelFormatUInt8);
const foundation::CanvasProperties& properties = frame->image().properties();
// fb_write(fbp, tile_x, tile_y, rgb.get_storage(), rgb.get_size());
// printf("yay!\n");
// printf("%lu %lu \n", tile_x, tile_y);
// printf("%lu %lu \n", rgb.get_width(), rgb.get_height());
for (size_t y = 0; y < rgb.get_height(); y++) {
for (size_t x = 0; x < rgb.get_width(); x++) {
size_t x_coord = tile_x * rgb.get_width() + x;
size_t y_coord = tile_y * rgb.get_height() + y;
size_t img_h = frame->image().properties().m_canvas_height;
fb_write(fbp, (int)x_coord, (int)(img_h - y_coord), rgb.get_storage()+((y * rgb.get_width() * 4) + (x * 4)), 1);
size_t x_coord = tile_x * properties.m_tile_width + x;
size_t y_coord = tile_y * properties.m_tile_height + y;
size_t img_h = properties.m_canvas_height;
fb_write(fbp, (int)x_coord, (int)(img_h - y_coord - 1), rgb.get_storage()+((y * rgb.get_width() * 4) + (x * 4)), 1);
}
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ if(TARGET libtclcad)
set(_itcl4_gui_test "${CMAKE_SOURCE_DIR}/src/libtclcad/tests/itcl4_gui.tcl")
foreach(_shell btclsh bwish)
brlcad_add_xmin_test(
brlcad_add_gui_test(
NAME tclcad_test_itcl4_gui_${_shell}
SCREEN 1280x960x24
TIMEOUT 120
-1
View File
@@ -42,7 +42,6 @@ if(BRLCAD_ENABLE_QT AND OPENGL_TARGETS)
Qt6::Widgets
Qt6::OpenGL
Qt6::OpenGLWidgets
${OPENGL_TARGETS}
)
else(Qt6Widgets_FOUND)
qt5_wrap_cpp(moc_srcs ${isst_headers})
+52 -15
View File
@@ -26,6 +26,7 @@
#include <QFile>
#include <QPlainTextEdit>
#include <QTextStream>
#include <vector>
#include "gfile.h"
#include "bu/malloc.h"
@@ -33,6 +34,7 @@
#include "gcv.h"
#include "nmg.h"
#include "rt/geom.h"
#include "load_g.h"
/* Replace the load_g globals from adrt with a struct that is
* passed through the callers. */
@@ -102,7 +104,8 @@ nmg_to_adrt_internal(TIE_3 **tribuf, struct tie_s *cur_tie, struct adrt_mesh_s *
NMG_CK_VERTEX(v);
/* convert mm to m */
VSCALE((*tribuf[vert_count]).v, v->vg_p->coord, 1.0/1000.0);
VSCALE((*tribuf[vert_count]).v, v->vg_p->coord,
1.0 / ADRT_MODEL_UNITS_PER_TIE_UNIT);
vert_count++;
}
if (vert_count > 3)
@@ -185,9 +188,12 @@ nmg_to_adrt_regstart(struct db_tree_state *ts, const struct db_full_path *path,
for (i=0;i<bot->num_faces;i++)
{
VSCALE((*d->tribuf[0]).v, (bot->vertices+3*bot->faces[3*i+0]), 1.0/1000.0);
VSCALE((*d->tribuf[1]).v, (bot->vertices+3*bot->faces[3*i+1]), 1.0/1000.0);
VSCALE((*d->tribuf[2]).v, (bot->vertices+3*bot->faces[3*i+2]), 1.0/1000.0);
VSCALE((*d->tribuf[0]).v, (bot->vertices+3*bot->faces[3*i+0]),
1.0 / ADRT_MODEL_UNITS_PER_TIE_UNIT);
VSCALE((*d->tribuf[1]).v, (bot->vertices+3*bot->faces[3*i+1]),
1.0 / ADRT_MODEL_UNITS_PER_TIE_UNIT);
VSCALE((*d->tribuf[2]).v, (bot->vertices+3*bot->faces[3*i+2]),
1.0 / ADRT_MODEL_UNITS_PER_TIE_UNIT);
TIE_VAL(tie_push)(d->cur_tie, d->tribuf, 1, mesh, 0);
}
@@ -233,6 +239,13 @@ nmg_to_adrt_gcvwrite(struct nmgregion *r, const struct db_full_path *pathp, stru
nmg_to_adrt_internal(d->tribuf, d->cur_tie, mesh, r);
}
GFile::~GFile()
{
closedb();
}
int
GFile::load_g(const char *filename, int argc, const char *argv[])
{
@@ -241,6 +254,9 @@ GFile::load_g(const char *filename, int argc, const char *argv[])
struct bg_tess_tol ttol; /* tessellation tolerance in mm */
struct db_tree_state tree_state; /* includes tol & model */
struct isst_nmg_data d;
std::vector<const char *> geometry_paths;
closedb();
RT_DBTS_INIT(&tree_state);
tree_state.ts_tol = &tol;
@@ -278,6 +294,8 @@ GFile::load_g(const char *filename, int argc, const char *argv[])
}
if (db_dirbuild(dbip)) {
bu_log("ERROR: db_dirbuild failed\n");
db_close(dbip);
dbip = NULL;
return -1;
}
d.dbip = dbip;
@@ -304,20 +322,30 @@ GFile::load_g(const char *filename, int argc, const char *argv[])
tribuf[2] = (TIE_3 *)bu_malloc(sizeof(TIE_3) * 3, "triangle tribuffer");
d.tribuf = this->tribuf;
(void) db_walk_tree(dbip,
argc, /* number of toplevel regions */
argv, /* region names */
1, /* ncpu */
&tree_state, /* initial tree state */
nmg_to_adrt_regstart, /* region start function */
gcv_region_end, /* region end function */
rt_booltree_leaf_tess, /* leaf func */
(void *)&gcvwriter); /* client data */
annotations = rt_annot_scene_create(dbip, argc, argv, &ttol, &tol);
for (int i = 0; i < argc; ++i) {
if (adrt_path_has_geometry(dbip, argv[i]))
geometry_paths.push_back(argv[i]);
}
if (!geometry_paths.empty()) {
(void) db_walk_tree(dbip,
(int)geometry_paths.size(), /* number of toplevel regions */
geometry_paths.data(), /* region names */
1, /* ncpu */
&tree_state, /* initial tree state */
nmg_to_adrt_regstart, /* region start function */
gcv_region_end, /* region end function */
adrt_leaf_tess, /* leaf func */
(void *)&gcvwriter); /* client data */
}
/* Release dynamic storage */
nmg_km(the_model);
rt_vlist_cleanup();
db_close(dbip);
dbip = NULL;
bu_free(tribuf[0], "vert");
bu_free(tribuf[1], "vert");
bu_free(tribuf[2], "vert");
@@ -331,7 +359,17 @@ GFile::load_g(const char *filename, int argc, const char *argv[])
void
GFile::closedb()
{
bu_free(tie, "free tie");
rt_annot_scene_destroy(annotations);
annotations = NULL;
if (tie) {
TIE_FREE(tie);
bu_free(tie, "free tie");
tie = NULL;
}
if (dbip) {
db_close(dbip);
dbip = NULL;
}
current_file.clear();
}
@@ -345,4 +383,3 @@ GFile::closedb()
* End:
* ex: shiftwidth=4 tabstop=8
*/
+8 -5
View File
@@ -32,6 +32,7 @@
extern "C" {
#include "bn/tol.h"
#include "raytrace.h"
#include "rt/primitives/annot.h"
#include "rt/tie.h"
#include "adrt.h"
@@ -43,13 +44,16 @@ class GFile : public QObject
Q_OBJECT
public:
~GFile() override;
int load_g(const char *filename, int argc, const char **argv);
void closedb();
struct tie_s *tie;
struct adrt_mesh_s *meshes;
struct db_i *dbip;
TIE_3 **tribuf;
struct tie_s *tie = NULL;
struct adrt_mesh_s *meshes = NULL;
struct db_i *dbip = NULL;
struct rt_annot_scene *annotations = NULL;
TIE_3 **tribuf = NULL;
QString current_file;
};
@@ -66,4 +70,3 @@ class GFile : public QObject
* End:
* ex: shiftwidth=4 tabstop=8
*/
+4 -2
View File
@@ -28,6 +28,9 @@
int
ISSTApp::load_g(const char *filename, int argc, const char *argv[])
{
/* Synchronize with the render thread before GFile releases the old
* annotation scene and TIE data. */
w.canvas->clear_tie();
if (g.load_g(filename, argc, argv)) {
w.statusBar()->showMessage("open failed");
return -1;
@@ -35,7 +38,7 @@ ISSTApp::load_g(const char *filename, int argc, const char *argv[])
// The OpenGL widget manages the rendering, so let it know about the
// TIE data structure associated with the current model
w.canvas->set_tie(g.tie);
w.canvas->set_tie(g.tie, g.annotations);
return 0;
}
@@ -51,4 +54,3 @@ ISSTApp::load_g(const char *filename, int argc, const char *argv[])
* End:
* ex: shiftwidth=4 tabstop=8
*/
+1 -4
View File
@@ -36,16 +36,14 @@
class ISSTApp : public QApplication
{
Q_OBJECT
public:
ISSTApp(int &argc, char *argv[]) :QApplication(argc, argv) {};
~ISSTApp() {};
int load_g(const char *filename, int argc, const char **argv);
ISST_MainWindow w;
GFile g;
ISST_MainWindow w;
};
#endif // ISSTAPP_H
@@ -60,4 +58,3 @@ class ISSTApp : public QApplication
* End:
* ex: shiftwidth=4 tabstop=8
*/
+29 -10
View File
@@ -159,8 +159,13 @@ void TIERenderer::render()
m_grabCond.wait(&m_grabMutex);
QMutexLocker lock(&m_renderMutex);
m_grabMutex.unlock();
if (m_exiting)
/* The GUI may clear a scene while a queued render is waiting for the
* context. Return the context before leaving that stale request. */
if (m_exiting || !tie) {
ctx->moveToThread(qGuiApp->thread());
return;
}
Q_ASSERT(ctx->thread() == QThread::currentThread());
@@ -183,7 +188,8 @@ void TIERenderer::render()
// Core TIE render
render_camera_prep(&camera);
render_camera_render(&camera, tie, &tile, &buffer_image);
render_camera_render_annotations(&camera, tie, &tile, &buffer_image,
annotations, ADRT_MODEL_UNITS_PER_TIE_UNIT);
glDisable(GL_LIGHTING);
@@ -297,17 +303,31 @@ void isstGL::grabContext()
}
void
isstGL::set_tie(struct tie_s *in_tie)
isstGL::clear_tie()
{
m_renderer->tie = in_tie;
m_renderer->lockRenderer();
m_renderer->tie = NULL;
m_renderer->annotations = NULL;
m_renderer->changed = false;
m_renderer->unlockRenderer();
}
// Initialize the camera position
VSETALL(m_renderer->camera.pos, m_renderer->tie->radius);
VMOVE(m_renderer->camera.focus, m_renderer->tie->mid);
void
isstGL::set_tie(struct tie_s *in_tie, struct rt_annot_scene *annotations)
{
m_renderer->lockRenderer();
m_renderer->tie = in_tie;
m_renderer->annotations = annotations;
render_camera_fit_scene(&m_renderer->camera, m_renderer->tie, annotations,
ADRT_MODEL_UNITS_PER_TIE_UNIT);
// Record the initial settings for use in subsequent calculations
VSETALL(m_renderer->camera_pos_init, m_renderer->tie->radius);
VMOVE(m_renderer->camera_focus_init, m_renderer->tie->mid);
VMOVE(m_renderer->camera_pos_init, m_renderer->camera.pos);
VMOVE(m_renderer->camera_focus_init, m_renderer->camera.focus);
m_renderer->changed = true;
m_renderer->unlockRenderer();
// Having just loaded a new TIE scene,
// we need a new image
@@ -369,4 +389,3 @@ void isstGL::save_image() {
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+3 -2
View File
@@ -77,6 +77,7 @@ class TIERenderer : public QObject, protected QOpenGLFunctions
public:
struct tie_s *tie = NULL; // From parent app
struct rt_annot_scene *annotations = NULL; // Owned by the parent app
struct render_camera_s camera;
vect_t camera_pos_init;
vect_t camera_focus_init;
@@ -113,7 +114,8 @@ class isstGL : public QOpenGLWidget
explicit isstGL(QWidget *parent = nullptr);
~isstGL();
void set_tie(struct tie_s *in_tie);
void clear_tie();
void set_tie(struct tie_s *in_tie, struct rt_annot_scene *annotations);
void save_image();
@@ -153,4 +155,3 @@ class isstGL : public QOpenGLWidget
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+196 -3
View File
@@ -480,6 +480,22 @@ _nirt_get_resource(struct nirt_state *nss)
}
static void
_nirt_annotation_result_clear(struct nirt_state *nss)
{
delete nss->i->vals->annotation;
nss->i->vals->annotation = NULL;
}
static void
_nirt_annotation_scene_clear(struct nirt_state *nss)
{
rt_annot_scene_destroy(nss->i->annotation_scene);
nss->i->annotation_scene = NULL;
}
int
_nirt_raytrace_prep(struct nirt_state *nss)
{
@@ -517,11 +533,17 @@ _nirt_raytrace_prep(struct nirt_state *nss)
bu_free(attrs, "objs");
return -1;
}
bu_free(objs, "objs");
bu_free(attrs, "objs");
nmsg(nss, "Prepping the geometry...\n");
rt_prep(nss->i->ap->a_rt_i);
_nirt_annotation_scene_clear(nss);
if (nss->i->use_annotations) {
nss->i->annotation_scene = rt_annot_scene_create(nss->i->dbip,
ocnt, objs, &nss->i->ap->a_rt_i->rti_ttol,
&nss->i->ap->a_rt_i->rti_tol);
}
bu_free(objs, "objs");
nmsg(nss, "%s ", (nss->i->active_paths.size() == 1) ? "Object" : "Objects");
for (size_t i = 0; i < nss->i->active_paths.size(); i++) {
if (i == 0) {
@@ -960,6 +982,17 @@ _nirt_print_fmt_substr(struct nirt_state *nss, struct bu_vls *ostr, const char *
nirt_print_key("a", r->a);
nirt_print_key("e", r->e);
if (r->annotation) {
nirt_print_key("annot_path", r->annotation->path.c_str());
nirt_print_key("annot_segment", r->annotation->segment);
nirt_print_key("annot_role", r->annotation->role.c_str());
nirt_print_key("annot_x", r->annotation->point[X] * base2local);
nirt_print_key("annot_y", r->annotation->point[Y] * base2local);
nirt_print_key("annot_z", r->annotation->point[Z] * base2local);
nirt_print_key("annot_d", r->annotation->d * base2local);
nirt_print_key("annot_visible", r->annotation->visible);
}
if (!r->seg || r->seg->type == 0) return;
if (r->seg->type == NIRT_PARTITION_SEG || r->seg->type == NIRT_ALL_SEG) {
@@ -1056,6 +1089,9 @@ _nirt_report(struct nirt_state *nss, char type, struct nirt_output_record *r)
std::vector<std::pair<std::string,std::string> >::iterator f_it;
switch (type) {
case 'a':
fmt_vect = &nss->i->fmt.annotation;
break;
case 'r':
fmt_vect = &nss->i->fmt.ray;
break;
@@ -1091,6 +1127,75 @@ _nirt_report(struct nirt_state *nss, char type, struct nirt_output_record *r)
}
static const char *
_nirt_annotation_role(uint32_t role)
{
switch (role) {
case RT_ANNOT_ROLE_UNSPECIFIED:
return "unspecified";
case RT_ANNOT_ROLE_GEOMETRY:
return "geometry";
case RT_ANNOT_ROLE_TEXT:
return "text";
case RT_ANNOT_ROLE_LEADER:
return "leader";
case RT_ANNOT_ROLE_EXTENSION:
return "extension";
case RT_ANNOT_ROLE_DIMENSION:
return "dimension";
case RT_ANNOT_ROLE_ARROWHEAD:
return "arrowhead";
case RT_ANNOT_ROLE_SYMBOL:
return "symbol";
case RT_ANNOT_ROLE_MASK:
return "mask";
case RT_ANNOT_ROLE_CENTERMARK:
return "centermark";
case RT_ANNOT_ROLE_PLACEHOLDER:
return "placeholder";
case RT_ANNOT_ROLE_TEXT_DECORATION:
return "text-decoration";
default:
return "unknown";
}
}
static void
_nirt_report_annotations(struct nirt_state *nss)
{
const size_t count = rt_annot_scene_query_model(nss->i->annotation_scene,
&nss->i->ap->a_ray, nss->i->first_solid_distance, NULL, 0);
if (!count)
return;
std::vector<struct rt_annot_hit> hits(count);
const size_t found = rt_annot_scene_query_model(nss->i->annotation_scene,
&nss->i->ap->a_ray, nss->i->first_solid_distance, hits.data(),
hits.size());
nirt_seg *saved_segment = nss->i->vals->seg;
nss->i->vals->seg = NULL;
for (size_t i = 0; i < std::min(found, hits.size()); ++i) {
const struct rt_annot_hit &hit = hits[i];
if (hit.segment > (size_t)std::numeric_limits<int>::max()) {
nerr(nss, "Annotation segment index is too large for NIRT reporting\n");
continue;
}
if (!nss->i->vals->annotation)
nss->i->vals->annotation = new nirt_annotation_record;
nirt_annotation_record *record = nss->i->vals->annotation;
record->path = hit.path ? hit.path : "";
record->segment = (int)hit.segment;
record->role = _nirt_annotation_role(hit.role);
VMOVE(record->point, hit.point);
record->d = d_calc(nss, record->point);
record->visible = hit.visible;
_nirt_report(nss, 'a', nss->i->vals);
}
nss->i->vals->seg = saved_segment;
}
/************************
* Raytracing Callbacks *
************************/
@@ -1213,6 +1318,21 @@ _nirt_if_hit(struct application *ap, struct partition *part_head, struct seg *UN
int ev_odd = 1; /* first partition is colored as "odd" */
point_t out_old = VINIT_ZERO;
double d_out_old = 0.0;
nss->i->first_solid_distance = INFINITY;
for (part = part_head->pt_forw; part != part_head; part = part->pt_forw) {
if (part->pt_regionp->reg_aircode)
continue;
if (part->pt_inhit->hit_dist >= 0.0) {
nss->i->first_solid_distance = part->pt_inhit->hit_dist;
break;
}
if (part->pt_outhit->hit_dist >= 0.0) {
nss->i->first_solid_distance = part->pt_outhit->hit_dist;
break;
}
}
nirt_seg *s = new nirt_seg;
if (vals->seg) {
delete vals->seg;
@@ -1549,9 +1669,10 @@ static const struct nirt_cmd_desc nirt_descs[] = {
{ "s", "shoot a ray at the target", NULL },
{ "backout", "back out of model", NULL },
{ "useair", "set/query use of air", "<0|1|2|...>" },
{ "useannot", "set/query annotation reporting", "<0|1>" },
{ "units", "set/query local units", "<mm|cm|m|in|ft>" },
{ "overlap_claims", "set/query overlap rebuilding/retention", "<0|1|2|3>" },
{ "fmt", "set/query output formats", "{rhpfmog} format item item ..." },
{ "fmt", "set/query output formats", "{arhpfmog} format item item ..." },
{ "plotfile", "designate an output file to hold plot data", "file" },
{ "print", "query an output item", "item" },
{ "debug", "set/query nirt debug flags", "[-h] [-l [lib]] [-C [lib]] [-V [lib] [val]] [lib [flag]]" },
@@ -1828,6 +1949,9 @@ _nirt_cmd_shoot(void *ns, int argc, const char **UNUSED(argv))
return -1;
}
_nirt_annotation_result_clear(nss);
nss->i->first_solid_distance = INFINITY;
/* If we have no active rtip, we don't need to prep or shoot */
if (!_nirt_get_rtip(nss)) {
return 0;
@@ -1860,9 +1984,10 @@ _nirt_cmd_shoot(void *ns, int argc, const char **UNUSED(argv))
nss->i->ap->a_ray.r_pt[2] * nss->i->base2local,
V3ARGS(nss->i->ap->a_ray.r_dir));
// TODO - any necessary initialization for data collection by callbacks
_nirt_init_ovlp(nss);
(void)rt_shootray(nss->i->ap);
if (nss->i->use_annotations)
_nirt_report_annotations(nss);
// Undo backout
for (i = 0; i < 3; ++i) {
@@ -1965,6 +2090,45 @@ _nirt_cmd_use_air(void *ns, int argc, const char *argv[])
}
extern "C" int
_nirt_cmd_use_annotations(void *ns, int argc, const char *argv[])
{
struct nirt_state *nss = (struct nirt_state *)ns;
struct bu_vls optparse_msg = BU_VLS_INIT_ZERO;
int enabled = 0;
if (!ns) return -1;
if (argc == 1) {
nout(nss, "use_annotations = %d\n", nss->i->use_annotations);
return 0;
}
if (argc > 2) {
nerr(nss, "Usage: useannot %s\n", _nirt_get_desc_args("useannot"));
return -1;
}
if (bu_opt_int(&optparse_msg, 1, (const char **)&argv[1],
(void *)&enabled) == -1) {
nerr(nss, "Error: bu_opt value read failure: %s\n\nUsage: useannot %s\n",
bu_vls_cstr(&optparse_msg), _nirt_get_desc_args("useannot"));
bu_vls_free(&optparse_msg);
return -1;
}
bu_vls_free(&optparse_msg);
if (enabled != 0 && enabled != 1) {
nerr(nss, "useannot must be set to 0 (off) or 1 (on)\n");
return -1;
}
if (enabled != nss->i->use_annotations) {
nss->i->use_annotations = enabled;
_nirt_annotation_scene_clear(nss);
_nirt_annotation_result_clear(nss);
if (enabled)
nss->i->need_reprep = 1;
}
return 0;
}
extern "C" int
_nirt_cmd_units(void *ns, int argc, const char *argv[])
{
@@ -2085,6 +2249,9 @@ _nirt_cmd_format_output(void *ns, int argc, const char **argv)
// print out current fmt str for the specified type
struct bu_vls ostr = BU_VLS_INIT_ZERO;
switch (type) {
case 'a':
_nirt_print_fmt_str(&ostr, nss->i->fmt.annotation);
break;
case 'r':
_nirt_print_fmt_str(&ostr, nss->i->fmt.ray);
break;
@@ -2191,6 +2358,10 @@ _nirt_cmd_format_output(void *ns, int argc, const char **argv)
* formatting instructions with the new. */
set_fmt:
switch (type) {
case 'a':
nss->i->fmt.annotation.clear();
nss->i->fmt.annotation = fmt_tmp;
break;
case 'r':
nss->i->fmt.ray.clear();
nss->i->fmt.ray = fmt_tmp;
@@ -2543,6 +2714,7 @@ _nirt_cmd_state(void *ns, int argc, const char *argv[])
bu_vls_printf(&dumpstr, "xyz %g %g %g\n", V3ARGS(nss->i->vals->orig));
bu_vls_printf(&dumpstr, "dir %g %g %g\n", V3ARGS(nss->i->vals->dir));
bu_vls_printf(&dumpstr, "useair %d\n", nss->i->use_air);
bu_vls_printf(&dumpstr, "useannot %d\n", nss->i->use_annotations);
bu_vls_printf(&dumpstr, "units %s\n", bu_units_string(nss->i->local2base));
bu_vls_printf(&dumpstr, "overlap_claims ");
switch (nss->i->overlap_claims) {
@@ -2563,6 +2735,8 @@ _nirt_cmd_state(void *ns, int argc, const char *argv[])
break;
}
struct bu_vls ostr = BU_VLS_INIT_ZERO;
_nirt_print_fmt_cmd(&ostr, 'a', nss->i->fmt.annotation);
bu_vls_printf(&dumpstr, "%s", bu_vls_cstr(&ostr));
_nirt_print_fmt_cmd(&ostr, 'r', nss->i->fmt.ray);
bu_vls_printf(&dumpstr, "%s", bu_vls_cstr(&ostr));
_nirt_print_fmt_cmd(&ostr, 'h', nss->i->fmt.head);
@@ -2617,6 +2791,7 @@ const struct bu_cmdtab _libanalyze_nirt_cmds[] = {
{ "state", _nirt_cmd_state},
{ "units", _nirt_cmd_units},
{ "useair", _nirt_cmd_use_air},
{ "useannot", _nirt_cmd_use_annotations},
{ "xyz", _nirt_cmd_target_coor},
{ (char *)NULL, NULL}
};
@@ -2783,6 +2958,14 @@ static const char *nirt_cmd_tbl_defs =
"z_dir, FNOUNIT, Ray direction unit vector z component,"
"a, FNOUNIT, Azimuth,"
"e, FNOUNIT, Elevation,"
"annot_path, STRING, Annotation database path,"
"annot_segment, INT, Annotation segment index,"
"annot_role, STRING, Annotation segment role,"
"annot_x, FLOAT, Annotation hit X coordinate,"
"annot_y, FLOAT, Annotation hit Y coordinate,"
"annot_z, FLOAT, Annotation hit Z coordinate,"
"annot_d, FLOAT, Annotation hit gridplane distance,"
"annot_visible, INT, Annotation hit visibility,"
"x_in, FLOAT, ,"
"y_in, FLOAT, ,"
"z_in, FLOAT, ,"
@@ -2895,6 +3078,7 @@ nirt_init(struct nirt_state *ns)
n->base2local = 0.0;
n->local2base = 0.0;
n->use_air = 0;
n->use_annotations = 0;
n->backout = 1;
n->b_state = false;
@@ -2909,6 +3093,8 @@ nirt_init(struct nirt_state *ns)
BU_GET(n->res_air, struct resource);
n->rtip = RTI_NULL;
n->rtip_air = RTI_NULL;
n->annotation_scene = NULL;
n->first_solid_distance = INFINITY;
n->need_reprep = 1;
BU_GET(n->val_types, struct bu_attribute_value_set);
@@ -2922,6 +3108,7 @@ nirt_init(struct nirt_state *ns)
n->vals->h = 0.0;
n->vals->v = 0.0;
n->vals->d_orig = 0.0;
n->vals->annotation = NULL;
n->vals->seg = NULL;
_nirt_diff_create(ns);
@@ -3025,6 +3212,8 @@ nirt_clear_dbip(struct nirt_state *ns)
{
if (!ns) return -1;
_nirt_annotation_scene_clear(ns);
_nirt_annotation_result_clear(ns);
db_close(ns->i->dbip);
ns->i->dbip = NULL;
@@ -3045,6 +3234,9 @@ void
nirt_destroy(struct nirt_state *ns)
{
if (!ns) return;
_nirt_annotation_scene_clear(ns);
_nirt_annotation_result_clear(ns);
bu_vls_free(&ns->nirt_cmd);
bu_vls_free(ns->i->err);
bu_vls_free(ns->i->msg);
@@ -3057,6 +3249,7 @@ nirt_destroy(struct nirt_state *ns)
db_close(ns->i->dbip);
delete ns->i->vals->seg;
BU_PUT(ns->i->vals, struct nirt_output_record);
BU_PUT(ns->i->res, struct resource);
+17 -1
View File
@@ -83,7 +83,7 @@ extern "C" b_off_t ftello(FILE *);
#define NIRT_OVLP_RETAIN 3
#define NIRT_PRINTF_SPECIFIERS "difeEgGs"
#define NIRT_OUTPUT_TYPE_SPECIFIERS "rhpfmog"
#define NIRT_OUTPUT_TYPE_SPECIFIERS "arhpfmog"
#define HELPFLAG "--print-help"
#define PURPOSEFLAG "--print-purpose"
@@ -274,6 +274,16 @@ class nirt_seg {
}
};
class nirt_annotation_record {
public:
std::string path;
int segment;
std::string role;
point_t point;
fastf_t d;
int visible;
};
/* This record structure doesn't have to correspond exactly to the above list
* of available values, but it needs to retain sufficient information to
* support the ability to generate all of them upon request. */
@@ -286,6 +296,7 @@ struct nirt_output_record {
fastf_t a;
fastf_t e;
struct nirt_overlap ovlp_list;
nirt_annotation_record *annotation;
nirt_seg *seg;
};
@@ -294,6 +305,7 @@ struct nirt_diff_state;
class nirt_fmt_state {
public:
std::vector<std::pair<std::string,std::string> > annotation;
std::vector<std::pair<std::string,std::string> > ray;
std::vector<std::pair<std::string,std::string> > head;
std::vector<std::pair<std::string,std::string> > part;
@@ -303,6 +315,7 @@ class nirt_fmt_state {
std::vector<std::pair<std::string,std::string> > gap;
void clear() {
annotation.clear();
ray.clear();
head.clear();
part.clear();
@@ -347,6 +360,7 @@ struct nirt_state_impl {
int backout;
int overlap_claims;
int use_air;
int use_annotations;
std::set<std::string> attrs; // active attributes
std::vector<std::string> active_paths; // active paths for raytracer
struct nirt_output_record *vals;
@@ -368,6 +382,8 @@ struct nirt_state_impl {
struct resource *res;
struct rt_i *rtip_air;
struct resource *res_air;
struct rt_annot_scene *annotation_scene;
fastf_t first_solid_distance;
int need_reprep;
/* internal format specifier arrays */
+3
View File
@@ -1,6 +1,9 @@
brlcad_addexec(analyze_raydiff raydiff.c "libanalyze;librt;libbu" TEST)
brlcad_addexec(analyze_sp solid_partitions.c "libanalyze;librt;libbu" TEST)
brlcad_addexec(analyze_nhit nhit.cpp "libanalyze;librt;libbu" TEST_USESDATA)
brlcad_addexec(analyze_nirt_annotations nirt_annotations.cpp
"libanalyze;libwdb;librt;libbu" TEST_USESDATA)
brlcad_add_test(NAME analyze_nirt_annotations COMMAND analyze_nirt_annotations)
#####################################
# analyze_densities testing #
+191
View File
@@ -0,0 +1,191 @@
/* N I R T _ A N N O T A T I O N S . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by the
* U.S. Army Research Laboratory.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "common.h"
#include <cstdio>
#include <cstdlib>
#include "analyze.h"
#include "bu/app.h"
#include "bu/file.h"
#include "bu/malloc.h"
#include "raytrace.h"
#include "wdb.h"
static constexpr fastf_t FRONT_ANNOTATION_X = 18.0;
static constexpr fastf_t REAR_ANNOTATION_X = 10.0;
static constexpr fastf_t SCREEN_ANNOTATION_X = 19.0;
static constexpr fastf_t OCCLUDER_MINIMUM_X = 15.0;
static constexpr fastf_t OCCLUDER_MAXIMUM_X = 16.0;
static constexpr fastf_t RAY_ORIGIN_X = 20.0;
static constexpr fastf_t TEST_HALF_EXTENT = 1.0;
static int
capture_output(struct nirt_state *state, void *data)
{
struct bu_vls current = BU_VLS_INIT_ZERO;
struct bu_vls *captured = (struct bu_vls *)data;
nirt_log(&current, state, NIRT_OUT);
bu_vls_vlscat(captured, &current);
bu_vls_free(&current);
return 0;
}
static void
write_annotation(struct rt_wdb *wdbp, const char *name, fastf_t x,
bool screen_space)
{
struct rt_annot_internal annotation = {};
struct line_seg line = {};
struct rt_annot_seg_style style = {};
int reverse = 0;
void *segment = &line;
point2d_t vertices[2] = {{0.0, 0.0}, {TEST_HALF_EXTENT, 0.0}};
annotation.magic = RT_ANNOT_INTERNAL_MAGIC;
VSET(annotation.V, x, 0.0, 0.0);
annotation.flags = screen_space ? RT_ANNOT_SCREEN_SPACE :
RT_ANNOT_MODEL_SPACE;
VSET(annotation.u_vec, 0.0, 1.0, 0.0);
VSET(annotation.v_vec, 0.0, 0.0, 1.0);
annotation.vert_count = 2;
annotation.verts = vertices;
annotation.ant.count = 1;
annotation.ant.reverse = &reverse;
annotation.ant.segments = &segment;
annotation.styles = &style;
line.magic = CURVE_LSEG_MAGIC;
line.start = 0;
line.end = 1;
style.role = RT_ANNOT_ROLE_LEADER;
style.line_pattern = RT_ANNOT_LINE_CONTINUOUS;
if (rt_annot_validate(&annotation, NULL) ||
mk_annot(wdbp, name, &annotation) < 0)
bu_exit(EXIT_FAILURE, "unable to write %s\n", name);
}
static void
clear_standard_formats(struct nirt_state *state)
{
const char *commands[] = {
"fmt r \"\"", "fmt h \"\"", "fmt p \"\"", "fmt f \"\"",
"fmt m \"\"", "fmt o \"\"", "fmt g \"\""
};
for (size_t i = 0; i < sizeof(commands) / sizeof(commands[0]); ++i)
if (nirt_exec(state, commands[i]))
bu_exit(EXIT_FAILURE, "NIRT rejected command: %s\n", commands[i]);
}
static void
get_output(struct bu_vls *output, struct bu_vls *captured)
{
bu_vls_sprintf(output, "%s", bu_vls_cstr(captured));
bu_vls_trunc(captured, 0);
}
int
main(int argc, char **argv)
{
char path[MAXPATHLEN];
struct db_i *dbip;
struct rt_wdb *wdbp;
struct nirt_state *state;
struct bu_vls captured = BU_VLS_INIT_ZERO;
struct bu_vls output = BU_VLS_INIT_ZERO;
struct wmember members;
point_t minimum, maximum;
bu_setprogname(argv[0]);
if (argc != 1)
return EXIT_FAILURE;
{
FILE *fp = bu_temp_file(path, sizeof(path));
if (!fp)
bu_exit(EXIT_FAILURE, "unable to create temporary database path\n");
fclose(fp);
}
dbip = db_create(path, 5);
if (!dbip)
bu_exit(EXIT_FAILURE, "unable to create temporary database\n");
wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_DISK);
if (!wdbp)
bu_exit(EXIT_FAILURE, "unable to open temporary database for writing\n");
write_annotation(wdbp, "front.annot", FRONT_ANNOTATION_X, false);
write_annotation(wdbp, "rear.annot", REAR_ANNOTATION_X, false);
write_annotation(wdbp, "screen.annot", SCREEN_ANNOTATION_X, true);
VSET(minimum, OCCLUDER_MINIMUM_X, -TEST_HALF_EXTENT,
-TEST_HALF_EXTENT);
VSET(maximum, OCCLUDER_MAXIMUM_X, TEST_HALF_EXTENT,
TEST_HALF_EXTENT);
if (mk_rpp(wdbp, "occluder.s", minimum, maximum) < 0)
bu_exit(EXIT_FAILURE, "unable to write occluder\n");
BU_LIST_INIT(&members.l);
if (!mk_addmember("occluder.s", &members.l, NULL, WMOP_UNION) ||
mk_lcomb(wdbp, "occluder.r", &members, 1, NULL, NULL, NULL, 0))
bu_exit(EXIT_FAILURE, "unable to write occluding region\n");
BU_GET(state, struct nirt_state);
if (nirt_init(state))
bu_exit(EXIT_FAILURE, "unable to initialize NIRT\n");
nirt_udata(state, &captured);
nirt_hook(state, capture_output, NIRT_OUT);
if (nirt_exec(state,
"draw front.annot rear.annot screen.annot occluder.r") ||
nirt_init_dbip(state, dbip))
bu_exit(EXIT_FAILURE, "unable to prepare NIRT test database\n");
clear_standard_formats(state);
if (nirt_exec(state,
"fmt a \"%s|%d|%s|%.1f|%.1f|%d\\n\" annot_path annot_segment annot_role annot_x annot_d annot_visible"))
bu_exit(EXIT_FAILURE, "unable to set annotation report format\n");
struct bu_vls shot = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&shot, "backout 0;xyz %g 0 0;dir -1 0 0;s",
RAY_ORIGIN_X);
if (nirt_exec(state, bu_vls_cstr(&shot)))
bu_exit(EXIT_FAILURE, "unable to shoot default NIRT ray\n");
bu_vls_free(&shot);
get_output(&output, &captured);
if (bu_vls_strlen(&output))
bu_exit(EXIT_FAILURE,
"annotations were reported without useannot: %s\n",
bu_vls_cstr(&output));
if (nirt_exec(state, "useannot 1;s"))
bu_exit(EXIT_FAILURE, "unable to shoot annotation NIRT ray\n");
get_output(&output, &captured);
struct bu_vls expected = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&expected,
"/front.annot|0|leader|%.1f|%.1f|1\n"
"/rear.annot|0|leader|%.1f|%.1f|0\n",
FRONT_ANNOTATION_X, FRONT_ANNOTATION_X,
REAR_ANNOTATION_X, REAR_ANNOTATION_X);
if (!BU_STR_EQUAL(bu_vls_cstr(&output), bu_vls_cstr(&expected)))
bu_exit(EXIT_FAILURE, "unexpected annotation report:\n%s",
bu_vls_cstr(&output));
bu_vls_free(&expected);
nirt_destroy(state);
BU_PUT(state, struct nirt_state);
bu_vls_free(&captured);
bu_vls_free(&output);
db_close(dbip);
bu_file_delete(path);
return EXIT_SUCCESS;
}
+1
View File
@@ -113,6 +113,7 @@ set(
trimesh_plot3.cpp
trimesh_repair.cpp
trimesh_remesh.cpp
trimesh_self_isect.cpp
trimesh_sync.cpp
trimesh_split.cpp
vert_tree.c
+9 -15
View File
@@ -249,23 +249,17 @@ namespace gte
// should be called by the user when topology cleanup is needed after
// FillHoles. This avoids a circular include dependency.
// Step 5: Optional output validation. When requireManifold is set
// and the result fails the manifold check, restore the pre-fill
// snapshot so the caller receives the original (unmodified) mesh.
// Validation is only performed when validateOutput is true and at
// least one pass/fail criterion (requireManifold or
// requireNoSelfIntersections) is enabled, since without a criterion
// the result cannot be acted upon.
// Step 5: Optional output validation. Repair callers can accept
// self intersections when a usable manifold is more important.
// When requested, stop on the first failed criterion and restore
// the pre-fill mesh.
if (numFilled > 0 && params.validateOutput &&
(params.requireManifold || params.requireNoSelfIntersections))
((params.requireManifold && !MeshValidation<Real>::IsManifold(triangles)) ||
(params.requireNoSelfIntersections &&
MeshValidation<Real>::HasSelfIntersections(vertices, triangles))))
{
auto validationResult = MeshValidation<Real>::Validate(
vertices, triangles, params.requireNoSelfIntersections);
if (params.requireManifold && !validationResult.isManifold)
{
vertices = std::move(savedVertices);
triangles = std::move(savedTriangles);
}
vertices = std::move(savedVertices);
triangles = std::move(savedTriangles);
}
}
+343 -89
View File
@@ -13,14 +13,15 @@
#pragma once
#include <Mathematics/Vector3.h>
#include <Mathematics/ETManifoldMesh.h>
#include <Mathematics/ETNonmanifoldMesh.h>
#include <Mathematics/IntrTriangle3Triangle3.h>
#include <Mathematics/DistTriangle3Triangle3.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <map>
#include <set>
#include <numeric>
#include <string>
#include <vector>
@@ -132,9 +133,7 @@ template <typename Real>
std::vector<Vector3<Real>> const& vertices,
std::vector<std::array<int32_t, 3>> const& triangles)
{
ValidationResult result;
CheckSelfIntersections(vertices, triangles, result);
return result.hasSelfIntersections;
return FindSelfIntersections(vertices, triangles, true) > 0;
}
private:
@@ -215,107 +214,362 @@ template <typename Real>
result.isOriented = oriented;
}
static bool TriangleBoxesOverlap(
std::vector<Vector3<Real>> const& vertices,
std::array<int32_t, 3> const& tri1,
std::array<int32_t, 3> const& tri2)
static constexpr size_t invalidNode = std::numeric_limits<size_t>::max();
// Small leaves limit pair tests without creating a node per triangle.
static constexpr size_t leafTriangleCount = 4;
// Thin facets can amplify FIQuery's shared-vertex drift by over 100 ulps.
static constexpr Real contactToleranceFactor = static_cast<Real>(256);
// Distance-query contacts need only a much smaller roundoff allowance.
static constexpr Real unconnectedContactToleranceFactor = static_cast<Real>(8);
struct TriangleBounds
{
Vector3<Real> min1 = vertices[tri1[0]];
Vector3<Real> max1 = vertices[tri1[0]];
Vector3<Real> min2 = vertices[tri2[0]];
Vector3<Real> max2 = vertices[tri2[0]];
Vector3<Real> min;
Vector3<Real> max;
Vector3<Real> center;
};
for (int i = 1; i < 3; ++i)
{
for (int axis = 0; axis < 3; ++axis)
{
min1[axis] = std::min(min1[axis], vertices[tri1[i]][axis]);
max1[axis] = std::max(max1[axis], vertices[tri1[i]][axis]);
min2[axis] = std::min(min2[axis], vertices[tri2[i]][axis]);
max2[axis] = std::max(max2[axis], vertices[tri2[i]][axis]);
}
}
struct TreeNode
{
Vector3<Real> min;
Vector3<Real> max;
size_t begin = 0;
size_t end = 0;
size_t left = invalidNode;
size_t right = invalidNode;
};
static bool BoxesOverlap(Vector3<Real> const& min0, Vector3<Real> const& max0,
Vector3<Real> const& min1, Vector3<Real> const& max1)
{
for (int axis = 0; axis < 3; ++axis)
{
if (max1[axis] < min2[axis] || max2[axis] < min1[axis])
if (max0[axis] < min1[axis] || max1[axis] < min0[axis])
{
return false;
}
}
return true;
}
// Check for self-intersecting triangles
static void ExpandBounds(Vector3<Real>& min, Vector3<Real>& max,
Vector3<Real> const& otherMin, Vector3<Real> const& otherMax)
{
for (int axis = 0; axis < 3; ++axis)
{
min[axis] = std::min(min[axis], otherMin[axis]);
max[axis] = std::max(max[axis], otherMax[axis]);
}
}
static size_t BuildTree(std::vector<TriangleBounds> const& bounds,
std::vector<size_t>& order, std::vector<TreeNode>& nodes,
size_t begin, size_t end)
{
size_t nodeIndex = nodes.size();
nodes.emplace_back();
nodes[nodeIndex].begin = begin;
nodes[nodeIndex].end = end;
if (end - begin <= leafTriangleCount)
{
auto const& first = bounds[order[begin]];
nodes[nodeIndex].min = first.min;
nodes[nodeIndex].max = first.max;
for (size_t i = begin + 1; i < end; ++i)
{
auto const& box = bounds[order[i]];
ExpandBounds(nodes[nodeIndex].min, nodes[nodeIndex].max,
box.min, box.max);
}
return nodeIndex;
}
Vector3<Real> centerMin = bounds[order[begin]].center;
Vector3<Real> centerMax = centerMin;
for (size_t i = begin + 1; i < end; ++i)
{
auto const& center = bounds[order[i]].center;
ExpandBounds(centerMin, centerMax, center, center);
}
int axis = 0;
for (int i = 1; i < 3; ++i)
{
if (centerMax[i] - centerMin[i] > centerMax[axis] - centerMin[axis])
{
axis = i;
}
}
size_t middle = begin + (end - begin) / 2;
std::nth_element(order.begin() + begin, order.begin() + middle,
order.begin() + end, [&bounds, axis](size_t a, size_t b) {
return bounds[a].center[axis] < bounds[b].center[axis];
});
size_t left = BuildTree(bounds, order, nodes, begin, middle);
size_t right = BuildTree(bounds, order, nodes, middle, end);
nodes[nodeIndex].left = left;
nodes[nodeIndex].right = right;
nodes[nodeIndex].min = nodes[left].min;
nodes[nodeIndex].max = nodes[left].max;
ExpandBounds(nodes[nodeIndex].min, nodes[nodeIndex].max,
nodes[right].min, nodes[right].max);
return nodeIndex;
}
static bool IntersectBeyondSharedFeature(
std::vector<Vector3<Real>> const& vertices,
std::vector<std::array<int32_t, 3>> const& triangles,
size_t i, size_t j)
{
auto const& indices0 = triangles[i];
auto const& indices1 = triangles[j];
std::array<int32_t, 3> shared{};
size_t sharedCount = 0;
for (int32_t index0 : indices0)
{
for (int32_t index1 : indices1)
{
if (index0 == index1)
{
shared[sharedCount++] = index0;
break;
}
}
}
if (sharedCount == 3)
{
return true; // Coincident faces overlap, regardless of winding.
}
if (sharedCount == 2)
{
// Two planes through the same edge meet only on that edge unless
// they are coplanar. In the coplanar case, the third vertices
// must be on opposite sides of the edge to avoid overlap.
auto const& a = vertices[shared[0]];
Vector3<Real> edge = vertices[shared[1]] - a;
int32_t third0 = -1;
int32_t third1 = -1;
for (int32_t index : indices0)
{
if (index != shared[0] && index != shared[1]) third0 = index;
}
for (int32_t index : indices1)
{
if (index != shared[0] && index != shared[1]) third1 = index;
}
if (third0 < 0 || third1 < 0)
{
return true;
}
Vector3<Real> side0 = Cross(edge, vertices[third0] - a);
Vector3<Real> side1 = Cross(edge, vertices[third1] - a);
Vector3<Real> planeDifference = Cross(side0, side1);
return Dot(planeDifference, planeDifference) <= static_cast<Real>(0) &&
Dot(side0, side1) > static_cast<Real>(0);
}
Triangle3<Real> triangle0(vertices[indices0[0]], vertices[indices0[1]],
vertices[indices0[2]]);
Triangle3<Real> triangle1(vertices[indices1[0]], vertices[indices1[1]],
vertices[indices1[2]]);
if (sharedCount == 0)
{
// The separating-axis test can report contact for near-coplanar
// disjoint faces. Confirm positives with the intersection query.
TIQuery<Real, Triangle3<Real>, Triangle3<Real>> test;
if (!test(triangle0, triangle1).intersect)
{
return false;
}
FIQuery<Real, Triangle3<Real>, Triangle3<Real>> find;
if (find(triangle0, triangle1).intersect)
{
return true;
}
// FIQuery can omit isolated boundary contact. The distance query
// distinguishes that from the separating-axis false positives.
DCPQuery<Real, Triangle3<Real>, Triangle3<Real>> distance;
Real scale = static_cast<Real>(0);
for (auto const& triangle : {triangle0, triangle1})
{
for (auto const& point : triangle.v)
{
for (int axis = 0; axis < 3; ++axis)
{
scale = std::max(scale, std::abs(point[axis]));
}
}
}
Real tolerance = unconnectedContactToleranceFactor *
std::numeric_limits<Real>::epsilon() * scale;
return distance(triangle0, triangle1).sqrDistance <= tolerance * tolerance;
}
FIQuery<Real, Triangle3<Real>, Triangle3<Real>> query;
auto result = query(triangle0, triangle1);
if (!result.intersect)
{
return false;
}
if (result.intersection.empty())
{
return true;
}
auto const& vertex = vertices[shared[0]];
Real scale = static_cast<Real>(0);
for (auto const& indices : {indices0, indices1})
{
for (int32_t index : indices)
{
for (int axis = 0; axis < 3; ++axis)
{
scale = std::max(scale, std::abs(vertices[index][axis]));
scale = std::max(scale, std::abs(vertices[index][axis] - vertex[axis]));
}
}
}
// GTE's computed contact can drift from a shared vertex on thin
// triangles or when their extent greatly exceeds its coordinates.
Real tolerance = contactToleranceFactor *
std::numeric_limits<Real>::epsilon() * scale;
for (auto const& point : result.intersection)
{
Vector3<Real> offset = point - vertex;
if (Dot(offset, offset) > tolerance * tolerance)
{
return true;
}
}
return false;
}
static size_t CountNodePairs(
std::vector<Vector3<Real>> const& vertices,
std::vector<std::array<int32_t, 3>> const& triangles,
std::vector<TriangleBounds> const& bounds,
std::vector<size_t> const& order,
std::vector<TreeNode> const& nodes,
size_t a, size_t b, bool stopAtFirst)
{
auto const& nodeA = nodes[a];
auto const& nodeB = nodes[b];
if (!BoxesOverlap(nodeA.min, nodeA.max, nodeB.min, nodeB.max))
{
return 0;
}
bool leafA = nodeA.left == invalidNode;
bool leafB = nodeB.left == invalidNode;
if (leafA && leafB)
{
size_t count = 0;
for (size_t ia = nodeA.begin; ia < nodeA.end; ++ia)
{
size_t i = order[ia];
for (size_t ib = (a == b ? ia + 1 : nodeB.begin);
ib < nodeB.end; ++ib)
{
size_t j = order[ib];
if (BoxesOverlap(bounds[i].min, bounds[i].max,
bounds[j].min, bounds[j].max) &&
IntersectBeyondSharedFeature(vertices, triangles, i, j))
{
++count;
if (stopAtFirst)
{
return count;
}
}
}
}
return count;
}
if (a == b)
{
size_t count = CountNodePairs(vertices, triangles, bounds, order,
nodes, nodeA.left, nodeA.left, stopAtFirst);
if (stopAtFirst && count)
{
return count;
}
count += CountNodePairs(vertices, triangles, bounds, order,
nodes, nodeA.left, nodeA.right, stopAtFirst);
if (stopAtFirst && count)
{
return count;
}
return count + CountNodePairs(vertices, triangles, bounds, order,
nodes, nodeA.right, nodeA.right, stopAtFirst);
}
if (!leafA && (leafB || nodeA.end - nodeA.begin >= nodeB.end - nodeB.begin))
{
size_t count = CountNodePairs(vertices, triangles, bounds, order,
nodes, nodeA.left, b, stopAtFirst);
if (stopAtFirst && count)
{
return count;
}
return count + CountNodePairs(vertices, triangles, bounds, order,
nodes, nodeA.right, b, stopAtFirst);
}
size_t count = CountNodePairs(vertices, triangles, bounds, order,
nodes, a, nodeB.left, stopAtFirst);
if (stopAtFirst && count)
{
return count;
}
return count + CountNodePairs(vertices, triangles, bounds, order,
nodes, a, nodeB.right, stopAtFirst);
}
static size_t FindSelfIntersections(
std::vector<Vector3<Real>> const& vertices,
std::vector<std::array<int32_t, 3>> const& triangles,
bool stopAtFirst)
{
if (triangles.size() < 2)
{
return 0;
}
std::vector<TriangleBounds> bounds(triangles.size());
for (size_t i = 0; i < triangles.size(); ++i)
{
auto const& tri = triangles[i];
TriangleBounds& box = bounds[i];
box.min = vertices[tri[0]];
box.max = box.min;
for (size_t j = 1; j < 3; ++j)
{
ExpandBounds(box.min, box.max, vertices[tri[j]], vertices[tri[j]]);
}
box.center = box.min / static_cast<Real>(2) +
box.max / static_cast<Real>(2);
}
std::vector<size_t> order(triangles.size());
std::iota(order.begin(), order.end(), 0);
std::vector<TreeNode> nodes;
nodes.reserve(triangles.size());
BuildTree(bounds, order, nodes, 0, triangles.size());
return CountNodePairs(vertices, triangles, bounds, order, nodes,
0, 0, stopAtFirst);
}
static void CheckSelfIntersections(
std::vector<Vector3<Real>> const& vertices,
std::vector<std::array<int32_t, 3>> const& triangles,
ValidationResult& result)
{
result.intersectingTrianglePairs = 0;
size_t n = triangles.size();
// Brute force check all pairs
// TODO: Could optimize with spatial data structure (octree, BVH) for large meshes
for (size_t i = 0; i < n; ++i)
{
auto const& tri1 = triangles[i];
Triangle3<Real> triangle1(
vertices[tri1[0]],
vertices[tri1[1]],
vertices[tri1[2]]
);
for (size_t j = i + 1; j < n; ++j)
{
auto const& tri2 = triangles[j];
// Skip if triangles share a vertex (adjacent triangles)
bool shareVertex = false;
for (int a = 0; a < 3; ++a)
{
for (int b = 0; b < 3; ++b)
{
if (tri1[a] == tri2[b])
{
shareVertex = true;
break;
}
}
if (shareVertex) break;
}
if (shareVertex)
{
continue; // Adjacent triangles can touch at edges/vertices
}
if (!TriangleBoxesOverlap(vertices, tri1, tri2))
{
continue;
}
// Check for intersection
Triangle3<Real> triangle2(
vertices[tri2[0]],
vertices[tri2[1]],
vertices[tri2[2]]
);
FIQuery<Real, Triangle3<Real>, Triangle3<Real>> query;
auto queryResult = query(triangle1, triangle2);
if (queryResult.intersect)
{
result.intersectingTrianglePairs++;
}
}
}
result.hasSelfIntersections = (result.intersectingTrianglePairs > 0);
result.intersectingTrianglePairs = FindSelfIntersections(vertices, triangles, false);
result.hasSelfIntersections = result.intersectingTrianglePairs > 0;
}
};
}
+12 -6
View File
@@ -419,7 +419,7 @@ protected:
ListNode* AllocListNode();
void FreeListNode(ListNode* a_listNode);
bool Overlap(Rect* a_rectA, Rect* a_rectB) const;
bool Intersect(Rect* a_rect, Ray* a_ray) const;
bool Intersect(const Rect* a_rect, const Ray* a_ray) const;
void ReInsert(Node* a_node, ListNode** a_listNode);
bool Search(Node* a_node, Rect* a_rect, int& a_foundCount, std::function<bool(const DataType&, void *)> callback, void *a_context) const;
void RemoveAllRec(Node* a_node);
@@ -433,14 +433,15 @@ protected:
size_t CheckNodes(Node *a_nodeA, Node *a_nodeB, std::set<DataType> *a_result, std::set<DataType> *b_result);
size_t CheckNodes(Node *a_nodeA, Node *a_nodeB, std::set<std::pair<DataType, DataType>> *result);
size_t IsectNode(Node *a_node, Ray *a_ray, std::set<DataType> *result);
size_t IsectNode(const Node *a_node, const Ray *a_ray,
std::set<DataType> *result) const;
Node* m_root = nullptr; ///< Root of tree
ElementTypeReal m_unitSphereVolume = kElementTypeRealZero; ///< Unit sphere constant for required number of dimensions
public:
// Find the leaves intersected by a ray, returning the data from the leaves in a set.
size_t Intersects(Ray *a_ray, std::set<DataType> *result);
size_t Intersects(const Ray *a_ray, std::set<DataType> *result) const;
void plot(const char *fname);
void plot2d(const char *fname);
@@ -1551,7 +1552,7 @@ bool RTREE_QUAL::Overlap(Rect* a_rectA, Rect* a_rectB) const
// Decide whether a ray intersect a Rect.
RTREE_TEMPLATE
bool RTREE_QUAL::Intersect(Rect* a_rect, Ray* a_ray) const
bool RTREE_QUAL::Intersect(const Rect* a_rect, const Ray* a_ray) const
{
RTreeAssert(a_rect);
if (kNumDimensions != 3) return false;
@@ -1685,7 +1686,8 @@ size_t RTREE_QUAL::Overlaps(RTree &other, std::set<std::pair<DataType, DataType>
RTREE_TEMPLATE
size_t RTREE_QUAL::IsectNode(Node *a_node, Ray *a_ray, std::set<DataType> *result)
size_t RTREE_QUAL::IsectNode(const Node *a_node, const Ray *a_ray,
std::set<DataType> *result) const
{
size_t ocnt = 0;
for (int index = 0; index < a_node->m_count; ++index) {
@@ -1703,7 +1705,8 @@ size_t RTREE_QUAL::IsectNode(Node *a_node, Ray *a_ray, std::set<DataType> *resul
// Loosely based on the idea from the openNURBS code to search two R-trees for all pairs elements whose bounding boxes overlap.
RTREE_TEMPLATE
size_t RTREE_QUAL::Intersects(Ray *a_ray, std::set<DataType> *result)
size_t RTREE_QUAL::Intersects(const Ray *a_ray,
std::set<DataType> *result) const
{
if (!m_root) return 0;
size_t ovlp_cnt = IsectNode(m_root, a_ray, result);
@@ -1800,6 +1803,9 @@ void RTREE_QUAL::plot2d(const char *fname)
}
#undef BB_PLOT2D
#undef BB_PLOT
#undef TREE_LEAF_FACE_3D
#undef RTREE_TEMPLATE
#undef RTREE_QUAL
+10
View File
@@ -459,6 +459,16 @@ if(BUILD_TESTING AND TARGET bg_trimesh_repair)
)
endif()
brlcad_addexec(bg_trimesh_self_isect trimesh_self_isect.cpp "${BG_TEST_LIBS}" TEST)
if(BUILD_TESTING AND TARGET bg_trimesh_self_isect)
target_include_directories(
bg_trimesh_self_isect SYSTEM PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../GTE
${GTE_INCLUDE_DIR}
)
endif()
brlcad_add_test(NAME bg_trimesh_self_isect COMMAND bg_trimesh_self_isect)
brlcad_add_test(NAME bg_trimesh_repair COMMAND bg_trimesh_repair)
if(BUILD_TESTING AND TARGET bg_trimesh_repair)
set_tests_properties(bg_trimesh_repair PROPERTIES TIMEOUT 30)
+228
View File
@@ -0,0 +1,228 @@
/* T R I M E S H _ S E L F _ I S E C T . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library 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 this file; see the file named COPYING for more
* information.
*/
/** @file tests/trimesh_self_isect.cpp */
#include "common.h"
#include <array>
#include <limits>
#include <vector>
#include "bg/trimesh.h"
#include "bu/app.h"
#include "bu/log.h"
#include "Mathematics/MeshValidation.h"
#include "Mathematics/MeshHoleFilling.h"
static int
check_case(const char *name, const point_t *points, size_t num_points,
const int *faces, size_t num_faces, int expected, int expected_pairs = -1)
{
int found = bg_trimesh_self_isect(faces, num_faces, points, num_points);
if (found != expected) {
bu_log("%s: expected %d, got %d\n", name, expected, found);
return 1;
}
if (expected < 0)
return 0;
std::vector<gte::Vector3<double>> vertices;
std::vector<std::array<int32_t, 3>> triangles;
for (size_t i = 0; i < num_points; ++i)
vertices.push_back({points[i][0], points[i][1], points[i][2]});
for (size_t i = 0; i < num_faces; ++i)
triangles.push_back({faces[3*i], faces[3*i+1], faces[3*i+2]});
if (num_faces > 0) {
auto result = gte::MeshValidation<double>::Validate(vertices, triangles, true);
if (result.hasSelfIntersections != (expected > 0) ||
(expected_pairs >= 0 && result.intersectingTrianglePairs != static_cast<size_t>(expected_pairs))) {
bu_log("%s: diagnostic count %zu, expected %d\n", name,
result.intersectingTrianglePairs, expected_pairs);
return 1;
}
}
return 0;
}
int
main(int UNUSED(argc), const char *argv[])
{
bu_setprogname(argv[0]);
int failures = 0;
const point_t base[] = {
{0, 0, 0}, {2, 0, 0}, {0, 2, 0},
{0.5, 0.5, -1}, {0.5, 0.5, 1}
};
const int crossing[] = {0, 1, 2, 0, 3, 4};
failures += check_case("shared vertex with crossing", base, 5, crossing, 2, 1, 1);
const point_t large_extent_touch[] = {
{1.0e-11, 0, 0}, {1.0e7 + 1.0e-11, 0, 0},
{1.0e-11, 1.0e7, 0}, {-1.0e7 + 1.0e-11, 0, 1.0e7},
{1.0e-11, -1.0e7, 1.0e7}
};
const int vertex_last[] = {1, 2, 0, 3, 4, 0};
failures += check_case("shared vertex with large extent", large_extent_touch, 5,
vertex_last, 2, 0, 0);
const point_t large_extent_cross[] = {
{1.0e-11, 0, 0}, {1.0e7 + 1.0e-11, 0, 0},
{1.0e-11, 1.0e7, 0}, {1.0e-5 + 1.0e-11, 1.0e-5, -1},
{1.0e-5 + 1.0e-11, 1.0e-5, 1}
};
failures += check_case("small crossing on large triangle", large_extent_cross, 5,
vertex_last, 2, 1, 1);
// Two thin wing facets sharing only vertex 1. GTE's computed contact
// points differ from that vertex by roundoff, not by a crossing.
const point_t thin_wing_joint[] = {
{226.67034893030407, 280, 21.576417093095706},
{219.99057362097182, 420, 31.951862388809637},
{219.55966619917172, 420, 32.033775058413987},
{226.6426768041579, 280, 21.073177335689227},
{219.96830407125381, 420, 31.49880938180566}
};
const int wing_faces[] = {0, 1, 2, 3, 4, 1};
failures += check_case("thin wing shared vertex", thin_wing_joint, 5,
wing_faces, 2, 0, 0);
const point_t thin_wing_roundoff[] = {
{112.50942906974174, 341.25, 24.0039606218491},
{90.007543255793394, 455, 31.929848271198175},
{89.150655159118429, 455, 32.1073840972605},
{112.49057093025826, 341.25, 23.721088529596759},
{89.992456744206606, 455, 31.703550597396301}
};
failures += check_case("thin wing vertex roundoff", thin_wing_roundoff, 5,
wing_faces, 2, 0, 0);
// Their boxes overlap, and GTE's separating-axis test reports contact,
// but its full intersection query finds these thin faces disjoint.
const point_t thin_wing_disjoint[] = {
{151.56909011106083, 113.75, 9.3593771810054793},
{123.7207397950647, 227.5, 18.298201672124883},
{144.34086309424214, 113.75, 10.742335472713284},
{129.91636295233786, 227.5, 17.11280885066105},
{108.26363579361488, 341.25, 24.866240520316623},
{103.10061649588725, 341.25, 25.854067871536483}
};
const int disjoint_faces[] = {0, 1, 2, 3, 4, 5};
failures += check_case("thin disjoint faces", thin_wing_disjoint, 6,
disjoint_faces, 2, 0, 0);
const point_t shared_edge[] = {
{0, 0, 0}, {2, 0, 0}, {0, 2, 0}, {1, 0, 1}
};
const int edge_faces[] = {0, 1, 2, 0, 1, 3};
failures += check_case("valid shared edge", shared_edge, 4, edge_faces, 2, 0, 0);
const point_t thin_edge[] = {
{0, 0, 0}, {2, 0, 0}, {0, 2, 0}, {1, 0, 1.0e-10}
};
failures += check_case("thin nonplanar shared edge", thin_edge, 4, edge_faces, 2, 0, 0);
const point_t overlapping_edge[] = {
{0, 0, 0}, {2, 0, 0}, {0, 2, 0}, {1, 0.3, 0}
};
failures += check_case("shared edge with overlap", overlapping_edge, 4, edge_faces, 2, 1, 1);
const point_t shared_vertex[] = {
{0, 0, 0}, {2, 0, 0}, {0, 2, 0}, {-1, 0, 0}, {0, -1, 0}
};
const int vertex_faces[] = {0, 1, 2, 0, 3, 4};
failures += check_case("valid shared vertex", shared_vertex, 5, vertex_faces, 2, 0, 0);
const point_t planar_overlap[] = {
{0, 0, 0}, {2, 0, 0}, {0, 2, 0}, {1, 0.2, 0}, {0.2, 1, 0}
};
failures += check_case("coplanar shared vertex overlap", planar_overlap, 5,
vertex_faces, 2, 1, 1);
const point_t coincident[] = {
{0, 0, 0}, {1, 0, 0}, {0, 1, 0},
{0, 0, 0}, {1, 0, 0}, {0, 1, 0},
{0, 0, 0}, {1, 0, 0}, {0, 1, 0}
};
const int coincident_faces[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
failures += check_case("three coincident faces", coincident, 9, coincident_faces, 3, 1, 3);
const point_t unconnected_touch[] = {
{0, 0, 0}, {1, 0, 0}, {0, 1, 0},
{0, 0, 0}, {-1, 0, 0}, {0, -1, 0}
};
const int two_faces[] = {0, 1, 2, 3, 4, 5};
failures += check_case("unconnected contact", unconnected_touch, 6, two_faces, 2, 1, 1);
const point_t cube[] = {
{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0},
{0, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 1, 1}
};
const int cube_faces[] = {
0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7,
0, 1, 5, 0, 5, 4, 1, 2, 6, 1, 6, 5,
2, 3, 7, 2, 7, 6, 3, 0, 4, 3, 4, 7
};
failures += check_case("closed cube", cube, 8, cube_faces, 12, 0, 0);
point_t overlapping_cubes[16];
int overlapping_faces[72];
for (size_t i = 0; i < 8; ++i) {
for (size_t axis = 0; axis < 3; ++axis) {
overlapping_cubes[i][axis] = cube[i][axis];
overlapping_cubes[i+8][axis] = cube[i][axis] + 0.5;
}
}
for (size_t i = 0; i < 36; ++i) {
overlapping_faces[i] = cube_faces[i];
overlapping_faces[i+36] = cube_faces[i] + 8;
}
failures += check_case("overlapping closed cubes", overlapping_cubes, 16,
overlapping_faces, 24, 1);
const int bad_index[] = {0, 1, 5};
failures += check_case("invalid index", base, 5, bad_index, 1, -1);
const int degenerate[] = {0, 0, 1};
failures += check_case("degenerate face", base, 5, degenerate, 1, -1);
point_t nonfinite[3] = {{0, 0, 0}, {1, 0, 0}, {0, 1, 0}};
nonfinite[1][0] = std::numeric_limits<double>::quiet_NaN();
const int triangle[] = {0, 1, 2};
failures += check_case("nonfinite point", nonfinite, 3, triangle, 1, -1);
failures += check_case("empty mesh", nullptr, 0, nullptr, 0, 0);
// A single open triangle gets a coincident cap. Permissive filling may
// keep it; the explicitly requested self-intersection check rolls back.
std::vector<gte::Vector3<double>> fill_vertices = {{0, 0, 0}, {2, 0, 0}, {0, 2, 0}};
const std::vector<std::array<int32_t, 3>> original = {{0, 1, 2}};
for (bool strict : {false, true}) {
auto faces = original;
gte::MeshHoleFilling<double>::Parameters params;
params.method = gte::MeshHoleFilling<double>::TriangulationMethod::EarClipping3D;
params.validateOutput = strict;
params.requireNoSelfIntersections = strict;
gte::MeshHoleFilling<double>::FillHoles(fill_vertices, faces, params);
if (faces.size() != (strict ? original.size() : original.size() + 1)) {
bu_log("hole filling self-intersection option failed (strict=%d)\n", strict);
++failures;
}
}
if (failures)
bu_log("%d self intersection cases failed\n", failures);
return failures ? 1 : 0;
}
+89
View File
@@ -0,0 +1,89 @@
/* T R I M E S H _ S E L F _ I S E C T . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library 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 this file; see the file named COPYING for more
* information.
*/
/** @file trimesh_self_isect.cpp */
#include "common.h"
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <exception>
#include <vector>
#include "bg/trimesh.h"
#include "Mathematics/MeshValidation.h"
extern "C" int
bg_trimesh_self_isect(const int *faces, size_t num_faces,
const point_t *points, size_t num_points)
{
if (num_faces == 0)
return 0;
if (!faces || !points || num_points == 0 ||
num_points > static_cast<size_t>(std::numeric_limits<int32_t>::max()) ||
num_faces > std::numeric_limits<size_t>::max() / 3)
return -1;
try {
std::vector<gte::Vector3<fastf_t>> vertices;
vertices.reserve(num_points);
for (size_t i = 0; i < num_points; ++i) {
if (!std::isfinite(points[i][0]) || !std::isfinite(points[i][1]) ||
!std::isfinite(points[i][2]))
return -1;
vertices.push_back({points[i][0], points[i][1], points[i][2]});
}
std::vector<std::array<int32_t, 3>> triangles;
triangles.reserve(num_faces);
for (size_t i = 0; i < num_faces; ++i) {
std::array<int32_t, 3> tri;
for (size_t j = 0; j < 3; ++j) {
int index = faces[3 * i + j];
if (index < 0 || static_cast<size_t>(index) >= num_points)
return -1;
tri[j] = static_cast<int32_t>(index);
}
if (tri[0] == tri[1] || tri[1] == tri[2] || tri[2] == tri[0])
return -1;
auto normal = Cross(vertices[tri[1]] - vertices[tri[0]],
vertices[tri[2]] - vertices[tri[0]]);
fastf_t area_squared = Dot(normal, normal);
if (!(area_squared > 0.0) || !std::isfinite(area_squared))
return -1;
triangles.push_back(tri);
}
return gte::MeshValidation<fastf_t>::HasSelfIntersections(vertices, triangles) ? 1 : 0;
} catch (std::exception const&) {
// A C caller must receive an error code rather than a C++ exception.
return -1;
}
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+1 -1
View File
@@ -234,7 +234,7 @@ set(
mime.cmake
parallel.h
parallel_cpp11thread.cpp
process.h
process_private.h
sha1.h
struetype.h
vfont.h
+1 -1
View File
@@ -49,7 +49,7 @@
#include "bu/str.h"
#include "bu/datetime.h"
#include "bu/vls.h"
#include "./process.h"
#include "./process_private.h"
#ifndef HAVE_KILL
# include <TlHelp32.h>
@@ -1,4 +1,4 @@
/* P R O C E S S . H
/* P R O C E S S _ P R I V A T E . H
* BRL-CAD
*
* Copyright (c) 2013-2026 United States Government as represented by
@@ -20,8 +20,8 @@
/* Private libbu process related definitions shared between multiple files */
#ifndef LIBBU_PROCESS_H
#define LIBBU_PROCESS_H
#ifndef LIBBU_PROCESS_PRIVATE_H
#define LIBBU_PROCESS_PRIVATE_H
/* Apparently the termination code on Windows is arbitrary and somewhat
* ill-defined - see https://bugs.python.org/issue31863 for more info. Try our
@@ -29,7 +29,7 @@
* abort identification. */
#define BU_MSVC_ABORT_EXIT 97101 /* ascii - a == 97 && e == 101 */
#endif /* LIBBU_PROCESS_H */
#endif /* LIBBU_PROCESS_PRIVATE_H */
/*
* Local Variables:
@@ -188,8 +188,7 @@ test_uncertain()
require(result.shells.empty() || (result.shells.front().orientation == ShellOrientation::Indeterminate &&
!result.shells.front().corrected), "invalid parameter-loop senses caused an incorrect face repair");
auto flat = box();
ON_Xform flatten;
flatten.DiagonalTransformation(1.0);
ON_Xform flatten = ON_Xform::DiagonalTransformation(1.0);
flatten[2][2] = 1.0e-13;
require(flat->Transform(flatten), "could not construct thin shell");
result = repair_brep_orientation(*flat);
+8 -6
View File
@@ -62,9 +62,8 @@ constexpr fastf_t DEFAULT_LEADER_MARGIN_SCALE = 2.0;
constexpr fastf_t DEFAULT_SCREEN_TEXT_HEIGHT_MM = 3.0;
/* The display-plane renderer uses this nominal density. --dpi makes the
* physical size explicit for displays whose effective density differs. */
constexpr fastf_t DEFAULT_SCREEN_DPI = 96.0;
constexpr fastf_t MILLIMETERS_PER_INCH = 25.4;
constexpr fastf_t DISPLAY_PIXELS_PER_MM = DEFAULT_SCREEN_DPI / MILLIMETERS_PER_INCH;
constexpr fastf_t DEFAULT_SCREEN_DPI = RT_ANNOT_SCREEN_DPI;
constexpr fastf_t DISPLAY_PIXELS_PER_MM = RT_ANNOT_SCREEN_PIXELS_PER_MM;
constexpr double AUTODIM_TEXT_WIDTH_SCALE = 0.65;
/* Layout penalties deliberately dominate the smaller rewards so avoiding
* collisions and crossings wins over merely moving a label farther out. */
@@ -473,10 +472,12 @@ parse_options(struct ged *gedp, int argc, const char **argv,
bu_vls_printf(gedp->ged_result_str, "Precision must be between 0 and 15");
return -1;
}
if (opts.text_height < 0.0 || opts.line_width < 0.0 ||
if (!std::isfinite(opts.text_height) || !std::isfinite(opts.line_width) ||
opts.text_height < 0.0 || opts.line_width < 0.0 ||
std::isinf(opts.offset) ||
(std::isfinite(opts.offset) && opts.offset < 0.0)) {
bu_vls_printf(gedp->ged_result_str,
"Text height, line width, and offset must not be negative");
"Text height and line width must be finite and non-negative; offset must be finite and non-negative when specified");
return -1;
}
if (!std::isfinite(opts.dpi) || opts.dpi <= 0.0) {
@@ -2904,7 +2905,8 @@ cmd_info(void *data, int argc, const char **argv)
if (rt_db_get_internal(&intern, dp, gedp->dbip, NULL) == ID_ANNOT) {
struct rt_annot_internal *annotation =
static_cast<struct rt_annot_internal *>(intern.idb_ptr);
bu_vls_printf(gedp->ged_result_str, "\nspace: %s\nsegments: %zu",
bu_vls_printf(gedp->ged_result_str,
"\nspace: %s\nsegments: %zu",
(annotation->flags & RT_ANNOT_MODEL_SPACE) ? "model" : "view",
annotation->ant.count);
rt_db_free_internal(&intern);
+1
View File
@@ -29,6 +29,7 @@
#include <vector>
extern "C" {
#include "bu/units.h"
#include "rt/calc.h"
#include "wdb.h"
}
+1
View File
@@ -30,6 +30,7 @@
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include "bio.h"
#include "vmath.h"
+2
View File
@@ -150,6 +150,7 @@ set(
prep.cpp
${LIBRT_PRIMEDIT_SOURCES}
primitives/annot/annot.c
primitives/annot/annot_render.cpp
primitives/arb8/arb8.c
primitives/arb8/arb8_brep.cpp
primitives/arb8/arb8_mirror.c
@@ -427,6 +428,7 @@ endif(BRLCAD_ENABLE_SPR)
set(LIBRT_PUBLIC_LIBS ${librt_deps} OPENNURBS::OPENNURBS)
set(
LIBRT_PRIVATE_LIBS
Threads::Threads
${OPENCL_LIBS}
mmesh::mmesh
REGEX::REGEX
+47 -586
View File
@@ -49,7 +49,6 @@
#include "nmg.h"
#include "rt/geom.h"
#include "rt/primitives/annot.h"
#include "rt/primitives/bot.h"
#include "raytrace.h"
#if defined(__GNUC__) && !defined(__clang__)
@@ -77,46 +76,8 @@
#define ANNOT_PRES_MAGIC 0x414e5032u /* "ANP2" */
#define ANNOT_PRES_VERSION 1u
/* Stored line widths are display-scale multipliers, not model distances.
* Primitive prep has no view pixel scale, so infer a stable model-space base
* width from the annotation content before applying that multiplier. */
#define ANNOT_TEXT_STROKE_RATIO (1.0 / 14.0)
#define ANNOT_GEOMETRY_STROKE_RATIO 0.01
#define ANNOT_MIN_WIDTH_TOL_FACTOR 4.0
struct annot_mesh {
point_t *vertices;
size_t vertex_count;
size_t vertex_capacity;
int *faces;
fastf_t *thickness;
size_t face_count;
size_t face_capacity;
};
C_DECL void rt_annot_ifree(struct rt_db_internal *ip);
/* Annotation prep uses BOT as a transient raytrace representation, as ARS
* does. These callbacks are internal to librt rather than public BOT APIs. */
C_DECL int rt_bot_prep(struct soltab *stp, struct rt_db_internal *ip,
struct rt_i *rtip);
C_DECL int rt_bot_shot(struct soltab *stp, struct xray *rp,
struct application *ap, struct seg *seghead);
C_DECL void rt_bot_print(const struct soltab *stp);
C_DECL void rt_bot_norm(struct hit *hitp, struct soltab *stp,
struct xray *rp);
C_DECL void rt_bot_curve(struct curvature *cvp, struct hit *hitp,
struct soltab *stp);
C_DECL void rt_bot_uv(struct application *ap, struct soltab *stp,
struct hit *hitp, struct uvcoord *uvp);
C_DECL void rt_bot_free(struct soltab *stp);
C_DECL void rt_bot_ifree(struct rt_db_internal *ip);
static int annot_to_bot(struct rt_bot_internal **result,
const struct rt_annot_internal *annot_ip,
const struct bg_tess_tol *ttol, const struct bn_tol *tol,
unsigned char bot_mode);
static uint32_t
annot_get_uint32(const unsigned char *cp)
{
@@ -473,8 +434,9 @@ rt_annot_validate(const struct rt_annot_internal *annot_ip,
annot_validation_message(messages, i, "fill has no compatible outline segments");
ret++;
} else {
for (j = (size_t)fsg->legacy_start;
j < (size_t)(fsg->legacy_start + fsg->legacy_count); ++j)
size_t legacy_end = (size_t)fsg->legacy_start +
(size_t)fsg->legacy_count;
for (j = (size_t)fsg->legacy_start; j < legacy_end; ++j)
if (!ant->segments[j] ||
*(uint32_t *)ant->segments[j] != CURVE_LSEG_MAGIC) {
annot_validation_message(messages, i,
@@ -541,59 +503,25 @@ rt_annot_validate(const struct rt_annot_internal *annot_ip,
C_DECL int
rt_annot_prep(struct soltab *stp, struct rt_db_internal *ip, struct rt_i *rtip)
{
struct bg_tess_tol default_ttol = BG_TESS_TOL_INIT_TOL;
struct bn_tol default_tol = BN_TOL_INIT_TOL;
const struct rt_annot_internal *annot_ip;
struct rt_bot_internal *bot = NULL;
struct rt_db_internal bot_intern;
const struct bg_tess_tol *ttol = &default_ttol;
const struct bn_tol *tol = &default_tol;
int ret;
if (!stp)
return -1;
RT_CK_SOLTAB(stp);
if (!ip)
return -1;
RT_CK_DB_INTERNAL(ip);
if (rtip) {
RT_CK_RTI(rtip);
ttol = &rtip->rti_ttol;
tol = &rtip->rti_tol;
}
if (ip) RT_CK_DB_INTERNAL(ip);
if (rtip) RT_CK_RTI(rtip);
annot_ip = (const struct rt_annot_internal *)ip->idb_ptr;
if (rt_annot_validate(annot_ip, NULL))
if (ip && rt_annot_validate(
(const struct rt_annot_internal *)ip->idb_ptr, NULL))
return -1;
/* View-space annotations need camera state, which primitive prep does not
* have. Leave them non-intersecting until a view-aware prep API exists. */
stp->st_specific = (void *)NULL;
if (!(annot_ip->flags & RT_ANNOT_MODEL_SPACE))
return 0;
if (annot_to_bot(&bot, annot_ip, ttol, tol, RT_BOT_PLATE) || !bot)
return -1;
RT_DB_INTERNAL_INIT(&bot_intern);
bot_intern.idb_major_type = DB5_MAJORTYPE_BRLCAD;
bot_intern.idb_type = ID_BOT;
bot_intern.idb_meth = &OBJ[ID_BOT];
bot_intern.idb_ptr = bot;
ret = rt_bot_prep(stp, &bot_intern, rtip);
rt_bot_ifree(&bot_intern);
return ret;
return 0;
}
C_DECL void
rt_annot_print(const struct soltab *stp)
{
if (!stp)
return;
RT_CK_SOLTAB(stp);
if (stp->st_specific)
rt_bot_print(stp);
if (stp) RT_CK_SOLTAB(stp);
}
@@ -615,18 +543,25 @@ rt_annot_shot(struct soltab *stp, struct xray *rp, struct application *ap, struc
RT_CK_RAY(rp);
RT_CK_APPLICATION(ap);
return stp->st_specific ? rt_bot_shot(stp, rp, ap, seghead) : 0;
/* Annotation coverage is evaluated separately from solid partitions. */
return 0; /* MISS */
}
/**
* Vectorized annotation shooting uses the scalar wrapper so view-space
* annotations without prepared geometry continue to miss safely.
* Vectorized rt_annot_shot(): annotations do not create solid partitions, so
* every ray in the batch misses.
*/
C_DECL void
rt_annot_vshot(struct soltab **stp, struct xray **rp, struct seg *segp, int n, struct application *ap)
rt_annot_vshot(struct soltab **stp, struct xray **UNUSED(rp), struct seg *segp, int n, struct application *ap)
{
rt_vshot_via_shot(rt_annot_shot, stp, rp, segp, n, ap);
int i;
if (ap) RT_CK_APPLICATION(ap);
for (i = 0; i < n; i++) {
if (stp[i] == 0) continue; /* skip this ray */
segp[i].seg_stp = (struct soltab *)0; /* always MISS */
}
}
@@ -643,10 +578,6 @@ rt_annot_norm(struct hit *hitp, struct soltab *stp, struct xray *rp)
if (stp) RT_CK_SOLTAB(stp);
RT_CK_RAY(rp);
if (stp && stp->st_specific) {
rt_bot_norm(hitp, stp, rp);
return;
}
VJOIN1(hitp->hit_point, rp->r_pt, hitp->hit_dist, rp->r_dir);
}
@@ -663,10 +594,6 @@ rt_annot_curve(struct curvature *cvp, struct hit *hitp, struct soltab *stp)
RT_CK_HIT(hitp);
if (stp) RT_CK_SOLTAB(stp);
if (stp && stp->st_specific) {
rt_bot_curve(cvp, hitp, stp);
return;
}
cvp->crv_c1 = cvp->crv_c2 = 0;
bn_vec_ortho(cvp->crv_pdir, hitp->hit_normal);
@@ -681,19 +608,13 @@ rt_annot_uv(struct application *ap, struct soltab *stp, struct hit *hitp, struct
if (hitp) RT_CK_HIT(hitp);
if (!uvp)
return;
if (stp && stp->st_specific)
rt_bot_uv(ap, stp, hitp, uvp);
}
C_DECL void
rt_annot_free(struct soltab *stp)
{
if (!stp)
return;
RT_CK_SOLTAB(stp);
if (stp->st_specific)
rt_bot_free(stp);
if (stp) RT_CK_SOLTAB(stp);
}
@@ -1605,6 +1526,29 @@ seg_to_vlist(struct bu_list *vlfree, struct bu_list *vhead, const struct bg_tess
}
int
rt_annot_segment_vlist(struct bu_list *vlfree, struct bu_list *vhead,
const struct bg_tess_tol *ttol,
const struct rt_annot_internal *annot_ip, size_t segment)
{
struct rt_annot_internal local;
point_t base = VINIT_ZERO;
if (!vlfree || !vhead || !ttol || !annot_ip ||
segment >= annot_ip->ant.count || !annot_ip->ant.segments ||
!annot_ip->ant.segments[segment])
return 1;
RT_ANNOT_CK_MAGIC(annot_ip);
local = *annot_ip;
local.flags = RT_ANNOT_SCREEN_SPACE;
VSETALL(local.V, 0.0);
return seg_to_vlist(vlfree, vhead, ttol, base, &local,
annot_ip->ant.segments[segment], annot_ip->styles ?
&annot_ip->styles[segment] : NULL);
}
static int
annot_is_fill_compatibility_outline(const struct rt_ant *ant, size_t seg_no)
{
@@ -1691,489 +1635,6 @@ ant_to_vlist(struct bu_list *vlfree, struct bu_list *vhead, const struct bg_tess
}
static void
annot_mesh_free(struct annot_mesh *mesh)
{
if (!mesh)
return;
if (mesh->vertices)
bu_free(mesh->vertices, "annotation mesh vertices");
if (mesh->faces)
bu_free(mesh->faces, "annotation mesh faces");
if (mesh->thickness)
bu_free(mesh->thickness, "annotation mesh thickness");
memset(mesh, 0, sizeof(*mesh));
}
static int
annot_mesh_reserve_vertices(struct annot_mesh *mesh, size_t additional)
{
size_t capacity;
size_t required;
if (additional > SIZE_MAX - mesh->vertex_count)
return 1;
required = mesh->vertex_count + additional;
if (required <= mesh->vertex_capacity)
return 0;
capacity = mesh->vertex_capacity ? mesh->vertex_capacity : 64;
while (capacity < required) {
if (capacity > SIZE_MAX / 2) {
capacity = required;
break;
}
capacity *= 2;
}
if (capacity > SIZE_MAX / sizeof(point_t))
return 1;
mesh->vertices = (point_t *)bu_realloc(mesh->vertices,
capacity * sizeof(point_t), "annotation mesh vertices");
mesh->vertex_capacity = capacity;
return 0;
}
static int
annot_mesh_reserve_faces(struct annot_mesh *mesh, size_t additional)
{
size_t capacity;
size_t required;
if (additional > SIZE_MAX - mesh->face_count)
return 1;
required = mesh->face_count + additional;
if (required <= mesh->face_capacity)
return 0;
capacity = mesh->face_capacity ? mesh->face_capacity : 96;
while (capacity < required) {
if (capacity > SIZE_MAX / 2) {
capacity = required;
break;
}
capacity *= 2;
}
if (capacity > SIZE_MAX / (3 * sizeof(int)) ||
capacity > SIZE_MAX / sizeof(fastf_t))
return 1;
mesh->faces = (int *)bu_realloc(mesh->faces,
capacity * 3 * sizeof(int), "annotation mesh faces");
mesh->thickness = (fastf_t *)bu_realloc(mesh->thickness,
capacity * sizeof(fastf_t), "annotation mesh thickness");
mesh->face_capacity = capacity;
return 0;
}
static int
annot_mesh_add_triangle(struct annot_mesh *mesh, const point_t a,
const point_t b, const point_t c, const vect_t normal, fastf_t thickness,
const struct bn_tol *tol)
{
point_t ordered[3];
vect_t ab, ac, bc, face_normal;
size_t base;
if (VINVALID(a) || VINVALID(b) || VINVALID(c)) {
bu_log("annotation mesh contains a non-finite triangle\n");
return 1;
}
VSUB2(ab, b, a);
VSUB2(ac, c, a);
VSUB2(bc, c, b);
VCROSS(face_normal, ab, ac);
if (INVALID(MAGSQ(ab)) || INVALID(MAGSQ(ac)) || INVALID(MAGSQ(bc)) ||
INVALID(MAGSQ(face_normal))) {
bu_log("annotation mesh triangle exceeds the numeric range\n");
return 1;
}
if (MAGNITUDE(face_normal) <= tol->dist_sq)
return 0;
VMOVE(ordered[0], a);
if (VDOT(face_normal, normal) >= 0.0) {
VMOVE(ordered[1], b);
VMOVE(ordered[2], c);
} else {
VMOVE(ordered[1], c);
VMOVE(ordered[2], b);
}
if (!mesh || mesh->vertex_count > (size_t)INT_MAX - 3 ||
annot_mesh_reserve_vertices(mesh, 3) ||
annot_mesh_reserve_faces(mesh, 1))
return 1;
base = mesh->vertex_count;
VMOVE(mesh->vertices[base], ordered[0]);
VMOVE(mesh->vertices[base + 1], ordered[1]);
VMOVE(mesh->vertices[base + 2], ordered[2]);
mesh->vertex_count += 3;
mesh->faces[3 * mesh->face_count] = (int)base;
mesh->faces[3 * mesh->face_count + 1] = (int)base + 1;
mesh->faces[3 * mesh->face_count + 2] = (int)base + 2;
mesh->thickness[mesh->face_count++] = thickness;
return 0;
}
static int
annot_mesh_add_stroke_span(struct annot_mesh *mesh, const point_t start,
const point_t end, const vect_t normal, fastf_t width,
const struct bn_tol *tol)
{
point_t corners[4];
point_t first, last;
vect_t direction, side;
fastf_t half_width;
fastf_t length;
VSUB2(direction, end, start);
length = MAGNITUDE(direction);
if (INVALID(length))
return 1;
if (length <= tol->dist)
return 0;
VSCALE(direction, direction, 1.0 / length);
VCROSS(side, normal, direction);
if (VINVALID(side))
return 1;
if (MAGNITUDE(side) <= SMALL_FASTF)
return 0;
VUNITIZE(side);
half_width = 0.5 * width;
VJOIN1(first, start, -half_width, direction);
VJOIN1(last, end, half_width, direction);
VJOIN1(corners[0], first, -half_width, side);
VJOIN1(corners[1], last, -half_width, side);
VJOIN1(corners[2], last, half_width, side);
VJOIN1(corners[3], first, half_width, side);
if (annot_mesh_add_triangle(mesh, corners[0], corners[1], corners[2],
normal, width, tol))
return 1;
return annot_mesh_add_triangle(mesh, corners[0], corners[2], corners[3],
normal, width, tol);
}
static void
annot_line_pattern(uint32_t pattern, const fastf_t **intervals,
size_t *interval_count)
{
/* Entries alternate mark and gap lengths in stroke-width units. */
static const fastf_t dashed[] = {8.0, 4.0};
static const fastf_t dotted[] = {1.0, 3.0};
static const fastf_t center[] = {8.0, 3.0, 2.0, 3.0};
static const fastf_t phantom[] = {8.0, 3.0, 2.0, 3.0, 2.0, 3.0};
*intervals = NULL;
*interval_count = 0;
switch (pattern) {
case RT_ANNOT_LINE_DASHED:
*intervals = dashed;
*interval_count = sizeof(dashed) / sizeof(dashed[0]);
break;
case RT_ANNOT_LINE_DOTTED:
*intervals = dotted;
*interval_count = sizeof(dotted) / sizeof(dotted[0]);
break;
case RT_ANNOT_LINE_CENTER:
*intervals = center;
*interval_count = sizeof(center) / sizeof(center[0]);
break;
case RT_ANNOT_LINE_PHANTOM:
*intervals = phantom;
*interval_count = sizeof(phantom) / sizeof(phantom[0]);
break;
default:
break;
}
}
static int
annot_mesh_add_stroke(struct annot_mesh *mesh, const point_t start,
const point_t end, const vect_t normal, fastf_t width, uint32_t pattern,
fastf_t *path_distance, const struct bn_tol *tol)
{
const fastf_t *intervals;
size_t interval_count;
vect_t delta;
fastf_t length;
fastf_t period = 0.0;
fastf_t position = 0.0;
fastf_t phase;
size_t interval = 0;
size_t i;
VSUB2(delta, end, start);
length = MAGNITUDE(delta);
if (INVALID(length))
return 1;
if (length <= tol->dist)
return 0;
annot_line_pattern(pattern, &intervals, &interval_count);
if (!interval_count) {
*path_distance += length;
return annot_mesh_add_stroke_span(mesh, start, end, normal, width, tol);
}
for (i = 0; i < interval_count; ++i)
period += intervals[i] * width;
phase = fmod(*path_distance, period);
while (phase >= intervals[interval] * width) {
phase -= intervals[interval] * width;
interval = (interval + 1) % interval_count;
}
while (position < length) {
fastf_t available = intervals[interval] * width - phase;
fastf_t run = available < length - position ? available : length - position;
if (!(interval & 1) && run > tol->dist) {
point_t run_start, run_end;
VJOIN1(run_start, start, position / length, delta);
VJOIN1(run_end, start, (position + run) / length, delta);
if (annot_mesh_add_stroke_span(mesh, run_start, run_end, normal,
width, tol))
return 1;
}
position += run;
phase = 0.0;
interval = (interval + 1) % interval_count;
}
*path_distance += length;
return 0;
}
static fastf_t
annot_default_width(const struct rt_annot_internal *annot_ip,
const struct bn_tol *tol)
{
fastf_t largest_text = 0.0;
fastf_t local_width = 0.0;
fastf_t plane_scale;
point2d_t minimum, maximum;
vect_t normal;
size_t i;
for (i = 0; i < annot_ip->ant.count; ++i) {
if (annot_ip->ant.segments[i] &&
*(uint32_t *)annot_ip->ant.segments[i] == ANN_TSEG_MAGIC) {
const struct txt_seg *text =
(const struct txt_seg *)annot_ip->ant.segments[i];
if (text->txt_size > largest_text)
largest_text = text->txt_size;
}
}
if (largest_text > 0.0) {
local_width = largest_text * ANNOT_TEXT_STROKE_RATIO;
} else if (annot_ip->vert_count) {
V2MOVE(minimum, annot_ip->verts[0]);
V2MOVE(maximum, annot_ip->verts[0]);
for (i = 1; i < annot_ip->vert_count; ++i) {
V_MIN(minimum[X], annot_ip->verts[i][X]);
V_MIN(minimum[Y], annot_ip->verts[i][Y]);
V_MAX(maximum[X], annot_ip->verts[i][X]);
V_MAX(maximum[Y], annot_ip->verts[i][Y]);
}
local_width = hypot(maximum[X] - minimum[X],
maximum[Y] - minimum[Y]) * ANNOT_GEOMETRY_STROKE_RATIO;
}
VCROSS(normal, annot_ip->u_vec, annot_ip->v_vec);
plane_scale = sqrt(MAGNITUDE(normal));
local_width *= plane_scale;
if (local_width < ANNOT_MIN_WIDTH_TOL_FACTOR * tol->dist)
local_width = ANNOT_MIN_WIDTH_TOL_FACTOR * tol->dist;
return local_width;
}
static int
annot_segment_mesh(struct annot_mesh *mesh,
const struct rt_annot_internal *annot_ip, size_t segment,
const struct bg_tess_tol *ttol, const struct bn_tol *tol,
const vect_t normal, fastf_t base_width)
{
struct bu_list vhead;
struct bu_list vlfree;
struct bv_vlist *vp;
const struct rt_annot_seg_style *style = annot_ip->styles ?
&annot_ip->styles[segment] : NULL;
uint32_t magic = *(uint32_t *)annot_ip->ant.segments[segment];
uint32_t pattern = style ? style->line_pattern : RT_ANNOT_LINE_CONTINUOUS;
fastf_t width = base_width;
point_t line_start = VINIT_ZERO;
point_t polygon[3];
fastf_t path_distance = 0.0;
size_t polygon_count = 0;
int have_line_start = 0;
int ret = 0;
if (style && (style->flags & RT_ANNOT_STYLE_WIDTH))
width *= style->line_width;
else if (style && magic == ANN_TSEG_MAGIC &&
(style->flags & RT_ANNOT_STYLE_BOLD))
width *= 2.0;
if (!isfinite(width) || width <= 0.0)
return 1;
BU_LIST_INIT(&vhead);
BU_LIST_INIT(&vlfree);
ret = seg_to_vlist(&vlfree, &vhead, ttol,
(fastf_t *)annot_ip->V, (struct rt_annot_internal *)annot_ip,
annot_ip->ant.segments[segment], style);
if (ret)
goto cleanup;
for (BU_LIST_FOR(vp, bv_vlist, &vhead)) {
size_t i;
for (i = 0; i < vp->nused; ++i) {
switch (vp->cmd[i]) {
case BV_VLIST_LINE_MOVE:
VMOVE(line_start, vp->pt[i]);
have_line_start = 1;
path_distance = 0.0;
break;
case BV_VLIST_LINE_DRAW:
if (have_line_start && annot_mesh_add_stroke(mesh,
line_start, vp->pt[i], normal, width, pattern,
&path_distance, tol)) {
ret = 1;
goto cleanup;
}
VMOVE(line_start, vp->pt[i]);
have_line_start = 1;
break;
case BV_VLIST_POLY_MOVE:
case BV_VLIST_TRI_MOVE:
VMOVE(polygon[0], vp->pt[i]);
polygon_count = 1;
break;
case BV_VLIST_POLY_DRAW:
case BV_VLIST_TRI_DRAW:
if (polygon_count < 3) {
VMOVE(polygon[polygon_count], vp->pt[i]);
polygon_count++;
}
break;
case BV_VLIST_POLY_END:
case BV_VLIST_TRI_END:
if (polygon_count == 3 && annot_mesh_add_triangle(mesh,
polygon[0], polygon[1], polygon[2], normal,
base_width, tol)) {
ret = 1;
goto cleanup;
}
polygon_count = 0;
break;
default:
break;
}
}
}
cleanup:
BV_FREE_VLIST(&vlfree, &vhead);
bv_vlist_cleanup(&vlfree);
return ret;
}
static int
annot_to_bot(struct rt_bot_internal **result,
const struct rt_annot_internal *annot_ip,
const struct bg_tess_tol *ttol, const struct bn_tol *tol,
unsigned char bot_mode)
{
struct annot_mesh mesh = {0};
struct rt_bot_internal *bot;
vect_t normal;
fastf_t base_width;
size_t i;
if (!result)
return 1;
*result = NULL;
if (!annot_ip || !ttol || !tol ||
(bot_mode != RT_BOT_PLATE && bot_mode != RT_BOT_SURFACE) ||
!(annot_ip->flags & RT_ANNOT_MODEL_SPACE))
return 1;
VCROSS(normal, annot_ip->u_vec, annot_ip->v_vec);
if (VINVALID(normal) || MAGNITUDE(normal) <= SMALL_FASTF)
return 1;
VUNITIZE(normal);
base_width = annot_default_width(annot_ip, tol);
if (!isfinite(base_width) || base_width <= 0.0)
return 1;
for (i = 0; i < annot_ip->ant.count; ++i) {
if (!annot_ip->ant.segments[i] ||
annot_is_fill_compatibility_outline(&annot_ip->ant, i))
continue;
if (annot_segment_mesh(&mesh, annot_ip, i, ttol, tol, normal,
base_width)) {
annot_mesh_free(&mesh);
return 1;
}
}
if (!mesh.vertex_count || !mesh.face_count) {
annot_mesh_free(&mesh);
return 1;
}
BU_ALLOC(bot, struct rt_bot_internal);
bot->magic = RT_BOT_INTERNAL_MAGIC;
bot->mode = bot_mode;
bot->orientation = RT_BOT_UNORIENTED;
bot->num_vertices = mesh.vertex_count;
bot->vertices = (fastf_t *)mesh.vertices;
bot->num_faces = mesh.face_count;
bot->faces = mesh.faces;
if (bot_mode == RT_BOT_PLATE)
bot->thickness = mesh.thickness;
else
bu_free(mesh.thickness, "annotation mesh thickness");
mesh.vertices = NULL;
mesh.faces = NULL;
mesh.thickness = NULL;
*result = bot;
return 0;
}
C_DECL int
rt_annot_tess(struct nmgregion **r, struct model *m,
struct rt_db_internal *ip, const struct bg_tess_tol *ttol,
const struct bn_tol *tol)
{
struct rt_bot_internal *bot = NULL;
struct rt_db_internal bot_intern;
const struct rt_annot_internal *annot_ip;
int ret;
if (!r || !m || !ip || !ttol || !tol)
return -1;
RT_CK_DB_INTERNAL(ip);
annot_ip = (const struct rt_annot_internal *)ip->idb_ptr;
if (rt_annot_validate(annot_ip, NULL) ||
annot_to_bot(&bot, annot_ip, ttol, tol, RT_BOT_SURFACE))
return -1;
RT_DB_INTERNAL_INIT(&bot_intern);
bot_intern.idb_major_type = DB5_MAJORTYPE_BRLCAD;
bot_intern.idb_type = ID_BOT;
bot_intern.idb_meth = &OBJ[ID_BOT];
bot_intern.idb_ptr = bot;
ret = rt_bot_tess(r, m, &bot_intern, ttol, tol);
rt_bot_ifree(&bot_intern);
return ret;
}
C_DECL int
rt_annot_plot(struct bu_list *vhead, struct rt_db_internal *ip, const struct bg_tess_tol *ttol, const struct bn_tol *UNUSED(tol), const struct bview *UNUSED(info))
{
@@ -4066,11 +3527,11 @@ rt_annot_adjust(struct bu_vls *logstr, struct rt_db_internal *intern, int argc,
fastf_t *new_vert;
vert_no = atol(argv[0] + 1);
new_vert = annot_ip->verts[vert_no];
if (vert_no < 0 || (size_t)vert_no > annot_ip->vert_count) {
if (vert_no < 0 || (size_t)vert_no >= annot_ip->vert_count) {
bu_vls_printf(logstr, "ERROR: Illegal vertex number\n");
return BRLCAD_ERROR;
}
new_vert = annot_ip->verts[vert_no];
array_len = 2;
if (_rt_tcl_list_to_fastf_array(argv[1], &new_vert, &array_len) != array_len) {
bu_vls_printf(logstr, "ERROR: Incorrect number of coordinates for vertex\n");
File diff suppressed because it is too large Load Diff
+147 -32
View File
@@ -514,6 +514,11 @@ rt_bot_prep(struct soltab *stp, struct rt_db_internal *ip, struct rt_i *rtip)
bot->bot_mode = bot_ip->mode;
bot->bot_orientation = bot_ip->orientation;
bot->bot_flags = bot_ip->bot_flags;
/* Experimental: assume a closed, non-self-intersecting solid. The
* caller accepts that parity can give false interiors on bad meshes. */
const char *parity_shot = getenv("LIBRT_BOT_PARITY_SHOT");
bot->bot_parity_shot = (bot->bot_mode == RT_BOT_SOLID &&
parity_shot && bu_str_true(parity_shot));
bot->bot_ntri = 0; // set after validating faces
// set up thickness if requested
@@ -727,8 +732,66 @@ bot_makesegs_specific(hit_da *hits,
struct rt_piecestate *psp);
static inline int
bot_origin_in_bounds(const struct xray *rp, const struct bvh_flat_node *root)
{
for (int axis = X; axis <= Z; axis++) {
if (rp->r_pt[axis] < root->bounds[axis] ||
rp->r_pt[axis] > root->bounds[axis + 3])
return 0;
}
return 1;
}
/* r_min can describe a spatial partition cell that begins inside this
* solid. Start BVH traversal just before the ray enters the BoT's own
* root box so entrance triangles behind the ray origin remain visible. */
static inline void
bot_backed_origin(point_t b_pt, const struct xray *rp, const struct bvh_flat_node *root,
const vect_t inv_dir, const struct soltab *stp)
{
fastf_t backout = MAX_FASTF;
int axis;
if (!bot_origin_in_bounds(rp, root)) {
VMOVE(b_pt, rp->r_pt);
return;
}
for (axis = X; axis <= Z; axis++) {
fastf_t distance;
if (rp->r_dir[axis] > 0.0)
distance = (rp->r_pt[axis] - root->bounds[axis]) * inv_dir[axis];
else if (rp->r_dir[axis] < 0.0)
distance = (root->bounds[axis + 3] - rp->r_pt[axis]) * -inv_dir[axis];
else
continue;
backout = FMIN(backout, distance);
}
if (backout < MAX_FASTF) {
fastf_t padding = stp->st_rtip ? stp->st_rtip->rti_tol.dist : BN_TOL_DIST;
VJOIN1(b_pt, rp->r_pt, -(backout + padding), rp->r_dir);
} else {
VMOVE(b_pt, rp->r_pt);
}
}
static inline void
bot_forward_hits(hit_da *hits)
{
size_t kept = 0;
for (size_t i = 0; i < hits->count; i++) {
if (hits->items[i].hit_dist >= 0.0)
hits->items[kept++] = hits->items[i];
}
hits->count = kept;
}
void
bot_shot_hlbvh_flat(struct bvh_flat_node *root, struct xray* rp, triangle_s *tris, size_t ntris, hit_da* hits, fastf_t toldist)
bot_shot_hlbvh_flat(struct bvh_flat_node *root, struct xray* rp, triangle_s *tris, size_t ntris, hit_da* hits, fastf_t toldist, const struct soltab *stp)
{
struct bvh_flat_node *stack_node[HLBVH_STACK_SIZE];
unsigned char stack_child_index[HLBVH_STACK_SIZE];
@@ -744,25 +807,12 @@ bot_shot_hlbvh_flat(struct bvh_flat_node *root, struct xray* rp, triangle_s *tri
inverse_r_dir[1] = RAYDIR_INV(rp->r_dir[1]);
inverse_r_dir[2] = RAYDIR_INV(rp->r_dir[2]);
// Because we are doing solid shotlining, we need to intersect all
// triangles on the line of the ray, even when the r_pt is inside the mesh.
// This means we DON'T want to cull hlbvh leaves behind our r_pt when the
// r_pt is inside the mesh. To avoid this, we back our r_pt up to the
// bounding sphere if needed. This incurs a slight
// performance penalty when r_pt is past the mesh but still within the
// bounding sphere diameter distance, since backing up will cause false
// bbox intersect matches in those cases. This is necessary to ensure we
// get the intersections needed for solid segment creation - the subsequent
// full intersection solves will still produce the correct results.
//
// NOTE: We DO, however, need to use the real rp->r_pt when doing the
// actual intersection solve math so our segments end up in the right
// place.
fastf_t backout = FMAX(0.0, -rp->r_min);
point_t b_pt;
b_pt[X] = rp->r_pt[X] - backout * rp->r_dir[X];
b_pt[Y] = rp->r_pt[Y] - backout * rp->r_dir[Y];
b_pt[Z] = rp->r_pt[Z] - backout * rp->r_dir[Z];
const struct bot_specific *bot = (const struct bot_specific *)stp->st_specific;
if (bot->bot_parity_shot)
VMOVE(b_pt, rp->r_pt);
else
bot_backed_origin(b_pt, rp, root, inverse_r_dir, stp);
while (stack_ind >= 0) {
if (UNLIKELY(stack_ind >= HLBVH_STACK_SIZE)) {
@@ -924,7 +974,9 @@ rt_bot_shot_specific(struct bot_specific *bot, struct soltab *stp, struct xray *
toldist = (DBL_EPSILON * stp->st_aradius * 10);
}
bot_shot_hlbvh_flat(sps->root, rp, sps->tris, bot->bot_ntri, &hits_per_cpu, toldist);
bot_shot_hlbvh_flat(sps->root, rp, sps->tris, bot->bot_ntri, &hits_per_cpu, toldist, stp);
if (bot->bot_parity_shot)
bot_forward_hits(&hits_per_cpu);
if (hits_per_cpu.count == 0) {
return 0;
@@ -1026,14 +1078,13 @@ bot_vshot_packet(struct soltab *stp, struct bot_specific *bot,
/* Per-ray setup: inverse dir, backed-up origin, reset hit list. */
for (k = 0; k < m; k++) {
fastf_t backout;
inv_dir[k][0] = bot_vshot_rdinv(rp[k]->r_dir[0]);
inv_dir[k][1] = bot_vshot_rdinv(rp[k]->r_dir[1]);
inv_dir[k][2] = bot_vshot_rdinv(rp[k]->r_dir[2]);
backout = FMAX(0.0, -rp[k]->r_min);
b_pt[k][X] = rp[k]->r_pt[X] - backout * rp[k]->r_dir[X];
b_pt[k][Y] = rp[k]->r_pt[Y] - backout * rp[k]->r_dir[Y];
b_pt[k][Z] = rp[k]->r_pt[Z] - backout * rp[k]->r_dir[Z];
if (bot->bot_parity_shot)
VMOVE(b_pt[k], rp[k]->r_pt);
else
bot_backed_origin(b_pt[k], rp[k], sps->root, inv_dir[k], stp);
vhits[k].count = 0;
segp[k].seg_stp = (struct soltab *)0;
}
@@ -1133,6 +1184,8 @@ bot_vshot_packet(struct soltab *stp, struct bot_specific *bot,
/* Per-ray: sort hits, build segments, collapse to the outer span. */
for (k = 0; k < m; k++) {
if (bot->bot_parity_shot)
bot_forward_hits(&vhits[k]);
size_t nhits = vhits[k].count;
struct hit *hh = vhits[k].items;
struct seg seghd;
@@ -1255,6 +1308,10 @@ rt_bot_norm(struct hit *hitp, struct soltab *stp, struct xray *rp)
triangle_s *trip=(triangle_s *)hitp->hit_private;
VJOIN1(hitp->hit_point, rp->r_pt, hitp->hit_dist, rp->r_dir);
if (hitp->hit_surfno == RT_BOT_SURFNO_INTERIOR) {
VREVERSE(hitp->hit_normal, rp->r_dir);
return;
}
VMOVE(old_norm, hitp->hit_normal);
if ((bot->bot_flags & RT_BOT_HAS_SURFACE_NORMALS) && (bot->bot_flags & RT_BOT_USE_NORMALS) && trip->norms) {
@@ -1505,13 +1562,38 @@ rt_bot_surface_segs(struct hit *hits, size_t nhits, struct soltab *stp, struct a
}
/* The parity experiment represents an interior ray start explicitly. This
* hit has no triangle: its surface number records why its normal is -dir. */
static void
bot_interior_segment(struct soltab *stp, struct xray *rp, struct application *ap,
struct seg *seghead, const struct hit *exit_hit)
{
struct seg *segp;
triangle_s *trip = (triangle_s *)exit_hit->hit_private;
static const int OUT_SEG = 1;
RT_GET_SEG(segp, ap->a_resource);
segp->seg_stp = stp;
segp->seg_in = (struct hit)RT_HIT_INIT_ZERO;
segp->seg_in.hit_surfno = RT_BOT_SURFNO_INTERIOR;
segp->seg_in.hit_private = NULL;
segp->seg_in.hit_rayp = rp;
segp->seg_in.hit_vpriv[X] = -1.0;
VREVERSE(segp->seg_in.hit_normal, rp->r_dir);
VMOVE(segp->seg_in.hit_point, rp->r_pt);
segp->seg_out = *exit_hit;
BOT_UNORIENTED_NORM(ap, &segp->seg_out, trip->face_norm, OUT_SEG);
BU_LIST_INSERT(&(seghead->l), &(segp->l));
}
static int
rt_bot_unoriented_segs(struct hit *hits,
size_t nhits,
struct soltab *stp,
struct xray *rp,
struct application *ap,
struct seg *seghead)
struct seg *seghead,
int parity_origin)
{
register struct seg *segp;
register size_t i, j;
@@ -1521,12 +1603,17 @@ rt_bot_unoriented_segs(struct hit *hits,
*/
fastf_t rm_dist = 0.0;
int removed = 0;
int synthetic_hits = 0;
static const int IN_SEG = 0;
static const int OUT_SEG = 1;
const fastf_t toldist = (ap && ap->a_rt_i)?ap->a_rt_i->rti_tol.dist:BN_TOL_DIST;
if (nhits == 1) {
triangle_s *trip = (triangle_s *)hits[0].hit_private;
if (parity_origin) {
bot_interior_segment(stp, rp, ap, seghead, &hits[0]);
return 2;
}
/* make a zero length partition */
RT_GET_SEG(segp, ap->a_resource);
@@ -1559,8 +1646,13 @@ rt_bot_unoriented_segs(struct hit *hits,
}
}
if (nhits == 1)
if (nhits == 1) {
if (parity_origin) {
bot_interior_segment(stp, rp, ap, seghead, &hits[0]);
return 2;
}
return 0;
}
if (nhits&1 && removed) {
/* If we have an odd number of hits and have removed a
@@ -1578,6 +1670,14 @@ rt_bot_unoriented_segs(struct hit *hits,
}
}
if ((nhits & 1) && parity_origin) {
bot_interior_segment(stp, rp, ap, seghead, &hits[0]);
for (j = 1; j < nhits; j++)
hits[j-1] = hits[j];
nhits--;
synthetic_hits = 2;
}
for (i = 0; i < (nhits&~1); i += 2) {
triangle_s *trip = (triangle_s *)hits[i].hit_private;
@@ -1604,12 +1704,12 @@ rt_bot_unoriented_segs(struct hit *hits,
}
nhits--;
}
return nhits;
return nhits + synthetic_hits;
}
int
rt_bot_oriented_segs(hit_da *hits_da, struct soltab *stp, struct application *ap, struct seg *seghead, struct rt_piecestate *psp)
rt_bot_oriented_segs(hit_da *hits_da, struct soltab *stp, struct xray *rp, struct application *ap, struct seg *seghead, struct rt_piecestate *psp, int parity_origin)
{
register struct seg *segp;
register ssize_t i;
@@ -1618,6 +1718,7 @@ rt_bot_oriented_segs(hit_da *hits_da, struct soltab *stp, struct application *ap
/* TODO: review the use of a signed tmp var. Var i was changed to be signed in
* r44239 as a bug in another project was segfaulting. */
ssize_t snhits = (ssize_t)hits_da->count;
int synthetic_hits = 0;
struct hit *hits = hits_da->items;
const fastf_t toldist = (ap && ap->a_rt_i)?ap->a_rt_i->rti_tol.dist:BN_TOL_DIST;
@@ -1891,6 +1992,18 @@ rt_bot_oriented_segs(hit_da *hits_da, struct soltab *stp, struct application *ap
*/
}
/* A valid closed solid with an interior origin has an unmatched first
* exit in the forward hit list. Construct its entry after duplicate
* and grazing-hit cleanup, before the normal stray-exit handling. */
if (parity_origin && (snhits & 1) && hits[0].hit_vpriv[X] > 0.0) {
ssize_t j;
bot_interior_segment(stp, rp, ap, seghead, &hits[0]);
for (j = 1; j < snhits; j++)
hits[j-1] = hits[j];
snhits--;
synthetic_hits = 2;
}
/* if first hit is an exit, it is likely due to the "piece" for
* the corresponding entrance not being processed (this is OK, but
* we need to eliminate the stray exit hit)
@@ -1988,7 +2101,7 @@ rt_bot_oriented_segs(hit_da *hits_da, struct soltab *stp, struct application *ap
BU_LIST_INSERT(&(seghead->l), &(segp->l));
}
return snhits; /* HIT */
return snhits + synthetic_hits; /* HIT */
}
/**
* Given an array of hits, make sebgents out of them. Exactly how
@@ -2010,11 +2123,13 @@ bot_makesegs_specific(hit_da *hits, struct bot_specific *bot, struct soltab *stp
BU_ASSERT(bot->bot_mode == RT_BOT_SOLID);
int parity_origin = bot->bot_parity_shot &&
bot_origin_in_bounds(rp, ((struct spatial_partition_s *)bot->tie)->root);
if (bot->bot_orientation == RT_BOT_UNORIENTED) {
return rt_bot_unoriented_segs(hits->items, hits->count, stp, rp, ap, seghead);
return rt_bot_unoriented_segs(hits->items, hits->count, stp, rp, ap, seghead, parity_origin);
}
return rt_bot_oriented_segs(hits, stp, ap, seghead, psp);
return rt_bot_oriented_segs(hits, stp, rp, ap, seghead, psp, parity_origin);
}
+469 -203
View File
@@ -45,24 +45,28 @@
* intersecting closed surface), and total_chord is the sum of solid
* segment lengths.
*
* References:
* Li et al. (2003), "Using low-discrepancy sequences and the Crofton
* formula to compute surface areas of geometric models",
* Computer-Aided Design 35, 771-782.
*
* Liu et al. (2010), "Surface area estimation of digitized 3D objects
* using quasi-Monte Carlo methods", Pattern Recognition 43, 3900-3909.
* Chord endpoints are independent pseudorandom points on the bounding
* sphere. Reinitializing the generator with a fixed seed makes sampling
* reproducible without changing the ordinary Monte Carlo convergence
* behavior on which the estimator's stability checks rely.
*/
#include "common.h"
#include <chrono>
#include <climits>
#include <condition_variable>
#include <cstdint>
#include <exception>
#include <mutex>
#include <random>
#include <stdlib.h>
#include <math.h>
#include <random>
#include <stdexcept>
#include <string.h>
#include <thread>
#include <time.h>
#include <vector>
#include "vmath.h"
#include "bu/log.h"
@@ -100,8 +104,15 @@
#define RT_CROFTON_STABILITY_MIN_WINDOWS 2u
/** Fixed seed for reproducible estimates in tests and cross-platform runs. */
#define RT_CROFTON_RNG_SEED 0x9e3779b97f4a7c15ULL
#define RT_CROFTON_RNG_SEED UINT64_C(0x9e3779b97f4a7c15)
/** Two sphere coordinates for each of two chord endpoints. */
#define RT_CROFTON_RANDOM_VALUES_PER_RAY 4u
/** Convert the high 53 random bits exactly to a double in [0, 1). */
static constexpr unsigned int CROFTON_RANDOM_SHIFT = 11u;
static constexpr double CROFTON_RANDOM_SCALE =
1.0 / 9007199254740992.0;
/* ------------------------------------------------------------------ */
/* Internal types */
@@ -110,6 +121,7 @@
struct crofton_ray {
point_t r_pt;
vect_t r_dir;
size_t id;
};
struct crofton_shared {
@@ -117,14 +129,19 @@ struct crofton_shared {
size_t total_crossings; /* in+out hit events */
double total_chord; /* solid segment length sum (mm) */
size_t total_rays; /* rays fired (hits + misses) */
size_t ray_offset; /* first identifier in this sample stream */
point_t *points;
size_t point_count;
size_t point_capacity;
struct rt_crofton_segment *segments;
size_t segment_count;
size_t segment_capacity;
bool collect_points;
bool collect_segments;
};
struct crofton_worker_data {
struct application *ap; /* per-CPU application struct */
struct application ap; /* per-CPU application struct */
struct crofton_ray *rays; /* shared ray array (read-only) */
size_t start;
size_t end;
@@ -135,6 +152,10 @@ struct crofton_worker_data {
point_t *local_points;
size_t local_point_count;
size_t local_point_capacity;
struct rt_crofton_segment *local_segments;
size_t local_segment_count;
size_t local_segment_capacity;
size_t current_ray;
};
@@ -143,46 +164,88 @@ struct crofton_worker_data {
/* ------------------------------------------------------------------ */
static int
crofton_hit(struct application *ap, struct partition *PartHeadp, struct seg *UNUSED(segs))
crofton_hit(struct application *ap, struct partition *PartHeadp,
struct seg *UNUSED(segs))
{
struct crofton_worker_data *wd = (struct crofton_worker_data *)ap->a_uptr;
struct crofton_worker_data *worker =
(struct crofton_worker_data *)ap->a_uptr;
size_t crossings = 0;
double chord = 0.0;
double chord = 0.0;
struct partition *pp;
for (pp = PartHeadp->pt_forw; pp != PartHeadp; pp = pp->pt_forw) {
/* Each partition contributes an in-hit and an out-hit */
crossings += 2;
chord += pp->pt_outhit->hit_dist - pp->pt_inhit->hit_dist;
if (wd->shared->collect_points) {
if (wd->local_point_count + 2 > wd->local_point_capacity) {
const size_t initial_capacity = 1024;
size_t new_capacity = wd->local_point_capacity ?
2 * wd->local_point_capacity : initial_capacity;
while (new_capacity < wd->local_point_count + 2)
new_capacity *= 2;
wd->local_points = (point_t *)bu_realloc(wd->local_points,
new_capacity * sizeof(point_t), "Crofton worker hit points");
wd->local_point_capacity = new_capacity;
}
VJOIN1(wd->local_points[wd->local_point_count], ap->a_ray.r_pt,
pp->pt_inhit->hit_dist, ap->a_ray.r_dir);
wd->local_point_count++;
VJOIN1(wd->local_points[wd->local_point_count], ap->a_ray.r_pt,
pp->pt_outhit->hit_dist, ap->a_ray.r_dir);
wd->local_point_count++;
}
struct partition *partition;
for (partition = PartHeadp->pt_forw; partition != PartHeadp;
partition = partition->pt_forw) {
fastf_t thickness = partition->pt_outhit->hit_dist -
partition->pt_inhit->hit_dist;
crossings += 2;
chord += thickness;
if (worker->shared->collect_points) {
if (worker->local_point_count + 2 >
worker->local_point_capacity) {
const size_t initial_capacity = 1024;
size_t new_capacity = worker->local_point_capacity ?
2 * worker->local_point_capacity : initial_capacity;
while (new_capacity < worker->local_point_count + 2)
new_capacity *= 2;
worker->local_points = (point_t *)bu_realloc(
worker->local_points,
new_capacity * sizeof(point_t),
"Crofton worker hit points");
worker->local_point_capacity = new_capacity;
}
VJOIN1(worker->local_points[worker->local_point_count],
ap->a_ray.r_pt, partition->pt_inhit->hit_dist,
ap->a_ray.r_dir);
worker->local_point_count++;
VJOIN1(worker->local_points[worker->local_point_count],
ap->a_ray.r_pt, partition->pt_outhit->hit_dist,
ap->a_ray.r_dir);
worker->local_point_count++;
}
if (worker->shared->collect_segments) {
if (worker->local_segment_count + 1 >
worker->local_segment_capacity) {
const size_t initial_capacity = 512;
size_t new_capacity = worker->local_segment_capacity ?
2 * worker->local_segment_capacity : initial_capacity;
worker->local_segments =
(struct rt_crofton_segment *)bu_realloc(
worker->local_segments,
new_capacity * sizeof(struct rt_crofton_segment),
"Crofton worker segments");
worker->local_segment_capacity = new_capacity;
}
struct rt_crofton_segment *sample =
&worker->local_segments[worker->local_segment_count];
struct soltab *in_solid =
partition->pt_inseg->seg_stp;
struct soltab *out_solid =
partition->pt_outseg->seg_stp;
VJOIN1(sample->in_point, ap->a_ray.r_pt,
partition->pt_inhit->hit_dist, ap->a_ray.r_dir);
VJOIN1(sample->out_point, ap->a_ray.r_pt,
partition->pt_outhit->hit_dist, ap->a_ray.r_dir);
RT_HIT_NORMAL(sample->in_normal, partition->pt_inhit,
in_solid, &ap->a_ray, partition->pt_inflip);
RT_HIT_NORMAL(sample->out_normal, partition->pt_outhit,
out_solid, &ap->a_ray, partition->pt_outflip);
sample->thickness = thickness;
sample->ray_id = worker->rays[worker->current_ray].id;
if (!VNEAR_ZERO(sample->in_normal, VUNITIZE_TOL) &&
!VNEAR_ZERO(sample->out_normal, VUNITIZE_TOL))
worker->local_segment_count++;
}
}
wd->local_crossings += crossings;
wd->local_chord += chord;
wd->local_rays += 1;
worker->local_crossings += crossings;
worker->local_chord += chord;
worker->local_rays++;
return 1;
}
static int
crofton_miss(struct application *ap)
{
@@ -193,159 +256,273 @@ crofton_miss(struct application *ap)
}
/* ------------------------------------------------------------------ */
/* Parallel worker */
/* ------------------------------------------------------------------ */
static void
crofton_worker(int id, void *data)
{
struct crofton_worker_data *wd = &((struct crofton_worker_data *)data)[id - 1];
struct application *ap = wd->ap;
struct crofton_ray *rays = wd->rays;
for (size_t i = wd->start; i < wd->end; i++) {
VMOVE(ap->a_ray.r_pt, rays[i].r_pt);
VMOVE(ap->a_ray.r_dir, rays[i].r_dir);
/* r_dir is already unit-length (set during ray generation) */
rt_shootray(ap);
}
}
/* ------------------------------------------------------------------ */
/* Point / ray generation */
/* ------------------------------------------------------------------ */
static double
crofton_rand01(std::mt19937_64 &rng)
crofton_random01(std::mt19937_64 &rng)
{
return std::generate_canonical<double, 53>(rng);
return static_cast<double>(rng() >> CROFTON_RANDOM_SHIFT) *
CROFTON_RANDOM_SCALE;
}
static void
random_point_on_sphere(double radius, const point_t center, point_t out, std::mt19937_64 &rng)
random_point_on_sphere(double radius, const point_t center, double u,
double v, point_t point)
{
double theta = 2.0 * M_PI * crofton_rand01(rng);
double phi = acos(2.0 * crofton_rand01(rng) - 1.0);
double sp = sin(phi);
out[X] = center[X] + radius * sp * cos(theta);
out[Y] = center[Y] + radius * sp * sin(theta);
out[Z] = center[Z] + radius * cos(phi);
double z = 1.0 - 2.0 * u;
double radial = sqrt(std::max(0.0, 1.0 - z * z));
double angle = 2.0 * M_PI * v;
point[X] = center[X] + radius * radial * cos(angle);
point[Y] = center[Y] + radius * radial * sin(angle);
point[Z] = center[Z] + radius * z;
}
/**
* Generate @p nrays chord rays from 2*nrays random points on the
* bounding sphere. Each ray connects one randomly chosen point to
* another.
* Generate independent random chord endpoints. Pairing consecutive IID
* sphere points has the same distribution as shuffling a point array before
* pairing, but consumes exactly four random values per ray. That fixed
* stride lets callers continue the deterministic stream with ray_offset.
*/
static void
generate_rays(struct crofton_ray *rays, size_t nrays,
double radius, const point_t center, std::mt19937_64 &rng)
generate_rays(struct crofton_ray *rays, size_t ray_count, size_t first_id,
double radius, const point_t center, std::mt19937_64 &rng)
{
size_t npts = nrays * 2;
point_t *pts = (point_t *)bu_calloc(npts, sizeof(point_t), "crofton pts");
for (size_t i = 0; i < npts; i++)
random_point_on_sphere(radius, center, pts[i], rng);
/* Shuffle so pairing is random */
for (size_t i = npts - 1; i > 0; i--) {
size_t j = (size_t)(crofton_rand01(rng) * (i + 1));
if (j > i) j = i;
point_t tmp;
VMOVE(tmp, pts[i]);
VMOVE(pts[i], pts[j]);
VMOVE(pts[j], tmp);
for (size_t i = 0; i < ray_count; i++) {
point_t endpoint;
random_point_on_sphere(radius, center, crofton_random01(rng),
crofton_random01(rng), rays[i].r_pt);
random_point_on_sphere(radius, center, crofton_random01(rng),
crofton_random01(rng), endpoint);
VSUB2(rays[i].r_dir, endpoint, rays[i].r_pt);
VUNITIZE(rays[i].r_dir);
rays[i].id = first_id + i;
}
for (size_t i = 0; i < nrays; i++) {
VMOVE(rays[i].r_pt, pts[i * 2]);
VSUB2(rays[i].r_dir, pts[i * 2 + 1], pts[i * 2]);
VUNITIZE(rays[i].r_dir);
}
bu_free(pts, "crofton pts");
}
/* ------------------------------------------------------------------ */
/* Reusable parallel workers */
/* ------------------------------------------------------------------ */
class CroftonWorkerPool
{
public:
CroftonWorkerPool(const struct application *ap_template,
struct resource *resources, size_t worker_count) :
count(worker_count), data(worker_count)
{
for (size_t i = 0; i < count; i++) {
data[i].ap = *ap_template;
data[i].ap.a_resource = &resources[i];
data[i].shared = NULL;
data[i].ap.a_uptr = &data[i];
}
try {
executor = std::thread(&CroftonWorkerPool::run_workers, this);
} catch (...) {
free_worker_points();
throw;
}
std::unique_lock<std::mutex> lock(mutex);
if (!workers_started.wait_for(lock, worker_start_timeout,
[this]() { return registered == count; })) {
stopping = true;
work_ready.notify_all();
lock.unlock();
executor.join();
free_worker_points();
throw std::runtime_error("worker startup timed out");
}
}
~CroftonWorkerPool()
{
stop_and_join();
free_worker_points();
}
CroftonWorkerPool(const CroftonWorkerPool &) = delete;
CroftonWorkerPool &operator=(const CroftonWorkerPool &) = delete;
void run(struct crofton_ray *rays, size_t nrays,
struct crofton_shared *shared)
{
const size_t rays_per_worker = nrays / count;
std::unique_lock<std::mutex> lock(mutex);
for (size_t i = 0; i < count; i++) {
struct crofton_worker_data &wd = data[i];
wd.rays = rays;
wd.start = i * rays_per_worker;
wd.end = (i == count - 1) ? nrays : (i + 1) * rays_per_worker;
wd.shared = shared;
wd.local_crossings = 0;
wd.local_chord = 0.0;
wd.local_rays = 0;
wd.local_point_count = 0;
wd.local_segment_count = 0;
}
completed = 0;
generation++;
work_ready.notify_all();
work_done.wait(lock, [this]() { return completed == count; });
}
const std::vector<struct crofton_worker_data> &worker_data() const
{
return data;
}
private:
/* Native thread creation should complete promptly. A bounded startup wait
* turns partial bu_parallel startup into a reported failure, not a hang. */
static constexpr std::chrono::seconds worker_start_timeout{5};
static void worker_entry(int UNUSED(id), void *context)
{
CroftonWorkerPool *pool = static_cast<CroftonWorkerPool *>(context);
size_t slot = 0;
{
std::lock_guard<std::mutex> lock(pool->mutex);
slot = pool->registered++;
if (pool->registered == pool->count)
pool->workers_started.notify_one();
}
pool->worker(slot);
}
void run_workers()
{
bu_parallel(worker_entry, count, this);
}
void worker(size_t slot)
{
size_t observed_generation = 0;
std::unique_lock<std::mutex> lock(mutex);
while (true) {
work_ready.wait(lock, [this, observed_generation]() {
return stopping || generation != observed_generation;
});
if (stopping)
return;
observed_generation = generation;
struct crofton_worker_data *wd = &data[slot];
lock.unlock();
for (size_t i = wd->start; i < wd->end; i++) {
wd->current_ray = i;
VMOVE(wd->ap.a_ray.r_pt, wd->rays[i].r_pt);
VMOVE(wd->ap.a_ray.r_dir, wd->rays[i].r_dir);
rt_shootray(&wd->ap);
}
lock.lock();
completed++;
if (completed == count)
work_done.notify_one();
}
}
void stop_and_join()
{
{
std::lock_guard<std::mutex> lock(mutex);
stopping = true;
work_ready.notify_all();
}
if (executor.joinable())
executor.join();
}
void free_worker_points()
{
for (struct crofton_worker_data &wd : data) {
if (wd.local_points)
bu_free(wd.local_points, "Crofton worker hit points");
if (wd.local_segments)
bu_free(wd.local_segments, "Crofton worker segments");
wd.local_points = NULL;
wd.local_segments = NULL;
}
}
size_t count;
std::vector<struct crofton_worker_data> data;
std::thread executor;
std::mutex mutex;
std::condition_variable workers_started;
std::condition_variable work_ready;
std::condition_variable work_done;
size_t registered = 0;
size_t generation = 0;
size_t completed = 0;
bool stopping = false;
};
/* ------------------------------------------------------------------ */
/* One iteration: fire nrays, accumulate into shared */
/* ------------------------------------------------------------------ */
static void
do_one_iteration(struct application *ap_template,
struct resource *resources,
size_t nrays,
double radius,
const point_t center,
struct crofton_shared *shared,
std::mt19937_64 &rng)
do_one_iteration(CroftonWorkerPool &pool, size_t ray_count, double radius,
const point_t center, struct crofton_shared *shared,
std::mt19937_64 &rng)
{
struct crofton_ray *rays = (struct crofton_ray *)bu_calloc(
nrays, sizeof(struct crofton_ray), "crofton rays");
ray_count, sizeof(struct crofton_ray), "Crofton rays");
generate_rays(rays, ray_count, shared->ray_offset + shared->total_rays,
radius, center, rng);
pool.run(rays, ray_count, shared);
generate_rays(rays, nrays, radius, center, rng);
for (const struct crofton_worker_data &worker : pool.worker_data()) {
shared->total_crossings += worker.local_crossings;
shared->total_chord += worker.local_chord;
shared->total_rays += worker.local_rays;
size_t ncpus = bu_avail_cpus();
if (ncpus < 1) ncpus = 1;
if (ncpus > MAX_PSW) ncpus = MAX_PSW;
if (worker.local_point_count) {
const size_t needed =
shared->point_count + worker.local_point_count;
if (needed > shared->point_capacity) {
size_t new_capacity = shared->point_capacity ?
2 * shared->point_capacity : needed;
if (new_capacity < needed)
new_capacity = needed;
shared->points = (point_t *)bu_realloc(shared->points,
new_capacity * sizeof(point_t),
"Crofton hit points");
shared->point_capacity = new_capacity;
}
memcpy(&shared->points[shared->point_count],
worker.local_points,
worker.local_point_count * sizeof(point_t));
shared->point_count = needed;
}
struct crofton_worker_data *wdata = (struct crofton_worker_data *)bu_calloc(
ncpus, sizeof(struct crofton_worker_data), "crofton wdata");
size_t per_cpu = nrays / ncpus;
for (size_t i = 0; i < ncpus; i++) {
struct application *a = (struct application *)bu_calloc(
1, sizeof(struct application), "crofton app");
*a = *ap_template; /* struct copy */
a->a_resource = &resources[i];
struct crofton_worker_data *wd = &wdata[i];
wd->ap = a;
wd->rays = rays;
wd->start = i * per_cpu;
wd->end = (i == ncpus - 1) ? nrays : (i + 1) * per_cpu;
wd->shared = shared;
a->a_uptr = wd;
if (worker.local_segment_count) {
const size_t needed =
shared->segment_count + worker.local_segment_count;
if (needed > shared->segment_capacity) {
size_t new_capacity = shared->segment_capacity ?
2 * shared->segment_capacity : needed;
if (new_capacity < needed)
new_capacity = needed;
shared->segments =
(struct rt_crofton_segment *)bu_realloc(
shared->segments,
new_capacity * sizeof(struct rt_crofton_segment),
"Crofton segments");
shared->segment_capacity = new_capacity;
}
memcpy(&shared->segments[shared->segment_count],
worker.local_segments,
worker.local_segment_count *
sizeof(struct rt_crofton_segment));
shared->segment_count = needed;
}
}
bu_parallel(crofton_worker, (int)ncpus, (void *)wdata);
for (size_t i = 0; i < ncpus; i++) {
shared->total_crossings += wdata[i].local_crossings;
shared->total_chord += wdata[i].local_chord;
shared->total_rays += wdata[i].local_rays;
if (wdata[i].local_point_count) {
const size_t needed = shared->point_count + wdata[i].local_point_count;
if (needed > shared->point_capacity) {
size_t new_capacity = shared->point_capacity ?
2 * shared->point_capacity : needed;
if (new_capacity < needed)
new_capacity = needed;
shared->points = (point_t *)bu_realloc(shared->points,
new_capacity * sizeof(point_t), "Crofton hit points");
shared->point_capacity = new_capacity;
}
memcpy(&shared->points[shared->point_count], wdata[i].local_points,
wdata[i].local_point_count * sizeof(point_t));
shared->point_count = needed;
}
}
for (size_t i = 0; i < ncpus; i++) {
if (wdata[i].local_points)
bu_free(wdata[i].local_points, "Crofton worker hit points");
bu_free(wdata[i].ap, "crofton app");
}
bu_free(wdata, "crofton wdata");
bu_free(rays, "crofton rays");
bu_free(rays, "Crofton rays");
}
/* ------------------------------------------------------------------ */
/* Public API: rt_crofton_shoot */
/* ------------------------------------------------------------------ */
@@ -375,7 +552,8 @@ do_one_iteration(struct application *ap_template,
* sampling (>= 0) on success; -1 on bad arguments.
*/
static int
crofton_shoot_impl(point_t **out_points,
crofton_shoot_impl(struct rt_crofton_result *out_result,
point_t **out_points,
size_t *out_point_count,
double *out_surf_area,
double *out_volume,
@@ -384,13 +562,22 @@ crofton_shoot_impl(point_t **out_points,
point_t out_obb[8],
struct rt_i *rtip,
const struct rt_crofton_params *params,
size_t ray_offset,
const fastf_t *bbox_min,
const fastf_t *bbox_max)
{
if (!rtip || (!out_surf_area && !out_volume && !out_aabb_min && !out_obb && !out_points) ||
((out_points == NULL) != (out_point_count == NULL)) ||
((out_aabb_min == NULL) != (out_aabb_max == NULL)))
return -1;
if (!rtip ||
(!out_result && !out_surf_area && !out_volume && !out_aabb_min &&
!out_obb && !out_points) ||
((out_points == NULL) != (out_point_count == NULL)) ||
((out_aabb_min == NULL) != (out_aabb_max == NULL)) ||
(out_result && out_result->segments) ||
ray_offset > UINT64_MAX / RT_CROFTON_RANDOM_VALUES_PER_RAY)
return -1;
if (out_result) {
struct rt_crofton_result empty = RT_CROFTON_RESULT_INIT;
*out_result = empty;
}
if (out_points) {
*out_points = NULL;
*out_point_count = 0;
@@ -473,10 +660,14 @@ crofton_shoot_impl(point_t **out_points,
if (!use_default && max_rays > 0 && stability_mm <= 0.0 && time_ms <= 0.0)
batch = max_rays; /* single-iteration mode */
size_t ncpus = bu_avail_cpus();
if (ncpus < 1) ncpus = 1;
if (ncpus > MAX_PSW) ncpus = MAX_PSW;
/* ---- Initialize per-CPU resources ---- */
struct resource *resources = (struct resource *)bu_calloc(
MAX_PSW, sizeof(struct resource), "crofton resources");
for (int i = 0; i < MAX_PSW; i++)
ncpus, sizeof(struct resource), "crofton resources");
for (size_t i = 0; i < ncpus; i++)
rt_init_resource(&resources[i], i, rtip);
/* ---- Set up application template ---- */
@@ -494,10 +685,32 @@ crofton_shoot_impl(point_t **out_points,
/* ---- Shared accumulator ---- */
struct crofton_shared shared;
memset(&shared, 0, sizeof(shared));
shared.collect_points = out_points != NULL || out_aabb_min != NULL || out_obb != NULL;
const bool stabilize_surface = out_surf_area != NULL || shared.collect_points;
shared.ray_offset = ray_offset;
shared.collect_points =
out_points != NULL || out_aabb_min != NULL || out_obb != NULL;
shared.collect_segments = out_result != NULL;
const bool stabilize_surface =
out_surf_area != NULL || out_result != NULL ||
shared.collect_points;
CroftonWorkerPool *pool = NULL;
try {
pool = new CroftonWorkerPool(&ap, resources, ncpus);
} catch (const std::exception &e) {
bu_log("rt_crofton: unable to start worker pool: %s\n", e.what());
for (size_t i = 0; i < ncpus; i++) {
if (resources[i].re_magic == RESOURCE_MAGIC) {
rt_clean_resource_basic(rtip, &resources[i]);
BU_PTBL_SET(&rtip->rti_resources, i, NULL);
}
}
bu_free(resources, "crofton resources");
return -1;
}
std::mt19937_64 rng(RT_CROFTON_RNG_SEED);
rng.discard(static_cast<uint64_t>(ray_offset) *
RT_CROFTON_RANDOM_VALUES_PER_RAY);
const double FOUR_PI = 4.0 * M_PI;
const double PI = M_PI;
@@ -521,7 +734,7 @@ crofton_shoot_impl(point_t **out_points,
curr_rays = batch;
}
do_one_iteration(&ap, resources, curr_rays, R, center, &shared, rng);
do_one_iteration(*pool, curr_rays, R, center, &shared, rng);
iteration++;
if (shared.total_rays == 0) break;
@@ -562,9 +775,12 @@ crofton_shoot_impl(point_t **out_points,
} else {
/* ---- Parametric loop: n_rays / stability_mm / time_ms ---- */
double prev_r_sa = -1.0, prev_r_v = -1.0;
double prev_sa = -1.0, prev_v = -1.0;
size_t total_fired = 0;
size_t stable_windows = 0;
int64_t t0 = (time_ms > 0.0) ? bu_gettime() : 0;
const bool use_relative_stability = out_result && time_ms > 0.0 &&
stability_mm <= 0.0;
for (;;) {
/* Time-budget check before firing */
@@ -581,7 +797,7 @@ crofton_shoot_impl(point_t **out_points,
if (fire > remaining) fire = remaining;
}
do_one_iteration(&ap, resources, fire, R, center, &shared, rng);
do_one_iteration(*pool, fire, R, center, &shared, rng);
total_fired += fire;
if (shared.total_rays == 0) break;
@@ -594,27 +810,39 @@ crofton_shoot_impl(point_t **out_points,
/ (double)shared.total_rays;
/* Stability check */
if (stability_mm > 0.0 &&
if ((stability_mm > 0.0 || use_relative_stability) &&
shared.total_rays >= RT_CROFTON_STABILITY_MIN_RAYS &&
shared.total_crossings >= RT_CROFTON_STABILITY_MIN_CROSSINGS) {
double r_sa = (curr_est_sa > 0.0) ? sqrt(curr_est_sa * INV_4PI) : 0.0;
double r_v = (curr_est_v > 0.0) ? cbrt(curr_est_v * INV_4PI3) : 0.0;
if (prev_r_sa >= 0.0) {
int sa_ok = (!stabilize_surface) ||
fabs(r_sa - prev_r_sa) < stability_mm;
int v_ok = (!out_volume) ||
fabs(r_v - prev_r_v) < stability_mm;
if (sa_ok && v_ok) {
stable_windows++;
if (stable_windows >= RT_CROFTON_STABILITY_MIN_WINDOWS)
break;
} else {
stable_windows = 0;
}
bool sa_ok = false;
bool v_ok = false;
if (use_relative_stability) {
sa_ok = !stabilize_surface || (prev_sa > 0.0 &&
fabs(curr_est_sa - prev_sa) / prev_sa * 100.0 <=
RT_CROFTON_DEFAULT_THRESHOLD);
v_ok = !out_volume || (prev_v > 0.0 &&
fabs(curr_est_v - prev_v) / prev_v * 100.0 <=
RT_CROFTON_DEFAULT_THRESHOLD);
prev_sa = curr_est_sa;
prev_v = curr_est_v;
} else {
double r_sa = (curr_est_sa > 0.0) ?
sqrt(curr_est_sa * INV_4PI) : 0.0;
double r_v = (curr_est_v > 0.0) ?
cbrt(curr_est_v * INV_4PI3) : 0.0;
sa_ok = !stabilize_surface || (prev_r_sa >= 0.0 &&
fabs(r_sa - prev_r_sa) < stability_mm);
v_ok = !out_volume || (prev_r_v >= 0.0 &&
fabs(r_v - prev_r_v) < stability_mm);
prev_r_sa = r_sa;
prev_r_v = r_v;
}
if (sa_ok && v_ok) {
stable_windows++;
if (stable_windows >= RT_CROFTON_STABILITY_MIN_WINDOWS)
break;
} else {
stable_windows = 0;
}
prev_r_sa = r_sa;
prev_r_v = r_v;
}
/* Time-budget check after firing */
@@ -624,8 +852,16 @@ crofton_shoot_impl(point_t **out_points,
}
}
if (out_surf_area) *out_surf_area = curr_est_sa;
if (out_volume) *out_volume = curr_est_v;
if (out_surf_area)
*out_surf_area = curr_est_sa;
if (out_volume)
*out_volume = curr_est_v;
if (out_result) {
out_result->surface_area = curr_est_sa;
out_result->volume = curr_est_v;
out_result->ray_count = shared.total_rays;
out_result->crossing_count = shared.total_crossings;
}
if (out_aabb_min) {
for (size_t i = 0; i < shared.point_count; ++i)
VMINMAX(*out_aabb_min, *out_aabb_max, shared.points[i]);
@@ -641,11 +877,19 @@ crofton_shoot_impl(point_t **out_points,
bounds_ret = -1;
}
if (out_points && !bounds_ret) {
*out_points = shared.points;
*out_point_count = shared.point_count;
*out_points = shared.points;
*out_point_count = shared.point_count;
} else if (shared.points) {
bu_free(shared.points, "Crofton hit points");
bu_free(shared.points, "Crofton hit points");
}
if (out_result && !bounds_ret) {
out_result->segments = shared.segments;
out_result->segment_count = shared.segment_count;
} else if (shared.segments) {
bu_free(shared.segments, "Crofton segments");
}
delete pool;
/* Clean each resource and NULL out its slot in rtip->rti_resources.
* This is necessary because crofton_from_ip calls rt_i_destroy(rtip)
@@ -657,7 +901,7 @@ crofton_shoot_impl(point_t **out_points,
*
* By setting the slot to NULL we let rt_i_destroy's cleanup skip it,
* and then we can safely bu_free the resources array. */
for (int i = 0; i < MAX_PSW; i++) {
for (size_t i = 0; i < ncpus; i++) {
if (resources[i].re_magic == RESOURCE_MAGIC) {
rt_clean_resource_basic(rtip, &resources[i]);
BU_PTBL_SET(&rtip->rti_resources, i, NULL);
@@ -681,9 +925,31 @@ rt_crofton_shoot(double *out_surf_area, double *out_volume,
const struct rt_crofton_params *params,
const fastf_t *bbox_min, const fastf_t *bbox_max)
{
return crofton_shoot_impl(out_points, out_point_count, out_surf_area,
out_volume, out_aabb_min, out_aabb_max, out_obb, rtip, params,
bbox_min, bbox_max);
return crofton_shoot_impl(NULL, out_points, out_point_count,
out_surf_area, out_volume, out_aabb_min, out_aabb_max, out_obb,
rtip, params, 0, bbox_min, bbox_max);
}
int
rt_crofton_collect(struct rt_crofton_result *result, struct rt_i *rtip,
const struct rt_crofton_params *params, size_t ray_offset,
const fastf_t *bbox_min, const fastf_t *bbox_max)
{
if (!result)
return -1;
return crofton_shoot_impl(result, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, rtip, params, ray_offset, bbox_min, bbox_max);
}
void
rt_crofton_result_free(struct rt_crofton_result *result)
{
if (!result)
return;
if (result->segments)
bu_free(result->segments, "Crofton segments");
struct rt_crofton_result empty = RT_CROFTON_RESULT_INIT;
*result = empty;
}
@@ -821,7 +1087,7 @@ crofton_from_ip_n(const struct rt_db_internal *ip,
double sa = 0.0;
double vol = 0.0;
(void)rt_crofton_shoot(&sa, &vol, NULL, NULL, NULL, NULL, NULL,
rtip, params, NULL, NULL);
rtip, params, NULL, NULL);
if (out_sa) *out_sa = sa;
if (out_vol) *out_vol = vol;
+9 -59
View File
@@ -1543,8 +1543,7 @@ permute_cell(point_t A,
struct dsp_specific *dsp,
struct dsp_rpp *dsp_rpp)
{
int x, y;
point_t tmp;
#ifdef FULL_DSP_DEBUGGING
if (RT_G_DEBUG & RT_DEBUG_HF) {
@@ -1558,10 +1557,8 @@ permute_cell(point_t A,
switch (dsp->dsp_i.dsp_cuttype) {
case DSP_CUT_DIR_llUR:
return DSP_CUT_DIR_llUR;
break;
case DSP_CUT_DIR_ADAPT: {
point_t tmp;
size_t cx = (size_t)dsp_rpp->dsp_min[X];
size_t cy = (size_t)dsp_rpp->dsp_min[Y];
@@ -1570,28 +1567,8 @@ permute_cell(point_t A,
dsp_rpp->dsp_min[X],
dsp_rpp->dsp_min[Y]);
/*
* We look at the points in the diagonal next cells to
* determine the curvature along each diagonal of this
* cell. This cell is divided into two triangles by
* cutting across the cell in the direction of least
* curvature.
*
* * * * *
* \ /
* \C D/
* * *--* *
* |\/|
* |/\|
* * *--* *
* /A B\
* / \
* * * * *
*/
/* leaf-cell precondition: the rpp spans exactly one grid unit, so
* (cx, cy) plus the cell counts fully describe the curvature
* stencil that rt_dsp_cell_cut() recomputes.
/* The rpp spans one grid cell; use its indices to select the
* lower-curvature diagonal.
*/
BU_ASSERT(dsp_rpp->dsp_max[X] == dsp_rpp->dsp_min[X] + 1);
BU_ASSERT(dsp_rpp->dsp_max[Y] == dsp_rpp->dsp_min[Y] + 1);
@@ -1599,15 +1576,16 @@ permute_cell(point_t A,
if (rt_dsp_cell_cut(&dsp->dsp_i, cx, cy,
(size_t)dsp->xsiz, (size_t)dsp->ysiz)
== DSP_CUT_DIR_llUR) {
/* A-D cut is fine, no need to permute */
if (RT_G_DEBUG & RT_DEBUG_HF)
bu_log("A-D cut\n");
return DSP_CUT_DIR_llUR;
}
/* prefer the B-C cut */
}
/* fall through */
case DSP_CUT_DIR_ULlr:
/* Both B-C modes need the same corner order. The top triangles
* must keep outward (+Z) normals for the entry/exit state machine.
*/
VMOVE(tmp, A);
VMOVE(A, B);
VMOVE(B, D);
@@ -1615,35 +1593,7 @@ permute_cell(point_t A,
VMOVE(C, tmp);
if (RT_G_DEBUG & RT_DEBUG_HF)
bu_log("B-C cut\n");
return DSP_CUT_DIR_ULlr;
break;
}
case DSP_CUT_DIR_ULlr:
/* assign the values for the corner points
*
* D----C
* | |
* | |
* | |
* B----A
*/
x = dsp_rpp->dsp_min[X];
y = dsp_rpp->dsp_min[Y];
VSET(B, x, y, DSP(&dsp->dsp_i, x, y));
x = dsp_rpp->dsp_max[X];
VSET(A, x, y, DSP(&dsp->dsp_i, x, y));
y = dsp_rpp->dsp_max[Y];
VSET(C, x, y, DSP(&dsp->dsp_i, x, y));
x = dsp_rpp->dsp_min[X];
VSET(D, x, y, DSP(&dsp->dsp_i, x, y));
return DSP_CUT_DIR_ULlr;
break;
}
bu_log("%s:%d Unknown DSP cut direction: %d\n",
__FILE__, __LINE__, dsp->dsp_i.dsp_cuttype);
+13 -9
View File
@@ -196,6 +196,13 @@ pnts_node_scale(void *node, rt_pnt_type type)
}
}
static fastf_t
pnts_effective_scale(const struct rt_pnts_internal *pnts, struct pnt *point)
{
fastf_t *scale = pnts_node_scale(point, pnts->type);
return scale ? *scale : pnts->scale;
}
static fastf_t *
pnts_node_normal(void *node, rt_pnt_type type)
{
@@ -296,12 +303,8 @@ rt_pnts_bbox(struct rt_db_internal *ip, point_t *min, point_t *max, const struct
pnts = (struct rt_pnts_internal *)ip->idb_ptr;
RT_PNTS_CK_MAGIC(pnts);
if (pnts->count <= 0) {
return 0;
}
if (!pnts_type_is_valid(pnts->type)) {
return 0;
}
if (pnts->count == 0 || !pnts_type_is_valid(pnts->type) || !pnts->point)
return -1;
VSETALL((*min), INFINITY);
VSETALL((*max), -INFINITY);
@@ -312,8 +315,10 @@ rt_pnts_bbox(struct rt_db_internal *ip, point_t *min, point_t *max, const struct
* 'point', so it can't double as the head expression. */
point = (struct pnt *)pnts->point;
head = &point->l;
if (BU_LIST_IS_EMPTY(head))
return -1;
for (BU_LIST_FOR(point, pnt, head)) {
_pnts_calc_bbox(min, max, &(point->v), pnts->scale);
_pnts_calc_bbox(min, max, &(point->v), pnts_effective_scale(pnts, point));
}
return 0;
@@ -369,8 +374,7 @@ rt_pnts_prep(struct soltab *stp, struct rt_db_internal *ip, struct rt_i *rtip)
point = (struct pnt *)pnts_ip->point;
head = &point->l;
for (BU_LIST_FOR(point, pnt, head)) {
fastf_t *sp = pnts_node_scale(point, pnts_ip->type);
fastf_t r = sp ? *sp : pnts_ip->scale;
fastf_t r = pnts_effective_scale(pnts_ip, point);
if (r > 0) {
VMOVE(spec->centers[spec->count], point->v);
spec->radii[spec->count] = r;
+8 -8
View File
@@ -1032,7 +1032,7 @@ const struct rt_functab OBJ[] = {
{
/* 16 */
RT_FUNCTAB_MAGIC, "ID_PARTICLE", "part",
0,
1,
RTFUNCTAB_FUNC_PREP_CAST(rt_part_prep),
RTFUNCTAB_FUNC_SHOT_CAST(rt_part_shot),
RTFUNCTAB_FUNC_PRINT_CAST(rt_part_print),
@@ -1084,7 +1084,7 @@ const struct rt_functab OBJ[] = {
{
/* 17 */
RT_FUNCTAB_MAGIC, "ID_RPC", "rpc",
0,
1,
RTFUNCTAB_FUNC_PREP_CAST(rt_rpc_prep),
RTFUNCTAB_FUNC_SHOT_CAST(rt_rpc_shot),
RTFUNCTAB_FUNC_PRINT_CAST(rt_rpc_print),
@@ -1136,7 +1136,7 @@ const struct rt_functab OBJ[] = {
{
/* 18 */
RT_FUNCTAB_MAGIC, "ID_RHC", "rhc",
0,
1,
RTFUNCTAB_FUNC_PREP_CAST(rt_rhc_prep),
RTFUNCTAB_FUNC_SHOT_CAST(rt_rhc_shot),
RTFUNCTAB_FUNC_PRINT_CAST(rt_rhc_print),
@@ -1188,7 +1188,7 @@ const struct rt_functab OBJ[] = {
{
/* 19 */
RT_FUNCTAB_MAGIC, "ID_EPA", "epa",
0,
1,
RTFUNCTAB_FUNC_PREP_CAST(rt_epa_prep),
RTFUNCTAB_FUNC_SHOT_CAST(rt_epa_shot),
RTFUNCTAB_FUNC_PRINT_CAST(rt_epa_print),
@@ -1240,7 +1240,7 @@ const struct rt_functab OBJ[] = {
{
/* 20 */
RT_FUNCTAB_MAGIC, "ID_EHY", "ehy",
0,
1,
RTFUNCTAB_FUNC_PREP_CAST(rt_ehy_prep),
RTFUNCTAB_FUNC_SHOT_CAST(rt_ehy_shot),
RTFUNCTAB_FUNC_PRINT_CAST(rt_ehy_print),
@@ -1708,7 +1708,7 @@ const struct rt_functab OBJ[] = {
{
/* 29 Fastgen cline solid */
RT_FUNCTAB_MAGIC, "ID_CLINE", "cline",
0,
1,
RTFUNCTAB_FUNC_PREP_CAST(rt_cline_prep),
RTFUNCTAB_FUNC_SHOT_CAST(rt_cline_shot),
RTFUNCTAB_FUNC_PRINT_CAST(rt_cline_print),
@@ -2336,7 +2336,7 @@ const struct rt_functab OBJ[] = {
{
/* 41 */
RT_FUNCTAB_MAGIC, "ID_PNTS", "pnts",
0,
1,
RTFUNCTAB_FUNC_PREP_CAST(rt_pnts_prep),
RTFUNCTAB_FUNC_SHOT_CAST(rt_pnts_shot),
RTFUNCTAB_FUNC_PRINT_CAST(rt_pnts_print),
@@ -2388,7 +2388,7 @@ const struct rt_functab OBJ[] = {
{
/* 42 */
RT_FUNCTAB_MAGIC, "ID_ANNOT", "annot",
0, /* ft_use_rpp */
1, /* model-space prep supplies BoT bounds; view-space does not shoot */
RTFUNCTAB_FUNC_PREP_CAST(rt_annot_prep),
RTFUNCTAB_FUNC_SHOT_CAST(rt_annot_shot),
RTFUNCTAB_FUNC_PRINT_CAST(rt_annot_print),
+10
View File
@@ -150,6 +150,14 @@ brlcad_addexec(rt_bot_split
target_include_directories(rt_bot_split PRIVATE ${BRLCAD_SOURCE_DIR}/src/libbg)
brlcad_add_test(NAME rt_bot_split COMMAND rt_bot_split)
brlcad_addexec(rt_ars_inside ars_inside.c "librt;libwdb;libbu;${M_LIBRARY}" TEST)
brlcad_add_test(NAME rt_ars_inside COMMAND rt_ars_inside)
brlcad_add_test(NAME rt_ars_inside_parity COMMAND rt_ars_inside)
set_tests_properties(rt_ars_inside_parity PROPERTIES ENVIRONMENT "LIBRT_BOT_PARITY_SHOT=1")
brlcad_addexec(rt_pnts_bounds pnts_bounds.c "${RT_TEST_LIBS}" TEST)
brlcad_add_test(NAME rt_pnts_bounds COMMAND rt_pnts_bounds)
# boolweave testing
brlcad_addexec(rt_boolweave rt_boolweave.c "${RT_TEST_LIBS}" TEST)
brlcad_add_test(NAME rt_boolweave COMMAND rt_boolweave)
@@ -202,6 +210,7 @@ brlcad_add_test(NAME rt_metaball_adjust COMMAND rt_metaball_adjust)
set(
distcheck_files
CMakeLists.txt
ars_inside.c
arb_intersect.g
binary_attribute.c
binunif.c
@@ -230,6 +239,7 @@ set(
sketch.g
submodel_xform.c
prim_tess.c
pnts_bounds.c
tess_timing.c
tree_prune.c
vds_tree.c
+354
View File
@@ -0,0 +1,354 @@
/* A R S _ I N S I D E . C
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library 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 this file; see the file named COPYING for more
* information.
*/
#include "common.h"
#include <math.h>
#include <string.h>
#include "bu/app.h"
#include "bu/log.h"
#include "bu/malloc.h"
#include "raytrace.h"
#include "rt/geom.h"
#include "rt/primitives/bot.h"
#include "wdb.h"
#define RING_SIDES 8
#define RING_POINTS (RING_SIDES + 1)
static int
check_shot(const struct rt_functab *ftp, struct soltab *stp, struct application *ap, struct xray *ray, const char *label)
{
struct seg seghead;
struct seg *segp;
struct resource *resp = ap->a_resource;
int nhits;
int failed = 0;
struct bot_specific *bot = (struct bot_specific *)stp->st_specific;
fastf_t expected_in = bot->bot_parity_shot ? 0.0 : -9.0;
BU_LIST_INIT(&seghead.l);
nhits = ftp->ft_shot(stp, ray, ap, &seghead);
if (nhits <= 0 || BU_LIST_IS_EMPTY(&seghead.l)) {
bu_log("%s: inside-origin ray missed\n", label);
failed = 1;
} else {
segp = BU_LIST_FIRST(seg, &seghead.l);
if (!NEAR_EQUAL(segp->seg_in.hit_dist, expected_in, SMALL_FASTF) ||
!NEAR_EQUAL(segp->seg_out.hit_dist, 7.0, SMALL_FASTF)) {
bu_log("%s: interval %g..%g, expected %g..7\n", label,
segp->seg_in.hit_dist, segp->seg_out.hit_dist, expected_in);
failed = 1;
}
if (bot->bot_parity_shot) {
if (segp->seg_in.hit_surfno != RT_BOT_SURFNO_INTERIOR ||
segp->seg_in.hit_private != NULL) {
bu_log("%s: missing interior entry marker\n", label);
failed = 1;
}
ftp->ft_norm(&segp->seg_in, stp, ray);
if (!NEAR_EQUAL(segp->seg_in.hit_normal[X], -1.0, SMALL_FASTF)) {
bu_log("%s: wrong interior entry normal\n", label);
failed = 1;
}
}
}
while (BU_LIST_WHILE(segp, seg, &seghead.l)) {
BU_LIST_DEQUEUE(&segp->l);
RT_FREE_SEG(segp, resp);
}
if (ftp->ft_vshot) {
struct soltab *stps[1] = {stp};
struct xray *rays[1] = {ray};
struct seg vseg = {0};
ftp->ft_vshot(stps, rays, &vseg, 1, ap);
if (vseg.seg_stp != stp ||
!NEAR_EQUAL(vseg.seg_in.hit_dist, expected_in, SMALL_FASTF) ||
!NEAR_EQUAL(vseg.seg_out.hit_dist, 7.0, SMALL_FASTF)) {
bu_log("%s: vector shot missed or returned wrong interval\n", label);
failed = 1;
}
}
return failed;
}
struct scene_result {
int subject_hits;
fastf_t in_dist;
fastf_t out_dist;
};
static int
scene_hit(struct application *ap, struct partition *part_head,
struct seg *UNUSED(finished_segs))
{
struct scene_result *result = (struct scene_result *)ap->a_uptr;
struct partition *part;
for (part = part_head->pt_forw; part != part_head; part = part->pt_forw) {
if (strstr(part->pt_regionp->reg_name, "subject.r")) {
result->subject_hits++;
result->in_dist = part->pt_inhit->hit_dist;
result->out_dist = part->pt_outhit->hit_dist;
}
}
return result->subject_hits > 0;
}
static int
scene_miss(struct application *UNUSED(ap))
{
return 0;
}
/* The support sphere makes the cut tree start the ray's first cell
* inside the subject, while the subject itself spans the entire cell. */
static int
check_scene(int subject_id, const fastf_t *const curve_data[3],
fastf_t *vertices, int *faces, const char *file)
{
struct db_i *dbip = NULL;
struct rt_wdb *wdbp = NULL;
struct rt_i *rtip = NULL;
struct resource resp = RT_RESOURCE_INIT_ZERO;
struct wmember members;
struct application ap;
struct scene_result result = {0};
point_t support_center;
int failed = 0;
if (file) {
wdbp = wdb_fopen(file);
if (wdbp)
dbip = wdbp->dbip;
} else {
dbip = db_create_inmem();
if (dbip)
wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_INMEM);
}
if (!wdbp) {
if (dbip)
db_close(dbip);
return 1;
}
if (subject_id == ID_ARS) {
fastf_t **curves = (fastf_t **)bu_malloc(3 * sizeof(fastf_t *), "ARS curves");
size_t i;
for (i = 0; i < 3; i++) {
curves[i] = (fastf_t *)bu_malloc(RING_POINTS * ELEMENTS_PER_POINT *
sizeof(fastf_t), "ARS curve");
memcpy(curves[i], curve_data[i],
RING_POINTS * ELEMENTS_PER_POINT * sizeof(fastf_t));
}
failed += mk_ars(wdbp, "subject.s", 3, RING_POINTS, curves) != 0;
} else {
failed += mk_bot(wdbp, "subject.s", RT_BOT_SOLID, RT_BOT_CCW, 0,
RING_SIDES + 2, RING_SIDES * 2, vertices, faces,
NULL, NULL) != 0;
}
BU_LIST_INIT(&members.l);
if (!mk_addmember("subject.s", &members.l, NULL, WMOP_UNION))
failed++;
failed += mk_lcomb(wdbp, "subject.r", &members, 1, NULL, NULL, NULL, 0) != 0;
VSET(support_center, 1.5, 0, 4);
failed += mk_sph(wdbp, "support.s", support_center, 1.0) != 0;
BU_LIST_INIT(&members.l);
if (!mk_addmember("support.s", &members.l, NULL, WMOP_UNION))
failed++;
failed += mk_lcomb(wdbp, "support.r", &members, 1, NULL, NULL, NULL, 0) != 0;
if (failed)
goto done;
rtip = rt_i_create(dbip);
if (!rtip || rt_gettree(rtip, "subject.r") ||
rt_gettree(rtip, "support.r")) {
failed = 1;
goto done;
}
rt_prep(rtip);
rt_init_resource(&resp, 0, rtip);
RT_APPLICATION_INIT(&ap);
ap.a_rt_i = rtip;
ap.a_resource = &resp;
ap.a_hit = scene_hit;
ap.a_miss = scene_miss;
ap.a_uptr = &result;
VSET(ap.a_ray.r_pt, 3, 0, 4);
VSET(ap.a_ray.r_dir, 1, 0, 0);
rt_shootray(&ap);
if (result.subject_hits != 1 ||
!NEAR_EQUAL(result.out_dist, 5.0, rtip->rti_tol.dist)) {
bu_log("%s scene: %d subject partitions, interval %.17g..%.17g; expected one exit at 5\n",
subject_id == ID_ARS ? "ARS" : "BoT",
result.subject_hits, result.in_dist, result.out_dist);
failed = 1;
}
done:
if (rtip)
rt_i_destroy(rtip);
wdb_close(wdbp);
return failed;
}
int
main(int argc, char **argv)
{
fastf_t curve_data[3][RING_POINTS * ELEMENTS_PER_POINT] = {{0}};
fastf_t *curves[3] = {curve_data[0], curve_data[1], curve_data[2]};
fastf_t bot_vertices[(RING_SIDES + 2) * ELEMENTS_PER_POINT] = {0};
int bot_faces[RING_SIDES * 2 * 3];
struct rt_bot_internal bot = {0};
struct rt_ars_internal ars = {0};
struct rt_db_internal intern;
struct rt_db_internal bot_intern;
struct soltab st = {0};
struct soltab bot_st = {0};
struct rt_i *rtip;
struct resource *resp = &rt_uniresource;
struct application ap;
struct xray ray = {0};
vect_t invdir;
size_t i;
int failed = 0;
(void)argc;
bu_setprogname(argv[0]);
for (i = 0; i < RING_POINTS; i++) {
fastf_t angle = 2.0 * M_PI * (i % RING_SIDES) / RING_SIDES;
VSET(&curve_data[1][i * ELEMENTS_PER_POINT],
10.0 * cos(angle), 10.0 * sin(angle), 5.0);
if (i < RING_SIDES)
VMOVE(&bot_vertices[(i + 1) * ELEMENTS_PER_POINT],
&curve_data[1][i * ELEMENTS_PER_POINT]);
curve_data[2][i * ELEMENTS_PER_POINT + Z] = 10.0;
}
bot_vertices[(RING_SIDES + 1) * ELEMENTS_PER_POINT + Z] = 10.0;
for (i = 0; i < RING_SIDES; i++) {
int current = (int)i + 1;
int next = (int)((i + 1) % RING_SIDES) + 1;
int *bottom = &bot_faces[i * 6];
int *top = &bottom[3];
bottom[0] = 0;
bottom[1] = next;
bottom[2] = current;
top[0] = current;
top[1] = next;
top[2] = RING_SIDES + 1;
}
bot.magic = RT_BOT_INTERNAL_MAGIC;
bot.mode = RT_BOT_SOLID;
bot.orientation = RT_BOT_CCW;
bot.num_vertices = RING_SIDES + 2;
bot.vertices = bot_vertices;
bot.num_faces = RING_SIDES * 2;
bot.faces = bot_faces;
ars.magic = RT_ARS_INTERNAL_MAGIC;
ars.ncurves = 3;
ars.pts_per_curve = RING_POINTS;
ars.curves = curves;
RT_DB_INTERNAL_INIT(&intern);
intern.idb_major_type = DB5_MAJORTYPE_BRLCAD;
intern.idb_type = ID_ARS;
intern.idb_meth = &OBJ[ID_ARS];
intern.idb_ptr = &ars;
RT_DB_INTERNAL_INIT(&bot_intern);
bot_intern.idb_major_type = DB5_MAJORTYPE_BRLCAD;
bot_intern.idb_type = ID_BOT;
bot_intern.idb_meth = &OBJ[ID_BOT];
bot_intern.idb_ptr = &bot;
rtip = rt_dirbuild_inmem(NULL, 0, NULL, 0);
if (!rtip)
return 1;
rt_init_resource(resp, 0, rtip);
st.l.magic = RT_SOLTAB_MAGIC;
st.l2.magic = RT_SOLTAB2_MAGIC;
st.st_rtip = rtip;
st.st_meth = &OBJ[ID_ARS];
st.st_id = ID_ARS;
if (OBJ[ID_ARS].ft_prep(&st, &intern, rtip)) {
bu_log("ARS prep failed\n");
return 1;
}
bot_st.l.magic = RT_SOLTAB_MAGIC;
bot_st.l2.magic = RT_SOLTAB2_MAGIC;
bot_st.st_rtip = rtip;
bot_st.st_meth = &OBJ[ID_BOT];
bot_st.st_id = ID_BOT;
if (OBJ[ID_BOT].ft_prep(&bot_st, &bot_intern, rtip)) {
bu_log("BoT prep failed\n");
return 1;
}
RT_APPLICATION_INIT(&ap);
ap.a_rt_i = rtip;
ap.a_resource = resp;
ray.magic = RT_RAY_MAGIC;
VSET(ray.r_pt, 1, 0, 4);
VSET(ray.r_dir, 1, 0, 0);
ray.r_min = -0.05; /* the current spatial cell starts inside the ARS */
failed += check_shot(&OBJ[ID_ARS], &st, &ap, &ray, "ARS cell bound");
failed += check_shot(&OBJ[ID_BOT], &bot_st, &ap, &ray, "BoT cell bound");
VSET(invdir, 1, INFINITY, INFINITY);
if (!rt_in_rpp(&ray, invdir, st.st_min, st.st_max)) {
bu_log("ARS bounding box missed the test ray\n");
failed++;
} else {
failed += check_shot(&OBJ[ID_ARS], &st, &ap, &ray, "ARS solid bound");
}
ray.r_min = -0.05;
if (!rt_in_rpp(&ray, invdir, bot_st.st_min, bot_st.st_max)) {
bu_log("BoT bounding box missed the test ray\n");
failed++;
} else {
failed += check_shot(&OBJ[ID_BOT], &bot_st, &ap, &ray, "BoT solid bound");
}
OBJ[ID_ARS].ft_free(&st);
OBJ[ID_BOT].ft_free(&bot_st);
rt_i_destroy(rtip);
if (argc != 1 && argc != 3) {
bu_log("Usage: %s [ars_scene.g bot_scene.g]\n", argv[0]);
return 1;
}
{
const fastf_t *scene_curves[3] = {curve_data[0], curve_data[1], curve_data[2]};
failed += check_scene(ID_ARS, scene_curves, bot_vertices, bot_faces,
argc == 3 ? argv[1] : NULL);
failed += check_scene(ID_BOT, scene_curves, bot_vertices, bot_faces,
argc == 3 ? argv[2] : NULL);
}
return failed ? 1 : 0;
}
+108
View File
@@ -43,6 +43,18 @@ rel_err(double estimated, double exact)
}
static int
crofton_segments_equal(const struct rt_crofton_segment *left,
const struct rt_crofton_segment *right)
{
return left->ray_id == right->ray_id &&
NEAR_EQUAL(left->thickness, right->thickness, SMALL_FASTF) &&
VNEAR_EQUAL(left->in_point, right->in_point, SMALL_FASTF) &&
VNEAR_EQUAL(left->in_normal, right->in_normal, SMALL_FASTF) &&
VNEAR_EQUAL(left->out_point, right->out_point, SMALL_FASTF) &&
VNEAR_EQUAL(left->out_normal, right->out_normal, SMALL_FASTF);
}
static int
verify_crofton_estimates(void)
{
@@ -379,6 +391,102 @@ run_convergence_case(struct db_i *dbip,
&sample_points, &sample_count,
rtip, &p, NULL, NULL);
run_sec = (double)(bu_gettime() - t0) / 1000000.0;
if (i == 0) {
struct rt_crofton_result samples = RT_CROFTON_RESULT_INIT;
const size_t ray_offset = 4096u;
const size_t continuation_rays = 256u;
struct rt_crofton_params sample_params =
{ray_offset + continuation_rays, 0.0, 0.0};
int sample_ret = rt_crofton_collect(&samples, rtip,
&sample_params, 0, NULL, NULL);
int full_samples_valid = sample_ret > 0 && samples.segments &&
samples.segment_count * 2 == samples.crossing_count &&
samples.ray_count == sample_params.n_rays;
if (!full_samples_valid) {
printf(" %-24s invalid structured Crofton output\\n", label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < samples.segment_count; sample_index++) {
struct rt_crofton_segment *segment =
&samples.segments[sample_index];
if (segment->thickness <= 0.0 ||
!NEAR_EQUAL(DIST_PNT_PNT(segment->in_point,
segment->out_point), segment->thickness, RT_LEN_TOL) ||
!NEAR_EQUAL(MAGNITUDE(segment->in_normal), 1.0,
VUNITIZE_TOL) ||
!NEAR_EQUAL(MAGNITUDE(segment->out_normal), 1.0,
VUNITIZE_TOL) ||
segment->ray_id >= samples.ray_count) {
printf(" %-24s invalid structured segment\\n", label);
failures++;
break;
}
}
}
struct rt_crofton_result offset_samples =
RT_CROFTON_RESULT_INIT;
struct rt_crofton_params offset_params =
{continuation_rays, 0.0, 0.0};
sample_ret = rt_crofton_collect(&offset_samples, rtip,
&offset_params, ray_offset, NULL, NULL);
int offset_samples_valid = sample_ret > 0 &&
offset_samples.ray_count == offset_params.n_rays;
if (!offset_samples_valid) {
printf(" %-24s invalid offset Crofton output\\n", label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < offset_samples.segment_count;
sample_index++) {
size_t ray_id = offset_samples.segments[sample_index].ray_id;
if (ray_id < ray_offset ||
ray_id >= ray_offset + offset_params.n_rays) {
printf(" %-24s invalid offset ray identifier\\n",
label);
failures++;
break;
}
}
}
if (full_samples_valid && offset_samples_valid) {
size_t suffix_start = 0;
while (suffix_start < samples.segment_count &&
samples.segments[suffix_start].ray_id < ray_offset)
suffix_start++;
size_t suffix_count = samples.segment_count - suffix_start;
if (suffix_count != offset_samples.segment_count) {
printf(" %-24s random stream continuation count mismatch\\n",
label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < suffix_count; sample_index++) {
const struct rt_crofton_segment *full_segment =
&samples.segments[suffix_start + sample_index];
const struct rt_crofton_segment *offset_segment =
&offset_samples.segments[sample_index];
if (!crofton_segments_equal(full_segment, offset_segment)) {
printf(" %-24s random stream continuation mismatch\\n",
label);
failures++;
break;
}
}
}
}
rt_crofton_result_free(&samples);
if (samples.segments || samples.segment_count)
failures++;
rt_crofton_result_free(&offset_samples);
if (offset_samples.segments || offset_samples.segment_count)
failures++;
}
rt_i_destroy(rtip);
if (elapsed_total_sec)
+316 -136
View File
@@ -19,13 +19,14 @@
#include "bu/malloc.h"
#include "bu/str.h"
#include "bv/vlist.h"
#include "nmg.h"
#include "raytrace.h"
#include "rt/geom.h"
#include "rt/primitives/annot.h"
#include "rt/primitives/datum.h"
#include "wdb.h"
enum { LAYER_TEST_MAX_SEGMENTS = 2 };
static void
check_vector(const char *name, const fastf_t *actual, const fastf_t *expected)
@@ -46,79 +47,46 @@ lookup(struct db_i *dbip, const char *name)
}
struct annot_ray_result {
fastf_t in_dist;
fastf_t out_dist;
fastf_t color[3];
int color_valid;
int partitions;
};
static int
annot_ray_hit(struct application *ap, struct partition *part_head,
struct seg *UNUSED(finished_segs))
{
struct annot_ray_result *result = (struct annot_ray_result *)ap->a_uptr;
struct partition *part;
for (part = part_head->pt_forw; part != part_head;
part = part->pt_forw) {
if (!result->partitions) {
result->in_dist = part->pt_inhit->hit_dist;
result->out_dist = part->pt_outhit->hit_dist;
result->color_valid = part->pt_regionp->reg_mater.ma_color_valid;
VMOVE(result->color, part->pt_regionp->reg_mater.ma_color);
}
result->partitions++;
}
return result->partitions > 0;
}
static int
annot_ray_miss(struct application *UNUSED(ap))
{
return 0;
}
static void
check_annot_ray(struct db_i *dbip, const char *object,
const fastf_t *expected_color)
write_layer_annotation(struct rt_wdb *wdbp, const char *name,
fastf_t anchor_x, uint32_t space, size_t segment_count,
const unsigned char colors[][4])
{
struct rt_i *rtip = rt_i_create(dbip);
struct resource resource = RT_RESOURCE_INIT_ZERO;
struct application ap;
struct annot_ray_result result = {0};
const fastf_t expected_thickness = 0.2;
int shot_result;
struct rt_annot_internal annot = {0};
point2d_t vertices[2];
struct line_seg lines[LAYER_TEST_MAX_SEGMENTS] = {{0}};
void *segments[LAYER_TEST_MAX_SEGMENTS] = {NULL, NULL};
int reverse[LAYER_TEST_MAX_SEGMENTS] = {0, 0};
struct rt_annot_seg_style styles[LAYER_TEST_MAX_SEGMENTS] = {{0}};
size_t i;
if (!rtip || rt_gettree(rtip, object))
bu_exit(1, "unable to prepare annotation raytrace object %s\n", object);
rt_prep(rtip);
rt_init_resource(&resource, 0, rtip);
RT_APPLICATION_INIT(&ap);
ap.a_rt_i = rtip;
ap.a_resource = &resource;
ap.a_hit = annot_ray_hit;
ap.a_miss = annot_ray_miss;
ap.a_uptr = &result;
VSET(ap.a_ray.r_pt, 0.0, -2.0, 0.03);
VSET(ap.a_ray.r_dir, 1.0, 0.0, 0.0);
shot_result = rt_shootray(&ap);
if (shot_result != 1 || result.partitions != 1 ||
!result.color_valid ||
!NEAR_EQUAL(result.out_dist - result.in_dist,
expected_thickness, rtip->rti_tol.dist) ||
!VNEAR_EQUAL(result.color, expected_color, VUNITIZE_TOL)) {
bu_log("%s: shot=%d partitions=%d LOS=%g color-valid=%d color=%g/%g/%g\n",
object, shot_result, result.partitions,
result.out_dist - result.in_dist, result.color_valid,
V3ARGS(result.color));
bu_exit(1, "annotation did not produce the expected colored partition\n");
if (!wdbp || !name || !colors || segment_count < 1 ||
segment_count > LAYER_TEST_MAX_SEGMENTS)
bu_exit(1, "invalid ordered-layer test annotation\n");
annot.magic = RT_ANNOT_INTERNAL_MAGIC;
VSET(annot.V, anchor_x, 0.0, 0.0);
annot.flags = space;
VSET(annot.u_vec, 0.0, 1.0, 0.0);
VSET(annot.v_vec, 0.0, 0.0, 1.0);
annot.vert_count = 2;
annot.verts = vertices;
V2SET(vertices[0], -1.0, 0.0);
V2SET(vertices[1], 1.0, 0.0);
annot.ant.count = segment_count;
annot.ant.reverse = reverse;
annot.ant.segments = segments;
annot.styles = styles;
for (i = 0; i < segment_count; ++i) {
lines[i].magic = CURVE_LSEG_MAGIC;
lines[i].start = 0;
lines[i].end = 1;
segments[i] = &lines[i];
styles[i].flags = RT_ANNOT_STYLE_COLOR;
styles[i].line_pattern = RT_ANNOT_LINE_CONTINUOUS;
memcpy(styles[i].color, colors[i], sizeof(styles[i].color));
}
rt_i_destroy(rtip);
if (rt_annot_validate(&annot, NULL) || mk_annot(wdbp, name, &annot) < 0)
bu_exit(1, "unable to write ordered-layer test annotation\n");
}
@@ -257,48 +225,78 @@ main(int argc, char **argv)
annot.styles[10].role = RT_ANNOT_ROLE_MASK;
annot.styles[10].flags = RT_ANNOT_STYLE_FILLED;
annot.styles[10].symbol = (char *)"owned fill with hole";
{
const size_t segment_count = annot.ant.count;
annot.ant.count = 1;
annot.styles[0].line_pattern = RT_ANNOT_LINE_CONTINUOUS;
annot.styles[0].line_width = 1.0;
if (rt_annot_validate(&annot, NULL) ||
mk_annot(wdbp, "thin.annot", &annot) < 0)
bu_exit(1, "unable to write default-width annotation\n");
annot.styles[0].line_width = 2.5;
if (rt_annot_validate(&annot, NULL) ||
mk_annot(wdbp, "wide.annot", &annot) < 0)
bu_exit(1, "unable to write wide annotation\n");
annot.ant.count = segment_count;
annot.styles[0].line_pattern = RT_ANNOT_LINE_DASHED;
}
if (rt_annot_validate(&annot, NULL) ||
mk_annot(wdbp, "leader.annot", &annot) < 0)
bu_exit(1, "unable to write enhanced annotation\n");
{
struct rt_annot_internal ray_annot = {0};
point2d_t ray_vertices[2];
struct line_seg ray_line = {0};
void *ray_segments[1];
int ray_reverse[1] = {0};
struct rt_annot_seg_style ray_style[1] = {{0}};
struct wmember member;
unsigned char region_color[3] = {90, 80, 70};
ray_annot.magic = RT_ANNOT_INTERNAL_MAGIC;
VSET(ray_annot.V, 10.0, 0.0, 0.0);
ray_annot.flags = RT_ANNOT_MODEL_SPACE;
VSET(ray_annot.u_vec, 0.0, 1.0, 0.0);
VSET(ray_annot.v_vec, 0.0, 0.0, 1.0);
ray_annot.vert_count = 2;
ray_annot.verts = ray_vertices;
V2SET(ray_vertices[0], -5.0, 0.0);
V2SET(ray_vertices[1], 5.0, 0.0);
ray_line.magic = CURVE_LSEG_MAGIC;
ray_line.start = 0;
ray_line.end = 1;
ray_segments[0] = &ray_line;
ray_annot.ant.count = 1;
ray_annot.ant.reverse = ray_reverse;
ray_annot.ant.segments = ray_segments;
ray_style[0].flags = RT_ANNOT_STYLE_WIDTH;
ray_style[0].line_pattern = RT_ANNOT_LINE_CONTINUOUS;
ray_style[0].line_width = 2.0;
ray_annot.styles = ray_style;
if (rt_annot_validate(&ray_annot, NULL) ||
mk_annot(wdbp, "ray.annot", &ray_annot) < 0)
bu_exit(1, "unable to write raytrace annotation\n");
BU_LIST_INIT(&member.l);
if (!mk_addmember("ray.annot", &member.l, NULL, WMOP_UNION) ||
mk_lcomb(wdbp, "ray.r", &member, 1, NULL, NULL,
region_color, 0))
bu_exit(1, "unable to write raytrace annotation region\n");
struct wmember members;
mat_t member_transform = MAT_INIT_IDN;
unsigned char inherited_color[3] = {90, 80, 70};
BU_LIST_INIT(&members.l);
MAT_DELTAS(member_transform, 100.0, 0.0, 0.0);
if (!mk_addmember("leader.annot", &members.l, member_transform,
WMOP_UNION) ||
mk_lcomb(wdbp, "annotation-group", &members, 0, NULL, NULL,
inherited_color, 0))
bu_exit(1, "unable to write annotation instance combination\n");
}
{
struct wmember members;
unsigned char inherited_color[3] = {21, 31, 41};
BU_LIST_INIT(&members.l);
if (!mk_addmember("annotation-group", &members.l, NULL, WMOP_UNION) ||
mk_lcomb(wdbp, "inherited-annotation-group", &members, 0,
NULL, NULL, inherited_color, 1))
bu_exit(1, "unable to write inherited annotation combination\n");
}
{
struct wmember members;
mat_t member_transform = MAT_INIT_IDN;
BU_LIST_INIT(&members.l);
MAT_SCALE_ALL(member_transform, 1000.0);
if (!mk_addmember("leader.annot", &members.l, member_transform,
WMOP_UNION) ||
mk_lcomb(wdbp, "scaled-annotation-group", &members, 0, NULL,
NULL, NULL, 0))
bu_exit(1, "unable to write scaled annotation instance combination\n");
}
{
point_t saved_anchor;
VMOVE(saved_anchor, annot.V);
VSETALL(annot.V, 0.0);
annot.flags = RT_ANNOT_SCREEN_SPACE;
if (mk_annot(wdbp, "screen.annot", &annot) < 0)
bu_exit(1, "unable to write screen annotation\n");
VMOVE(annot.V, saved_anchor);
annot.flags = RT_ANNOT_MODEL_SPACE;
}
{
const unsigned char far_color[][4] = {{0, 0, 255, 128}};
const unsigned char near_colors[][4] = {
{255, 0, 0, 128}, {0, 255, 0, 128}
};
const unsigned char screen_color[][4] = {{255, 255, 255, 128}};
write_layer_annotation(wdbp, "alpha-far.annot", 20.0,
RT_ANNOT_MODEL_SPACE, 1, far_color);
write_layer_annotation(wdbp, "alpha-near.annot", 10.0,
RT_ANNOT_MODEL_SPACE, 2, near_colors);
write_layer_annotation(wdbp, "alpha-screen.annot", 0.0,
RT_ANNOT_SCREEN_SPACE, 1, screen_color);
}
/* ANP2 is deliberately appended after the main-branch ANT2 body. A
@@ -398,12 +396,7 @@ main(int argc, char **argv)
struct bu_list vhead;
struct bg_tess_tol ttol = BG_TESS_TOL_INIT_TOL;
struct bn_tol tol = BN_TOL_INIT_TOL;
if (!loaded || !loaded->styles || !loaded->ant.segments ||
loaded->ant.count != 11 || !loaded->ant.segments[10])
bu_exit(1,
"enhanced annotation structure did not round trip\n");
if (!(loaded->flags & RT_ANNOT_MODEL_SPACE) ||
if (!(loaded->flags & RT_ANNOT_MODEL_SPACE) || !loaded->styles ||
loaded->styles[0].role != RT_ANNOT_ROLE_CENTERMARK ||
loaded->styles[0].line_pattern != RT_ANNOT_LINE_DASHED ||
!loaded->styles[0].font ||
@@ -422,6 +415,7 @@ main(int argc, char **argv)
!NEAR_EQUAL(loaded->styles[1].xy_scale, 0.25, SMALL_FASTF) ||
!NEAR_EQUAL(loaded->styles[1].yx_scale, 0.1, SMALL_FASTF) ||
!NEAR_EQUAL(loaded->styles[1].y_scale, 0.75, SMALL_FASTF) ||
loaded->ant.count != 11 ||
*(uint32_t *)loaded->ant.segments[10] != ANN_FSEG_MAGIC ||
loaded->styles[10].role != RT_ANNOT_ROLE_MASK ||
!loaded->styles[10].symbol ||
@@ -473,36 +467,222 @@ main(int argc, char **argv)
}
check_vector("annotation plot anchor", loaded->V, original_anchor);
BV_FREE_VLIST(&rt_vlfree, &vhead);
{
struct model *model = nmg_mm();
struct nmgregion *region = NULL;
if (rt_obj_tess(&region, model, &intern, &ttol, &tol) || !region)
bu_exit(1, "unable to tessellate model-space annotation\n");
nmg_km(model);
}
}
rt_db_free_internal(&intern);
{
const fastf_t default_color[3] = {1.0, 1.0, 1.0};
const fastf_t annotation_color[3] = {
12.0 / 255.0, 34.0 / 255.0, 56.0 / 255.0
const char *paths[] = {"leader.annot"};
struct bg_tess_tol ttol = BG_TESS_TOL_INIT_TOL;
struct bn_tol tol = BN_TOL_INIT_TOL;
struct rt_annot_scene *scene = rt_annot_scene_create(dbip, 1, paths,
&ttol, &tol);
struct xray ray;
struct rt_annot_hit hit = {0};
struct rt_annot_hit all_hit = {0};
point_t render_min, render_max, width_sample;
const fastf_t line_offset = 0.12;
const fastf_t inverse_line_length = 1.0 / sqrt(41.0);
const fastf_t offset_x = 2.0 - 5.0 * line_offset * inverse_line_length;
const fastf_t offset_y = 2.5 + 4.0 * line_offset * inverse_line_length;
VSET(ray.r_pt, 0.0, 24.0, 37.5);
VSET(ray.r_dir, 1.0, 0.0, 0.0);
if (!scene || rt_annot_scene_count(scene) != 1)
bu_exit(1, "annotation render scene did not collect its leaf\n");
if (!rt_annot_scene_bounds(scene, render_min, render_max) ||
!NEAR_EQUAL(render_min[X], 10.0, SMALL_FASTF) ||
!NEAR_EQUAL(render_max[X], 10.0, SMALL_FASTF) ||
render_min[Y] >= render_max[Y] ||
render_min[Z] >= render_max[Z])
bu_exit(1, "annotation render scene bounds are invalid\n");
if (!rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0,
INFINITY, &hit) || hit.screen_space || !hit.visible ||
!NEAR_EQUAL(hit.distance, 10.0, SMALL_FASTF) ||
hit.color[0] != 12 || hit.color[1] != 34 || hit.color[2] != 56)
bu_exit(1, "model annotation coverage query failed\n");
if (rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0, 5.0, &hit))
bu_exit(1, "occluded annotation was returned as visible\n");
if (!rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0,
9.99975, &hit))
bu_exit(1, "annotation visibility ignored model tolerance\n");
if (rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0,
9.999, &hit))
bu_exit(1, "annotation visibility exceeded model tolerance\n");
if (rt_annot_scene_query_model(scene, &ray, 5.0, &all_hit, 1) != 1 ||
all_hit.visible)
bu_exit(1, "annotation diagnostic query lost occluded coverage\n");
VSET(ray.r_pt, 0.0, 12.0, 18.0);
if (!rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0,
INFINITY, &hit) || hit.segment != 10)
bu_exit(1, "filled annotation coverage was not returned\n");
VSET(ray.r_pt, 0.0, 17.0, 25.5);
if (rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0,
INFINITY, &hit))
bu_exit(1, "annotation fill hole unexpectedly reported coverage\n");
rt_annot_scene_destroy(scene);
VSET(width_sample, 0.0, 20.0 + 2.0 * offset_x,
30.0 + 3.0 * offset_y);
paths[0] = "wide.annot";
scene = rt_annot_scene_create(dbip, 1, paths, &ttol, &tol);
VMOVE(ray.r_pt, width_sample);
if (!scene || !rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0,
INFINITY, &hit) || hit.segment != 0)
bu_exit(1, "annotation line width did not expand ray coverage\n");
rt_annot_scene_destroy(scene);
paths[0] = "thin.annot";
scene = rt_annot_scene_create(dbip, 1, paths, &ttol, &tol);
VMOVE(ray.r_pt, width_sample);
if (!scene || rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0,
INFINITY, &hit))
bu_exit(1, "default line width unexpectedly covered wide sample\n");
rt_annot_scene_destroy(scene);
}
{
const char *paths[] = {"scaled-annotation-group"};
struct bg_tess_tol ttol = BG_TESS_TOL_INIT_TOL;
struct bn_tol tol = BN_TOL_INIT_TOL;
struct rt_annot_scene *scene = rt_annot_scene_create(dbip, 1, paths,
&ttol, &tol);
struct xray ray;
struct rt_annot_hit hit = {0};
VSET(ray.r_pt, 0.0, 0.028, 0.045);
VSET(ray.r_dir, 1.0, 0.0, 0.0);
if (!scene || rt_annot_scene_count(scene) != 1 ||
!rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0,
INFINITY, &hit) ||
!NEAR_EQUAL(hit.distance, 0.01, SMALL_FASTF))
bu_exit(1, "scaled annotation instance coverage failed\n");
rt_annot_scene_destroy(scene);
}
{
const char *paths[] = {"annotation-group"};
struct bg_tess_tol ttol = BG_TESS_TOL_INIT_TOL;
struct bn_tol tol = BN_TOL_INIT_TOL;
struct rt_annot_scene *scene = rt_annot_scene_create(dbip, 1, paths,
&ttol, &tol);
struct xray ray;
struct rt_annot_hit hit = {0};
VSET(ray.r_pt, 0.0, 28.0, 45.0);
VSET(ray.r_dir, 1.0, 0.0, 0.0);
if (!scene || rt_annot_scene_count(scene) != 1 ||
!rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0,
INFINITY, &hit) ||
!NEAR_EQUAL(hit.distance, 110.0, SMALL_FASTF) ||
hit.segment != 10 || hit.color[0] != 90 ||
hit.color[1] != 80 || hit.color[2] != 70)
bu_exit(1, "annotation instance transform was not applied\n");
VSET(ray.r_pt, 0.0, 24.0, 37.5);
if (!rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0,
INFINITY, &hit) || hit.segment != 0 || hit.color[0] != 12 ||
hit.color[1] != 34 || hit.color[2] != 56)
bu_exit(1, "annotation segment color did not override inherited color\n");
rt_annot_scene_destroy(scene);
}
{
const char *paths[] = {"inherited-annotation-group"};
struct bg_tess_tol ttol = BG_TESS_TOL_INIT_TOL;
struct bn_tol tol = BN_TOL_INIT_TOL;
struct rt_annot_scene *scene = rt_annot_scene_create(dbip, 1, paths,
&ttol, &tol);
struct xray ray;
struct rt_annot_hit hit = {0};
VSET(ray.r_pt, 0.0, 28.0, 45.0);
VSET(ray.r_dir, 1.0, 0.0, 0.0);
if (!scene || !rt_annot_scene_query(scene, NULL, &ray, 0.0, 0.0,
INFINITY, &hit) || hit.segment != 10 || hit.color[0] != 21 ||
hit.color[1] != 31 || hit.color[2] != 41)
bu_exit(1, "annotation color did not follow tree inheritance\n");
rt_annot_scene_destroy(scene);
}
{
const char *paths[] = {"screen.annot"};
struct bg_tess_tol ttol = BG_TESS_TOL_INIT_TOL;
struct bn_tol tol = BN_TOL_INIT_TOL;
struct rt_annot_scene *scene = rt_annot_scene_create(dbip, 1, paths,
&ttol, &tol);
struct rt_annot_view view = {MAT_INIT_IDN, 100, 100, 0.0};
struct xray ray;
struct rt_annot_hit hit = {0};
VSET(ray.r_pt, 0.0, 0.0, 10.0);
VSET(ray.r_dir, 0.0, 0.0, -1.0);
if (!scene || !rt_annot_scene_query(scene, &view, &ray, 50.0, 50.0,
0.0, &hit) || !hit.screen_space || !hit.visible)
bu_exit(1, "screen annotation coverage query failed\n");
rt_annot_scene_destroy(scene);
}
{
const char *paths[] = {
"alpha-far.annot", "alpha-near.annot", "alpha-screen.annot"
};
const fastf_t region_color[3] = {
90.0 / 255.0, 80.0 / 255.0, 70.0 / 255.0
struct bg_tess_tol ttol = BG_TESS_TOL_INIT_TOL;
struct bn_tol tol = BN_TOL_INIT_TOL;
struct rt_annot_scene *scene = rt_annot_scene_create(dbip, 3, paths,
&ttol, &tol);
struct rt_annot_view view = {MAT_INIT_IDN, 100, 100, 0.0};
struct rt_annot_hit layers[4] = {{0}};
struct rt_annot_hit diagnostic_hits[2] = {{0}};
struct rt_annot_hit top = {0};
struct xray ray;
size_t layer_count;
fastf_t color[3] = {0.0, 0.0, 0.0};
const fastf_t alpha = 128.0 / 255.0;
const fastf_t remainder = 1.0 - alpha;
const fastf_t expected_color[3] = {
alpha + remainder * remainder * alpha,
alpha + remainder * alpha,
alpha + remainder * remainder * remainder * alpha
};
/* m35.g has this broad table entry, which includes the synthetic ID 0
* assigned to bare solids. It must not replace annotation colors. */
db_mater_add(dbip, 0, 15000, 210, 146, 1, MATER_NO_ADDR);
check_annot_ray(dbip, "ray.annot", default_color);
if (db5_update_attribute("ray.annot", "rgb", "12/34/56", dbip))
bu_exit(1, "unable to set annotation raytrace color\n");
check_annot_ray(dbip, "ray.annot", annotation_color);
check_annot_ray(dbip, "ray.r", region_color);
VSET(ray.r_pt, 0.0, 0.0, 0.0);
VSET(ray.r_dir, 1.0, 0.0, 0.0);
layer_count = rt_annot_scene_query_layers(scene, &view, &ray,
50.0, 50.0, INFINITY, layers, 2);
if (!scene || layer_count != 4 ||
bu_strcmp(layers[0].path, "/alpha-far.annot") ||
bu_strcmp(layers[1].path, "/alpha-near.annot") ||
layers[1].segment != 0)
bu_exit(1, "annotation layer count or capacity handling failed: "
"count=%zu first=%s second=%s/%zu\n", layer_count,
layers[0].path ? layers[0].path : "(null)",
layers[1].path ? layers[1].path : "(null)", layers[1].segment);
if (rt_annot_scene_query_layers(scene, &view, &ray, 50.0, 50.0,
INFINITY, layers, 4) != 4 ||
!NEAR_EQUAL(layers[0].distance, 20.0, SMALL_FASTF) ||
!NEAR_EQUAL(layers[1].distance, 10.0, SMALL_FASTF) ||
layers[1].segment != 0 ||
layers[2].segment != 1 || !layers[3].screen_space)
bu_exit(1, "annotation layers are not back-to-front\n");
if (!rt_annot_scene_query(scene, &view, &ray, 50.0, 50.0,
INFINITY, &top) || !top.screen_space ||
bu_strcmp(top.path, "/alpha-screen.annot"))
bu_exit(1, "annotation top-layer compatibility query changed\n");
if (rt_annot_scene_query_layers(scene, &view, &ray, 50.0, 50.0,
15.0, NULL, 0) != 3)
bu_exit(1, "annotation layer visibility filtering failed\n");
if (rt_annot_scene_query_model(scene, &ray, INFINITY,
diagnostic_hits, 2) != 2 ||
bu_strcmp(diagnostic_hits[0].path, "/alpha-near.annot") ||
diagnostic_hits[0].segment != 1 ||
bu_strcmp(diagnostic_hits[1].path, "/alpha-far.annot"))
bu_exit(1, "annotation diagnostic hits changed layer semantics\n");
if (!rt_annot_scene_composite(scene, &view, &ray, 50.0, 50.0,
INFINITY, color, &top) ||
!VNEAR_EQUAL(color, expected_color, SMALL_FASTF) ||
!top.screen_space)
bu_exit(1, "annotation alpha compositing order failed\n");
MAT_DELTAS(view.model2view, 0.0, 0.0, -2.0);
view.width = 160;
view.height = 90;
view.perspective = 60.0;
if (rt_annot_scene_query_layers(scene, &view, &ray, 80.0, 45.0,
INFINITY, NULL, 0) != 4)
bu_exit(1, "perspective screen annotation coverage failed\n");
rt_annot_scene_destroy(scene);
}
dp = lookup(dbip, "leader.annot");
MAT_DELTAS(transform, -5.0, 6.0, 7.0);
RT_DB_INTERNAL_INIT(&intern);
if (rt_db_get_internal(&intern, dp, dbip, transform) != ID_ANNOT)
+36 -21
View File
@@ -414,13 +414,20 @@ flat_dsp_analytic(uint32_t xcnt, uint32_t ycnt, unsigned short h,
/* Build an in-memory DB, prep, and run comparison */
/* ------------------------------------------------------------------ */
enum dsp_ray_check_mode {
DSP_RAY_CHECK_NONE,
DSP_RAY_CHECK_FLAT,
DSP_RAY_CHECK_SMOOTH,
DSP_RAY_CHECK_BC_CUT
};
struct dsp_case {
const char *label;
uint32_t xcnt, ycnt;
const unsigned short *buf;
int cuttype;
int smooth;
int ray_checks;
enum dsp_ray_check_mode ray_checks;
/* stom diagonal (identity: dx=dy=dz=1): */
double dx, dy, dz;
/* analytic SA/vol (-1 if not available): */
@@ -550,7 +557,7 @@ check_smooth_normal(const struct dsp_case *tc, struct soltab *stp)
static int
run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
{
if (!tc->ray_checks)
if (tc->ray_checks == DSP_RAY_CHECK_NONE)
return 0;
int failures = 0;
@@ -562,16 +569,14 @@ run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
double x_extent = (tc->xcnt - 1) * tc->dx;
double y_extent = (tc->ycnt - 1) * tc->dy;
/* ray_checks == 1 is reserved for flat DSP cases, so buf[0] is the
* expected terrain height for the deterministic vertical/side rays.
*/
double top_z = (double)tc->buf[0] * tc->dz;
double expected_vertical_in = 10.0;
double expected_vertical_out = expected_vertical_in + top_z;
point_t o;
vect_t d;
if (tc->ray_checks == 1) {
if (tc->ray_checks == DSP_RAY_CHECK_FLAT) {
/* Flat cases use buf[0] as the terrain height for direct rays. */
double top_z = (double)tc->buf[0] * tc->dz;
double expected_vertical_in = 10.0;
double expected_vertical_out = expected_vertical_in + top_z;
VSET(o, 0.5 * x_extent, 0.5 * y_extent, top_z + 10.0);
VSET(d, 0.0, 0.0, -1.0);
failures += check_direct_ray("vertical down flat DSP", stp, rtip,
@@ -598,12 +603,22 @@ run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
failures += check_direct_ray("vertical up flat DSP", stp, rtip,
o, d, 1, expected_vertical_in,
expected_vertical_out, 0);
} else if (tc->ray_checks == 2) {
} else if (tc->ray_checks == DSP_RAY_CHECK_SMOOTH) {
VSET(o, 0.5 * x_extent, 0.5 * y_extent, 10000.0);
VSET(d, 0.0, 0.0, -1.0);
failures += check_direct_ray("smooth-mode direct vertical shot", stp, rtip,
o, d, 1, -1.0, -1.0, 0);
failures += check_smooth_normal(tc, stp);
} else if (tc->ray_checks == DSP_RAY_CHECK_BC_CUT) {
/* In cell (3,3), the B-C triangle rises from 315 at y=3.25 by
* 68 per x. A horizontal ray at z=330 enters at x=3+15/68
* and exits through the outer wall at x=4. This also checks
* that the explicit B-C cut gives the top an outward normal.
*/
VSET(o, 2.5, 3.25, 330.0);
VSET(d, 1.0, 0.0, 0.0);
failures += check_direct_ray("explicit B-C top-to-side shot", stp, rtip,
o, d, 1, 0.5 + 15.0 / 68.0, 1.5, 1);
}
return failures;
@@ -782,7 +797,7 @@ test_synthetic(void)
struct dsp_case tc = {
"flat 5x5 h=100 (4x4 cells, identity stom)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 1, 1.0, 1.0, 1.0, asa, avol
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_FLAT, 1.0, 1.0, 1.0, asa, avol
};
failures += run_inmem_case(&tc);
}
@@ -798,7 +813,7 @@ test_synthetic(void)
struct dsp_case tc = {
"flat 10x10 h=200 (9x9 cells)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, asa, avol
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, asa, avol
};
failures += run_inmem_case(&tc);
}
@@ -814,7 +829,7 @@ test_synthetic(void)
struct dsp_case tc = {
"flat 10x10 h=50 (stom dx=5 dy=5 dz=2)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 1, 5.0, 5.0, 2.0, asa, avol
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_FLAT, 5.0, 5.0, 2.0, asa, avol
};
failures += run_inmem_case(&tc);
}
@@ -829,7 +844,7 @@ test_synthetic(void)
struct dsp_case tc = {
"ramp 9x9 (h=100+20x+10y)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -844,7 +859,7 @@ test_synthetic(void)
struct dsp_case tc = {
"asymmetric 5x5 explicit ULlr cut",
GW, GH, buf, DSP_CUT_DIR_ULlr, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_ULlr, 0, DSP_RAY_CHECK_BC_CUT, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -859,7 +874,7 @@ test_synthetic(void)
struct dsp_case tc = {
"asymmetric 5x5 adaptive cut",
GW, GH, buf, DSP_CUT_DIR_ADAPT, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_ADAPT, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -874,7 +889,7 @@ test_synthetic(void)
struct dsp_case tc = {
"smooth=1 ramp normal check",
GW, GH, buf, DSP_CUT_DIR_llUR, 1, 2, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 1, DSP_RAY_CHECK_SMOOTH, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -889,7 +904,7 @@ test_synthetic(void)
struct dsp_case tc = {
"smooth=2 ramp normal check",
GW, GH, buf, DSP_CUT_DIR_llUR, 2, 2, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 2, DSP_RAY_CHECK_SMOOTH, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -908,7 +923,7 @@ test_synthetic(void)
struct dsp_case tc = {
"sinusoidal 16x16",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -928,7 +943,7 @@ test_synthetic(void)
struct dsp_case tc = {
"complex 33x33 wave",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
bu_free(buf, "33x33 buf");
@@ -971,7 +986,7 @@ test_synthetic(void)
struct dsp_case tc = {
"larger 129x129 mixed ramp/wave terrain",
grid_width, grid_height, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
grid_width, grid_height, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
bu_free(sin_x, "129x129 sin x");
+136
View File
@@ -0,0 +1,136 @@
/* P N T S _ B O U N D S . C
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library 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 this file; see the file named COPYING for more
* information.
*/
#include "common.h"
#include <string.h>
#include "bu/app.h"
#include "bu/log.h"
#include "raytrace.h"
#include "rt/geom.h"
int
main(int argc, char **argv)
{
struct pnt_scale head = {0};
struct pnt_scale large = {0};
struct pnt_scale small = {0};
struct rt_pnts_internal pnts = {0};
struct rt_db_internal intern;
struct soltab st = {0};
struct rt_i *rtip;
struct resource *resp = &rt_uniresource;
struct application ap;
struct xray ray = {0};
struct seg seghead;
struct seg *segp;
point_t min, max, expected_min, expected_max;
vect_t invdir;
int failed = 0;
(void)argc;
bu_setprogname(argv[0]);
BU_LIST_INIT(&head.l);
VSET(large.v, 0, 0, 0);
large.s = 3.0;
BU_LIST_APPEND(&head.l, &large.l);
VSET(small.v, 10, 0, 0);
small.s = 0.5;
BU_LIST_APPEND(&head.l, &small.l);
pnts.magic = RT_PNTS_INTERNAL_MAGIC;
pnts.scale = 1.0;
pnts.type = RT_PNT_TYPE_SCA;
pnts.count = 2;
pnts.point = &head;
RT_DB_INTERNAL_INIT(&intern);
intern.idb_major_type = DB5_MAJORTYPE_BRLCAD;
intern.idb_type = ID_PNTS;
intern.idb_meth = &OBJ[ID_PNTS];
intern.idb_ptr = &pnts;
if (OBJ[ID_PNTS].ft_bbox(&intern, &min, &max, NULL)) {
bu_log("PNTS bbox failed\n");
return 1;
}
VSET(expected_min, -3, -3, -3);
VSET(expected_max, 10.5, 3, 3);
if (!VNEAR_EQUAL(min, expected_min, SMALL_FASTF) ||
!VNEAR_EQUAL(max, expected_max, SMALL_FASTF)) {
bu_log("PNTS bbox (%g %g %g) .. (%g %g %g) ignored a per-point scale\n",
V3ARGS(min), V3ARGS(max));
failed = 1;
}
if (!OBJ[ID_PNTS].ft_use_rpp) {
bu_log("PNTS solid-box pruning is disabled\n");
failed = 1;
}
pnts.count = 0;
if (!OBJ[ID_PNTS].ft_bbox(&intern, &min, &max, NULL)) {
bu_log("empty PNTS unexpectedly returned a usable box\n");
failed = 1;
}
pnts.count = 2;
rtip = rt_dirbuild_inmem(NULL, 0, NULL, 0);
if (!rtip)
return 1;
rt_init_resource(resp, 0, rtip);
st.l.magic = RT_SOLTAB_MAGIC;
st.l2.magic = RT_SOLTAB2_MAGIC;
st.st_rtip = rtip;
st.st_meth = &OBJ[ID_PNTS];
st.st_id = ID_PNTS;
if (OBJ[ID_PNTS].ft_prep(&st, &intern, rtip)) {
bu_log("PNTS prep failed\n");
rt_i_destroy(rtip);
return 1;
}
RT_APPLICATION_INIT(&ap);
ap.a_rt_i = rtip;
ap.a_resource = resp;
ray.magic = RT_RAY_MAGIC;
VSET(ray.r_pt, -5, 2.5, 0);
VSET(ray.r_dir, 1, 0, 0);
VSET(invdir, 1, INFINITY, INFINITY);
if (!rt_in_rpp(&ray, invdir, st.st_min, st.st_max)) {
bu_log("PNTS bounds rejected a ray that hits the larger point\n");
failed = 1;
} else {
BU_LIST_INIT(&seghead.l);
if (OBJ[ID_PNTS].ft_shot(&st, &ray, &ap, &seghead) <= 0 ||
BU_LIST_IS_EMPTY(&seghead.l)) {
bu_log("PNTS shot missed the larger point\n");
failed = 1;
}
while (BU_LIST_WHILE(segp, seg, &seghead.l)) {
BU_LIST_DEQUEUE(&segp->l);
RT_FREE_SEG(segp, resp);
}
}
OBJ[ID_PNTS].ft_free(&st);
rt_i_destroy(rtip);
return failed ? 1 : 0;
}

Some files were not shown because too many files have changed in this diff Show More