Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89a1ffa10a | ||
|
|
fb1a921bdd |
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
+12
-1
@@ -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));
|
||||
|
||||
@@ -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 */
|
||||
|
||||
+46
-23
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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`.
|
||||
@@ -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 §ion, 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 §ion,
|
||||
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");
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"} {
|
||||
|
||||
Reference in New Issue
Block a user