Compare commits

..
2 Commits
Author SHA1 Message Date
Clifford Yapp 89a1ffa10a First cut at moving rtwizard to Qt 2026-09-18 18:29:26 +00:00
Clifford Yapp fb1a921bdd Add more general animation support to rtwizard
Key additions:

 - Complete versioned JSON render specs covering database, objects, image/output, view, style, runtime, and animation.
 - CLI options override JSON values; explicit object lists replace JSON lists.
 - Cut, orbit, and turntable presets.
 - General typed tracks for camera, cutting planes, transforms, colors, materials, and temperatures.
 - Step, linear, smooth, and quaternion interpolation.
 - Persistent PNG frames, resumable rendering, atomic output, and APNG play counts.
 - View-keyframe capture into render specifications.
 - GUI controls for all three presets.
 - Strict field validation with no arbitrary shell or RT command execution.
 - Full specification documentation in doc/asciidoc/specifications/rtwizard_render_spec.adoc.
 - CLI documentation updated in doc/asciidoc/system/man1/rtwizard.adoc.

 Core implementation is in src/rtwizard/animation.cpp, src/rtwizard/main.c, and src/tclscripts/lib/RtImage.tcl.
2026-09-18 18:29:02 +00:00
68 changed files with 6434 additions and 4687 deletions
-246
View File
@@ -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.
@@ -2,6 +2,7 @@ set(
specifications_EN
BRL_CAD_g_format_V6.adoc
BRL_CAD_g_format_V5.adoc
rtwizard_render_spec.adoc
)
if(TARGET librt)
@@ -0,0 +1,268 @@
= RtWizard Render Specification, Version 1
:toc: left
== Purpose
The RtWizard render specification is a declarative JSON instruction file for
one still image or animation. It can contain the database, object roles,
image and output settings, view, rendering style, runtime settings, and an
optional animation. It does not contain shell commands or arbitrary
ray-tracer commands.
Run a specification with:
[source,bash]
----
rtwizard --render-spec scene.json
----
Options written after `--render-spec` override scalar values loaded from the
file. If `--color-objects`, `--ghost-objects`, or `--line-objects` is present
on the command line, that list replaces its JSON counterpart. Filenames in
`database`, `output.file`, `output.frame_dir`, `runtime.log_file`, and
`runtime.pid_file` are relative to the specification file unless absolute.
Unknown fields are errors. This makes misspellings visible and permits a
future version to assign new meanings without silently changing an old
render.
== Complete still-image example
[source,json]
----
{
"schema": "brlcad.rtwizard.render",
"version": 1,
"name": "M35 highlighted power train",
"database": "m35.g",
"objects": {
"color": ["component/power.train"],
"ghost": ["component"],
"line": ["component"]
},
"image": {
"width": 1024,
"height": 768,
"type": "F"
},
"output": {
"file": "m35-highlight.png"
},
"view": {
"azimuth": -35,
"elevation": 15,
"zoom": 1.6,
"perspective": 0
},
"style": {
"background": [255, 255, 255],
"line_color": [0, 0, 0],
"ghost_intensity": 6,
"occlusion": 1,
"ao_samples": 0,
"ao_radius": 0
},
"runtime": {
"benchmark": true,
"cpu_count": 0,
"verbose": 0,
"no_gui": true
}
}
----
== Top-level members
`schema`:: Required string `brlcad.rtwizard.render`.
`version`:: Required integer `1`.
`name`, `description`:: Optional human-readable metadata.
`database`:: Geometry database filename.
`objects`:: Object-role lists.
`image`:: Raster dimensions and picture type.
`output`:: Still, animation, frame-directory, or framebuffer destination.
`view`:: Initial or still-image camera.
`style`:: Compositing and ray-tracing appearance.
`runtime`:: Execution controls.
`animation`:: Optional preset or general track animation.
The following tables map JSON members to the corresponding RtWizard option.
Arrays used for points, vectors, quaternions, and colors contain only numbers.
[cols="2,2,3"]
|===
|Section |Member |Meaning
|`objects` |`color`, `ghost`, `line` |Arrays of database object or path names.
|`image` |`size`, `width`, `height` |Square size or per-axis raster dimensions.
|`image` |`type` |Picture type `A` through `F`.
|`output` |`file` |Still or animation filename.
|`output` |`frame_dir` |Directory for numbered PNG frames.
|`output` |`resume` |Reuse valid, correctly sized frames already in `frame_dir`.
|`output` |`framebuffer`, `framebuffer_port` |Framebuffer device and optional service port.
|`output` |`framebuffer_transport` |`auto` (local IPC preferred), `ipc`, or `tcp`.
|`view` |`azimuth`, `elevation`, `twist`, `zoom` |High-level view controls.
|`view` |`center` |Three-number high-level view center.
|`view` |`eye`, `view_size`, `orientation` |Complete low-level view: three-number eye, size, and four-number quaternion. All three are required together.
|`view` |`perspective` |Perspective angle.
|`style` |`background`, `line_color`, `non_line_color` |R/G/B arrays or color strings accepted by RtWizard.
|`style` |`ghost_intensity`, `occlusion` |Ghost visibility and edge occlusion mode.
|`style` |`ao_samples`, `ao_radius` |Ambient-occlusion sample count and maximum radius.
|`runtime` |`benchmark`, `cpu_count`, `verbose` |Repeatable rendering and process controls.
|`runtime` |`log_file`, `pid_file` |Diagnostic output files.
|`runtime` |`gui`, `no_gui` |Explicit interface selection. Do not set both true.
|===
== Preset animations
The three presets cover common jobs without keyframes:
`cut`:: Move a model-space cutting plane through the rendered object bounds.
`orbit`:: Move the camera around a fixed target while continuously looking at
that target. The geometry remains stationary.
`turntable`:: Rotate one database path about a model-space pivot while the
camera remains stationary.
This example makes a seamless model overview. A cyclic animation omits the
duplicate terminal frame.
[source,json]
----
{
"schema": "brlcad.rtwizard.render",
"version": 1,
"database": "vehicle.g",
"objects": { "color": ["all"] },
"image": { "size": 720, "type": "A" },
"output": { "file": "overview.apng", "frame_dir": "overview-frames" },
"animation": {
"preset": "orbit",
"timing": { "duration": 8, "fps": 24, "cyclic": true, "plays": 0 },
"options": {
"orbit_angle": 360,
"orbit_axis": [0, 0, 1],
"orbit_elevation": 20
}
}
}
----
Animation `timing` accepts `duration` in seconds, `fps`, an exact `frames`
count, `cyclic`, and `plays`. `plays: 0` means indefinite APNG playback;
MJPEG has no equivalent metadata. If `frames` is absent, a non-cyclic
animation has `round(duration * fps) + 1` frames and includes both endpoints.
A cyclic animation has `round(duration * fps)` frames and excludes its
duplicate endpoint.
Preset `options` are:
[cols="2,2,4"]
|===
|Preset |Member |Meaning and default
|cut |`cut_direction` |Nonzero model-space vector; default is forward through the initial view.
|orbit |`orbit_angle` |Sweep in degrees; default 360.
|orbit |`orbit_axis` |Model-space rotation axis; default `[0,0,1]`.
|orbit |`orbit_center` |Look-at target; default is the combined rendered-object bounds center.
|orbit |`orbit_elevation` |Camera elevation above the orbit plane in degrees; default preserves the initial eye.
|orbit |`orbit_radius` |Eye-to-target distance; default preserves the initial eye.
|turntable |`turntable_object` |Required database object or full path to rotate.
|turntable |`turntable_angle` |Sweep in degrees; default 360.
|turntable |`turntable_axis` |Model-space rotation axis; default `[0,0,1]`.
|turntable |`turntable_center` |Pivot; default is the turntable object's bounds center.
|===
== General animation tracks
Use `tracks` instead of `preset` and `options` when a preset is insufficient.
Track specifications require `timing`. `units` is either `database` or a
distance unit recognized by BRL-CAD, such as `mm`, `cm`, `m`, `in`, or `ft`.
It applies to animated points, translations, pivots, view sizes, and matrix
translation terms. The default is `database`.
[source,json]
----
"animation": {
"units": "mm",
"timing": { "duration": 4, "fps": 20, "cyclic": false, "plays": 1 },
"tracks": [
{
"type": "camera",
"interpolation": "smooth",
"keyframes": [
{ "time": 0, "eye": [4000, -4000, 1800], "look_at": [0, 0, 500] },
{ "time": 4, "eye": [-4000, -4000, 1800], "look_at": [0, 0, 500] }
]
},
{
"type": "transform",
"path": "vehicle/turret",
"operation": "left_multiply",
"pivot": [0, 0, 1200],
"keyframes": [
{ "time": 0, "rotation": { "axis": [0, 0, 1], "degrees": 0 } },
{ "time": 4, "rotation": { "axis": [0, 0, 1], "degrees": 90 } }
]
}
]
}
----
Every track contains a `type`, optional interpolation, and time-ordered
`keyframes`. Times are seconds in the inclusive range zero through
`timing.duration`; duplicate times for the same property are errors. A
single track may animate multiple properties, and properties need not occur
in every keyframe.
`interpolation` may be `step`, `linear`, or `smooth`. It may also be an
object with a `default` and per-property choices, for example
`{"default":"linear", "orientation":"smooth"}`. Numeric smooth
interpolation uses a Catmull-Rom curve. Rotations use normalized quaternion
spherical interpolation for both `linear` and `smooth`.
[cols="2,3,5"]
|===
|Track type |Fields |Details
|`camera` |`eye`; `look_at` or `orientation`; `up`; `view_size`; `perspective` |Each supplied property overlays the initial camera, so a track may animate only the properties it needs. `eye` and `look_at` are three-number points. `orientation` is `[x,y,z,w]` or an axis/degrees object. An optional `up` vector controls roll and requires `look_at`. Do not mix `look_at` and `orientation` in one camera track.
|`cut_plane` |`point`, `normal` |The normal is normalized. Both properties are required.
|`transform` |`path`, `operation`, optional `pivot`; keyframe `translation`, `rotation`, `scale`, or `matrix` |Rotation is quaternion or axis/degrees. Scale is uniform and nonzero. A matrix is 16 row-major numbers and is sampled with step interpolation. Do not mix matrix and decomposed values.
|`color` |`path`; keyframe `color` or `value` |Three channels in the inclusive range 0 through 255.
|`temperature` |`path`; keyframe `temperature` or `value` |Numeric region temperature.
|`material` |`path`, `operation`; keyframe `material` or `value` |String or structured material, sampled with step interpolation. Operation is `replace` (default) or `append`.
|===
A structured material avoids assembling a shader string. For example,
`{"shader":"plastic", "parameters":{"di":0.8, "sp":0.2}}` becomes
the BRL-CAD material `plastic di=0.8 sp=0.2`. Parameter values may be finite
numbers, booleans, or command-word strings.
Transform operations are `replace_stack`, `replace_arc`, `left_multiply`,
`right_multiply` (default), and `replace_both`, corresponding to the typed
BRL-CAD `anim ... matrix` operations. Animation paths may not contain
whitespace or command separators. Material strings may not contain command
separators. These restrictions keep the format declarative.
== Frames, restart, and keyframe capture
If `output.frame_dir` is set, each completed frame is atomically stored as
`frame-NNNNNN.png`. Existing frames are an error unless `output.resume` is
true. Resume accepts only readable images with the requested dimensions and
rerenders invalid ones. The final animation is also written when
`output.file` is present.
An interactively selected or command-line view can be captured into a render
specification without rendering:
[source,bash]
----
rtwizard -i vehicle.g -c all -a 25 -e 15 \
--save-view-keyframe vehicle.json --time 2.5
----
For a new animation the command records camera distances in millimeters. If
the specification already declares animation units, that unit convention is
preserved and the resolved base-unit view is converted before it is appended.
A duplicate time is rejected; add `--replace-keyframe` to replace it.
Capturing multiple views is a convenient way to author a camera track without
calculating eye points or quaternions. The Qt workspace offers the same
operation as _Capture Current View_ on its camera-keyframe timeline.
+1 -53
View File
@@ -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
+141 -11
View File
@@ -25,18 +25,29 @@ BRL-CAD image-manipulation tools. These layers are composited into a
single image written to a *pix*(5) or PNG file, or displayed on a
framebuffer.
When *--cut-steps* is supplied, rtwizard produces an animation in which a
cutting plane advances through the rendered objects. The output may be APNG
or MJPEG, and every full-color, ghost, and edge pass uses the same cutting
plane for each frame.
*rtwizard* can also produce APNG or MJPEG animations. Simple presets move a
cutting plane, orbit the camera around a fixed model, or rotate a database
object on a turntable. A declarative JSON render specification can describe
the complete render and, when needed, general camera, cutting-plane,
transform, color, material, and temperature tracks. Every full-color,
ghost, and edge pass uses the same state for each frame.
*rtwizard* has two modes. With no arguments (or with *--gui*) it launches
a wizard-style graphical interface that guides the user through building an
image interactively. Given sufficient options on the command line it runs
non-interactively, which is useful for scripting and bulk rendering. If a
command line invocation lacks enough information to produce the requested
a Qt workspace with a live libqtcad model view, hierarchy-based render-role
assignment, still-image controls, animation presets, camera-keyframe capture,
preview, render progress, cancellation, and a diagnostic log. Render
specifications can be opened and saved directly; advanced tracks the workspace
does not edit are preserved. Given sufficient options on the command line it
runs non-interactively, which is useful for scripting and bulk rendering. If
a command line invocation lacks enough information to produce the requested
image, *rtwizard* falls back to the GUI unless *--no-gui* is given.
The GUI and command-line modes use the same C++17 rendering engine. Renderer
processes are launched and managed with libbu, view state is resolved with
libged, images and animations are handled with libicv, and the application
does not evaluate Tcl startup or preference files. A small, recognized subset
of legacy `.rtwizardrc` window settings is imported once as data.
The kind of image produced is governed by the *--type* option, or inferred
from which object lists are supplied. The six picture types are:
@@ -75,9 +86,18 @@ preferred.
*-o* _filename_, *--output-file* _filename_::
Output image file. A .png extension produces a PNG image; otherwise
a BRL-CAD *pix*(5) image is written. Other supported extensions are
.ppm, .bw, and .dpix. In cutting-plane animation mode, .png and .apng
.ppm, .bw, and .dpix. In animation mode, .png and .apng
produce APNG while .avi and .mjpg produce MJPEG.
*--render-spec* _file.json_::
Load a complete version 1 `brlcad.rtwizard.render` JSON specification.
It may supply the database, object roles, image/output, view, style,
runtime, and animation. Ordinary command-line options override scalar
values from the file; an explicit command-line object-role list replaces
that JSON list. Recipe filenames are resolved relative to the recipe.
See `RtWizard Render Specification, Version 1` in the BRL-CAD
specifications documentation.
*-s* _#_, *--size* _#_::
Width and height of a square output image, in pixels (default 512).
Overridden per-axis by *-w* and *-n*.
@@ -92,6 +112,8 @@ preferred.
Produce a cutting-plane animation containing exactly this many frames.
The value must be at least 2. The first frame is just before clipping
begins and the last is just before the rendered bounds are fully clipped.
This is the compatibility shorthand for `--animation cut` with an exact
frame count.
*--cut-direction* _x/y/z_::
Model-space direction in which the cutting plane advances. The vector is
@@ -101,6 +123,80 @@ preferred.
*--animation-fps* _#_::
Animation playback rate in frames per second (default 10).
*--animation* _cut|orbit|turntable_::
Select an easy animation preset. Orbit moves the camera while the model
stays fixed; turntable moves a specified model path while the camera stays
fixed.
*--animation-file* _file.json_::
Read general animation tracks from a version 1 RtWizard render
specification. Usually *--render-spec* is more convenient because it can
also supply the other render settings.
*--animation-duration* _seconds_::
Animation duration (preset default 5). Overrides JSON timing when given.
*--animation-frames* _#_::
Exact number of frames. If omitted, it is derived from duration and fps.
*--animation-plays* _#_::
APNG playback count. Zero means indefinite playback. MJPEG ignores this
metadata.
*--animation-cyclic* _0|1_::
Whether sampling excludes a terminal frame that duplicates the first.
Complete 360-degree orbit and turntable presets default to cyclic.
*--frame-dir* _directory_::
Preserve completed animation frames as numbered PNG files. The directory
may be used with or without a final *-o* animation.
*--resume*::
Reuse readable, correctly sized frames already in *--frame-dir*. Without
this option, an existing numbered frame is an error.
*--orbit-angle* _degrees_::
Camera sweep for the orbit preset (default 360).
*--orbit-axis* _x/y/z_::
Model-space orbit axis (default 0/0/1).
*--orbit-center* _x/y/z_::
Fixed look-at target. Defaults to the combined rendered-object bounds
center.
*--orbit-elevation* _degrees_::
Camera elevation above the plane normal to the orbit axis. If omitted,
the initial eye's elevation is preserved.
*--orbit-radius* _distance_::
Camera distance from the orbit center. If omitted, the initial eye's
distance is preserved.
*--turntable-object* _path_::
Database object or full path rotated by the turntable preset. Required
for that preset.
*--turntable-angle* _degrees_::
Model rotation sweep (default 360).
*--turntable-axis* _x/y/z_::
Model-space turntable axis (default 0/0/1).
*--turntable-center* _x/y/z_::
Rotation pivot. Defaults to the turntable object's bounds center.
*--save-view-keyframe* _file.json_::
Save the resolved view as a camera keyframe in a render specification and
exit without rendering. Creates the file when necessary.
*--time* _seconds_::
Nonnegative keyframe time required by *--save-view-keyframe*.
*--replace-keyframe*::
Replace a camera keyframe already present at *--time* rather than report
an error.
*-t* _A|B|C|D|E|F_, *--type* _A|B|C|D|E|F_::
Picture type to produce (see the table above). If a type is requested
but the supplied object lists are insufficient for it, *rtwizard* exits
@@ -185,8 +281,17 @@ preferred.
after image generation.
*-p* _#_, *--fbserv-port* _#_::
TCP port for the *fbserv*(1) framebuffer. An existing framebuffer server
may be reused; in that case *rtwizard* leaves it open after rendering.
Framebuffer service port. With the default automatic transport, *rtwizard*
launches a managed server over local IPC and uses this number only if it
must fall back to TCP. A TCP server selected explicitly is left open after
rendering when it was already running.
*--fbserv-transport* _auto|ipc|tcp_::
Select communication with a managed *fbserv*(1). `auto` (the default)
prefers BRL-CAD local IPC and falls back to TCP only when local IPC is not
available. `ipc` requires local IPC. `tcp` preserves explicit TCP reuse
and startup behavior. Authentication is enabled for servers started by
*rtwizard* regardless of transport.
*--benchmark*::
Benchmark mode: disable randomness for reproducible timing.
@@ -254,6 +359,31 @@ MJPEG. Picture types using edge lines and the ambient-occlusion options may
be combined with the animation options.
====
.Orbit overview with restartable frames
====
[source,bash]
----
rtwizard --no-gui -i vehicle.g -c all -o overview.apng \
--animation orbit --orbit-axis 0/0/1 --orbit-elevation 20 \
--animation-duration 8 --animation-fps 24 \
--frame-dir overview-frames
----
Moves the camera through a full circle around the rendered bounds center,
looking at that center throughout. The model does not move. Add *--resume*
to a later identical invocation to reuse valid frames.
====
.Complete render recipe
====
[source,bash]
----
rtwizard --render-spec vehicle-overview.json
----
Loads the database, objects, dimensions, output, style, view, and optional
animation from one declarative file. For a one-off higher resolution render,
append `--size 1440`; the command-line value overrides the recipe.
====
[[see_also]]
== SEE ALSO
+2 -32
View File
@@ -25,7 +25,7 @@ where the "char" argument is replaced by a single character identifying the cate
[[nirt_format_categories]]
== Output Categories
NIRT offers 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]
+9 -8
View File
@@ -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*,
+4
View File
@@ -137,6 +137,10 @@ ICV_EXPORT extern size_t icv_anim_num_frames(const icv_anim_t *anim);
*/
ICV_EXPORT extern int icv_anim_set_fps(icv_anim_t *anim, int fps);
/** Set the container playback count. Zero means indefinite playback.
* Formats without playback-count metadata ignore this value. */
ICV_EXPORT extern int icv_anim_set_plays(icv_anim_t *anim, uint32_t plays);
/**
* Set a specific frame's delay in microseconds. This overrides the global FPS
* for this specific frame. (Note: Only fully supported by APNG format. MJPG
+11
View File
@@ -461,6 +461,17 @@ PKG_EXPORT extern struct pkg_conn *pkg_connect_addr(const char *addr,
PKG_EXPORT extern int pkg_ipc_addr(char *addr, size_t len,
const char *name_hint);
/**
* Remove any filesystem name associated with an opaque IPC listener address.
* This is safe to call after normal listener shutdown and is useful when an
* externally launched listener had to be terminated before it could clean up.
* Address types without persistent filesystem state are successful no-ops.
*
* Returns 0 on success (including an already absent endpoint), -1 for an
* invalid/unsupported address or a cleanup failure.
*/
PKG_EXPORT extern int pkg_ipc_addr_cleanup(const char *addr);
/**
* Return non-zero when addr names a reusable listener-style IPC endpoint,
* as opposed to a single inherited connected endpoint.
-85
View File
@@ -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
View File
@@ -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
+4
View File
@@ -403,6 +403,8 @@ test_pkg_ipc_listener(void)
}
}
TEST("opaque local IPC endpoint cleanup succeeds",
pkg_ipc_addr_cleanup(addr) == 0);
pkg_listener_close(listener);
}
#endif
@@ -907,6 +909,8 @@ test_pkg_ipc_listener_win(void)
TEST("Windows npipe client round-trip succeeds", client_rc == 0);
}
TEST("opaque Windows IPC endpoint cleanup succeeds",
pkg_ipc_addr_cleanup(addr) == 0);
pkg_listener_close(listener);
}
#endif /* _WIN32 */
+187 -2
View File
@@ -17,7 +17,28 @@ set(STILL "${TEST_DIR}/rtwizard_cut_still.pix")
set(FIRST "${TEST_DIR}/rtwizard_cut_first.pix")
set(LAST "${TEST_DIR}/rtwizard_cut_last.pix")
set(PREFIX "${TEST_DIR}/rtwizard_cut_frame_")
file(REMOVE "${ANIM}" "${STILL}" "${FIRST}" "${LAST}")
set(SPEC "${TEST_DIR}/rtwizard_render_spec.json")
set(SPEC_STILL "${TEST_DIR}/rtwizard_spec_still.pix")
set(SPEC_OVERRIDE "${TEST_DIR}/rtwizard_spec_override.pix")
set(SPEC_CONTROL "${TEST_DIR}/rtwizard_spec_control.pix")
set(ORBIT_SPEC "${TEST_DIR}/rtwizard_orbit_spec.json")
set(ORBIT "${TEST_DIR}/rtwizard_orbit.apng")
set(ORBIT_PREFIX "${TEST_DIR}/rtwizard_orbit_frame_")
set(TRACK_SPEC "${TEST_DIR}/rtwizard_track_spec.json")
set(TRACK_ANIM "${TEST_DIR}/rtwizard_track.apng")
set(TRACK_PREFIX "${TEST_DIR}/rtwizard_track_frame_")
set(TRACK_DIRECT "${TEST_DIR}/rtwizard_track_direct.apng")
set(TRACK_DIRECT_PREFIX "${TEST_DIR}/rtwizard_track_direct_frame_")
set(TURNTABLE_SPEC "${TEST_DIR}/rtwizard_turntable_spec.json")
set(TURNTABLE_ANIM "${TEST_DIR}/rtwizard_turntable.apng")
set(TURNTABLE_DIR "${TEST_DIR}/rtwizard_turntable_frames")
set(IPC_SPEC "${TEST_DIR}/rtwizard_ipc_spec.json")
set(IPC_OUTPUT "${TEST_DIR}/rtwizard_ipc.png")
file(TO_CMAKE_PATH "${SRCFILE}" SRCFILE_JSON)
file(REMOVE "${ANIM}" "${STILL}" "${FIRST}" "${LAST}" "${SPEC}" "${SPEC_STILL}" "${SPEC_OVERRIDE}" "${SPEC_CONTROL}"
"${ORBIT_SPEC}" "${ORBIT}" "${TRACK_SPEC}" "${TRACK_ANIM}" "${TRACK_DIRECT}"
"${TURNTABLE_SPEC}" "${TURNTABLE_ANIM}" "${IPC_SPEC}" "${IPC_OUTPUT}")
file(REMOVE_RECURSE "${TURNTABLE_DIR}")
file(GLOB OLD_FRAMES "${PREFIX}*")
if(OLD_FRAMES)
file(REMOVE ${OLD_FRAMES})
@@ -78,4 +99,168 @@ if(last_convert_result OR stat_result OR stat_log MATCHES "Min[ \t]+255[ \t]+255
message(FATAL_ERROR "The final animation frame is fully clipped:\n${stat_log}")
endif()
file(REMOVE "${ANIM}" "${STILL}" "${FIRST}" "${LAST}" ${FRAMES})
# A render specification can provide the complete still-image invocation.
# A later scalar command-line option must override the value in the file.
file(WRITE "${SPEC}" "{
\"schema\": \"brlcad.rtwizard.render\",
\"version\": 1,
\"database\": \"${SRCFILE_JSON}\",
\"objects\": {\"color\": [\"component\"], \"line\": [], \"ghost\": []},
\"image\": {\"size\": 64, \"type\": \"A\"},
\"output\": {\"file\": \"rtwizard_spec_still.pix\"},
\"view\": {\"azimuth\": 35, \"elevation\": 25, \"zoom\": 1.5},
\"runtime\": {\"benchmark\": true, \"no_gui\": true}
}\n")
execute_process(
COMMAND "${RTWIZ}" --render-spec "${SPEC}" --size 48 --output-file "${SPEC_OVERRIDE}"
RESULT_VARIABLE spec_result OUTPUT_VARIABLE spec_log ERROR_VARIABLE spec_log
)
execute_process(
COMMAND "${RTWIZ}" --no-gui -i "${SRCFILE}" -o "${SPEC_CONTROL}" -c component
-t A -s 48 -a 35 -e 25 -z 1.5 --benchmark
RESULT_VARIABLE control_result
)
execute_process(COMMAND "${PIXCMP_EXEC}" "${SPEC_OVERRIDE}" "${SPEC_CONTROL}"
RESULT_VARIABLE spec_compare_result OUTPUT_VARIABLE spec_compare_log ERROR_VARIABLE spec_compare_log)
if(spec_result OR control_result OR spec_compare_result OR EXISTS "${SPEC_STILL}")
message(FATAL_ERROR "Render-spec still or CLI override failed:\n${spec_log}\n${spec_compare_log}")
endif()
# Exercise the orbit preset and persistent frame directory.
file(WRITE "${ORBIT_SPEC}" "{
\"schema\": \"brlcad.rtwizard.render\",
\"version\": 1,
\"database\": \"${SRCFILE_JSON}\",
\"objects\": {\"color\": [\"component\"]},
\"image\": {\"size\": 32, \"type\": \"A\"},
\"output\": {\"file\": \"rtwizard_orbit.apng\"},
\"runtime\": {\"benchmark\": true, \"no_gui\": true},
\"animation\": {
\"preset\": \"orbit\",
\"timing\": {\"duration\": 1, \"fps\": 3, \"frames\": 3, \"cyclic\": true, \"plays\": 2},
\"options\": {\"orbit_angle\": 360, \"orbit_axis\": [0, 0, 1], \"orbit_elevation\": 20}
}
}\n")
execute_process(COMMAND "${RTWIZ}" --render-spec "${ORBIT_SPEC}"
RESULT_VARIABLE orbit_result OUTPUT_VARIABLE orbit_log ERROR_VARIABLE orbit_log)
execute_process(COMMAND "${ICV_EXEC}" anim extract "${ORBIT}" "${ORBIT_PREFIX}"
RESULT_VARIABLE orbit_extract_result OUTPUT_VARIABLE orbit_extract_log ERROR_VARIABLE orbit_extract_log)
file(GLOB ORBIT_FRAMES "${ORBIT_PREFIX}*_ctrl.png")
list(LENGTH ORBIT_FRAMES ORBIT_FRAME_COUNT)
if(orbit_result OR orbit_extract_result OR NOT ORBIT_FRAME_COUNT EQUAL 3)
message(FATAL_ERROR "Orbit render specification failed or produced ${ORBIT_FRAME_COUNT} frames:\n${orbit_log}\n${orbit_extract_log}")
endif()
# Exercise a general, interpolated camera track rather than a preset.
file(WRITE "${TRACK_SPEC}" "{
\"schema\": \"brlcad.rtwizard.render\",
\"version\": 1,
\"database\": \"${SRCFILE_JSON}\",
\"objects\": {\"color\": [\"component\"]},
\"image\": {\"size\": 32, \"type\": \"A\"},
\"output\": {\"file\": \"rtwizard_track.apng\"},
\"runtime\": {\"benchmark\": true, \"no_gui\": true},
\"animation\": {
\"units\": \"mm\",
\"timing\": {\"duration\": 1, \"fps\": 2, \"frames\": 2, \"cyclic\": false},
\"tracks\": [
{
\"type\": \"camera\",
\"interpolation\": \"linear\",
\"keyframes\": [
{\"time\": 0, \"view_size\": 12000},
{\"time\": 1, \"view_size\": 10000}
]
},
{
\"type\": \"color\",
\"path\": \"/all.g/component/bed/r850\",
\"keyframes\": [
{\"time\": 0, \"color\": [255, 0, 0]},
{\"time\": 1, \"color\": [0, 0, 255]}
]
}
]
}
}\n")
execute_process(COMMAND "${RTWIZ}" --render-spec "${TRACK_SPEC}"
RESULT_VARIABLE track_result OUTPUT_VARIABLE track_log ERROR_VARIABLE track_log)
execute_process(COMMAND "${ICV_EXEC}" anim extract "${TRACK_ANIM}" "${TRACK_PREFIX}"
RESULT_VARIABLE track_extract_result OUTPUT_VARIABLE track_extract_log ERROR_VARIABLE track_extract_log)
file(GLOB TRACK_FRAMES "${TRACK_PREFIX}*_ctrl.png")
list(LENGTH TRACK_FRAMES TRACK_FRAME_COUNT)
if(track_result OR track_extract_result OR NOT TRACK_FRAME_COUNT EQUAL 2)
message(FATAL_ERROR "General camera track failed or produced ${TRACK_FRAME_COUNT} frames:\n${track_log}\n${track_extract_log}")
endif()
# The animation-only entry point must honor timing.frames from the same
# general render specification without relying on render-spec expansion.
execute_process(
COMMAND "${RTWIZ}" --no-gui -i "${SRCFILE}" -o "${TRACK_DIRECT}" -c component
-t A -s 32 --benchmark --animation-file "${TRACK_SPEC}"
RESULT_VARIABLE track_direct_result OUTPUT_VARIABLE track_direct_log ERROR_VARIABLE track_direct_log)
execute_process(COMMAND "${ICV_EXEC}" anim extract "${TRACK_DIRECT}" "${TRACK_DIRECT_PREFIX}"
RESULT_VARIABLE track_direct_extract_result
OUTPUT_VARIABLE track_direct_extract_log ERROR_VARIABLE track_direct_extract_log)
file(GLOB TRACK_DIRECT_FRAMES "${TRACK_DIRECT_PREFIX}*_ctrl.png")
list(LENGTH TRACK_DIRECT_FRAMES TRACK_DIRECT_FRAME_COUNT)
if(track_direct_result OR track_direct_extract_result OR NOT TRACK_DIRECT_FRAME_COUNT EQUAL 2)
message(FATAL_ERROR "Direct animation-file timing failed or produced ${TRACK_DIRECT_FRAME_COUNT} frames:\n${track_direct_log}\n${track_direct_extract_log}")
endif()
# A turntable animates the model, not the camera. Its two persistent frames
# must differ, proving the typed database animation reached rt after DB open.
file(WRITE "${TURNTABLE_SPEC}" "{
\"schema\": \"brlcad.rtwizard.render\",
\"version\": 1,
\"database\": \"${SRCFILE_JSON}\",
\"objects\": {\"color\": [\"component\"]},
\"image\": {\"size\": 32, \"type\": \"A\"},
\"output\": {\"file\": \"rtwizard_turntable.apng\", \"frame_dir\": \"rtwizard_turntable_frames\"},
\"runtime\": {\"benchmark\": true, \"no_gui\": true},
\"animation\": {
\"preset\": \"turntable\",
\"timing\": {\"duration\": 1, \"fps\": 2, \"frames\": 2, \"cyclic\": false},
\"options\": {\"turntable_object\": \"component\", \"turntable_angle\": 90}
}
}\n")
execute_process(COMMAND "${RTWIZ}" --render-spec "${TURNTABLE_SPEC}"
RESULT_VARIABLE turntable_result OUTPUT_VARIABLE turntable_log ERROR_VARIABLE turntable_log)
set(TURNTABLE_FIRST "${TURNTABLE_DIR}/frame-000000.png")
set(TURNTABLE_LAST "${TURNTABLE_DIR}/frame-000001.png")
if(turntable_result OR NOT EXISTS "${TURNTABLE_FIRST}" OR NOT EXISTS "${TURNTABLE_LAST}")
message(FATAL_ERROR "Turntable render specification failed:\n${turntable_log}")
endif()
file(SHA256 "${TURNTABLE_FIRST}" TURNTABLE_FIRST_HASH)
file(SHA256 "${TURNTABLE_LAST}" TURNTABLE_LAST_HASH)
if(TURNTABLE_FIRST_HASH STREQUAL TURNTABLE_LAST_HASH)
message(FATAL_ERROR "Turntable did not change the rendered model between frames")
endif()
# A managed framebuffer uses the platform's local libpkg transport without
# opening a TCP listener. Exercise the JSON spelling as well as the data path.
file(WRITE "${IPC_SPEC}" "{
\"schema\": \"brlcad.rtwizard.render\",
\"version\": 1,
\"database\": \"${SRCFILE_JSON}\",
\"objects\": {\"color\": [\"component\"]},
\"image\": {\"size\": 16, \"type\": \"A\"},
\"output\": {
\"file\": \"rtwizard_ipc.png\",
\"framebuffer\": \"/dev/mem\",
\"framebuffer_port\": 51,
\"framebuffer_transport\": \"ipc\"
},
\"runtime\": {\"benchmark\": true, \"no_gui\": true, \"verbose\": 1}
}\n")
execute_process(COMMAND "${RTWIZ}" --render-spec "${IPC_SPEC}"
RESULT_VARIABLE ipc_result OUTPUT_VARIABLE ipc_log ERROR_VARIABLE ipc_log)
if(ipc_result OR NOT EXISTS "${IPC_OUTPUT}" OR NOT ipc_log MATCHES "over local IPC")
message(FATAL_ERROR "Local IPC framebuffer rendering failed:\n${ipc_log}")
endif()
file(REMOVE "${ANIM}" "${STILL}" "${FIRST}" "${LAST}" "${SPEC}" "${SPEC_STILL}" "${SPEC_OVERRIDE}" "${SPEC_CONTROL}"
"${ORBIT_SPEC}" "${ORBIT}" "${TRACK_SPEC}" "${TRACK_ANIM}" "${TRACK_DIRECT}"
"${TURNTABLE_SPEC}" "${TURNTABLE_ANIM}" "${IPC_SPEC}" "${IPC_OUTPUT}"
${FRAMES} ${ORBIT_FRAMES} ${TRACK_FRAMES} ${TRACK_DIRECT_FRAMES})
file(REMOVE_RECURSE "${TURNTABLE_DIR}")
+2 -3
View File
@@ -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)
-8
View File
@@ -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
View File
@@ -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
View File
@@ -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;
-11
View File
@@ -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
View File
@@ -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;
}
/*
-43
View File
@@ -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
*/
-16
View File
@@ -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
-179
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -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);
}
}
}
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
*/
+2 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -196
View File
@@ -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);
+1 -17
View File
@@ -83,7 +83,7 @@ extern "C" b_off_t ftello(FILE *);
#define NIRT_OVLP_RETAIN 3
#define NIRT_PRINTF_SPECIFIERS "difeEgGs"
#define NIRT_OUTPUT_TYPE_SPECIFIERS "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 */
-3
View File
@@ -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 #
-191
View File
@@ -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(&current, state, NIRT_OUT);
bu_vls_vlscat(captured, &current);
bu_vls_free(&current);
return 0;
}
static void
write_annotation(struct rt_wdb *wdbp, const char *name, fastf_t x,
bool screen_space)
{
struct rt_annot_internal annotation = {};
struct line_seg line = {};
struct rt_annot_seg_style style = {};
int reverse = 0;
void *segment = &line;
point2d_t vertices[2] = {{0.0, 0.0}, {TEST_HALF_EXTENT, 0.0}};
annotation.magic = RT_ANNOT_INTERNAL_MAGIC;
VSET(annotation.V, x, 0.0, 0.0);
annotation.flags = screen_space ? RT_ANNOT_SCREEN_SPACE :
RT_ANNOT_MODEL_SPACE;
VSET(annotation.u_vec, 0.0, 1.0, 0.0);
VSET(annotation.v_vec, 0.0, 0.0, 1.0);
annotation.vert_count = 2;
annotation.verts = vertices;
annotation.ant.count = 1;
annotation.ant.reverse = &reverse;
annotation.ant.segments = &segment;
annotation.styles = &style;
line.magic = CURVE_LSEG_MAGIC;
line.start = 0;
line.end = 1;
style.role = RT_ANNOT_ROLE_LEADER;
style.line_pattern = RT_ANNOT_LINE_CONTINUOUS;
if (rt_annot_validate(&annotation, NULL) ||
mk_annot(wdbp, name, &annotation) < 0)
bu_exit(EXIT_FAILURE, "unable to write %s\n", name);
}
static void
clear_standard_formats(struct nirt_state *state)
{
const char *commands[] = {
"fmt r \"\"", "fmt h \"\"", "fmt p \"\"", "fmt f \"\"",
"fmt m \"\"", "fmt o \"\"", "fmt g \"\""
};
for (size_t i = 0; i < sizeof(commands) / sizeof(commands[0]); ++i)
if (nirt_exec(state, commands[i]))
bu_exit(EXIT_FAILURE, "NIRT rejected command: %s\n", commands[i]);
}
static void
get_output(struct bu_vls *output, struct bu_vls *captured)
{
bu_vls_sprintf(output, "%s", bu_vls_cstr(captured));
bu_vls_trunc(captured, 0);
}
int
main(int argc, char **argv)
{
char path[MAXPATHLEN];
struct db_i *dbip;
struct rt_wdb *wdbp;
struct nirt_state *state;
struct bu_vls captured = BU_VLS_INIT_ZERO;
struct bu_vls output = BU_VLS_INIT_ZERO;
struct wmember members;
point_t minimum, maximum;
bu_setprogname(argv[0]);
if (argc != 1)
return EXIT_FAILURE;
{
FILE *fp = bu_temp_file(path, sizeof(path));
if (!fp)
bu_exit(EXIT_FAILURE, "unable to create temporary database path\n");
fclose(fp);
}
dbip = db_create(path, 5);
if (!dbip)
bu_exit(EXIT_FAILURE, "unable to create temporary database\n");
wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_DISK);
if (!wdbp)
bu_exit(EXIT_FAILURE, "unable to open temporary database for writing\n");
write_annotation(wdbp, "front.annot", FRONT_ANNOTATION_X, false);
write_annotation(wdbp, "rear.annot", REAR_ANNOTATION_X, false);
write_annotation(wdbp, "screen.annot", SCREEN_ANNOTATION_X, true);
VSET(minimum, OCCLUDER_MINIMUM_X, -TEST_HALF_EXTENT,
-TEST_HALF_EXTENT);
VSET(maximum, OCCLUDER_MAXIMUM_X, TEST_HALF_EXTENT,
TEST_HALF_EXTENT);
if (mk_rpp(wdbp, "occluder.s", minimum, maximum) < 0)
bu_exit(EXIT_FAILURE, "unable to write occluder\n");
BU_LIST_INIT(&members.l);
if (!mk_addmember("occluder.s", &members.l, NULL, WMOP_UNION) ||
mk_lcomb(wdbp, "occluder.r", &members, 1, NULL, NULL, NULL, 0))
bu_exit(EXIT_FAILURE, "unable to write occluding region\n");
BU_GET(state, struct nirt_state);
if (nirt_init(state))
bu_exit(EXIT_FAILURE, "unable to initialize NIRT\n");
nirt_udata(state, &captured);
nirt_hook(state, capture_output, NIRT_OUT);
if (nirt_exec(state,
"draw front.annot rear.annot screen.annot occluder.r") ||
nirt_init_dbip(state, dbip))
bu_exit(EXIT_FAILURE, "unable to prepare NIRT test database\n");
clear_standard_formats(state);
if (nirt_exec(state,
"fmt a \"%s|%d|%s|%.1f|%.1f|%d\\n\" annot_path annot_segment annot_role annot_x annot_d annot_visible"))
bu_exit(EXIT_FAILURE, "unable to set annotation report format\n");
struct bu_vls shot = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&shot, "backout 0;xyz %g 0 0;dir -1 0 0;s",
RAY_ORIGIN_X);
if (nirt_exec(state, bu_vls_cstr(&shot)))
bu_exit(EXIT_FAILURE, "unable to shoot default NIRT ray\n");
bu_vls_free(&shot);
get_output(&output, &captured);
if (bu_vls_strlen(&output))
bu_exit(EXIT_FAILURE,
"annotations were reported without useannot: %s\n",
bu_vls_cstr(&output));
if (nirt_exec(state, "useannot 1;s"))
bu_exit(EXIT_FAILURE, "unable to shoot annotation NIRT ray\n");
get_output(&output, &captured);
struct bu_vls expected = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&expected,
"/front.annot|0|leader|%.1f|%.1f|1\n"
"/rear.annot|0|leader|%.1f|%.1f|0\n",
FRONT_ANNOTATION_X, FRONT_ANNOTATION_X,
REAR_ANNOTATION_X, REAR_ANNOTATION_X);
if (!BU_STR_EQUAL(bu_vls_cstr(&output), bu_vls_cstr(&expected)))
bu_exit(EXIT_FAILURE, "unexpected annotation report:\n%s",
bu_vls_cstr(&output));
bu_vls_free(&expected);
nirt_destroy(state);
BU_PUT(state, struct nirt_state);
bu_vls_free(&captured);
bu_vls_free(&output);
db_close(dbip);
bu_file_delete(path);
return EXIT_SUCCESS;
}
+6 -12
View File
@@ -419,7 +419,7 @@ protected:
ListNode* AllocListNode();
void FreeListNode(ListNode* a_listNode);
bool Overlap(Rect* a_rectA, Rect* a_rectB) const;
bool Intersect(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
+20
View File
@@ -318,6 +318,25 @@ test_exec_wait(const char* cmd)
}
/* A normal non-zero child exit must be reported to callers. */
static int
test_exit_status(const char* cmd)
{
struct bu_process* p;
const char* run_av[3] = {cmd, "exit_7", NULL};
bu_process_create(&p, (const char**)run_av, BU_PROCESS_DEFAULT);
int status = bu_process_wait_n(&p, 0);
if (status != 7) {
fprintf(stderr, "bu_process_test[\"exit_status\"] - got %d, expected 7\n", status);
return PROCESS_FAIL;
}
return PROCESS_PASS;
}
/* tests: process creation with options
* bu_process_create() [bu_process_opts]
* also relies on
@@ -966,6 +985,7 @@ typedef struct {
ProcessTest tests[] = {
{"exec_wait", test_exec_wait},
{"exit_status", test_exit_status},
{"create_opts", test_create_opts},
{"create_timeout", test_create_timeout},
{"func_capture", test_func_capture},
+1 -1
View File
@@ -83,7 +83,7 @@ main(int argc, const char *argv[])
return 0;
}
if (BU_STR_EQUAL(argv[1], "exit7"))
if (BU_STR_EQUAL(argv[1], "exit7") || BU_STR_EQUAL(argv[1], "exit_7"))
return 7;
if (BU_STR_EQUAL(argv[1], "output")) {
+6 -8
View File
@@ -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);
+12 -1
View File
@@ -52,6 +52,7 @@ struct icv_anim {
uint32_t width;
uint32_t height;
int fps;
uint32_t plays;
std::vector<icv_anim_frame> frames;
};
@@ -66,6 +67,7 @@ extern "C" {
anim->width = width;
anim->height = height;
anim->fps = (fps > 0) ? fps : 10;
anim->plays = 0;
return anim;
}
@@ -102,6 +104,14 @@ extern "C" {
return 0;
}
int
icv_anim_set_plays(icv_anim_t *anim, uint32_t plays)
{
if (!anim) return -1;
anim->plays = plays;
return 0;
}
int
icv_anim_set_frame_delay(icv_anim_t *anim, size_t index, uint32_t delay_usec)
{
@@ -296,6 +306,7 @@ extern "C" {
if (aapng.frames.empty()) return NULL;
icv_anim_t *anim = icv_anim_create(ICV_ANIM_APNG, aapng.canvas_width, aapng.canvas_height, 10);
anim->plays = aapng.num_plays;
bool use_composed_frames = false;
for (size_t i = 0; i < aapng.frames.size(); ++i) {
@@ -517,7 +528,7 @@ extern "C" {
apngmini::Animation aapng;
aapng.canvas_width = w;
aapng.canvas_height = h;
aapng.num_plays = 0;
aapng.num_plays = anim->plays;
for (size_t i = 0; i < anim->frames.size(); ++i) {
aapng.frames.push_back(icv_to_apng(anim->frames[i].img, anim->frames[i].delay_usec));
+25
View File
@@ -513,6 +513,31 @@ pkg_ipc_addr(char *addr, size_t len, const char *name_hint)
#endif
}
int
pkg_ipc_addr_cleanup(const char *addr)
{
if (!addr || addr[0] == '\0')
return -1;
#ifdef _WIN32
/* Named pipe names cease to exist when their final handle closes. */
return strncmp(addr, "npipe:", 6) == 0 ? 0 : -1;
#else
const char *path = NULL;
if (strncmp(addr, "fifo:", 5) == 0 || strncmp(addr, "unix:", 5) == 0)
path = addr + 5;
# if defined(__linux__)
else if (strncmp(addr, "unix-abstract:", 14) == 0)
return 0;
# endif
if (!path || path[0] == '\0')
return -1;
if (remove(path) == 0 || errno == ENOENT)
return 0;
return -1;
#endif
}
/* ================================================================== */
/* pkg_connect_addr / pkg_connect_env */
-1
View File
@@ -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
+586 -47
View File
@@ -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
+138 -318
View File
@@ -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(&region, model, &intern, &ttol, &tol) || !region)
bu_exit(1, "unable to tessellate model-space annotation\n");
nmg_km(model);
}
}
rt_db_free_internal(&intern);
{
const 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)
+1
View File
@@ -889,3 +889,4 @@ done:
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
-1
View File
@@ -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
View File
@@ -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,
-9
View File
@@ -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 {
-6
View File
@@ -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++) {
-9
View File
@@ -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
View File
@@ -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);
+46 -23
View File
@@ -1,35 +1,58 @@
if(TARGET libtclcad)
set(RTWIZARD_SRCS main.c)
set(RTWIZARD_SRCS main.c animation.cpp render.cpp)
if(HAVE_WINDOWS_H)
# To associate an icon with the application for Windows (needed
# for the WiX installer) we must use an rc file.
enable_language(RC)
set(RTWIZARD_SRCS ${RTWIZARD_SRCS} rtwizard.rc)
endif(HAVE_WINDOWS_H)
set(RTWIZARD_HAVE_QT 0)
if(BRLCAD_ENABLE_QT AND TARGET libqtcad)
find_package_qt(REQUIRED)
if(Qt6Widgets_FOUND)
qt6_wrap_cpp(RTWIZARD_MOC_SRCS gui.h)
else()
qt5_wrap_cpp(RTWIZARD_MOC_SRCS gui.h)
endif()
list(APPEND RTWIZARD_SRCS gui.cpp ${RTWIZARD_MOC_SRCS})
set(RTWIZARD_HAVE_QT 1)
endif()
brlcad_addexec(rtwizard "${RTWIZARD_SRCS}" "libtclcad;librt;libdm;liboptical;libicv;Threads::Threads;${M_LIBRARY}")
target_include_directories(rtwizard BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_target_deps(rtwizard rt rtedge fbserv fb-png fb-pix fbclear fblabel fbfree fbline fb-fb pix-bw bwmod bw-pix pixmatte pix-fb tkpng TclIndexBld dm_plugins)
if(HAVE_WINDOWS_H)
# To associate an icon with the application for Windows (needed
# for the WiX installer) we must use an rc file.
enable_language(RC)
set(RTWIZARD_SRCS ${RTWIZARD_SRCS} rtwizard.rc)
endif(HAVE_WINDOWS_H)
# rtwizard needs the tclIndex and pkgIndex files set up correctly
add_dependencies(rtwizard TclIndexBld)
brlcad_addexec(rtwizard "${RTWIZARD_SRCS}" "libged;librt;libbv;libdm;libpkg;liboptical;libicv;Threads::Threads;${M_LIBRARY}")
target_include_directories(rtwizard BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(rtwizard PRIVATE RTWIZARD_HAVE_GUI=${RTWIZARD_HAVE_QT})
if(RTWIZARD_HAVE_QT)
if(Qt6Widgets_FOUND)
target_link_libraries(rtwizard libqtcad Qt6::Widgets)
else()
target_link_libraries(rtwizard libqtcad Qt5::Widgets)
endif()
endif()
add_target_deps(rtwizard rt rtedge fbserv dm_plugins ged-draw ged-view ged-get_eyemodel)
# RtWizard is one of the programs that gets a start menu entry
set_property(
INSTALL "${BIN_DIR}/$<TARGET_FILE_NAME:rtwizard>"
PROPERTY CPACK_START_MENU_SHORTCUTS "RtWizard ${BRLCAD_VERSION}"
)
set_property(
INSTALL "${BIN_DIR}/$<TARGET_FILE_NAME:rtwizard>"
PROPERTY CPACK_DESKTOP_SHORTCUTS "RtWizard ${BRLCAD_VERSION}"
)
endif(TARGET libtclcad)
# RtWizard is one of the programs that gets a start menu entry
set_property(
INSTALL "${BIN_DIR}/$<TARGET_FILE_NAME:rtwizard>"
PROPERTY CPACK_START_MENU_SHORTCUTS "RtWizard ${BRLCAD_VERSION}"
)
set_property(
INSTALL "${BIN_DIR}/$<TARGET_FILE_NAME:rtwizard>"
PROPERTY CPACK_DESKTOP_SHORTCUTS "RtWizard ${BRLCAD_VERSION}"
)
cmakefiles(
CMakeLists.txt
animation.cpp
animation.h
gui.cpp
gui.h
main.c
README.md
render.cpp
render.h
rtwizard.rc
settings.h
rtwizard.ico
)
+27
View File
@@ -0,0 +1,27 @@
# RtWizard architecture
RtWizard is a C++17 application with one rendering implementation shared by
its command-line and optional Qt interfaces. It has no Tcl/Tk runtime
dependency.
- `main.c` owns option parsing, command-line compatibility, and expansion of
declarative render specifications.
- `animation.cpp` validates versioned JSON, converts specifications to normal
options, evaluates typed animation tracks, and safely updates camera
keyframes.
- `render.cpp` resolves views and model bounds with librt/libged, launches
`rt` and `rtedge` through libbu, composites layers with libicv, and writes
stills, numbered frames, APNG, or MJPEG output.
- `gui.cpp` is a libqtcad/Qt workspace. It edits the same version 1 render
specification consumed by headless mode and starts a `--no-gui` child job,
keeping render failures and cancellation isolated from the interface.
Qt is optional at build time. A build without Qt still produces the complete
headless renderer; an incomplete invocation reports that the graphical
interface is unavailable. The retired Tcl source remains under
`src/tclscripts/rtwizard` for migration reference but is neither indexed nor
installed.
The GUI never evaluates `.rtwizardrc`. On first launch it recognizes only the
old numeric window-size and pane-position assignments, then stores settings in
`QSettings`.
+928
View File
@@ -0,0 +1,928 @@
/* A N I M A T I O N . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* Parse complete rtwizard render specifications and evaluate their typed
* animation tracks. This file deliberately produces data, not rt command
* text supplied by the user: only commands assembled from validated fields
* leave this module.
*/
#include "common.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#include "bio.h"
#include "vmath.h"
#include "bn/mat.h"
#include "bn/qmath.h"
#include "bu/malloc.h"
#include "bu/process.h"
#include "bu/str.h"
#include "bu/units.h"
#include "../libbu/json.hpp"
#include "animation.h"
using json = nlohmann::json;
namespace {
namespace fs = std::filesystem;
static bool
publish_file(const fs::path &temporary, const fs::path &destination)
{
#ifdef HAVE_WINDOWS_H
return MoveFileEx(temporary.string().c_str(), destination.string().c_str(),
MOVEFILE_WRITE_THROUGH | MOVEFILE_REPLACE_EXISTING) != 0;
#else
std::error_code error;
fs::rename(temporary, destination, error);
return !error;
#endif
}
struct JKey {
double time;
json value;
};
static bool
finite_number(const json &v)
{
return v.is_number() && std::isfinite(v.get<double>());
}
static void
check_keys(const json &j, const std::vector<std::string> &allowed, const std::string &where)
{
if (!j.is_object())
throw std::runtime_error(where + " must be an object");
for (json::const_iterator i = j.begin(); i != j.end(); ++i) {
if (std::find(allowed.begin(), allowed.end(), i.key()) == allowed.end())
throw std::runtime_error(where + ": unknown field '" + i.key() + "'");
}
}
static std::vector<double>
number_array(const json &v, size_t count, const std::string &where)
{
if (!v.is_array() || v.size() != count)
throw std::runtime_error(where + " must contain exactly " + std::to_string(count) + " numbers");
std::vector<double> out(count);
for (size_t i = 0; i < count; ++i) {
if (!finite_number(v[i]))
throw std::runtime_error(where + " contains a non-finite value");
out[i] = v[i].get<double>();
}
return out;
}
static std::vector<JKey>
property_keys(const json &track, const char *property, double duration)
{
std::vector<JKey> out;
if (!track.contains("keyframes") || !track["keyframes"].is_array())
throw std::runtime_error("track keyframes must be an array");
for (const json &k : track["keyframes"]) {
check_keys(k, {"time", "value", "eye", "look_at", "orientation", "up",
"view_size", "perspective", "point", "normal", "translation",
"rotation", "scale", "matrix", "color", "temperature", "material"},
"keyframe");
if (!k.contains("time") || !finite_number(k["time"]))
throw std::runtime_error("keyframe time must be finite");
double t = k["time"].get<double>();
if (t < 0.0 || t > duration)
throw std::runtime_error("keyframe time is outside the animation duration");
if (k.contains(property))
out.push_back({t, k[property]});
else if (std::string(property) == "value" && k.contains("value"))
out.push_back({t, k["value"]});
}
std::sort(out.begin(), out.end(), [](const JKey &a, const JKey &b) { return a.time < b.time; });
for (size_t i = 1; i < out.size(); ++i) {
if (std::fabs(out[i].time - out[i-1].time) <= std::numeric_limits<double>::epsilon())
throw std::runtime_error(std::string("duplicate keyframe time for '") + property + "'");
}
return out;
}
static std::string
interpolation_for(const json &track, const char *property)
{
if (!track.contains("interpolation"))
return "linear";
const json &in = track["interpolation"];
std::string result;
if (in.is_string())
result = in.get<std::string>();
else if (in.is_object() && in.contains(property) && in[property].is_string())
result = in[property].get<std::string>();
else if (in.is_object() && in.contains("default") && in["default"].is_string())
result = in["default"].get<std::string>();
else
result = "linear";
if (result != "step" && result != "linear" && result != "smooth")
throw std::runtime_error("interpolation must be step, linear, or smooth");
return result;
}
static void
validate_track(const json &track, const std::string &type)
{
std::vector<std::string> properties;
if (type == "camera") {
check_keys(track, {"type", "name", "interpolation", "keyframes"}, "camera track");
properties = {"eye", "look_at", "up", "orientation", "view_size", "perspective"};
} else if (type == "cut_plane") {
check_keys(track, {"type", "name", "interpolation", "keyframes"}, "cut_plane track");
properties = {"point", "normal"};
} else if (type == "transform") {
check_keys(track, {"type", "name", "path", "operation", "pivot", "interpolation", "keyframes"}, "transform track");
properties = {"translation", "rotation", "scale", "matrix"};
} else if (type == "material") {
check_keys(track, {"type", "name", "path", "operation", "interpolation", "keyframes"}, "material track");
properties = {"value", "material"};
} else if (type == "color") {
check_keys(track, {"type", "name", "path", "interpolation", "keyframes"}, "color track");
properties = {"value", "color"};
} else if (type == "temperature") {
check_keys(track, {"type", "name", "path", "interpolation", "keyframes"}, "temperature track");
properties = {"value", "temperature"};
} else {
throw std::runtime_error("unknown animation track type '" + type + "'");
}
if (!track.contains("keyframes") || !track["keyframes"].is_array())
throw std::runtime_error(type + " track keyframes must be an array");
std::vector<std::string> key_fields = {"time"};
key_fields.insert(key_fields.end(), properties.begin(), properties.end());
bool has_value = false;
for (const json &key : track["keyframes"]) {
check_keys(key, key_fields, type + " keyframe");
if (key.size() > 1) has_value = true;
}
if (!has_value) throw std::runtime_error(type + " track contains no animated values");
if (track.contains("interpolation") && track["interpolation"].is_string()) {
std::string choice = track["interpolation"].get<std::string>();
if (choice != "step" && choice != "linear" && choice != "smooth")
throw std::runtime_error(type + " interpolation must be step, linear, or smooth");
} else if (track.contains("interpolation") && track["interpolation"].is_object()) {
std::vector<std::string> interpolation_fields = {"default"};
interpolation_fields.insert(interpolation_fields.end(), properties.begin(), properties.end());
check_keys(track["interpolation"], interpolation_fields, type + " interpolation");
for (json::const_iterator i = track["interpolation"].begin(); i != track["interpolation"].end(); ++i) {
if (!i.value().is_string()) throw std::runtime_error(type + " interpolation choices must be strings");
std::string choice = i.value().get<std::string>();
if (choice != "step" && choice != "linear" && choice != "smooth")
throw std::runtime_error(type + " interpolation must be step, linear, or smooth");
}
} else if (track.contains("interpolation")) {
throw std::runtime_error(type + " interpolation must be a string or object");
}
}
static size_t
key_segment(const std::vector<JKey> &keys, double t)
{
if (keys.size() < 2 || t <= keys.front().time)
return 0;
for (size_t i = 0; i + 1 < keys.size(); ++i) {
if (t <= keys[i+1].time)
return i;
}
return keys.size() - 2;
}
static json
json_lerp(const json &a, const json &b, double f)
{
if (finite_number(a) && finite_number(b))
return a.get<double>() + (b.get<double>() - a.get<double>()) * f;
if (a.is_array() && b.is_array() && a.size() == b.size()) {
json out = json::array();
for (size_t i = 0; i < a.size(); ++i)
out.push_back(json_lerp(a[i], b[i], f));
return out;
}
throw std::runtime_error("linear interpolation requires matching numeric values");
}
static json
json_smooth(const json &p0, const json &p1, const json &p2, const json &p3, double f)
{
if (finite_number(p0) && finite_number(p1) && finite_number(p2) && finite_number(p3)) {
double a = p0.get<double>(), b = p1.get<double>();
double c = p2.get<double>(), d = p3.get<double>();
double f2 = f*f, f3 = f2*f;
return 0.5 * ((2.0*b) + (-a+c)*f + (2.0*a-5.0*b+4.0*c-d)*f2 + (-a+3.0*b-3.0*c+d)*f3);
}
if (p0.is_array() && p1.is_array() && p2.is_array() && p3.is_array() &&
p0.size() == p1.size() && p1.size() == p2.size() && p2.size() == p3.size()) {
json out = json::array();
for (size_t i = 0; i < p0.size(); ++i)
out.push_back(json_smooth(p0[i], p1[i], p2[i], p3[i], f));
return out;
}
throw std::runtime_error("smooth interpolation requires matching numeric values");
}
static json
evaluate_keys(const std::vector<JKey> &keys, double t, const std::string &interp)
{
if (keys.empty())
return json();
if (keys.size() == 1 || t <= keys.front().time)
return keys.front().value;
if (t >= keys.back().time)
return keys.back().value;
size_t i = key_segment(keys, t);
if (interp == "step")
return keys[i].value;
double span = keys[i+1].time - keys[i].time;
double f = span > 0.0 ? (t - keys[i].time) / span : 0.0;
if (interp == "smooth") {
const json &p0 = keys[i ? i-1 : i].value;
const json &p3 = keys[(i+2 < keys.size()) ? i+2 : i+1].value;
return json_smooth(p0, keys[i].value, keys[i+1].value, p3, f);
}
return json_lerp(keys[i].value, keys[i+1].value, f);
}
static void
normalize_quat(std::vector<double> &q)
{
double m = std::sqrt(q[0]*q[0] + q[1]*q[1] + q[2]*q[2] + q[3]*q[3]);
if (!(m > 1.0e-12))
throw std::runtime_error("orientation quaternion must be nonzero");
for (double &v : q) v /= m;
}
static std::vector<double>
rotation_quat(const json &v, const std::string &where)
{
std::vector<double> q(4);
if (v.is_array()) {
q = number_array(v, 4, where);
} else if (v.is_object() && v.contains("quaternion")) {
check_keys(v, {"quaternion"}, where);
q = number_array(v["quaternion"], 4, where + ".quaternion");
} else if (v.is_object() && v.contains("axis") && v.contains("degrees")) {
check_keys(v, {"axis", "degrees"}, where);
std::vector<double> axis = number_array(v["axis"], 3, where + ".axis");
if (!finite_number(v["degrees"]))
throw std::runtime_error(where + ".degrees must be finite");
double m = std::sqrt(axis[0]*axis[0] + axis[1]*axis[1] + axis[2]*axis[2]);
if (!(m > 1.0e-12))
throw std::runtime_error(where + ".axis must be nonzero");
double a = v["degrees"].get<double>() * DEG2RAD * 0.5;
double s = std::sin(a) / m;
q = {axis[0]*s, axis[1]*s, axis[2]*s, std::cos(a)};
} else {
throw std::runtime_error(where + " must be a quaternion array or axis/degrees object");
}
normalize_quat(q);
return q;
}
static std::vector<double>
evaluate_quat(const std::vector<JKey> &keys, double t, const std::string &interp)
{
if (keys.empty()) return {};
if (keys.size() == 1 || t <= keys.front().time)
return rotation_quat(keys.front().value, "rotation");
if (t >= keys.back().time)
return rotation_quat(keys.back().value, "rotation");
size_t i = key_segment(keys, t);
if (interp == "step")
return rotation_quat(keys[i].value, "rotation");
double f = (t - keys[i].time) / (keys[i+1].time - keys[i].time);
std::vector<double> a = rotation_quat(keys[i].value, "rotation");
std::vector<double> b = rotation_quat(keys[i+1].value, "rotation");
quat_t qa = {a[0], a[1], a[2], a[3]};
quat_t qb = {b[0], b[1], b[2], b[3]};
quat_t qo;
quat_slerp(qo, qa, qb, f);
return {qo[0], qo[1], qo[2], qo[3]};
}
static std::string
command_path(const json &track, const std::string &type)
{
std::string path = track.value("path", "");
if (path.empty()) throw std::runtime_error(type + " path may not be empty");
if (path.find_first_of(";\r\n\t ") != std::string::npos)
throw std::runtime_error(type + " path may not contain whitespace or command separators");
return path;
}
static std::string
material_string(const json &value)
{
if (value.is_string()) return value.get<std::string>();
check_keys(value, {"shader", "parameters"}, "structured material");
if (!value.contains("shader") || !value["shader"].is_string() || value["shader"].get<std::string>().empty())
throw std::runtime_error("structured material requires a nonempty shader string");
std::string shader = value["shader"].get<std::string>();
if (shader.find_first_of(";\r\n\t ") != std::string::npos)
throw std::runtime_error("structured material shader must be one command word");
std::ostringstream ss;
ss.precision(17);
ss << shader;
if (value.contains("parameters")) {
if (!value["parameters"].is_object())
throw std::runtime_error("structured material parameters must be an object");
for (json::const_iterator i = value["parameters"].begin(); i != value["parameters"].end(); ++i) {
if (i.key().empty() || i.key().find_first_of("=;\r\n\t ") != std::string::npos)
throw std::runtime_error("structured material parameter names must be command words");
ss << ' ' << i.key() << '=';
if (finite_number(i.value())) ss << i.value().get<double>();
else if (i.value().is_boolean()) ss << (i.value().get<bool>() ? 1 : 0);
else if (i.value().is_string()) {
std::string parameter = i.value().get<std::string>();
if (parameter.find_first_of(";\r\n\t ") != std::string::npos)
throw std::runtime_error("structured material string parameters may not contain whitespace or command separators");
ss << parameter;
} else {
throw std::runtime_error("structured material parameters must be finite numbers, booleans, or strings");
}
}
}
return ss.str();
}
static std::string
matrix_command(const json &track, double t, double duration, double local2base)
{
std::string path = command_path(track, "transform");
std::string op = track.value("operation", "right_multiply");
if (op == "replace_stack") op = "rstack";
else if (op == "replace_arc") op = "rarc";
else if (op == "left_multiply") op = "lmul";
else if (op == "right_multiply") op = "rmul";
else if (op == "replace_both") op = "rboth";
else throw std::runtime_error("unknown transform operation");
std::vector<JKey> mk = property_keys(track, "matrix", duration);
std::vector<JKey> tk = property_keys(track, "translation", duration);
std::vector<JKey> rk = property_keys(track, "rotation", duration);
std::vector<JKey> sk = property_keys(track, "scale", duration);
if (!mk.empty() && (!tk.empty() || !rk.empty() || !sk.empty()))
throw std::runtime_error("transform track may not mix matrix and decomposed values");
mat_t mat;
MAT_IDN(mat);
if (!mk.empty()) {
json mv = evaluate_keys(mk, t, "step");
std::vector<double> m = number_array(mv, 16, "transform matrix");
for (int i = 0; i < 16; ++i) mat[i] = m[(size_t)i];
mat[3] *= local2base; mat[7] *= local2base; mat[11] *= local2base;
} else {
std::vector<double> tr = {0, 0, 0};
std::vector<double> q = {0, 0, 0, 1};
double scale = 1.0;
if (!tk.empty()) tr = number_array(evaluate_keys(tk, t, interpolation_for(track, "translation")), 3, "translation");
if (!rk.empty()) q = evaluate_quat(rk, t, interpolation_for(track, "rotation"));
if (!sk.empty()) {
json sv = evaluate_keys(sk, t, interpolation_for(track, "scale"));
if (!finite_number(sv) || std::fabs(sv.get<double>()) < 1.0e-12)
throw std::runtime_error("transform scale must be finite and nonzero");
scale = sv.get<double>();
}
quat_t qq = {q[0], q[1], q[2], q[3]};
quat_quat2mat(mat, qq);
mat[15] = 1.0 / scale;
if (track.contains("pivot")) {
std::vector<double> pv = number_array(track["pivot"], 3, "transform pivot");
point_t p = {pv[0]*local2base, pv[1]*local2base, pv[2]*local2base};
mat_t about;
bn_mat_xform_about_pnt(about, mat, p);
MAT_COPY(mat, about);
}
mat[3] += tr[0]*local2base; mat[7] += tr[1]*local2base; mat[11] += tr[2]*local2base;
}
std::ostringstream ss;
ss.precision(17);
ss << "anim " << path << " matrix " << op;
for (int i = 0; i < 16; ++i) ss << ' ' << mat[i];
return ss.str();
}
static std::string
value_command(const json &track, double t, double duration)
{
std::string type = track.value("type", "");
std::string path = command_path(track, type);
std::vector<JKey> keys = property_keys(track, "value", duration);
if (keys.empty()) {
const char *prop = type == "color" ? "color" : (type == "temperature" ? "temperature" : "material");
keys = property_keys(track, prop, duration);
}
if (keys.empty()) throw std::runtime_error(type + " track has no values");
std::ostringstream ss;
ss.precision(17);
if (type == "color") {
json v = evaluate_keys(keys, t, interpolation_for(track, "color"));
std::vector<double> rgb = number_array(v, 3, "color");
ss << "anim " << path << " color";
for (double c : rgb) {
if (c < 0.0 || c > 255.0) throw std::runtime_error("color channels must be in [0,255]");
ss << ' ' << (int)std::lround(c);
}
} else if (type == "temperature") {
json v = evaluate_keys(keys, t, interpolation_for(track, "temperature"));
if (!finite_number(v)) throw std::runtime_error("temperature must be numeric");
ss << "anim " << path << " temperature " << v.get<double>();
} else {
json v = evaluate_keys(keys, t, "step");
std::string op = track.value("operation", "replace");
if (op != "replace" && op != "append") throw std::runtime_error("material operation must be replace or append");
std::string material = material_string(v);
if (material.find(';') != std::string::npos || material.find('\n') != std::string::npos || material.find('\r') != std::string::npos)
throw std::runtime_error("material value may not contain command separators");
ss << "anim " << path << " material " << op << ' ' << material;
}
return ss.str();
}
static void
evaluate_animation_impl(const std::string &path, double base_viewsize,
const std::array<double, 4> &base_q_in,
const std::array<double, 3> &base_eye_in, double base_perspective,
double duration_override, int fps_override, int frames_override,
int plays_override, int cyclic_override, double local2base,
::rtwizard::AnimationPlan &plan)
{
std::ifstream in(path);
if (!in) throw std::runtime_error("unable to open animation file '" + path + "'");
json root;
in >> root;
if (root.value("schema", "") == "brlcad.rtwizard.render") {
check_keys(root, {"schema", "version", "name", "description", "database", "objects", "image", "output",
"view", "style", "runtime", "animation"}, "render specification");
if (root.value("version", 0) != 1 || !root.contains("animation"))
throw std::runtime_error("render specification has no version 1 animation section");
root = root["animation"];
} else {
check_keys(root, {"schema", "version", "units", "timing", "tracks"}, "animation");
if (root.value("schema", "") != "brlcad.rtwizard.animation" || root.value("version", 0) != 1)
throw std::runtime_error("unsupported animation schema or version");
}
check_keys(root, {"schema", "version", "preset", "units", "timing", "options", "tracks"}, "animation");
if (root.contains("preset")) throw std::runtime_error("animation track files may not contain a preset");
if (root.contains("options")) throw std::runtime_error("animation options are only valid with a preset");
if (root.contains("units")) {
if (!root["units"].is_string()) throw std::runtime_error("animation units must be a string");
std::string units = root["units"].get<std::string>();
if (units != "database") {
local2base = bu_units_conversion(units.c_str());
if (!(local2base > 0.0)) throw std::runtime_error("unknown animation distance unit '" + units + "'");
}
}
if (!root.contains("timing") || !root["timing"].is_object())
throw std::runtime_error("animation timing is required");
check_keys(root["timing"], {"duration", "fps", "frames", "cyclic", "plays"}, "timing");
const double duration = duration_override > 0.0 ? duration_override : root["timing"].value("duration", 0.0);
const int fps = fps_override > 0 ? fps_override : root["timing"].value("fps", 10);
const bool cyclic = cyclic_override >= 0 ? cyclic_override != 0 : root["timing"].value("cyclic", false);
const int plays = plays_override >= 0 ? plays_override : root["timing"].value("plays", cyclic ? 0 : 1);
if (!(duration > 0.0) || !std::isfinite(duration) || fps <= 0 || plays < 0)
throw std::runtime_error("duration and fps must be positive and plays nonnegative");
const int specified_frames = frames_override > 0 ? frames_override : root["timing"].value("frames", 0);
const int frame_count = specified_frames > 0 ? specified_frames :
static_cast<int>(std::llround(duration * fps)) + (cyclic ? 0 : 1);
if (frame_count < 2) throw std::runtime_error("animation must contain at least two frames");
if (!root.contains("tracks") || !root["tracks"].is_array() || root["tracks"].empty())
throw std::runtime_error("animation tracks must be a nonempty array");
plan = ::rtwizard::AnimationPlan();
plan.duration = duration;
plan.fps = fps;
plan.cyclic = cyclic;
plan.plays = plays;
plan.frames.reserve(static_cast<size_t>(frame_count));
for (int fi = 0; fi < frame_count; ++fi) {
const double denom = cyclic ? static_cast<double>(frame_count) : static_cast<double>(frame_count - 1);
const double time = duration * static_cast<double>(fi) / denom;
std::vector<double> eye(base_eye_in.begin(), base_eye_in.end());
std::vector<double> orient(base_q_in.begin(), base_q_in.end());
std::vector<double> look;
std::vector<double> up;
double viewsize = base_viewsize;
double perspective = base_perspective;
std::string cut;
std::vector<std::string> commands;
for (const json &track : root["tracks"]) {
const std::string type = track.value("type", "");
validate_track(track, type);
if (type == "camera") {
std::vector<JKey> ek = property_keys(track, "eye", duration);
std::vector<JKey> lk = property_keys(track, "look_at", duration);
std::vector<JKey> uk = property_keys(track, "up", duration);
std::vector<JKey> ok = property_keys(track, "orientation", duration);
if (!lk.empty() && !ok.empty()) throw std::runtime_error("camera track may not mix look_at and orientation");
if (!uk.empty() && lk.empty()) throw std::runtime_error("camera up requires look_at values in the same track");
if (!ek.empty()) {
eye = number_array(evaluate_keys(ek, time, interpolation_for(track, "eye")), 3, "camera eye");
for (double &value : eye) value *= local2base;
}
if (!lk.empty()) {
look = number_array(evaluate_keys(lk, time, interpolation_for(track, "look_at")), 3, "camera look_at");
for (double &value : look) value *= local2base;
up.clear();
}
if (!uk.empty()) up = number_array(evaluate_keys(uk, time, interpolation_for(track, "up")), 3, "camera up");
if (!ok.empty()) {
orient = evaluate_quat(ok, time, interpolation_for(track, "orientation"));
look.clear();
up.clear();
}
std::vector<JKey> vk = property_keys(track, "view_size", duration);
if (!vk.empty()) {
json value = evaluate_keys(vk, time, interpolation_for(track, "view_size"));
if (!finite_number(value) || value.get<double>() <= 0.0) throw std::runtime_error("view_size must be positive");
viewsize = value.get<double>() * local2base;
}
std::vector<JKey> pk = property_keys(track, "perspective", duration);
if (!pk.empty()) {
json value = evaluate_keys(pk, time, interpolation_for(track, "perspective"));
if (!finite_number(value)) throw std::runtime_error("perspective must be finite");
perspective = value.get<double>();
}
} else if (type == "cut_plane") {
std::vector<JKey> pk = property_keys(track, "point", duration);
std::vector<JKey> nk = property_keys(track, "normal", duration);
if (pk.empty() || nk.empty()) throw std::runtime_error("cut_plane requires point and normal values");
std::vector<double> point = number_array(evaluate_keys(pk, time, interpolation_for(track, "point")), 3, "cut point");
std::vector<double> normal = number_array(evaluate_keys(nk, time, interpolation_for(track, "normal")), 3, "cut normal");
const double magnitude = std::sqrt(normal[0]*normal[0] + normal[1]*normal[1] + normal[2]*normal[2]);
if (!(magnitude > 1.0e-12)) throw std::runtime_error("cut normal must be nonzero");
std::ostringstream stream;
stream.precision(17);
stream << point[0]*local2base << ',' << point[1]*local2base << ',' << point[2]*local2base << ','
<< normal[0]/magnitude << ',' << normal[1]/magnitude << ',' << normal[2]/magnitude;
cut = stream.str();
} else if (type == "transform") {
commands.push_back(matrix_command(track, time, duration, local2base));
} else if (type == "material" || type == "color" || type == "temperature") {
commands.push_back(value_command(track, time, duration));
} else {
throw std::runtime_error("unknown animation track type '" + type + "'");
}
}
if (!look.empty()) {
vect_t direction = {look[0]-eye[0], look[1]-eye[1], look[2]-eye[2]};
if (MAGNITUDE(direction) <= SMALL_FASTF) throw std::runtime_error("camera eye and look_at point coincide");
VUNITIZE(direction);
mat_t matrix;
quat_t quaternion;
if (up.empty()) {
bn_mat_lookat(matrix, direction, 0);
} else {
vect_t up_vector = {up[0], up[1], up[2]};
vect_t right, camera_up;
if (MAGNITUDE(up_vector) <= SMALL_FASTF) throw std::runtime_error("camera up vector must be nonzero");
VUNITIZE(up_vector);
VCROSS(right, direction, up_vector);
if (MAGNITUDE(right) <= SMALL_FASTF) throw std::runtime_error("camera up vector is parallel to the viewing direction");
VUNITIZE(right);
VCROSS(camera_up, right, direction);
MAT_IDN(matrix);
VMOVE(&matrix[0], right);
VMOVE(&matrix[4], camera_up);
VREVERSE(&matrix[8], direction);
}
quat_mat2quat(quaternion, matrix);
orient = {quaternion[0], quaternion[1], quaternion[2], quaternion[3]};
}
::rtwizard::AnimationFrame frame;
frame.time = time;
frame.view_size = viewsize;
std::copy(orient.begin(), orient.end(), frame.orientation.begin());
std::copy(eye.begin(), eye.end(), frame.eye.begin());
frame.perspective = perspective;
frame.cut_plane = cut;
frame.commands = std::move(commands);
plan.frames.push_back(std::move(frame));
}
}
} // namespace
bool
rtwizard::evaluate_animation(const std::string &path, double base_view_size,
const std::array<double, 4> &base_orientation,
const std::array<double, 3> &base_eye, double base_perspective,
double duration_override, int fps_override, int frames_override,
int plays_override, int cyclic_override, double local_to_base,
AnimationPlan &plan, std::string &error)
{
try {
evaluate_animation_impl(path, base_view_size, base_orientation, base_eye,
base_perspective, duration_override, fps_override, frames_override,
plays_override, cyclic_override, local_to_base, plan);
error.clear();
return true;
} catch (const std::exception &exception) {
error = exception.what();
return false;
}
}
bool
rtwizard::save_view_keyframe(const std::string &path, double time, bool replace,
const std::array<double, 3> &eye,
const std::array<double, 4> &orientation,
double view_size, double perspective, double database_local_to_base,
std::string &error)
{
try {
if (time < 0.0 || !std::isfinite(time) || !(view_size > 0.0) || !std::isfinite(view_size) ||
!std::isfinite(perspective))
throw std::runtime_error("keyframe time must be nonnegative and view size positive");
for (double value : eye) if (!std::isfinite(value)) throw std::runtime_error("eye contains a non-finite value");
for (double value : orientation) if (!std::isfinite(value)) throw std::runtime_error("orientation contains a non-finite value");
json root;
{
std::ifstream input(path);
if (input) input >> root;
}
if (root.is_null()) root = {{"schema", "brlcad.rtwizard.render"}, {"version", 1}};
if (root.value("schema", "") != "brlcad.rtwizard.render" || root.value("version", 0) != 1)
throw std::runtime_error("view keyframes may only be saved to a version 1 rtwizard render specification");
json &animation = root["animation"];
if (!animation.is_object()) animation = json::object();
if (!animation.contains("units")) animation["units"] = "mm";
if (!animation["units"].is_string())
throw std::runtime_error("animation units must be a string");
const std::string units = animation["units"].get<std::string>();
double unit_to_base = units == "database" ? database_local_to_base : bu_units_conversion(units.c_str());
if (!(unit_to_base > 0.0) || !std::isfinite(unit_to_base))
throw std::runtime_error("unknown animation distance unit '" + units + "'");
if (animation.contains("preset"))
throw std::runtime_error("replace the render specification's animation preset before adding camera keyframes");
json &timing = animation["timing"];
if (!timing.is_object()) timing = json::object();
timing["duration"] = std::max(time, timing.value("duration", time > 0.0 ? time : 1.0));
if (!timing.contains("fps")) timing["fps"] = 10;
if (!timing.contains("cyclic")) timing["cyclic"] = false;
json &tracks = animation["tracks"];
if (!tracks.is_array()) tracks = json::array();
size_t camera_index = tracks.size();
for (size_t index = 0; index < tracks.size(); ++index) {
if (tracks[index].is_object() && tracks[index].value("type", "") == "camera") {
camera_index = index;
break;
}
}
if (camera_index == tracks.size())
tracks.push_back({{"type", "camera"}, {"interpolation", "smooth"}, {"keyframes", json::array()}});
json &keys = tracks[camera_index]["keyframes"];
if (!keys.is_array()) keys = json::array();
json key = {{"time", time},
{"eye", {eye[0] / unit_to_base, eye[1] / unit_to_base, eye[2] / unit_to_base}},
{"orientation", orientation}, {"view_size", view_size / unit_to_base},
{"perspective", perspective}};
bool found = false;
for (json &existing : keys) {
if (existing.is_object() && existing.contains("time") && existing["time"].is_number() &&
std::fabs(existing["time"].get<double>() - time) <= std::numeric_limits<double>::epsilon()) {
if (!replace) throw std::runtime_error("a camera keyframe already exists at that time; use --replace-keyframe");
existing = key;
found = true;
break;
}
}
if (!found) keys.push_back(key);
std::sort(keys.begin(), keys.end(), [](const json &left, const json &right) {
return left.value("time", 0.0) < right.value("time", 0.0);
});
const fs::path destination(path);
const fs::path temporary = destination.parent_path() /
(destination.filename().string() + ".tmp-" + std::to_string(bu_pid()));
{
std::ofstream output(temporary, std::ios::trunc);
if (!output) throw std::runtime_error("unable to create temporary keyframe file");
output << root.dump(2) << '\n';
if (!output) throw std::runtime_error("unable to write keyframe file");
}
if (!publish_file(temporary, destination)) {
std::error_code ec;
fs::remove(temporary, ec);
throw std::runtime_error("unable to replace keyframe file");
}
error.clear();
return true;
} catch (const std::exception &exception) {
error = exception.what();
return false;
}
}
namespace {
static void
push_arg(std::vector<std::string> &args, const char *option, const std::string &value)
{
args.push_back(option);
args.push_back(value);
}
static std::string
json_arg(const json &v, const std::string &where)
{
if (v.is_string()) return v.get<std::string>();
if (v.is_number_float()) {
if (!finite_number(v)) throw std::runtime_error(where + " must be finite");
std::ostringstream ss; ss.precision(17); ss << v.get<double>(); return ss.str();
}
if (v.is_number_integer()) return std::to_string(v.get<long long>());
if (v.is_array()) {
std::ostringstream ss;
for (size_t i = 0; i < v.size(); ++i) {
if (!finite_number(v[i])) throw std::runtime_error(where + " must contain numbers");
if (i) ss << '/';
ss.precision(17); ss << v[i].get<double>();
}
return ss.str();
}
throw std::runtime_error(where + " has an unsupported value");
}
static std::string
file_arg(const json &v, const fs::path &base, const std::string &where)
{
if (!v.is_string() || v.get<std::string>().empty())
throw std::runtime_error(where + " must be a nonempty filename");
fs::path p(v.get<std::string>());
if (p.is_relative()) p = base / p;
return p.lexically_normal().string();
}
static bool
boolean_arg(const json &section, const char *key, const std::string &where)
{
if (!section.contains(key)) return false;
if (!section[key].is_boolean())
throw std::runtime_error(where + "." + key + " must be true or false");
return section[key].get<bool>();
}
static void
section_args(std::vector<std::string> &args, const json &section,
const std::vector<std::pair<std::string, std::string> > &mapping,
const std::string &where)
{
std::vector<std::string> allowed;
for (const auto &m : mapping) allowed.push_back(m.first);
check_keys(section, allowed, where);
for (const auto &m : mapping) {
if (section.contains(m.first)) push_arg(args, m.second.c_str(), json_arg(section[m.first], where + "." + m.first));
}
}
static std::vector<std::string>
render_spec_args(const char *path)
{
std::ifstream in(path);
if (!in) throw std::runtime_error(std::string("unable to open render specification '") + path + "'");
json root; in >> root;
check_keys(root, {"schema", "version", "name", "description", "database", "objects", "image", "output",
"view", "style", "runtime", "animation"}, "render specification");
if (root.value("schema", "") != "brlcad.rtwizard.render" || root.value("version", 0) != 1)
throw std::runtime_error("render specification must use brlcad.rtwizard.render version 1");
fs::path spec_path = fs::absolute(fs::path(path));
fs::path base = spec_path.parent_path();
std::vector<std::string> args;
if (root.contains("database")) push_arg(args, "--input-file", file_arg(root["database"], base, "database"));
if (root.contains("objects")) {
const json &o = root["objects"];
check_keys(o, {"color", "ghost", "line"}, "objects");
for (const auto &entry : {std::make_pair("color", "--color-objects"),
std::make_pair("ghost", "--ghost-objects"), std::make_pair("line", "--line-objects")}) {
if (!o.contains(entry.first)) continue;
if (!o[entry.first].is_array()) throw std::runtime_error(std::string("objects.") + entry.first + " must be an array");
/* An empty role is meaningful in a complete rendering specification,
* but the corresponding command-line option requires at least one
* object. Omitting the option preserves the empty role. */
if (o[entry.first].empty()) continue;
std::ostringstream ss;
for (size_t i = 0; i < o[entry.first].size(); ++i) {
if (!o[entry.first][i].is_string()) throw std::runtime_error("object names must be strings");
if (i) ss << ',';
ss << o[entry.first][i].get<std::string>();
}
push_arg(args, entry.second, ss.str());
}
}
if (root.contains("image")) section_args(args, root["image"], {
{"size", "--size"}, {"width", "--width"}, {"height", "--height"}, {"type", "--type"}}, "image");
if (root.contains("output")) {
const json &o = root["output"];
check_keys(o, {"file", "frame_dir", "framebuffer", "framebuffer_port", "framebuffer_transport", "resume"}, "output");
if (o.contains("file")) push_arg(args, "--output-file", file_arg(o["file"], base, "output.file"));
if (o.contains("frame_dir")) push_arg(args, "--frame-dir", file_arg(o["frame_dir"], base, "output.frame_dir"));
if (o.contains("framebuffer")) push_arg(args, "--fbserv-device", json_arg(o["framebuffer"], "output.framebuffer"));
if (o.contains("framebuffer_port")) push_arg(args, "--fbserv-port", json_arg(o["framebuffer_port"], "output.framebuffer_port"));
if (o.contains("framebuffer_transport")) push_arg(args, "--fbserv-transport", json_arg(o["framebuffer_transport"], "output.framebuffer_transport"));
if (boolean_arg(o, "resume", "output")) args.push_back("--resume");
}
if (root.contains("view")) section_args(args, root["view"], {
{"azimuth", "--azimuth"}, {"elevation", "--elevation"}, {"twist", "--twist"},
{"zoom", "--zoom"}, {"center", "--center"}, {"eye", "--eye_pt"},
{"view_size", "--viewsize"}, {"orientation", "--orientation"}, {"perspective", "--perspective"}}, "view");
if (root.contains("style")) section_args(args, root["style"], {
{"background", "--background-color"}, {"line_color", "--line-color"},
{"non_line_color", "--non-line-color"}, {"ghost_intensity", "--ghost-intensity"},
{"occlusion", "--occlusion"}, {"ao_samples", "--ao-samples"}, {"ao_radius", "--ao-radius"}}, "style");
if (root.contains("runtime")) {
const json &r = root["runtime"];
check_keys(r, {"benchmark", "cpu_count", "verbose", "log_file", "pid_file", "gui", "no_gui"}, "runtime");
if (boolean_arg(r, "benchmark", "runtime")) args.push_back("--benchmark");
if (r.contains("cpu_count")) push_arg(args, "--cpu-count", json_arg(r["cpu_count"], "runtime.cpu_count"));
if (r.contains("verbose")) push_arg(args, "--verbose", json_arg(r["verbose"], "runtime.verbose"));
if (r.contains("log_file")) push_arg(args, "--log-file", file_arg(r["log_file"], base, "runtime.log_file"));
if (r.contains("pid_file")) push_arg(args, "--pid-file", file_arg(r["pid_file"], base, "runtime.pid_file"));
if (boolean_arg(r, "gui", "runtime")) args.push_back("--gui");
if (boolean_arg(r, "no_gui", "runtime")) args.push_back("--no-gui");
}
if (root.contains("animation")) {
const json &a = root["animation"];
check_keys(a, {"schema", "version", "preset", "units", "timing", "options", "tracks"}, "animation");
if (!a.contains("preset") && !a.contains("tracks"))
throw std::runtime_error("animation requires a preset or tracks");
if (a.contains("options") && !a.contains("preset"))
throw std::runtime_error("animation options require a preset");
if (a.contains("timing")) {
const json &tm = a["timing"];
check_keys(tm, {"duration", "fps", "frames", "cyclic", "plays"}, "animation.timing");
if (tm.contains("duration")) push_arg(args, "--animation-duration", json_arg(tm["duration"], "animation.timing.duration"));
if (tm.contains("fps")) push_arg(args, "--animation-fps", json_arg(tm["fps"], "animation.timing.fps"));
if (tm.contains("frames")) push_arg(args, "--animation-frames", json_arg(tm["frames"], "animation.timing.frames"));
if (tm.contains("plays")) push_arg(args, "--animation-plays", json_arg(tm["plays"], "animation.timing.plays"));
if (tm.contains("cyclic")) {
if (!tm["cyclic"].is_boolean()) throw std::runtime_error("animation.timing.cyclic must be true or false");
push_arg(args, "--animation-cyclic", tm["cyclic"].get<bool>() ? "1" : "0");
}
}
if (a.contains("options")) section_args(args, a["options"], {
{"cut_direction", "--cut-direction"}, {"orbit_angle", "--orbit-angle"},
{"orbit_axis", "--orbit-axis"}, {"orbit_center", "--orbit-center"},
{"orbit_elevation", "--orbit-elevation"}, {"orbit_radius", "--orbit-radius"},
{"turntable_object", "--turntable-object"}, {"turntable_angle", "--turntable-angle"},
{"turntable_axis", "--turntable-axis"}, {"turntable_center", "--turntable-center"}}, "animation.options");
if (a.contains("preset")) push_arg(args, "--animation", json_arg(a["preset"], "animation.preset"));
if (a.contains("tracks")) {
if (a.contains("preset")) throw std::runtime_error("animation may contain a preset or tracks, not both");
if (a.contains("options")) throw std::runtime_error("animation options are only valid with a preset");
push_arg(args, "--animation-file", spec_path.string());
}
}
return args;
}
} // namespace
extern "C" int
rtwizard_spec_to_argv(const char *path, int *argc, char ***argv, char **errmsg)
{
if (!path || !argc || !argv) return -1;
try {
std::vector<std::string> a = render_spec_args(path);
*argc = (int)a.size();
*argv = (char **)bu_calloc(a.size() + 1, sizeof(char *), "rtwizard render spec argv");
for (size_t i = 0; i < a.size(); ++i) (*argv)[i] = bu_strdup(a[i].c_str());
if (errmsg) *errmsg = NULL;
return 0;
} catch (const std::exception &e) {
if (errmsg) *errmsg = bu_strdup(e.what());
return -1;
}
}
extern "C" void
rtwizard_spec_argv_free(int argc, char **argv)
{
if (!argv) return;
for (int i = 0; i < argc; ++i) bu_free(argv[i], "rtwizard render spec arg");
bu_free(argv, "rtwizard render spec argv");
}
+68
View File
@@ -0,0 +1,68 @@
/* A N I M A T I O N . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*/
#ifndef RTWIZARD_ANIMATION_H
#define RTWIZARD_ANIMATION_H
#ifdef __cplusplus
# include <array>
# include <string>
# include <vector>
namespace rtwizard {
struct AnimationFrame {
double time = 0.0;
double view_size = 0.0;
std::array<double, 4> orientation{{0.0, 0.0, 0.0, 1.0}};
std::array<double, 3> eye{{0.0, 0.0, 0.0}};
double perspective = 0.0;
std::string cut_plane;
std::vector<std::string> commands;
};
struct AnimationPlan {
double duration = 0.0;
int fps = 0;
bool cyclic = false;
int plays = 1;
std::vector<AnimationFrame> frames;
};
/* Evaluate a track-based version 1 animation without involving Tcl. */
bool evaluate_animation(const std::string &path, double base_view_size,
const std::array<double, 4> &base_orientation,
const std::array<double, 3> &base_eye, double base_perspective,
double duration_override, int fps_override, int frames_override,
int plays_override, int cyclic_override, double local_to_base,
AnimationPlan &plan, std::string &error);
/* Safely update the camera track in a render specification. */
bool save_view_keyframe(const std::string &path, double time, bool replace,
const std::array<double, 3> &eye,
const std::array<double, 4> &orientation,
double view_size, double perspective, double database_local_to_base,
std::string &error);
} // namespace rtwizard
#endif
#ifdef __cplusplus
extern "C" {
#endif
/* Convert a declarative render specification into ordinary rtwizard options.
* General animation tracks retain the specification path for later typed
* evaluation. The caller owns the returned vector. */
int rtwizard_spec_to_argv(const char *path, int *argc, char ***argv, char **errmsg);
void rtwizard_spec_argv_free(int argc, char **argv);
#ifdef __cplusplus
}
#endif
#endif /* RTWIZARD_ANIMATION_H */
+947
View File
@@ -0,0 +1,947 @@
/* G U I . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*/
#include "common.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <set>
#include <sstream>
#include <unordered_set>
#include <vector>
#include <QAction>
#include <QApplication>
#include <QCloseEvent>
#include <QComboBox>
#include <QCoreApplication>
#include <QDir>
#include <QDockWidget>
#include <QDoubleSpinBox>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QFormLayout>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QKeySequence>
#include <QMenuBar>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QProgressBar>
#include <QPushButton>
#include <QRegularExpression>
#include <QSettings>
#include <QSpinBox>
#include <QSplitter>
#include <QStackedLayout>
#include <QStackedWidget>
#include <QStandardPaths>
#include <QStatusBar>
#include <QTabWidget>
#include <QTableWidget>
#include <QTemporaryDir>
#include <QThread>
#include <QTimer>
#include <QToolBar>
#include <QVBoxLayout>
#include "bu/app.h"
#include "bu/process.h"
#include "bu/units.h"
#include "bu/vls.h"
#include "bv.h"
#include "qtcad/QgModel.h"
#include "qtcad/QgSignalFlags.h"
#include "qtcad/QgTreeView.h"
#include "qtcad/QgView.h"
#include "../libbu/json.hpp"
#include "../libged/dbi.h"
#include "gui.h"
#include "settings.h"
using json = nlohmann::json;
namespace fs = std::filesystem;
namespace {
static json
parse_document(const std::string &text)
{
if (text.empty()) return {{"schema", "brlcad.rtwizard.render"}, {"version", 1}};
return json::parse(text);
}
static QStringList
json_strings(const json &value)
{
QStringList result;
if (!value.is_array()) return result;
for (const json &entry : value) if (entry.is_string()) result << QString::fromStdString(entry.get<std::string>());
return result;
}
static json
string_array(const QStringList &values)
{
json result = json::array();
for (const QString &value : values) result.push_back(value.toStdString());
return result;
}
static QStringList
table_strings(const bu_ptbl *table)
{
QStringList result;
if (!table) return result;
for (size_t index = 0; index < BU_PTBL_LEN(table); ++index) {
const char *value = reinterpret_cast<const char *>(BU_PTBL_GET(table, index));
if (value && value[0]) result << QString::fromLocal8Bit(value);
}
return result;
}
static QString
color_text(const bu_color *color)
{
unsigned char rgb[3] = {0, 0, 0};
bu_color_to_rgb_chars(color, rgb);
return QStringLiteral("%1 %2 %3").arg(rgb[0]).arg(rgb[1]).arg(rgb[2]);
}
static json
vector_value(const QString &text, size_t expected)
{
QString normalized = text;
normalized.replace(',', ' ');
normalized.replace('/', ' ');
QStringList fields = normalized.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
if (static_cast<size_t>(fields.size()) != expected) throw std::runtime_error("expected " + std::to_string(expected) + " numeric values");
json result = json::array();
for (const QString &field : fields) {
bool ok = false;
double value = field.toDouble(&ok);
if (!ok || !std::isfinite(value)) throw std::runtime_error("vector contains an invalid number");
result.push_back(value);
}
return result;
}
static QString
vector_text(const json &value, const QString &fallback)
{
if (!value.is_array()) return fallback;
QStringList fields;
for (const json &entry : value) {
if (!entry.is_number()) return fallback;
fields << QString::number(entry.get<double>(), 'g', 15);
}
return fields.join(' ');
}
static QWidget *
pageWithForm(QFormLayout *&form, QWidget *parent)
{
QWidget *page = new QWidget(parent);
form = new QFormLayout(page);
form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
return page;
}
} // namespace
RtWizardRenderWorker::RtWizardRenderWorker(const QStringList &arguments) : m_arguments(arguments)
{
}
void
RtWizardRenderWorker::requestCancel()
{
m_cancelled.store(true);
}
void
RtWizardRenderWorker::run()
{
std::vector<QByteArray> encoded;
std::vector<const char *> argv;
for (const QString &argument : m_arguments) encoded.push_back(QFile::encodeName(argument));
for (const QByteArray &argument : encoded) argv.push_back(argument.constData());
argv.push_back(nullptr);
bu_process *process = nullptr;
bu_process_create(&process, argv.data(), BU_PROCESS_OUT_EQ_ERR | BU_PROCESS_HIDE_WINDOW);
if (!process) {
emit finished(1, tr("Unable to start rtwizard rendering process."));
return;
}
bu_process_file_close(process, BU_PROCESS_STDIN);
const int fd = bu_process_fileno(process, BU_PROCESS_STDOUT);
bool terminated = false;
while (bu_process_alive(process)) {
if (m_cancelled.load() && !terminated) {
bu_pid_terminate(bu_process_pid(process));
terminated = true;
}
if (bu_process_pending(fd)) {
char buffer[4096];
int count = bu_process_read_n(process, BU_PROCESS_STDOUT, sizeof(buffer), buffer);
if (count > 0) emit output(QString::fromLocal8Bit(buffer, count));
} else {
QThread::msleep(25);
}
}
while (bu_process_pending(fd)) {
char buffer[4096];
int count = bu_process_read_n(process, BU_PROCESS_STDOUT, sizeof(buffer), buffer);
if (count <= 0) break;
emit output(QString::fromLocal8Bit(buffer, count));
}
const int status = bu_process_wait_n(&process, 0);
emit finished(status, m_cancelled.load() ? tr("Render cancelled.") :
(status == 0 ? tr("Render complete.") : tr("Render failed with status %1.").arg(status)));
}
RtWizardMainWindow::RtWizardMainWindow(const QString &program,
const rtwizard_settings *settings, char pictureType) :
m_program(program)
{
setWindowTitle(tr("BRL-CAD RtWizard"));
setObjectName("RtWizardMainWindow");
createActions();
createWorkspace();
importLegacySettings();
loadSettings();
if (settings && bu_vls_strlen(settings->render_spec))
loadSpecification(QString::fromLocal8Bit(bu_vls_addr(settings->render_spec)));
applySettings(settings, pictureType);
updateValidation();
}
RtWizardMainWindow::~RtWizardMainWindow()
{
if (m_worker) m_worker->requestCancel();
if (m_renderThread) {
m_renderThread->quit();
m_renderThread->wait();
}
if (m_view && m_view->view() && m_view->view()->vset)
bv_set_rm_view(m_view->view()->vset, m_view->view());
}
void
RtWizardMainWindow::createActions()
{
QMenu *file = menuBar()->addMenu(tr("&File"));
QAction *openDb = file->addAction(tr("Open &Database..."), this, &RtWizardMainWindow::openDatabase);
openDb->setShortcut(QKeySequence::Open);
file->addAction(tr("Open Render &Specification..."), this, &RtWizardMainWindow::openSpecification);
file->addSeparator();
QAction *save = file->addAction(tr("&Save Specification"), this, &RtWizardMainWindow::saveSpecification);
save->setShortcut(QKeySequence::Save);
QAction *saveAs = file->addAction(tr("Save Specification &As..."), this, &RtWizardMainWindow::saveSpecificationAs);
saveAs->setShortcut(QKeySequence::SaveAs);
file->addSeparator();
QAction *exitAction = file->addAction(tr("E&xit"));
exitAction->setShortcut(QKeySequence::Quit);
connect(exitAction, &QAction::triggered, this, &QWidget::close);
m_imageMenu = menuBar()->addMenu(tr("&Image"));
m_imageMenu->addAction(tr("Select Image Type..."), this, [this]() { showSettingsPage(0); });
m_imageMenu->addSeparator();
const QStringList pictureTypes = {
tr("Type A — Full color"), tr("Type B — Line drawing"),
tr("Type C — Color with highlights"), tr("Type D — Mixed color and line"),
tr("Type E — Color with ghost"), tr("Type F — Color, line, and ghost")
};
for (int index = 0; index < pictureTypes.size(); ++index) {
m_imageMenu->addAction(pictureTypes[index], this, [this, index]() {
if (m_type) m_type->setCurrentIndex(index);
showSettingsPage(0);
});
}
m_stepsMenu = menuBar()->addMenu(tr("&Steps"));
m_stepsMenu->addAction(tr("Select Image Type"), this, [this]() { showSettingsPage(0); });
m_stepsMenu->addSeparator();
m_stepsMenu->addAction(tr("Configure Full-Color Elements"), this, [this]() { showRolePage(0); });
m_stepsMenu->addAction(tr("Configure Line-Drawing Elements"), this, [this]() { showRolePage(1); });
m_stepsMenu->addAction(tr("Configure Ghost Elements"), this, [this]() { showRolePage(2); });
m_stepsMenu->addSeparator();
m_stepsMenu->addAction(tr("Configure View"), this, [this]() { showSettingsPage(1); });
m_stepsMenu->addAction(tr("Configure Framebuffer and Output"), this, [this]() { showSettingsPage(2); });
m_stepsMenu->addAction(tr("Configure Animation"), this, [this]() { showSettingsPage(3); });
QMenu *render = menuBar()->addMenu(tr("&Render"));
QAction *previewAction = render->addAction(tr("&Preview"));
previewAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R));
connect(previewAction, &QAction::triggered, this, &RtWizardMainWindow::renderPreview);
QAction *fullAction = render->addAction(tr("Render &Full Size"));
fullAction->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_R));
connect(fullAction, &QAction::triggered, this, &RtWizardMainWindow::renderFull);
QAction *cancelAction = render->addAction(tr("&Cancel"));
cancelAction->setShortcut(QKeySequence(Qt::Key_Escape));
connect(cancelAction, &QAction::triggered, this, &RtWizardMainWindow::cancelRender);
QMenu *help = menuBar()->addMenu(tr("&Help"));
help->addAction(tr("About RtWizard"), this, [this]() {
QMessageBox::about(this, tr("About RtWizard"),
tr("BRL-CAD RtWizard prepares still images and animations using the native ray tracing tools."));
});
}
void
RtWizardMainWindow::createWorkspace()
{
QWidget *central = new QWidget(this);
QStackedLayout *stack = new QStackedLayout(central);
stack->setStackingMode(QStackedLayout::StackAll);
m_view = new QgView(central, QgView_SW);
m_view->enableDefaultKeyBindings();
m_view->enableDefaultMouseBindings();
stack->addWidget(m_view);
m_result = new QLabel(central);
m_result->setAlignment(Qt::AlignCenter);
m_result->setScaledContents(false);
m_result->setAttribute(Qt::WA_TransparentForMouseEvents);
m_result->hide();
stack->addWidget(m_result);
setCentralWidget(central);
m_rolesDock = new QDockWidget(tr("Configure Render Elements"), this);
m_rolesDock->setObjectName("ModelRolesDockV2");
m_treeContainer = new QWidget(m_rolesDock);
QVBoxLayout *treeLayout = new QVBoxLayout(m_treeContainer);
QLabel *hint = new QLabel(tr("Select an object, then assign one or more rendering roles."), m_treeContainer);
hint->setWordWrap(true);
treeLayout->addWidget(hint);
QWidget *treePlaceholder = new QWidget(m_treeContainer);
treePlaceholder->setObjectName("TreePlaceholder");
treeLayout->addWidget(treePlaceholder, 2);
QHBoxLayout *roleButtons = new QHBoxLayout;
roleButtons->addWidget(new QPushButton(tr("+ Color"), m_treeContainer));
roleButtons->addWidget(new QPushButton(tr("+ Line"), m_treeContainer));
roleButtons->addWidget(new QPushButton(tr("+ Ghost"), m_treeContainer));
treeLayout->addLayout(roleButtons);
connect(qobject_cast<QPushButton *>(roleButtons->itemAt(0)->widget()), &QPushButton::clicked, this, &RtWizardMainWindow::addColorRole);
connect(qobject_cast<QPushButton *>(roleButtons->itemAt(1)->widget()), &QPushButton::clicked, this, &RtWizardMainWindow::addLineRole);
connect(qobject_cast<QPushButton *>(roleButtons->itemAt(2)->widget()), &QPushButton::clicked, this, &RtWizardMainWindow::addGhostRole);
m_roleTabs = new QTabWidget(m_treeContainer);
m_colorRoles = new QListWidget(m_roleTabs);
m_lineRoles = new QListWidget(m_roleTabs);
m_ghostRoles = new QListWidget(m_roleTabs);
m_roleTabs->addTab(m_colorRoles, tr("Full Color"));
m_roleTabs->addTab(m_lineRoles, tr("Line Drawing"));
m_roleTabs->addTab(m_ghostRoles, tr("Ghost"));
treeLayout->addWidget(m_roleTabs, 1);
QPushButton *remove = new QPushButton(tr("Remove Selected Role"), m_treeContainer);
treeLayout->addWidget(remove);
connect(remove, &QPushButton::clicked, this, &RtWizardMainWindow::removeRole);
connect(m_roleTabs, &QTabWidget::currentChanged, this, [this](int index) {
m_colorRoles->setProperty("activeRole", index == 0);
m_lineRoles->setProperty("activeRole", index == 1);
m_ghostRoles->setProperty("activeRole", index == 2);
if (m_rolesDock) {
const QStringList titles = {tr("Configure the Full-Color Elements"),
tr("Configure the Line-Drawing Elements"), tr("Configure the Ghost Elements")};
if (index >= 0 && index < titles.size()) m_rolesDock->setWindowTitle(titles[index]);
}
});
m_colorRoles->setProperty("activeRole", true);
QHBoxLayout *viewButtons = new QHBoxLayout;
QPushButton *clear = new QPushButton(tr("Clear Display"), m_treeContainer);
QPushButton *autoview = new QPushButton(tr("Auto View"), m_treeContainer);
viewButtons->addWidget(clear);
viewButtons->addWidget(autoview);
treeLayout->addLayout(viewButtons);
connect(clear, &QPushButton::clicked, this, &RtWizardMainWindow::clearScene);
connect(autoview, &QPushButton::clicked, this, &RtWizardMainWindow::autoviewScene);
m_rolesDock->setWidget(m_treeContainer);
addDockWidget(Qt::LeftDockWidgetArea, m_rolesDock);
m_settingsDock = new QDockWidget(tr("Image and Framebuffer Options"), this);
m_settingsDock->setObjectName("RenderSettingsDockV2");
m_tabs = new QTabWidget(m_settingsDock);
QFormLayout *imageForm = nullptr;
QWidget *imagePage = pageWithForm(imageForm, m_tabs);
m_type = new QComboBox(imagePage);
m_type->addItems({"A — Full color", "B — Line drawing", "C — Color with highlights",
"D — Mixed color and line", "E — Color with ghost", "F — Color, line, and ghost"});
m_width = new QSpinBox(imagePage); m_width->setRange(1, 32768); m_width->setValue(512);
m_height = new QSpinBox(imagePage); m_height->setRange(1, 32768); m_height->setValue(512);
m_background = new QLineEdit("255 255 255", imagePage);
m_lineColor = new QLineEdit("0 0 0", imagePage);
m_nonLineColor = new QLineEdit("0 0 0", imagePage);
m_ghostIntensity = new QDoubleSpinBox(imagePage); m_ghostIntensity->setRange(0.01, 100.0); m_ghostIntensity->setValue(6.0);
m_occlusion = new QSpinBox(imagePage); m_occlusion->setRange(1, 3); m_occlusion->setValue(1);
m_aoSamples = new QSpinBox(imagePage); m_aoSamples->setRange(0, 100000);
m_aoRadius = new QDoubleSpinBox(imagePage); m_aoRadius->setRange(0.0, 1.0e12); m_aoRadius->setDecimals(6);
imageForm->addRow(tr("Picture template"), m_type);
imageForm->addRow(tr("Width"), m_width); imageForm->addRow(tr("Height"), m_height);
imageForm->addRow(tr("Background RGB"), m_background); imageForm->addRow(tr("Line RGB"), m_lineColor);
imageForm->addRow(tr("Non-line RGB"), m_nonLineColor); imageForm->addRow(tr("Ghost intensity"), m_ghostIntensity);
imageForm->addRow(tr("Occlusion mode"), m_occlusion); imageForm->addRow(tr("AO samples"), m_aoSamples);
imageForm->addRow(tr("AO radius"), m_aoRadius);
m_tabs->addTab(imagePage, tr("Image"));
QFormLayout *viewForm = nullptr;
QWidget *viewPage = pageWithForm(viewForm, m_tabs);
auto angle = [viewPage]() { QDoubleSpinBox *s = new QDoubleSpinBox(viewPage); s->setRange(-36000, 36000); s->setDecimals(4); return s; };
m_viewMode = new QComboBox(viewPage); m_viewMode->addItems({tr("Azimuth/elevation"), tr("Exact eye/quaternion")});
m_azimuth = angle(); m_azimuth->setValue(35); m_elevation = angle(); m_elevation->setValue(25); m_twist = angle();
m_zoom = new QDoubleSpinBox(viewPage); m_zoom->setRange(0.000001, 1.0e9); m_zoom->setValue(1); m_zoom->setDecimals(6);
m_perspective = new QDoubleSpinBox(viewPage); m_perspective->setRange(0, 179); m_perspective->setDecimals(4);
m_center = new QLineEdit(viewPage); m_center->setPlaceholderText(tr("Automatic, or X Y Z"));
m_eye = new QLineEdit(viewPage); m_eye->setPlaceholderText(tr("X Y Z"));
m_orientation = new QLineEdit(viewPage); m_orientation->setPlaceholderText(tr("Quaternion X Y Z W"));
m_viewSize = new QDoubleSpinBox(viewPage); m_viewSize->setRange(0.000001, 1.0e12); m_viewSize->setDecimals(6);
viewForm->addRow(tr("View mode"), m_viewMode);
viewForm->addRow(tr("Azimuth"), m_azimuth); viewForm->addRow(tr("Elevation"), m_elevation);
viewForm->addRow(tr("Twist"), m_twist); viewForm->addRow(tr("Zoom"), m_zoom);
viewForm->addRow(tr("Perspective"), m_perspective); viewForm->addRow(tr("Center"), m_center);
viewForm->addRow(tr("Eye point"), m_eye); viewForm->addRow(tr("Orientation"), m_orientation);
viewForm->addRow(tr("View size"), m_viewSize);
connect(m_viewMode, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int mode) {
const bool exact = mode == 1;
m_azimuth->setEnabled(!exact); m_elevation->setEnabled(!exact);
m_twist->setEnabled(!exact); m_zoom->setEnabled(!exact); m_center->setEnabled(!exact);
m_eye->setEnabled(exact); m_orientation->setEnabled(exact); m_viewSize->setEnabled(exact);
updateValidation();
});
m_eye->setEnabled(false); m_orientation->setEnabled(false); m_viewSize->setEnabled(false);
m_tabs->addTab(viewPage, tr("View"));
QFormLayout *outputForm = nullptr;
QWidget *outputPage = pageWithForm(outputForm, m_tabs);
QWidget *outputRow = new QWidget(outputPage); QHBoxLayout *outputLayout = new QHBoxLayout(outputRow); outputLayout->setContentsMargins(0, 0, 0, 0);
m_outputPath = new QLineEdit(outputRow); QPushButton *outputBrowse = new QPushButton(tr("..."), outputRow); outputLayout->addWidget(m_outputPath); outputLayout->addWidget(outputBrowse);
m_frameDir = new QLineEdit(outputPage); m_frameDir->setPlaceholderText(tr("Optional numbered PNG frame directory"));
m_fbDevice = new QLineEdit(outputPage); m_fbDevice->setPlaceholderText(tr("Optional, for example /dev/mem or /dev/wgl"));
m_fbPort = new QSpinBox(outputPage); m_fbPort->setRange(-1, 65535); m_fbPort->setValue(-1); m_fbPort->setSpecialValueText(tr("Disabled"));
m_fbTransport = new QComboBox(outputPage); m_fbTransport->addItems({tr("Automatic (local IPC preferred)"), tr("Local IPC"), tr("TCP")});
outputForm->addRow(tr("Output file"), outputRow); outputForm->addRow(tr("Frame directory"), m_frameDir);
outputForm->addRow(tr("Framebuffer device"), m_fbDevice); outputForm->addRow(tr("Framebuffer port"), m_fbPort); outputForm->addRow(tr("Framebuffer transport"), m_fbTransport);
connect(outputBrowse, &QPushButton::clicked, this, &RtWizardMainWindow::chooseOutput);
m_tabs->addTab(outputPage, tr("Output"));
QWidget *animationPage = new QWidget(m_tabs); QVBoxLayout *animationLayout = new QVBoxLayout(animationPage);
QFormLayout *timing = new QFormLayout;
m_animation = new QComboBox(animationPage); m_animation->addItems({tr("None"), tr("Cutting plane"), tr("Orbit camera"), tr("Turntable object"), tr("Camera keyframes")});
m_duration = new QDoubleSpinBox(animationPage); m_duration->setRange(0.001, 1.0e9); m_duration->setValue(5); m_duration->setDecimals(3);
m_fps = new QSpinBox(animationPage); m_fps->setRange(1, 1000); m_fps->setValue(10);
m_frames = new QSpinBox(animationPage); m_frames->setRange(0, 1000000); m_frames->setSpecialValueText(tr("Automatic"));
timing->addRow(tr("Animation"), m_animation); timing->addRow(tr("Duration (s)"), m_duration);
timing->addRow(tr("Frames/second"), m_fps); timing->addRow(tr("Exact frames"), m_frames); animationLayout->addLayout(timing);
m_animationPages = new QStackedWidget(animationPage);
m_animationPages->addWidget(new QWidget(m_animationPages));
QFormLayout *cutForm = nullptr; QWidget *cutPage = pageWithForm(cutForm, m_animationPages); m_cutDirection = new QLineEdit("0 0 1", cutPage); cutForm->addRow(tr("Direction"), m_cutDirection); m_animationPages->addWidget(cutPage);
QFormLayout *orbitForm = nullptr; QWidget *orbitPage = pageWithForm(orbitForm, m_animationPages); m_orbitAngle = angle(); m_orbitAngle->setValue(360); m_orbitAxis = new QLineEdit("0 0 1", orbitPage); m_orbitCenter = new QLineEdit(orbitPage); m_orbitCenter->setPlaceholderText(tr("Automatic rendered-bounds center")); m_orbitElevation = angle(); m_orbitRadius = new QDoubleSpinBox(orbitPage); m_orbitRadius->setRange(0.0, 1.0e12); m_orbitRadius->setDecimals(6); m_orbitRadius->setSpecialValueText(tr("Automatic"));
orbitForm->addRow(tr("Angle"), m_orbitAngle); orbitForm->addRow(tr("Axis"), m_orbitAxis); orbitForm->addRow(tr("Look-at center"), m_orbitCenter); orbitForm->addRow(tr("Elevation"), m_orbitElevation); orbitForm->addRow(tr("Radius"), m_orbitRadius); m_animationPages->addWidget(orbitPage);
QFormLayout *turntableForm = nullptr; QWidget *turntablePage = pageWithForm(turntableForm, m_animationPages); m_turntableObject = new QLineEdit(turntablePage); m_turntableAngle = angle(); m_turntableAngle->setValue(360); m_turntableAxis = new QLineEdit("0 0 1", turntablePage); m_turntableCenter = new QLineEdit(turntablePage); m_turntableCenter->setPlaceholderText(tr("Automatic object-bounds center"));
turntableForm->addRow(tr("Object path"), m_turntableObject); turntableForm->addRow(tr("Angle"), m_turntableAngle); turntableForm->addRow(tr("Axis"), m_turntableAxis); turntableForm->addRow(tr("Pivot center"), m_turntableCenter); m_animationPages->addWidget(turntablePage);
QWidget *keyPage = new QWidget(m_animationPages); QVBoxLayout *keyLayout = new QVBoxLayout(keyPage); m_timeline = new QTableWidget(0, 4, keyPage); m_timeline->setHorizontalHeaderLabels({tr("Time"), tr("Eye"), tr("Orientation"), tr("View size")}); m_timeline->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); keyLayout->addWidget(m_timeline);
QHBoxLayout *keyButtons = new QHBoxLayout; m_keyTime = new QDoubleSpinBox(keyPage); m_keyTime->setRange(0, 1.0e9); QPushButton *addKey = new QPushButton(tr("Capture Current View"), keyPage); QPushButton *removeKey = new QPushButton(tr("Delete Keyframe"), keyPage); keyButtons->addWidget(m_keyTime); keyButtons->addWidget(addKey); keyButtons->addWidget(removeKey); keyLayout->addLayout(keyButtons); m_animationPages->addWidget(keyPage);
animationLayout->addWidget(m_animationPages);
m_advancedTracks = new QLabel(animationPage); m_advancedTracks->setWordWrap(true); animationLayout->addWidget(m_advancedTracks);
connect(m_animation, qOverload<int>(&QComboBox::currentIndexChanged), this, &RtWizardMainWindow::updateAnimationPage);
connect(addKey, &QPushButton::clicked, this, &RtWizardMainWindow::addCameraKeyframe); connect(removeKey, &QPushButton::clicked, this, &RtWizardMainWindow::removeCameraKeyframe);
m_tabs->addTab(animationPage, tr("Animation"));
m_settingsDock->setWidget(m_tabs); addDockWidget(Qt::LeftDockWidgetArea, m_settingsDock);
tabifyDockWidget(m_rolesDock, m_settingsDock);
setTabPosition(Qt::LeftDockWidgetArea, QTabWidget::North);
m_rolesDock->raise();
m_jobDock = new QDockWidget(tr("Render Status"), this); m_jobDock->setObjectName("RenderJobDockV2"); QWidget *job = new QWidget(m_jobDock); QVBoxLayout *jobLayout = new QVBoxLayout(job);
m_validation = new QLabel(job); m_validation->setWordWrap(true); jobLayout->addWidget(m_validation);
m_progress = new QProgressBar(job); m_progress->setRange(0, 1); m_progress->setValue(0); jobLayout->addWidget(m_progress);
m_log = new QPlainTextEdit(job); m_log->setReadOnly(true); m_log->setMaximumBlockCount(2000); m_log->setMaximumHeight(100); jobLayout->addWidget(m_log);
QHBoxLayout *jobButtons = new QHBoxLayout; m_previewButton = new QPushButton(tr("Preview"), job); m_renderButton = new QPushButton(tr("Render"), job); m_cancelButton = new QPushButton(tr("Cancel"), job); m_cancelButton->setEnabled(false); jobButtons->addWidget(m_previewButton); jobButtons->addWidget(m_renderButton); jobButtons->addWidget(m_cancelButton); jobLayout->addLayout(jobButtons);
connect(m_previewButton, &QPushButton::clicked, this, &RtWizardMainWindow::renderPreview); connect(m_renderButton, &QPushButton::clicked, this, &RtWizardMainWindow::renderFull); connect(m_cancelButton, &QPushButton::clicked, this, &RtWizardMainWindow::cancelRender);
m_jobDock->setWidget(job); addDockWidget(Qt::BottomDockWidgetArea, m_jobDock);
resizeDocks({m_rolesDock}, {390}, Qt::Horizontal);
resizeDocks({m_jobDock}, {170}, Qt::Vertical);
connect(m_type, qOverload<int>(&QComboBox::currentIndexChanged), this, &RtWizardMainWindow::updateValidation);
connect(m_turntableObject, &QLineEdit::textChanged, this, &RtWizardMainWindow::updateValidation);
connect(m_eye, &QLineEdit::textChanged, this, &RtWizardMainWindow::updateValidation);
connect(m_orientation, &QLineEdit::textChanged, this, &RtWizardMainWindow::updateValidation);
connect(m_viewSize, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &RtWizardMainWindow::updateValidation);
for (QListWidget *list : {m_colorRoles, m_lineRoles, m_ghostRoles}) connect(list, &QListWidget::itemChanged, this, &RtWizardMainWindow::updateValidation);
resize(1280, 820);
}
void
RtWizardMainWindow::showRolePage(int index)
{
const int page = std::clamp(index, 0, 2);
if (m_roleTabs) m_roleTabs->setCurrentIndex(page);
if (m_rolesDock) {
const QStringList titles = {tr("Configure the Full-Color Elements"),
tr("Configure the Line-Drawing Elements"), tr("Configure the Ghost Elements")};
m_rolesDock->setWindowTitle(titles[page]);
m_rolesDock->show();
m_rolesDock->raise();
}
}
void
RtWizardMainWindow::showSettingsPage(int index)
{
if (m_tabs) m_tabs->setCurrentIndex(std::clamp(index, 0, m_tabs->count() - 1));
if (m_settingsDock) {
m_settingsDock->show();
m_settingsDock->raise();
}
}
void
RtWizardMainWindow::attachModelView()
{
if (!m_model || !m_model->gedp || !m_view || !m_view->view()) return;
bview *view = m_view->view();
if (view->vset && view->vset != &m_model->gedp->ged_views)
bv_set_rm_view(view->vset, view);
bv_set_add_view(&m_model->gedp->ged_views, view);
m_model->gedp->ged_gvp = view;
}
void
RtWizardMainWindow::updateSceneView(unsigned long long flags)
{
if (!m_model || !m_model->gedp || !m_view || !m_view->view()) return;
attachModelView();
if ((flags & QG_VIEW_DRAWN) && m_model->gedp->dbi_state) {
DbiState *databaseState = static_cast<DbiState *>(m_model->gedp->dbi_state);
BViewState *viewState = databaseState->get_view_state(m_view->view());
if (viewState) {
std::unordered_set<bview *> views = {m_view->view()};
viewState->redraw(nullptr, views, 1);
}
}
if (m_tree) m_tree->do_view_update(flags);
m_view->need_update(flags);
}
void
RtWizardMainWindow::loadDatabase(const QString &path)
{
if (path.isEmpty()) return;
QFileInfo info(path);
if (!info.exists()) { QMessageBox::critical(this, tr("Open Database"), tr("Database does not exist: %1").arg(path)); return; }
m_database = info.absoluteFilePath();
QgTreeView *oldTree = m_tree;
QgModel *oldModel = m_model;
if (m_view && m_view->view() && m_view->view()->vset)
bv_set_rm_view(m_view->view()->vset, m_view->view());
m_model = new QgModel(this, QFile::encodeName(m_database).constData());
m_tree = new QgTreeView(m_treeContainer, m_model);
QVBoxLayout *layout = qobject_cast<QVBoxLayout *>(m_treeContainer->layout());
QWidget *replaced = oldTree ? static_cast<QWidget *>(oldTree) :
m_treeContainer->findChild<QWidget *>("TreePlaceholder");
layout->replaceWidget(replaced, m_tree);
if (replaced) replaced->deleteLater();
if (oldModel) oldModel->deleteLater();
attachModelView();
connect(m_model, &QgModel::view_change, this, [this](unsigned long long flags) {
updateSceneView(flags);
if (flags & QG_VIEW_DRAWN) autoviewScene();
});
connect(m_model, &QgModel::opened_item, m_tree, &QgTreeView::qgitem_select_sync);
connect(m_model, &QgModel::mdl_changed_db, m_tree, &QgTreeView::redo_expansions);
connect(m_model, &QgModel::check_highlights, m_tree, &QgTreeView::redo_highlights);
connect(m_tree, &QgTreeView::expanded, m_model, &QgModel::item_expanded);
connect(m_tree, &QgTreeView::collapsed, m_model, &QgModel::item_collapsed);
disconnect(m_tree, &QTreeView::doubleClicked, m_tree, &QgTreeView::do_draw_toggle);
connect(m_tree, &QTreeView::clicked, this, [this](const QModelIndex &) {
if (m_result) m_result->hide();
const QString objectPath = selectedObjectPath();
if (m_model && !objectPath.isEmpty())
m_model->draw(QFile::encodeName(objectPath).constData());
});
connect(m_tree, &QTreeView::doubleClicked, this, [this](const QModelIndex &) {
updateSceneView(QG_VIEW_DRAWN);
autoviewScene();
});
setWindowTitle(tr("BRL-CAD RtWizard — %1").arg(info.fileName()));
QSettings settings("BRL-CAD", "RtWizard"); settings.setValue("lastDatabaseDirectory", info.absolutePath());
refreshScene(); updateValidation();
}
void
RtWizardMainWindow::applySettings(const rtwizard_settings *settings, char pictureType)
{
if (!settings) return;
const QString database = QString::fromLocal8Bit(bu_vls_addr(settings->input_file));
if (!database.isEmpty() && QFileInfo(database).absoluteFilePath() != m_database)
loadDatabase(database);
setRoleValues(m_colorRoles, table_strings(settings->color));
setRoleValues(m_lineRoles, table_strings(settings->line));
setRoleValues(m_ghostRoles, table_strings(settings->ghost));
if (pictureType >= 'A' && pictureType <= 'F') {
m_type->setCurrentIndex(pictureType - 'A');
} else if (BU_PTBL_LEN(settings->ghost) && BU_PTBL_LEN(settings->line) && BU_PTBL_LEN(settings->color)) {
m_type->setCurrentIndex(5);
} else if (BU_PTBL_LEN(settings->ghost) && BU_PTBL_LEN(settings->color)) {
m_type->setCurrentIndex(4);
} else if (BU_PTBL_LEN(settings->line) && BU_PTBL_LEN(settings->color)) {
m_type->setCurrentIndex(3);
} else if (BU_PTBL_LEN(settings->line)) {
m_type->setCurrentIndex(1);
}
m_width->setValue(static_cast<int>(settings->width));
m_height->setValue(static_cast<int>(settings->height));
m_background->setText(color_text(settings->bkg_color));
m_lineColor->setText(color_text(settings->line_color));
m_nonLineColor->setText(color_text(settings->non_line_color));
m_ghostIntensity->setValue(settings->ghost_intensity);
m_occlusion->setValue(settings->occlusion);
m_aoSamples->setValue(settings->ao_samples);
m_aoRadius->setValue(settings->ao_radius);
if (settings->az < DBL_MAX) m_azimuth->setValue(settings->az);
if (settings->el < DBL_MAX) m_elevation->setValue(settings->el);
if (settings->tw < DBL_MAX) m_twist->setValue(settings->tw);
if (settings->zoom < DBL_MAX) m_zoom->setValue(settings->zoom);
if (settings->perspective < DBL_MAX) m_perspective->setValue(settings->perspective);
if (settings->center[0] < DBL_MAX)
m_center->setText(QStringLiteral("%1 %2 %3").arg(settings->center[0], 0, 'g', 15).arg(settings->center[1], 0, 'g', 15).arg(settings->center[2], 0, 'g', 15));
if (settings->viewsize < DBL_MAX && settings->orientation[0] < DBL_MAX && settings->eye_pt[0] < DBL_MAX) {
m_viewMode->setCurrentIndex(1);
m_viewSize->setValue(settings->viewsize);
m_eye->setText(QStringLiteral("%1 %2 %3").arg(settings->eye_pt[0], 0, 'g', 15).arg(settings->eye_pt[1], 0, 'g', 15).arg(settings->eye_pt[2], 0, 'g', 15));
m_orientation->setText(QStringLiteral("%1 %2 %3 %4").arg(settings->orientation[0], 0, 'g', 15).arg(settings->orientation[1], 0, 'g', 15).arg(settings->orientation[2], 0, 'g', 15).arg(settings->orientation[3], 0, 'g', 15));
}
m_outputPath->setText(QString::fromLocal8Bit(bu_vls_addr(settings->output_file)));
m_frameDir->setText(QString::fromLocal8Bit(bu_vls_addr(settings->frame_dir)));
m_fbDevice->setText(QString::fromLocal8Bit(bu_vls_addr(settings->fb_dev)));
m_fbPort->setValue(settings->port);
m_fbTransport->setCurrentIndex(std::clamp(settings->fb_transport, 0, 2));
if (settings->animation_duration > 0.0) m_duration->setValue(settings->animation_duration);
if (settings->animation_fps > 0) m_fps->setValue(settings->animation_fps);
if (settings->animation_frames > 0) m_frames->setValue(settings->animation_frames);
const QString preset = QString::fromLocal8Bit(bu_vls_addr(settings->animation_preset));
if (preset == "cut") m_animation->setCurrentIndex(1);
else if (preset == "orbit") m_animation->setCurrentIndex(2);
else if (preset == "turntable") m_animation->setCurrentIndex(3);
else if (bu_vls_strlen(settings->animation_file)) m_animation->setCurrentIndex(4);
if (settings->cut_direction_set)
m_cutDirection->setText(QStringLiteral("%1 %2 %3").arg(settings->cut_direction[0], 0, 'g', 15).arg(settings->cut_direction[1], 0, 'g', 15).arg(settings->cut_direction[2], 0, 'g', 15));
m_orbitAngle->setValue(settings->orbit_angle);
m_orbitAxis->setText(QStringLiteral("%1 %2 %3").arg(settings->orbit_axis[0], 0, 'g', 15).arg(settings->orbit_axis[1], 0, 'g', 15).arg(settings->orbit_axis[2], 0, 'g', 15));
if (settings->orbit_center[0] < DBL_MAX)
m_orbitCenter->setText(QStringLiteral("%1 %2 %3").arg(settings->orbit_center[0], 0, 'g', 15).arg(settings->orbit_center[1], 0, 'g', 15).arg(settings->orbit_center[2], 0, 'g', 15));
if (settings->orbit_elevation < DBL_MAX) m_orbitElevation->setValue(settings->orbit_elevation);
if (settings->orbit_radius < DBL_MAX) m_orbitRadius->setValue(settings->orbit_radius);
m_turntableObject->setText(QString::fromLocal8Bit(bu_vls_addr(settings->turntable_object)));
m_turntableAngle->setValue(settings->turntable_angle);
m_turntableAxis->setText(QStringLiteral("%1 %2 %3").arg(settings->turntable_axis[0], 0, 'g', 15).arg(settings->turntable_axis[1], 0, 'g', 15).arg(settings->turntable_axis[2], 0, 'g', 15));
if (settings->turntable_center[0] < DBL_MAX)
m_turntableCenter->setText(QStringLiteral("%1 %2 %3").arg(settings->turntable_center[0], 0, 'g', 15).arg(settings->turntable_center[1], 0, 'g', 15).arg(settings->turntable_center[2], 0, 'g', 15));
syncTimeline();
refreshScene();
updateValidation();
}
QString
RtWizardMainWindow::selectedObjectPath() const
{
if (!m_tree || !m_model) return QString();
QgItem *item = m_model->getItem(m_tree->currentIndex());
QStringList names;
while (item && item != m_model->root()) {
QString name = QString::fromUtf8(bu_vls_addr(&item->name));
if (!name.isEmpty()) names.prepend(name);
item = item->parent();
}
return names.join('/');
}
void RtWizardMainWindow::addRole(QListWidget *list)
{
QString path = selectedObjectPath();
if (path.isEmpty()) return;
if (list->findItems(path, Qt::MatchExactly).isEmpty()) list->addItem(path);
refreshScene(); updateValidation();
}
void RtWizardMainWindow::addColorRole() { addRole(m_colorRoles); }
void RtWizardMainWindow::addLineRole() { addRole(m_lineRoles); }
void RtWizardMainWindow::addGhostRole() { addRole(m_ghostRoles); }
void
RtWizardMainWindow::removeRole()
{
for (QListWidget *list : {m_colorRoles, m_lineRoles, m_ghostRoles}) {
if (!list->property("activeRole").toBool()) continue;
delete list->takeItem(list->currentRow());
}
refreshScene(); updateValidation();
}
QStringList RtWizardMainWindow::roleValues(const QListWidget *list) const
{
QStringList values; for (int i = 0; i < list->count(); ++i) values << list->item(i)->text(); return values;
}
void RtWizardMainWindow::setRoleValues(QListWidget *list, const QStringList &values) { list->clear(); list->addItems(values); }
void
RtWizardMainWindow::refreshScene()
{
if (!m_model || !m_model->gedp || m_database.isEmpty()) return;
m_result->hide();
attachModelView();
bu_vls message = BU_VLS_INIT_ZERO;
if (m_model->gedp->dbi_state) {
DbiState *databaseState = static_cast<DbiState *>(m_model->gedp->dbi_state);
BViewState *viewState = databaseState->get_view_state(m_view->view());
if (viewState) viewState->clear();
}
const char *zap[] = {"zap"}; m_model->run_cmd(&message, 1, zap);
std::set<QString> objects;
for (QListWidget *list : {m_colorRoles, m_lineRoles, m_ghostRoles}) for (const QString &value : roleValues(list)) objects.insert(value);
if (!objects.empty()) {
std::vector<QByteArray> encoded; std::vector<const char *> argv; encoded.push_back("draw");
for (const QString &object : objects) encoded.push_back(QFile::encodeName(object));
for (const QByteArray &value : encoded) argv.push_back(value.constData());
m_model->run_cmd(&message, static_cast<int>(argv.size()), argv.data());
updateSceneView(QG_VIEW_DRAWN);
const char *autoview[] = {"autoview"}; m_model->run_cmd(&message, 1, autoview);
} else {
updateSceneView(QG_VIEW_DRAWN);
}
bu_vls_free(&message);
updateSceneView(QG_VIEW_REFRESH);
}
void
RtWizardMainWindow::autoviewScene()
{
if (!m_model || !m_model->gedp) return;
m_result->hide();
attachModelView();
bu_vls message = BU_VLS_INIT_ZERO;
const char *autoview[] = {"autoview"};
m_model->run_cmd(&message, 1, autoview);
bu_vls_free(&message);
updateSceneView(QG_VIEW_REFRESH);
}
void
RtWizardMainWindow::clearScene()
{
if (!m_model || !m_model->gedp) return;
m_result->hide();
attachModelView();
bu_vls message = BU_VLS_INIT_ZERO;
if (m_model->gedp->dbi_state) {
DbiState *databaseState = static_cast<DbiState *>(m_model->gedp->dbi_state);
BViewState *viewState = databaseState->get_view_state(m_view->view());
if (viewState) viewState->clear();
}
const char *zap[] = {"zap"};
m_model->run_cmd(&message, 1, zap);
bu_vls_free(&message);
updateSceneView(QG_VIEW_DRAWN);
}
void
RtWizardMainWindow::updateValidation()
{
QStringList missing;
const int type = m_type->currentIndex();
if (m_database.isEmpty()) missing << tr("database");
if ((type == 0 || type == 2 || type == 3 || type == 4 || type == 5) && m_colorRoles->count() == 0) missing << tr("color objects");
if ((type == 1 || type == 2 || type == 3 || type == 5) && m_lineRoles->count() == 0) missing << tr("line objects");
if ((type == 4 || type == 5) && m_ghostRoles->count() == 0) missing << tr("ghost objects");
if (m_animation->currentIndex() == 3 && m_turntableObject->text().trimmed().isEmpty()) missing << tr("turntable object");
if (m_viewMode->currentIndex() == 1) {
try {
(void)vector_value(m_eye->text(), 3);
(void)vector_value(m_orientation->text(), 4);
if (!(m_viewSize->value() > 0.0)) missing << tr("positive exact view size");
} catch (...) {
missing << tr("complete exact view");
}
}
const bool valid = missing.isEmpty();
m_validation->setText(valid ? tr("Ready to render.") : tr("Required: %1.").arg(missing.join(", ")));
m_renderButton->setEnabled(valid && !m_renderThread); m_previewButton->setEnabled(valid && !m_renderThread);
}
void RtWizardMainWindow::updateAnimationPage() { m_animationPages->setCurrentIndex(m_animation->currentIndex()); updateValidation(); }
bool
RtWizardMainWindow::loadSpecification(const QString &path)
{
try {
std::ifstream input(QFile::encodeName(path).constData()); json root; input >> root;
if (root.value("schema", "") != "brlcad.rtwizard.render" || root.value("version", 0) != 1) throw std::runtime_error("unsupported render specification schema or version");
m_document = root.dump(2); m_specPath = QFileInfo(path).absoluteFilePath();
fs::path base = fs::path(QFile::encodeName(m_specPath).constData()).parent_path();
if (root.contains("database")) { fs::path db = root["database"].get<std::string>(); if (db.is_relative()) db = base / db; loadDatabase(QString::fromStdString(db.lexically_normal().string())); }
if (root.contains("objects")) { setRoleValues(m_colorRoles, json_strings(root["objects"].value("color", json::array()))); setRoleValues(m_lineRoles, json_strings(root["objects"].value("line", json::array()))); setRoleValues(m_ghostRoles, json_strings(root["objects"].value("ghost", json::array()))); }
const json image = root.value("image", json::object()); const std::string pictureType = image.value("type", std::string("A")); m_type->setCurrentIndex(pictureType.empty() ? 0 : std::clamp(static_cast<int>(pictureType[0] - 'A'), 0, 5)); int size = image.value("size", 512); m_width->setValue(image.value("width", size)); m_height->setValue(image.value("height", size));
const json style = root.value("style", json::object()); m_background->setText(vector_text(style.value("background", json::array()), "255 255 255")); m_lineColor->setText(vector_text(style.value("line_color", json::array()), "0 0 0")); m_nonLineColor->setText(vector_text(style.value("non_line_color", json::array()), "0 0 0")); m_ghostIntensity->setValue(style.value("ghost_intensity", 6.0)); m_occlusion->setValue(style.value("occlusion", 1)); m_aoSamples->setValue(style.value("ao_samples", 0)); m_aoRadius->setValue(style.value("ao_radius", 0.0));
const json view = root.value("view", json::object()); const bool exactView = view.contains("eye") && view.contains("orientation") && view.contains("view_size"); m_viewMode->setCurrentIndex(exactView ? 1 : 0); m_azimuth->setValue(view.value("azimuth", 35.0)); m_elevation->setValue(view.value("elevation", 25.0)); m_twist->setValue(view.value("twist", 0.0)); m_zoom->setValue(view.value("zoom", 1.0)); m_perspective->setValue(view.value("perspective", 0.0)); m_center->setText(vector_text(view.value("center", json::array()), QString())); m_eye->setText(vector_text(view.value("eye", json::array()), QString())); m_orientation->setText(vector_text(view.value("orientation", json::array()), QString())); m_viewSize->setValue(view.value("view_size", 1.0));
const json output = root.value("output", json::object()); if (output.contains("file")) { fs::path p = output["file"].get<std::string>(); if (p.is_relative()) p = base / p; m_outputPath->setText(QString::fromStdString(p.lexically_normal().string())); } if (output.contains("frame_dir")) m_frameDir->setText(QString::fromStdString(output["frame_dir"].get<std::string>())); m_fbDevice->setText(QString::fromStdString(output.value("framebuffer", std::string()))); m_fbPort->setValue(output.value("framebuffer_port", -1)); const std::string transport = output.value("framebuffer_transport", std::string("auto")); m_fbTransport->setCurrentIndex(transport == "ipc" ? 1 : transport == "tcp" ? 2 : 0);
m_animation->setCurrentIndex(0); m_advancedTracks->clear();
if (root.contains("animation")) { const json &a = root["animation"]; const json timing = a.value("timing", json::object()); m_duration->setValue(timing.value("duration", 5.0)); m_fps->setValue(timing.value("fps", 10)); m_frames->setValue(timing.value("frames", 0)); if (a.contains("preset")) { std::string preset = a["preset"].get<std::string>(); m_animation->setCurrentIndex(preset == "cut" ? 1 : preset == "orbit" ? 2 : 3); const json options = a.value("options", json::object()); m_cutDirection->setText(vector_text(options.value("cut_direction", json::array()), "0 0 1")); m_orbitAngle->setValue(options.value("orbit_angle", 360.0)); m_orbitAxis->setText(vector_text(options.value("orbit_axis", json::array()), "0 0 1")); m_orbitCenter->setText(vector_text(options.value("orbit_center", json::array()), QString())); m_orbitElevation->setValue(options.value("orbit_elevation", 0.0)); m_orbitRadius->setValue(options.value("orbit_radius", 0.0)); m_turntableObject->setText(QString::fromStdString(options.value("turntable_object", std::string()))); m_turntableAngle->setValue(options.value("turntable_angle", 360.0)); m_turntableAxis->setText(vector_text(options.value("turntable_axis", json::array()), "0 0 1")); m_turntableCenter->setText(vector_text(options.value("turntable_center", json::array()), QString())); } else if (a.contains("tracks")) { m_animation->setCurrentIndex(4); int advanced = 0; for (const json &track : a["tracks"]) if (track.value("type", "") != "camera") ++advanced; if (advanced) m_advancedTracks->setText(tr("%1 advanced object/material track(s) loaded; they will be preserved when saving.").arg(advanced)); } }
syncTimeline(); refreshScene(); updateValidation(); return true;
} catch (const std::exception &error) { QMessageBox::critical(this, tr("Open Render Specification"), QString::fromLocal8Bit(error.what())); return false; }
}
bool
RtWizardMainWindow::writeSpecification(const QString &path, bool preview)
{
try {
json root = parse_document(m_document); root["schema"] = "brlcad.rtwizard.render"; root["version"] = 1; root["database"] = m_database.toStdString();
root["objects"] = {{"color", string_array(roleValues(m_colorRoles))}, {"line", string_array(roleValues(m_lineRoles))}, {"ghost", string_array(roleValues(m_ghostRoles))}};
root["image"] = {{"width", preview ? std::min(256, m_width->value()) : m_width->value()}, {"height", preview ? std::min(256, m_height->value()) : m_height->value()}, {"type", std::string(1, static_cast<char>('A' + m_type->currentIndex()))}};
root["style"] = {{"background", vector_value(m_background->text(), 3)}, {"line_color", vector_value(m_lineColor->text(), 3)}, {"non_line_color", vector_value(m_nonLineColor->text(), 3)}, {"ghost_intensity", m_ghostIntensity->value()}, {"occlusion", m_occlusion->value()}, {"ao_samples", m_aoSamples->value()}, {"ao_radius", m_aoRadius->value()}};
if (m_viewMode->currentIndex() == 1) {
root["view"] = {{"eye", vector_value(m_eye->text(), 3)}, {"orientation", vector_value(m_orientation->text(), 4)}, {"view_size", m_viewSize->value()}, {"perspective", m_perspective->value()}};
} else {
root["view"] = {{"azimuth", m_azimuth->value()}, {"elevation", m_elevation->value()}, {"twist", m_twist->value()}, {"zoom", m_zoom->value()}, {"perspective", m_perspective->value()}};
if (!m_center->text().trimmed().isEmpty()) root["view"]["center"] = vector_value(m_center->text(), 3);
}
QString output = preview ? m_previewOutput : m_outputPath->text().trimmed(); root["output"]["file"] = output.toStdString(); if (!m_frameDir->text().trimmed().isEmpty() && !preview) root["output"]["frame_dir"] = m_frameDir->text().trimmed().toStdString(); else root["output"].erase("frame_dir"); if (!m_fbDevice->text().trimmed().isEmpty()) root["output"]["framebuffer"] = m_fbDevice->text().trimmed().toStdString(); else root["output"].erase("framebuffer"); if (m_fbPort->value() >= 0) root["output"]["framebuffer_port"] = m_fbPort->value(); else root["output"].erase("framebuffer_port"); root["output"]["framebuffer_transport"] = m_fbTransport->currentIndex() == 1 ? "ipc" : m_fbTransport->currentIndex() == 2 ? "tcp" : "auto"; root["runtime"]["no_gui"] = true; root["runtime"].erase("gui");
const int animation = m_animation->currentIndex();
if (animation == 0 || preview) root.erase("animation");
else { json &a = root["animation"]; a["timing"] = {{"duration", m_duration->value()}, {"fps", m_fps->value()}}; if (m_frames->value() > 0) a["timing"]["frames"] = m_frames->value(); else a["timing"].erase("frames"); if (animation >= 1 && animation <= 3) { a.erase("tracks"); a["preset"] = animation == 1 ? "cut" : animation == 2 ? "orbit" : "turntable"; json options = json::object(); if (animation == 1) options["cut_direction"] = vector_value(m_cutDirection->text(), 3); if (animation == 2) { options["orbit_angle"] = m_orbitAngle->value(); options["orbit_axis"] = vector_value(m_orbitAxis->text(), 3); if (!m_orbitCenter->text().trimmed().isEmpty()) options["orbit_center"] = vector_value(m_orbitCenter->text(), 3); options["orbit_elevation"] = m_orbitElevation->value(); if (m_orbitRadius->value() > 0.0) options["orbit_radius"] = m_orbitRadius->value(); } if (animation == 3) { options["turntable_object"] = m_turntableObject->text().trimmed().toStdString(); options["turntable_angle"] = m_turntableAngle->value(); options["turntable_axis"] = vector_value(m_turntableAxis->text(), 3); if (!m_turntableCenter->text().trimmed().isEmpty()) options["turntable_center"] = vector_value(m_turntableCenter->text(), 3); } a["options"] = options; } else { a.erase("preset"); a.erase("options"); if (!a.contains("tracks")) a["tracks"] = json::array(); } }
std::ofstream file(QFile::encodeName(path).constData(), std::ios::trunc); file << root.dump(2) << '\n'; if (!file) throw std::runtime_error("unable to write render specification"); if (!preview) m_document = root.dump(2); return true;
} catch (const std::exception &error) { QMessageBox::critical(this, tr("Save Render Specification"), QString::fromLocal8Bit(error.what())); return false; }
}
void RtWizardMainWindow::openDatabase() { QSettings s("BRL-CAD", "RtWizard"); QString p = QFileDialog::getOpenFileName(this, tr("Open BRL-CAD Database"), s.value("lastDatabaseDirectory").toString(), tr("BRL-CAD databases (*.g);;All files (*)")); if (!p.isEmpty()) loadDatabase(p); }
void RtWizardMainWindow::openSpecification() { QString p = QFileDialog::getOpenFileName(this, tr("Open Render Specification"), QFileInfo(m_specPath).absolutePath(), tr("JSON files (*.json);;All files (*)")); if (!p.isEmpty()) loadSpecification(p); }
void RtWizardMainWindow::saveSpecification() { if (m_specPath.isEmpty()) saveSpecificationAs(); else writeSpecification(m_specPath, false); }
void RtWizardMainWindow::saveSpecificationAs() { QString p = QFileDialog::getSaveFileName(this, tr("Save Render Specification"), m_specPath.isEmpty() ? "rtwizard.json" : m_specPath, tr("JSON files (*.json)")); if (!p.isEmpty() && writeSpecification(p, false)) m_specPath = p; }
void RtWizardMainWindow::chooseOutput() { QString p = QFileDialog::getSaveFileName(this, tr("Select Render Output"), m_outputPath->text(), tr("Images (*.png *.pix *.ppm *.bw *.dpix);;Animations (*.apng *.mjpg *.avi);;All files (*)")); if (!p.isEmpty()) m_outputPath->setText(p); }
void
RtWizardMainWindow::addCameraKeyframe()
{
try {
json root = parse_document(m_document); json &a = root["animation"]; a.erase("preset"); a.erase("options"); a["timing"]["duration"] = std::max(m_duration->value(), m_keyTime->value()); a["timing"]["fps"] = m_fps->value(); double unitToBase = 1.0; if (!a.contains("units")) a["units"] = "mm"; const std::string units = a.value("units", std::string("mm")); if (units == "database") { if (!m_model || !m_model->gedp || !m_model->gedp->dbip) throw std::runtime_error("database units are unavailable"); unitToBase = m_model->gedp->dbip->dbi_local2base; } else { unitToBase = bu_units_conversion(units.c_str()); if (!(unitToBase > 0.0)) throw std::runtime_error("unknown animation distance unit"); } json &tracks = a["tracks"]; if (!tracks.is_array()) tracks = json::array(); json *camera = nullptr; for (json &track : tracks) if (track.value("type", "") == "camera") { camera = &track; break; } if (!camera) { tracks.push_back({{"type", "camera"}, {"interpolation", "smooth"}, {"keyframes", json::array()}}); camera = &tracks.back(); }
bview *view = m_view->view(); if (!view) throw std::runtime_error("CAD view is not initialized"); quat_t orientation; quat_mat2quat(orientation, view->gv_rotation); point_t eye = VINIT_ZERO; point_t viewEye = {0, 0, 1}; MAT4X3PNT(eye, view->gv_view2model, viewEye);
json key = {{"time", m_keyTime->value()}, {"eye", {eye[0] / unitToBase, eye[1] / unitToBase, eye[2] / unitToBase}}, {"orientation", {orientation[0], orientation[1], orientation[2], orientation[3]}}, {"view_size", view->gv_size / unitToBase}, {"perspective", view->gv_perspective}};
json &keys = (*camera)["keyframes"]; bool replaced = false; for (json &existing : keys) if (std::fabs(existing.value("time", -1.0) - m_keyTime->value()) < 1.0e-12) { existing = key; replaced = true; break; } if (!replaced) keys.push_back(key); std::sort(keys.begin(), keys.end(), [](const json &l, const json &r) { return l.value("time", 0.0) < r.value("time", 0.0); }); m_document = root.dump(2); m_animation->setCurrentIndex(4); syncTimeline();
} catch (const std::exception &error) { QMessageBox::critical(this, tr("Camera Keyframe"), QString::fromLocal8Bit(error.what())); }
}
void
RtWizardMainWindow::removeCameraKeyframe()
{
int row = m_timeline->currentRow(); if (row < 0) return;
json root = parse_document(m_document); if (!root.contains("animation")) return; for (json &track : root["animation"]["tracks"]) if (track.value("type", "") == "camera") { json &keys = track["keyframes"]; if (row < static_cast<int>(keys.size())) keys.erase(keys.begin() + row); break; } m_document = root.dump(2); syncTimeline();
}
void
RtWizardMainWindow::syncTimeline()
{
m_timeline->setRowCount(0); try { json root = parse_document(m_document); if (!root.contains("animation") || !root["animation"].contains("tracks")) return; for (const json &track : root["animation"]["tracks"]) { if (track.value("type", "") != "camera") continue; for (const json &key : track["keyframes"]) { int row = m_timeline->rowCount(); m_timeline->insertRow(row); m_timeline->setItem(row, 0, new QTableWidgetItem(QString::number(key.value("time", 0.0)))); m_timeline->setItem(row, 1, new QTableWidgetItem(vector_text(key.value("eye", json::array()), QString()))); m_timeline->setItem(row, 2, new QTableWidgetItem(vector_text(key.value("orientation", json::array()), QString()))); m_timeline->setItem(row, 3, new QTableWidgetItem(QString::number(key.value("view_size", 0.0)))); } break; } } catch (...) {}
}
void RtWizardMainWindow::renderPreview() { startRender(true); }
void RtWizardMainWindow::renderFull() { startRender(false); }
void
RtWizardMainWindow::startRender(bool preview)
{
if (m_renderThread)
return;
updateValidation();
if (!m_renderButton->isEnabled() && !m_previewButton->isEnabled())
return;
if (!preview && m_outputPath->text().trimmed().isEmpty()) { chooseOutput(); if (m_outputPath->text().trimmed().isEmpty()) return; }
QString cache = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QDir().mkpath(cache); m_previewOutput = QDir(cache).filePath("rtwizard-preview.png"); m_activeOutput = preview ? m_previewOutput : m_outputPath->text().trimmed();
if (preview) QFile::remove(m_previewOutput);
QString spec = QDir(cache).filePath("rtwizard-job.json"); if (!writeSpecification(spec, preview)) return;
m_log->clear(); m_result->hide(); m_progress->setRange(0, 0); m_previewButton->setEnabled(false); m_renderButton->setEnabled(false); m_cancelButton->setEnabled(true);
QStringList arguments = {m_program, "--no-gui", "--render-spec", spec, "--verbose", "1"};
m_renderThread = new QThread(this);
QThread *thread = m_renderThread;
m_worker = new RtWizardRenderWorker(arguments);
m_worker->moveToThread(thread);
connect(thread, &QThread::started, m_worker, &RtWizardRenderWorker::run);
connect(m_worker, &RtWizardRenderWorker::output, m_log, &QPlainTextEdit::appendPlainText);
connect(m_worker, &RtWizardRenderWorker::finished, this, &RtWizardMainWindow::renderFinished);
connect(m_worker, &RtWizardRenderWorker::finished, thread, &QThread::quit);
connect(thread, &QThread::finished, m_worker, &QObject::deleteLater);
connect(thread, &QThread::finished, this, [this, thread]() {
if (m_renderThread == thread) {
m_renderThread = nullptr;
m_worker = nullptr;
updateValidation();
}
thread->deleteLater();
});
thread->start();
}
void RtWizardMainWindow::cancelRender() { if (m_worker) m_worker->requestCancel(); }
void
RtWizardMainWindow::renderFinished(int status, const QString &message)
{
QString completion = message;
if (status == 0 && !QFileInfo::exists(m_activeOutput)) {
status = 1;
completion = tr("Render failed: the expected output file was not created.");
}
m_log->appendPlainText(completion); m_progress->setRange(0, 1); m_progress->setValue(status == 0 ? 1 : 0); m_cancelButton->setEnabled(false);
if (status == 0 && QFileInfo(m_activeOutput).suffix().compare("png", Qt::CaseInsensitive) == 0) { QPixmap pixmap(m_activeOutput); if (!pixmap.isNull()) { m_result->setPixmap(pixmap.scaled(m_result->size().isEmpty() ? QSize(640, 480) : m_result->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); m_result->show(); m_result->raise(); } }
statusBar()->showMessage(completion, 5000);
}
void
RtWizardMainWindow::importLegacySettings()
{
QSettings settings("BRL-CAD", "RtWizard"); if (settings.value("legacyImportComplete", false).toBool()) return;
QString path = qEnvironmentVariable("RTWIZARD_RCFILE"); if (path.isEmpty()) path = QDir::home().filePath(".rtwizardrc"); if (!QFileInfo::exists(path)) path = QDir::current().filePath(".rtwizardrc");
QFile file(path); int width = 0, height = 0; QList<int> panes; if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { while (!file.atEnd()) { QString line = QString::fromUtf8(file.readLine()).trimmed(); QRegularExpressionMatch match; match = QRegularExpression("^set\\s+::wizard_width\\s+([0-9]+)$").match(line); if (match.hasMatch()) width = match.captured(1).toInt(); match = QRegularExpression("^set\\s+::wizard_height\\s+([0-9]+)$").match(line); if (match.hasMatch()) height = match.captured(1).toInt(); match = QRegularExpression("^set\\s+::gpane\\s+\\{?([0-9]+)\\s+([0-9]+)\\}?$").match(line); if (match.hasMatch()) panes = {match.captured(1).toInt(), match.captured(2).toInt()}; } }
if (width > 0 && height > 0)
settings.setValue("legacySize", QSize(width, height));
if (panes.size() == 2)
settings.setValue("legacyPane", QVariant::fromValue(panes));
settings.setValue("legacyImportComplete", true);
}
void
RtWizardMainWindow::loadSettings()
{
QSettings settings("BRL-CAD", "RtWizard"); if (settings.contains("geometry")) restoreGeometry(settings.value("geometry").toByteArray()); else if (settings.contains("legacySize")) resize(settings.value("legacySize").toSize()); if (settings.value("layoutVersion", 0).toInt() == 2 && settings.contains("windowState")) restoreState(settings.value("windowState").toByteArray(), 2);
}
void RtWizardMainWindow::saveSettings() { QSettings settings("BRL-CAD", "RtWizard"); settings.setValue("geometry", saveGeometry()); settings.setValue("windowState", saveState(2)); settings.setValue("layoutVersion", 2); settings.setValue("previewSize", 256); }
void RtWizardMainWindow::closeEvent(QCloseEvent *event) { saveSettings(); QMainWindow::closeEvent(event); }
extern "C" int
rtwizard_gui(const char *program, const rtwizard_settings *settings, char picture_type)
{
int argc = 1;
QByteArray programName = QFile::encodeName(program && program[0] ? QString::fromLocal8Bit(program) : QStringLiteral("rtwizard"));
char *argv[] = {programName.data(), nullptr};
QApplication application(argc, argv);
application.setOrganizationName("BRL-CAD"); application.setOrganizationDomain("brlcad.org"); application.setApplicationName("RtWizard");
RtWizardMainWindow window(QString::fromLocal8Bit(program ? program : "rtwizard"),
settings, picture_type);
window.show();
return application.exec();
}
+201
View File
@@ -0,0 +1,201 @@
/* G U I . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*/
#ifndef RTWIZARD_GUI_H
#define RTWIZARD_GUI_H
#include "common.h"
struct rtwizard_settings;
__BEGIN_DECLS
int rtwizard_gui(const char *program, const struct rtwizard_settings *settings,
char picture_type);
__END_DECLS
#ifdef __cplusplus
#include <atomic>
#include <string>
#include <QMainWindow>
#include <QObject>
#include <QStringList>
class QComboBox;
class QDockWidget;
class QDoubleSpinBox;
class QFormLayout;
class QLabel;
class QLineEdit;
class QListWidget;
class QMenu;
class QPlainTextEdit;
class QProgressBar;
class QPushButton;
class QSpinBox;
class QStackedWidget;
class QTabWidget;
class QTableWidget;
class QThread;
class QWidget;
class QgModel;
class QgTreeView;
class QgView;
class RtWizardRenderWorker : public QObject
{
Q_OBJECT
public:
explicit RtWizardRenderWorker(const QStringList &arguments);
void requestCancel();
public slots:
void run();
signals:
void output(const QString &text);
void finished(int status, const QString &message);
private:
QStringList m_arguments;
std::atomic_bool m_cancelled{false};
};
class RtWizardMainWindow : public QMainWindow
{
Q_OBJECT
public:
RtWizardMainWindow(const QString &program, const rtwizard_settings *settings,
char pictureType);
~RtWizardMainWindow() override;
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
void openDatabase();
void openSpecification();
void saveSpecification();
void saveSpecificationAs();
void chooseOutput();
void addColorRole();
void addLineRole();
void addGhostRole();
void removeRole();
void autoviewScene();
void clearScene();
void refreshScene();
void updateSceneView(unsigned long long flags);
void updateValidation();
void updateAnimationPage();
void addCameraKeyframe();
void removeCameraKeyframe();
void renderPreview();
void renderFull();
void cancelRender();
void renderFinished(int status, const QString &message);
private:
void createActions();
void createWorkspace();
void loadDatabase(const QString &path);
void applySettings(const rtwizard_settings *settings, char pictureType);
bool loadSpecification(const QString &path);
bool writeSpecification(const QString &path, bool preview);
QString selectedObjectPath() const;
void addRole(QListWidget *list);
QStringList roleValues(const QListWidget *list) const;
void setRoleValues(QListWidget *list, const QStringList &values);
void attachModelView();
void showRolePage(int index);
void showSettingsPage(int index);
void startRender(bool preview);
void loadSettings();
void saveSettings();
void importLegacySettings();
void syncTimeline();
QString m_program;
QString m_database;
QString m_specPath;
QString m_previewOutput;
QString m_activeOutput;
std::string m_document;
QgModel *m_model = nullptr;
QgTreeView *m_tree = nullptr;
QgView *m_view = nullptr;
QWidget *m_treeContainer = nullptr;
QDockWidget *m_rolesDock = nullptr;
QDockWidget *m_settingsDock = nullptr;
QDockWidget *m_jobDock = nullptr;
QMenu *m_imageMenu = nullptr;
QMenu *m_stepsMenu = nullptr;
QLabel *m_result = nullptr;
QListWidget *m_colorRoles = nullptr;
QListWidget *m_lineRoles = nullptr;
QListWidget *m_ghostRoles = nullptr;
QTabWidget *m_roleTabs = nullptr;
QTabWidget *m_tabs = nullptr;
QComboBox *m_type = nullptr;
QSpinBox *m_width = nullptr;
QSpinBox *m_height = nullptr;
QLineEdit *m_background = nullptr;
QLineEdit *m_lineColor = nullptr;
QLineEdit *m_nonLineColor = nullptr;
QDoubleSpinBox *m_ghostIntensity = nullptr;
QSpinBox *m_occlusion = nullptr;
QSpinBox *m_aoSamples = nullptr;
QDoubleSpinBox *m_aoRadius = nullptr;
QComboBox *m_viewMode = nullptr;
QDoubleSpinBox *m_azimuth = nullptr;
QDoubleSpinBox *m_elevation = nullptr;
QDoubleSpinBox *m_twist = nullptr;
QDoubleSpinBox *m_zoom = nullptr;
QDoubleSpinBox *m_perspective = nullptr;
QLineEdit *m_center = nullptr;
QLineEdit *m_eye = nullptr;
QLineEdit *m_orientation = nullptr;
QDoubleSpinBox *m_viewSize = nullptr;
QLineEdit *m_outputPath = nullptr;
QLineEdit *m_frameDir = nullptr;
QLineEdit *m_fbDevice = nullptr;
QSpinBox *m_fbPort = nullptr;
QComboBox *m_fbTransport = nullptr;
QComboBox *m_animation = nullptr;
QStackedWidget *m_animationPages = nullptr;
QDoubleSpinBox *m_duration = nullptr;
QSpinBox *m_fps = nullptr;
QSpinBox *m_frames = nullptr;
QLineEdit *m_cutDirection = nullptr;
QDoubleSpinBox *m_orbitAngle = nullptr;
QLineEdit *m_orbitAxis = nullptr;
QLineEdit *m_orbitCenter = nullptr;
QDoubleSpinBox *m_orbitElevation = nullptr;
QDoubleSpinBox *m_orbitRadius = nullptr;
QLineEdit *m_turntableObject = nullptr;
QDoubleSpinBox *m_turntableAngle = nullptr;
QLineEdit *m_turntableAxis = nullptr;
QLineEdit *m_turntableCenter = nullptr;
QTableWidget *m_timeline = nullptr;
QDoubleSpinBox *m_keyTime = nullptr;
QLabel *m_advancedTracks = nullptr;
QLabel *m_validation = nullptr;
QPlainTextEdit *m_log = nullptr;
QProgressBar *m_progress = nullptr;
QPushButton *m_previewButton = nullptr;
QPushButton *m_renderButton = nullptr;
QPushButton *m_cancelButton = nullptr;
QThread *m_renderThread = nullptr;
RtWizardRenderWorker *m_worker = nullptr;
};
#endif /* __cplusplus */
#endif /* RTWIZARD_GUI_H */
+322 -812
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
/* R E N D E R . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*/
#ifndef RTWIZARD_RENDER_H
#define RTWIZARD_RENDER_H
#include "common.h"
struct rtwizard_settings;
__BEGIN_DECLS
struct rtwizard_render_callbacks {
void (*log)(void *data, const char *message);
void (*progress)(void *data, int completed, int total);
void (*frame)(void *data, const char *filename, int frame, int total);
int (*cancelled)(void *data);
};
/* Execute a validated rtwizard request without Tcl/Tk. errmsg, when set, is
* allocated with bu_malloc and is owned by the caller. */
int rtwizard_render(const struct rtwizard_settings *settings, char picture_type,
const struct rtwizard_render_callbacks *callbacks, void *callback_data,
char **errmsg);
__END_DECLS
#endif /* RTWIZARD_RENDER_H */
+107
View File
@@ -0,0 +1,107 @@
/* S E T T I N G S . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*/
#ifndef RTWIZARD_SETTINGS_H
#define RTWIZARD_SETTINGS_H
#include "common.h"
#include "vmath.h"
#include "bu/color.h"
#include "bu/ptbl.h"
#include "bu/vls.h"
#define RTWIZARD_SIZE_DEFAULT 512
#define RTWIZARD_MAGIC 0x72747769
enum rtwizard_fb_transport {
RTWIZARD_FB_AUTO = 0,
RTWIZARD_FB_IPC,
RTWIZARD_FB_TCP
};
/* Private application settings shared by the command-line parser and the
* native render engine. This is deliberately not an installed API. */
struct rtwizard_settings {
uint32_t magic;
int use_gui;
int no_gui;
int verbose;
struct bu_ptbl *color;
struct bu_ptbl *ghost;
struct bu_ptbl *line;
struct bu_vls *input_file;
struct bu_vls *output_file;
struct bu_vls *fb_dev;
int port;
int fb_transport;
struct bu_vls *log_file;
struct bu_vls *pid_file;
struct bu_vls *render_spec;
struct bu_vls *animation_file;
struct bu_vls *animation_preset;
struct bu_vls *frame_dir;
double animation_duration;
int animation_frames;
int animation_plays;
int animation_cyclic;
int resume;
double orbit_angle;
vect_t orbit_axis;
vect_t orbit_center;
double orbit_elevation;
double orbit_radius;
struct bu_vls *turntable_object;
double turntable_angle;
vect_t turntable_axis;
vect_t turntable_center;
struct bu_vls *save_view_keyframe;
double keyframe_time;
int replace_keyframe;
size_t width;
int width_set;
size_t height;
int height_set;
size_t size;
int size_set;
struct bu_color *bkg_color;
struct bu_color *line_color;
struct bu_color *non_line_color;
double ghost_intensity;
int occlusion;
int benchmark;
int cpus;
int cut_steps;
int animation_fps;
vect_t cut_direction;
int cut_direction_set;
int ao_samples;
double ao_radius;
double viewsize;
quat_t orientation;
vect_t eye_pt;
double az, el, tw;
double perspective;
double zoom;
vect_t center;
};
#endif /* RTWIZARD_SETTINGS_H */
+3 -2
View File
@@ -160,8 +160,9 @@ if(BRLCAD_ENABLE_TCL)
tclindex_build(tclscripts)
endif(BRLCAD_ENABLE_TCL)
# Put rtwizard last so we can define a build target
# that relies on the other tclscripts being set up.
# Keep the retired implementation in source distributions for migration
# reference. Its subdirectory no longer installs or indexes Tcl runtime
# files; the C++17/Qt application in src/rtwizard is self-contained.
add_subdirectory(rtwizard)
if(TARGET btclsh)
+253 -40
View File
@@ -43,13 +43,75 @@ proc ::pid_wait { pid } {
namespace eval cadwidgets {
proc rtimage_exec_log {cmd log_file} {
if {[catch {exec {*}$cmd >& $log_file} msg]} {
proc rtimage_control_word {word} {
if {[regexp {[;\r\n]} $word]} {
return -code error "rtwizard: animation object names may not contain command separators"
}
return \"[string map [list \\ \\\\ \" \\\"] $word]\"
}
proc rtimage_exec_log {cmd log_file {objects {}} {anim_commands {}}} {
if {[llength $anim_commands]} {
set script ""
foreach anim_cmd $anim_commands { append script $anim_cmd ";\n" }
append script "tree"
foreach object $objects { append script " " [rtimage_control_word $object] }
append script ";\nend;\n"
# Object arguments make rt take its immediate-render path, which does not
# consume the control stream. The script's typed tree command supplies
# those objects after the database and animations have been loaded.
set exec_cmd $cmd
if {[llength $objects]} { set exec_cmd [lrange $cmd 0 end-[llength $objects]] }
set status [catch {exec {*}$exec_cmd << $script >& $log_file} msg]
} else {
set status [catch {exec {*}$cmd >& $log_file} msg]
}
if {$status} {
return -code error "rtimage command failed: $cmd\n$msg"
}
}
proc rtimage_cut_animation {rtimage_dict} {
proc rtimage_bounds_center {dbfile objects} {
foreach {xmin xmax} [rtwizard_cut_bounds $dbfile {1 0 0} {*}$objects] break
foreach {ymin ymax} [rtwizard_cut_bounds $dbfile {0 1 0} {*}$objects] break
foreach {zmin zmax} [rtwizard_cut_bounds $dbfile {0 0 1} {*}$objects] break
return [list [expr {($xmin+$xmax)/2.0}] [expr {($ymin+$ymax)/2.0}] [expr {($zmin+$zmax)/2.0}]]
}
proc rtimage_rotate_vector {v axis angle_degrees} {
set a [vunitize $axis]
set r [expr {$angle_degrees * acos(-1.0) / 180.0}]
set c [expr {cos($r)}]
set s [expr {sin($r)}]
set dot [expr {[lindex $a 0]*[lindex $v 0]+[lindex $a 1]*[lindex $v 1]+[lindex $a 2]*[lindex $v 2]}]
set cross [list \
[expr {[lindex $a 1]*[lindex $v 2]-[lindex $a 2]*[lindex $v 1]}] \
[expr {[lindex $a 2]*[lindex $v 0]-[lindex $a 0]*[lindex $v 2]}] \
[expr {[lindex $a 0]*[lindex $v 1]-[lindex $a 1]*[lindex $v 0]}]]
return [list \
[expr {[lindex $v 0]*$c+[lindex $cross 0]*$s+[lindex $a 0]*$dot*(1.0-$c)}] \
[expr {[lindex $v 1]*$c+[lindex $cross 1]*$s+[lindex $a 1]*$dot*(1.0-$c)}] \
[expr {[lindex $v 2]*$c+[lindex $cross 2]*$s+[lindex $a 2]*$dot*(1.0-$c)}]]
}
proc rtimage_lookat_quat {direction {up {}}} {
set forward [vunitize $direction]
if {![llength $up]} { return [quat_mat2quat [mat_lookat $forward 0]] }
set right [vcross $forward [vunitize $up]]
if {[magnitude $right] < 1.0e-12} {
return [quat_mat2quat [mat_lookat $forward 0]]
}
set right [vunitize $right]
set camera_up [vcross $right $forward]
set m [list \
[lindex $right 0] [lindex $right 1] [lindex $right 2] 0 \
[lindex $camera_up 0] [lindex $camera_up 1] [lindex $camera_up 2] 0 \
[expr {-[lindex $forward 0]}] [expr {-[lindex $forward 1]}] [expr {-[lindex $forward 2]}] 0 \
0 0 0 1]
return [quat_mat2quat $m]
}
proc rtimage_preset_plan {rtimage_dict} {
foreach param [dict keys $rtimage_dict] {
set $param [dict get $rtimage_dict $param]
}
@@ -61,12 +123,29 @@ proc rtimage_cut_animation {rtimage_dict} {
if {![llength $anim_objects]} {
return -code error "rtwizard: no objects available for cutting-plane animation bounds"
}
if {![info exists ::RtWizard::wizard_state(output_filename)] ||
$::RtWizard::wizard_state(output_filename) eq ""} {
return -code error "rtwizard: cutting-plane animation requires a file output"
set preset [expr {[info exists ::RtWizard::wizard_state(animation_preset)] ?
$::RtWizard::wizard_state(animation_preset) : "cut"}]
set fps $::RtWizard::wizard_state(animation_fps)
set duration $::RtWizard::wizard_state(animation_duration)
set frame_count $::RtWizard::wizard_state(animation_frames)
set cyclic [expr {$preset in {orbit turntable} &&
abs($::RtWizard::wizard_state(${preset}_angle)) >= 360.0}]
if {[info exists ::RtWizard::wizard_state(animation_cyclic)] &&
$::RtWizard::wizard_state(animation_cyclic) >= 0} {
set cyclic $::RtWizard::wizard_state(animation_cyclic)
}
if {$frame_count < 2} { set frame_count [expr {round($duration*$fps) + ($cyclic ? 0 : 1)}] }
if {$frame_count < 2} { return -code error "rtwizard: animation requires at least two frames" }
set plays $::RtWizard::wizard_state(animation_plays)
if {$plays < 0} { set plays [expr {$cyclic ? 0 : 1}] }
set frames {}
if {[info exists ::RtWizard::wizard_state(cut_direction)] &&
if {$preset eq "cut"} {
if {[info exists ::RtWizard::wizard_state(cut_steps)] && $::RtWizard::wizard_state(cut_steps) >= 2} {
set frame_count $::RtWizard::wizard_state(cut_steps)
}
if {[info exists ::RtWizard::wizard_state(cut_direction)] &&
[string trim $::RtWizard::wizard_state(cut_direction)] ne ""} {
if {[catch {set cut_dir [vunitize $::RtWizard::wizard_state(cut_direction)]}]} {
return -code error "rtwizard: cutting direction must be a non-zero XYZ vector"
@@ -77,7 +156,7 @@ proc rtimage_cut_animation {rtimage_dict} {
[expr {-[lindex $omat 9]}] [expr {-[lindex $omat 10]}]]]
}
set sweep_bounds [rtwizard_cut_bounds $_dbfile $cut_dir {*}$anim_objects]
set sweep_bounds [rtwizard_cut_bounds $_dbfile $cut_dir {*}$anim_objects]
set sweep_min [lindex $sweep_bounds 0]
set sweep_max [lindex $sweep_bounds 1]
set sweep_span [expr {$sweep_max - $sweep_min}]
@@ -90,34 +169,167 @@ proc rtimage_cut_animation {rtimage_dict} {
abs($sweep_span) * 0.05, 1.0e-9)}]
set sweep_first [expr {$sweep_min - $sweep_eps}]
set sweep_last [expr {$sweep_max - $sweep_eps}]
set frame_count $::RtWizard::wizard_state(cut_steps)
if {$frame_count < 2} {
return -code error "rtwizard: animation frame count must be at least 2"
for {set frame_no 0} {$frame_no < $frame_count} {incr frame_no} {
set fraction [expr {double($frame_no) / double($frame_count - 1)}]
set plane_dist [expr {$sweep_first + $fraction * ($sweep_last - $sweep_first)}]
set plane_pt [vscale $cut_dir $plane_dist]
lappend frames [dict create time [expr {$duration*$fraction}] \
viewsize $_viewsize orientation $_orientation eye_pt $_eye_pt perspective $_perspective \
cut_plane [format "%.15g,%.15g,%.15g,%.15g,%.15g,%.15g" \
[lindex $plane_pt 0] [lindex $plane_pt 1] [lindex $plane_pt 2] \
[lindex $cut_dir 0] [lindex $cut_dir 1] [lindex $cut_dir 2]] anim_commands {}]
}
} elseif {$preset eq "orbit"} {
set axis [vunitize $::RtWizard::wizard_state(orbit_axis)]
if {[info exists ::RtWizard::wizard_state(orbit_center)]} {
set center $::RtWizard::wizard_state(orbit_center)
} else {
set center [rtimage_bounds_center $_dbfile $anim_objects]
}
set radial [list [expr {[lindex $_eye_pt 0]-[lindex $center 0]}] \
[expr {[lindex $_eye_pt 1]-[lindex $center 1]}] [expr {[lindex $_eye_pt 2]-[lindex $center 2]}]]
set radius [magnitude $radial]
if {$radius < 1.0e-12} {
if {abs([lindex $axis 2]) < 0.9} { set reference {0 0 1} } else { set reference {1 0 0} }
set radius $_viewsize
set radial [vscale [vunitize [vcross $axis $reference]] $radius]
}
if {[info exists ::RtWizard::wizard_state(orbit_radius)]} {
set radius $::RtWizard::wizard_state(orbit_radius)
set radial [vscale [vunitize $radial] $radius]
}
if {[info exists ::RtWizard::wizard_state(orbit_elevation)]} {
set elev [expr {$::RtWizard::wizard_state(orbit_elevation)*acos(-1.0)/180.0}]
set along [expr {[lindex $radial 0]*[lindex $axis 0]+[lindex $radial 1]*[lindex $axis 1]+[lindex $radial 2]*[lindex $axis 2]}]
set plane [list [expr {[lindex $radial 0]-$along*[lindex $axis 0]}] \
[expr {[lindex $radial 1]-$along*[lindex $axis 1]}] [expr {[lindex $radial 2]-$along*[lindex $axis 2]}]]
if {[magnitude $plane] < 1.0e-12} {
if {abs([lindex $axis 2]) < 0.9} { set reference {0 0 1} } else { set reference {1 0 0} }
set plane [vcross $axis $reference]
}
set radial [vadd2 [vscale [vunitize $plane] [expr {$radius*cos($elev)}]] \
[vscale $axis [expr {$radius*sin($elev)}]]]
}
for {set frame_no 0} {$frame_no < $frame_count} {incr frame_no} {
set fraction [expr {double($frame_no) / double($cyclic ? $frame_count : $frame_count-1)}]
set rv [rtimage_rotate_vector $radial $axis [expr {$fraction*$::RtWizard::wizard_state(orbit_angle)}]]
set eye [vadd2 $center $rv]
set dir [vunitize [list [expr {[lindex $center 0]-[lindex $eye 0]}] \
[expr {[lindex $center 1]-[lindex $eye 1]}] [expr {[lindex $center 2]-[lindex $eye 2]}]]]
set q [rtimage_lookat_quat $dir $axis]
lappend frames [dict create time [expr {$duration*$fraction}] viewsize $_viewsize \
orientation $q eye_pt $eye perspective $_perspective cut_plane "" anim_commands {}]
}
} elseif {$preset eq "turntable"} {
set axis [vunitize $::RtWizard::wizard_state(turntable_axis)]
if {[regexp {[[:space:];]} $::RtWizard::wizard_state(turntable_object)]} {
return -code error "rtwizard: turntable object paths may not contain whitespace or semicolons"
}
set turntable_path $::RtWizard::wizard_state(turntable_object)
if {[string first "/" $turntable_path] < 0} { set turntable_path "/$turntable_path" }
if {[info exists ::RtWizard::wizard_state(turntable_center)]} {
set center $::RtWizard::wizard_state(turntable_center)
} else {
set center [rtimage_bounds_center $_dbfile [list $::RtWizard::wizard_state(turntable_object)]]
}
foreach {x y z} $axis break
foreach {px py pz} $center break
for {set frame_no 0} {$frame_no < $frame_count} {incr frame_no} {
set fraction [expr {double($frame_no) / double($cyclic ? $frame_count : $frame_count-1)}]
set a [expr {$fraction*$::RtWizard::wizard_state(turntable_angle)*acos(-1.0)/180.0}]
set c [expr {cos($a)}]; set s [expr {sin($a)}]; set m [expr {1.0-$c}]
set r00 [expr {$c+$x*$x*$m}]; set r01 [expr {$x*$y*$m-$z*$s}]; set r02 [expr {$x*$z*$m+$y*$s}]
set r10 [expr {$y*$x*$m+$z*$s}]; set r11 [expr {$c+$y*$y*$m}]; set r12 [expr {$y*$z*$m-$x*$s}]
set r20 [expr {$z*$x*$m-$y*$s}]; set r21 [expr {$z*$y*$m+$x*$s}]; set r22 [expr {$c+$z*$z*$m}]
set tx [expr {$px-($r00*$px+$r01*$py+$r02*$pz)}]
set ty [expr {$py-($r10*$px+$r11*$py+$r12*$pz)}]
set tz [expr {$pz-($r20*$px+$r21*$py+$r22*$pz)}]
set cmd [format "anim %s matrix lmul %.17g %.17g %.17g %.17g %.17g %.17g %.17g %.17g %.17g %.17g %.17g %.17g 0 0 0 1" \
$turntable_path $r00 $r01 $r02 $tx $r10 $r11 $r12 $ty $r20 $r21 $r22 $tz]
lappend frames [dict create time [expr {$duration*$fraction}] viewsize $_viewsize \
orientation $_orientation eye_pt $_eye_pt perspective $_perspective cut_plane "" anim_commands [list $cmd]]
}
} else {
return -code error "rtwizard: unknown animation preset '$preset'"
}
return [dict create duration $duration fps $fps cyclic $cyclic plays $plays frames $frames]
}
proc rtimage_animation {rtimage_dict} {
foreach param [dict keys $rtimage_dict] { set $param [dict get $rtimage_dict $param] }
if {[info exists ::RtWizard::wizard_state(animation_file)] &&
$::RtWizard::wizard_state(animation_file) ne ""} {
set local2base 1.0
if {![catch {set unit_name [db units -s]}] && $unit_name ne ""} {
set local2base [bu_units_conversion $unit_name]
}
set plan [rtwizard_animation_json $::RtWizard::wizard_state(animation_file) \
$_viewsize $_orientation $_eye_pt $_perspective \
$::RtWizard::wizard_state(animation_duration) $::RtWizard::wizard_state(animation_fps) \
$::RtWizard::wizard_state(animation_frames) $::RtWizard::wizard_state(animation_plays) \
$::RtWizard::wizard_state(animation_cyclic) $local2base]
} else {
set plan [rtimage_preset_plan $rtimage_dict]
}
dict set rtimage_dict _animate 0
set animation_frames {}
set frame_dir [file dirname $::RtWizard::wizard_state(output_filename)]
set temp_dir [expr {[info exists ::RtWizard::wizard_state(output_filename)] ?
[file dirname $::RtWizard::wizard_state(output_filename)] : [pwd]}]
set keep_dir ""
if {[info exists ::RtWizard::wizard_state(frame_dir)] && $::RtWizard::wizard_state(frame_dir) ne ""} {
set keep_dir $::RtWizard::wizard_state(frame_dir)
file mkdir $keep_dir
}
set output_tmp ""
try {
set frames [dict get $plan frames]
set frame_count [llength $frames]
for {set frame_no 0} {$frame_no < $frame_count} {incr frame_no} {
set fraction [expr {double($frame_no) / double($frame_count - 1)}]
set plane_dist [expr {$sweep_first + $fraction * ($sweep_last - $sweep_first)}]
set plane_pt [vscale $cut_dir $plane_dist]
dict set rtimage_dict _cut_plane [format "%.15g,%.15g,%.15g,%.15g,%.15g,%.15g" \
[lindex $plane_pt 0] [lindex $plane_pt 1] [lindex $plane_pt 2] \
[lindex $cut_dir 0] [lindex $cut_dir 1] [lindex $cut_dir 2]]
if {$keep_dir ne ""} {
set frame_file [file join $keep_dir [format "frame-%06d.png" $frame_no]]
if {[file exists $frame_file]} {
if {[info exists ::RtWizard::wizard_state(resume_animation)] &&
$::RtWizard::wizard_state(resume_animation) &&
[rtwizard_image_valid $frame_file $_w $_n]} {
puts "Using existing frame [expr {$frame_no + 1}]/$frame_count"
lappend animation_frames $frame_file
continue
}
if {![info exists ::RtWizard::wizard_state(resume_animation)] ||
!$::RtWizard::wizard_state(resume_animation)} {
return -code error "rtwizard: frame already exists: $frame_file (use --resume)"
}
}
} else {
set frame_file [file join $temp_dir [format ".rtwizard-%d-%06d.pix" [pid] $frame_no]]
}
set frame [lindex $frames $frame_no]
foreach key {viewsize orientation eye_pt perspective cut_plane anim_commands} {
dict set rtimage_dict _$key [dict get $frame $key]
}
puts "Rendering frame [expr {$frame_no + 1}]/$frame_count"
catch {exec [file join [bu_dir bin] fbclear] -F $_port \
[lindex $_bgcolor 0] [lindex $_bgcolor 1] [lindex $_bgcolor 2]}
rtimage $rtimage_dict
set frame_file [file join $frame_dir [format ".rtwizard-%d-%06d.pix" [pid] $frame_no]]
exec [file join [bu_dir bin] fb-pix] -w $_w -n $_n -F $_port $frame_file 2>@1
if {$keep_dir ne ""} {
set frame_tmp "${frame_file}.tmp-[pid]"
exec [file join [bu_dir bin] fb-png] -w $_w -n $_n -F $_port $frame_tmp 2>@1
file rename -force $frame_tmp $frame_file
} else {
exec [file join [bu_dir bin] fb-pix] -w $_w -n $_n -F $_port $frame_file 2>@1
}
lappend animation_frames $frame_file
}
rtwizard_anim_write $::RtWizard::wizard_state(output_filename) $_w $_n \
$::RtWizard::wizard_state(animation_fps) {*}$animation_frames
if {[info exists ::RtWizard::wizard_state(output_filename)] && $::RtWizard::wizard_state(output_filename) ne ""} {
set output $::RtWizard::wizard_state(output_filename)
set output_tmp "${output}.tmp-[pid][file extension $output]"
rtwizard_anim_write $output_tmp $_w $_n \
[dict get $plan fps] [dict get $plan plays] {*}$animation_frames
file rename -force $output_tmp $output
}
} finally {
foreach frame_file $animation_frames { catch {file delete -force $frame_file} }
if {$output_tmp ne ""} { catch {file delete -force $output_tmp} }
if {$keep_dir eq ""} { foreach frame_file $animation_frames { catch {file delete -force $frame_file} } }
}
return 1
}
@@ -127,7 +339,7 @@ proc rtimage {rtimage_dict} {
global env
set necessary_vars [list _dbfile _port _w _n _viewsize _orientation \
_eye_pt _perspective _bgcolor _ecolor _necolor _occmode _gamma _benchmark_mode \
_cut_plane _ao_samples _ao_radius _animate]
_cut_plane _anim_commands _ao_samples _ao_radius _animate]
set necessary_lists [list _color_objects _ghost_objects _edge_objects]
# It's the responsibility of the calling function
@@ -149,7 +361,7 @@ proc rtimage {rtimage_dict} {
if {$_animate ne "" && $_animate &&
[info exists ::RtWizard::wizard_state(make_animation)] &&
$::RtWizard::wizard_state(make_animation)} {
return [rtimage_cut_animation $rtimage_dict]
return [rtimage_animation $rtimage_dict]
}
set ar [ expr $_w.0 / $_n.0 ]
@@ -201,8 +413,8 @@ proc rtimage {rtimage_dict} {
-C [lindex $_bgcolor 0]/[lindex $_bgcolor 1]/[lindex $_bgcolor 2] \
-c "viewsize $_viewsize" \
-c "orientation $_orientation" \
-c "eye_pt $_eye_pt" \
$_dbfile
-c "eye_pt $_eye_pt"
lappend cmd $_dbfile
foreach obj $_color_objects {
lappend cmd $obj
@@ -213,7 +425,7 @@ proc rtimage {rtimage_dict} {
#
# Run rt to generate the color insert
#
rtimage_exec_log $cmd $_log_file
rtimage_exec_log $cmd $_log_file $_color_objects $_anim_commands
# Look for color objects that also get edges
if {[llength $_edge_objects] && [llength $_ecolor] == 3} {
@@ -259,8 +471,8 @@ proc rtimage {rtimage_dict} {
-c "set ov=1" \
-c "viewsize $_viewsize" \
-c "orientation $_orientation" \
-c "eye_pt $_eye_pt" \
$_dbfile
-c "eye_pt $_eye_pt"
lappend cmd $_dbfile
foreach obj $ce_objects {
lappend cmd $obj
@@ -273,7 +485,7 @@ proc rtimage {rtimage_dict} {
#
# Run rtedge to generate the full-color with edges
#
rtimage_exec_log $cmd $_log_file
rtimage_exec_log $cmd $_log_file $ce_objects $_anim_commands
}
}
@@ -312,8 +524,8 @@ proc rtimage {rtimage_dict} {
-C [lindex $_bgcolor 0]/[lindex $_bgcolor 1]/[lindex $_bgcolor 2] \
-c "viewsize $_viewsize" \
-c "orientation $_orientation" \
-c "eye_pt $_eye_pt" \
$_dbfile
-c "eye_pt $_eye_pt"
lappend cmd $_dbfile
foreach obj $_ghost_objects {
lappend cmd $obj
@@ -324,7 +536,7 @@ proc rtimage {rtimage_dict} {
#
# Run rt to generate the full-color version of the ghost image
#
rtimage_exec_log $cmd $_log_file
rtimage_exec_log $cmd $_log_file $_ghost_objects $_anim_commands
set cmd [list [file join $binpath rt] -w $_w -n $_n]
if {$_benchmark_mode != ""} {
@@ -346,8 +558,8 @@ proc rtimage {rtimage_dict} {
-C [lindex $_bgcolor 0]/[lindex $_bgcolor 1]/[lindex $_bgcolor 2] \
-c "viewsize $_viewsize" \
-c "orientation $_orientation" \
-c "eye_pt $_eye_pt" \
$_dbfile
-c "eye_pt $_eye_pt"
lappend cmd $_dbfile
foreach obj $occlude_objects {
lappend cmd $obj
@@ -358,7 +570,7 @@ proc rtimage {rtimage_dict} {
#
# Run rt to generate the full-color version of the occlude_objects (i.e. color and ghost)
#
rtimage_exec_log $cmd $_log_file
rtimage_exec_log $cmd $_log_file $occlude_objects $_anim_commands
#
# Convert to ghost image
@@ -425,8 +637,8 @@ proc rtimage {rtimage_dict} {
lappend cmd \
-c "viewsize $_viewsize" \
-c "orientation $_orientation" \
-c "eye_pt $_eye_pt" \
$_dbfile
-c "eye_pt $_eye_pt"
lappend cmd $_dbfile
foreach obj $_edge_objects {
lappend cmd $obj
}
@@ -434,7 +646,8 @@ proc rtimage {rtimage_dict} {
#puts "RTEDGE: $cmd"
# Run rtedge to generate the full-color version of the ghost image
rtimage_exec_log $cmd $_log_file
# !!! manually write an rtedge log
rtimage_exec_log $cmd rtedge.log $_edge_objects $_anim_commands
}
catch {file delete -force $tgi}
+19 -14
View File
@@ -1,11 +1,16 @@
# The Qt implementation replaced these runtime assets. Keep their source
# manifest intact for historical reference without installing or indexing
# them in new builds.
set(RTWIZARD_INSTALL_LEGACY_TCL OFF)
add_subdirectory(lib)
set(rtwizard_TCLSCRIPTS RaytraceWizard.tcl rtwizard)
if(BRLCAD_ENABLE_TCL)
if(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
brlcad_adddata(rtwizard_TCLSCRIPTS tclscripts/rtwizard)
pkgindex_build(tclscripts/rtwizard)
tclindex_build(tclscripts/rtwizard)
endif(BRLCAD_ENABLE_TCL)
endif(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
# Examples
@@ -17,9 +22,9 @@ set(
examples/PictureTypeA/title.txt
examples/PictureTypeA/preview.small.gif
)
if(BRLCAD_ENABLE_TCL)
if(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
brlcad_adddata(PictureTypeA_DATA tclscripts/rtwizard/examples/PictureTypeA)
endif(BRLCAD_ENABLE_TCL)
endif(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
set(
PictureTypeB_DATA
@@ -29,9 +34,9 @@ set(
examples/PictureTypeB/title.txt
examples/PictureTypeB/preview.small.gif
)
if(BRLCAD_ENABLE_TCL)
if(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
brlcad_adddata(PictureTypeB_DATA tclscripts/rtwizard/examples/PictureTypeB)
endif(BRLCAD_ENABLE_TCL)
endif(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
set(
PictureTypeC_DATA
@@ -41,9 +46,9 @@ set(
examples/PictureTypeC/title.txt
examples/PictureTypeC/preview.small.gif
)
if(BRLCAD_ENABLE_TCL)
if(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
brlcad_adddata(PictureTypeC_DATA tclscripts/rtwizard/examples/PictureTypeC)
endif(BRLCAD_ENABLE_TCL)
endif(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
set(
PictureTypeD_DATA
@@ -53,9 +58,9 @@ set(
examples/PictureTypeD/title.txt
examples/PictureTypeD/preview.small.gif
)
if(BRLCAD_ENABLE_TCL)
if(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
brlcad_adddata(PictureTypeD_DATA tclscripts/rtwizard/examples/PictureTypeD)
endif(BRLCAD_ENABLE_TCL)
endif(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
set(
PictureTypeE_DATA
@@ -65,9 +70,9 @@ set(
examples/PictureTypeE/title.txt
examples/PictureTypeE/preview.small.gif
)
if(BRLCAD_ENABLE_TCL)
if(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
brlcad_adddata(PictureTypeE_DATA tclscripts/rtwizard/examples/PictureTypeE)
endif(BRLCAD_ENABLE_TCL)
endif(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
set(
PictureTypeF_DATA
@@ -77,9 +82,9 @@ set(
examples/PictureTypeF/title.txt
examples/PictureTypeF/preview.small.gif
)
if(BRLCAD_ENABLE_TCL)
if(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
brlcad_adddata(PictureTypeF_DATA tclscripts/rtwizard/examples/PictureTypeF)
endif(BRLCAD_ENABLE_TCL)
endif(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
cmakefiles(
CMakeLists.txt
+2 -2
View File
@@ -20,11 +20,11 @@ set(
PictureTypeF.itcl
Wizard.itk
)
if(BRLCAD_ENABLE_TCL)
if(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
brlcad_adddata(rtwizard_lib_TCLSCRIPTS tclscripts/rtwizard/lib)
pkgindex_build(tclscripts/rtwizard/lib)
tclindex_build(tclscripts/rtwizard/lib)
endif(BRLCAD_ENABLE_TCL)
endif(RTWIZARD_INSTALL_LEGACY_TCL AND BRLCAD_ENABLE_TCL)
cmakefiles(
CMakeLists.txt
+79 -8
View File
@@ -188,9 +188,23 @@ namespace eval RtWizard {
#
::itcl::body FbPage::constructor {args} {
if {![info exists ::RtWizard::wizard_state(make_animation)]} { set ::RtWizard::wizard_state(make_animation) 0 }
if {![info exists ::RtWizard::wizard_state(cut_steps)]} { set ::RtWizard::wizard_state(cut_steps) 30 }
if {![info exists ::RtWizard::wizard_state(animation_fps)]} { set ::RtWizard::wizard_state(animation_fps) 10 }
if {![info exists ::RtWizard::wizard_state(animation_preset)]} { set ::RtWizard::wizard_state(animation_preset) "" }
if {![info exists ::RtWizard::wizard_state(animation_fps)] || $::RtWizard::wizard_state(animation_fps) <= 0} {
set ::RtWizard::wizard_state(animation_fps) 10
}
if {![info exists ::RtWizard::wizard_state(animation_duration)] || $::RtWizard::wizard_state(animation_duration) <= 0} {
set ::RtWizard::wizard_state(animation_duration) 5
}
if {![info exists ::RtWizard::wizard_state(animation_frames)] || $::RtWizard::wizard_state(animation_frames) < 2} {
set ::RtWizard::wizard_state(animation_frames) 30
}
if {![info exists ::RtWizard::wizard_state(animation_plays)]} { set ::RtWizard::wizard_state(animation_plays) -1 }
if {![info exists ::RtWizard::wizard_state(cut_direction)]} { set ::RtWizard::wizard_state(cut_direction) "" }
if {![info exists ::RtWizard::wizard_state(orbit_angle)]} { set ::RtWizard::wizard_state(orbit_angle) 360 }
if {![info exists ::RtWizard::wizard_state(orbit_axis)]} { set ::RtWizard::wizard_state(orbit_axis) "0 0 1" }
if {![info exists ::RtWizard::wizard_state(turntable_object)]} { set ::RtWizard::wizard_state(turntable_object) "" }
if {![info exists ::RtWizard::wizard_state(turntable_angle)]} { set ::RtWizard::wizard_state(turntable_angle) 360 }
if {![info exists ::RtWizard::wizard_state(turntable_axis)]} { set ::RtWizard::wizard_state(turntable_axis) "0 0 1" }
if {![info exists ::RtWizard::wizard_state(ao_samples)]} { set ::RtWizard::wizard_state(ao_samples) 0 }
if {![info exists ::RtWizard::wizard_state(ao_radius)]} { set ::RtWizard::wizard_state(ao_radius) 0 }
@@ -335,17 +349,32 @@ namespace eval RtWizard {
usual
}
itk_component add cutAnimation {
checkbutton $devCs.cutAnimation \
-text "Cutting-plane animation" \
-variable ::RtWizard::wizard_state(make_animation)
itk_component add animationMode {
iwidgets::radiobox $devCs.animationMode -labeltext "Output:" -orient horizontal
}
$itk_component(animationMode) add still -text "Still" -command {
set ::RtWizard::wizard_state(animation_preset) ""
set ::RtWizard::wizard_state(make_animation) 0
}
$itk_component(animationMode) add cut -text "Cut" -command {
set ::RtWizard::wizard_state(animation_preset) cut
set ::RtWizard::wizard_state(make_animation) 1
}
$itk_component(animationMode) add orbit -text "Orbit" -command {
set ::RtWizard::wizard_state(animation_preset) orbit
set ::RtWizard::wizard_state(make_animation) 1
}
$itk_component(animationMode) add turntable -text "Turntable" -command {
set ::RtWizard::wizard_state(animation_preset) turntable
set ::RtWizard::wizard_state(make_animation) 1
}
$itk_component(animationMode) select 0
itk_component add cutSteps {
iwidgets::entryfield $devCs.cutSteps \
-labeltext "Frames:" -width 8 \
-validate integer \
-textvariable ::RtWizard::wizard_state(cut_steps)
-textvariable ::RtWizard::wizard_state(animation_frames)
} { usual }
itk_component add animationFps {
@@ -355,12 +384,48 @@ namespace eval RtWizard {
-textvariable ::RtWizard::wizard_state(animation_fps)
} { usual }
itk_component add animationDuration {
iwidgets::entryfield $devCs.animationDuration \
-labeltext "Duration (sec):" -width 8 \
-textvariable ::RtWizard::wizard_state(animation_duration)
} { usual }
itk_component add cutDirection {
iwidgets::entryfield $devCs.cutDirection \
-labeltext "Direction XYZ (blank = view):" -width 24 \
-textvariable ::RtWizard::wizard_state(cut_direction)
} { usual }
itk_component add orbitSettings {
iwidgets::entryfield $devCs.orbitSettings \
-labeltext "Orbit angle:" -width 24 \
-textvariable ::RtWizard::wizard_state(orbit_angle)
} { usual }
itk_component add orbitAxis {
iwidgets::entryfield $devCs.orbitAxis \
-labeltext "Orbit axis XYZ:" -width 24 \
-textvariable ::RtWizard::wizard_state(orbit_axis)
} { usual }
itk_component add turntableObject {
iwidgets::entryfield $devCs.turntableObject \
-labeltext "Turntable object path:" -width 24 \
-textvariable ::RtWizard::wizard_state(turntable_object)
} { usual }
itk_component add turntableSettings {
iwidgets::entryfield $devCs.turntableSettings \
-labeltext "Turntable angle:" -width 12 \
-textvariable ::RtWizard::wizard_state(turntable_angle)
} { usual }
itk_component add turntableAxis {
iwidgets::entryfield $devCs.turntableAxis \
-labeltext "Turntable axis XYZ:" -width 24 \
-textvariable ::RtWizard::wizard_state(turntable_axis)
} { usual }
itk_component add aoSamples {
iwidgets::entryfield $devCs.aoSamples \
-labeltext "Ambient samples:" -width 8 \
@@ -407,10 +472,16 @@ namespace eval RtWizard {
pack $itk_component(radBox) -anchor w
pack $itk_component(fileName) -anchor w
pack $itk_component(browse) -anchor e
pack $itk_component(cutAnimation) -anchor w
pack $itk_component(animationMode) -anchor w
pack $itk_component(cutSteps) -anchor w
pack $itk_component(animationFps) -anchor w
pack $itk_component(animationDuration) -anchor w
pack $itk_component(cutDirection) -anchor w
pack $itk_component(orbitSettings) -anchor w
pack $itk_component(orbitAxis) -anchor w
pack $itk_component(turntableObject) -anchor w
pack $itk_component(turntableSettings) -anchor w
pack $itk_component(turntableAxis) -anchor w
pack $itk_component(aoSamples) -anchor w
pack $itk_component(aoRadius) -anchor w
+30 -83
View File
@@ -130,7 +130,11 @@ if {[info exists ::use_gui]} {
if {[info exists argv]} {exit}
} else {
if {![info exists ::RtWizard::wizard_state(output_filename)] && ![info exists ::RtWizard::wizard_state(fbserv_port)] && ![info exists ::RtWizard::wizard_state(fbserv_device)]} {
if {![info exists ::RtWizard::wizard_state(output_filename)] &&
![info exists ::RtWizard::wizard_state(frame_dir)] &&
![info exists ::RtWizard::wizard_state(save_view_keyframe)] &&
![info exists ::RtWizard::wizard_state(fbserv_port)] &&
![info exists ::RtWizard::wizard_state(fbserv_device)]} {
set ::RtWizard::wizard_state(output_filename) rtwizard.pix
if {![file exists $::RtWizard::wizard_state(output_filename)]} {
puts "Warning - no output file or framebuffer specified - using file rtwizard.pix for output."
@@ -138,8 +142,15 @@ if {[info exists ::use_gui]} {
}
if {[info exists ::RtWizard::wizard_state(output_filename)]} {
if {[file exists $::RtWizard::wizard_state(output_filename)]} {
puts "Error - cannot create output file, $::RtWizard::wizard_state(output_filename) already exists."
if {[info exists argv]} {exit}
set resume_output [expr {
[info exists ::RtWizard::wizard_state(make_animation)] &&
$::RtWizard::wizard_state(make_animation) &&
[info exists ::RtWizard::wizard_state(resume_animation)] &&
$::RtWizard::wizard_state(resume_animation)}]
if {!$resume_output} {
puts "Error - cannot create output file, $::RtWizard::wizard_state(output_filename) already exists."
if {[info exists argv]} {exit}
}
}
}
@@ -332,6 +343,15 @@ if {[info exists ::use_gui]} {
set eye_pt [lrange [lindex $vdata 2] 1 end]
}
set save_only 0
if {[info exists ::RtWizard::wizard_state(save_view_keyframe)]} {
rtwizard_save_view_keyframe $::RtWizard::wizard_state(save_view_keyframe) \
$::RtWizard::wizard_state(keyframe_time) $::RtWizard::wizard_state(replace_keyframe) \
$eye_pt $orientation $viewsize $::RtWizard::wizard_state(perspective)
puts "Saved view keyframe at $::RtWizard::wizard_state(keyframe_time) seconds to $::RtWizard::wizard_state(save_view_keyframe)"
set save_only 1
}
# populate a dictionary to pass to rtimage
set rtimage_dict [dict create \
_dbfile $::RtWizard::wizard_state(dbFile) \
@@ -354,95 +374,22 @@ if {[info exists ::use_gui]} {
_log_file $::RtWizard::wizard_state(log_file) \
_pid_filename $::RtWizard::wizard_state(pid_filename) \
_cut_plane "" \
_anim_commands {} \
_ao_samples $::RtWizard::wizard_state(ao_samples) \
_ao_radius $::RtWizard::wizard_state(ao_radius)
_ao_radius $::RtWizard::wizard_state(ao_radius) \
_animate 1
]
if {[info exists ::RtWizard::wizard_state(cut_steps)] && $::RtWizard::wizard_state(cut_steps) >= 2} {
set anim_objects [lsort -unique [concat \
$::RtWizard::wizard_state(color_objlist) \
$::RtWizard::wizard_state(line_objlist) \
$::RtWizard::wizard_state(ghost_objlist)]]
if {![llength $anim_objects]} {
error "rtwizard: no objects available for cutting-plane animation bounds"
}
if {[info exists ::RtWizard::wizard_state(cut_direction)]} {
set cut_dir [vunitize $::RtWizard::wizard_state(cut_direction)]
} else {
# orientation is the model-to-view rotation. Its third row points
# from the model toward the eye, so negate it to march into the view.
set omat [quat_quat2mat $orientation]
set cut_dir [vunitize [list \
[expr {-[lindex $omat 8]}] \
[expr {-[lindex $omat 9]}] \
[expr {-[lindex $omat 10]}]]]
}
set sweep_bounds [rtwizard_cut_bounds \
$::RtWizard::wizard_state(dbFile) $cut_dir {*}$anim_objects]
set sweep_min [lindex $sweep_bounds 0]
set sweep_max [lindex $sweep_bounds 1]
set sweep_span [expr {$sweep_max - $sweep_min}]
if {$sweep_span <= 0.0} {
error "rtwizard: rendered objects have degenerate cutting-plane bounds"
}
# Keep the terminal slice large enough to survive rasterization while
# remaining approximately one output-pixel fraction of the sweep.
set sweep_eps [expr {max(abs($sweep_span) / double(max(
$::RtWizard::wizard_state(width), $::RtWizard::wizard_state(scanlines))),
abs($sweep_span) * 0.05, 1.0e-9)}]
set sweep_first [expr {$sweep_min - $sweep_eps}]
set sweep_last [expr {$sweep_max - $sweep_eps}]
if {$::RtWizard::wizard_state(verbose)} {
puts "Cut sweep: direction=$cut_dir bounds=$sweep_min..$sweep_max margin=$sweep_eps"
}
set animation_frames {}
set frame_count $::RtWizard::wizard_state(cut_steps)
set frame_dir [file dirname $::RtWizard::wizard_state(output_filename)]
try {
for {set frame_no 0} {$frame_no < $frame_count} {incr frame_no} {
set fraction [expr {double($frame_no) / double($frame_count - 1)}]
set plane_dist [expr {$sweep_first + $fraction * ($sweep_last - $sweep_first)}]
set plane_pt [vscale $cut_dir $plane_dist]
set cut_plane [format "%.15g,%.15g,%.15g,%.15g,%.15g,%.15g" \
[lindex $plane_pt 0] [lindex $plane_pt 1] [lindex $plane_pt 2] \
[lindex $cut_dir 0] [lindex $cut_dir 1] [lindex $cut_dir 2]]
dict set rtimage_dict _cut_plane $cut_plane
puts "Rendering frame [expr {$frame_no + 1}]/$frame_count"
catch {exec [file join [bu_dir bin] fbclear] -F [rtwiz_fb_target] \
[lindex $::RtWizard::wizard_state(bg_color) 0] \
[lindex $::RtWizard::wizard_state(bg_color) 1] \
[lindex $::RtWizard::wizard_state(bg_color) 2]}
::cadwidgets::rtimage $rtimage_dict
set frame_file [file join $frame_dir [format ".rtwizard-%d-%06d.pix" [pid] $frame_no]]
exec [file join [bu_dir bin] fb-pix] \
-w $::RtWizard::wizard_state(width) \
-n $::RtWizard::wizard_state(scanlines) \
-F [rtwiz_fb_target] $frame_file 2>@1
lappend animation_frames $frame_file
}
rtwizard_anim_write $::RtWizard::wizard_state(output_filename) \
$::RtWizard::wizard_state(width) \
$::RtWizard::wizard_state(scanlines) \
$::RtWizard::wizard_state(animation_fps) {*}$animation_frames
} finally {
foreach frame_file $animation_frames {
catch {file delete -force $frame_file}
}
}
if {$save_only} {
# View capture deliberately performs no rendering.
} else {
::cadwidgets::rtimage $rtimage_dict
}
if {[info exists ::RtWizard::wizard_state(output_filename)]} {
if {!$save_only && [info exists ::RtWizard::wizard_state(output_filename)]} {
set output_generated 0
if {[info exists ::RtWizard::wizard_state(cut_steps)] && $::RtWizard::wizard_state(cut_steps) >= 2} {
if {[info exists ::RtWizard::wizard_state(make_animation)] && $::RtWizard::wizard_state(make_animation)} {
set output_generated 1
}
if {!$output_generated && [file extension $::RtWizard::wizard_state(output_filename)] == ".png"} {