Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc4837815c |
@@ -1,246 +0,0 @@
|
||||
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.
|
||||
@@ -132,10 +132,6 @@ 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".
|
||||
|
||||
@@ -201,11 +197,7 @@ 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 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.
|
||||
*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:
|
||||
|
||||
|
||||
*r*::
|
||||
@@ -286,41 +278,6 @@ 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
|
||||
|
||||
@@ -564,15 +521,6 @@ 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
|
||||
|
||||
@@ -25,7 +25,7 @@ where the "char" argument is replaced by a single character identifying the cate
|
||||
[[nirt_format_categories]]
|
||||
== Output Categories
|
||||
|
||||
NIRT offers eight categories of output reporting:
|
||||
NIRT offers seven categories of output reporting:
|
||||
|
||||
[cols="3*"]
|
||||
[%noheader]
|
||||
@@ -33,9 +33,6 @@ NIRT offers eight 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.
|
||||
@@ -65,7 +62,7 @@ NIRT offers eight categories of output reporting:
|
||||
[[nirt_format_str]]
|
||||
== Format Strings
|
||||
|
||||
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'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_format_keys]]
|
||||
@@ -103,33 +100,6 @@ 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]
|
||||
|
||||
@@ -40,11 +40,12 @@ 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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
The supported dimensions follow the general presentation conventions of ISO
|
||||
129-1. Lettering defaults to OSIFont, an ISO 3098-style technical font shipped
|
||||
@@ -208,9 +209,9 @@ Set the persistent annotation color using integer red, green, and blue values
|
||||
from 0 through 255.
|
||||
|
||||
*--line-width width*::
|
||||
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.
|
||||
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.
|
||||
|
||||
*--line-style style*::
|
||||
Set semantic line-pattern metadata to *continuous*, *dashed*, *dotted*,
|
||||
|
||||
@@ -227,32 +227,6 @@ 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
|
||||
@@ -363,26 +337,6 @@ struct rt_crofton_result {
|
||||
*/
|
||||
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.
|
||||
|
||||
@@ -29,19 +29,10 @@
|
||||
#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
|
||||
@@ -49,82 +40,6 @@ 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
|
||||
|
||||
/** @} */
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# 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
|
||||
|
||||
@@ -45,6 +45,14 @@ cmakefiles(
|
||||
asciiquack/vendor/cxxopts.hpp
|
||||
db2adoc/db2adoc.py
|
||||
db2adoc/db2adoc.xsl
|
||||
brep_corpus_audit.py
|
||||
step_corpus_capture.py
|
||||
step_corpus_run.py
|
||||
step_corpus_summary.py
|
||||
step_corpus_verify.py
|
||||
step_external_corpus_expectations.json
|
||||
step_local_external_corpus_expectations.json
|
||||
step_dependency_subset.py
|
||||
inactvhdrs/CMakeLists.txt
|
||||
inactvhdrs/inactvhdrs.cpp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,619 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit BRep wireframe and fast shaded realizations in a directory of .g files.
|
||||
|
||||
The companion ``brep-audit`` executable does one BRep per process. This
|
||||
driver inventories each database, runs those isolated checks, checkpoints
|
||||
every result to JSON Lines, and writes JSON and Markdown summaries. Geometry
|
||||
findings do not make the driver fail: they are the intended work list.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from collections import Counter
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FORMAT = "brlcad-brep-corpus-audit-v1"
|
||||
HELPER_FORMAT = "brlcad-brep-realization-audit-v1"
|
||||
|
||||
|
||||
def file_identity(path):
|
||||
try:
|
||||
info = path.stat()
|
||||
return {"size": info.st_size, "mtime_ns": info.st_mtime_ns}
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def text_tail(value, limit=8000):
|
||||
return (value or "")[-limit:]
|
||||
|
||||
|
||||
def last_phase(stderr):
|
||||
phases = re.findall(r"^brep-audit: phase=([a-z]+)$", stderr or "", re.MULTILINE)
|
||||
return phases[-1] if phases else None
|
||||
|
||||
|
||||
def process_rss_bytes(pid):
|
||||
"""Read current Linux RSS, returning zero when it is unavailable."""
|
||||
try:
|
||||
with open("/proc/{}/status".format(pid), "r", encoding="ascii") as stream:
|
||||
for line in stream:
|
||||
if line.startswith("VmRSS:"):
|
||||
return int(line.split()[1]) * 1024
|
||||
except (OSError, ValueError, IndexError):
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def kill_process(process):
|
||||
try:
|
||||
if os.name == "posix":
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
else:
|
||||
process.kill()
|
||||
except (OSError, ProcessLookupError):
|
||||
pass
|
||||
|
||||
|
||||
def file_tail(stream, limit):
|
||||
stream.flush()
|
||||
size = os.fstat(stream.fileno()).st_size
|
||||
stream.seek(max(0, size - limit))
|
||||
return stream.read().decode("utf-8", errors="replace"), size > limit
|
||||
|
||||
|
||||
def run_process(command, timeout, max_rss_mib=0):
|
||||
"""Run and monitor one helper, retaining enough detail for diagnostics."""
|
||||
started = time.monotonic()
|
||||
with tempfile.TemporaryFile() as stdout_file, \
|
||||
tempfile.TemporaryFile() as stderr_file:
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdout=stdout_file,
|
||||
stderr=stderr_file,
|
||||
start_new_session=(os.name == "posix"),
|
||||
)
|
||||
except OSError as error:
|
||||
return {
|
||||
"returncode": 127,
|
||||
"stdout": "",
|
||||
"stdout_truncated": False,
|
||||
"stderr": str(error),
|
||||
"elapsed_seconds": time.monotonic() - started,
|
||||
"observed_peak_rss_bytes": 0,
|
||||
"termination_reason": "launch_error",
|
||||
}
|
||||
|
||||
peak_rss = 0
|
||||
termination_reason = None
|
||||
while True:
|
||||
elapsed = time.monotonic() - started
|
||||
remaining = timeout - elapsed
|
||||
if remaining <= 0:
|
||||
termination_reason = "timeout"
|
||||
kill_process(process)
|
||||
process.wait()
|
||||
break
|
||||
try:
|
||||
process.wait(timeout=min(0.1, remaining))
|
||||
peak_rss = max(peak_rss, process_rss_bytes(process.pid))
|
||||
break
|
||||
except subprocess.TimeoutExpired:
|
||||
rss = process_rss_bytes(process.pid)
|
||||
peak_rss = max(peak_rss, rss)
|
||||
if max_rss_mib and rss > max_rss_mib * 1024 * 1024:
|
||||
termination_reason = "rss_limit"
|
||||
kill_process(process)
|
||||
process.wait()
|
||||
break
|
||||
|
||||
stdout, stdout_truncated = file_tail(
|
||||
stdout_file, 16 * 1024 * 1024
|
||||
)
|
||||
stderr, _ = file_tail(stderr_file, 64 * 1024)
|
||||
|
||||
return {
|
||||
"returncode": process.returncode,
|
||||
"stdout": stdout,
|
||||
"stdout_truncated": stdout_truncated,
|
||||
"stderr": stderr,
|
||||
"elapsed_seconds": time.monotonic() - started,
|
||||
"observed_peak_rss_bytes": peak_rss,
|
||||
"termination_reason": termination_reason,
|
||||
}
|
||||
|
||||
|
||||
def synthetic_record(database, object_name, status, issue, run, signature):
|
||||
return {
|
||||
"format": HELPER_FORMAT,
|
||||
"scope": "brep" if object_name else "database",
|
||||
"database": str(database),
|
||||
"object": object_name,
|
||||
"status": status,
|
||||
"reference": None,
|
||||
"wireframe": None,
|
||||
"shaded": None,
|
||||
"issues": [issue],
|
||||
"audit_signature": signature,
|
||||
"database_identity": file_identity(database),
|
||||
"process": {
|
||||
"return_code": run["returncode"],
|
||||
"elapsed_seconds": run["elapsed_seconds"],
|
||||
"observed_peak_rss_bytes": run["observed_peak_rss_bytes"],
|
||||
"termination_reason": run["termination_reason"],
|
||||
"last_phase": last_phase(run["stderr"]),
|
||||
"stdout_tail": text_tail(run["stdout"]),
|
||||
"stderr_tail": text_tail(run["stderr"]),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def parse_helper_json(output):
|
||||
for line in reversed(output.splitlines()):
|
||||
if not line.lstrip().startswith("{"):
|
||||
continue
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def audit_one(helper, database, object_name, args, signature):
|
||||
command = [
|
||||
str(helper),
|
||||
"--ratio-min", str(args.ratio_min),
|
||||
"--ratio-max", str(args.ratio_max),
|
||||
"--tess-abs", str(args.tess_abs),
|
||||
"--tess-rel", str(args.tess_rel),
|
||||
"--tess-norm", str(args.tess_norm),
|
||||
]
|
||||
if args.address_space_limit_mib:
|
||||
command.extend([
|
||||
"--memory-limit-mib", str(args.address_space_limit_mib)
|
||||
])
|
||||
command.extend([str(database), object_name])
|
||||
run = run_process(command, args.timeout, args.max_rss_mib)
|
||||
|
||||
if run["termination_reason"] == "timeout":
|
||||
phase = last_phase(run["stderr"])
|
||||
return synthetic_record(
|
||||
database, object_name, "timeout",
|
||||
"{}_timeout".format(phase) if phase else "process_timeout",
|
||||
run, signature
|
||||
)
|
||||
if run["termination_reason"] == "rss_limit":
|
||||
phase = last_phase(run["stderr"])
|
||||
return synthetic_record(
|
||||
database, object_name, "memory_limit",
|
||||
"{}_rss_limit_exceeded".format(phase)
|
||||
if phase else "process_rss_limit_exceeded",
|
||||
run, signature
|
||||
)
|
||||
if run["termination_reason"] == "launch_error":
|
||||
return synthetic_record(
|
||||
database, object_name, "error", "helper_launch_failed", run, signature
|
||||
)
|
||||
|
||||
record = parse_helper_json(run["stdout"])
|
||||
if record is None:
|
||||
issue = (
|
||||
"process_signal_{}".format(-run["returncode"])
|
||||
if run["returncode"] is not None and run["returncode"] < 0
|
||||
else "helper_output_not_json"
|
||||
)
|
||||
return synthetic_record(
|
||||
database, object_name, "error", issue, run, signature
|
||||
)
|
||||
if record.get("format") != HELPER_FORMAT:
|
||||
return synthetic_record(
|
||||
database, object_name, "error", "unexpected_helper_format",
|
||||
run, signature
|
||||
)
|
||||
|
||||
record["scope"] = "brep"
|
||||
record["audit_signature"] = signature
|
||||
record["database_identity"] = file_identity(database)
|
||||
record["process"] = {
|
||||
"return_code": run["returncode"],
|
||||
"elapsed_seconds": run["elapsed_seconds"],
|
||||
"observed_peak_rss_bytes": run["observed_peak_rss_bytes"],
|
||||
"termination_reason": None,
|
||||
"last_phase": last_phase(run["stderr"]),
|
||||
"stdout_tail": "",
|
||||
"stderr_tail": text_tail(run["stderr"]),
|
||||
}
|
||||
expected_return = 0 if record.get("status") == "ok" else 1
|
||||
if run["returncode"] != expected_return:
|
||||
record["status"] = "error"
|
||||
record.setdefault("issues", []).append("helper_exit_status_mismatch")
|
||||
return record
|
||||
|
||||
|
||||
def list_breps(helper, database, timeout, signature):
|
||||
run = run_process([str(helper), "--list", str(database)], timeout)
|
||||
if run["termination_reason"] == "timeout":
|
||||
return [], synthetic_record(
|
||||
database, "", "timeout", "brep_inventory_timeout", run, signature
|
||||
)
|
||||
if run["returncode"] != 0:
|
||||
return [], synthetic_record(
|
||||
database, "", "error", "brep_inventory_failed", run, signature
|
||||
)
|
||||
if run["stdout_truncated"]:
|
||||
return [], synthetic_record(
|
||||
database, "", "error", "brep_inventory_output_too_large",
|
||||
run, signature
|
||||
)
|
||||
names = sorted(set(line.strip() for line in run["stdout"].splitlines()
|
||||
if line.strip()))
|
||||
return names, None
|
||||
|
||||
|
||||
def git_state(start):
|
||||
try:
|
||||
revision = subprocess.check_output(
|
||||
["git", "-C", str(start), "rev-parse", "HEAD"],
|
||||
stderr=subprocess.DEVNULL, text=True,
|
||||
).strip()
|
||||
dirty = bool(subprocess.check_output(
|
||||
["git", "-C", str(start), "status", "--porcelain"],
|
||||
stderr=subprocess.DEVNULL, text=True,
|
||||
).strip())
|
||||
return revision, dirty
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return "unknown", None
|
||||
|
||||
|
||||
def read_checkpoint(path, signature):
|
||||
records = {}
|
||||
if not path.is_file():
|
||||
return records
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
for number, line in enumerate(stream, 1):
|
||||
try:
|
||||
record = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
print("ignoring malformed checkpoint line {} in {}".format(
|
||||
number, path), file=sys.stderr)
|
||||
continue
|
||||
if record.get("audit_signature") != signature:
|
||||
continue
|
||||
records[(record.get("database", ""), record.get("object", ""))] = record
|
||||
return records
|
||||
|
||||
|
||||
def all_issues(record):
|
||||
issues = list(record.get("issues") or [])
|
||||
for mode in ("wireframe", "shaded"):
|
||||
result = record.get(mode)
|
||||
if result:
|
||||
issues.extend(result.get("issues") or [])
|
||||
return list(dict.fromkeys(str(issue) for issue in issues))
|
||||
|
||||
|
||||
def format_vector(values):
|
||||
if not values:
|
||||
return "-"
|
||||
return " × ".join("{:.5g}".format(value) for value in values)
|
||||
|
||||
|
||||
def format_geometry(result, primitive):
|
||||
if not result:
|
||||
return "-"
|
||||
ratios = result.get("dimension_ratios")
|
||||
ratio_text = ""
|
||||
if ratios:
|
||||
ratio_text = "; ratios " + "/".join(
|
||||
"-" if value is None else "{:.3g}".format(value)
|
||||
for value in ratios
|
||||
)
|
||||
return "{} points, {} {}{}".format(
|
||||
result.get("vertices", 0), result.get("primitives", 0),
|
||||
primitive, ratio_text,
|
||||
)
|
||||
|
||||
|
||||
def markdown_escape(value):
|
||||
return str(value).replace("|", "\\|").replace("\n", "<br>")
|
||||
|
||||
|
||||
def worklist_markdown(summary, input_directory):
|
||||
totals = summary["totals"]
|
||||
lines = [
|
||||
"# BRep realization audit",
|
||||
"",
|
||||
"Revision: `{}`{}".format(
|
||||
summary["revision"], " (dirty)" if summary.get("dirty") else ""
|
||||
),
|
||||
"",
|
||||
(
|
||||
"{} databases, {} BReps: {} okay, {} geometry findings, "
|
||||
"{} timeouts, {} memory-limit stops, and {} process errors."
|
||||
).format(
|
||||
totals["databases"], totals["breps"], totals["ok"], totals["fail"],
|
||||
totals["timeout"], totals["memory_limit"], totals["error"],
|
||||
),
|
||||
"",
|
||||
(
|
||||
"Accepted generated/reference dimension ratios: {}–{}. "
|
||||
"Shaded tessellation tolerance: abs {}, rel {}, norm {}."
|
||||
).format(
|
||||
summary["options"]["ratio_min"], summary["options"]["ratio_max"],
|
||||
summary["options"]["tess_abs"], summary["options"]["tess_rel"],
|
||||
summary["options"]["tess_norm"],
|
||||
),
|
||||
"",
|
||||
"## Work list",
|
||||
"",
|
||||
]
|
||||
findings = [
|
||||
record for record in summary["results"]
|
||||
if record.get("status") != "ok"
|
||||
]
|
||||
if not findings:
|
||||
lines.append("No findings.")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
lines.extend([
|
||||
"| Database | BRep | Status | Reference dimensions | Wireframe | Shaded | Peak RSS MiB | Issues |",
|
||||
"|---|---|---|---|---|---|---:|---|",
|
||||
])
|
||||
for record in findings:
|
||||
database = Path(record.get("database", ""))
|
||||
try:
|
||||
database_name = str(database.relative_to(input_directory))
|
||||
except ValueError:
|
||||
database_name = str(database)
|
||||
wire = record.get("wireframe")
|
||||
shaded = record.get("shaded")
|
||||
peaks = [
|
||||
int(result.get("peak_rss_bytes") or 0)
|
||||
for result in (wire, shaded) if result
|
||||
]
|
||||
process = record.get("process") or {}
|
||||
peaks.append(int(process.get("observed_peak_rss_bytes") or 0))
|
||||
reference = record.get("reference") or {}
|
||||
lines.append("| {} | {} | {} | {} | {} | {} | {:.1f} | {} |".format(
|
||||
markdown_escape(database_name),
|
||||
markdown_escape(record.get("object") or "(inventory)"),
|
||||
markdown_escape(record.get("status", "error")),
|
||||
markdown_escape(format_vector(reference.get("dimensions"))),
|
||||
markdown_escape(format_geometry(wire, "segments")),
|
||||
markdown_escape(format_geometry(shaded, "triangles")),
|
||||
max(peaks, default=0) / (1024.0 * 1024.0),
|
||||
markdown_escape(", ".join(all_issues(record)) or "unspecified"),
|
||||
))
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input_directory", type=Path)
|
||||
parser.add_argument("output_directory", type=Path)
|
||||
parser.add_argument(
|
||||
"--brep-audit", type=Path, default=Path(".build/bin/brep-audit")
|
||||
)
|
||||
parser.add_argument("-j", "--jobs", type=int, default=1,
|
||||
help="BRep processes to run concurrently (default: 1)")
|
||||
parser.add_argument("--timeout", type=float, default=300.0,
|
||||
help="seconds allowed per BRep")
|
||||
parser.add_argument("--list-timeout", type=float, default=120.0,
|
||||
help="seconds allowed to inventory one database")
|
||||
parser.add_argument("--ratio-min", type=float, default=0.5)
|
||||
parser.add_argument("--ratio-max", type=float, default=2.0)
|
||||
parser.add_argument("--tess-abs", type=float, default=0.0)
|
||||
parser.add_argument("--tess-rel", type=float, default=0.01)
|
||||
parser.add_argument("--tess-norm", type=float, default=0.0)
|
||||
parser.add_argument(
|
||||
"--max-rss-mib", type=int, default=0,
|
||||
help="kill a BRep helper above this observed RSS (zero disables)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--address-space-limit-mib", type=int, default=0,
|
||||
help="set the helper RLIMIT_AS in MiB (zero disables)"
|
||||
)
|
||||
parser.add_argument("--only-file",
|
||||
help="regular expression matched against .g filenames")
|
||||
parser.add_argument("--only-object",
|
||||
help="regular expression matched against BRep names")
|
||||
parser.add_argument("--resume", action="store_true",
|
||||
help="retain checkpointed results with identical options")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.jobs < 1:
|
||||
parser.error("--jobs must be positive")
|
||||
if args.timeout <= 0 or args.list_timeout <= 0:
|
||||
parser.error("timeouts must be positive")
|
||||
if args.ratio_min <= 0 or args.ratio_max < args.ratio_min:
|
||||
parser.error("dimension ratio limits are invalid")
|
||||
if min(args.tess_abs, args.tess_rel, args.tess_norm) < 0:
|
||||
parser.error("tessellation tolerances cannot be negative")
|
||||
if args.max_rss_mib < 0 or args.address_space_limit_mib < 0:
|
||||
parser.error("memory limits cannot be negative")
|
||||
|
||||
input_directory = args.input_directory.resolve()
|
||||
output_directory = args.output_directory.resolve()
|
||||
helper = args.brep_audit.resolve()
|
||||
if not helper.is_file():
|
||||
parser.error("brep-audit not found: {}".format(helper))
|
||||
file_matcher = re.compile(args.only_file) if args.only_file else None
|
||||
object_matcher = re.compile(args.only_object) if args.only_object else None
|
||||
databases = sorted(
|
||||
path.resolve() for path in input_directory.iterdir()
|
||||
if path.is_file() and path.suffix.lower() == ".g" and
|
||||
(not file_matcher or file_matcher.search(path.name))
|
||||
)
|
||||
if not databases:
|
||||
parser.error("no matching .g files in {}".format(input_directory))
|
||||
|
||||
options = {
|
||||
"ratio_min": args.ratio_min,
|
||||
"ratio_max": args.ratio_max,
|
||||
"tess_abs": args.tess_abs,
|
||||
"tess_rel": args.tess_rel,
|
||||
"tess_norm": args.tess_norm,
|
||||
"timeout": args.timeout,
|
||||
"list_timeout": args.list_timeout,
|
||||
"max_rss_mib": args.max_rss_mib,
|
||||
"address_space_limit_mib": args.address_space_limit_mib,
|
||||
"only_file": args.only_file,
|
||||
"only_object": args.only_object,
|
||||
"helper": str(helper),
|
||||
"helper_size": helper.stat().st_size,
|
||||
"helper_mtime_ns": helper.stat().st_mtime_ns,
|
||||
}
|
||||
signature = options
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
checkpoint_path = output_directory / "brep-audit-results.jsonl"
|
||||
records = (
|
||||
read_checkpoint(checkpoint_path, signature) if args.resume else {}
|
||||
)
|
||||
|
||||
tasks = []
|
||||
for index, database in enumerate(databases, 1):
|
||||
print("[inventory {}/{}] {}".format(
|
||||
index, len(databases), database.name), flush=True)
|
||||
names, error_record = list_breps(
|
||||
helper, database, args.list_timeout, signature
|
||||
)
|
||||
if error_record:
|
||||
for key in list(records):
|
||||
if key[0] == str(database):
|
||||
records.pop(key)
|
||||
records[(str(database), "")] = error_record
|
||||
continue
|
||||
records.pop((str(database), ""), None)
|
||||
if object_matcher:
|
||||
names = [name for name in names if object_matcher.search(name)]
|
||||
current_names = set(names)
|
||||
identity = file_identity(database)
|
||||
for key, record in list(records.items()):
|
||||
if key[0] != str(database):
|
||||
continue
|
||||
if key[1] not in current_names or \
|
||||
record.get("database_identity") != identity:
|
||||
records.pop(key)
|
||||
tasks.extend((database, name) for name in names)
|
||||
|
||||
# Rewrite a resumed checkpoint with only the selected run signature, then
|
||||
# append each new result immediately so an interrupted corpus run is usable.
|
||||
with checkpoint_path.open("w", encoding="utf-8") as checkpoint:
|
||||
for record in sorted(
|
||||
records.values(),
|
||||
key=lambda item: (item.get("database", ""), item.get("object", ""))):
|
||||
checkpoint.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
checkpoint.flush()
|
||||
|
||||
pending = [
|
||||
task for task in tasks if (str(task[0]), task[1]) not in records
|
||||
]
|
||||
resumed = len(tasks) - len(pending)
|
||||
if resumed:
|
||||
print("resuming {} completed BRep records".format(resumed), flush=True)
|
||||
|
||||
completed = 0
|
||||
with checkpoint_path.open("a", encoding="utf-8") as checkpoint:
|
||||
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
|
||||
futures = {
|
||||
pool.submit(
|
||||
audit_one, helper, database, object_name, args, signature
|
||||
): (database, object_name)
|
||||
for database, object_name in pending
|
||||
}
|
||||
for future in as_completed(futures):
|
||||
database, object_name = futures[future]
|
||||
try:
|
||||
record = future.result()
|
||||
except Exception as error: # Preserve the rest of a corpus run.
|
||||
run = {
|
||||
"returncode": 125,
|
||||
"stdout": "",
|
||||
"stderr": repr(error),
|
||||
"elapsed_seconds": 0.0,
|
||||
"observed_peak_rss_bytes": 0,
|
||||
"termination_reason": "driver_error",
|
||||
}
|
||||
record = synthetic_record(
|
||||
database, object_name, "error",
|
||||
"audit_driver_exception", run, signature
|
||||
)
|
||||
records[(str(database), object_name)] = record
|
||||
checkpoint.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
checkpoint.flush()
|
||||
completed += 1
|
||||
print("[audit {}/{}] {}:{} {}".format(
|
||||
completed, len(pending), database.name, object_name,
|
||||
record["status"],
|
||||
), flush=True)
|
||||
|
||||
results = sorted(
|
||||
records.values(),
|
||||
key=lambda item: (item.get("database", ""), item.get("object", "")),
|
||||
)
|
||||
statuses = Counter(
|
||||
record.get("status", "error")
|
||||
for record in results if record.get("scope") == "brep"
|
||||
)
|
||||
database_errors = [
|
||||
record for record in results if record.get("scope") == "database"
|
||||
]
|
||||
revision, dirty = git_state(Path(__file__).resolve().parents[2])
|
||||
summary = {
|
||||
"format": FORMAT,
|
||||
"generated_utc": datetime.datetime.now(
|
||||
datetime.timezone.utc
|
||||
).isoformat(),
|
||||
"revision": revision,
|
||||
"dirty": dirty,
|
||||
"input_directory": str(input_directory),
|
||||
"output_directory": str(output_directory),
|
||||
"helper": str(helper),
|
||||
"options": options,
|
||||
"totals": {
|
||||
"databases": len(databases),
|
||||
"database_inventory_errors": len(database_errors),
|
||||
"breps": sum(statuses.values()),
|
||||
"ok": statuses["ok"],
|
||||
"fail": statuses["fail"],
|
||||
"timeout": statuses["timeout"],
|
||||
"memory_limit": statuses["memory_limit"],
|
||||
"error": statuses["error"] + len(database_errors),
|
||||
},
|
||||
"results": results,
|
||||
}
|
||||
summary_path = output_directory / "brep-audit-summary.json"
|
||||
worklist_path = output_directory / "brep-audit-worklist.md"
|
||||
summary_path.write_text(
|
||||
json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
worklist_path.write_text(
|
||||
worklist_markdown(summary, input_directory), encoding="utf-8"
|
||||
)
|
||||
totals = summary["totals"]
|
||||
print(
|
||||
"audited {} BReps: {} okay; {} work items; reports in {}".format(
|
||||
totals["breps"], totals["ok"],
|
||||
totals["breps"] - totals["ok"] + len(database_errors),
|
||||
output_directory,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture standard swrast overview images for converted STEP databases.
|
||||
|
||||
Each database is opened in its own gsh process so the display manager and GED
|
||||
state are isolated. The two modes are written to separate directories and
|
||||
use the database filename (without its final ``.g``) as the image name.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def image_stem(path):
|
||||
return path.name[:-2] if path.name.lower().endswith(".g") else path.stem
|
||||
|
||||
|
||||
def capture(gsh, model, output, mode, azimuth, elevation, timeout):
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
image = output / (image_stem(model) + ".png")
|
||||
try:
|
||||
tops = subprocess.run(
|
||||
[str(gsh), "--new-cmds", str(model), "tops"],
|
||||
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
timeout=timeout,
|
||||
).stdout.splitlines()
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"model": str(model), "mode": mode, "image": str(image),
|
||||
"returncode": 124, "complete": False,
|
||||
"log": "timed out while listing top-level objects"}
|
||||
tops = [line.strip().rstrip("/") for line in tops
|
||||
if line.strip().endswith("/") and line.strip().count("/") == 1]
|
||||
if not tops:
|
||||
return {"model": str(model), "mode": mode, "image": str(image),
|
||||
"returncode": 1, "complete": False,
|
||||
"log": "database has no top-level objects"}
|
||||
commands = [
|
||||
"dm attach swrast SWDM",
|
||||
"draw -m{} {}".format(mode, " ".join(tops)),
|
||||
"autoview",
|
||||
"ae {} {}".format(azimuth, elevation),
|
||||
"screengrab -D SWDM {}".format(image),
|
||||
"quit",
|
||||
]
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[str(gsh), "--new-cmds", str(model)],
|
||||
input="\n".join(commands) + "\n",
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
return {"model": str(model), "mode": mode, "image": str(image),
|
||||
"returncode": 124, "complete": False,
|
||||
"log": str(error.stdout or "")[-4000:] + "\ntimed out during capture"}
|
||||
return {
|
||||
"model": str(model),
|
||||
"mode": mode,
|
||||
"image": str(image),
|
||||
"returncode": proc.returncode,
|
||||
"complete": proc.returncode == 0 and image.is_file() and image.stat().st_size > 0,
|
||||
"log": proc.stdout[-4000:],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input_directory", type=Path)
|
||||
parser.add_argument("output_directory", type=Path)
|
||||
parser.add_argument("--gsh", type=Path, default=Path(".build/bin/gsh"))
|
||||
parser.add_argument("--jobs", type=int, default=min(2, os.cpu_count() or 1))
|
||||
parser.add_argument("--timeout", type=int, default=300,
|
||||
help="maximum seconds per model and view")
|
||||
parser.add_argument("--mode", choices=("wireframe", "shaded", "both"), default="both")
|
||||
parser.add_argument("--only", help="regular expression matched against model names")
|
||||
parser.add_argument("--azimuth", type=float, default=35.0)
|
||||
parser.add_argument("--elevation", type=float, default=25.0)
|
||||
args = parser.parse_args()
|
||||
if args.jobs < 1:
|
||||
parser.error("--jobs must be positive")
|
||||
if args.timeout < 1:
|
||||
parser.error("--timeout must be positive")
|
||||
if not args.gsh.is_file():
|
||||
parser.error("gsh not found: {}".format(args.gsh))
|
||||
|
||||
matcher = re.compile(args.only) if args.only else None
|
||||
models = sorted(path for path in args.input_directory.glob("*.g")
|
||||
if not matcher or matcher.search(path.name))
|
||||
if not models:
|
||||
parser.error("no converted .g models in {}".format(args.input_directory))
|
||||
modes = [("wireframe", 0), ("shaded", 1)] if args.mode == "both" else [
|
||||
(args.mode, 0 if args.mode == "wireframe" else 1)
|
||||
]
|
||||
jobs = []
|
||||
for name, mode in modes:
|
||||
target = args.output_directory / name
|
||||
jobs.extend((model, target, mode) for model in models)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
|
||||
results = list(pool.map(
|
||||
lambda item: capture(args.gsh, item[0], item[1], item[2],
|
||||
args.azimuth, args.elevation, args.timeout), jobs
|
||||
))
|
||||
args.output_directory.mkdir(parents=True, exist_ok=True)
|
||||
manifest = {
|
||||
"format": "brlcad-step-corpus-capture-v1",
|
||||
"azimuth": args.azimuth,
|
||||
"elevation": args.elevation,
|
||||
"jobs": args.jobs,
|
||||
"results": results,
|
||||
}
|
||||
(args.output_directory / "capture-manifest.json").write_text(
|
||||
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
failed = [item for item in results if not item["complete"]]
|
||||
print("captured {}/{} images".format(len(results) - len(failed), len(results)))
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,501 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the schema-dispatching BRL-CAD STEP importer over an external corpus.
|
||||
|
||||
Unknown schemas are recorded in the run manifest but are not passed to the
|
||||
converter. Each supported input gets a structured report, a diagnostic log,
|
||||
and a tab-separated status record consumed by ``step_corpus_summary.py``.
|
||||
Nested input directories are scanned recursively, and existing complete
|
||||
records may be resumed.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
STEP_SUFFIXES = {".stp", ".step", ".p21"}
|
||||
INCOMPLETE_PROCESS_STATUSES = {124, 130, 137, 143}
|
||||
|
||||
|
||||
def schema_names(path):
|
||||
with path.open("rb") as stream:
|
||||
header = stream.read(2 * 1024 * 1024).decode("latin-1", errors="replace")
|
||||
match = re.search(
|
||||
r"FILE_SCHEMA\s*\(\s*\((.*?)\)\s*\)", header,
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
if not match:
|
||||
return []
|
||||
return [
|
||||
value.replace("''", "'").upper()
|
||||
for value in re.findall(r"'((?:''|[^'])*)'", match.group(1))
|
||||
]
|
||||
|
||||
|
||||
def schema_family(names):
|
||||
"""Mirror the public host's supported-family and interim-profile routing."""
|
||||
families = set()
|
||||
legacy_ap203e2 = {
|
||||
"CONFIGURATION_CONTROL_3D_DESIGN_MIM_LF",
|
||||
"CCD_CLA_GVP_AST",
|
||||
"CCD_CLA_GVP_AST_ASD",
|
||||
}
|
||||
interim_companions = (
|
||||
"GEOMETRIC_VALIDATION_PROPERTIES_MIM",
|
||||
"GEOMETRIC_VALIDATION_PROPERTY_REPRESENTATION_MIM",
|
||||
"SHAPE_APPEARANCE_LAYER_MIM",
|
||||
"SHAPE_APPEARANCE_LAYERS_MIM",
|
||||
)
|
||||
for name in names:
|
||||
if "AP203_CONFIGURATION_CONTROLLED" in name or any(
|
||||
name.startswith(alias) for alias in legacy_ap203e2):
|
||||
families.add("AP203e2")
|
||||
elif "AUTOMOTIVE_DESIGN" in name:
|
||||
families.add("AP214")
|
||||
elif "CONFIG_CONTROL_DESIGN" in name:
|
||||
families.add("AP203")
|
||||
elif "AP242" in name or "MANAGED_MODEL_BASED_3D_ENGINEERING" in name:
|
||||
families.add("AP242")
|
||||
|
||||
if families == {"AP203"} and any(
|
||||
name.startswith(companion)
|
||||
for name in names for companion in interim_companions):
|
||||
return "AP203e2"
|
||||
return next(iter(families)) if len(families) == 1 else "unsupported"
|
||||
|
||||
|
||||
def artifact_stem(path, input_directory):
|
||||
relative = path.relative_to(input_directory).as_posix()
|
||||
readable = re.sub(r"[^A-Za-z0-9_.-]+", "_", relative)
|
||||
# Sanitizing path separators and punctuation is not injective. Retain a
|
||||
# readable prefix while making nested and similarly named inputs safe.
|
||||
identity = hashlib.sha256(relative.encode("utf-8")).hexdigest()[:12]
|
||||
return "{}__{}_".format(readable, identity)
|
||||
|
||||
|
||||
def legacy_artifact_stem(path, input_directory):
|
||||
"""Return the pre-v2 stem so an in-progress corpus can be resumed."""
|
||||
relative = path.relative_to(input_directory).as_posix()
|
||||
return re.sub(r"[^A-Za-z0-9_.-]+", "_", relative) + "_"
|
||||
|
||||
|
||||
def completed_status(path):
|
||||
try:
|
||||
status = int(path.read_text(encoding="utf-8").split("\t", 1)[0])
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
return status not in INCOMPLETE_PROCESS_STATUSES
|
||||
|
||||
|
||||
def synthetic_report(input_path, status, message):
|
||||
return {
|
||||
"format": "brlcad-step-import-report-v1",
|
||||
"input": str(input_path),
|
||||
"output": "",
|
||||
"exit_status": status,
|
||||
"coverage": {
|
||||
"geometry_attempted": 0,
|
||||
"geometry_written": 0,
|
||||
"geometry_skipped": 0,
|
||||
},
|
||||
"validation": {
|
||||
"invalid_breps": 0,
|
||||
"output_failures": 0,
|
||||
"repairs": 0,
|
||||
"inferred_curves": 0,
|
||||
},
|
||||
"diagnostics": [{
|
||||
"severity": "error",
|
||||
"entity_id": 0,
|
||||
"entity_type": "",
|
||||
"attribute": "",
|
||||
"message": message,
|
||||
"count": 1,
|
||||
"aggregated": False,
|
||||
}],
|
||||
"timings_us": {},
|
||||
"peak_rss_bytes": 0,
|
||||
"skipped_items": [],
|
||||
"corpus_run_incomplete": True,
|
||||
}
|
||||
|
||||
|
||||
def file_sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def executable_identity(path):
|
||||
stat = path.stat()
|
||||
return {
|
||||
"path": str(path),
|
||||
"size": stat.st_size,
|
||||
"mtime_ns": stat.st_mtime_ns,
|
||||
"sha256": file_sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def schema_plugin_paths(step_g):
|
||||
"""Locate build/install-tree schema modules used by the plugin host."""
|
||||
plugin_directory = step_g.parent.parent / "libexec" / "step"
|
||||
if not plugin_directory.is_dir():
|
||||
return []
|
||||
return sorted(
|
||||
path for path in plugin_directory.glob("libstep-schema-*")
|
||||
if path.is_file()
|
||||
)
|
||||
|
||||
|
||||
def schema_plugin_identities(step_g):
|
||||
"""Identify build/install-tree schema modules used by the plugin host."""
|
||||
return [executable_identity(path) for path in schema_plugin_paths(step_g)]
|
||||
|
||||
|
||||
def linked_library_identities(executables, runtime_root):
|
||||
"""Hash resolved runtime libraries supplied by the selected build tree.
|
||||
|
||||
Converter and plugin hashes do not detect a rebuilt geometry or STEPcode
|
||||
shared library. On platforms with ``ldd``, retain every resolved library
|
||||
below the executable's build/install root so a long run cannot be mistaken
|
||||
for a homogeneous baseline after one of those dependencies changes.
|
||||
"""
|
||||
libraries = {}
|
||||
for executable in executables:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ldd", str(executable)], check=False, capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except OSError:
|
||||
return []
|
||||
if result.returncode:
|
||||
continue
|
||||
for line in result.stdout.splitlines():
|
||||
match = re.search(r"=>\s+(/\S+)", line)
|
||||
if not match:
|
||||
match = re.match(r"\s*(/\S+)", line)
|
||||
if not match:
|
||||
continue
|
||||
path = Path(match.group(1)).resolve()
|
||||
try:
|
||||
path.relative_to(runtime_root)
|
||||
except ValueError:
|
||||
continue
|
||||
if path.is_file():
|
||||
libraries[str(path)] = path
|
||||
return [executable_identity(libraries[name]) for name in sorted(libraries)]
|
||||
|
||||
|
||||
def host_snapshot():
|
||||
"""Capture lightweight run context without adding a monitoring dependency."""
|
||||
snapshot = {
|
||||
"utc": datetime.datetime.now(datetime.timezone.utc).replace(
|
||||
microsecond=0
|
||||
).isoformat(),
|
||||
}
|
||||
try:
|
||||
snapshot["load_average"] = list(os.getloadavg())
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try:
|
||||
values = {}
|
||||
with Path("/proc/meminfo").open("r", encoding="ascii") as stream:
|
||||
for line in stream:
|
||||
key, value = line.split(":", 1)
|
||||
if key in ("MemTotal", "MemAvailable"):
|
||||
values[key] = int(value.split()[0]) * 1024
|
||||
if values:
|
||||
snapshot["memory_bytes"] = values
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return snapshot
|
||||
|
||||
|
||||
def cpu_model():
|
||||
try:
|
||||
with Path("/proc/cpuinfo").open("r", encoding="ascii") as stream:
|
||||
for line in stream:
|
||||
if line.lower().startswith("model name"):
|
||||
return line.split(":", 1)[1].strip()
|
||||
except (OSError, IndexError):
|
||||
pass
|
||||
return platform.processor()
|
||||
|
||||
|
||||
def write_manifest(path, manifest):
|
||||
"""Atomically preserve progress so an interrupted long run is auditable."""
|
||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def terminate_process(process, force=False):
|
||||
try:
|
||||
if os.name == "nt":
|
||||
process.kill() if force else process.terminate()
|
||||
else:
|
||||
os.killpg(process.pid, signal.SIGKILL if force else signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
|
||||
def run_converter(command, log, timeout):
|
||||
"""Run one converter and terminate its complete process group on timeout."""
|
||||
process = subprocess.Popen(
|
||||
command, stdout=log, stderr=subprocess.STDOUT,
|
||||
start_new_session=os.name != "nt",
|
||||
)
|
||||
try:
|
||||
return process.wait(timeout=timeout if timeout > 0 else None), False
|
||||
except subprocess.TimeoutExpired:
|
||||
terminate_process(process)
|
||||
try:
|
||||
process.wait(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
terminate_process(process, force=True)
|
||||
process.wait()
|
||||
return 124, True
|
||||
except BaseException:
|
||||
terminate_process(process)
|
||||
try:
|
||||
process.wait(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
terminate_process(process, force=True)
|
||||
process.wait()
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input_directory", type=Path)
|
||||
parser.add_argument("output_directory", type=Path)
|
||||
parser.add_argument("--step-g", type=Path, default=Path(".build/bin/step-g"))
|
||||
parser.add_argument("-j", "--jobs", type=int, default=min(12, os.cpu_count() or 1),
|
||||
help="geometry workers inside each converter")
|
||||
parser.add_argument("--previous", type=Path,
|
||||
help="previous corpus-summary.json for trend deltas")
|
||||
parser.add_argument("--expectations", type=Path,
|
||||
help="checksum-pinned corpus expectation manifest")
|
||||
parser.add_argument("--allow-missing-expectations", action="store_true",
|
||||
help="allow expectation entries absent from a filtered run")
|
||||
parser.add_argument("--run-id", help="stable label for this run")
|
||||
parser.add_argument("--resume", action="store_true")
|
||||
parser.add_argument("--exact", action="store_true")
|
||||
parser.add_argument("--timeout", type=float, default=0.0,
|
||||
help="outer wall-clock limit per file in seconds (0 disables)")
|
||||
parser.add_argument("--stall-timeout", type=float,
|
||||
help="override step-g's no-progress timeout in seconds")
|
||||
parser.add_argument(
|
||||
"--only", help="regular expression matched against relative input paths"
|
||||
)
|
||||
parser.add_argument("--list", action="store_true",
|
||||
help="classify inputs without running converters")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.jobs < 1 or args.timeout < 0:
|
||||
parser.error("--jobs must be positive and --timeout must be nonnegative")
|
||||
if args.stall_timeout is not None and args.stall_timeout <= 0:
|
||||
parser.error("--stall-timeout must be positive")
|
||||
input_directory = args.input_directory.resolve()
|
||||
output_directory = args.output_directory.resolve()
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
matcher = re.compile(args.only) if args.only else None
|
||||
inputs = sorted(
|
||||
path for path in input_directory.rglob("*")
|
||||
if path.is_file() and path.suffix.lower() in STEP_SUFFIXES and
|
||||
(not matcher or matcher.search(path.relative_to(input_directory).as_posix()))
|
||||
)
|
||||
if not inputs:
|
||||
parser.error("no matching STEP files in {}".format(input_directory))
|
||||
|
||||
run_id = args.run_id or datetime.datetime.now(
|
||||
datetime.timezone.utc
|
||||
).strftime("%Y%m%dT%H%M%SZ")
|
||||
step_g = args.step_g.resolve()
|
||||
if not step_g.is_file():
|
||||
parser.error("step-g not found: {}".format(step_g))
|
||||
plugin_paths = schema_plugin_paths(step_g)
|
||||
runtime_root = step_g.parent.parent.resolve()
|
||||
manifest = {
|
||||
"format": "brlcad-step-corpus-run-v1",
|
||||
"run_id": run_id,
|
||||
"started_utc": datetime.datetime.now(datetime.timezone.utc).replace(
|
||||
microsecond=0
|
||||
).isoformat(),
|
||||
"host": {
|
||||
"hostname": platform.node(),
|
||||
"platform": platform.platform(),
|
||||
"machine": platform.machine(),
|
||||
"cpu_model": cpu_model(),
|
||||
"logical_cpus": os.cpu_count(),
|
||||
"initial_snapshot": host_snapshot(),
|
||||
},
|
||||
"input_directory": str(input_directory),
|
||||
"output_directory": str(output_directory),
|
||||
"options": {
|
||||
"jobs": args.jobs,
|
||||
"exact": args.exact,
|
||||
"recursive": True,
|
||||
"timeout_seconds": args.timeout,
|
||||
"stall_timeout_seconds": args.stall_timeout,
|
||||
},
|
||||
"step_g": executable_identity(step_g),
|
||||
"schema_plugins": [executable_identity(path) for path in plugin_paths],
|
||||
"linked_libraries": linked_library_identities(
|
||||
[step_g] + plugin_paths, runtime_root
|
||||
),
|
||||
"expectations": str(args.expectations.resolve()) if args.expectations else None,
|
||||
"files": [],
|
||||
}
|
||||
manifest_path = output_directory / "run-manifest.json"
|
||||
write_manifest(manifest_path, manifest)
|
||||
|
||||
for index, input_path in enumerate(inputs, 1):
|
||||
relative_input = input_path.relative_to(input_directory).as_posix()
|
||||
declared_schemas = schema_names(input_path)
|
||||
declared_schema = declared_schemas[0] if declared_schemas else ""
|
||||
family = schema_family(declared_schemas)
|
||||
record = {
|
||||
"input": str(input_path),
|
||||
"declared_schema": declared_schema,
|
||||
"declared_schemas": declared_schemas,
|
||||
"schema": family,
|
||||
}
|
||||
if args.list:
|
||||
print("{}\t{}\t{}".format(
|
||||
family, ";".join(declared_schemas), relative_input
|
||||
))
|
||||
manifest["files"].append(record)
|
||||
write_manifest(manifest_path, manifest)
|
||||
continue
|
||||
input_stat = input_path.stat()
|
||||
record["input_identity"] = {
|
||||
"size": input_stat.st_size,
|
||||
"mtime_ns": input_stat.st_mtime_ns,
|
||||
"sha256": file_sha256(input_path),
|
||||
}
|
||||
if family == "unsupported":
|
||||
record["result"] = "skipped"
|
||||
record["reason"] = "unrecognized schema"
|
||||
manifest["files"].append(record)
|
||||
write_manifest(manifest_path, manifest)
|
||||
print("[{}/{}] skip {} ({})".format(
|
||||
index, len(inputs), relative_input, family
|
||||
))
|
||||
continue
|
||||
|
||||
stem = artifact_stem(input_path, input_directory)
|
||||
report_path = output_directory / (stem + ".json")
|
||||
log_path = output_directory / (stem + ".log")
|
||||
status_path = output_directory / (stem + ".status")
|
||||
if args.resume:
|
||||
legacy_stem = legacy_artifact_stem(input_path, input_directory)
|
||||
legacy_report = output_directory / (legacy_stem + ".json")
|
||||
legacy_status = output_directory / (legacy_stem + ".status")
|
||||
if legacy_report.is_file() and completed_status(legacy_status):
|
||||
stem = legacy_stem
|
||||
report_path = legacy_report
|
||||
log_path = output_directory / (stem + ".log")
|
||||
status_path = legacy_status
|
||||
if args.resume and report_path.is_file() and completed_status(status_path):
|
||||
record["result"] = "resumed"
|
||||
record["report"] = report_path.name
|
||||
manifest["files"].append(record)
|
||||
write_manifest(manifest_path, manifest)
|
||||
print("[{}/{}] keep {}".format(index, len(inputs), relative_input))
|
||||
continue
|
||||
|
||||
command = [
|
||||
str(step_g), "-D", "-j", str(args.jobs),
|
||||
"--report", str(report_path),
|
||||
]
|
||||
if args.exact:
|
||||
command.append("--exact")
|
||||
if args.stall_timeout is not None:
|
||||
command.extend(["--stall-timeout", str(args.stall_timeout)])
|
||||
command.append(str(input_path))
|
||||
print("[{}/{}] {} {}".format(index, len(inputs), family, relative_input),
|
||||
flush=True)
|
||||
record.update({"result": "running", "host_before": host_snapshot()})
|
||||
manifest["files"].append(record)
|
||||
write_manifest(manifest_path, manifest)
|
||||
started = time.monotonic()
|
||||
with log_path.open("wb") as log:
|
||||
try:
|
||||
status, timed_out = run_converter(command, log, args.timeout)
|
||||
message = "converter exited without writing a structured report"
|
||||
if timed_out:
|
||||
message = "converter exceeded the {:.1f}-second outer timeout".format(
|
||||
args.timeout
|
||||
)
|
||||
except OSError as error:
|
||||
status = 127
|
||||
timed_out = False
|
||||
message = "could not execute converter: {}".format(error)
|
||||
elapsed = time.monotonic() - started
|
||||
if not report_path.is_file():
|
||||
report_path.write_text(
|
||||
json.dumps(synthetic_report(input_path, status, message), indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
status_path.write_text(
|
||||
"{}\t{:.3f}\t{}\t{}\n".format(status, elapsed, family, input_path),
|
||||
encoding="utf-8",
|
||||
)
|
||||
record.update({
|
||||
"result": "completed",
|
||||
"process_exit_status": status,
|
||||
"elapsed_seconds": elapsed,
|
||||
"timed_out": timed_out,
|
||||
"report": report_path.name,
|
||||
"log": log_path.name,
|
||||
"host_after": host_snapshot(),
|
||||
})
|
||||
write_manifest(manifest_path, manifest)
|
||||
|
||||
manifest["finished_utc"] = datetime.datetime.now(
|
||||
datetime.timezone.utc
|
||||
).replace(microsecond=0).isoformat()
|
||||
manifest["host"]["final_snapshot"] = host_snapshot()
|
||||
write_manifest(manifest_path, manifest)
|
||||
if not args.list:
|
||||
summary_command = [
|
||||
sys.executable,
|
||||
str(Path(__file__).with_name("step_corpus_summary.py")),
|
||||
str(output_directory),
|
||||
"--run-id", run_id,
|
||||
]
|
||||
if args.previous:
|
||||
summary_command.extend(["--previous", str(args.previous.resolve())])
|
||||
summary_status = subprocess.call(summary_command)
|
||||
if summary_status or not args.expectations:
|
||||
return summary_status
|
||||
verify_command = [
|
||||
sys.executable,
|
||||
str(Path(__file__).with_name("step_corpus_verify.py")),
|
||||
str(output_directory),
|
||||
str(args.expectations.resolve()),
|
||||
]
|
||||
if args.allow_missing_expectations:
|
||||
verify_command.append("--allow-missing")
|
||||
return subprocess.call(verify_command)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,939 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Summarize one BRL-CAD STEP converter corpus run and compare it to another.
|
||||
|
||||
The corpus runner writes one ``NAME.status`` record and one ``NAME.json``
|
||||
converter report per input. A status record is tab separated::
|
||||
|
||||
exit_status<TAB>elapsed_seconds<TAB>schema<TAB>input_path
|
||||
|
||||
This utility intentionally consumes the converters' structured reports rather
|
||||
than scraping diagnostics. It writes a durable JSON record suitable for trend
|
||||
plots and a compact Markdown companion for review.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FORMAT = "brlcad-step-corpus-summary-v2"
|
||||
|
||||
|
||||
INCOMPLETE_PROCESS_STATUSES = {124, 130, 137, 143}
|
||||
|
||||
|
||||
def run_complete(item):
|
||||
"""Return whether an input reached a normal converter terminal state."""
|
||||
if "run_complete" in item:
|
||||
return bool(item["run_complete"])
|
||||
return int(item.get("process_exit_status", 0)) not in INCOMPLETE_PROCESS_STATUSES
|
||||
|
||||
|
||||
def read_json(path):
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
return json.load(stream)
|
||||
|
||||
|
||||
def git_state(start):
|
||||
try:
|
||||
revision = subprocess.check_output(
|
||||
["git", "-C", str(start), "rev-parse", "HEAD"],
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
).strip()
|
||||
dirty = bool(
|
||||
subprocess.check_output(
|
||||
["git", "-C", str(start), "status", "--porcelain"],
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
).strip()
|
||||
)
|
||||
return revision, dirty
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return "unknown", None
|
||||
|
||||
|
||||
def failure_category(reason):
|
||||
text = (reason or "").lower()
|
||||
if any(word in text for word in ("budget", "expired", "timed out", "timeout")):
|
||||
return "work_budget"
|
||||
if any(word in text for word in (
|
||||
"not solid", "non-solid", "solid validation", "validate as a solid"
|
||||
)):
|
||||
return "non_solid"
|
||||
if "structural" in text:
|
||||
return "structural"
|
||||
if "exact" in text or "conversion failed" in text:
|
||||
return "exact_conversion"
|
||||
if "opennurbs" in text:
|
||||
return "structural"
|
||||
if "no importer" in text or "unsupported" in text:
|
||||
return "unsupported"
|
||||
if "output" in text or "database" in text or "write" in text:
|
||||
return "output"
|
||||
return "other"
|
||||
|
||||
|
||||
def parse_status(path):
|
||||
fields = path.read_text(encoding="utf-8").rstrip("\n").split("\t", 3)
|
||||
if len(fields) != 4:
|
||||
raise ValueError("{} is not a four-field corpus status record".format(path))
|
||||
return {
|
||||
"process_exit_status": int(fields[0]),
|
||||
"elapsed_seconds": float(fields[1]),
|
||||
"schema": fields[2],
|
||||
"input": fields[3],
|
||||
}
|
||||
|
||||
|
||||
def occurrence_depth(occurrences):
|
||||
"""Return the maximum product-assembly depth represented by occurrences."""
|
||||
children = {}
|
||||
vertices = set()
|
||||
for occurrence in occurrences:
|
||||
parent = int(occurrence.get("parent_product_id") or 0)
|
||||
child = int(occurrence.get("child_product_id") or 0)
|
||||
if parent <= 0 or child <= 0:
|
||||
continue
|
||||
children.setdefault(parent, set()).add(child)
|
||||
vertices.update((parent, child))
|
||||
|
||||
active = set()
|
||||
memo = {}
|
||||
|
||||
def visit(vertex):
|
||||
if vertex in memo:
|
||||
return memo[vertex]
|
||||
if vertex in active:
|
||||
# A cycle is invalid, but retain a useful finite metric and let the
|
||||
# converter's structured diagnostics report the underlying issue.
|
||||
return 0
|
||||
active.add(vertex)
|
||||
depth = max((1 + visit(child)
|
||||
for child in children.get(vertex, ())), default=0)
|
||||
active.remove(vertex)
|
||||
memo[vertex] = depth
|
||||
return depth
|
||||
|
||||
return max((visit(vertex) for vertex in vertices), default=0)
|
||||
|
||||
|
||||
def report_record(status_path):
|
||||
status = parse_status(status_path)
|
||||
report_path = status_path.with_suffix(".json")
|
||||
if not report_path.is_file():
|
||||
raise ValueError("missing report paired with {}".format(status_path))
|
||||
report = read_json(report_path)
|
||||
coverage = report.get("coverage") or {}
|
||||
validation = report.get("validation") or {}
|
||||
timings = report.get("timings_us") or {}
|
||||
performance = report.get("performance") or {}
|
||||
calibration = performance.get("calibration") or {}
|
||||
stage_timings = performance.get("stages") or {}
|
||||
skipped = report.get("skipped_items") or []
|
||||
occurrences = report.get("occurrence_details") or []
|
||||
configuration_records = report.get("configuration_records") or []
|
||||
representation_coverage = report.get("representation_coverage") or []
|
||||
attempted = int(coverage.get("geometry_attempted") or 0)
|
||||
written = int(coverage.get("geometry_written") or 0)
|
||||
skipped_count = int(coverage.get("geometry_skipped") or len(skipped))
|
||||
categories = Counter(failure_category(item.get("reason")) for item in skipped)
|
||||
invalid_breps = int(validation.get("invalid_breps") or 0)
|
||||
invalid_breps_written = int(validation.get("invalid_breps_written") or 0)
|
||||
invalid_breps_rejected = int(validation.get("invalid_breps_rejected") or 0)
|
||||
output_failures = int(validation.get("output_failures") or 0)
|
||||
inferred_curves = int(validation.get("inferred_curves") or 0)
|
||||
if invalid_breps_written:
|
||||
categories["invalid_brep_preserved"] += invalid_breps_written
|
||||
if output_failures:
|
||||
categories["output"] += output_failures
|
||||
configuration_types = Counter(
|
||||
item.get("type") or "UNKNOWN" for item in configuration_records
|
||||
)
|
||||
representation_statuses = Counter(
|
||||
item.get("status") or "unknown" for item in representation_coverage
|
||||
)
|
||||
converter_status = int(
|
||||
report.get("exit_status", status["process_exit_status"])
|
||||
)
|
||||
intentionally_empty = (
|
||||
status["process_exit_status"] == 6 and converter_status == 6 and
|
||||
attempted == 0 and bool(representation_coverage) and
|
||||
all(item.get("status") == "intentionally_non_geometric"
|
||||
for item in representation_coverage)
|
||||
)
|
||||
input_path = str(report.get("input") or status["input"])
|
||||
result = {
|
||||
"name": os.path.basename(input_path),
|
||||
"input": input_path,
|
||||
"schema": status["schema"],
|
||||
"report": report_path.name,
|
||||
"process_exit_status": status["process_exit_status"],
|
||||
"converter_exit_status": converter_status,
|
||||
"run_complete": status["process_exit_status"] not in
|
||||
INCOMPLETE_PROCESS_STATUSES,
|
||||
"elapsed_seconds": status["elapsed_seconds"],
|
||||
"geometry_attempted": attempted,
|
||||
"geometry_written": written,
|
||||
"geometry_skipped": skipped_count,
|
||||
"success_fraction": (float(written) / attempted) if attempted else 0.0,
|
||||
"products": int(coverage.get("products") or 0),
|
||||
"occurrences": int(coverage.get("occurrences") or 0),
|
||||
"assembly_usage_count": len(report.get("assembly_usages") or []),
|
||||
"occurrence_detail_count": len(occurrences),
|
||||
"occurrence_max_depth": occurrence_depth(occurrences),
|
||||
"occurrence_methods": sorted({
|
||||
item.get("shape_method") or "unknown" for item in occurrences
|
||||
}),
|
||||
"styles_extracted": int(coverage.get("styles_extracted") or 0),
|
||||
"styles_applied": int(coverage.get("styles_applied") or 0),
|
||||
"layers_extracted": int(coverage.get("layers_extracted") or 0),
|
||||
"materials_extracted": int(coverage.get("materials_extracted") or 0),
|
||||
"properties_extracted": int(coverage.get("properties_extracted") or 0),
|
||||
"configuration_record_count": len(configuration_records),
|
||||
"configuration_record_types": dict(sorted(configuration_types.items())),
|
||||
"configuration_metadata_count": len(
|
||||
report.get("configuration_metadata") or {}
|
||||
),
|
||||
"product_alternative_count": len(
|
||||
report.get("product_alternatives") or []
|
||||
),
|
||||
"usage_substitute_count": len(report.get("usage_substitutes") or []),
|
||||
"entity_type_counts": dict(sorted(
|
||||
(coverage.get("entity_counts") or {}).items()
|
||||
)),
|
||||
"unsupported_entity_counts": dict(sorted(
|
||||
(coverage.get("unsupported_counts") or {}).items()
|
||||
)),
|
||||
"representation_status_counts": dict(sorted(
|
||||
representation_statuses.items()
|
||||
)),
|
||||
"clean": status["process_exit_status"] not in
|
||||
INCOMPLETE_PROCESS_STATUSES and attempted > 0 and
|
||||
written == attempted and
|
||||
converter_status == 0 and
|
||||
invalid_breps_written == 0 and output_failures == 0 and
|
||||
inferred_curves == 0,
|
||||
"intentionally_empty": intentionally_empty,
|
||||
"invalid_breps": invalid_breps,
|
||||
"invalid_breps_written": invalid_breps_written,
|
||||
"invalid_breps_rejected": invalid_breps_rejected,
|
||||
"output_failures": output_failures,
|
||||
"repairs": int(validation.get("repairs") or 0),
|
||||
"inferred_curves": inferred_curves,
|
||||
"tolerance_mm": validation.get("tolerance_mm"),
|
||||
"peak_rss_bytes": int(report.get("peak_rss_bytes") or 0),
|
||||
"load_us": int(timings.get("load") or 0),
|
||||
"index_us": int(timings.get("index") or 0),
|
||||
"inventory_us": int(timings.get("inventory") or 0),
|
||||
"convert_us": int(timings.get("convert") or 0),
|
||||
"calibration": {
|
||||
"queries": int(calibration.get("queries") or 0),
|
||||
"elapsed_us": int(calibration.get("elapsed_us") or 0),
|
||||
"scalar_queries_per_second": float(
|
||||
calibration.get("scalar_queries_per_second") or 0.0
|
||||
),
|
||||
"parallel_queries_per_second": float(
|
||||
calibration.get("parallel_queries_per_second") or 0.0
|
||||
),
|
||||
},
|
||||
"stage_timings_us": {
|
||||
name: int(values.get("total_us") or 0)
|
||||
for name, values in sorted(stage_timings.items())
|
||||
},
|
||||
"failure_categories": dict(sorted(categories.items())),
|
||||
"skipped_items": [
|
||||
{
|
||||
"entity_id": item.get("entity_id"),
|
||||
"entity_type": item.get("entity_type"),
|
||||
"reason": item.get("reason"),
|
||||
"category": failure_category(item.get("reason")),
|
||||
}
|
||||
for item in skipped
|
||||
],
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def totals(files):
|
||||
by_schema = {}
|
||||
for schema in sorted(set(item["schema"] for item in files)):
|
||||
by_schema[schema] = totals_base(
|
||||
[item for item in files if item["schema"] == schema]
|
||||
)
|
||||
result = totals_base(files)
|
||||
result["by_schema"] = by_schema
|
||||
return result
|
||||
|
||||
|
||||
def totals_base(files):
|
||||
completed = [item for item in files if run_complete(item)]
|
||||
incomplete = [item for item in files if not run_complete(item)]
|
||||
process_statuses = Counter(
|
||||
str(item["process_exit_status"]) for item in completed
|
||||
)
|
||||
converter_statuses = Counter(
|
||||
str(item["converter_exit_status"]) for item in completed
|
||||
)
|
||||
attempted = sum(item["geometry_attempted"] for item in completed)
|
||||
written = sum(item["geometry_written"] for item in completed)
|
||||
categories = Counter()
|
||||
entity_types = Counter()
|
||||
unsupported_types = Counter()
|
||||
configuration_types = Counter()
|
||||
representation_statuses = Counter()
|
||||
occurrence_methods = set()
|
||||
for item in completed:
|
||||
categories.update(item["failure_categories"])
|
||||
entity_types.update(item.get("entity_type_counts") or {})
|
||||
unsupported_types.update(item.get("unsupported_entity_counts") or {})
|
||||
configuration_types.update(item.get("configuration_record_types") or {})
|
||||
representation_statuses.update(
|
||||
item.get("representation_status_counts") or {}
|
||||
)
|
||||
occurrence_methods.update(item.get("occurrence_methods") or [])
|
||||
return {
|
||||
"files": len(files),
|
||||
"completed_files": len(completed),
|
||||
"inconclusive_files": len(incomplete),
|
||||
"clean_files": sum(1 for item in completed if item["clean"]),
|
||||
"intentionally_empty_files": sum(
|
||||
1 for item in completed if item.get("intentionally_empty")
|
||||
),
|
||||
"process_exit_status_counts": dict(sorted(process_statuses.items())),
|
||||
"converter_exit_status_counts": dict(sorted(converter_statuses.items())),
|
||||
"geometry_attempted": attempted,
|
||||
"geometry_written": written,
|
||||
"geometry_skipped": sum(item["geometry_skipped"] for item in completed),
|
||||
"success_fraction": (float(written) / attempted) if attempted else 0.0,
|
||||
"products": sum(item.get("products", 0) for item in completed),
|
||||
"occurrences": sum(item.get("occurrences", 0) for item in completed),
|
||||
"assembly_usage_count": sum(
|
||||
item.get("assembly_usage_count", 0) for item in completed
|
||||
),
|
||||
"occurrence_detail_count": sum(
|
||||
item.get("occurrence_detail_count", 0) for item in completed
|
||||
),
|
||||
"occurrence_max_depth": max(
|
||||
(item.get("occurrence_max_depth", 0) for item in completed),
|
||||
default=0,
|
||||
),
|
||||
"occurrence_methods": sorted(occurrence_methods),
|
||||
"styles_extracted": sum(
|
||||
item.get("styles_extracted", 0) for item in completed
|
||||
),
|
||||
"styles_applied": sum(
|
||||
item.get("styles_applied", 0) for item in completed
|
||||
),
|
||||
"layers_extracted": sum(
|
||||
item.get("layers_extracted", 0) for item in completed
|
||||
),
|
||||
"materials_extracted": sum(
|
||||
item.get("materials_extracted", 0) for item in completed
|
||||
),
|
||||
"properties_extracted": sum(
|
||||
item.get("properties_extracted", 0) for item in completed
|
||||
),
|
||||
"configuration_record_count": sum(
|
||||
item.get("configuration_record_count", 0) for item in completed
|
||||
),
|
||||
"configuration_metadata_count": sum(
|
||||
item.get("configuration_metadata_count", 0) for item in completed
|
||||
),
|
||||
"product_alternative_count": sum(
|
||||
item.get("product_alternative_count", 0) for item in completed
|
||||
),
|
||||
"usage_substitute_count": sum(
|
||||
item.get("usage_substitute_count", 0) for item in completed
|
||||
),
|
||||
"invalid_breps": sum(
|
||||
item.get("invalid_breps", 0) for item in completed
|
||||
),
|
||||
"invalid_breps_written": sum(
|
||||
item.get("invalid_breps_written", 0) for item in completed
|
||||
),
|
||||
"invalid_breps_rejected": sum(
|
||||
item.get("invalid_breps_rejected", 0) for item in completed
|
||||
),
|
||||
"output_failures": sum(
|
||||
item.get("output_failures", 0) for item in completed
|
||||
),
|
||||
"repairs": sum(item.get("repairs", 0) for item in completed),
|
||||
"inferred_curves": sum(
|
||||
item.get("inferred_curves", 0) for item in completed
|
||||
),
|
||||
"elapsed_seconds": sum(item["elapsed_seconds"] for item in completed),
|
||||
"load_seconds": sum(item.get("load_us", 0) for item in completed) / 1.0e6,
|
||||
"index_seconds": sum(item.get("index_us", 0) for item in completed) / 1.0e6,
|
||||
"inventory_seconds": sum(
|
||||
item.get("inventory_us", 0) for item in completed
|
||||
) / 1.0e6,
|
||||
"convert_seconds": sum(
|
||||
item.get("convert_us", 0) for item in completed
|
||||
) / 1.0e6,
|
||||
"inconclusive_elapsed_seconds": sum(
|
||||
item["elapsed_seconds"] for item in incomplete
|
||||
),
|
||||
"observed_inconclusive_geometry_attempted": sum(
|
||||
item["geometry_attempted"] for item in incomplete
|
||||
),
|
||||
"observed_inconclusive_geometry_written": sum(
|
||||
item["geometry_written"] for item in incomplete
|
||||
),
|
||||
"observed_inconclusive_geometry_skipped": sum(
|
||||
item["geometry_skipped"] for item in incomplete
|
||||
),
|
||||
"peak_file_rss_bytes": max(
|
||||
(item["peak_rss_bytes"] for item in files), default=0
|
||||
),
|
||||
"entity_type_counts": dict(sorted(entity_types.items())),
|
||||
"unsupported_entity_counts": dict(sorted(unsupported_types.items())),
|
||||
"configuration_record_types": dict(sorted(configuration_types.items())),
|
||||
"representation_status_counts": dict(
|
||||
sorted(representation_statuses.items())
|
||||
),
|
||||
"failure_categories": dict(sorted(categories.items())),
|
||||
}
|
||||
|
||||
|
||||
def positive_ratio(current, previous):
|
||||
return (float(current) / float(previous)) if previous > 0 else None
|
||||
|
||||
|
||||
def median_ratio(current_files, previous_files, field):
|
||||
ratios = [
|
||||
positive_ratio(current.get(field, 0), previous.get(field, 0))
|
||||
for current, previous in zip(current_files, previous_files)
|
||||
]
|
||||
ratios = [value for value in ratios if value is not None and value > 0]
|
||||
return statistics.median(ratios) if ratios else None
|
||||
|
||||
|
||||
def geometric_mean_ratio(current_files, previous_files, field):
|
||||
ratios = [
|
||||
positive_ratio(current.get(field, 0), previous.get(field, 0))
|
||||
for current, previous in zip(current_files, previous_files)
|
||||
]
|
||||
ratios = [value for value in ratios if value is not None and value > 0]
|
||||
return math.exp(sum(math.log(value) for value in ratios) / len(ratios)) \
|
||||
if ratios else None
|
||||
|
||||
|
||||
def performance_comparison(current_files, previous_files):
|
||||
current = totals_base(current_files)
|
||||
previous = totals_base(previous_files)
|
||||
fields = {
|
||||
"elapsed": "elapsed_seconds",
|
||||
"load": "load_seconds",
|
||||
"convert": "convert_seconds",
|
||||
"peak_file_rss": "peak_file_rss_bytes",
|
||||
}
|
||||
aggregate_ratios = {
|
||||
label: positive_ratio(current[field], previous[field])
|
||||
for label, field in fields.items()
|
||||
}
|
||||
per_file_fields = {
|
||||
"elapsed": "elapsed_seconds",
|
||||
"load": "load_us",
|
||||
"convert": "convert_us",
|
||||
"peak_rss": "peak_rss_bytes",
|
||||
}
|
||||
return {
|
||||
"comparable_files": len(current_files),
|
||||
"current": {field: current[field] for field in fields.values()},
|
||||
"previous": {field: previous[field] for field in fields.values()},
|
||||
"aggregate_ratios": aggregate_ratios,
|
||||
"median_file_ratios": {
|
||||
label: median_ratio(current_files, previous_files, field)
|
||||
for label, field in per_file_fields.items()
|
||||
},
|
||||
"geometric_mean_file_ratios": {
|
||||
label: geometric_mean_ratio(current_files, previous_files, field)
|
||||
for label, field in per_file_fields.items()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def converter_options(configuration):
|
||||
options = (configuration or {}).get("options") or {}
|
||||
return {
|
||||
"jobs": options.get("jobs"),
|
||||
"exact": bool(options.get("exact", False)),
|
||||
"stall_timeout_seconds": options.get("stall_timeout_seconds"),
|
||||
}
|
||||
|
||||
|
||||
def converter_identity(configuration):
|
||||
configuration = configuration or {}
|
||||
executable = configuration.get("step_g") or {}
|
||||
plugins = configuration.get("schema_plugins") or []
|
||||
libraries = configuration.get("linked_libraries") or []
|
||||
return {
|
||||
"host": executable.get("sha256"),
|
||||
"schema_plugins": [
|
||||
(os.path.basename(item.get("path") or ""), item.get("sha256"))
|
||||
for item in plugins
|
||||
],
|
||||
"linked_libraries": [
|
||||
(os.path.basename(item.get("path") or ""), item.get("sha256"))
|
||||
for item in libraries
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def host_identity(configuration):
|
||||
host = (configuration or {}).get("host") or {}
|
||||
return {
|
||||
key: host.get(key)
|
||||
for key in ("hostname", "platform", "machine", "cpu_model",
|
||||
"logical_cpus")
|
||||
}
|
||||
|
||||
|
||||
def host_observations(manifest):
|
||||
snapshots = []
|
||||
host = manifest.get("host") or {}
|
||||
for name in ("initial_snapshot", "final_snapshot"):
|
||||
if isinstance(host.get(name), dict):
|
||||
snapshots.append(host[name])
|
||||
for item in manifest.get("files", []):
|
||||
for name in ("host_before", "host_after"):
|
||||
if isinstance(item.get(name), dict):
|
||||
snapshots.append(item[name])
|
||||
|
||||
loads = [item.get("load_average") for item in snapshots]
|
||||
loads = [item for item in loads if isinstance(item, list) and len(item) == 3]
|
||||
available = [
|
||||
(item.get("memory_bytes") or {}).get("MemAvailable")
|
||||
for item in snapshots
|
||||
]
|
||||
available = [int(value) for value in available if value is not None]
|
||||
result = {"snapshot_count": len(snapshots)}
|
||||
if loads:
|
||||
labels = ("one_minute", "five_minutes", "fifteen_minutes")
|
||||
result["load_average"] = {
|
||||
label: {
|
||||
"minimum": min(float(item[index]) for item in loads),
|
||||
"mean": statistics.mean(float(item[index]) for item in loads),
|
||||
"maximum": max(float(item[index]) for item in loads),
|
||||
}
|
||||
for index, label in enumerate(labels)
|
||||
}
|
||||
if available:
|
||||
result["available_memory_bytes"] = {
|
||||
"minimum": min(available),
|
||||
"maximum": max(available),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def compare(current_files, previous, current_configuration=None,
|
||||
previous_configuration=None):
|
||||
if not previous:
|
||||
return None
|
||||
old_files = {item["input"]: item for item in previous.get("files", [])}
|
||||
changes = []
|
||||
comparable_current = []
|
||||
comparable_previous = []
|
||||
for current in current_files:
|
||||
old = old_files.get(current["input"])
|
||||
if not run_complete(current):
|
||||
changes.append({
|
||||
"input": current["input"],
|
||||
"change": "inconclusive",
|
||||
"process_exit_status": current["process_exit_status"],
|
||||
})
|
||||
continue
|
||||
if not old:
|
||||
changes.append({"input": current["input"], "change": "new"})
|
||||
continue
|
||||
if not run_complete(old):
|
||||
changes.append({"input": current["input"],
|
||||
"change": "previous_inconclusive"})
|
||||
continue
|
||||
comparable_current.append(current)
|
||||
comparable_previous.append(old)
|
||||
written_delta = current["geometry_written"] - old.get("geometry_written", 0)
|
||||
skipped_delta = current["geometry_skipped"] - old.get("geometry_skipped", 0)
|
||||
inferred_delta = current.get("inferred_curves", 0) - \
|
||||
old.get("inferred_curves", 0)
|
||||
old_skips = {
|
||||
(item.get("entity_id"), item.get("entity_type"), item.get("reason"))
|
||||
for item in old.get("skipped_items", [])
|
||||
}
|
||||
new_skips = {
|
||||
(item.get("entity_id"), item.get("entity_type"), item.get("reason"))
|
||||
for item in current.get("skipped_items", [])
|
||||
}
|
||||
if (written_delta or skipped_delta or inferred_delta or
|
||||
bool(old.get("clean")) != current["clean"] or
|
||||
old_skips != new_skips):
|
||||
changes.append({
|
||||
"input": current["input"],
|
||||
"written_delta": written_delta,
|
||||
"skipped_delta": skipped_delta,
|
||||
"inferred_curves_delta": inferred_delta,
|
||||
"became_clean": current["clean"] and not old.get("clean"),
|
||||
"regressed_from_clean": bool(old.get("clean")) and not current["clean"],
|
||||
"fixed_skips": [list(item) for item in sorted(old_skips - new_skips)],
|
||||
"new_skips": [list(item) for item in sorted(new_skips - old_skips)],
|
||||
})
|
||||
previous_totals = totals(comparable_previous)
|
||||
current_totals = totals(comparable_current)
|
||||
input_hash_pairs = [
|
||||
(current.get("input_sha256"), old.get("input_sha256"))
|
||||
for current, old in zip(comparable_current, comparable_previous)
|
||||
]
|
||||
input_hashes_verified = bool(input_hash_pairs) and all(
|
||||
current and old and current == old
|
||||
for current, old in input_hash_pairs
|
||||
)
|
||||
options_match = bool(
|
||||
current_configuration and previous_configuration
|
||||
) and converter_options(current_configuration) == converter_options(
|
||||
previous_configuration
|
||||
)
|
||||
hosts_match = bool(
|
||||
current_configuration and previous_configuration
|
||||
) and host_identity(current_configuration) == host_identity(
|
||||
previous_configuration
|
||||
)
|
||||
current_observations = (
|
||||
current_configuration or {}
|
||||
).get("host_observations") or {}
|
||||
previous_observations = (
|
||||
previous_configuration or {}
|
||||
).get("host_observations") or {}
|
||||
loads_recorded = bool(
|
||||
current_observations.get("load_average") and
|
||||
previous_observations.get("load_average")
|
||||
)
|
||||
return {
|
||||
"previous_run_id": previous.get("run_id"),
|
||||
"geometry_written_delta": current_totals["geometry_written"] -
|
||||
int(previous_totals.get("geometry_written") or 0),
|
||||
"geometry_skipped_delta": current_totals["geometry_skipped"] -
|
||||
int(previous_totals.get("geometry_skipped") or 0),
|
||||
"clean_files_delta": current_totals["clean_files"] -
|
||||
int(previous_totals.get("clean_files") or 0),
|
||||
"regressed_clean_files": [
|
||||
item["input"] for item in changes if item.get("regressed_from_clean")
|
||||
],
|
||||
"newly_clean_files": [
|
||||
item["input"] for item in changes if item.get("became_clean")
|
||||
],
|
||||
"inconclusive_files": [
|
||||
item["input"] for item in changes
|
||||
if item.get("change") == "inconclusive"
|
||||
],
|
||||
"configuration": {
|
||||
"current_options": (current_configuration or {}).get("options"),
|
||||
"previous_options": (previous_configuration or {}).get("options"),
|
||||
"converter_options_match": options_match,
|
||||
"host_identity_match": hosts_match,
|
||||
"input_hashes_verified": input_hashes_verified,
|
||||
"load_observations_recorded": loads_recorded,
|
||||
"timing_context_comparable": options_match and hosts_match and
|
||||
input_hashes_verified and
|
||||
loads_recorded,
|
||||
"executable_match": bool(
|
||||
(current_configuration or {}).get("step_g") and
|
||||
(previous_configuration or {}).get("step_g")
|
||||
) and converter_identity(current_configuration) ==
|
||||
converter_identity(previous_configuration),
|
||||
"current_outer_timeout_seconds": (
|
||||
(current_configuration or {}).get("options") or {}
|
||||
).get("timeout_seconds"),
|
||||
"previous_outer_timeout_seconds": (
|
||||
(previous_configuration or {}).get("options") or {}
|
||||
).get("timeout_seconds"),
|
||||
"current_host_identity": host_identity(current_configuration),
|
||||
"previous_host_identity": host_identity(previous_configuration),
|
||||
"current_host_observations": current_observations,
|
||||
"previous_host_observations": previous_observations,
|
||||
},
|
||||
"performance": performance_comparison(
|
||||
comparable_current, comparable_previous
|
||||
),
|
||||
"changes": changes,
|
||||
}
|
||||
|
||||
|
||||
def run_configuration(run_directory):
|
||||
path = Path(run_directory) / "run-manifest.json"
|
||||
if not path.is_file():
|
||||
return None
|
||||
manifest = read_json(path)
|
||||
return {
|
||||
"manifest": str(path.resolve()),
|
||||
"options": manifest.get("options"),
|
||||
"step_g": manifest.get("step_g"),
|
||||
"schema_plugins": manifest.get("schema_plugins"),
|
||||
"linked_libraries": manifest.get("linked_libraries"),
|
||||
"host": manifest.get("host"),
|
||||
"host_observations": host_observations(manifest),
|
||||
"started_utc": manifest.get("started_utc"),
|
||||
"finished_utc": manifest.get("finished_utc"),
|
||||
}
|
||||
|
||||
|
||||
def attach_input_identities(files, run_directory):
|
||||
path = Path(run_directory) / "run-manifest.json"
|
||||
if not path.is_file():
|
||||
return
|
||||
manifest = read_json(path)
|
||||
identities = {
|
||||
record.get("input"): record.get("input_identity") or {}
|
||||
for record in manifest.get("files") or []
|
||||
}
|
||||
for item in files:
|
||||
identity = identities.get(item["input"]) or {}
|
||||
if identity.get("sha256"):
|
||||
item["input_sha256"] = identity["sha256"]
|
||||
item["input_size"] = identity.get("size")
|
||||
|
||||
|
||||
def markdown(summary):
|
||||
total = summary["totals"]
|
||||
def count_list(values):
|
||||
return ", ".join(
|
||||
"{} {}".format(count, name)
|
||||
for name, count in sorted(
|
||||
values.items(), key=lambda item: (-item[1], item[0])
|
||||
)
|
||||
)
|
||||
|
||||
lines = [
|
||||
"# STEP corpus run {}".format(summary["run_id"]),
|
||||
"",
|
||||
"Revision: `{}`{}".format(
|
||||
summary["revision"], " (dirty)" if summary.get("dirty") else ""
|
||||
),
|
||||
"",
|
||||
"| Schema | Files clean | Intentional empty | Geometry written | Success |",
|
||||
"|---|---:|---:|---:|---:|",
|
||||
]
|
||||
for schema, values in sorted(total["by_schema"].items()):
|
||||
lines.append("| {} | {}/{} | {} | {}/{} | {:.2%} |".format(
|
||||
schema, values["clean_files"], values["completed_files"],
|
||||
values["intentionally_empty_files"],
|
||||
values["geometry_written"], values["geometry_attempted"],
|
||||
values["success_fraction"],
|
||||
))
|
||||
lines.append("| Combined | {}/{} | {} | {}/{} | {:.2%} |".format(
|
||||
total["clean_files"], total["completed_files"],
|
||||
total["intentionally_empty_files"], total["geometry_written"],
|
||||
total["geometry_attempted"], total["success_fraction"],
|
||||
))
|
||||
lines.extend([
|
||||
"",
|
||||
"Assembly coverage: {} product(s), {} occurrence(s), maximum depth "
|
||||
"{}, using {}.".format(
|
||||
total["products"], total["occurrences"],
|
||||
total["occurrence_max_depth"],
|
||||
", ".join(total["occurrence_methods"]) or "no occurrence method",
|
||||
),
|
||||
"Presentation and metadata: {} style(s) extracted, {} applied, {} "
|
||||
"layer(s), {} material(s), {} property value(s), and {} retained "
|
||||
"configuration record(s).".format(
|
||||
total["styles_extracted"], total["styles_applied"],
|
||||
total["layers_extracted"], total["materials_extracted"],
|
||||
total["properties_extracted"],
|
||||
total["configuration_record_count"],
|
||||
),
|
||||
"Validation: {} invalid B-Rep(s), {} preserved and {} rejected; {} "
|
||||
"bounded repair(s) and {} tagged curve inference(s) applied.".format(
|
||||
total["invalid_breps"], total["invalid_breps_written"],
|
||||
total["invalid_breps_rejected"],
|
||||
total["repairs"],
|
||||
total["inferred_curves"],
|
||||
),
|
||||
])
|
||||
if total["failure_categories"]:
|
||||
lines.append("Geometry omission categories: {}.".format(
|
||||
count_list(total["failure_categories"])
|
||||
))
|
||||
lines.append("Converter terminal statuses: {}.".format(
|
||||
count_list(total["converter_exit_status_counts"])
|
||||
))
|
||||
if total["unsupported_entity_counts"]:
|
||||
lines.append("Unsupported STEP entity inventory: {}.".format(
|
||||
count_list(total["unsupported_entity_counts"])
|
||||
))
|
||||
observations = ((summary.get("run_configuration") or {}).get(
|
||||
"host_observations") or {})
|
||||
one_minute = (observations.get("load_average") or {}).get("one_minute")
|
||||
if one_minute:
|
||||
lines.extend([
|
||||
"",
|
||||
"Host one-minute load over {} boundary samples: {:.2f} mean "
|
||||
"({:.2f}-{:.2f}).".format(
|
||||
observations["snapshot_count"], one_minute["mean"],
|
||||
one_minute["minimum"], one_minute["maximum"],
|
||||
),
|
||||
])
|
||||
if total["inconclusive_files"]:
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Inconclusive (timed out or interrupted): {} file(s); partial "
|
||||
"geometry is excluded from totals and deltas.".format(
|
||||
total["inconclusive_files"]
|
||||
)
|
||||
)
|
||||
lines.extend([
|
||||
"", "## Per file", "",
|
||||
"| Schema | Input | Status | Written | Skipped | Inferred | Reported load s | "
|
||||
"Reported convert s | Wall s | Peak RSS MiB |",
|
||||
"|---|---|---|---:|---:|---:|---:|---:|---:|---:|",
|
||||
])
|
||||
for item in summary["files"]:
|
||||
lines.append("| {} | {} | {} | {}/{} | {} | {} | {:.1f} | {:.1f} | {:.1f} | {:.1f} |".format(
|
||||
item["schema"], item["name"].replace("|", "\\|"),
|
||||
"clean" if item["clean"] else
|
||||
("intentional empty" if item.get("intentionally_empty") else
|
||||
("partial" if run_complete(item) else "inconclusive")),
|
||||
item["geometry_written"], item["geometry_attempted"],
|
||||
item["geometry_skipped"], item.get("inferred_curves", 0),
|
||||
item["load_us"] / 1.0e6,
|
||||
item["convert_us"] / 1.0e6, item["elapsed_seconds"],
|
||||
item["peak_rss_bytes"] / (1024.0 * 1024.0),
|
||||
))
|
||||
delta = summary.get("delta")
|
||||
if delta:
|
||||
lines.extend([
|
||||
"", "## Delta", "",
|
||||
"Written geometry: {:+d}; skipped geometry: {:+d}; clean files: {:+d}.".format(
|
||||
delta["geometry_written_delta"], delta["geometry_skipped_delta"],
|
||||
delta["clean_files_delta"],
|
||||
),
|
||||
])
|
||||
if delta["regressed_clean_files"]:
|
||||
lines.append("Regressed clean files: {}.".format(
|
||||
", ".join(delta["regressed_clean_files"])
|
||||
))
|
||||
if delta["inconclusive_files"]:
|
||||
lines.append("Not compared because the run was incomplete: {}.".format(
|
||||
", ".join(delta["inconclusive_files"])
|
||||
))
|
||||
performance = delta.get("performance") or {}
|
||||
ratios = performance.get("aggregate_ratios") or {}
|
||||
if performance.get("comparable_files"):
|
||||
lines.extend([
|
||||
"",
|
||||
"Performance over {} comparable files (current/previous): "
|
||||
"wall {:.3f}x and peak-file RSS {:.3f}x. Reported phase "
|
||||
"ratios are load {:.3f}x and conversion {:.3f}x; phase "
|
||||
"boundaries may differ across binaries.".format(
|
||||
performance["comparable_files"],
|
||||
ratios.get("elapsed") or 0.0,
|
||||
ratios.get("peak_file_rss") or 0.0,
|
||||
ratios.get("load") or 0.0,
|
||||
ratios.get("convert") or 0.0,
|
||||
),
|
||||
])
|
||||
configuration = delta.get("configuration") or {}
|
||||
if not configuration.get("converter_options_match"):
|
||||
lines.append(
|
||||
"Run options differ or could not be verified; performance "
|
||||
"ratios are informational only."
|
||||
)
|
||||
if not configuration.get("host_identity_match"):
|
||||
lines.append(
|
||||
"Host identity differs or could not be verified; timing "
|
||||
"ratios are not controlled benchmarks."
|
||||
)
|
||||
if not configuration.get("input_hashes_verified"):
|
||||
lines.append(
|
||||
"Input hashes differ or were not recorded in both runs; "
|
||||
"timing ratios are informational only."
|
||||
)
|
||||
if not configuration.get("load_observations_recorded"):
|
||||
lines.append(
|
||||
"Both runs did not record host-load observations; "
|
||||
"timing ratios are not controlled benchmarks."
|
||||
)
|
||||
else:
|
||||
current_load = configuration["current_host_observations"][
|
||||
"load_average"
|
||||
]["one_minute"]["mean"]
|
||||
previous_load = configuration["previous_host_observations"][
|
||||
"load_average"
|
||||
]["one_minute"]["mean"]
|
||||
lines.append(
|
||||
"Recorded one-minute load mean was {:.2f} current versus "
|
||||
"{:.2f} previous.".format(current_load, previous_load)
|
||||
)
|
||||
if configuration.get("timing_context_comparable"):
|
||||
lines.append(
|
||||
"Host, input hashes, converter options, and host-load "
|
||||
"telemetry are comparable; unrecorded background activity "
|
||||
"may still affect timings."
|
||||
)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("run_directory", type=Path)
|
||||
parser.add_argument("--previous", type=Path,
|
||||
help="previous corpus-summary.json for delta reporting")
|
||||
parser.add_argument("--output", type=Path,
|
||||
help="output JSON (default: RUN_DIRECTORY/corpus-summary.json)")
|
||||
parser.add_argument("--run-id", help="stable run label; defaults to directory name")
|
||||
parser.add_argument("--no-markdown", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
run_directory = args.run_directory.resolve()
|
||||
status_paths = sorted(run_directory.glob("*.status"))
|
||||
if not status_paths:
|
||||
parser.error("{} contains no *.status corpus records".format(run_directory))
|
||||
try:
|
||||
files = [report_record(path) for path in status_paths]
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
print("step_corpus_summary.py: {}".format(error), file=sys.stderr)
|
||||
return 2
|
||||
attach_input_identities(files, run_directory)
|
||||
|
||||
revision, dirty = git_state(Path(__file__).resolve().parents[2])
|
||||
previous = read_json(args.previous) if args.previous else None
|
||||
current_configuration = run_configuration(run_directory)
|
||||
previous_configuration = run_configuration(
|
||||
previous.get("run_directory")
|
||||
) if previous and previous.get("run_directory") else None
|
||||
generated = datetime.datetime.now(datetime.timezone.utc).replace(
|
||||
microsecond=0
|
||||
).isoformat()
|
||||
summary = {
|
||||
"format": FORMAT,
|
||||
"run_id": args.run_id or run_directory.name,
|
||||
"generated_utc": generated,
|
||||
"run_directory": str(run_directory),
|
||||
"revision": revision,
|
||||
"dirty": dirty,
|
||||
"run_configuration": current_configuration,
|
||||
"totals": totals(files),
|
||||
"files": files,
|
||||
}
|
||||
summary["delta"] = compare(
|
||||
files, previous, current_configuration, previous_configuration
|
||||
)
|
||||
|
||||
output = args.output or (run_directory / "corpus-summary.json")
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with output.open("w", encoding="utf-8") as stream:
|
||||
json.dump(summary, stream, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
if not args.no_markdown:
|
||||
output.with_suffix(".md").write_text(markdown(summary), encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify a recorded STEP corpus run against checksum-pinned expectations.
|
||||
|
||||
External STEP samples are often valuable but not suitable for copying into
|
||||
BRL-CAD. This tool keeps their evidence reproducible without copying the
|
||||
files: an expectation manifest records the source, accepted local filenames,
|
||||
SHA-256 digest, and report assertions. The verifier consumes the structured
|
||||
reports and run manifest produced by ``step_corpus_run.py``.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FORMAT = "brlcad-step-corpus-verification-v1"
|
||||
|
||||
|
||||
def read_json(path):
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
return json.load(stream)
|
||||
|
||||
|
||||
def file_sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def dotted_value(document, path):
|
||||
value = document
|
||||
for component in path.split("."):
|
||||
if not isinstance(value, dict) or component not in value:
|
||||
raise KeyError(path)
|
||||
value = value[component]
|
||||
return value
|
||||
|
||||
|
||||
def occurrence_depth(occurrences):
|
||||
children = {}
|
||||
vertices = set()
|
||||
for occurrence in occurrences:
|
||||
parent = int(occurrence.get("parent_product_id") or 0)
|
||||
child = int(occurrence.get("child_product_id") or 0)
|
||||
if parent <= 0 or child <= 0:
|
||||
continue
|
||||
children.setdefault(parent, set()).add(child)
|
||||
vertices.update((parent, child))
|
||||
|
||||
active = set()
|
||||
memo = {}
|
||||
|
||||
def visit(vertex):
|
||||
if vertex in memo:
|
||||
return memo[vertex]
|
||||
if vertex in active:
|
||||
raise ValueError("occurrence product graph contains a cycle")
|
||||
active.add(vertex)
|
||||
depth = max((1 + visit(child) for child in children.get(vertex, ())),
|
||||
default=0)
|
||||
active.remove(vertex)
|
||||
memo[vertex] = depth
|
||||
return depth
|
||||
|
||||
return max((visit(vertex) for vertex in vertices), default=0)
|
||||
|
||||
|
||||
def derived_values(report):
|
||||
occurrences = report.get("occurrence_details") or []
|
||||
metadata = report.get("product_metadata") or []
|
||||
materials = [
|
||||
material
|
||||
for product in metadata
|
||||
for material in product.get("materials") or []
|
||||
]
|
||||
material_properties = [
|
||||
prop
|
||||
for material in materials
|
||||
for prop in material.get("properties") or []
|
||||
]
|
||||
material_numeric_values = sorted({
|
||||
value
|
||||
for prop in material_properties
|
||||
for value in prop.get("values") or []
|
||||
if isinstance(value, (int, float))
|
||||
})
|
||||
return {
|
||||
"assembly_usage_count": len(report.get("assembly_usages") or []),
|
||||
"occurrence_detail_count": len(occurrences),
|
||||
"occurrence_max_depth": occurrence_depth(occurrences),
|
||||
"occurrence_methods": sorted({
|
||||
item.get("shape_method") or "" for item in occurrences
|
||||
}),
|
||||
"skipped_entity_ids": sorted({
|
||||
int(item.get("entity_id") or 0)
|
||||
for item in report.get("skipped_items") or []
|
||||
if int(item.get("entity_id") or 0) > 0
|
||||
}),
|
||||
"product_metadata_count": len(metadata),
|
||||
"material_count": len(materials),
|
||||
"material_identifiers": sorted({
|
||||
material.get("identifier") or "" for material in materials
|
||||
}),
|
||||
"material_names": sorted({
|
||||
material.get("name") or "" for material in materials
|
||||
}),
|
||||
"material_property_count": len(material_properties),
|
||||
"material_property_names": sorted({
|
||||
prop.get("name") or "" for prop in material_properties
|
||||
}),
|
||||
"material_property_units": sorted({
|
||||
prop.get("units") or "" for prop in material_properties
|
||||
if prop.get("units")
|
||||
}),
|
||||
"material_property_numeric_values": material_numeric_values,
|
||||
"validation_property_count": sum(
|
||||
len(product.get("validation_properties") or [])
|
||||
for product in metadata
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def compare_expected(report, expected):
|
||||
combined = dict(report)
|
||||
combined["derived"] = derived_values(report)
|
||||
checks = []
|
||||
errors = []
|
||||
for path, wanted in sorted((expected.get("equals") or {}).items()):
|
||||
try:
|
||||
actual = dotted_value(combined, path)
|
||||
except KeyError:
|
||||
errors.append("{} is missing".format(path))
|
||||
continue
|
||||
checks.append(path)
|
||||
if actual != wanted:
|
||||
errors.append("{} is {!r}, expected {!r}".format(
|
||||
path, actual, wanted
|
||||
))
|
||||
for path, wanted in sorted((expected.get("minimum") or {}).items()):
|
||||
try:
|
||||
actual = dotted_value(combined, path)
|
||||
except KeyError:
|
||||
errors.append("{} is missing".format(path))
|
||||
continue
|
||||
checks.append(path)
|
||||
if not isinstance(actual, (int, float)) or actual < wanted:
|
||||
errors.append("{} is {!r}, expected at least {!r}".format(
|
||||
path, actual, wanted
|
||||
))
|
||||
return checks, errors, combined["derived"]
|
||||
|
||||
|
||||
def markdown(result):
|
||||
lines = [
|
||||
"# STEP external corpus verification",
|
||||
"",
|
||||
"Expectation manifest: `{}`".format(result["expectations"]),
|
||||
"",
|
||||
"| Input | Source/producer | Result | Checks |",
|
||||
"|---|---|---:|---:|",
|
||||
]
|
||||
for item in result["files"]:
|
||||
source = item.get("producer") or item.get("source_page") or ""
|
||||
lines.append("| {} | {} | {} | {} |".format(
|
||||
item["name"].replace("|", "\\|"),
|
||||
source.replace("|", "\\|"),
|
||||
item["result"], len(item.get("checks") or []),
|
||||
))
|
||||
for error in item.get("errors") or []:
|
||||
lines.append(" - **{}:** {}".format(item["name"], error))
|
||||
lines.extend([
|
||||
"",
|
||||
"Verified {}/{} present files; {} manifest entries were missing.".format(
|
||||
result["passed_files"], result["present_files"],
|
||||
result["missing_files"],
|
||||
),
|
||||
])
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("run_directory", type=Path)
|
||||
parser.add_argument("expectations", type=Path)
|
||||
parser.add_argument("--allow-missing", action="store_true",
|
||||
help="do not fail for expectation entries absent from the run")
|
||||
parser.add_argument("--output", type=Path,
|
||||
help="output JSON (default: RUN/corpus-verification.json)")
|
||||
args = parser.parse_args()
|
||||
|
||||
run_directory = args.run_directory.resolve()
|
||||
manifest_path = run_directory / "run-manifest.json"
|
||||
if not manifest_path.is_file():
|
||||
parser.error("{} does not contain run-manifest.json".format(run_directory))
|
||||
expectations_path = args.expectations.resolve()
|
||||
run = read_json(manifest_path)
|
||||
expectations = read_json(expectations_path)
|
||||
if expectations.get("format") != "brlcad-step-corpus-expectations-v1":
|
||||
parser.error("unsupported expectation manifest format")
|
||||
|
||||
run_files = {}
|
||||
for record in run.get("files") or []:
|
||||
input_name = Path(record.get("input") or "").name
|
||||
if input_name:
|
||||
run_files[input_name] = record
|
||||
|
||||
results = []
|
||||
passed = 0
|
||||
present = 0
|
||||
missing = 0
|
||||
for expectation in expectations.get("files") or []:
|
||||
names = [expectation["name"]] + list(expectation.get("aliases") or [])
|
||||
record = next((run_files[name] for name in names if name in run_files), None)
|
||||
item = {
|
||||
"name": expectation["name"],
|
||||
"source_page": expectation.get("source_page"),
|
||||
"producer": expectation.get("producer"),
|
||||
"checks": [],
|
||||
"errors": [],
|
||||
}
|
||||
if record is None:
|
||||
missing += 1
|
||||
item["result"] = "missing"
|
||||
if not args.allow_missing:
|
||||
item["errors"].append("input was not present in the corpus run")
|
||||
results.append(item)
|
||||
continue
|
||||
|
||||
present += 1
|
||||
input_path = Path(record["input"])
|
||||
actual_hash = file_sha256(input_path) if input_path.is_file() else None
|
||||
item["input"] = str(input_path)
|
||||
item["sha256"] = actual_hash
|
||||
item["checks"].append("sha256")
|
||||
recorded_hash = (record.get("input_identity") or {}).get("sha256")
|
||||
if recorded_hash and recorded_hash != actual_hash:
|
||||
item["errors"].append(
|
||||
"input changed after the run manifest recorded its SHA-256"
|
||||
)
|
||||
if actual_hash != expectation.get("sha256"):
|
||||
item["errors"].append("SHA-256 is {}, expected {}".format(
|
||||
actual_hash, expectation.get("sha256")
|
||||
))
|
||||
|
||||
report_name = record.get("report")
|
||||
report_path = run_directory / report_name if report_name else None
|
||||
if not report_path or not report_path.is_file():
|
||||
item["errors"].append("structured converter report is missing")
|
||||
else:
|
||||
report = read_json(report_path)
|
||||
checks, errors, derived = compare_expected(
|
||||
report, expectation.get("expected") or {}
|
||||
)
|
||||
item["checks"].extend(checks)
|
||||
item["errors"].extend(errors)
|
||||
item["derived"] = derived
|
||||
item["report"] = str(report_path)
|
||||
|
||||
item["result"] = "pass" if not item["errors"] else "fail"
|
||||
if item["result"] == "pass":
|
||||
passed += 1
|
||||
results.append(item)
|
||||
|
||||
failed = sum(1 for item in results if item.get("errors"))
|
||||
result = {
|
||||
"format": FORMAT,
|
||||
"expectations": str(expectations_path),
|
||||
"run_directory": str(run_directory),
|
||||
"passed_files": passed,
|
||||
"present_files": present,
|
||||
"missing_files": missing,
|
||||
"failed_files": failed,
|
||||
"files": results,
|
||||
}
|
||||
output = args.output or (run_directory / "corpus-verification.json")
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
output.with_suffix(".md").write_text(markdown(result), encoding="utf-8")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,449 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract a small, forward-complete STEP geometry subset.
|
||||
|
||||
This helper is intended for local diagnosis and for identifying the minimal
|
||||
geometry that a project-owned synthetic regression test must reproduce. It
|
||||
copies the dependency closure of one or more STEP face entities and wraps them
|
||||
in an OPEN_SHELL product, retaining the source entity numbers so converter
|
||||
diagnostics remain directly comparable to the original file. A diagnostic
|
||||
--closed-shell mode instead uses CLOSED_SHELL/MANIFOLD_SOLID_BREP so reductions
|
||||
can exercise topology rules which only apply to asserted solids. A face subset
|
||||
is not necessarily a valid closed shell, so that mode is for failure reduction,
|
||||
not evidence that the extracted result is valid geometry. The output keeps the
|
||||
input model's provenance and is not automatically suitable for committing or
|
||||
redistribution.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ADVANCED_FACE = re.compile(
|
||||
r"^[ \t]*#[0-9]+[ \t]*=[ \t]*ADVANCED_FACE[ \t]*\(",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
FILE_SCHEMA = re.compile(
|
||||
r"FILE_SCHEMA\s*\(\s*\(\s*'([^']+)'", re.IGNORECASE | re.DOTALL
|
||||
)
|
||||
ENTITY_TYPE = re.compile(
|
||||
r"^[ \t]*#[0-9]+[ \t]*=[ \t]*([A-Z0-9_]+)[ \t]*\(",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def instance_end(text, start):
|
||||
"""Return the first record-ending semicolon outside strings/comments."""
|
||||
in_string = False
|
||||
in_comment = False
|
||||
offset = start
|
||||
while offset < len(text):
|
||||
current = text[offset]
|
||||
following = text[offset + 1] if offset + 1 < len(text) else ""
|
||||
if in_comment:
|
||||
if current == "*" and following == "/":
|
||||
in_comment = False
|
||||
offset += 2
|
||||
continue
|
||||
elif in_string:
|
||||
if current == "'":
|
||||
if following == "'":
|
||||
offset += 2
|
||||
continue
|
||||
in_string = False
|
||||
elif current == "/" and following == "*":
|
||||
in_comment = True
|
||||
offset += 2
|
||||
continue
|
||||
elif current == "'":
|
||||
in_string = True
|
||||
elif current == ";":
|
||||
return offset + 1
|
||||
offset += 1
|
||||
raise ValueError("unterminated STEP instance starting at byte {}".format(start))
|
||||
|
||||
|
||||
def instances(text):
|
||||
"""Return Part 21 instances without recognizing text in strings/comments."""
|
||||
result = {}
|
||||
offset = 0
|
||||
line_start = True
|
||||
in_string = False
|
||||
in_comment = False
|
||||
while offset < len(text):
|
||||
current = text[offset]
|
||||
following = text[offset + 1] if offset + 1 < len(text) else ""
|
||||
if in_comment:
|
||||
if current == "*" and following == "/":
|
||||
in_comment = False
|
||||
offset += 2
|
||||
continue
|
||||
elif in_string:
|
||||
if current == "'":
|
||||
if following == "'":
|
||||
offset += 2
|
||||
continue
|
||||
in_string = False
|
||||
elif current == "/" and following == "*":
|
||||
in_comment = True
|
||||
offset += 2
|
||||
continue
|
||||
elif current == "'":
|
||||
in_string = True
|
||||
elif current == "\n" or current == "\r":
|
||||
line_start = True
|
||||
elif line_start and current in " \t":
|
||||
pass
|
||||
elif line_start and current == "#":
|
||||
digit_end = offset + 1
|
||||
while digit_end < len(text) and text[digit_end].isdigit():
|
||||
digit_end += 1
|
||||
separator = digit_end
|
||||
while separator < len(text) and text[separator] in " \t":
|
||||
separator += 1
|
||||
if digit_end > offset + 1 and separator < len(text) and \
|
||||
text[separator] == "=":
|
||||
entity_id = int(text[offset + 1:digit_end])
|
||||
end = instance_end(text, offset)
|
||||
if entity_id in result:
|
||||
raise ValueError(
|
||||
"duplicate STEP entity #{}".format(entity_id)
|
||||
)
|
||||
result[entity_id] = text[offset:end].strip()
|
||||
offset = end
|
||||
line_start = False
|
||||
continue
|
||||
line_start = False
|
||||
else:
|
||||
line_start = False
|
||||
offset += 1
|
||||
return result
|
||||
|
||||
|
||||
def references(record):
|
||||
"""Yield entity references outside Part 21 strings and comments."""
|
||||
offset = 0
|
||||
in_string = False
|
||||
in_comment = False
|
||||
while offset < len(record):
|
||||
current = record[offset]
|
||||
following = record[offset + 1] if offset + 1 < len(record) else ""
|
||||
if in_comment:
|
||||
if current == "*" and following == "/":
|
||||
in_comment = False
|
||||
offset += 2
|
||||
continue
|
||||
elif in_string:
|
||||
if current == "'":
|
||||
if following == "'":
|
||||
offset += 2
|
||||
continue
|
||||
in_string = False
|
||||
elif current == "/" and following == "*":
|
||||
in_comment = True
|
||||
offset += 2
|
||||
continue
|
||||
elif current == "'":
|
||||
in_string = True
|
||||
elif current == "#":
|
||||
digit_end = offset + 1
|
||||
while digit_end < len(record) and record[digit_end].isdigit():
|
||||
digit_end += 1
|
||||
if digit_end > offset + 1:
|
||||
yield int(record[offset + 1:digit_end])
|
||||
offset = digit_end
|
||||
continue
|
||||
offset += 1
|
||||
|
||||
|
||||
def dependency_closure(records, roots):
|
||||
pending = list(roots)
|
||||
selected = set()
|
||||
while pending:
|
||||
entity_id = pending.pop()
|
||||
if entity_id in selected:
|
||||
continue
|
||||
record = records.get(entity_id)
|
||||
if record is None:
|
||||
raise ValueError("STEP entity #{} is not present".format(entity_id))
|
||||
selected.add(entity_id)
|
||||
for referenced_id in references(record):
|
||||
if referenced_id != entity_id and referenced_id not in selected:
|
||||
pending.append(referenced_id)
|
||||
return selected
|
||||
|
||||
|
||||
def record_type(record):
|
||||
match = ENTITY_TYPE.match(record)
|
||||
return match.group(1).upper() if match else ""
|
||||
|
||||
|
||||
def adjacent_faces(records, faces):
|
||||
"""Return ADVANCED_FACE entities sharing an EDGE_CURVE with faces."""
|
||||
reverse = {}
|
||||
for entity_id, record in records.items():
|
||||
for referenced_id in references(record):
|
||||
reverse.setdefault(referenced_id, set()).add(entity_id)
|
||||
|
||||
edge_curves = {
|
||||
entity_id
|
||||
for entity_id in dependency_closure(records, faces)
|
||||
if record_type(records[entity_id]) == "EDGE_CURVE"
|
||||
}
|
||||
oriented_edges = {
|
||||
entity_id
|
||||
for edge_id in edge_curves
|
||||
for entity_id in reverse.get(edge_id, ())
|
||||
if record_type(records[entity_id]) == "ORIENTED_EDGE"
|
||||
}
|
||||
edge_loops = {
|
||||
entity_id
|
||||
for oriented_id in oriented_edges
|
||||
for entity_id in reverse.get(oriented_id, ())
|
||||
if record_type(records[entity_id]) == "EDGE_LOOP"
|
||||
}
|
||||
face_bounds = {
|
||||
entity_id
|
||||
for loop_id in edge_loops
|
||||
for entity_id in reverse.get(loop_id, ())
|
||||
if record_type(records[entity_id]) in {
|
||||
"FACE_BOUND", "FACE_OUTER_BOUND"
|
||||
}
|
||||
}
|
||||
return {
|
||||
entity_id
|
||||
for bound_id in face_bounds
|
||||
for entity_id in reverse.get(bound_id, ())
|
||||
if record_type(records[entity_id]) == "ADVANCED_FACE"
|
||||
}
|
||||
|
||||
|
||||
def quoted(value):
|
||||
return value.replace("'", "''")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument(
|
||||
"--face",
|
||||
type=int,
|
||||
action="append",
|
||||
default=[],
|
||||
help="ADVANCED_FACE entity ID to retain (repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--face-range",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="FIRST:LAST",
|
||||
help="inclusive contiguous ADVANCED_FACE ID range to retain (repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--face-neighbor-depth",
|
||||
type=int,
|
||||
default=0,
|
||||
metavar="N",
|
||||
help=(
|
||||
"also retain ADVANCED_FACE neighbors reachable through N layers "
|
||||
"of shared EDGE_CURVEs"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--name", default="reduced_step_regression")
|
||||
parser.add_argument(
|
||||
"--context",
|
||||
type=int,
|
||||
help=(
|
||||
"source GEOMETRIC_REPRESENTATION_CONTEXT entity ID to preserve; "
|
||||
"recommended when source angle or length units are not SI defaults"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--uncertainty",
|
||||
type=float,
|
||||
default=1.0e-6,
|
||||
help="fixture uncertainty in millimetres (default: 1e-6)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--closed-shell",
|
||||
action="store_true",
|
||||
help=(
|
||||
"wrap the face subset as CLOSED_SHELL/MANIFOLD_SOLID_BREP "
|
||||
"instead of OPEN_SHELL/SHELL_BASED_SURFACE_MODEL"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
faces = list(args.face)
|
||||
for face_range in args.face_range:
|
||||
fields = face_range.split(":", 1)
|
||||
if len(fields) != 2:
|
||||
parser.error("--face-range must be FIRST:LAST")
|
||||
try:
|
||||
first, last = (int(field) for field in fields)
|
||||
except ValueError:
|
||||
parser.error("--face-range must contain integer entity IDs")
|
||||
if first > last:
|
||||
parser.error("--face-range FIRST must not exceed LAST")
|
||||
faces.extend(range(first, last + 1))
|
||||
faces = sorted(set(faces))
|
||||
if not faces:
|
||||
parser.error("at least one --face or --face-range is required")
|
||||
if args.face_neighbor_depth < 0:
|
||||
parser.error("--face-neighbor-depth must be nonnegative")
|
||||
|
||||
source = args.input.read_text(encoding="latin-1")
|
||||
records = instances(source)
|
||||
for entity_id in faces:
|
||||
record = records.get(entity_id)
|
||||
if record is None:
|
||||
parser.error("STEP entity #{} is not present".format(entity_id))
|
||||
if not ADVANCED_FACE.match(record):
|
||||
parser.error(
|
||||
"STEP entity #{} is not an ADVANCED_FACE".format(entity_id)
|
||||
)
|
||||
expanded_faces = set(faces)
|
||||
frontier = set(faces)
|
||||
for _ in range(args.face_neighbor_depth):
|
||||
neighbors = adjacent_faces(records, frontier) - expanded_faces
|
||||
if not neighbors:
|
||||
break
|
||||
expanded_faces.update(neighbors)
|
||||
frontier = neighbors
|
||||
faces = sorted(expanded_faces)
|
||||
closure_roots = list(faces)
|
||||
if args.context is not None:
|
||||
closure_roots.append(args.context)
|
||||
selected = dependency_closure(records, closure_roots)
|
||||
schema_match = FILE_SCHEMA.search(source)
|
||||
schema = (
|
||||
schema_match.group(1)
|
||||
if schema_match
|
||||
else "AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 1 }"
|
||||
)
|
||||
|
||||
next_id = max(records) + 1
|
||||
wrapper_ids = list(range(next_id, next_id + 18))
|
||||
(
|
||||
shell_id,
|
||||
model_id,
|
||||
length_id,
|
||||
angle_id,
|
||||
solid_angle_id,
|
||||
uncertainty_id,
|
||||
context_id,
|
||||
application_id,
|
||||
protocol_id,
|
||||
product_context_id,
|
||||
definition_context_id,
|
||||
product_id,
|
||||
formation_id,
|
||||
definition_id,
|
||||
definition_shape_id,
|
||||
representation_id,
|
||||
shape_definition_id,
|
||||
_unused_id,
|
||||
) = wrapper_ids
|
||||
|
||||
safe_name = quoted(args.name)
|
||||
face_refs = ",".join("#{}".format(entity_id) for entity_id in faces)
|
||||
if args.closed_shell:
|
||||
wrapper = [
|
||||
"#{}=CLOSED_SHELL('{} shell',({}));".format(
|
||||
shell_id, safe_name, face_refs
|
||||
),
|
||||
"#{}=MANIFOLD_SOLID_BREP('{} model',#{});".format(
|
||||
model_id, safe_name, shell_id
|
||||
),
|
||||
]
|
||||
else:
|
||||
wrapper = [
|
||||
"#{}=OPEN_SHELL('{} shell',({}));".format(
|
||||
shell_id, safe_name, face_refs
|
||||
),
|
||||
"#{}=SHELL_BASED_SURFACE_MODEL('{} model',(#{}));".format(
|
||||
model_id, safe_name, shell_id
|
||||
),
|
||||
]
|
||||
if args.context is None:
|
||||
wrapper.extend(
|
||||
[
|
||||
"#{}=(LENGTH_UNIT() NAMED_UNIT(*) "
|
||||
"SI_UNIT(.MILLI.,.METRE.));".format(length_id),
|
||||
"#{}=(NAMED_UNIT(*) PLANE_ANGLE_UNIT() "
|
||||
"SI_UNIT($,.RADIAN.));".format(angle_id),
|
||||
"#{}=(NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) "
|
||||
"SOLID_ANGLE_UNIT());".format(solid_angle_id),
|
||||
"#{}=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE({:.17g}),"
|
||||
"#{},'distance_accuracy_value','maximum gap value');".format(
|
||||
uncertainty_id, args.uncertainty, length_id
|
||||
),
|
||||
"#{}=(GEOMETRIC_REPRESENTATION_CONTEXT(3) "
|
||||
"GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{})) "
|
||||
"GLOBAL_UNIT_ASSIGNED_CONTEXT((#{},#{},#{})) "
|
||||
"REPRESENTATION_CONTEXT('fixture','3D'));".format(
|
||||
context_id,
|
||||
uncertainty_id,
|
||||
length_id,
|
||||
angle_id,
|
||||
solid_angle_id,
|
||||
),
|
||||
]
|
||||
)
|
||||
representation_context_id = (
|
||||
args.context if args.context is not None else context_id
|
||||
)
|
||||
wrapper.extend(
|
||||
[
|
||||
"#{}=APPLICATION_CONTEXT('automotive design');".format(application_id),
|
||||
"#{}=APPLICATION_PROTOCOL_DEFINITION("
|
||||
"'fixture','automotive_design',2010,#{});".format(
|
||||
protocol_id, application_id
|
||||
),
|
||||
"#{}=PRODUCT_CONTEXT('',#{},'mechanical');".format(
|
||||
product_context_id, application_id
|
||||
),
|
||||
"#{}=PRODUCT_DEFINITION_CONTEXT('part definition',#{},'design');".format(
|
||||
definition_context_id, application_id
|
||||
),
|
||||
"#{}=PRODUCT('{}','{}','reduced corpus regression fixture',(#{}));".format(
|
||||
product_id, safe_name, safe_name, product_context_id
|
||||
),
|
||||
"#{}=PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE("
|
||||
"'1','',#{},.MADE.);".format(formation_id, product_id),
|
||||
"#{}=PRODUCT_DEFINITION('design','',#{},#{});".format(
|
||||
definition_id, formation_id, definition_context_id
|
||||
),
|
||||
"#{}=PRODUCT_DEFINITION_SHAPE('','',#{});".format(
|
||||
definition_shape_id, definition_id
|
||||
),
|
||||
"#{}={}('',(#{}),#{});".format(
|
||||
representation_id,
|
||||
"ADVANCED_BREP_SHAPE_REPRESENTATION" if args.closed_shell else
|
||||
"MANIFOLD_SURFACE_SHAPE_REPRESENTATION",
|
||||
model_id,
|
||||
representation_context_id,
|
||||
),
|
||||
"#{}=SHAPE_DEFINITION_REPRESENTATION(#{},#{});".format(
|
||||
shape_definition_id, definition_shape_id, representation_id
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
lines = [
|
||||
"ISO-10303-21;",
|
||||
"HEADER;",
|
||||
"FILE_DESCRIPTION(('BRL-CAD reduced corpus regression fixture'),'2;1');",
|
||||
"FILE_NAME('{}','',('BRL-CAD'),('BRL-CAD'),'','','');".format(safe_name),
|
||||
"FILE_SCHEMA(('{}'));".format(quoted(schema)),
|
||||
"ENDSEC;",
|
||||
"DATA;",
|
||||
]
|
||||
lines.extend(records[entity_id] for entity_id in sorted(selected))
|
||||
lines.extend(wrapper)
|
||||
lines.extend(("ENDSEC;", "END-ISO-10303-21;", ""))
|
||||
args.output.write_text("\n".join(lines), encoding="latin-1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"format": "brlcad-step-corpus-expectations-v1",
|
||||
"description": "Checksum-pinned external samples used for independent producer qualification without copying their exchange files into BRL-CAD.",
|
||||
"redistribution": "The linked STEP Tools pages state All Rights Reserved, while the STEPcode collective work is distributed under its 3-clause BSD COPYING terms. Obtain each sample from its stated source; this manifest does not independently grant redistribution rights.",
|
||||
"files": [
|
||||
{
|
||||
"name": "cubsomcy.stp",
|
||||
"aliases": ["ap203_cubsomcy.stp"],
|
||||
"sha256": "eb8fb40f56523fb154b435066f7b2719a88e15e30d0b8d8e6d01dd99339ddcf1",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap203/index.html",
|
||||
"producer": "CATIA / Alcatel Space Industries",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 0,
|
||||
"coverage.products": 1,
|
||||
"coverage.occurrences": 0,
|
||||
"coverage.geometry_attempted": 2,
|
||||
"coverage.geometry_written": 2,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "complete",
|
||||
"coverage.entity_counts.ADVANCED_BREP_SHAPE_REPRESENTATION": 1,
|
||||
"coverage.entity_counts.FACETED_BREP_SHAPE_REPRESENTATION": 1,
|
||||
"derived.occurrence_max_depth": 0,
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "moon_buggy_asm.stp",
|
||||
"aliases": ["ap203_moon_buggy_asm.stp"],
|
||||
"sha256": "2afaaf50cd7ec15e36367be0a1e41730e7af5440ed974b62a0a21cf6346289dd",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap203/index.html",
|
||||
"producer": "STEPNet / EDS Unigraphics 10.5",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 1,
|
||||
"coverage.products": 20,
|
||||
"coverage.occurrences": 23,
|
||||
"coverage.geometry_attempted": 12,
|
||||
"coverage.geometry_written": 9,
|
||||
"coverage.geometry_skipped": 3,
|
||||
"coverage.outcome": "partial",
|
||||
"coverage.entity_counts.MAPPED_ITEM": 23,
|
||||
"derived.assembly_usage_count": 23,
|
||||
"derived.occurrence_detail_count": 23,
|
||||
"derived.occurrence_max_depth": 4,
|
||||
"derived.occurrence_methods": ["mapped"],
|
||||
"derived.skipped_entity_ids": [32060, 32220, 32820]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "123Block_Color.stp",
|
||||
"aliases": ["ap203e2_123Block_Color.stp"],
|
||||
"sha256": "4bbdd752fd3c4e7ebfee633da210b9e3df62c583a604e874ca5e9c36545d2be4",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap203e2/index.html",
|
||||
"producer": "CATIA / Boeing",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 0,
|
||||
"coverage.products": 1,
|
||||
"coverage.geometry_attempted": 1,
|
||||
"coverage.geometry_written": 1,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "complete",
|
||||
"coverage.styles_extracted": 1,
|
||||
"coverage.styles_applied": 1,
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "boxy_with_linearsize.stp",
|
||||
"aliases": ["ap203e2_boxy_with_linearsize.stp"],
|
||||
"sha256": "e120b982c5bb5c8b62c99bb03d5ae46c16d9200fcb53be6521c201be98ead6d6",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
|
||||
"producer": "STEP-NC Machine / STEP Tools, Inc.",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 3,
|
||||
"coverage.products": 1,
|
||||
"coverage.geometry_attempted": 1,
|
||||
"coverage.geometry_written": 0,
|
||||
"coverage.geometry_skipped": 1,
|
||||
"coverage.outcome": "failed",
|
||||
"coverage.entity_counts.DATUM": 3,
|
||||
"coverage.entity_counts.PLUS_MINUS_TOLERANCE": 1,
|
||||
"derived.skipped_entity_ids": [26]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "as1-ac-214.stp",
|
||||
"aliases": ["ap214_as1-ac-214.stp"],
|
||||
"sha256": "1bb1a0e55dc4a0169e329529eb520596690f8745077c1bdd2f9fa41fa5a2aa95",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
|
||||
"producer": "AutoCAD / STEPnet",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 0,
|
||||
"coverage.products": 9,
|
||||
"coverage.occurrences": 13,
|
||||
"coverage.geometry_attempted": 5,
|
||||
"coverage.geometry_written": 5,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "complete",
|
||||
"coverage.styles_extracted": 5,
|
||||
"coverage.styles_applied": 5,
|
||||
"coverage.layers_extracted": 1,
|
||||
"derived.assembly_usage_count": 13,
|
||||
"derived.occurrence_detail_count": 13,
|
||||
"derived.occurrence_max_depth": 3,
|
||||
"derived.occurrence_methods": ["referenced"],
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "as1-ug-214.stp",
|
||||
"aliases": ["ap214_as1-ug-214.stp"],
|
||||
"sha256": "53c5069f5d6b59435eb32ac743cc82e53bd3607954c94785d989d579d0e29426",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
|
||||
"producer": "Unigraphics / STEPnet",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 0,
|
||||
"coverage.products": 9,
|
||||
"coverage.occurrences": 13,
|
||||
"coverage.geometry_attempted": 5,
|
||||
"coverage.geometry_written": 5,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "complete",
|
||||
"coverage.styles_extracted": 5,
|
||||
"coverage.styles_applied": 5,
|
||||
"coverage.layers_extracted": 3,
|
||||
"coverage.properties_extracted": 42,
|
||||
"derived.assembly_usage_count": 13,
|
||||
"derived.occurrence_detail_count": 13,
|
||||
"derived.occurrence_max_depth": 3,
|
||||
"derived.occurrence_methods": ["referenced"],
|
||||
"derived.validation_property_count": 42,
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dm1-id-214.stp",
|
||||
"sha256": "25e727cdc4d738064da7907d604bd4ceeb4e941c6fdc201f3df04c0b94671661",
|
||||
"source_page": "STEPcode data/ap214e3/dm1-id-214.stp (3-clause BSD collective work)",
|
||||
"producer": "I-DEAS Master Series 9 / SDRC",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 0,
|
||||
"coverage.products": 7,
|
||||
"coverage.occurrences": 7,
|
||||
"coverage.geometry_attempted": 3,
|
||||
"coverage.geometry_written": 3,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "complete",
|
||||
"coverage.styles_extracted": 3,
|
||||
"coverage.styles_applied": 3,
|
||||
"coverage.materials_extracted": 3,
|
||||
"coverage.properties_extracted": 22,
|
||||
"coverage.entity_counts.MAKE_FROM_USAGE_OPTION": 3,
|
||||
"derived.assembly_usage_count": 7,
|
||||
"derived.occurrence_detail_count": 7,
|
||||
"derived.occurrence_max_depth": 1,
|
||||
"derived.occurrence_methods": ["referenced"],
|
||||
"derived.material_count": 3,
|
||||
"derived.material_identifiers": ["AMS 4928", "AMS 5613", "AMS 5662"],
|
||||
"derived.material_names": ["Greek Ascoloy", "Inconel 718", "Titanium 6-4"],
|
||||
"derived.validation_property_count": 19,
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"format": "brlcad-step-corpus-expectations-v1",
|
||||
"description": "Checksum-pinned independent-producer samples from locally retained, non-redistributed corpora whose original download pages were not recorded.",
|
||||
"redistribution": "These records provide reproducible local qualification evidence only. They do not grant permission to copy the exchange files into BRL-CAD or redistribute them.",
|
||||
"files": [
|
||||
{
|
||||
"name": "Leopard2-A6.step",
|
||||
"sha256": "ef8f21f6454f7d952b619ed1e41dab824f31f393e81a3e138392c53605a0ab94",
|
||||
"source_page": "Local non-redistributed GrabCAD corpus; original download page not recorded",
|
||||
"producer": "Autodesk Inventor 2018 / ST-Developer v17",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 1,
|
||||
"coverage.products": 14,
|
||||
"coverage.occurrences": 338,
|
||||
"coverage.geometry_attempted": 31,
|
||||
"coverage.geometry_written": 31,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "partial",
|
||||
"coverage.styles_extracted": 2425,
|
||||
"coverage.styles_applied": 31,
|
||||
"coverage.materials_extracted": 13,
|
||||
"coverage.properties_extracted": 26,
|
||||
"coverage.properties_invalid": 13,
|
||||
"validation.invalid_breps": 1,
|
||||
"validation.invalid_breps_written": 1,
|
||||
"validation.invalid_breps_rejected": 0,
|
||||
"derived.assembly_usage_count": 338,
|
||||
"derived.occurrence_detail_count": 338,
|
||||
"derived.occurrence_max_depth": 1,
|
||||
"derived.occurrence_methods": ["referenced"],
|
||||
"derived.product_metadata_count": 13,
|
||||
"derived.material_count": 13,
|
||||
"derived.material_identifiers": [""],
|
||||
"derived.material_names": ["Generic", "SAE 1020"],
|
||||
"derived.material_property_count": 26,
|
||||
"derived.material_property_names": ["Generic", "SAE 1020", "density measure"],
|
||||
"derived.material_property_units": ["gram^1*centimetre^3"],
|
||||
"derived.material_property_numeric_values": [1, 7.84],
|
||||
"derived.validation_property_count": 0,
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
Remaining STEP Import/Export Work
|
||||
=================================
|
||||
|
||||
Status: 2026-08-05
|
||||
|
||||
|
||||
Purpose
|
||||
-------
|
||||
|
||||
This is the current implementation and qualification plan for the public
|
||||
step-g and g-step converters. It replaces recommended_practices_todo.txt,
|
||||
save_invalid_plan.txt, and step_refactor.txt. It is not a development
|
||||
changelog: completed work is summarized only to define the supported baseline,
|
||||
and the detailed sections describe work which remains.
|
||||
|
||||
Status terms used below are:
|
||||
|
||||
* COMPLETE -- implemented and covered by focused tests;
|
||||
* ACTIVE -- being qualified or reduced from real inputs;
|
||||
* REMAINING -- useful work with a defined scope;
|
||||
* BLOCKED -- requires a missing prerequisite and must not be approximated; and
|
||||
* DEFERRED -- outside the present converter objective.
|
||||
|
||||
A feature is complete only when schema-valid fixtures, permissive and strict
|
||||
policy tests, and structured report assertions prove that geometry or semantics
|
||||
did not disappear. Independent-producer evidence is required wherever a
|
||||
BRL-CAD export and re-import could otherwise repeat the same mistake.
|
||||
|
||||
|
||||
Supported Baseline
|
||||
------------------
|
||||
|
||||
Architecture and schemas
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The public converter architecture is COMPLETE:
|
||||
|
||||
* step-g and g-step are schema-dispatching hosts;
|
||||
* AP203 edition 1, AP203 edition 2, AP214 edition 3, and AP242 editions 1
|
||||
through 4 are bidirectional plugins;
|
||||
* AP242 generated bindings remain edition-specific because the entity sets and
|
||||
some Part 21 physical layouts differ;
|
||||
* the AP242 export alias targets edition 4, while automatic import probes
|
||||
editions 4 through 1;
|
||||
* common conversion behavior is schema-neutral and schema-only behavior is
|
||||
under ap203, ap203e2, ap214, or ap242; and
|
||||
* obsolete schema-specific public converter programs are not built.
|
||||
|
||||
Explicit --schema ap242e1 through ap242e4 selection remains useful when a file
|
||||
uses only the editions' common physical subset. Interim identifiers
|
||||
CONFIGURATION_CONTROL_3D_DESIGN_MIM_LF, CCD_CLA_GVP_AST, and
|
||||
CCD_CLA_GVP_AST_ASD route to AP203e2. Supported multi-schema AP203e2 headers
|
||||
also route correctly; unknown companion schemas remain visible, and genuinely
|
||||
ambiguous supported-AP combinations fail rather than guessing.
|
||||
|
||||
The STEPcode lazy reader supports multiple DATA sections, SCOPE,
|
||||
source-ordered complex components, bounded diagnostics, type census output,
|
||||
and schema-independent retention of unknown configuration records. AP203
|
||||
Recommended Practice 8 fixtures cover referenced, mapped, and replicated
|
||||
shapes, multi-level hierarchy, SHUO and usage semantics, multiple SDR forms,
|
||||
wireframes, configuration data, and SCOPE.
|
||||
|
||||
STEPcode and build tractability
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
BRL-CAD uses STEPcode's compact generated API v2, packed schema modules,
|
||||
precomputed attribute layouts, bounded unity chunks, and generated source
|
||||
manifests. Entity and type chunk limits are independently tunable; 64:8 is the
|
||||
qualified default. STEP plugins retain optimized compilation but disable LTO.
|
||||
|
||||
On the qualification host, a serial AP242e4 binding build without LTO fell
|
||||
from 8m25s and 6.22 GB peak RSS at 64:64 to 6m12s and 1.33 GB at 64:8. NIST
|
||||
CTC 03 showed no measurable conversion benefit from LTO.
|
||||
|
||||
The importer is split into schema-neutral common build units for session and
|
||||
cache handling, document traversal, job scheduling, geometric sets, topology,
|
||||
periodic bands, pullback, seam repair, loop repair, validation, and
|
||||
finalization. Schema plugins consume those objects directly, preserving
|
||||
static Factory registration without a new runtime ABI. A local geometry
|
||||
repair edit therefore recompiles its topical unit and relinks only requested
|
||||
plugins. Revisit these boundaries only in response to a measured dependency,
|
||||
ownership, or rebuild problem.
|
||||
|
||||
Default policy and loss accounting
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The user-facing default is deliberately permissive:
|
||||
|
||||
* safe topology repair is enabled;
|
||||
* exact and strict modes are disabled;
|
||||
* storage-safe invalid BReps are preserved and explicitly tagged;
|
||||
* useful partial output is published with status 1 and a structured report;
|
||||
* --strict is transactional; and
|
||||
* --reject-invalid-objs restores invalid-BRep rejection.
|
||||
|
||||
Recovery order is source-faithful construction, bounded safe repair, tagged
|
||||
modeling inference for a proven source contradiction, marked storage-safe
|
||||
invalid fallback, and finally omission. Scale-bounded inference is attempted
|
||||
only in a detached whole-item retry after the ordinary candidate fails. It is
|
||||
committed only when either:
|
||||
|
||||
* the rebuilt BRep passes normal structural and solidness validation; or
|
||||
* permissive invalid preservation proves that a structurally safe open result
|
||||
corresponds one-for-one to source EDGE_CURVEs with exactly one authored
|
||||
ORIENTED_EDGE use. Such a result remains invalid and is tagged
|
||||
`step:invalidity=authored_open_topology`; it is never presented as a solid.
|
||||
|
||||
Inference is disabled by exact, strict, no-repair, and reject-invalid policy.
|
||||
Reports and affected database objects retain source edge identifiers,
|
||||
discrepancy, safe and inference limits, declared tolerance, inference kind,
|
||||
and explanatory detail. Rejected inference transactions do not contaminate
|
||||
ordinary results or diagnostics.
|
||||
|
||||
Every product-bound representation, representation item, selected BRL-CAD
|
||||
object, and reachable child receives a handled, intentionally non-geometric,
|
||||
skipped, malformed, unsupported, or failed disposition. Import and export
|
||||
distinguish complete, partial, empty, and failed outcomes. The pre-conversion
|
||||
entity census describes the input and does not imply conversion support.
|
||||
|
||||
A preserved invalid BRep is not, by itself, proof of bad authoring. Unless an
|
||||
independent topology proof establishes a source contradiction, it remains an
|
||||
unresolved converter result tagged `step:geometry_status=invalid_preserved`
|
||||
and `step:source_validity=unresolved`. It forces a partial result and remains
|
||||
in the failure ratchet.
|
||||
|
||||
Geometry
|
||||
~~~~~~~~
|
||||
|
||||
The following geometry paths are COMPLETE for their documented subsets:
|
||||
|
||||
* rigid product hierarchies, repeated occurrences, transforms, and shared
|
||||
product/occurrence emission in every supported schema;
|
||||
* optional native CSG import/export in AP203e2, AP214, and AP242, including
|
||||
supported Boolean trees, exact mapped primitives, BRep leaves, transforms,
|
||||
and RECTANGULAR_PYRAMID;
|
||||
* exact compatible extruded and revolved area/face solids and linear or
|
||||
circular SWEPT_DISK_SOLID in AP203e2, AP214, and AP242;
|
||||
* AP242 triangulated faces, simple/complex surface sets, shells, and solids,
|
||||
including strips, fans, pnindex, normals, welding, closure, and winding;
|
||||
* analytic and spline BRep import, periodic and singular topology, voids,
|
||||
bounded repair, transactional inference, and explicit invalid fallback;
|
||||
* solid-BRep export/re-import for sphere, cylinder, cone, torus, box, open
|
||||
NURBS faces with voids, rational Cobb surfaces, and BREP_WITH_VOIDS;
|
||||
* mated librt BRep fallback for EHY, EPA, HYP, PART, PIPE, EXTRUDE, REVOLVE,
|
||||
solid BOT, closed ARS, and supported NMG input;
|
||||
* sketch recovery for reordered or reversed segments, zero-length segments,
|
||||
inner loops, lines, circles, arcs, Beziers, and rational/non-rational NURBS;
|
||||
and
|
||||
* mixed GEOMETRIC_SET curves, points, and CURVE_BOUNDED_SURFACEs with partial
|
||||
set loss accounting and shared bounded pullback validation.
|
||||
|
||||
The detailed native-CSG primitive and operator matrix is maintained in
|
||||
src/conv/step/README.csg.md. AP203e1 has no native-CSG path; non-union AP203e1
|
||||
trees are BLOCKED on reliable Boolean evaluation.
|
||||
|
||||
STEP B-spline curves and surfaces share exact conversion from EXPRESS full knot
|
||||
vectors to OpenNURBS endpoint-truncated storage. Valid unclamped closed
|
||||
surfaces are clamped and only numerically equal seam controls are normalized,
|
||||
subject to dense before/after audits.
|
||||
|
||||
Presentation, units, configuration, and PMI
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
AP203e2, AP214, and AP242 share colour, transparency, style, layer, material,
|
||||
property, validation-value, and recursive unit-graph extraction. Export
|
||||
round-trips product identity and revision, presentation, multiple materials,
|
||||
typed properties, known SI and exact conversion units, derived units,
|
||||
occurrence labels, and reference designators. AP203e1 explicitly reports
|
||||
presentation or property requests it cannot author.
|
||||
|
||||
One constrained safe-mode repair handles an identified producer defect where
|
||||
a positive density's complete unit graph is mass^1 times length^3 instead of
|
||||
mass^1 times length^-3. The value is retained, the denominator dimension is
|
||||
corrected, provenance is recorded, and exact, strict, and no-repair modes
|
||||
reject the graph. This is not a general unit-guessing rule.
|
||||
|
||||
--output-units, --angle-units, and --uncertainty form one schema-neutral export
|
||||
policy covering BReps, native CSG, cone angles, and rigid occurrence
|
||||
translations.
|
||||
|
||||
Configuration data is retained as a typed reference graph with exact Part 21
|
||||
right-hand sides. Cross-schema authoring covers approvals, provenance,
|
||||
assignments, effectivity, alternatives, substitutes, categories, people,
|
||||
organizations, addresses, security classifications, documents, date/time
|
||||
chains, and their legal schema-specific assignment forms.
|
||||
|
||||
DATUM supports point, line, plane, frame, and bounded-target forms, semantic
|
||||
roles, axes, dimensions, identifiers, validation, transforms, and versioned
|
||||
legacy-compatible database storage. ANNOT supports model-space planes,
|
||||
per-segment roles and styles, UTF-8 text, runtime TrueType outlines,
|
||||
transforms, and legacy screen-space compatibility. The hardened private
|
||||
struetype code is shared from libbu; LGPL OSIFont remains runtime data and is
|
||||
not compiled into a binary.
|
||||
|
||||
AP242 PMI stage 1 retains connected semantic, presentation, association, and
|
||||
dependency records with source values, component order, references, product
|
||||
ownership, and native disposition. Exactly resolved point, axis, and plane
|
||||
datums become DATUM objects. Supported graphical content becomes model-space
|
||||
ANNOT geometry; ambiguous content remains semantic-only. Malformed
|
||||
presentation is partial in permissive mode and transactional in strict mode.
|
||||
|
||||
Safety and test infrastructure
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
BRL-CAD and STEPcode support ASan+UBSan and a separate TSan mode. Release and
|
||||
ASan+UBSan have covered the complete STEP/AP range; focused parallel, failure,
|
||||
native-CSG, and BRep-export paths have run under TSan. On the qualification
|
||||
host TSan must run through `setarch x86_64 -R` because its runtime otherwise
|
||||
fails before main. Repeated -j8 versus -j1 output comparison remains part of
|
||||
the determinism gate.
|
||||
|
||||
|
||||
Validation Snapshot
|
||||
-------------------
|
||||
|
||||
In-tree tests include success, malformed, partial, strict, round-trip,
|
||||
sanitizer, and output-validity cases across every supported schema. The last
|
||||
complete focused Release STEP matrix contains 197 tests. Official NIST MBE PMI
|
||||
files are stored under db/nist with documented provenance. Five AP203 MIM
|
||||
files exercise AP203e2, and 17 AP242 files span editions 1 through 4.
|
||||
|
||||
Checksum-pinned, non-redistributed STEP Tools and STEPcode samples cover CATIA,
|
||||
Unigraphics, AutoCAD, I-DEAS, STEPnet, and STEP Tools producers. Important
|
||||
independent modern-schema evidence currently includes:
|
||||
|
||||
* AP242 Leopard2-A6: 31/31 geometry items, 14 products, 338 occurrences,
|
||||
2,425 styles, 13 material identities, and 26 material values;
|
||||
* AP242 Open Source Rover: six product levels, 713 products, 1,390 rigid
|
||||
occurrences, and 569 applied styles; and
|
||||
* AP203e2 ROTATOR_ASM: eight product levels, 126 participating products, 404
|
||||
usages, 671 product-property records, and 3,991 styles.
|
||||
|
||||
The last complete unified corpus baseline is the 1,138-file dry run at
|
||||
`.build/step-corpus-full-j8-validity-ladder-20260804-post-speaker`:
|
||||
|
||||
* schemas: 616 AP203, 375 AP214, four AP203e2, and 143 AP242 files;
|
||||
* files: 1,057 clean; process statuses were 1,064 complete, 69 useful partial,
|
||||
three intentionally empty, one failed, and one SIGSEGV;
|
||||
* geometry: 41,765 of 41,992 attempted items written (99.46 percent), 221 exact
|
||||
omissions, and six unsupported-representation omissions;
|
||||
* invalid fallback: 386 structurally invalid but storage-safe BReps preserved,
|
||||
none rejected;
|
||||
* aggregate wall/conversion time: 36,796.2/34,678.9 seconds; and
|
||||
* maximum per-file RSS: 3.62 GB.
|
||||
|
||||
This remains the comparison baseline, not the current expected result. The
|
||||
crash and several omission groups have since been fixed in selected working
|
||||
runs. Its timing delta from the preceding baseline is not a controlled
|
||||
regression result because host load and available memory differed materially.
|
||||
Preserve both manifests and compare future runs only under similar load.
|
||||
|
||||
The corpus runner records hashes, schemas, converter/plugin/shared-library
|
||||
identities, reports, logs, wall time, peak RSS, host load, memory availability,
|
||||
geometry outcomes, hierarchy depth, metadata, and failure categories. Dry
|
||||
mode still parses, constructs, validates, and repairs geometry; it omits only
|
||||
database serialization. Historical `.g` files are comparative artifacts, not
|
||||
correctness oracles, and must remain intact.
|
||||
|
||||
Selected post-baseline qualifications establish:
|
||||
|
||||
* cargo ship now terminates normally; its two remaining selected omissions are
|
||||
explicit missing required FACE_SURFACE references;
|
||||
* hAVYARD now terminates normally after rejected seam transactions reacquire
|
||||
topology owned by the restored BRep; a direct owned lifetime fixture remains
|
||||
to be added;
|
||||
* SNAP #801290 is a valid solid after a face-scoped horn-torus keyhole repair;
|
||||
* all ten historical T90 omissions serialize: six valid solids, two
|
||||
source-open BReps tagged `authored_open_topology`, and two storage-safe
|
||||
partial fallbacks whose contradictions exceed bounded inference limits;
|
||||
* paired 135-file AP203/AP242 Recurta runs write all 139 attempted items per
|
||||
schema as valid BReps, with three tagged inferences per schema; and
|
||||
* the selected 421-face speaker solid now produces a validated 419-face solid
|
||||
through a whole-item shared-spur transaction.
|
||||
|
||||
The T90 serialized selection is recorded in
|
||||
`.build/t90_omissions_final_write.json`; it attempted and wrote ten of ten,
|
||||
skipped none, and explicitly preserved four invalid objects. The file-level
|
||||
partial status is intentional and does not represent an omission.
|
||||
|
||||
Two measured performance targets remain useful:
|
||||
|
||||
* Recurta `10012.step`, 992 faces: 78.7 seconds in the final qualification;
|
||||
profiling attributes most time to periodic trim-orientation repair and
|
||||
repeated transaction work, not preflight construction; and
|
||||
* chess king, 13,234 faces: about 616.8 seconds and 2.92 GB in its selected
|
||||
clean run.
|
||||
|
||||
|
||||
Remaining Work in Recommended Order
|
||||
-----------------------------------
|
||||
|
||||
1. Close the current corpus ratchet -- ACTIVE
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Complete selected work before starting another expensive unified run:
|
||||
|
||||
1. Add a compact rejected-seam-transaction lifetime fixture, qualify it under
|
||||
ASan+UBSan, and retain the complete hAVYARD real-file check.
|
||||
2. Sample and reduce the largest post-baseline omission sets:
|
||||
|
||||
* Garrett: 49;
|
||||
* 2-litre engine: 49, including 48 GEOMETRIC_SET items;
|
||||
* Open Source Rover: 16;
|
||||
* Epoxy granite VMC: seven; and
|
||||
* BLANCO: five.
|
||||
|
||||
3. Retest the former 58-omission GEOMETRIC_SET group with the completed bounded
|
||||
surface path. The independent b/c/d compatibility files are already
|
||||
qualified except for six d-wireframe boundary/pcurve disagreements.
|
||||
4. Reduce the Leopard2-A6 export/re-import failure at STEP edge #460691. One
|
||||
of 31 exported BReps fails while splitting a pcurve whose candidate
|
||||
fragment is 36.4476 model units from its subedge. This is a geometry
|
||||
interoperability problem, not schema selection or assembly loss.
|
||||
5. Retain MOUSE as a source edge/surface mismatch unless independent geometry
|
||||
proves a narrower correction.
|
||||
6. Revisit T90 #20240 and #20307 only if independent geometry supports a
|
||||
narrower reconstruction. Their marked partial results are preferable to
|
||||
inventing a false solid.
|
||||
7. Remove repeated full-BRep scans and copies from large periodic repair
|
||||
transactions while preserving exact rollback behavior.
|
||||
8. Run focused Release, ASan+UBSan, TSan where applicable, and -j1/-j8
|
||||
determinism gates. Then run the complete 1,138-file qualification.
|
||||
|
||||
Corpus fixes require a compact reproducer, permissive and strict assertions,
|
||||
structured report coverage, and a selected real-file check. Do not introduce
|
||||
broad repair heuristics solely to improve corpus percentages.
|
||||
|
||||
The next unified run is acceptable only if every file reaches a normal terminal
|
||||
report; there is no crash, hang, schema-load failure, corrupt output, or
|
||||
unexplained zero-geometry result; and all omissions and invalid fallbacks are
|
||||
grouped by owned root cause.
|
||||
|
||||
2. Expand independent AP203e2/AP242 evidence -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Locate and pin independent examples for:
|
||||
|
||||
* AP203e2 material and property association variants;
|
||||
* AP242 materials beyond the Inventor MAKE_FROM_USAGE_OPTION pattern;
|
||||
* mixed representation contexts and large multiple-product files; and
|
||||
* AP203e2/AP242 sweeps, NURBS, shells, voids, and mixed BRep/CSG assemblies.
|
||||
|
||||
Promote small distributable reductions into in-tree fixtures. For external
|
||||
inputs record source, producer, SHA-256, and structured expectations. A
|
||||
BRL-CAD-authored round trip alone cannot close this item.
|
||||
|
||||
3. Complete AP242 semantic PMI and MBD -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Stage 1 graph retention and exact native point/axis/plane/curve creation are
|
||||
complete. Remaining interpretation and authoring work is to:
|
||||
|
||||
* normalize dimension type/value, tolerance type and magnitude,
|
||||
material-condition modifiers, datum-reference compartments, and zones;
|
||||
* preserve stable product, occurrence, face, edge, and vertex target links;
|
||||
* distinguish semantic-only, graphical-only, and validated combined PMI;
|
||||
* map representable text, leaders, extension/dimension lines, arrowheads,
|
||||
fills, area targets, and supported symbols;
|
||||
* explicitly report graphical content which cannot be mapped faithfully;
|
||||
* author retained semantic and supported presentation records in AP242; and
|
||||
* qualify against NIST PMI and independent AP242 files, not only hand-authored
|
||||
fixtures.
|
||||
|
||||
An ANNOT outline is not proof that original text or tolerance semantics were
|
||||
recovered. Do not infer missing semantics from graphical similarity.
|
||||
|
||||
4. Complete tessellated coverage and optional export -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Add faithful mappings for tessellated point, curve, wire, geometric-set, and
|
||||
repositioned-item forms. Add explicit optional AP242 BOT export using the
|
||||
shared unit and uncertainty policy. Tessellation must not silently replace
|
||||
the eventual evaluated-BRep default for analytic solids.
|
||||
|
||||
5. Broaden existing-BRep interoperability -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Reduce the six d-wireframe boundary/pcurve disagreements and independently
|
||||
classify each as an authored tolerance violation or adapter defect. Preserve
|
||||
faithful partial-set output until that proof exists; do not stretch pcurves or
|
||||
widen model uncertainty merely to reach complete counts.
|
||||
|
||||
Expand independent-producer coverage for rational and non-rational NURBS,
|
||||
open shells, nested BREP_WITH_VOIDS, reversed proxies/faces, difficult trims,
|
||||
periodic and singular surfaces, and closed curves. Define a legal policy for
|
||||
one BRL-CAD object containing disconnected material volumes. Add wireframe
|
||||
and surface-curve export for databases without face topology.
|
||||
|
||||
Verification must include STEPcode readback, strict BRL-CAD re-import,
|
||||
topology and material-ray comparison, and an independent STEP validator or
|
||||
consumer where available. Textual identity is insufficient.
|
||||
|
||||
Sketch containment still needs a complete loop hierarchy for disjoint outer
|
||||
regions and nested material islands. REVOLVE should split profiles at sharp
|
||||
C0/tangent discontinuities when needed to avoid pathological wireframe and CDT
|
||||
refinement.
|
||||
|
||||
DSP, deprecated HF, EBM, and VOL are a separate scalability and fidelity
|
||||
project. They require bounded-error patching or faithful faceting, discrete
|
||||
boundary simplification, source-semantic ray/material comparisons, and hard
|
||||
validity, memory, and complexity limits. A single high-degree DSP surface or
|
||||
unbounded voxel-to-BRep expansion is not an acceptable general solution.
|
||||
|
||||
6. Complete product and configuration mappings -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Define scoped mappings and loss policy for region identifiers,
|
||||
material/shader strings, and selected arbitrary BRL-CAD attributes. Do not
|
||||
conflate opaque attributes with typed STEP properties.
|
||||
|
||||
Broaden normalized named-unit conveniences only where schemas agree; the
|
||||
recursive retained graph remains authoritative. Extend authoring for
|
||||
remaining configuration identities, relationships, complex addresses, and
|
||||
application edges only when required source values exist. Expand
|
||||
import-export-import tests to multiple products, occurrences, shared complex
|
||||
effectivities, and schema-specific graphs.
|
||||
|
||||
7. Resolve assembly and native-CSG edge cases -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Define explicit policy for scale, mirror, and future non-rigid occurrence
|
||||
transforms. Preserve or explicitly report nested combination identity inside
|
||||
one CSG expression; STEP CSG_SELECT cannot directly use another CSG_SOLID as
|
||||
an operand. Expand stable naming and independent multi-level mixed BRep/CSG
|
||||
evidence across AP203e2, AP214, and AP242.
|
||||
|
||||
NOT and guard/no-op Boolean nodes have no lossless finite STEP CSG encoding.
|
||||
AP242 CYCLIDE_SEGMENT_SOLID remains unsupported because recognizing a faithful
|
||||
BRL-CAD PIPE subset requires additional geometric semantics; similar
|
||||
dimensions alone are insufficient.
|
||||
|
||||
8. Implement remaining exact sweeps -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Add nonlinear swept-disk directrices only when OpenNURBS can construct a
|
||||
faithful pipe with validated frame continuity and self-intersection handling.
|
||||
Ellipse, spline, and composite-curve cases remain. Implement drafted and
|
||||
trimmed face sweeps only after referenced termination, offset, trim intent,
|
||||
and per-edge draft can be evaluated exactly. Inherited depth or angle is not
|
||||
a faithful fallback.
|
||||
|
||||
|
||||
Blocked and Deferred Work
|
||||
-------------------------
|
||||
|
||||
Evaluated-BRep default export -- BLOCKED
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The broadly interoperable default should eventually evaluate arbitrary
|
||||
Boolean trees into final BReps. Current Boolean evaluation is not reliable
|
||||
enough. Continue evaluator work and validation; when it is ready, make
|
||||
evaluated BRep the default without weakening optional exact native CSG.
|
||||
|
||||
STEPcode upstreaming -- DEFERRED maintenance
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The generated-binding scalability work required by step-g and g-step is
|
||||
complete. Remaining STEPcode work is upstream preparation and maintenance:
|
||||
retain API v1 compatibility where required, and address correctness, cache,
|
||||
generation, or resource defects when measured. Reopen generator scalability
|
||||
only for a demonstrated regression or a new schema exceeding current limits.
|
||||
|
||||
Additional application protocols -- DEFERRED
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Do not build every ISO SMRL schema speculatively. AP224 machining features and
|
||||
AP238 STEP-NC are the strongest future geometry/manufacturing candidates once
|
||||
representative inputs and mapping goals exist. AP209 analysis and AP210
|
||||
electronics require domain-specific semantics. AP232 and AP239 are primarily
|
||||
metadata protocols and are not geometry targets merely because they share
|
||||
integrated resources.
|
||||
|
||||
ISO SMRL and historical STEPmod selection, provenance, and redistribution
|
||||
policy are documented in STEP(5).
|
||||
|
||||
|
||||
Definition of Done
|
||||
------------------
|
||||
|
||||
Every new decoder, emitter, or repair must include, as applicable:
|
||||
|
||||
1. an independent hand-authored or external fixture;
|
||||
2. a supported case and a malformed or explicitly unsupported case;
|
||||
3. permissive and transactional strict assertions;
|
||||
4. structured report checks proving exhaustive loss accounting;
|
||||
5. schema-specific coverage where legal alternatives or physical layouts
|
||||
differ;
|
||||
6. round-trip geometry, topology, semantic, and unit checks;
|
||||
7. Release, sanitizer, and deterministic parallel tests;
|
||||
8. selected corpus reruns for every affected real file; and
|
||||
9. matching step-g, g-step, STEP(5), CSG matrix, and plan documentation.
|
||||
|
||||
The overall STEP effort is broadly robust when:
|
||||
|
||||
* every input and selected output object is exhaustively accounted for;
|
||||
* the unified corpus has no crash, hang, schema-load failure, corrupt output,
|
||||
or unexplained zero-geometry result;
|
||||
* AP203e2 and AP242 have independent producer evidence comparable to the
|
||||
relevant AP203/AP214 baseline;
|
||||
* supported tessellation, rigid assemblies, presentation, configuration, and
|
||||
PMI round-trip with explicit losses;
|
||||
* existing BReps interoperate across analytic, NURBS, periodic, singular,
|
||||
shell, void, orientation, and trim cases with independent consumers;
|
||||
* native CSG remains optional, exact, and explicit about unsupported content;
|
||||
* parallel, sanitizer, deterministic-output, performance, and memory gates
|
||||
remain green; and
|
||||
* reliable Boolean evaluation enables evaluated-BRep default export without
|
||||
regressing native CSG.
|
||||
@@ -53,7 +53,7 @@ set(
|
||||
)
|
||||
|
||||
brlcad_addlib(librender "${LIBRENDER_SRCS}" "" "${ADRT_LOCAL_INCLUDE_DIRS}"
|
||||
PRIVATE_LIBS libgcv librt ZLIB::ZLIB ${M_LIBRARY}
|
||||
PRIVATE_LIBS libgcv ZLIB::ZLIB ${M_LIBRARY}
|
||||
)
|
||||
set_target_properties(librender PROPERTIES VERSION 20.0.1 SOVERSION 20)
|
||||
add_dependencies(librender gcv_plugins)
|
||||
@@ -109,9 +109,8 @@ cmakefiles(isst.c)
|
||||
|
||||
add_subdirectory(master)
|
||||
add_subdirectory(slave)
|
||||
add_subdirectory(tests)
|
||||
|
||||
set(adrt_ignore_files load.h load_g.h tienet.h)
|
||||
set(adrt_ignore_files load.h tienet.h)
|
||||
cmakefiles(${adrt_ignore_files})
|
||||
cmakefiles(CMakeLists.txt)
|
||||
|
||||
|
||||
@@ -90,19 +90,11 @@ 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
|
||||
|
||||
|
||||
+38
-23
@@ -59,7 +59,6 @@ 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;
|
||||
@@ -68,7 +67,6 @@ 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;
|
||||
@@ -121,7 +119,13 @@ 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);
|
||||
isstp->texdata = realloc(isstp->texdata, isstp->camera.w * isstp->camera.h * 3);
|
||||
{
|
||||
/* 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;
|
||||
}
|
||||
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGB, isstp->camera.w, isstp->camera.h, 0, GL_RGB, GL_UNSIGNED_BYTE, isstp->texdata);
|
||||
glDisable(GL_LIGHTING);
|
||||
|
||||
@@ -154,14 +158,11 @@ 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]));
|
||||
|
||||
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);
|
||||
load_g(isst->tie, Tcl_GetString(objv[2]), argc, (const char **)argv, &(isst->meshes));
|
||||
free(argv);
|
||||
|
||||
isst->scene_radius = render_camera_fit_scene(&isst->camera, isst->tie,
|
||||
isst->annotations, ADRT_MODEL_UNITS_PER_TIE_UNIT);
|
||||
VSETALL(isst->camera.pos, isst->tie->radius);
|
||||
VMOVE(isst->camera.focus, isst->tie->mid);
|
||||
VMOVE(isst->camera_pos_init, isst->camera.pos);
|
||||
VMOVE(isst->camera_focus_init, isst->camera.focus);
|
||||
|
||||
@@ -238,9 +239,7 @@ 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_annotations(&isst->camera, isst->tie, &isst->tile,
|
||||
&isst->buffer_image, isst->annotations,
|
||||
ADRT_MODEL_UNITS_PER_TIE_UNIT);
|
||||
render_camera_render(&isst->camera, isst->tie, &isst->tile, &isst->buffer_image);
|
||||
|
||||
isst->t1 = bu_gettime();
|
||||
|
||||
@@ -283,6 +282,7 @@ 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,10 +326,13 @@ 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;
|
||||
|
||||
@@ -351,7 +354,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;
|
||||
|
||||
@@ -388,22 +391,26 @@ zero_view(ClientData UNUSED(clientData), Tcl_Interp *UNUSED(interp), int UNUSED(
|
||||
|
||||
|
||||
static int
|
||||
move_walk(ClientData UNUSED(clientData), Tcl_Interp *interp, int UNUSED(objc), Tcl_Obj *const *objv)
|
||||
move_walk(ClientData UNUSED(clientData), Tcl_Interp *interp, int 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->scene_radius);
|
||||
VSCALE(vec, vec, 0.1 * isst->tie->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->scene_radius);
|
||||
VSCALE(vec, vec, 0.1 * isst->tie->radius);
|
||||
VADD2(isst->camera.pos, isst->camera.pos, vec);
|
||||
VADD2(isst->camera.focus, isst->camera.focus, vec);
|
||||
}
|
||||
@@ -412,12 +419,16 @@ move_walk(ClientData UNUSED(clientData), Tcl_Interp *interp, int UNUSED(objc), T
|
||||
}
|
||||
|
||||
static int
|
||||
move_strafe(ClientData UNUSED(clientData), Tcl_Interp *interp, int UNUSED(objc), Tcl_Obj *const *objv)
|
||||
move_strafe(ClientData UNUSED(clientData), Tcl_Interp *interp, int 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;
|
||||
|
||||
@@ -426,13 +437,13 @@ move_strafe(ClientData UNUSED(clientData), Tcl_Interp *interp, int UNUSED(objc),
|
||||
if (flag >= 0) {
|
||||
VSUB2(dir, isst->camera.focus, isst->camera.pos);
|
||||
VCROSS(vec, dir, up);
|
||||
VSCALE(vec, vec, 0.1 * isst->scene_radius);
|
||||
VSCALE(vec, vec, 0.1 * isst->tie->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->scene_radius);
|
||||
VSCALE(vec, vec, -0.1 * isst->tie->radius);
|
||||
VADD2(isst->camera.pos, isst->camera.pos, vec);
|
||||
VADD2(isst->camera.focus, isst->camera.pos, dir);
|
||||
}
|
||||
@@ -540,10 +551,14 @@ 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 */
|
||||
/* 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. */
|
||||
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);
|
||||
@@ -561,7 +576,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("dm failed?\n");
|
||||
printf("Failed to open display manager\n");
|
||||
return TCL_ERROR;
|
||||
}
|
||||
|
||||
@@ -639,7 +654,7 @@ const char *fullname;
|
||||
argv = __argv;
|
||||
#endif
|
||||
|
||||
/* initialize progname for run-tim resource finding */
|
||||
/* initialize progname for run-time resource finding */
|
||||
bu_setprogname(argv[0]);
|
||||
|
||||
#ifdef HAVE_WINDOWS_H
|
||||
|
||||
+21
-225
@@ -29,7 +29,6 @@
|
||||
#include "bu/dylib.h"
|
||||
#include "bu/log.h"
|
||||
#include "bu/str.h"
|
||||
#include "rt/primitives/annot.h"
|
||||
|
||||
#include "./camera.h"
|
||||
|
||||
@@ -44,27 +43,13 @@ struct render_shader_s {
|
||||
|
||||
static struct render_shader_s *shaders = NULL;
|
||||
|
||||
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;
|
||||
|
||||
/* number of leading characters compared when matching a shader by name */
|
||||
#define RENDER_SHADER_NAME_CMP_LEN 8
|
||||
|
||||
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 *));
|
||||
|
||||
@@ -408,147 +393,20 @@ render_camera_prep(render_camera_t *camera)
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
void
|
||||
render_camera_render_thread(int UNUSED(cpu), void *ptr)
|
||||
{
|
||||
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;
|
||||
@@ -599,15 +457,9 @@ render_camera_render_thread_impl(int UNUSED(cpu), render_camera_thread_data_t *t
|
||||
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);
|
||||
}
|
||||
@@ -624,7 +476,6 @@ render_camera_render_thread_impl(int UNUSED(cpu), render_camera_thread_data_t *t
|
||||
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);
|
||||
@@ -639,19 +490,12 @@ render_camera_render_thread_impl(int UNUSED(cpu), render_camera_thread_data_t *t
|
||||
|
||||
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);
|
||||
@@ -679,30 +523,9 @@ render_camera_render_thread_impl(int UNUSED(cpu), render_camera_thread_data_t *t
|
||||
|
||||
|
||||
void
|
||||
render_camera_render_thread(int cpu, void *ptr)
|
||||
render_camera_render(render_camera_t *camera, struct tie_s *tie, camera_tile_t *tile, tienet_buffer_t *result)
|
||||
{
|
||||
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};
|
||||
render_camera_thread_data_t td;
|
||||
unsigned int scanline;
|
||||
uint32_t ind;
|
||||
|
||||
@@ -720,47 +543,19 @@ render_camera_render_internal(render_camera_t *camera, struct tie_s *tie,
|
||||
TCOPY(camera_tile_t, tile, 0, result->data, result->ind);
|
||||
result->ind += sizeof(camera_tile_t);
|
||||
|
||||
data.thread.tie = tie;
|
||||
data.thread.camera = camera;
|
||||
data.thread.tile = tile;
|
||||
data.thread.res_buf = &((char *)result->data)[result->ind];
|
||||
td.tie = tie;
|
||||
td.camera = camera;
|
||||
td.tile = tile;
|
||||
td.res_buf = &((char *)result->data)[result->ind];
|
||||
scanline = 0;
|
||||
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);
|
||||
td.scanline = &scanline;
|
||||
td.sem_tie_worker = bu_semaphore_register("sem_tie_worker");
|
||||
|
||||
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);
|
||||
bu_parallel(render_camera_render_thread, camera->thread_num, &td);
|
||||
|
||||
result->ind = ind;
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -825,9 +620,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, 8)) {
|
||||
if (!bu_strncmp(s->name, name, RENDER_SHADER_NAME_CMP_LEN)) {
|
||||
t = s->next;
|
||||
if (r && r->shader && !bu_strncmp(r->shader, name, 8)) {
|
||||
if (r && r->shader && !bu_strncmp(r->shader, name, RENDER_SHADER_NAME_CMP_LEN)) {
|
||||
meh = s->next;
|
||||
while (meh) {
|
||||
if (render_shader_init(r, meh->name, NULL) != -1)
|
||||
@@ -846,7 +641,7 @@ LOADED:
|
||||
}
|
||||
|
||||
while (s->next) {
|
||||
if (!bu_strncmp(s->next->name, name, 8)) {
|
||||
if (!bu_strncmp(s->next->name, name, RENDER_SHADER_NAME_CMP_LEN)) {
|
||||
if (r)
|
||||
render_shader_init(r, s->name, NULL);
|
||||
if (s->next->dlh)
|
||||
@@ -856,6 +651,7 @@ LOADED:
|
||||
bu_free(t, "unload shader");
|
||||
return 0;
|
||||
}
|
||||
s = s->next;
|
||||
}
|
||||
|
||||
bu_log("Could not find shader \"%s\"\n", name);
|
||||
@@ -871,7 +667,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, 8)) {
|
||||
if (!bu_strncmp(s->name, name, RENDER_SHADER_NAME_CMP_LEN)) {
|
||||
s->init(r, buf);
|
||||
r->shader = s->name;
|
||||
return 0;
|
||||
|
||||
@@ -99,18 +99,7 @@ __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);
|
||||
|
||||
+10
-86
@@ -40,13 +40,11 @@
|
||||
#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 */
|
||||
@@ -106,42 +104,6 @@ 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)
|
||||
@@ -191,8 +153,7 @@ 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 / ADRT_MODEL_UNITS_PER_TIE_UNIT);
|
||||
VSCALE((*tribuf[vert_count]).v, v->vg_p->coord, 1.0/1000.0);
|
||||
vert_count++;
|
||||
}
|
||||
if (vert_count > 3)
|
||||
@@ -287,12 +248,9 @@ 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 / 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);
|
||||
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);
|
||||
|
||||
TIE_VAL(tie_push)(cur_tie, tribuf, 1, mesh, 0);
|
||||
}
|
||||
@@ -340,16 +298,11 @@ load_nmg_to_adrt_gcvwrite(struct nmgregion *r, const struct db_full_path *pathp,
|
||||
|
||||
|
||||
int
|
||||
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)
|
||||
load_g(struct tie_s *tie, const char *db, int argc, const char **argv, struct adrt_mesh_s **meshes)
|
||||
{
|
||||
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. */
|
||||
@@ -385,21 +338,13 @@ load_g_annotations(struct tie_s *tie, const char *db, int argc,
|
||||
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);
|
||||
|
||||
@@ -417,26 +362,15 @@ load_g_annotations(struct tie_s *tie, const char *db, int argc,
|
||||
tribuf[1] = (TIE_3 *)bu_malloc(sizeof(TIE_3) * 3, "triangle tribuffer");
|
||||
tribuf[2] = (TIE_3 *)bu_malloc(sizeof(TIE_3) * 3, "triangle tribuffer");
|
||||
|
||||
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 */
|
||||
(void) db_walk_tree(dbip,
|
||||
argc, /* number of toplevel regions */
|
||||
argv, /* region names */
|
||||
1, /* ncpu */
|
||||
&tree_state, /* initial tree state */
|
||||
load_nmg_to_adrt_regstart, /* region start function */
|
||||
gcv_region_end, /* region end function */
|
||||
adrt_leaf_tess, /* leaf func */
|
||||
rt_booltree_leaf_tess, /* leaf func */
|
||||
(void *)&gcvwriter); /* client data */
|
||||
}
|
||||
|
||||
/* Release dynamic storage */
|
||||
nmg_km(the_model);
|
||||
@@ -446,20 +380,10 @@ load_g_annotations(struct tie_s *tie, const char *db, int argc,
|
||||
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 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);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/* 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
|
||||
*/
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
@@ -1,179 +0,0 @@
|
||||
/* 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
|
||||
*/
|
||||
+480
-399
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -64,17 +64,16 @@ 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 * 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ 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})
|
||||
|
||||
+15
-52
@@ -26,7 +26,6 @@
|
||||
#include <QFile>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QTextStream>
|
||||
#include <vector>
|
||||
#include "gfile.h"
|
||||
|
||||
#include "bu/malloc.h"
|
||||
@@ -34,7 +33,6 @@
|
||||
#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. */
|
||||
@@ -104,8 +102,7 @@ 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 / ADRT_MODEL_UNITS_PER_TIE_UNIT);
|
||||
VSCALE((*tribuf[vert_count]).v, v->vg_p->coord, 1.0/1000.0);
|
||||
vert_count++;
|
||||
}
|
||||
if (vert_count > 3)
|
||||
@@ -188,12 +185,9 @@ 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 / 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);
|
||||
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);
|
||||
|
||||
TIE_VAL(tie_push)(d->cur_tie, d->tribuf, 1, mesh, 0);
|
||||
}
|
||||
@@ -239,13 +233,6 @@ 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[])
|
||||
{
|
||||
@@ -254,9 +241,6 @@ 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;
|
||||
@@ -294,8 +278,6 @@ 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;
|
||||
@@ -322,30 +304,20 @@ 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;
|
||||
|
||||
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 */
|
||||
}
|
||||
(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 */
|
||||
|
||||
/* 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");
|
||||
@@ -359,17 +331,7 @@ GFile::load_g(const char *filename, int argc, const char *argv[])
|
||||
void
|
||||
GFile::closedb()
|
||||
{
|
||||
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;
|
||||
}
|
||||
bu_free(tie, "free tie");
|
||||
current_file.clear();
|
||||
}
|
||||
|
||||
@@ -383,3 +345,4 @@ GFile::closedb()
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||
|
||||
|
||||
+5
-8
@@ -32,7 +32,6 @@
|
||||
extern "C" {
|
||||
#include "bn/tol.h"
|
||||
#include "raytrace.h"
|
||||
#include "rt/primitives/annot.h"
|
||||
|
||||
#include "rt/tie.h"
|
||||
#include "adrt.h"
|
||||
@@ -44,16 +43,13 @@ 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 = NULL;
|
||||
struct adrt_mesh_s *meshes = NULL;
|
||||
struct db_i *dbip = NULL;
|
||||
struct rt_annot_scene *annotations = NULL;
|
||||
TIE_3 **tribuf = NULL;
|
||||
struct tie_s *tie;
|
||||
struct adrt_mesh_s *meshes;
|
||||
struct db_i *dbip;
|
||||
TIE_3 **tribuf;
|
||||
|
||||
QString current_file;
|
||||
};
|
||||
@@ -70,3 +66,4 @@ class GFile : public QObject
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||
|
||||
|
||||
@@ -28,9 +28,6 @@
|
||||
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;
|
||||
@@ -38,7 +35,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, g.annotations);
|
||||
w.canvas->set_tie(g.tie);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -54,3 +51,4 @@ ISSTApp::load_g(const char *filename, int argc, const char *argv[])
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||
|
||||
|
||||
+4
-1
@@ -36,14 +36,16 @@
|
||||
|
||||
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);
|
||||
|
||||
GFile g;
|
||||
ISST_MainWindow w;
|
||||
GFile g;
|
||||
};
|
||||
|
||||
#endif // ISSTAPP_H
|
||||
@@ -58,3 +60,4 @@ class ISSTApp : public QApplication
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||
|
||||
|
||||
+9
-28
@@ -159,13 +159,8 @@ void TIERenderer::render()
|
||||
m_grabCond.wait(&m_grabMutex);
|
||||
QMutexLocker lock(&m_renderMutex);
|
||||
m_grabMutex.unlock();
|
||||
|
||||
/* 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());
|
||||
if (m_exiting)
|
||||
return;
|
||||
}
|
||||
Q_ASSERT(ctx->thread() == QThread::currentThread());
|
||||
|
||||
|
||||
@@ -188,8 +183,7 @@ void TIERenderer::render()
|
||||
|
||||
// Core TIE render
|
||||
render_camera_prep(&camera);
|
||||
render_camera_render_annotations(&camera, tie, &tile, &buffer_image,
|
||||
annotations, ADRT_MODEL_UNITS_PER_TIE_UNIT);
|
||||
render_camera_render(&camera, tie, &tile, &buffer_image);
|
||||
|
||||
glDisable(GL_LIGHTING);
|
||||
|
||||
@@ -303,31 +297,17 @@ void isstGL::grabContext()
|
||||
}
|
||||
|
||||
void
|
||||
isstGL::clear_tie()
|
||||
isstGL::set_tie(struct tie_s *in_tie)
|
||||
{
|
||||
m_renderer->lockRenderer();
|
||||
m_renderer->tie = NULL;
|
||||
m_renderer->annotations = NULL;
|
||||
m_renderer->changed = false;
|
||||
m_renderer->unlockRenderer();
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
// Initialize the camera position
|
||||
VSETALL(m_renderer->camera.pos, m_renderer->tie->radius);
|
||||
VMOVE(m_renderer->camera.focus, m_renderer->tie->mid);
|
||||
|
||||
// Record the initial settings for use in subsequent calculations
|
||||
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();
|
||||
VSETALL(m_renderer->camera_pos_init, m_renderer->tie->radius);
|
||||
VMOVE(m_renderer->camera_focus_init, m_renderer->tie->mid);
|
||||
|
||||
// Having just loaded a new TIE scene,
|
||||
// we need a new image
|
||||
@@ -389,3 +369,4 @@ void isstGL::save_image() {
|
||||
// c-file-style: "stroustrup"
|
||||
// End:
|
||||
// ex: shiftwidth=4 tabstop=8
|
||||
|
||||
|
||||
+2
-3
@@ -77,7 +77,6 @@ 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;
|
||||
@@ -114,8 +113,7 @@ class isstGL : public QOpenGLWidget
|
||||
explicit isstGL(QWidget *parent = nullptr);
|
||||
~isstGL();
|
||||
|
||||
void clear_tie();
|
||||
void set_tie(struct tie_s *in_tie, struct rt_annot_scene *annotations);
|
||||
void set_tie(struct tie_s *in_tie);
|
||||
|
||||
void save_image();
|
||||
|
||||
@@ -155,3 +153,4 @@ class isstGL : public QOpenGLWidget
|
||||
// c-file-style: "stroustrup"
|
||||
// End:
|
||||
// ex: shiftwidth=4 tabstop=8
|
||||
|
||||
|
||||
@@ -480,22 +480,6 @@ _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)
|
||||
{
|
||||
@@ -533,17 +517,11 @@ _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) {
|
||||
@@ -982,17 +960,6 @@ _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) {
|
||||
@@ -1089,9 +1056,6 @@ _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;
|
||||
@@ -1127,75 +1091,6 @@ _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 *
|
||||
************************/
|
||||
@@ -1318,21 +1213,6 @@ _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;
|
||||
@@ -1669,10 +1549,9 @@ 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", "{arhpfmog} format item item ..." },
|
||||
{ "fmt", "set/query output formats", "{rhpfmog} 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]]" },
|
||||
@@ -1949,9 +1828,6 @@ _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;
|
||||
@@ -1984,10 +1860,9 @@ _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) {
|
||||
@@ -2090,45 +1965,6 @@ _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[])
|
||||
{
|
||||
@@ -2249,9 +2085,6 @@ _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;
|
||||
@@ -2358,10 +2191,6 @@ _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;
|
||||
@@ -2714,7 +2543,6 @@ _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) {
|
||||
@@ -2735,8 +2563,6 @@ _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);
|
||||
@@ -2791,7 +2617,6 @@ 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}
|
||||
};
|
||||
@@ -2958,14 +2783,6 @@ 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, ,"
|
||||
@@ -3078,7 +2895,6 @@ 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;
|
||||
@@ -3093,8 +2909,6 @@ 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);
|
||||
@@ -3108,7 +2922,6 @@ 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);
|
||||
@@ -3212,8 +3025,6 @@ 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;
|
||||
|
||||
@@ -3234,9 +3045,6 @@ 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);
|
||||
@@ -3249,7 +3057,6 @@ 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);
|
||||
|
||||
@@ -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 "arhpfmog"
|
||||
#define NIRT_OUTPUT_TYPE_SPECIFIERS "rhpfmog"
|
||||
|
||||
#define HELPFLAG "--print-help"
|
||||
#define PURPOSEFLAG "--print-purpose"
|
||||
@@ -274,16 +274,6 @@ 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. */
|
||||
@@ -296,7 +286,6 @@ struct nirt_output_record {
|
||||
fastf_t a;
|
||||
fastf_t e;
|
||||
struct nirt_overlap ovlp_list;
|
||||
nirt_annotation_record *annotation;
|
||||
nirt_seg *seg;
|
||||
};
|
||||
|
||||
@@ -305,7 +294,6 @@ 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;
|
||||
@@ -315,7 +303,6 @@ class nirt_fmt_state {
|
||||
std::vector<std::pair<std::string,std::string> > gap;
|
||||
|
||||
void clear() {
|
||||
annotation.clear();
|
||||
ray.clear();
|
||||
head.clear();
|
||||
part.clear();
|
||||
@@ -360,7 +347,6 @@ 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;
|
||||
@@ -382,8 +368,6 @@ 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 */
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
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 #
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
/* 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(¤t, state, NIRT_OUT);
|
||||
bu_vls_vlscat(captured, ¤t);
|
||||
bu_vls_free(¤t);
|
||||
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;
|
||||
}
|
||||
+6
-12
@@ -419,7 +419,7 @@ protected:
|
||||
ListNode* AllocListNode();
|
||||
void FreeListNode(ListNode* a_listNode);
|
||||
bool Overlap(Rect* a_rectA, Rect* a_rectB) const;
|
||||
bool Intersect(const Rect* a_rect, const Ray* a_ray) const;
|
||||
bool Intersect(Rect* a_rect, 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,15 +433,14 @@ 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(const Node *a_node, const Ray *a_ray,
|
||||
std::set<DataType> *result) const;
|
||||
size_t IsectNode(Node *a_node, Ray *a_ray, std::set<DataType> *result);
|
||||
|
||||
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(const Ray *a_ray, std::set<DataType> *result) const;
|
||||
size_t Intersects(Ray *a_ray, std::set<DataType> *result);
|
||||
|
||||
void plot(const char *fname);
|
||||
void plot2d(const char *fname);
|
||||
@@ -1552,7 +1551,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(const Rect* a_rect, const Ray* a_ray) const
|
||||
bool RTREE_QUAL::Intersect(Rect* a_rect, Ray* a_ray) const
|
||||
{
|
||||
RTreeAssert(a_rect);
|
||||
if (kNumDimensions != 3) return false;
|
||||
@@ -1686,8 +1685,7 @@ size_t RTREE_QUAL::Overlaps(RTree &other, std::set<std::pair<DataType, DataType>
|
||||
|
||||
|
||||
RTREE_TEMPLATE
|
||||
size_t RTREE_QUAL::IsectNode(const Node *a_node, const Ray *a_ray,
|
||||
std::set<DataType> *result) const
|
||||
size_t RTREE_QUAL::IsectNode(Node *a_node, Ray *a_ray, std::set<DataType> *result)
|
||||
{
|
||||
size_t ocnt = 0;
|
||||
for (int index = 0; index < a_node->m_count; ++index) {
|
||||
@@ -1705,8 +1703,7 @@ size_t RTREE_QUAL::IsectNode(const Node *a_node, const Ray *a_ray,
|
||||
|
||||
// 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(const Ray *a_ray,
|
||||
std::set<DataType> *result) const
|
||||
size_t RTREE_QUAL::Intersects(Ray *a_ray, std::set<DataType> *result)
|
||||
{
|
||||
if (!m_root) return 0;
|
||||
size_t ovlp_cnt = IsectNode(m_root, a_ray, result);
|
||||
@@ -1803,9 +1800,6 @@ void RTREE_QUAL::plot2d(const char *fname)
|
||||
}
|
||||
|
||||
|
||||
#undef BB_PLOT2D
|
||||
#undef BB_PLOT
|
||||
#undef TREE_LEAF_FACE_3D
|
||||
#undef RTREE_TEMPLATE
|
||||
#undef RTREE_QUAL
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ set(
|
||||
mime.cmake
|
||||
parallel.h
|
||||
parallel_cpp11thread.cpp
|
||||
process_private.h
|
||||
process.h
|
||||
sha1.h
|
||||
struetype.h
|
||||
vfont.h
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@
|
||||
#include "bu/str.h"
|
||||
#include "bu/datetime.h"
|
||||
#include "bu/vls.h"
|
||||
#include "./process_private.h"
|
||||
#include "./process.h"
|
||||
|
||||
#ifndef HAVE_KILL
|
||||
# include <TlHelp32.h>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* P R O C E S S _ P R I V A T E . H
|
||||
/* P R O C E S S . 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_PRIVATE_H
|
||||
#define LIBBU_PROCESS_PRIVATE_H
|
||||
#ifndef LIBBU_PROCESS_H
|
||||
#define LIBBU_PROCESS_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_PRIVATE_H */
|
||||
#endif /* LIBBU_PROCESS_H */
|
||||
|
||||
/*
|
||||
* Local Variables:
|
||||
@@ -188,7 +188,8 @@ 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 = ON_Xform::DiagonalTransformation(1.0);
|
||||
ON_Xform flatten;
|
||||
flatten.DiagonalTransformation(1.0);
|
||||
flatten[2][2] = 1.0e-13;
|
||||
require(flat->Transform(flatten), "could not construct thin shell");
|
||||
result = repair_brep_orientation(*flat);
|
||||
|
||||
@@ -62,8 +62,9 @@ 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 = RT_ANNOT_SCREEN_DPI;
|
||||
constexpr fastf_t DISPLAY_PIXELS_PER_MM = RT_ANNOT_SCREEN_PIXELS_PER_MM;
|
||||
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 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. */
|
||||
@@ -472,12 +473,10 @@ 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 (!std::isfinite(opts.text_height) || !std::isfinite(opts.line_width) ||
|
||||
opts.text_height < 0.0 || opts.line_width < 0.0 ||
|
||||
std::isinf(opts.offset) ||
|
||||
if (opts.text_height < 0.0 || opts.line_width < 0.0 ||
|
||||
(std::isfinite(opts.offset) && opts.offset < 0.0)) {
|
||||
bu_vls_printf(gedp->ged_result_str,
|
||||
"Text height and line width must be finite and non-negative; offset must be finite and non-negative when specified");
|
||||
"Text height, line width, and offset must not be negative");
|
||||
return -1;
|
||||
}
|
||||
if (!std::isfinite(opts.dpi) || opts.dpi <= 0.0) {
|
||||
@@ -2905,8 +2904,7 @@ 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);
|
||||
|
||||
@@ -150,7 +150,6 @@ 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
|
||||
@@ -428,7 +427,6 @@ endif(BRLCAD_ENABLE_SPR)
|
||||
set(LIBRT_PUBLIC_LIBS ${librt_deps} OPENNURBS::OPENNURBS)
|
||||
set(
|
||||
LIBRT_PRIVATE_LIBS
|
||||
Threads::Threads
|
||||
${OPENCL_LIBS}
|
||||
mmesh::mmesh
|
||||
REGEX::REGEX
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#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__)
|
||||
@@ -76,8 +77,46 @@
|
||||
#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)
|
||||
{
|
||||
@@ -434,9 +473,8 @@ rt_annot_validate(const struct rt_annot_internal *annot_ip,
|
||||
annot_validation_message(messages, i, "fill has no compatible outline segments");
|
||||
ret++;
|
||||
} else {
|
||||
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)
|
||||
for (j = (size_t)fsg->legacy_start;
|
||||
j < (size_t)(fsg->legacy_start + fsg->legacy_count); ++j)
|
||||
if (!ant->segments[j] ||
|
||||
*(uint32_t *)ant->segments[j] != CURVE_LSEG_MAGIC) {
|
||||
annot_validation_message(messages, i,
|
||||
@@ -503,25 +541,59 @@ 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) RT_CK_DB_INTERNAL(ip);
|
||||
if (rtip) RT_CK_RTI(rtip);
|
||||
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_annot_validate(
|
||||
(const struct rt_annot_internal *)ip->idb_ptr, NULL))
|
||||
annot_ip = (const struct rt_annot_internal *)ip->idb_ptr;
|
||||
if (rt_annot_validate(annot_ip, 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;
|
||||
return 0;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
C_DECL void
|
||||
rt_annot_print(const struct soltab *stp)
|
||||
{
|
||||
if (stp) RT_CK_SOLTAB(stp);
|
||||
if (!stp)
|
||||
return;
|
||||
RT_CK_SOLTAB(stp);
|
||||
if (stp->st_specific)
|
||||
rt_bot_print(stp);
|
||||
}
|
||||
|
||||
|
||||
@@ -543,25 +615,18 @@ rt_annot_shot(struct soltab *stp, struct xray *rp, struct application *ap, struc
|
||||
RT_CK_RAY(rp);
|
||||
RT_CK_APPLICATION(ap);
|
||||
|
||||
/* Annotation coverage is evaluated separately from solid partitions. */
|
||||
|
||||
return 0; /* MISS */
|
||||
return stp->st_specific ? rt_bot_shot(stp, rp, ap, seghead) : 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Vectorized rt_annot_shot(): annotations do not create solid partitions, so
|
||||
* every ray in the batch misses.
|
||||
* Vectorized annotation shooting uses the scalar wrapper so view-space
|
||||
* annotations without prepared geometry continue to miss safely.
|
||||
*/
|
||||
C_DECL void
|
||||
rt_annot_vshot(struct soltab **stp, struct xray **UNUSED(rp), struct seg *segp, int n, struct application *ap)
|
||||
rt_annot_vshot(struct soltab **stp, struct xray **rp, struct seg *segp, int n, struct application *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 */
|
||||
}
|
||||
rt_vshot_via_shot(rt_annot_shot, stp, rp, segp, n, ap);
|
||||
}
|
||||
|
||||
|
||||
@@ -578,6 +643,10 @@ 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);
|
||||
}
|
||||
|
||||
@@ -594,6 +663,10 @@ 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);
|
||||
@@ -608,13 +681,19 @@ 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) RT_CK_SOLTAB(stp);
|
||||
if (!stp)
|
||||
return;
|
||||
RT_CK_SOLTAB(stp);
|
||||
if (stp->st_specific)
|
||||
rt_bot_free(stp);
|
||||
}
|
||||
|
||||
|
||||
@@ -1526,29 +1605,6 @@ 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)
|
||||
{
|
||||
@@ -1635,6 +1691,489 @@ 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))
|
||||
{
|
||||
@@ -3527,11 +4066,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);
|
||||
if (vert_no < 0 || (size_t)vert_no >= annot_ip->vert_count) {
|
||||
new_vert = annot_ip->verts[vert_no];
|
||||
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
+202
-468
@@ -45,28 +45,24 @@
|
||||
* intersecting closed surface), and total_chord is the sum of solid
|
||||
* segment lengths.
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#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 <stdexcept>
|
||||
#include <random>
|
||||
#include <string.h>
|
||||
#include <thread>
|
||||
#include <time.h>
|
||||
#include <vector>
|
||||
|
||||
#include "vmath.h"
|
||||
#include "bu/log.h"
|
||||
@@ -104,15 +100,8 @@
|
||||
#define RT_CROFTON_STABILITY_MIN_WINDOWS 2u
|
||||
|
||||
/** Fixed seed for reproducible estimates in tests and cross-platform runs. */
|
||||
#define RT_CROFTON_RNG_SEED UINT64_C(0x9e3779b97f4a7c15)
|
||||
#define RT_CROFTON_RNG_SEED 0x9e3779b97f4a7c15ULL
|
||||
|
||||
/** 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 */
|
||||
@@ -121,7 +110,6 @@ static constexpr double CROFTON_RANDOM_SCALE =
|
||||
struct crofton_ray {
|
||||
point_t r_pt;
|
||||
vect_t r_dir;
|
||||
size_t id;
|
||||
};
|
||||
|
||||
struct crofton_shared {
|
||||
@@ -129,19 +117,14 @@ 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;
|
||||
@@ -152,10 +135,6 @@ 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -164,88 +143,46 @@ 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 *worker =
|
||||
(struct crofton_worker_data *)ap->a_uptr;
|
||||
struct crofton_worker_data *wd = (struct crofton_worker_data *)ap->a_uptr;
|
||||
|
||||
size_t crossings = 0;
|
||||
double chord = 0.0;
|
||||
double chord = 0.0;
|
||||
|
||||
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++;
|
||||
}
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
worker->local_crossings += crossings;
|
||||
worker->local_chord += chord;
|
||||
worker->local_rays++;
|
||||
wd->local_crossings += crossings;
|
||||
wd->local_chord += chord;
|
||||
wd->local_rays += 1;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
crofton_miss(struct application *ap)
|
||||
{
|
||||
@@ -256,273 +193,159 @@ 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_random01(std::mt19937_64 &rng)
|
||||
crofton_rand01(std::mt19937_64 &rng)
|
||||
{
|
||||
return static_cast<double>(rng() >> CROFTON_RANDOM_SHIFT) *
|
||||
CROFTON_RANDOM_SCALE;
|
||||
return std::generate_canonical<double, 53>(rng);
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
random_point_on_sphere(double radius, const point_t center, double u,
|
||||
double v, point_t point)
|
||||
random_point_on_sphere(double radius, const point_t center, point_t out, std::mt19937_64 &rng)
|
||||
{
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Generate @p nrays chord rays from 2*nrays random points on the
|
||||
* bounding sphere. Each ray connects one randomly chosen point to
|
||||
* another.
|
||||
*/
|
||||
static void
|
||||
generate_rays(struct crofton_ray *rays, size_t ray_count, size_t first_id,
|
||||
double radius, const point_t center, std::mt19937_64 &rng)
|
||||
generate_rays(struct crofton_ray *rays, size_t nrays,
|
||||
double radius, const point_t center, std::mt19937_64 &rng)
|
||||
{
|
||||
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;
|
||||
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 < 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 */
|
||||
/* One iteration: fire nrays, accumulate into shared */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
static void
|
||||
do_one_iteration(CroftonWorkerPool &pool, size_t ray_count, double radius,
|
||||
const point_t center, struct crofton_shared *shared,
|
||||
std::mt19937_64 &rng)
|
||||
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)
|
||||
{
|
||||
struct crofton_ray *rays = (struct crofton_ray *)bu_calloc(
|
||||
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);
|
||||
nrays, sizeof(struct crofton_ray), "crofton rays");
|
||||
|
||||
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;
|
||||
generate_rays(rays, nrays, radius, center, rng);
|
||||
|
||||
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;
|
||||
}
|
||||
size_t ncpus = bu_avail_cpus();
|
||||
if (ncpus < 1) ncpus = 1;
|
||||
if (ncpus > MAX_PSW) ncpus = MAX_PSW;
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
bu_free(rays, "Crofton rays");
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Public API: rt_crofton_shoot */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -552,8 +375,7 @@ do_one_iteration(CroftonWorkerPool &pool, size_t ray_count, double radius,
|
||||
* sampling (>= 0) on success; -1 on bad arguments.
|
||||
*/
|
||||
static int
|
||||
crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
point_t **out_points,
|
||||
crofton_shoot_impl(point_t **out_points,
|
||||
size_t *out_point_count,
|
||||
double *out_surf_area,
|
||||
double *out_volume,
|
||||
@@ -562,22 +384,13 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
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_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 (!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 (out_points) {
|
||||
*out_points = NULL;
|
||||
*out_point_count = 0;
|
||||
@@ -660,14 +473,10 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
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(
|
||||
ncpus, sizeof(struct resource), "crofton resources");
|
||||
for (size_t i = 0; i < ncpus; i++)
|
||||
MAX_PSW, sizeof(struct resource), "crofton resources");
|
||||
for (int i = 0; i < MAX_PSW; i++)
|
||||
rt_init_resource(&resources[i], i, rtip);
|
||||
|
||||
/* ---- Set up application template ---- */
|
||||
@@ -685,32 +494,10 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
/* ---- Shared accumulator ---- */
|
||||
struct crofton_shared shared;
|
||||
memset(&shared, 0, sizeof(shared));
|
||||
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;
|
||||
}
|
||||
shared.collect_points = out_points != NULL || out_aabb_min != NULL || out_obb != NULL;
|
||||
const bool stabilize_surface = out_surf_area != NULL || shared.collect_points;
|
||||
|
||||
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;
|
||||
@@ -734,7 +521,7 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
curr_rays = batch;
|
||||
}
|
||||
|
||||
do_one_iteration(*pool, curr_rays, R, center, &shared, rng);
|
||||
do_one_iteration(&ap, resources, curr_rays, R, center, &shared, rng);
|
||||
iteration++;
|
||||
|
||||
if (shared.total_rays == 0) break;
|
||||
@@ -775,12 +562,9 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
} 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 */
|
||||
@@ -797,7 +581,7 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
if (fire > remaining) fire = remaining;
|
||||
}
|
||||
|
||||
do_one_iteration(*pool, fire, R, center, &shared, rng);
|
||||
do_one_iteration(&ap, resources, fire, R, center, &shared, rng);
|
||||
total_fired += fire;
|
||||
|
||||
if (shared.total_rays == 0) break;
|
||||
@@ -810,39 +594,27 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
/ (double)shared.total_rays;
|
||||
|
||||
/* Stability check */
|
||||
if ((stability_mm > 0.0 || use_relative_stability) &&
|
||||
if (stability_mm > 0.0 &&
|
||||
shared.total_rays >= RT_CROFTON_STABILITY_MIN_RAYS &&
|
||||
shared.total_crossings >= RT_CROFTON_STABILITY_MIN_CROSSINGS) {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
prev_r_sa = r_sa;
|
||||
prev_r_v = r_v;
|
||||
}
|
||||
|
||||
/* Time-budget check after firing */
|
||||
@@ -852,16 +624,8 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
}
|
||||
}
|
||||
|
||||
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_surf_area) *out_surf_area = curr_est_sa;
|
||||
if (out_volume) *out_volume = curr_est_v;
|
||||
if (out_aabb_min) {
|
||||
for (size_t i = 0; i < shared.point_count; ++i)
|
||||
VMINMAX(*out_aabb_min, *out_aabb_max, shared.points[i]);
|
||||
@@ -877,19 +641,11 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
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)
|
||||
@@ -901,7 +657,7 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
*
|
||||
* 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 (size_t i = 0; i < ncpus; i++) {
|
||||
for (int i = 0; i < MAX_PSW; i++) {
|
||||
if (resources[i].re_magic == RESOURCE_MAGIC) {
|
||||
rt_clean_resource_basic(rtip, &resources[i]);
|
||||
BU_PTBL_SET(&rtip->rti_resources, i, NULL);
|
||||
@@ -925,31 +681,9 @@ 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(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;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1087,7 +821,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;
|
||||
|
||||
@@ -1543,7 +1543,8 @@ permute_cell(point_t A,
|
||||
struct dsp_specific *dsp,
|
||||
struct dsp_rpp *dsp_rpp)
|
||||
{
|
||||
point_t tmp;
|
||||
int x, y;
|
||||
|
||||
|
||||
#ifdef FULL_DSP_DEBUGGING
|
||||
if (RT_G_DEBUG & RT_DEBUG_HF) {
|
||||
@@ -1557,8 +1558,10 @@ 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];
|
||||
|
||||
@@ -1567,8 +1570,28 @@ permute_cell(point_t A,
|
||||
dsp_rpp->dsp_min[X],
|
||||
dsp_rpp->dsp_min[Y]);
|
||||
|
||||
/* The rpp spans one grid cell; use its indices to select the
|
||||
* lower-curvature diagonal.
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
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);
|
||||
@@ -1576,16 +1599,15 @@ 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;
|
||||
|
||||
}
|
||||
}
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
/* prefer the B-C cut */
|
||||
VMOVE(tmp, A);
|
||||
VMOVE(A, B);
|
||||
VMOVE(B, D);
|
||||
@@ -1593,7 +1615,35 @@ 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);
|
||||
|
||||
@@ -43,18 +43,6 @@ 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)
|
||||
{
|
||||
@@ -391,102 +379,6 @@ 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)
|
||||
|
||||
@@ -19,14 +19,13 @@
|
||||
#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)
|
||||
@@ -47,46 +46,79 @@ lookup(struct db_i *dbip, const char *name)
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
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_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;
|
||||
struct annot_ray_result {
|
||||
fastf_t in_dist;
|
||||
fastf_t out_dist;
|
||||
fastf_t color[3];
|
||||
int color_valid;
|
||||
int partitions;
|
||||
};
|
||||
|
||||
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));
|
||||
|
||||
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++;
|
||||
}
|
||||
if (rt_annot_validate(&annot, NULL) || mk_annot(wdbp, name, &annot) < 0)
|
||||
bu_exit(1, "unable to write ordered-layer test annotation\n");
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
||||
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");
|
||||
}
|
||||
rt_i_destroy(rtip);
|
||||
}
|
||||
|
||||
|
||||
@@ -225,78 +257,48 @@ 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 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);
|
||||
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");
|
||||
}
|
||||
|
||||
/* ANP2 is deliberately appended after the main-branch ANT2 body. A
|
||||
@@ -396,7 +398,12 @@ 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->flags & RT_ANNOT_MODEL_SPACE) || !loaded->styles ||
|
||||
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) ||
|
||||
loaded->styles[0].role != RT_ANNOT_ROLE_CENTERMARK ||
|
||||
loaded->styles[0].line_pattern != RT_ANNOT_LINE_DASHED ||
|
||||
!loaded->styles[0].font ||
|
||||
@@ -415,7 +422,6 @@ 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 ||
|
||||
@@ -467,222 +473,36 @@ 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(®ion, model, &intern, &ttol, &tol) || !region)
|
||||
bu_exit(1, "unable to tessellate model-space annotation\n");
|
||||
nmg_km(model);
|
||||
}
|
||||
}
|
||||
rt_db_free_internal(&intern);
|
||||
|
||||
{
|
||||
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 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
|
||||
};
|
||||
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
|
||||
const fastf_t region_color[3] = {
|
||||
90.0 / 255.0, 80.0 / 255.0, 70.0 / 255.0
|
||||
};
|
||||
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);
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -414,20 +414,13 @@ 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;
|
||||
enum dsp_ray_check_mode ray_checks;
|
||||
int ray_checks;
|
||||
/* stom diagonal (identity: dx=dy=dz=1): */
|
||||
double dx, dy, dz;
|
||||
/* analytic SA/vol (-1 if not available): */
|
||||
@@ -557,7 +550,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 == DSP_RAY_CHECK_NONE)
|
||||
if (!tc->ray_checks)
|
||||
return 0;
|
||||
|
||||
int failures = 0;
|
||||
@@ -569,14 +562,16 @@ 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 == 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;
|
||||
if (tc->ray_checks == 1) {
|
||||
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,
|
||||
@@ -603,22 +598,12 @@ 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 == DSP_RAY_CHECK_SMOOTH) {
|
||||
} else if (tc->ray_checks == 2) {
|
||||
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;
|
||||
@@ -797,7 +782,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"flat 5x5 h=100 (4x4 cells, identity stom)",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_FLAT, 1.0, 1.0, 1.0, asa, avol
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 1, 1.0, 1.0, 1.0, asa, avol
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -813,7 +798,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"flat 10x10 h=200 (9x9 cells)",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, asa, avol
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, asa, avol
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -829,7 +814,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, DSP_RAY_CHECK_FLAT, 5.0, 5.0, 2.0, asa, avol
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 1, 5.0, 5.0, 2.0, asa, avol
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -844,7 +829,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"ramp 9x9 (h=100+20x+10y)",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -859,7 +844,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"asymmetric 5x5 explicit ULlr cut",
|
||||
GW, GH, buf, DSP_CUT_DIR_ULlr, 0, DSP_RAY_CHECK_BC_CUT, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_ULlr, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -874,7 +859,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"asymmetric 5x5 adaptive cut",
|
||||
GW, GH, buf, DSP_CUT_DIR_ADAPT, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_ADAPT, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -889,7 +874,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"smooth=1 ramp normal check",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 1, DSP_RAY_CHECK_SMOOTH, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 1, 2, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -904,7 +889,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"smooth=2 ramp normal check",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 2, DSP_RAY_CHECK_SMOOTH, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 2, 2, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -923,7 +908,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"sinusoidal 16x16",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -943,7 +928,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"complex 33x33 wave",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
bu_free(buf, "33x33 buf");
|
||||
@@ -986,7 +971,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"larger 129x129 mixed ramp/wave terrain",
|
||||
grid_width, grid_height, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
grid_width, grid_height, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
bu_free(sin_x, "129x129 sin x");
|
||||
|
||||
@@ -889,3 +889,4 @@ done:
|
||||
// c-file-style: "stroustrup"
|
||||
// End:
|
||||
// ex: shiftwidth=4 tabstop=8
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ add_target_deps(rtscale dm_plugins)
|
||||
|
||||
brlcad_addexec(rt "${LIBRTUIF_SOURCES};view.c" "${OPENCL_LIBS};${RT_STD_LIBS}" FOLDER Rt)
|
||||
target_include_directories(rt BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
set_property(TARGET rt APPEND PROPERTY COMPILE_DEFINITIONS "RT_ANNOT_OVERLAY")
|
||||
add_target_deps(rt dm_plugins dm-txt)
|
||||
|
||||
brlcad_addexec(rtarea "${LIBRTUIF_SOURCES};viewarea.c" "${RT_STD_LIBS}" FOLDER Rt)
|
||||
|
||||
+10
-211
@@ -80,15 +80,6 @@ extern void worker(int cpu, void *arg);
|
||||
|
||||
extern struct icv_image *bif;
|
||||
unsigned char *pixmap = NULL; /**< Pixel Map for rerendering of black pixels */
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
static struct rt_annot_scene *annotation_scene = NULL;
|
||||
static struct rt_annot_view annotation_view = {MAT_INIT_IDN, 0, 0, 0.0};
|
||||
static struct db_i *annotation_dbip = DBI_NULL;
|
||||
static struct bu_ptbl annotation_paths = BU_PTBL_INIT_ZERO;
|
||||
static struct bg_tess_tol annotation_ttol = BG_TESS_TOL_INIT_ZERO;
|
||||
static struct bn_tol annotation_tol = BN_TOL_INIT_ZERO;
|
||||
static int annotation_cache_valid = 0;
|
||||
#endif
|
||||
|
||||
enum view_command_flag {
|
||||
VIEW_COMMAND_EYE_SET = 1u << 0,
|
||||
@@ -329,9 +320,6 @@ int cm_end(const int UNUSED(argc), const char **UNUSED(argv))
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
rtuif_prepare_annotations(rtip);
|
||||
#endif
|
||||
do_view_finalize(azimuth, elevation);
|
||||
|
||||
if (do_frame(curframe) < 0)
|
||||
@@ -484,9 +472,6 @@ int cm_multiview(const int UNUSED(argc), const char **UNUSED(argv))
|
||||
if (rtip && BU_LIST_IS_EMPTY(&rtip->HeadRegion) && !def_tree(rtip, NULL)) {
|
||||
return -1;
|
||||
}
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
rtuif_prepare_annotations(rtip);
|
||||
#endif
|
||||
for (i = 0; i < (sizeof(a)/sizeof(a[0])); i++) {
|
||||
do_ae((double)a[i], (double)e[i]);
|
||||
(void)do_frame(curframe++);
|
||||
@@ -507,9 +492,6 @@ int cm_anim(const int argc, const char **argv)
|
||||
bu_log("cm_anim: %s %s failed\n", argv[1], argv[2]);
|
||||
return -1; /* BAD */
|
||||
}
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
rtuif_clear_annotations();
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -905,19 +887,11 @@ validate_raytrace(struct rt_i *rtip)
|
||||
bu_log("ERROR: No raytracing instance.\n");
|
||||
return 1;
|
||||
}
|
||||
if (rtip->stats.nsolids <= 0
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
&& (!annotation_scene || !rt_annot_scene_count(annotation_scene))
|
||||
#endif
|
||||
) {
|
||||
if (rtip->stats.nsolids <= 0) {
|
||||
bu_log("ERROR: No primitives remaining.\n");
|
||||
return 2;
|
||||
}
|
||||
if (rtip->stats.nregions <= 0
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
&& (!annotation_scene || !rt_annot_scene_count(annotation_scene))
|
||||
#endif
|
||||
) {
|
||||
if (rtip->stats.nregions <= 0) {
|
||||
bu_log("ERROR: No regions remaining.\n");
|
||||
return 3;
|
||||
}
|
||||
@@ -926,152 +900,6 @@ validate_raytrace(struct rt_i *rtip)
|
||||
}
|
||||
|
||||
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
static void
|
||||
annotation_cache_clear(void)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
rt_annot_scene_destroy(annotation_scene);
|
||||
annotation_scene = NULL;
|
||||
if (BU_PTBL_TEST(&annotation_paths)) {
|
||||
for (i = 0; i < BU_PTBL_LEN(&annotation_paths); ++i)
|
||||
bu_free((char *)BU_PTBL_GET(&annotation_paths, i),
|
||||
"cached annotation path");
|
||||
bu_ptbl_free(&annotation_paths);
|
||||
}
|
||||
annotation_dbip = DBI_NULL;
|
||||
annotation_cache_valid = 0;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
annotation_scalar_equal(double a, double b)
|
||||
{
|
||||
return a <= b && a >= b;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
annotation_tolerances_match(const struct rt_i *rtip)
|
||||
{
|
||||
const struct bg_tess_tol *ttol = &rtip->rti_ttol;
|
||||
const struct bn_tol *tol = &rtip->rti_tol;
|
||||
|
||||
return annotation_ttol.magic == ttol->magic &&
|
||||
annotation_scalar_equal(annotation_ttol.abs, ttol->abs) &&
|
||||
annotation_scalar_equal(annotation_ttol.rel, ttol->rel) &&
|
||||
annotation_scalar_equal(annotation_ttol.norm, ttol->norm) &&
|
||||
annotation_scalar_equal(annotation_ttol.absmax, ttol->absmax) &&
|
||||
annotation_scalar_equal(annotation_ttol.absmin, ttol->absmin) &&
|
||||
annotation_scalar_equal(annotation_ttol.relmax, ttol->relmax) &&
|
||||
annotation_scalar_equal(annotation_ttol.relmin, ttol->relmin) &&
|
||||
annotation_scalar_equal(annotation_ttol.rel_lmax, ttol->rel_lmax) &&
|
||||
annotation_scalar_equal(annotation_ttol.rel_lmin, ttol->rel_lmin) &&
|
||||
annotation_tol.magic == tol->magic &&
|
||||
annotation_scalar_equal(annotation_tol.dist, tol->dist) &&
|
||||
annotation_scalar_equal(annotation_tol.dist_sq, tol->dist_sq) &&
|
||||
annotation_scalar_equal(annotation_tol.perp, tol->perp) &&
|
||||
annotation_scalar_equal(annotation_tol.para, tol->para);
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
annotation_cache_matches(const struct rt_i *rtip, int path_count,
|
||||
const char * const *paths)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (!annotation_cache_valid || annotation_dbip != rtip->rti_dbip ||
|
||||
path_count < 0 ||
|
||||
(size_t)path_count != BU_PTBL_LEN(&annotation_paths) ||
|
||||
!annotation_tolerances_match(rtip))
|
||||
return 0;
|
||||
for (i = 0; i < path_count; ++i)
|
||||
if (!BU_STR_EQUAL((const char *)BU_PTBL_GET(&annotation_paths, i),
|
||||
paths[i]))
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
annotation_cache_store(const struct rt_i *rtip, int path_count,
|
||||
const char * const *paths)
|
||||
{
|
||||
int i;
|
||||
|
||||
bu_ptbl_init(&annotation_paths, (size_t)path_count,
|
||||
"cached annotation paths");
|
||||
for (i = 0; i < path_count; ++i)
|
||||
bu_ptbl_ins(&annotation_paths, (long *)bu_strdup(paths[i]));
|
||||
annotation_dbip = rtip->rti_dbip;
|
||||
annotation_ttol = rtip->rti_ttol;
|
||||
annotation_tol = rtip->rti_tol;
|
||||
annotation_cache_valid = 1;
|
||||
}
|
||||
|
||||
|
||||
const struct rt_annot_scene *
|
||||
rtuif_annotations(void)
|
||||
{
|
||||
return annotation_scene;
|
||||
}
|
||||
|
||||
|
||||
const struct rt_annot_view *
|
||||
rtuif_annotation_view(void)
|
||||
{
|
||||
return &annotation_view;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
rtuif_clear_annotations(void)
|
||||
{
|
||||
annotation_cache_clear();
|
||||
MAT_IDN(annotation_view.model2view);
|
||||
annotation_view.width = 0;
|
||||
annotation_view.height = 0;
|
||||
annotation_view.perspective = 0.0;
|
||||
}
|
||||
|
||||
|
||||
size_t
|
||||
rtuif_prepare_annotations(struct rt_i *rtip)
|
||||
{
|
||||
const char **treev = NULL;
|
||||
int treec = 0;
|
||||
size_t count;
|
||||
|
||||
if (!rtip) {
|
||||
rtuif_clear_annotations();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (cmd_objs && BU_PTBL_LEN(cmd_objs)) {
|
||||
treec = (int)BU_PTBL_LEN(cmd_objs);
|
||||
treev = (const char **)cmd_objs->buffer;
|
||||
} else if (!rtuif_tree_list(rtip, &treec, &treev, NULL)) {
|
||||
rtuif_clear_annotations();
|
||||
return 0;
|
||||
}
|
||||
if (!annotation_cache_matches(rtip, treec, treev)) {
|
||||
annotation_cache_clear();
|
||||
annotation_scene = rt_annot_scene_create(rtip->rti_dbip, treec, treev,
|
||||
&rtip->rti_ttol, &rtip->rti_tol);
|
||||
annotation_cache_store(rtip, treec, treev);
|
||||
}
|
||||
count = rt_annot_scene_count(annotation_scene);
|
||||
if (opencl_mode && count) {
|
||||
bu_log("rt: annotations require primary-sample compositing; using the CPU renderer\n");
|
||||
opencl_mode = 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
/**
|
||||
* Do all the actual work to run a frame.
|
||||
*
|
||||
@@ -1096,10 +924,6 @@ do_frame(int framenumber)
|
||||
/* Compute model RPP, etc. */
|
||||
do_prep(rtip);
|
||||
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
rtuif_prepare_annotations(rtip);
|
||||
#endif
|
||||
|
||||
if (rt_verbosity & VERBOSE_VIEWDETAIL)
|
||||
bu_log("Tree: %zu solids in %zu regions\n", rtip->stats.nsolids, rtip->stats.nregions);
|
||||
|
||||
@@ -1134,13 +958,6 @@ do_frame(int framenumber)
|
||||
bu_vls_free(&msg);
|
||||
}
|
||||
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
MAT_COPY(annotation_view.model2view, model2view);
|
||||
annotation_view.width = width;
|
||||
annotation_view.height = height;
|
||||
annotation_view.perspective = rt_perspective;
|
||||
#endif
|
||||
|
||||
/* az/el 0, 0 is when screen +Z is model +X */
|
||||
VSET(work, 0, 0, 1);
|
||||
MAT3X3VEC(temp, view2model, work);
|
||||
@@ -1589,32 +1406,22 @@ do_ae_internal(double azim, double elev, int center_eye)
|
||||
{
|
||||
struct rt_i *rtip = APP.a_rt_i;
|
||||
point_t view_min, view_max;
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
point_t annotation_min, annotation_max;
|
||||
int have_annotation_bounds;
|
||||
#endif
|
||||
|
||||
if (rtip == NULL)
|
||||
return;
|
||||
|
||||
if (rtip->mdl_min[X] >= INFINITY || rtip->mdl_max[X] <= -INFINITY) {
|
||||
if (rtip->mdl_max[X] >= INFINITY) {
|
||||
bu_log("do_ae: infinite model bounds? setting a unit minimum\n");
|
||||
VSETALL(rtip->mdl_min, -1);
|
||||
}
|
||||
if (rtip->mdl_max[X] <= -INFINITY) {
|
||||
/* Model not yet loaded (librt initial state: mdl_min=+INF, mdl_max=-INF).
|
||||
* Reset both bounds to a unit box so that autoviewsize() does not
|
||||
* propagate +INFINITY through mdl_min into cell_width (which causes a
|
||||
* bu_bomb("bad cell size") crash when work is dispatched). */
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
have_annotation_bounds = rt_annot_scene_bounds(annotation_scene,
|
||||
annotation_min, annotation_max);
|
||||
if (have_annotation_bounds) {
|
||||
VMOVE(rtip->mdl_min, annotation_min);
|
||||
VMOVE(rtip->mdl_max, annotation_max);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
bu_log("do_ae: infinite model bounds? setting unit bounds\n");
|
||||
VSETALL(rtip->mdl_min, -1);
|
||||
VSETALL(rtip->mdl_max, 1);
|
||||
}
|
||||
bu_log("do_ae: infinite model bounds? setting unit bounds\n");
|
||||
VSETALL(rtip->mdl_min, -1);
|
||||
VSETALL(rtip->mdl_max, 1);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1636,14 +1443,6 @@ do_ae_internal(double azim, double elev, int center_eye)
|
||||
*/
|
||||
VMOVE(view_min, rtip->mdl_min);
|
||||
VMOVE(view_max, rtip->mdl_max);
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
have_annotation_bounds = rt_annot_scene_bounds(annotation_scene,
|
||||
annotation_min, annotation_max);
|
||||
if (have_annotation_bounds) {
|
||||
VMINMAX(view_min, view_max, annotation_min);
|
||||
VMINMAX(view_min, view_max, annotation_max);
|
||||
}
|
||||
#endif
|
||||
if (autoview_argc > 0) {
|
||||
point_t sub_min, sub_max;
|
||||
if (rt_obj_bounds(NULL, rtip->rti_dbip, autoview_argc,
|
||||
|
||||
@@ -151,9 +151,6 @@ extern vect_t dx_model; /* view delta-X as model-space vect (width of pixel as
|
||||
extern vect_t dx_unit; /* unit-len dir vector of pixel side-to-side */
|
||||
extern vect_t dy_model; /* view delta-Y as model-space vect (height of pixel as vector) */
|
||||
extern vect_t dy_unit; /* unit-len dir vector of pixel top-to-bottom */
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
# define RTUIF_ANNOTATION_ONLY 2
|
||||
#endif
|
||||
/** 'jitter' variable values **/
|
||||
#define JITTER_CELL 0x1 /* jitter position of ray in each cell */
|
||||
#define JITTER_FRAME 0x2 /* jitter position of entire frame */
|
||||
@@ -200,12 +197,6 @@ extern void do_ae(double azim, double elev);
|
||||
extern void do_view_finalize(double azim, double elev);
|
||||
extern int old_way(FILE *fp);
|
||||
extern int do_frame(int framenumber);
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
extern size_t rtuif_prepare_annotations(struct rt_i *rtip);
|
||||
extern const struct rt_annot_scene *rtuif_annotations(void);
|
||||
extern const struct rt_annot_view *rtuif_annotation_view(void);
|
||||
extern void rtuif_clear_annotations(void);
|
||||
#endif
|
||||
|
||||
#ifdef USE_OPENCL
|
||||
enum {
|
||||
|
||||
@@ -620,9 +620,6 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
rtuif_prepare_annotations(APP.a_rt_i);
|
||||
#endif
|
||||
do_view_finalize(azimuth, elevation);
|
||||
|
||||
if (need_fb != 0 && !fbp) {
|
||||
@@ -742,9 +739,6 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
|
||||
rt_cleanup:
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
rtuif_clear_annotations();
|
||||
#endif
|
||||
/* Clean up objv memory, if necessary */
|
||||
if (objs_free_argv) {
|
||||
for (i = 0; i < objc; i++) {
|
||||
|
||||
@@ -267,14 +267,6 @@ view_pixel(struct application *ap)
|
||||
fp->ff_frame = -1; /* Don't cache misses */
|
||||
return;
|
||||
}
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
if (ap->a_flag == RTUIF_ANNOTATION_ONLY) {
|
||||
/* Annotation coverage has no region or reusable model hit. */
|
||||
fp->ff_dist = -INFINITY;
|
||||
fp->ff_regp = NULL;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
/* XXX a_dist is negative and misleading when eye is in air */
|
||||
fp->ff_dist = (float)ap->a_dist;
|
||||
VJOIN1(fp->ff_hitpt, ap->a_ray.r_pt,
|
||||
@@ -1238,7 +1230,6 @@ viewit(struct application *ap, struct partition *PartHeadp, struct seg *UNUSED(s
|
||||
}
|
||||
|
||||
hitp = pp->pt_inhit;
|
||||
ap->a_dist = hitp->hit_dist;
|
||||
RT_HIT_NORMAL(normal, hitp, pp->pt_inseg->seg_stp, &(ap->a_ray), pp->pt_inflip);
|
||||
|
||||
/*
|
||||
|
||||
+2
-70
@@ -151,46 +151,6 @@ jitter_start_pnt(vect_t point, struct application *a, int samplenum, int pat_num
|
||||
}
|
||||
|
||||
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
static int
|
||||
annotation_overlay_active(void)
|
||||
{
|
||||
const struct rt_annot_scene *scene = rtuif_annotations();
|
||||
|
||||
return scene && rt_annot_scene_count(scene);
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
annotate_primary_sample(struct application *ap, const point_t viewplane_point)
|
||||
{
|
||||
point_t view_point;
|
||||
struct rt_annot_hit hit = {0};
|
||||
fastf_t sample_x;
|
||||
fastf_t sample_y;
|
||||
fastf_t scene_distance;
|
||||
const struct rt_annot_scene *scene = rtuif_annotations();
|
||||
const struct rt_annot_view *view = rtuif_annotation_view();
|
||||
|
||||
if (!scene || !rt_annot_scene_count(scene))
|
||||
return;
|
||||
MAT4X3PNT(view_point, model2view, viewplane_point);
|
||||
sample_x = (view_point[X] + 1.0) * (fastf_t)width * 0.5;
|
||||
sample_y = view_point[Y] * (fastf_t)width * 0.5 +
|
||||
(fastf_t)height * 0.5;
|
||||
scene_distance = ap->a_user ? ap->a_dist : INFINITY;
|
||||
if (rt_annot_scene_composite(scene, view, &ap->a_ray,
|
||||
sample_x, sample_y, scene_distance, ap->a_color, &hit)) {
|
||||
if (!ap->a_user) {
|
||||
ap->a_user = 1;
|
||||
ap->a_flag = RTUIF_ANNOTATION_ONLY;
|
||||
ap->a_dist = hit.screen_space ? INFINITY : hit.distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
void
|
||||
do_pixel(int cpu, int pat_num, int pixelnum)
|
||||
{
|
||||
@@ -279,11 +239,7 @@ do_pixel(int cpu, int pat_num, int pixelnum)
|
||||
if (fullfloat_mode) {
|
||||
register struct floatpixel *fp;
|
||||
fp = &curr_float_frame[a.a_y*width + a.a_x];
|
||||
if (fp->ff_frame >= 0
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
&& !annotation_overlay_active()
|
||||
#endif
|
||||
) {
|
||||
if (fp->ff_frame >= 0) {
|
||||
return; /* pixel was reprojected */
|
||||
}
|
||||
}
|
||||
@@ -291,11 +247,7 @@ do_pixel(int cpu, int pat_num, int pixelnum)
|
||||
/* Check the pixel map to determine if this image should be
|
||||
* rendered or not.
|
||||
*/
|
||||
if (pixmap
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
&& !annotation_overlay_active()
|
||||
#endif
|
||||
) {
|
||||
if (pixmap) {
|
||||
a.a_user= 1; /* Force Shot Hit */
|
||||
|
||||
if (pixmap[pindex + RED] + pixmap[pindex + GRN] + pixmap[pindex + BLU]) {
|
||||
@@ -376,12 +328,7 @@ do_pixel(int cpu, int pat_num, int pixelnum)
|
||||
|
||||
a.a_level = 0; /* recursion level */
|
||||
a.a_purpose = "main ray";
|
||||
a.a_flag = 0;
|
||||
a.a_dist = INFINITY;
|
||||
(void)rt_shootray(&a);
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
annotate_primary_sample(&a, point);
|
||||
#endif
|
||||
|
||||
if (stereo) {
|
||||
fastf_t right, left;
|
||||
@@ -403,12 +350,7 @@ do_pixel(int cpu, int pat_num, int pixelnum)
|
||||
}
|
||||
a.a_level = 0; /* recursion level */
|
||||
a.a_purpose = "left eye ray";
|
||||
a.a_flag = 0;
|
||||
a.a_dist = INFINITY;
|
||||
(void)rt_shootray(&a);
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
annotate_primary_sample(&a, point);
|
||||
#endif
|
||||
|
||||
left = CRT_BLEND(a.a_color);
|
||||
VSET(a.a_color, left, 0, right);
|
||||
@@ -471,12 +413,7 @@ do_pixel(int cpu, int pat_num, int pixelnum)
|
||||
|
||||
a.a_level = 0; /* recursion level */
|
||||
a.a_purpose = "main ray";
|
||||
a.a_flag = 0;
|
||||
a.a_dist = INFINITY;
|
||||
(void)rt_shootray(&a);
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
annotate_primary_sample(&a, point);
|
||||
#endif
|
||||
|
||||
if (stereo) {
|
||||
fastf_t right, left;
|
||||
@@ -498,12 +435,7 @@ do_pixel(int cpu, int pat_num, int pixelnum)
|
||||
}
|
||||
a.a_level = 0; /* recursion level */
|
||||
a.a_purpose = "left eye ray";
|
||||
a.a_flag = 0;
|
||||
a.a_dist = INFINITY;
|
||||
(void)rt_shootray(&a);
|
||||
#ifdef RT_ANNOT_OVERLAY
|
||||
annotate_primary_sample(&a, point);
|
||||
#endif
|
||||
|
||||
left = CRT_BLEND(a.a_color);
|
||||
VSET(a.a_color, left, 0, right);
|
||||
|
||||
Reference in New Issue
Block a user