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
|
||||
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
/* B I N D I N G . H
|
||||
* BRL-CAD
|
||||
*
|
||||
* Copyright (c) 2026 United States Government as represented by
|
||||
* the U.S. Army Research Laboratory.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public License
|
||||
* version 2.1 as published by the Free Software Foundation.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this file; see the file named COPYING for more
|
||||
* information.
|
||||
*/
|
||||
|
||||
/** @addtogroup bu_binding
|
||||
* @brief
|
||||
* Universal input binding schema and action dispatch.
|
||||
*/
|
||||
/** @{ */
|
||||
/** @file bu/binding.h */
|
||||
|
||||
#ifndef BU_BINDING_H
|
||||
#define BU_BINDING_H
|
||||
|
||||
#include "common.h"
|
||||
#include "bu/defines.h"
|
||||
#include "bu/vls.h"
|
||||
|
||||
__BEGIN_DECLS
|
||||
|
||||
/* Standardized Attribute Keys (for W3C validation logic) */
|
||||
#define BU_BINDING_ATTR_TYPE "type"
|
||||
#define BU_BINDING_ATTR_KEY "key" /* Values MUST be W3C standard strings */
|
||||
#define BU_BINDING_ATTR_BUTTON "button" /* Values MUST be W3C button integers */
|
||||
#define BU_BINDING_ATTR_MODIFIERS "modifiers"
|
||||
|
||||
/* Standardized Event Types */
|
||||
#define BU_BINDING_EVENT_KEY_PRESS "key-press"
|
||||
#define BU_BINDING_EVENT_KEY_RELEASE "key-release"
|
||||
#define BU_BINDING_EVENT_MOUSE_PRESS "mouse-press"
|
||||
#define BU_BINDING_EVENT_MOUSE_RELEASE "mouse-release"
|
||||
#define BU_BINDING_EVENT_MOUSE_MOTION "mouse-motion"
|
||||
#define BU_BINDING_EVENT_MOUSE_SCROLL "mouse-scroll"
|
||||
|
||||
/**
|
||||
* @brief Action callback signature
|
||||
*
|
||||
* Invoked when an incoming event matches a loaded binding.
|
||||
* The callback receives the full JSON string of the triggering event.
|
||||
*
|
||||
* @param event_json Raw JSON string describing the incoming event
|
||||
* @param registered_data Opaque pointer passed during registration
|
||||
* @param event_data Opaque pointer passed during event processing
|
||||
*/
|
||||
typedef void (*bu_action_func_t)(const char *event_json, void *registered_data, void *event_data);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief Load JSON bindings from a file
|
||||
*
|
||||
* Parses a JSON configuration specifying bindings for various contexts.
|
||||
* Performs two-phase validation:
|
||||
* 1. Checks standard fields (like W3C key strings) for correctness.
|
||||
* 2. Invokes context-specific validation hooks (if registered).
|
||||
*
|
||||
* @param json_path Path to the JSON file
|
||||
* @return 0 on success, <0 on error
|
||||
*/
|
||||
BU_EXPORT extern int bu_binding_load(const char *json_path);
|
||||
|
||||
/**
|
||||
* @brief Load JSON bindings from a raw string
|
||||
*
|
||||
* Parses a JSON configuration string specifying bindings for various contexts.
|
||||
* Allows applications to embed overriding bindings natively without external files.
|
||||
*
|
||||
* @param json_string Raw JSON string containing bindings
|
||||
* @return 0 on success, <0 on error
|
||||
*/
|
||||
BU_EXPORT extern int bu_binding_load_str(const char *json_string);
|
||||
|
||||
/**
|
||||
* @brief Register a JSON schema for a specific context
|
||||
*
|
||||
* Defines the allowed vocabulary for a context using a JSON schema file.
|
||||
* The schema file is lazily parsed during bu_binding_load().
|
||||
*
|
||||
* @param context The interaction context (e.g., "dm", "fb")
|
||||
* @param json_path Absolute path to the schema JSON file
|
||||
*/
|
||||
BU_EXPORT extern void bu_binding_register_schema(const char *context, const char *json_path);
|
||||
|
||||
/**
|
||||
* @brief Augment an existing schema with a custom action
|
||||
*
|
||||
* @param context The interaction context (e.g., "dm")
|
||||
* @param action_name The new abstract action string
|
||||
* @param description Human-readable description of the action
|
||||
*/
|
||||
BU_EXPORT extern void bu_binding_schema_add_action(const char *context, const char *action_name, const char *description);
|
||||
|
||||
/**
|
||||
* @brief Remove a custom action from an existing schema
|
||||
*
|
||||
* @param context The interaction context (e.g., "dm")
|
||||
* @param action_name The abstract action string to remove
|
||||
*/
|
||||
BU_EXPORT extern void bu_binding_schema_remove_action(const char *context, const char *action_name);
|
||||
|
||||
/**
|
||||
* @brief Register a C callback for a semantic action
|
||||
*
|
||||
* @param action_name The abstract action string (e.g., "fb_close_window")
|
||||
* @param func The C function to invoke when the action is triggered
|
||||
* @param client_data Opaque pointer passed to the callback
|
||||
*/
|
||||
BU_EXPORT extern void bu_binding_register_action(const char *action_name, bu_action_func_t func, void *client_data);
|
||||
|
||||
/**
|
||||
* @brief Process an incoming native event
|
||||
*
|
||||
* Matches the event JSON against the loaded bindings for the given context.
|
||||
* If a matching action is found and a handler is registered for that action,
|
||||
* the callback is invoked.
|
||||
*
|
||||
* @param context The interaction context (e.g., "view3d", "fb_raw")
|
||||
* @param event_json A JSON string containing the event attributes
|
||||
* @param event_data Opaque pointer passed to the callback representing instance state
|
||||
* @return 1 if an action was triggered and handled, 0 otherwise
|
||||
*/
|
||||
BU_EXPORT extern int bu_binding_process_event(const char *context, const char *event_json, void *event_data);
|
||||
|
||||
/**
|
||||
* @brief Check if a specific action is bound in a given context
|
||||
*
|
||||
* Use this to verify that an application has provided a binding for an essential action.
|
||||
*
|
||||
* @param context The interaction context (e.g., "view3d", "fb_raw")
|
||||
* @param action_name The abstract action string (e.g., "view_mouse_drag_rotate")
|
||||
* @return 1 if bound, 0 if not
|
||||
*/
|
||||
BU_EXPORT extern int bu_binding_action_is_bound(const char *context, const char *action_name);
|
||||
|
||||
__END_DECLS
|
||||
|
||||
#endif /* BU_BINDING_H */
|
||||
|
||||
/** @} */
|
||||
/*
|
||||
* Local Variables:
|
||||
* mode: C
|
||||
* tab-width: 8
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "stroustrup"
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||
@@ -444,9 +444,6 @@ DM_EXPORT extern int fb_get_interactive(const struct fb *ifp);
|
||||
*/
|
||||
DM_EXPORT extern int fb_next_event(struct fb *ifp, struct fb_event *e);
|
||||
|
||||
/** Dispatch an application-facing event through the framebuffer bindings. */
|
||||
DM_EXPORT extern int fb_process_event_bindings(struct fb *ifp, const struct fb_event *e);
|
||||
|
||||
|
||||
/* Library entry points */
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -41,7 +41,6 @@ set(
|
||||
badmagic.c
|
||||
bitv.c
|
||||
bomb.c
|
||||
binding.cpp
|
||||
booleanize.c
|
||||
bu_init.cpp
|
||||
cache.cpp
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
/* B I N D I N G . C P P
|
||||
* BRL-CAD
|
||||
*
|
||||
* Copyright (c) 2026 United States Government as represented by
|
||||
* the U.S. Army Research Laboratory.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public License
|
||||
* version 2.1 as published by the Free Software Foundation.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this file; see the file named COPYING for more
|
||||
* information.
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "bu/binding.h"
|
||||
#include "bu/log.h"
|
||||
#include "bu/malloc.h"
|
||||
#include "bu/str.h"
|
||||
#include "bu/vls.h"
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
/*
|
||||
* Internal data structures to hold the parsed bindings and callbacks
|
||||
*/
|
||||
struct ActionCallback {
|
||||
bu_action_func_t func;
|
||||
void *client_data;
|
||||
};
|
||||
|
||||
struct BindingDef {
|
||||
json match;
|
||||
std::string action;
|
||||
};
|
||||
|
||||
/* Global state for libbu bindings */
|
||||
static std::map<std::string, std::vector<BindingDef>>& get_contexts() {
|
||||
static auto* ptr = new std::map<std::string, std::vector<BindingDef>>();
|
||||
return *ptr;
|
||||
}
|
||||
static std::map<std::string, ActionCallback>& get_action_registry() {
|
||||
static auto* ptr = new std::map<std::string, ActionCallback>();
|
||||
return *ptr;
|
||||
}
|
||||
static std::map<std::string, std::string>& get_schema_paths() {
|
||||
static auto* ptr = new std::map<std::string, std::string>();
|
||||
return *ptr;
|
||||
}
|
||||
static std::map<std::string, std::set<std::string>>& get_schemas() {
|
||||
static auto* ptr = new std::map<std::string, std::set<std::string>>();
|
||||
return *ptr;
|
||||
}
|
||||
|
||||
static void
|
||||
ensure_schema_loaded(const std::string& ctx_name)
|
||||
{
|
||||
if (get_schemas().find(ctx_name) != get_schemas().end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = get_schema_paths().find(ctx_name);
|
||||
if (it == get_schema_paths().end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::ifstream file(it->second);
|
||||
if (!file.is_open()) {
|
||||
bu_log("ensure_schema_loaded: Failed to open schema %s\n", it->second.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
json j;
|
||||
try {
|
||||
file >> j;
|
||||
} catch (const json::parse_error& e) {
|
||||
bu_log("ensure_schema_loaded: JSON parse error in schema %s: %s\n", it->second.c_str(), e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
std::set<std::string> valid_actions;
|
||||
if (j.contains("actions") && j["actions"].is_object()) {
|
||||
for (auto el = j["actions"].begin(); el != j["actions"].end(); ++el) {
|
||||
valid_actions.insert(el.key());
|
||||
}
|
||||
}
|
||||
get_schemas()[ctx_name] = valid_actions;
|
||||
}
|
||||
|
||||
/* W3C Key validation placeholder. A full implementation would check against
|
||||
* an exhaustive list of W3C standard keys (e.g., "Escape", "ArrowUp").
|
||||
* For demonstration, we'll just implement a simple sanity check.
|
||||
*/
|
||||
static int
|
||||
validate_w3c_key(const std::string& key)
|
||||
{
|
||||
/* Example: Reject completely upper-case strings that might be native C macros */
|
||||
if (key.length() > 1 && key == "RETURN") return 0;
|
||||
if (key.length() > 1 && key == "SPACE") return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* W3C Button validation. Values 0, 1, 2, 3, 4 are standard. */
|
||||
static int
|
||||
validate_w3c_button(int button)
|
||||
{
|
||||
return (button >= 0 && button <= 4);
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
bu_binding_load(const char *json_path)
|
||||
{
|
||||
if (!json_path) return -1;
|
||||
|
||||
std::ifstream file(json_path);
|
||||
if (!file.is_open()) {
|
||||
bu_log("bu_binding_load: Failed to open %s\n", json_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
return bu_binding_load_str(buffer.str().c_str());
|
||||
}
|
||||
|
||||
int
|
||||
bu_binding_load_str(const char *json_string)
|
||||
{
|
||||
if (!json_string) return -1;
|
||||
|
||||
json j;
|
||||
try {
|
||||
j = json::parse(json_string);
|
||||
} catch (const json::parse_error& e) {
|
||||
bu_log("bu_binding_load_str: JSON parse error: %s\n", e.what());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!j.contains("contexts") || !j["contexts"].is_object()) {
|
||||
bu_log("bu_binding_load_str: Invalid JSON structure (missing 'contexts' object)\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (auto it = j["contexts"].begin(); it != j["contexts"].end(); ++it) {
|
||||
std::string ctx_name = it.key();
|
||||
const auto& ctx_obj = it.value();
|
||||
|
||||
if (!ctx_obj.contains("bindings") || !ctx_obj["bindings"].is_array()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<BindingDef> defs;
|
||||
|
||||
ensure_schema_loaded(ctx_name);
|
||||
bool has_schema = (get_schemas().find(ctx_name) != get_schemas().end());
|
||||
|
||||
for (const auto& binding : ctx_obj["bindings"]) {
|
||||
if (!binding.contains("match") || !binding.contains("action")) {
|
||||
bu_log("bu_binding_load_str: Binding missing 'match' or 'action' in context %s. Skipping.\n", ctx_name.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
const json& match = binding["match"];
|
||||
std::string action = binding["action"].get<std::string>();
|
||||
|
||||
/* Phase 1 Validation: Check W3C standard compliance */
|
||||
int is_valid = 1;
|
||||
if (match.contains(BU_BINDING_ATTR_KEY)) {
|
||||
if (!match[BU_BINDING_ATTR_KEY].is_string() || !validate_w3c_key(match[BU_BINDING_ATTR_KEY].get<std::string>())) {
|
||||
bu_log("bu_binding_load_str: Invalid W3C key '%s' in context %s. Binding rejected.\n",
|
||||
match[BU_BINDING_ATTR_KEY].dump().c_str(), ctx_name.c_str());
|
||||
is_valid = 0;
|
||||
}
|
||||
}
|
||||
if (match.contains(BU_BINDING_ATTR_BUTTON)) {
|
||||
if (!match[BU_BINDING_ATTR_BUTTON].is_number_integer() || !validate_w3c_button(match[BU_BINDING_ATTR_BUTTON].get<int>())) {
|
||||
bu_log("bu_binding_load_str: Invalid W3C button '%s' in context %s. Binding rejected.\n",
|
||||
match[BU_BINDING_ATTR_BUTTON].dump().c_str(), ctx_name.c_str());
|
||||
is_valid = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_valid) continue;
|
||||
|
||||
/* Phase 2 Validation: Schema constraints */
|
||||
if (has_schema) {
|
||||
if (get_schemas()[ctx_name].find(action) == get_schemas()[ctx_name].end()) {
|
||||
bu_log("bu_binding_load_str: Binding for action '%s' rejected by schema for context '%s'.\n",
|
||||
action.c_str(), ctx_name.c_str());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
BindingDef def;
|
||||
def.match = match;
|
||||
def.action = action;
|
||||
get_contexts()[ctx_name].insert(get_contexts()[ctx_name].begin(), def);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
bu_binding_register_schema(const char *context, const char *json_path)
|
||||
{
|
||||
if (!context || !json_path) return;
|
||||
get_schema_paths()[std::string(context)] = std::string(json_path);
|
||||
}
|
||||
|
||||
void
|
||||
bu_binding_schema_add_action(const char *context, const char *action_name, const char *description)
|
||||
{
|
||||
if (!context || !action_name) return;
|
||||
std::string ctx(context);
|
||||
ensure_schema_loaded(ctx);
|
||||
get_schemas()[ctx].insert(std::string(action_name));
|
||||
}
|
||||
|
||||
void
|
||||
bu_binding_schema_remove_action(const char *context, const char *action_name)
|
||||
{
|
||||
if (!context || !action_name) return;
|
||||
std::string ctx(context);
|
||||
ensure_schema_loaded(ctx);
|
||||
get_schemas()[ctx].erase(std::string(action_name));
|
||||
}
|
||||
|
||||
void
|
||||
bu_binding_register_action(const char *action_name, bu_action_func_t func, void *client_data)
|
||||
{
|
||||
if (!action_name || !func) return;
|
||||
|
||||
ActionCallback cb;
|
||||
cb.func = func;
|
||||
cb.client_data = client_data;
|
||||
|
||||
get_action_registry()[std::string(action_name)] = cb;
|
||||
}
|
||||
|
||||
/* Helper to check if the json 'event' contains all the key-value pairs required by 'match' */
|
||||
static int
|
||||
is_match(const json& match, const json& event)
|
||||
{
|
||||
for (auto it = match.begin(); it != match.end(); ++it) {
|
||||
if (!event.contains(it.key())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* If it's an array (like modifiers), we need to ensure all elements in 'match' exist in 'event' */
|
||||
if (it.value().is_array()) {
|
||||
if (!event[it.key()].is_array()) return 0;
|
||||
|
||||
for (const auto& req_mod : it.value()) {
|
||||
int found = 0;
|
||||
for (const auto& ev_mod : event[it.key()]) {
|
||||
if (ev_mod == req_mod) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) return 0;
|
||||
}
|
||||
}
|
||||
/* Otherwise, simple equality */
|
||||
else if (event[it.key()] != it.value()) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
bu_binding_process_event(const char *context, const char *event_json, void *event_data)
|
||||
{
|
||||
if (!context || !event_json) return 0;
|
||||
|
||||
std::string ctx_name(context);
|
||||
auto it = get_contexts().find(ctx_name);
|
||||
if (it == get_contexts().end()) {
|
||||
return 0; /* Context not found */
|
||||
}
|
||||
|
||||
const char *json_str = event_json;
|
||||
|
||||
json event_json_obj;
|
||||
try {
|
||||
event_json_obj = json::parse(json_str);
|
||||
} catch (const json::parse_error& e) {
|
||||
bu_log("bu_binding_process_event: Invalid event JSON: %s\n", e.what());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!event_json_obj.is_object()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const auto& def : it->second) {
|
||||
if (is_match(def.match, event_json_obj)) {
|
||||
/* Match found */
|
||||
auto reg_it = get_action_registry().find(def.action);
|
||||
if (reg_it != get_action_registry().end()) {
|
||||
reg_it->second.func(event_json, reg_it->second.client_data, event_data);
|
||||
return 1;
|
||||
} else {
|
||||
/* We have a binding, but no callback registered for this action */
|
||||
bu_log("Warning: Event mapped to action '%s', but no callback is registered.\n", def.action.c_str());
|
||||
return 1; /* Still mark as handled since it matched a binding */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0; /* Unhandled */
|
||||
}
|
||||
|
||||
extern "C" int
|
||||
bu_binding_action_is_bound(const char *context, const char *action_name)
|
||||
{
|
||||
if (!context || !action_name) return 0;
|
||||
|
||||
std::string ctx(context);
|
||||
std::string act(action_name);
|
||||
|
||||
auto it = get_contexts().find(ctx);
|
||||
if (it == get_contexts().end()) return 0;
|
||||
|
||||
for (const auto& def : it->second) {
|
||||
if (def.action == act) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Local Variables:
|
||||
* mode: C++
|
||||
* tab-width: 8
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "stroustrup"
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||
@@ -10,7 +10,6 @@ set(
|
||||
test_bomb.c
|
||||
test_badmagic.c
|
||||
test_basename.c
|
||||
test_binding.cpp
|
||||
test_bitv.c
|
||||
test_booleanize.c
|
||||
test_cache.cpp
|
||||
@@ -123,7 +122,6 @@ brlcad_addexec(bu_gethostname test_gethostname.c libbu TEST)
|
||||
brlcad_add_test(NAME bu_argv COMMAND bu_test test_argv)
|
||||
brlcad_add_test(NAME bu_backtrace COMMAND bu_test test_backtrace)
|
||||
brlcad_add_test(NAME bu_bomb COMMAND bu_test test_bomb "$<TARGET_FILE:bu_test>")
|
||||
brlcad_add_test(NAME bu_binding COMMAND bu_test test_binding "${CMAKE_CURRENT_SOURCE_DIR}/test_bindings.json")
|
||||
brlcad_add_test(NAME bu_dir COMMAND bu_test test_dir)
|
||||
brlcad_add_test(NAME bu_list_empty COMMAND bu_test test_list 0)
|
||||
brlcad_add_test(NAME bu_list_single COMMAND bu_test test_list 1)
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
/* T E S T _ B I N D I N G . C P P
|
||||
* BRL-CAD
|
||||
*
|
||||
* Copyright (c) 2026 United States Government as represented by
|
||||
* the U.S. Army Research Laboratory.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public License
|
||||
* version 2.1 as published by the Free Software Foundation.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this file; see the file named COPYING for more
|
||||
* information.
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "bu/binding.h"
|
||||
#include "bu/vls.h"
|
||||
|
||||
int action_triggered = 0;
|
||||
|
||||
void mock_action_handler(const char *event_json, void *registered_data, void *event_data) {
|
||||
action_triggered = 1;
|
||||
printf("Action handler triggered with JSON: %s\n", event_json);
|
||||
}
|
||||
|
||||
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
bu_setprogname(argv[0]);
|
||||
|
||||
if (argc < 2) {
|
||||
printf("Usage: %s <path_to_test_bindings.json>\n", argv[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *json_path = argv[1];
|
||||
|
||||
/* Register schema to restrict allowed actions */
|
||||
bu_binding_schema_add_action("fb", "fb_zoom", "Zoom");
|
||||
bu_binding_schema_add_action("fb", "fb_close_window", "Close Window");
|
||||
|
||||
/* Register hooks before loading JSON */
|
||||
bu_binding_register_action("fb_close_window", mock_action_handler, NULL);
|
||||
|
||||
printf("Loading bindings from %s\n", json_path);
|
||||
if (bu_binding_load(json_path) < 0) {
|
||||
printf("Failed to load bindings.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Test 1: Simulate a matching event */
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-press\", \"button\": 2}");
|
||||
|
||||
action_triggered = 0;
|
||||
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), NULL);
|
||||
|
||||
if (!action_triggered) {
|
||||
printf("Test 1 Failed: Expected action was not triggered.\n");
|
||||
return -1;
|
||||
}
|
||||
printf("Test 1 Passed: Action triggered successfully.\n");
|
||||
|
||||
/* Test 2: Simulate a non-matching event */
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-press\", \"button\": 1}");
|
||||
action_triggered = 0;
|
||||
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), NULL);
|
||||
|
||||
if (action_triggered) {
|
||||
printf("Test 2 Failed: Action should not have been triggered.\n");
|
||||
return -1;
|
||||
}
|
||||
printf("Test 2 Passed: Non-matching event correctly ignored.\n");
|
||||
|
||||
/* Test 3: The Escape key binding should have been rejected by the validator */
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"key-press\", \"key\": \"Escape\"}");
|
||||
action_triggered = 0;
|
||||
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), NULL);
|
||||
|
||||
if (action_triggered) {
|
||||
printf("Test 3 Failed: Escape binding should have been filtered by the validator.\n");
|
||||
return -1;
|
||||
}
|
||||
printf("Test 3 Passed: Validator correctly rejected binding at load time.\n");
|
||||
|
||||
bu_vls_free(&ev_json);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Local Variables:
|
||||
* mode: C++
|
||||
* tab-width: 8
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "stroustrup"
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"contexts": {
|
||||
"fb": {
|
||||
"bindings": [
|
||||
{
|
||||
"match": {
|
||||
"type": "mouse-press",
|
||||
"button": 2
|
||||
},
|
||||
"action": "fb_close_window"
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"type": "key-press",
|
||||
"key": "Escape"
|
||||
},
|
||||
"action": "fb_invalid_action"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")) {
|
||||
|
||||
@@ -38,11 +38,6 @@ set_property(
|
||||
PROPERTY COMPILE_DEFINITIONS "DM_PLUGIN_SUFFIX=\"${CMAKE_SHARED_LIBRARY_SUFFIX}\""
|
||||
)
|
||||
|
||||
# Files related to key/mouse binding management
|
||||
brlcad_manage_files(dm_bindings.json ${DATA_DIR}/bindings)
|
||||
brlcad_manage_files(dm_schema.json ${DATA_DIR}/bindings)
|
||||
brlcad_manage_files(fb_schema.json ${DATA_DIR}/bindings)
|
||||
|
||||
brlcad_find_package(PNG REQUIRED)
|
||||
find_package(OpenSSL QUIET)
|
||||
|
||||
@@ -170,4 +165,3 @@ cmakefiles(${libdm_ignore_files})
|
||||
# indent-tabs-mode: t
|
||||
# End:
|
||||
# ex: shiftwidth=2 tabstop=8
|
||||
|
||||
|
||||
@@ -2925,7 +2925,6 @@ X24_handle_event(struct fb *ifp, XEvent *event)
|
||||
case ButtonPress:
|
||||
{
|
||||
int button = (int) event->xbutton.button;
|
||||
int w3c_button = 0;
|
||||
|
||||
/* In interactive mode, report the event to the application
|
||||
* rather than consuming it here. */
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
{
|
||||
"contexts": {
|
||||
"fb": {
|
||||
"description": "Standard interactions for raw framebuffers (rt, fbserv)",
|
||||
"bindings": [
|
||||
{
|
||||
"match": {
|
||||
"type": "mouse-press",
|
||||
"button": 1
|
||||
},
|
||||
"action": "fb_print_pixel_rgb"
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"type": "mouse-press",
|
||||
"button": 2
|
||||
},
|
||||
"action": "fb_close_window"
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"type": "key-press",
|
||||
"key": "Escape"
|
||||
},
|
||||
"action": "fb_close_window"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dm": {
|
||||
"description": "Standard bindings for the Geometry Editor (qged & mged)",
|
||||
"bindings": [
|
||||
{ "match": { "type": "key-press", "key": "A" }, "action": "view_toggle_adc" },
|
||||
{ "match": { "type": "key-press", "key": "M" }, "action": "view_toggle_model_axes" },
|
||||
{ "match": { "type": "key-press", "key": "V" }, "action": "view_toggle_view_axes" },
|
||||
{ "match": { "type": "key-press", "key": "2" }, "action": "view_2" },
|
||||
{ "match": { "type": "key-press", "key": "3" }, "action": "view_3" },
|
||||
{ "match": { "type": "key-press", "key": "4" }, "action": "view_4" },
|
||||
{ "match": { "type": "key-press", "key": "5" }, "action": "view_5" },
|
||||
{ "match": { "type": "key-press", "key": "6" }, "action": "view_6" },
|
||||
{ "match": { "type": "key-press", "key": "7" }, "action": "view_7" },
|
||||
{ "match": { "type": "key-press", "key": "F" }, "action": "view_front" },
|
||||
{ "match": { "type": "key-press", "key": "T" }, "action": "view_top" },
|
||||
{ "match": { "type": "key-press", "key": "B" }, "action": "view_bottom" },
|
||||
{ "match": { "type": "key-press", "key": "L" }, "action": "view_left" },
|
||||
{ "match": { "type": "key-press", "key": "R", "modifiers": ["Shift"] }, "action": "view_rear" },
|
||||
{ "match": { "type": "key-press", "key": "R" }, "action": "view_right" },
|
||||
{ "match": { "type": "mouse-press", "button": 0 }, "action": "view_mouse_press_left" },
|
||||
{ "match": { "type": "mouse-release", "button": 0 }, "action": "view_mouse_release_left" },
|
||||
{ "match": { "type": "mouse-release", "button": 2 }, "action": "view_mouse_release_right" },
|
||||
{ "match": { "type": "mouse-release", "button": 1 }, "action": "view_mouse_release_middle" },
|
||||
{ "match": { "type": "mouse-move", "button": 0, "modifiers": ["Control"] }, "action": "view_mouse_drag_rotate" },
|
||||
{ "match": { "type": "mouse-move", "button": 0, "modifiers": ["Shift"] }, "action": "view_mouse_drag_translate" },
|
||||
{ "match": { "type": "mouse-move", "button": 0, "modifiers": ["Control", "Shift"] }, "action": "view_mouse_drag_scale" },
|
||||
{ "match": { "type": "mouse-move", "button": 0 }, "action": "view_mouse_drag_adjust" },
|
||||
{ "match": { "type": "mouse-wheel" }, "action": "view_mouse_wheel_scale" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,22 +66,10 @@ dm_init_msgs()
|
||||
}
|
||||
|
||||
|
||||
#include "bu/binding.h"
|
||||
|
||||
static void
|
||||
libdm_init(void)
|
||||
{
|
||||
bu_vls_init(&init_msgs);
|
||||
|
||||
const char *dm_schema = bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "dm_schema.json", NULL);
|
||||
if (dm_schema) {
|
||||
bu_binding_register_schema("dm", dm_schema);
|
||||
}
|
||||
|
||||
const char *fb_schema = bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "fb_schema.json", NULL);
|
||||
if (fb_schema) {
|
||||
bu_binding_register_schema("fb", fb_schema);
|
||||
}
|
||||
|
||||
const char *ppath = bu_dir(NULL, 0, BU_DIR_LIBEXEC, "dm", NULL);
|
||||
char **dm_filenames;
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include "bu/binding.h"
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
@@ -63,13 +62,6 @@ dm_open(void *ctx, void *interp, const char *type, int argc, const char *argv[])
|
||||
|
||||
const struct dm *d = d_it->second;
|
||||
struct dm *dmp = d->i->dm_open(ctx, interp, argc, argv);
|
||||
|
||||
char *bindings_path = (char *)bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "dm_bindings.json", NULL);
|
||||
if (bindings_path) {
|
||||
bu_binding_load(bindings_path);
|
||||
bu_free(bindings_path, "bindings_path");
|
||||
}
|
||||
|
||||
return dmp;
|
||||
}
|
||||
|
||||
@@ -536,12 +528,6 @@ found_interface:
|
||||
|
||||
return FB_NULL;
|
||||
}
|
||||
char *bindings_path = (char *)bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "dm_bindings.json", NULL);
|
||||
if (bindings_path) {
|
||||
bu_binding_load(bindings_path);
|
||||
bu_free(bindings_path, "bindings_path");
|
||||
}
|
||||
|
||||
return ifp;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"actions": {
|
||||
"view_toggle_adc": "Toggle Angle Distance Cursor",
|
||||
"view_toggle_model_axes": "Toggle Model Axes",
|
||||
"view_toggle_view_axes": "Toggle View Axes",
|
||||
"view_2": "Set View 2",
|
||||
"view_3": "Set View 3",
|
||||
"view_4": "Set View 4",
|
||||
"view_5": "Set View 5",
|
||||
"view_6": "Set View 6",
|
||||
"view_7": "Set View 7",
|
||||
"view_front": "Set Front View",
|
||||
"view_top": "Set Top View",
|
||||
"view_bottom": "Set Bottom View",
|
||||
"view_left": "Set Left View",
|
||||
"view_right": "Set Right View",
|
||||
"view_rear": "Set Rear View",
|
||||
"view_mouse_drag_rotate": "Rotate view via mouse drag",
|
||||
"view_mouse_drag_translate": "Translate view via mouse drag",
|
||||
"view_mouse_drag_scale": "Scale view via mouse drag",
|
||||
"view_mouse_wheel_scale": "Scale view via mouse wheel",
|
||||
"view_mouse_press_left": "Left mouse press action",
|
||||
"view_mouse_press_right": "Right mouse press action"
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@
|
||||
#include "./include/private.h"
|
||||
#include "icv.h"
|
||||
#include "dm.h"
|
||||
#include "bu/binding.h"
|
||||
|
||||
|
||||
struct fb *fb_get(void)
|
||||
@@ -259,59 +258,6 @@ long fb_poll_rate(struct fb *ifp)
|
||||
return ifp->i->if_poll_refresh_rate;
|
||||
}
|
||||
|
||||
static int
|
||||
fb_event_button_to_w3c(int button)
|
||||
{
|
||||
return (button >= 1 && button <= 3) ? button - 1 : button;
|
||||
}
|
||||
|
||||
int
|
||||
fb_process_event_bindings(struct fb *ifp, const struct fb_event *event)
|
||||
{
|
||||
struct bu_vls event_json = BU_VLS_INIT_ZERO;
|
||||
int handled = 0;
|
||||
|
||||
if (!ifp || !event)
|
||||
return 0;
|
||||
|
||||
switch (event->type) {
|
||||
case FB_EVENT_BUTTON_PRESS:
|
||||
bu_vls_sprintf(&event_json,
|
||||
"{\"type\": \"mouse-press\", \"button\": %d, \"x\": %d, \"y\": %d}",
|
||||
fb_event_button_to_w3c(event->button), event->x, event->y);
|
||||
break;
|
||||
case FB_EVENT_BUTTON_RELEASE:
|
||||
bu_vls_sprintf(&event_json,
|
||||
"{\"type\": \"mouse-release\", \"button\": %d, \"x\": %d, \"y\": %d}",
|
||||
fb_event_button_to_w3c(event->button), event->x, event->y);
|
||||
break;
|
||||
case FB_EVENT_MOTION:
|
||||
bu_vls_sprintf(&event_json,
|
||||
"{\"type\": \"mouse-move\", \"x\": %d, \"y\": %d}",
|
||||
event->x, event->y);
|
||||
break;
|
||||
case FB_EVENT_KEY_PRESS:
|
||||
if (event->keycode == 27) {
|
||||
bu_vls_sprintf(&event_json, "{\"type\": \"key-press\", \"key\": \"Escape\"}");
|
||||
} else if (event->keycode >= 32 && event->keycode <= 126) {
|
||||
bu_vls_sprintf(&event_json,
|
||||
"{\"type\": \"key-press\", \"key\": \"%c\"}",
|
||||
event->keycode);
|
||||
}
|
||||
break;
|
||||
case FB_EVENT_CLOSE:
|
||||
bu_vls_sprintf(&event_json, "{\"type\": \"key-press\", \"key\": \"Escape\"}");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (bu_vls_strlen(&event_json) > 0)
|
||||
handled = bu_binding_process_event("fb", bu_vls_cstr(&event_json), ifp);
|
||||
bu_vls_free(&event_json);
|
||||
return handled;
|
||||
}
|
||||
|
||||
void fb_set_interactive(struct fb *ifp, int on)
|
||||
{
|
||||
if (!ifp)
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"actions": {
|
||||
"fb_print_pixel_rgb": "Print RGB values of pixel under cursor",
|
||||
"fb_close_window": "Close framebuffer window"
|
||||
}
|
||||
}
|
||||
+72
-171
@@ -61,10 +61,6 @@
|
||||
#include "dm.h"
|
||||
#include "./fb_ogl.h"
|
||||
|
||||
#include "bu/binding.h"
|
||||
#include "bu/file.h"
|
||||
#include <X11/keysym.h>
|
||||
|
||||
extern struct fb ogl_interface;
|
||||
|
||||
#define DIRECT_COLOR_VISUAL_ALLOWED 0
|
||||
@@ -778,52 +774,6 @@ ogl_configureWindow(struct fb *ifp, int width, int height)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
ogl_action_fb_print_pixel_rgb(const struct bu_vls *event_json, void *registered_data, void *event_data)
|
||||
{
|
||||
struct fb *ifp = (struct fb *)event_data;
|
||||
int x = -1, y = -1;
|
||||
char *p;
|
||||
|
||||
if (!ifp) return;
|
||||
|
||||
if ((p = strstr(bu_vls_cstr(event_json), "\"x\":"))) {
|
||||
sscanf(p + 4, "%d", &x);
|
||||
}
|
||||
if ((p = strstr(bu_vls_cstr(event_json), "\"y\":"))) {
|
||||
sscanf(p + 4, "%d", &y);
|
||||
}
|
||||
|
||||
if (x < 0 || y < 0) {
|
||||
fb_log("No RGB (outside image viewport)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t memidx = (y * WIN(ifp)->mi_pixwidth) * sizeof(struct fb_pixel);
|
||||
struct fb_pixel *oglp = (struct fb_pixel *)&ifp->i->if_mem[memidx];
|
||||
|
||||
fb_log("At image (%d, %d), real RGB=(%3d %3d %3d)\n",
|
||||
x, y, (int)oglp[x].red, (int)oglp[x].green, (int)oglp[x].blue);
|
||||
}
|
||||
|
||||
static void
|
||||
ogl_action_fb_close_window(const struct bu_vls *event_json, void *registered_data, void *event_data)
|
||||
{
|
||||
struct fb *ifp = (struct fb *)event_data;
|
||||
if (ifp) OGL(ifp)->alive = 0;
|
||||
}
|
||||
|
||||
static int bindings_loaded = 0;
|
||||
static void
|
||||
ogl_init_bindings(void)
|
||||
{
|
||||
if (bindings_loaded) return;
|
||||
bindings_loaded = 1;
|
||||
|
||||
|
||||
bu_binding_register_action("fb_print_pixel_rgb", ogl_action_fb_print_pixel_rgb, NULL);
|
||||
bu_binding_register_action("fb_close_window", ogl_action_fb_close_window, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
ogl_do_event(struct fb *ifp)
|
||||
@@ -831,122 +781,76 @@ ogl_do_event(struct fb *ifp)
|
||||
XEvent event;
|
||||
|
||||
while (XCheckWindowEvent(OGL(ifp)->dispp, OGL(ifp)->wind,
|
||||
OGL(ifp)->event_mask, &event)) {
|
||||
switch (event.type) {
|
||||
case Expose:
|
||||
if (!OGL(ifp)->use_ext_ctrl)
|
||||
expose_callback(ifp);
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev = {FB_EVENT_EXPOSE, 0, 0, 0, 0, 0};
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
}
|
||||
break;
|
||||
case ButtonPress:
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev;
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = FB_EVENT_BUTTON_PRESS;
|
||||
ev.button = (int)event.xbutton.button;
|
||||
ev.x = event.xbutton.x;
|
||||
ev.y = ifp->i->if_height - event.xbutton.y - 1;
|
||||
ev.state = (int)event.xbutton.state;
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
} else {
|
||||
int button = (int)event.xbutton.button;
|
||||
int w3c_button = 0;
|
||||
if (button == Button1) {
|
||||
if (event.xbutton.state & ControlMask) button = Button2;
|
||||
else if (event.xbutton.state & Mod1Mask) button = Button3;
|
||||
else if (event.xbutton.state & Mod2Mask) button = Button3;
|
||||
}
|
||||
if (button == Button1) w3c_button = 0;
|
||||
else if (button == Button2) w3c_button = 1;
|
||||
else if (button == Button3) w3c_button = 2;
|
||||
else w3c_button = button;
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json,
|
||||
"{\"type\": \"mouse-press\", \"button\": %d, \"x\": %d, \"y\": %d}",
|
||||
w3c_button, event.xbutton.x,
|
||||
ifp->i->if_height - event.xbutton.y - 1);
|
||||
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), ifp);
|
||||
bu_vls_free(&ev_json);
|
||||
}
|
||||
break;
|
||||
case ButtonRelease:
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev;
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = FB_EVENT_BUTTON_RELEASE;
|
||||
ev.button = (int)event.xbutton.button;
|
||||
ev.x = event.xbutton.x;
|
||||
ev.y = ifp->i->if_height - event.xbutton.y - 1;
|
||||
ev.state = (int)event.xbutton.state;
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
}
|
||||
break;
|
||||
case MotionNotify:
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev;
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = FB_EVENT_MOTION;
|
||||
ev.x = event.xmotion.x;
|
||||
ev.y = ifp->i->if_height - event.xmotion.y - 1;
|
||||
ev.state = (int)event.xmotion.state;
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
}
|
||||
break;
|
||||
case KeyPress:
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev;
|
||||
char keybuf[8] = {0};
|
||||
KeySym keysym;
|
||||
int n;
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = FB_EVENT_KEY_PRESS;
|
||||
n = XLookupString(&event.xkey, keybuf, sizeof(keybuf), &keysym, NULL);
|
||||
ev.keycode = (n > 0) ? (unsigned char)keybuf[0] : (int)keysym;
|
||||
ev.state = (int)event.xkey.state;
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
} else {
|
||||
KeySym keysym = XLookupKeysym(&event.xkey, 0);
|
||||
char *key_str = XKeysymToString(keysym);
|
||||
if (key_str) {
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json,
|
||||
"{\"type\": \"key-press\", \"key\": \"%s\"}", key_str);
|
||||
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), ifp);
|
||||
bu_vls_free(&ev_json);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ClientMessage:
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev = {FB_EVENT_CLOSE, 0, 0, 0, 0, 0};
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
} else {
|
||||
OGL(ifp)->alive = 0;
|
||||
}
|
||||
break;
|
||||
case ConfigureNotify:
|
||||
{
|
||||
XConfigureEvent *conf = (XConfigureEvent *)&event;
|
||||
if (conf->width == OGL(ifp)->win_width &&
|
||||
conf->height == OGL(ifp)->win_height)
|
||||
break;
|
||||
ogl_configureWindow(ifp, conf->width, conf->height);
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev;
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = FB_EVENT_RESIZE;
|
||||
ev.x = conf->width;
|
||||
ev.y = conf->height;
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
OGL(ifp)->event_mask, &event)) {
|
||||
switch (event.type) {
|
||||
case Expose:
|
||||
if (!OGL(ifp)->use_ext_ctrl)
|
||||
expose_callback(ifp);
|
||||
break;
|
||||
case ButtonPress:
|
||||
{
|
||||
int button = (int) event.xbutton.button;
|
||||
if (button == Button1) {
|
||||
/* Check for single button mouse remap.
|
||||
* ctrl-1 => 2
|
||||
* meta-1 => 3
|
||||
* cmdkey => 3
|
||||
*/
|
||||
if (event.xbutton.state & ControlMask) {
|
||||
button = Button2;
|
||||
} else if (event.xbutton.state & Mod1Mask) {
|
||||
button = Button3;
|
||||
} else if (event.xbutton.state & Mod2Mask) {
|
||||
button = Button3;
|
||||
}
|
||||
}
|
||||
|
||||
switch (button) {
|
||||
case Button1:
|
||||
break;
|
||||
case Button2:
|
||||
{
|
||||
int x, y;
|
||||
register struct fb_pixel *oglp;
|
||||
|
||||
x = event.xbutton.x;
|
||||
y = ifp->i->if_height - event.xbutton.y - 1;
|
||||
|
||||
if (x < 0 || y < 0) {
|
||||
fb_log("No RGB (outside image viewport)\n");
|
||||
break;
|
||||
}
|
||||
|
||||
size_t memidx = (y * WIN(ifp)->mi_pixwidth) * sizeof(struct fb_pixel);
|
||||
oglp = (struct fb_pixel *)&ifp->i->if_mem[memidx];
|
||||
|
||||
fb_log("At image (%d, %d), real RGB=(%3d %3d %3d)\n",
|
||||
x, y, (int)oglp[x].red, (int)oglp[x].green, (int)oglp[x].blue);
|
||||
|
||||
break;
|
||||
}
|
||||
case Button3:
|
||||
OGL(ifp)->alive = 0;
|
||||
break;
|
||||
default:
|
||||
fb_log("unhandled mouse event\n");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ConfigureNotify:
|
||||
{
|
||||
XConfigureEvent *conf = (XConfigureEvent *)&event;
|
||||
|
||||
if (conf->width == OGL(ifp)->win_width &&
|
||||
conf->height == OGL(ifp)->win_height)
|
||||
return;
|
||||
|
||||
ogl_configureWindow(ifp, conf->width, conf->height);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1117,9 +1021,8 @@ fb_ogl_open(struct fb *ifp, const char *file, int width, int height)
|
||||
int mode, i;
|
||||
long valuemask;
|
||||
XSetWindowAttributes swa;
|
||||
FB_CK_FB(ifp->i);
|
||||
|
||||
ogl_init_bindings();
|
||||
FB_CK_FB(ifp->i);
|
||||
|
||||
/*
|
||||
* First, attempt to determine operating mode for this open,
|
||||
@@ -1311,7 +1214,7 @@ fb_ogl_open(struct fb *ifp, const char *file, int width, int height)
|
||||
OGL(ifp)->vip->screen);
|
||||
swa.event_mask = OGL(ifp)->event_mask =
|
||||
ExposureMask | KeyPressMask | KeyReleaseMask |
|
||||
ButtonPressMask | ButtonReleaseMask | PointerMotionMask | StructureNotifyMask;
|
||||
ButtonPressMask | ButtonReleaseMask;
|
||||
swa.colormap = OGL(ifp)->xcmap;
|
||||
|
||||
#define XCreateWindowDebug(display, parent, x, y, width, height, border_width, depth, class, visual, valuemask, attributes) \
|
||||
@@ -1478,8 +1381,6 @@ ogl_open_existing(struct fb *ifp, int width, int height, struct fb_platform_spec
|
||||
{
|
||||
struct ogl_fb_info *ogl_internal = (struct ogl_fb_info *)fb_p->data;
|
||||
BU_CKMAG(fb_p, FB_OGL_MAGIC, "ogl framebuffer");
|
||||
|
||||
ogl_init_bindings();
|
||||
return open_existing(ifp, ogl_internal->dpy, ogl_internal->win,
|
||||
ogl_internal->cmap, ogl_internal->vip, width, height, ogl_internal->glxc,
|
||||
ogl_internal->double_buffer, ogl_internal->soft_cmap);
|
||||
|
||||
+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 */
|
||||
|
||||
+246
-241
@@ -37,12 +37,6 @@ extern "C" {
|
||||
#include "qtcad/defines.h"
|
||||
#include "bindings.h"
|
||||
|
||||
extern "C" {
|
||||
#include "bu/binding.h"
|
||||
#include "bu/file.h"
|
||||
#include "bu/log.h"
|
||||
}
|
||||
|
||||
typedef void (*bounds_update_t)(struct bview *);
|
||||
static std::unordered_map<struct bview *, bounds_update_t> drag_bounds_updates;
|
||||
static std::unordered_map<struct bview *, long long> drag_update_ts;
|
||||
@@ -74,256 +68,247 @@ restore_drag_bounds_update(struct bview *v, int refresh_bounds)
|
||||
drag_update_ts.erase(v);
|
||||
}
|
||||
|
||||
static int bindings_loaded = 0;
|
||||
|
||||
static void qtcad_action_toggle_adc(const char *, void *, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (v) v->gv_s->gv_adc.draw = !v->gv_s->gv_adc.draw;
|
||||
}
|
||||
static void qtcad_action_toggle_model_axes(const char *, void *, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (v) v->gv_s->gv_model_axes.draw = !v->gv_s->gv_model_axes.draw;
|
||||
}
|
||||
static void qtcad_action_toggle_view_axes(const char *, void *, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (v) v->gv_s->gv_view_axes.draw = !v->gv_s->gv_view_axes.draw;
|
||||
}
|
||||
|
||||
static void qtcad_action_view_set(const char *, void *registered_data, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
const char *aet_str = (const char *)registered_data;
|
||||
if (v && aet_str) {
|
||||
bn_decode_vect(v->gv_aet, aet_str);
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
}
|
||||
}
|
||||
|
||||
static void qtcad_action_mouse_release(const char *event_json, void *, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (!v) return;
|
||||
|
||||
int cx = 0, cy = 0, mode = 0, dx = 1, dy = 1;
|
||||
char *p;
|
||||
if ((p = strstr(event_json, "\"cx\":"))) sscanf(p + 5, "%d", &cx);
|
||||
if ((p = strstr(event_json, "\"cy\":"))) sscanf(p + 5, "%d", &cy);
|
||||
if ((p = strstr(event_json, "\"mode\":"))) sscanf(p + 7, "%d", &mode);
|
||||
|
||||
int button = 0;
|
||||
if ((p = strstr(event_json, "\"button\":"))) sscanf(p + 9, "%d", &button);
|
||||
|
||||
unsigned long long view_flags = BV_IDLE;
|
||||
if (button == 0) { // Left
|
||||
if (mode != BV_CENTER) { view_flags = BV_SCALE; dx = 10; dy = 5; }
|
||||
else { view_flags = BV_CENTER; dx = cx; dy = cy; }
|
||||
} else if (button == 2) { // Right
|
||||
if (mode == BV_CENTER) return;
|
||||
view_flags = BV_SCALE; dx = 1; dy = 2;
|
||||
} else if (button == 1) { // Middle
|
||||
view_flags = BV_CENTER; dx = cx; dy = cy;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
point_t keypt = VINIT_ZERO;
|
||||
bv_adjust(v, dx, dy, keypt, 0, view_flags);
|
||||
}
|
||||
|
||||
static void qtcad_action_adjust_view(const char *event_json, void *registered_data, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (!v) return;
|
||||
|
||||
int dx = 0, dy = 0, mode = 0;
|
||||
int view_flags = (int)(intptr_t)registered_data;
|
||||
char *p;
|
||||
|
||||
if ((p = strstr(event_json, "\"dx\":"))) sscanf(p + 5, "%d", &dx);
|
||||
if ((p = strstr(event_json, "\"dy\":"))) sscanf(p + 5, "%d", &dy);
|
||||
if ((p = strstr(event_json, "\"mode\":"))) sscanf(p + 7, "%d", &mode);
|
||||
|
||||
if (view_flags == 0) { // dynamic fallback
|
||||
view_flags = mode;
|
||||
if (mode == BV_CENTER) view_flags = BV_SCALE;
|
||||
}
|
||||
|
||||
if (view_flags == BV_SCALE) {
|
||||
int mdelta = (abs(dx) > abs(dy)) ? dx : -dy;
|
||||
int f = (int)(2*100*(double)abs(mdelta)/(double)v->gv_height);
|
||||
if (mdelta > 0) { dy = 101 + f; dx = 100; }
|
||||
else { dy = 99 - f; dx = 100; }
|
||||
}
|
||||
|
||||
point_t center;
|
||||
MAT_DELTAS_GET_NEG(center, v->gv_center);
|
||||
|
||||
if (view_flags & (BV_ROT | BV_TRANS | BV_SCALE))
|
||||
suspend_drag_bounds_update(v);
|
||||
|
||||
bv_adjust(v, dx, dy, center, 0, view_flags);
|
||||
}
|
||||
|
||||
static void qtcad_action_wheel_scale(const char *event_json, void *, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (!v) return;
|
||||
|
||||
int delta = 0;
|
||||
char *p;
|
||||
if ((p = strstr(event_json, "\"delta\":"))) sscanf(p + 8, "%d", &delta);
|
||||
|
||||
int dx = 100 + delta;
|
||||
int dy = 100;
|
||||
point_t origin = VINIT_ZERO;
|
||||
bv_adjust(v, dx, dy, origin, 0, BV_SCALE);
|
||||
}
|
||||
|
||||
extern "C" QCAD_EXPORT int qtcad_load_bindings(const char *json_path) {
|
||||
if (bu_binding_load(json_path) < 0) {
|
||||
bu_log("libqtcad: Failed to load bindings from %s\n", json_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!bu_binding_action_is_bound("dm", "view_toggle_adc") ||
|
||||
!bu_binding_action_is_bound("dm", "view_2")) {
|
||||
bu_log("libqtcad Validation Failed: Provided bindings missing required standard actions.\n");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void qtcad_init_bindings(void) {
|
||||
if (bindings_loaded) return;
|
||||
bindings_loaded = 1;
|
||||
|
||||
bu_binding_register_action("view_toggle_adc", qtcad_action_toggle_adc, NULL);
|
||||
bu_binding_register_action("view_toggle_model_axes", qtcad_action_toggle_model_axes, NULL);
|
||||
bu_binding_register_action("view_toggle_view_axes", qtcad_action_toggle_view_axes, NULL);
|
||||
|
||||
bu_binding_register_action("view_2", qtcad_action_view_set, (void*)"35 -25 0");
|
||||
bu_binding_register_action("view_3", qtcad_action_view_set, (void*)"35 25 0");
|
||||
bu_binding_register_action("view_4", qtcad_action_view_set, (void*)"45 45 0");
|
||||
bu_binding_register_action("view_5", qtcad_action_view_set, (void*)"145 25 0");
|
||||
bu_binding_register_action("view_6", qtcad_action_view_set, (void*)"215 25 0");
|
||||
bu_binding_register_action("view_7", qtcad_action_view_set, (void*)"325 25 0");
|
||||
bu_binding_register_action("view_front", qtcad_action_view_set, (void*)"0 0 0");
|
||||
bu_binding_register_action("view_top", qtcad_action_view_set, (void*)"270 90 0");
|
||||
bu_binding_register_action("view_bottom", qtcad_action_view_set, (void*)"270 -90 0");
|
||||
bu_binding_register_action("view_left", qtcad_action_view_set, (void*)"90 0 0");
|
||||
bu_binding_register_action("view_rear", qtcad_action_view_set, (void*)"180 0 0");
|
||||
bu_binding_register_action("view_right", qtcad_action_view_set, (void*)"270 0 0");
|
||||
|
||||
bu_binding_register_action("view_mouse_release_left", qtcad_action_mouse_release, NULL);
|
||||
bu_binding_register_action("view_mouse_release_right", qtcad_action_mouse_release, NULL);
|
||||
bu_binding_register_action("view_mouse_release_middle", qtcad_action_mouse_release, NULL);
|
||||
|
||||
bu_binding_register_action("view_mouse_drag_rotate", qtcad_action_adjust_view, (void*)(intptr_t)BV_ROT);
|
||||
bu_binding_register_action("view_mouse_drag_translate", qtcad_action_adjust_view, (void*)(intptr_t)BV_TRANS);
|
||||
bu_binding_register_action("view_mouse_drag_scale", qtcad_action_adjust_view, (void*)(intptr_t)BV_SCALE);
|
||||
bu_binding_register_action("view_mouse_drag_adjust", qtcad_action_adjust_view, (void*)(intptr_t)0);
|
||||
bu_binding_register_action("view_mouse_wheel_scale", qtcad_action_wheel_scale, NULL);
|
||||
}
|
||||
|
||||
static void append_modifiers(struct bu_vls *vls, QInputEvent *e) {
|
||||
if (!e->modifiers()) return;
|
||||
bu_vls_printf(vls, ", \"modifiers\": [");
|
||||
int added = 0;
|
||||
if (e->modifiers().testFlag(Qt::ShiftModifier)) { bu_vls_printf(vls, "\"Shift\""); added=1; }
|
||||
if (e->modifiers().testFlag(Qt::ControlModifier)) { if(added) bu_vls_printf(vls, ", "); bu_vls_printf(vls, "\"Control\""); added=1; }
|
||||
if (e->modifiers().testFlag(Qt::AltModifier)) { if(added) bu_vls_printf(vls, ", "); bu_vls_printf(vls, "\"Alt\""); }
|
||||
bu_vls_printf(vls, "]");
|
||||
}
|
||||
|
||||
// TODO - look into QShortcut, see if it might be a better way
|
||||
// to manage this
|
||||
int CADkeyPressEvent(struct bview *v, int UNUSED(x_prev), int UNUSED(y_prev), QKeyEvent *k)
|
||||
{
|
||||
QTCAD_EVENT("keyPress", 1);
|
||||
if (!v) return 0;
|
||||
qtcad_init_bindings();
|
||||
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
char key_str[8] = {0};
|
||||
if (k->key() >= 0x20 && k->key() <= 0x7E) {
|
||||
key_str[0] = (char)k->key();
|
||||
} else {
|
||||
QString kstr = QKeySequence(k->key()).toString();
|
||||
strncpy(key_str, kstr.toStdString().c_str(), 7);
|
||||
if (!v)
|
||||
return 0;
|
||||
#if 0
|
||||
QString kstr = QKeySequence(k->key()).toString();
|
||||
bu_log("%s\n", kstr.toStdString().c_str());
|
||||
#endif
|
||||
switch (k->key()) {
|
||||
case 'A':
|
||||
v->gv_s->gv_adc.draw = !v->gv_s->gv_adc.draw;
|
||||
return 1;
|
||||
case 'M':
|
||||
v->gv_s->gv_model_axes.draw = !v->gv_s->gv_model_axes.draw;
|
||||
return 1;
|
||||
case 'V':
|
||||
v->gv_s->gv_view_axes.draw = !v->gv_s->gv_view_axes.draw;
|
||||
return 1;
|
||||
case '2':
|
||||
bn_decode_vect(v->gv_aet, "35 -25 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case '3':
|
||||
bn_decode_vect(v->gv_aet, "35 25 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case '4':
|
||||
bn_decode_vect(v->gv_aet, "45 45 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case '5':
|
||||
bn_decode_vect(v->gv_aet, "145 25 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case '6':
|
||||
bn_decode_vect(v->gv_aet, "215 25 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case '7':
|
||||
bn_decode_vect(v->gv_aet, "325 25 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case 'F':
|
||||
bn_decode_vect(v->gv_aet, "0 0 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case 'T':
|
||||
bn_decode_vect(v->gv_aet, "270 90 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case 'B':
|
||||
bn_decode_vect(v->gv_aet, "270 -90 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case 'L':
|
||||
bn_decode_vect(v->gv_aet, "90 0 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case 'R':
|
||||
if (k->modifiers().testFlag(Qt::ShiftModifier) == true) {
|
||||
bn_decode_vect(v->gv_aet, "180 0 0");
|
||||
} else {
|
||||
bn_decode_vect(v->gv_aet, "270 0 0");
|
||||
}
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"key-press\", \"key\": \"%s\"", key_str);
|
||||
append_modifiers(&ev_json, k);
|
||||
bu_vls_printf(&ev_json, "}");
|
||||
|
||||
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
|
||||
bu_vls_free(&ev_json);
|
||||
return handled;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int CADmousePressEvent(struct bview *v, int UNUSED(x_prev), int UNUSED(y_prev), QMouseEvent *e)
|
||||
{
|
||||
QTCAD_EVENT("mousePress", 1);
|
||||
if (!v) return 0;
|
||||
qtcad_init_bindings();
|
||||
|
||||
int button = -1;
|
||||
if (e->buttons().testFlag(Qt::LeftButton)) button = 0;
|
||||
else if (e->buttons().testFlag(Qt::MiddleButton)) button = 1;
|
||||
else if (e->buttons().testFlag(Qt::RightButton)) button = 2;
|
||||
if (button < 0) return 0;
|
||||
if (!v)
|
||||
return 0;
|
||||
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-press\", \"button\": %d", button);
|
||||
append_modifiers(&ev_json, e);
|
||||
bu_vls_printf(&ev_json, "}");
|
||||
|
||||
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
|
||||
bu_vls_free(&ev_json);
|
||||
return handled;
|
||||
// If we're intending the mouse motion to do the work,
|
||||
// then the press has to be a no-op. If we're going
|
||||
// to do configurable key bindings, this will take some
|
||||
// thought - if we want unmodded left button to be a
|
||||
// rotation, and Ctrl+Left to do something else, these
|
||||
// checks are all going to have to be exact-flag-combo-only
|
||||
// actions.
|
||||
if (e->modifiers()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (e->buttons().testFlag(Qt::LeftButton)) {
|
||||
//bu_log("Press Left\n");
|
||||
}
|
||||
if (e->buttons().testFlag(Qt::RightButton)) {
|
||||
//bu_log("Press Right\n");
|
||||
}
|
||||
if (e->buttons().testFlag(Qt::MiddleButton)) {
|
||||
//bu_log("Press Middle\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int CADmouseReleaseEvent(struct bview *v, double x_press, double y_press, int UNUSED(x_prev), int UNUSED(y_prev), QMouseEvent *e, int mode)
|
||||
{
|
||||
QTCAD_EVENT("mouseRelease", 1);
|
||||
if (!v) return 0;
|
||||
qtcad_init_bindings();
|
||||
|
||||
if (!v)
|
||||
return 0;
|
||||
|
||||
restore_drag_bounds_update(v, 1);
|
||||
|
||||
// If we're intending the mouse motion to do the work,
|
||||
// then the release has to be a no-op. If we're going
|
||||
// to do configurable key bindings, this will take some
|
||||
// thought - if we want unmodded left button to be a
|
||||
// rotation, and Ctrl+Left to do something else, these
|
||||
// checks are all going to have to be exact-flag-combo-only
|
||||
// actions.
|
||||
if (e->modifiers()) {
|
||||
return 0;
|
||||
}
|
||||
if (e->buttons().testFlag(Qt::LeftButton) || e->buttons().testFlag(Qt::RightButton)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double cx, cy;
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
double cx = e->x();
|
||||
double cy = e->y();
|
||||
cx = (double)e->x();
|
||||
cy = (double)e->y();
|
||||
#else
|
||||
double cx = e->position().x();
|
||||
double cy = e->position().y();
|
||||
cx = e->position().x();
|
||||
cy = e->position().y();
|
||||
#endif
|
||||
if ((fabs(cx - x_press) > 10) || (fabs(cy - y_press) > 10))
|
||||
return 0;
|
||||
|
||||
if ((fabs(cx - x_press) > 10) || (fabs(cy - y_press) > 10)) return 0;
|
||||
int dx = 1;
|
||||
int dy = 1;
|
||||
unsigned long long view_flags = BV_IDLE;
|
||||
|
||||
int button = -1;
|
||||
if (e->button() == Qt::LeftButton) button = 0;
|
||||
else if (e->button() == Qt::MiddleButton) button = 1;
|
||||
else if (e->button() == Qt::RightButton) button = 2;
|
||||
if (button < 0) return 0;
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
//bu_log("Release Left\n");
|
||||
if (mode != BV_CENTER) {
|
||||
view_flags = BV_SCALE;
|
||||
dx = 10;
|
||||
dy = 5;
|
||||
} else {
|
||||
view_flags = BV_CENTER;
|
||||
dx = (int)cx;
|
||||
dy = (int)cy;
|
||||
}
|
||||
}
|
||||
if (e->button() == Qt::RightButton) {
|
||||
//bu_log("Release Right\n");
|
||||
if (mode == BV_CENTER)
|
||||
return 0;
|
||||
view_flags = BV_SCALE;
|
||||
dx = 1;
|
||||
dy = 2;
|
||||
}
|
||||
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-release\", \"button\": %d, \"cx\": %d, \"cy\": %d, \"mode\": %d", button, (int)cx, (int)cy, mode);
|
||||
append_modifiers(&ev_json, e);
|
||||
bu_vls_printf(&ev_json, "}");
|
||||
|
||||
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
|
||||
bu_vls_free(&ev_json);
|
||||
return handled;
|
||||
if (e->button() == Qt::MiddleButton) {
|
||||
//bu_log("Release Center\n");
|
||||
view_flags = BV_CENTER;
|
||||
dx = (int)cx;
|
||||
dy = (int)cy;
|
||||
}
|
||||
|
||||
point_t keypt = VINIT_ZERO;
|
||||
return bv_adjust(v, dx, dy, keypt, 0, view_flags);
|
||||
}
|
||||
|
||||
int CADmouseMoveEvent(struct bview *v, int x_prev, int y_prev, QMouseEvent *e, int mode)
|
||||
{
|
||||
QTCAD_EVENT("mouseMove", 2);
|
||||
if (!v) return 0;
|
||||
qtcad_init_bindings();
|
||||
|
||||
if (x_prev == -INT_MAX) return 0;
|
||||
if (!e->buttons().testFlag(Qt::LeftButton)) return 0;
|
||||
if (!v)
|
||||
return 0;
|
||||
|
||||
unsigned long long view_flags = BV_IDLE;
|
||||
|
||||
if (x_prev == -INT_MAX) {
|
||||
//x_prev = e->x();
|
||||
//y_prev = e->y();
|
||||
return 0;
|
||||
}
|
||||
|
||||
view_flags = mode;
|
||||
if (mode == BV_CENTER)
|
||||
view_flags = BV_SCALE;
|
||||
|
||||
if (e->buttons().testFlag(Qt::LeftButton)) {
|
||||
//bu_log("Left\n");
|
||||
|
||||
if (e->modifiers().testFlag(Qt::ControlModifier)) {
|
||||
//bu_log("Ctrl+Left\n");
|
||||
view_flags = BV_ROT;
|
||||
}
|
||||
|
||||
if (e->modifiers().testFlag(Qt::ShiftModifier)) {
|
||||
//bu_log("Shift+Left\n");
|
||||
view_flags = BV_TRANS;
|
||||
}
|
||||
|
||||
if (e->modifiers().testFlag(Qt::ShiftModifier) && e->modifiers().testFlag(Qt::ControlModifier)) {
|
||||
//bu_log("Ctrl+Shift+Left\n");
|
||||
view_flags = BV_SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
if (e->buttons().testFlag(Qt::MiddleButton)) {
|
||||
//bu_log("Middle\n");
|
||||
}
|
||||
|
||||
if (e->buttons().testFlag(Qt::RightButton)) {
|
||||
//bu_log("Right\n");
|
||||
}
|
||||
|
||||
if (!e->buttons().testFlag(Qt::LeftButton))
|
||||
return 0;
|
||||
|
||||
long long now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
std::unordered_map<struct bview *, long long>::iterator ts_it = drag_update_ts.find(v);
|
||||
if (ts_it != drag_update_ts.end() && (now_ms - ts_it->second) < drag_update_interval_ms) return -1;
|
||||
if (ts_it != drag_update_ts.end() && (now_ms - ts_it->second) < drag_update_interval_ms)
|
||||
return -1;
|
||||
drag_update_ts[v] = now_ms;
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
@@ -334,33 +319,53 @@ int CADmouseMoveEvent(struct bview *v, int x_prev, int y_prev, QMouseEvent *e, i
|
||||
int dy = e->position().y() - y_prev;
|
||||
#endif
|
||||
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-move\", \"button\": 0, \"dx\": %d, \"dy\": %d, \"mode\": %d", dx, dy, mode);
|
||||
append_modifiers(&ev_json, e);
|
||||
bu_vls_printf(&ev_json, "}");
|
||||
|
||||
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
|
||||
bu_vls_free(&ev_json);
|
||||
return handled;
|
||||
if (view_flags == BV_SCALE) {
|
||||
// Build in some sensitivity to how much the mouse moved when doing
|
||||
// a motion based scale
|
||||
int mdelta = (abs(dx) > abs(dy)) ? dx : -dy;
|
||||
int f = (int)(2*100*(double)abs(mdelta)/(double)v->gv_height);
|
||||
|
||||
if (mdelta > 0) {
|
||||
dy = 101 + f;
|
||||
dx = 100;
|
||||
} else {
|
||||
dy = 99 - f;
|
||||
dx = 100;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO - the key point and the mode/flags are all hardcoded
|
||||
// right now, but eventually for shift grips they will need to
|
||||
// respond to the various mod keys. The intent is to set flags
|
||||
// based on which mod keys are set to allow bv_adjust to
|
||||
// do the correct math.
|
||||
point_t center;
|
||||
MAT_DELTAS_GET_NEG(center, v->gv_center);
|
||||
|
||||
if (view_flags & (BV_ROT | BV_TRANS | BV_SCALE))
|
||||
suspend_drag_bounds_update(v);
|
||||
|
||||
return bv_adjust(v, dx, dy, center, 0, view_flags);
|
||||
|
||||
}
|
||||
|
||||
int CADwheelEvent(struct bview *v, QWheelEvent *e)
|
||||
{
|
||||
QTCAD_EVENT("mouseWheel", 1);
|
||||
if (!v) return 0;
|
||||
qtcad_init_bindings();
|
||||
|
||||
if (!v)
|
||||
return 0;
|
||||
|
||||
QPoint delta = e->angleDelta();
|
||||
int mdelta = -1 * delta.y() / 8;
|
||||
int mdelta = -1* delta.y() / 8;
|
||||
|
||||
int dx = 100 + mdelta;
|
||||
int dy = 100;
|
||||
|
||||
point_t origin = VINIT_ZERO;
|
||||
return bv_adjust(v, dx, dy, origin, 0, BV_SCALE);
|
||||
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-wheel\", \"delta\": %d", mdelta);
|
||||
append_modifiers(&ev_json, e);
|
||||
bu_vls_printf(&ev_json, "}");
|
||||
|
||||
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
|
||||
bu_vls_free(&ev_json);
|
||||
return handled;
|
||||
}
|
||||
|
||||
// Local Variables:
|
||||
|
||||
+234
-180
@@ -49,7 +49,6 @@
|
||||
#include "bu/path.h"
|
||||
#include "bu/process.h"
|
||||
#include "bu/units.h"
|
||||
#include "bu/binding.h"
|
||||
#include "vmath.h"
|
||||
#include "rt/db4.h"
|
||||
#include "rt/geom.h"
|
||||
@@ -2825,148 +2824,6 @@ to_data_vZ(struct ged *gedp,
|
||||
}
|
||||
|
||||
|
||||
static void tclcad_action_eval_aet(const struct bu_vls *ev, void *reg, void *event_data) {
|
||||
struct bview *gdvp = (struct bview *)event_data;
|
||||
const char *aet_args = (const char *)reg;
|
||||
struct bu_vls cmd = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&cmd, "%s aet %s %s", bu_vls_cstr(¤t_top->to_gedp->go_name), bu_vls_cstr(&gdvp->gv_name), aet_args);
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
|
||||
bu_vls_free(&cmd);
|
||||
}
|
||||
|
||||
static void tclcad_action_mouse_drag(const struct bu_vls *ev, void *reg, void *event_data) {
|
||||
struct bview *gdvp = (struct bview *)event_data;
|
||||
const char *cmd_name = (const char *)reg;
|
||||
int cx = gdvp->gv_prevMouseX;
|
||||
int cy = gdvp->gv_prevMouseY;
|
||||
char *p;
|
||||
if ((p = strstr(bu_vls_cstr(ev), "\"cx\":"))) sscanf(p + 5, "%d", &cx);
|
||||
if ((p = strstr(bu_vls_cstr(ev), "\"cy\":"))) sscanf(p + 5, "%d", &cy);
|
||||
|
||||
struct bu_vls cmd = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&cmd, "%s %s %s %d %d", bu_vls_cstr(¤t_top->to_gedp->go_name), cmd_name, bu_vls_cstr(&gdvp->gv_name), cx, cy);
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
|
||||
bu_vls_free(&cmd);
|
||||
}
|
||||
|
||||
static void tclcad_action_wheel_scale(const struct bu_vls *ev, void *reg, void *event_data) {
|
||||
struct bview *gdvp = (struct bview *)event_data;
|
||||
int delta = 0;
|
||||
char *p;
|
||||
if ((p = strstr(bu_vls_cstr(ev), "\"delta\":"))) sscanf(p + 8, "%d", &delta);
|
||||
const char *sf = (delta > 0) ? "1.1" : "0.9";
|
||||
struct bu_vls cmd = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&cmd, "%s zoom %s %s", bu_vls_cstr(¤t_top->to_gedp->go_name), bu_vls_cstr(&gdvp->gv_name), sf);
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
|
||||
bu_vls_free(&cmd);
|
||||
}
|
||||
|
||||
static void tclcad_action_mouse_click(const struct bu_vls *ev, void *reg, void *event_data) {
|
||||
struct bview *gdvp = (struct bview *)event_data;
|
||||
const char *cmd_format = (const char *)reg;
|
||||
int cx = gdvp->gv_prevMouseX;
|
||||
int cy = gdvp->gv_prevMouseY;
|
||||
char *p;
|
||||
if ((p = strstr(bu_vls_cstr(ev), "\"cx\":"))) sscanf(p + 5, "%d", &cx);
|
||||
if ((p = strstr(bu_vls_cstr(ev), "\"cy\":"))) sscanf(p + 5, "%d", &cy);
|
||||
|
||||
struct bu_vls cmd = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&cmd, cmd_format, bu_vls_cstr(¤t_top->to_gedp->go_name), bu_vls_cstr(&gdvp->gv_name), cx, cy);
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
|
||||
bu_vls_free(&cmd);
|
||||
}
|
||||
|
||||
static int bindings_loaded_mged = 0;
|
||||
|
||||
static int tclcad_load_bindings_cmd(ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]) {
|
||||
if (argc < 2) {
|
||||
Tcl_AppendResult(interp, "wrong # args: should be \"", argv[0], " <json_path>\"", NULL);
|
||||
return TCL_ERROR;
|
||||
}
|
||||
|
||||
char *path = (char *)bu_dir(NULL, 0, BU_DIR_DATA, argv[1], NULL);
|
||||
if (!path) {
|
||||
Tcl_AppendResult(interp, "Failed to resolve path: ", argv[1], NULL);
|
||||
return TCL_ERROR;
|
||||
}
|
||||
|
||||
if (bu_binding_load(path) < 0) {
|
||||
bu_free(path, "path");
|
||||
Tcl_AppendResult(interp, "Failed to load bindings from: ", argv[1], NULL);
|
||||
return TCL_ERROR;
|
||||
}
|
||||
bu_free(path, "path");
|
||||
|
||||
/* Validate that application provided required bindings */
|
||||
if (!bu_binding_action_is_bound("dm", "view_mouse_drag_rotate") ||
|
||||
!bu_binding_action_is_bound("dm", "view_mouse_drag_translate") ||
|
||||
!bu_binding_action_is_bound("dm", "view_mouse_drag_scale")) {
|
||||
Tcl_AppendResult(interp, "Validation Failed: The provided bindings do not map inputs for view_mouse_drag_rotate/translate/scale. These are required standard actions.", NULL);
|
||||
return TCL_ERROR;
|
||||
}
|
||||
|
||||
return TCL_OK;
|
||||
}
|
||||
|
||||
static int tclcad_dispatch_event_cmd(ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]) {
|
||||
if (argc < 8) return TCL_OK;
|
||||
struct tclcad_obj *to = (struct tclcad_obj *)clientData;
|
||||
struct bview *gdvp = bv_set_find_view(&to->to_gedp->ged_views, argv[2]);
|
||||
if (!gdvp) return TCL_OK;
|
||||
|
||||
int x = atoi(argv[5]);
|
||||
int y = atoi(argv[6]);
|
||||
int state = atoi(argv[7]);
|
||||
|
||||
struct bu_vls ev = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&ev, "{\"type\": \"%s\"", argv[3]);
|
||||
|
||||
if (!strcmp(argv[3], "key-press")) {
|
||||
char key_str[16] = {0};
|
||||
bu_strlcpy(key_str, argv[4], sizeof(key_str));
|
||||
if (strlen(key_str) == 1 && key_str[0] >= 'a' && key_str[0] <= 'z') {
|
||||
key_str[0] = key_str[0] - 'a' + 'A';
|
||||
}
|
||||
bu_vls_printf(&ev, ", \"key\": \"%s\"", key_str);
|
||||
} else if (!strcmp(argv[3], "mouse-wheel")) {
|
||||
int delta = (argc > 8) ? atoi(argv[8]) : 0;
|
||||
int mdelta = -1 * delta / 120;
|
||||
bu_vls_printf(&ev, ", \"delta\": %d", mdelta);
|
||||
} else {
|
||||
int btn = atoi(argv[4]) - 1;
|
||||
if (!strcmp(argv[3], "mouse-move")) {
|
||||
if (state & 0x100) btn = 0;
|
||||
else if (state & 0x200) btn = 1;
|
||||
else if (state & 0x400) btn = 2;
|
||||
else btn = -1;
|
||||
|
||||
int dx = x - gdvp->gv_prevMouseX;
|
||||
int dy = y - gdvp->gv_prevMouseY;
|
||||
bu_vls_printf(&ev, ", \"dx\": %d, \"dy\": %d, \"mode\": 0", dx, dy);
|
||||
}
|
||||
if (btn >= 0) bu_vls_printf(&ev, ", \"button\": %d", btn);
|
||||
bu_vls_printf(&ev, ", \"cx\": %d, \"cy\": %d", x, y);
|
||||
}
|
||||
|
||||
if (state & (1 | 4 | 8)) {
|
||||
bu_vls_printf(&ev, ", \"modifiers\": [");
|
||||
int added = 0;
|
||||
if (state & 1) { bu_vls_printf(&ev, "\"Shift\""); added=1; }
|
||||
if (state & 4) { if(added) bu_vls_printf(&ev, ", "); bu_vls_printf(&ev, "\"Control\""); added=1; }
|
||||
if (state & 8) { if(added) bu_vls_printf(&ev, ", "); bu_vls_printf(&ev, "\"Alt\""); }
|
||||
bu_vls_printf(&ev, "]");
|
||||
}
|
||||
bu_vls_printf(&ev, "}");
|
||||
|
||||
bu_binding_process_event("dm", bu_vls_cstr(&ev), gdvp);
|
||||
|
||||
gdvp->gv_prevMouseX = x;
|
||||
gdvp->gv_prevMouseY = y;
|
||||
|
||||
bu_vls_free(&ev);
|
||||
return TCL_OK;
|
||||
}
|
||||
|
||||
static void
|
||||
to_init_default_bindings(struct bview *gdvp)
|
||||
{
|
||||
@@ -2975,35 +2832,6 @@ to_init_default_bindings(struct bview *gdvp)
|
||||
if (dm_get_pathname((struct dm *)gdvp->dmp)) {
|
||||
struct bu_vls *pathvls = dm_get_pathname((struct dm *)gdvp->dmp);
|
||||
if (pathvls) {
|
||||
if (!bindings_loaded_mged) {
|
||||
bindings_loaded_mged = 1;
|
||||
|
||||
|
||||
bu_binding_register_action("view_2", tclcad_action_eval_aet, (void*)"35 -25 0");
|
||||
bu_binding_register_action("view_3", tclcad_action_eval_aet, (void*)"35 25 0");
|
||||
bu_binding_register_action("view_4", tclcad_action_eval_aet, (void*)"45 45 0");
|
||||
bu_binding_register_action("view_5", tclcad_action_eval_aet, (void*)"145 25 0");
|
||||
bu_binding_register_action("view_6", tclcad_action_eval_aet, (void*)"215 25 0");
|
||||
bu_binding_register_action("view_7", tclcad_action_eval_aet, (void*)"325 25 0");
|
||||
bu_binding_register_action("view_front", tclcad_action_eval_aet, (void*)"0 0 0");
|
||||
bu_binding_register_action("view_top", tclcad_action_eval_aet, (void*)"270 90 0");
|
||||
bu_binding_register_action("view_bottom", tclcad_action_eval_aet, (void*)"270 -90 0");
|
||||
bu_binding_register_action("view_left", tclcad_action_eval_aet, (void*)"90 0 0");
|
||||
bu_binding_register_action("view_rear", tclcad_action_eval_aet, (void*)"180 0 0");
|
||||
bu_binding_register_action("view_right", tclcad_action_eval_aet, (void*)"270 0 0");
|
||||
|
||||
bu_binding_register_action("view_mouse_drag_rotate", tclcad_action_mouse_drag, (void*)"mouse_rot");
|
||||
bu_binding_register_action("view_mouse_drag_translate", tclcad_action_mouse_drag, (void*)"mouse_trans");
|
||||
bu_binding_register_action("view_mouse_drag_scale", tclcad_action_mouse_drag, (void*)"mouse_scale");
|
||||
bu_binding_register_action("view_mouse_wheel_scale", tclcad_action_wheel_scale, NULL);
|
||||
|
||||
bu_binding_register_action("view_mouse_press_left", tclcad_action_mouse_click, (void*)"%s zoom %s 0.5");
|
||||
bu_binding_register_action("view_mouse_press_right", tclcad_action_mouse_click, (void*)"%s zoom %s 2.0");
|
||||
|
||||
Tcl_CreateCommand(current_top->to_interp, "tclcad_dispatch_event", tclcad_dispatch_event_cmd, (ClientData)current_top, (Tcl_CmdDeleteProc *)NULL);
|
||||
Tcl_CreateCommand(current_top->to_interp, "tclcad_load_bindings", tclcad_load_bindings_cmd, (ClientData)current_top, (Tcl_CmdDeleteProc *)NULL);
|
||||
}
|
||||
|
||||
bu_vls_printf(&bindings, "bind %s <Configure> {%s configure %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
@@ -3020,14 +2848,240 @@ to_init_default_bindings(struct bview *gdvp)
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Dispatch all events to bu_binding */
|
||||
bu_vls_printf(&bindings, "bind %s <Any-KeyPress> {tclcad_dispatch_event %%W %s %s key-press %%K %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(¤t_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Any-ButtonPress> {tclcad_dispatch_event %%W %s %s mouse-press %%b %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(¤t_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Any-ButtonRelease> {tclcad_dispatch_event %%W %s %s mouse-release %%b %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(¤t_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Any-Motion> {tclcad_dispatch_event %%W %s %s mouse-move 0 %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(¤t_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <MouseWheel> {tclcad_dispatch_event %%W %s %s mouse-wheel 0 %%x %%y %%s %%D; break}; ", bu_vls_addr(pathvls), bu_vls_addr(¤t_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&bindings));
|
||||
/* Mouse Bindings */
|
||||
bu_vls_printf(&bindings, "bind %s <2> {%s vslew %s %%x %%y; focus %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name),
|
||||
bu_vls_addr(pathvls));
|
||||
bu_vls_printf(&bindings, "bind %s <1> {%s zoom %s 0.5; focus %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name),
|
||||
bu_vls_addr(pathvls));
|
||||
bu_vls_printf(&bindings, "bind %s <3> {%s zoom %s 2.0; focus %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name),
|
||||
bu_vls_addr(pathvls));
|
||||
bu_vls_printf(&bindings, "bind %s <4> {%s zoom %s 1.1; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <5> {%s zoom %s 0.9; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <MouseWheel> {if {%%D < 0} {%s zoom %s 0.9} else {%s zoom %s 1.1}; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Idle Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <ButtonRelease> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Control_L> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Control_R> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Shift_L> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Shift_R> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Alt_L> {%s idle_mode %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Alt_R> {%s idle_mode %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Rotate Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <Control-ButtonRelease-1> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-ButtonPress-1> {%s rotate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-ButtonPress-2> {%s rotate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-ButtonPress-3> {%s rotate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Translate Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-ButtonRelease-1> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-ButtonPress-1> {%s translate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-ButtonPress-2> {%s translate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-ButtonPress-3> {%s translate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Scale Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonRelease-1> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonPress-1> {%s scale_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonPress-2> {%s scale_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonPress-3> {%s scale_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Constrained Rotate Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonRelease-1> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonPress-1> {%s constrain_rmode %s x %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonPress-2> {%s constrain_rmode %s y %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonPress-3> {%s constrain_rmode %s z %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Constrained Translate Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonRelease-1> {%s idle_mode %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonPress-1> {%s constrain_tmode %s x %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonPress-2> {%s constrain_tmode %s y %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonPress-3> {%s constrain_tmode %s z %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Key Bindings */
|
||||
bu_vls_printf(&bindings, "bind %s 3 {%s aet %s 35 25; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s 4 {%s aet %s 45 45; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s f {%s aet %s 0 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s F {%s aet %s 0 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s R {%s aet %s 180 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s r {%s aet %s 270 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s l {%s aet %s 90 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s L {%s aet %s 90 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s t {%s aet %s 270 90; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s T {%s aet %s 270 90; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s b {%s aet %s 270 -90; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s B {%s aet %s 270 -90; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s + {%s zoom %s 2.0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s = {%s zoom %s 2.0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s _ {%s zoom %s 0.5; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s - {%s zoom %s 0.5; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Key-Left> {%s rot %s -v 0 1 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Key-Right> {%s rot %s -v 0 -1 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Key-Up> {%s rot %s -v 1 0 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Key-Down> {%s rot %s -v -1 0 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_addr(&bindings));
|
||||
}
|
||||
}
|
||||
bu_vls_free(&bindings);
|
||||
|
||||
+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}
|
||||
|
||||
@@ -49,8 +49,12 @@ proc mged_bind_dm { w } {
|
||||
|
||||
bind $w <Enter> [list mged_activate_dm $w]
|
||||
|
||||
# Load standardized JSON bindings for mged
|
||||
#default mouse bindings
|
||||
default_mouse_bindings $w
|
||||
|
||||
#default key bindings
|
||||
set forwarding_key($w) 0
|
||||
default_key_bindings $w
|
||||
}
|
||||
|
||||
proc print_return_val str {
|
||||
@@ -60,6 +64,129 @@ proc print_return_val str {
|
||||
}
|
||||
}
|
||||
|
||||
if ![info exists mged_default(dm_key_bindings)] {
|
||||
set mged_default(dm_key_bindings) "\tKey Sequence\t\tBehavior
|
||||
\ta\t\t\ttoggle angle distance cursor (ADC)
|
||||
\te\t\t\ttoggle edit axes
|
||||
\tm\t\t\ttoggle model axes
|
||||
\tv\t\t\ttoggle view axes
|
||||
\ti\t\t\tadvance illumination pointer forward
|
||||
\tI\t\t\tadvance illumination pointer backward
|
||||
\tp\t\t\tsimulate mouse press (i.e. to pick a solid)
|
||||
\t3\t\t\tview - ae 35 25
|
||||
\t4\t\t\tview - ae 45 45
|
||||
\tf\t\t\tfront view
|
||||
\tt\t\t\ttop view
|
||||
\tb\t\t\tbottom view
|
||||
\tl\t\t\tleft view
|
||||
\tr\t\t\tright view
|
||||
\tR\t\t\trear view
|
||||
\ts\t\t\tenter solid illumination state
|
||||
\to\t\t\tenter object illumination state
|
||||
\tq\t\t\treject edit
|
||||
\tu\t\t\tzero knobs and sliders
|
||||
\t<F1>\t\t\ttoggle depthcue
|
||||
\t<F2>\t\t\ttoggle zclip
|
||||
\t<F3>\t\t\ttoggle perspective
|
||||
\t<F4>\t\t\ttoggle zbuffer
|
||||
\t<F5>\t\t\ttoggle lighting
|
||||
\t<F6>\t\t\ttoggle perspective angle
|
||||
\t<F7>\t\t\ttoggle faceplate
|
||||
\t<F8>\t\t\ttoggle faceplate GUI
|
||||
\t<F9>\t\t\ttoggle keystroke forwarding
|
||||
\t<F12>\t\t\tzero knobs
|
||||
\t<Left>\t\t\tabsolute rotate about y axis
|
||||
\t<Right>\t\t\tabsolute rotate about y axis
|
||||
\t<Down>\t\t\tabsolute rotate about x axis
|
||||
\t<Up>\t\t\tabsolute rotate about x axis
|
||||
\t<Shift-Left>\t\tabsolute translate in X direction
|
||||
\t<Shift-Right>\t\tabsolute translate in X direction
|
||||
\t<Shift-Down>\t\tabsolute translate in Z direction
|
||||
\t<Shift-Up>\t\tabsolute translate in Z direction
|
||||
\t<Control-Shift-Left>\tabsolute rotate about z axis
|
||||
\t<Control-Shift-Right>\tabsolute rotate about z axis
|
||||
\t<Control-Shift-Down>\tabsolute translate in Y direction
|
||||
\t<Control-Shift-Up>\tabsolute translate in Y direction
|
||||
\t<Control-n>\t\tgoto next view
|
||||
\t<Control-p>\t\tgoto previous view
|
||||
\t<Control-t>\t\ttoggle between the current view and the last view"
|
||||
}
|
||||
|
||||
proc default_key_bindings { w } {
|
||||
# common view shortcuts
|
||||
bind $w 2 "winset $w; ae 35 -25; break"
|
||||
bind $w 3 "winset $w; press 35,25; break"
|
||||
bind $w 4 "winset $w; press 45,45; break"
|
||||
bind $w 5 "winset $w; ae 145 25; break"
|
||||
bind $w 6 "winset $w; ae 215 25; break"
|
||||
bind $w 7 "winset $w; ae 325 25; break"
|
||||
|
||||
# default key shortcuts
|
||||
bind $w a "winset $w; adc; break"
|
||||
bind $w b "winset $w; press bottom; break"
|
||||
bind $w c "open_cmd_win id_0"
|
||||
bind $w e "winset $w; rset ax edit_draw !; update_gui $w edit_draw \[rset ax edit_draw\]; break"
|
||||
bind $w f "winset $w; press front; break"
|
||||
bind $w i "winset $w; aip f; break"
|
||||
bind $w l "winset $w; press left; break"
|
||||
bind $w m "winset $w; rset ax model_draw !; update_gui $w model_draw \[rset ax model_draw\]; break"
|
||||
bind $w o "winset $w; press oill; break"
|
||||
bind $w p "winset $w; M 1 0 0; break"
|
||||
bind $w q "winset $w; press reject; break"
|
||||
bind $w r "winset $w; press right; break"
|
||||
bind $w s "winset $w; press sill; break"
|
||||
bind $w t "winset $w; press top; break"
|
||||
bind $w u "winset $w; svb; break"
|
||||
bind $w v "winset $w; rset ax view_draw !; update_gui $w view_draw \[rset ax view_draw\]; break"
|
||||
|
||||
# default shift+key shortcuts
|
||||
bind $w A "winset $w; press accept; break"
|
||||
bind $w I "winset $w; aip b; break"
|
||||
bind $w N "winset $w; puts \[nirt -b\]; break"
|
||||
bind $w P "winset $w; catch {sed_apply}; break"
|
||||
bind $w R "winset $w; press rear; break"
|
||||
bind $w S "winset $w; catch {sed_reset}; break"
|
||||
|
||||
# default control+key shortcuts
|
||||
bind $w <Control-n> "winset $w; _mged_view_ring next; break"
|
||||
bind $w <Control-p> "winset $w; _mged_view_ring prev; break"
|
||||
bind $w <Control-t> "winset $w; _mged_view_ring toggle; break"
|
||||
|
||||
# shift grips navigation
|
||||
bind $w <Control-Shift-Down> "winset $w; knob -i aY \$mged_default(tran_factor); break"
|
||||
bind $w <Control-Shift-Left> "winset $w; knob -i az \$mged_default(rot_factor); break"
|
||||
bind $w <Control-Shift-Right> "winset $w; knob -i az -\$mged_default(rot_factor); break"
|
||||
bind $w <Control-Shift-Up> "winset $w; knob -i aY -\$mged_default(tran_factor); break"
|
||||
bind $w <Down> "winset $w; knob -i ax \$mged_default(rot_factor); break"
|
||||
bind $w <Left> "winset $w; knob -i ay -\$mged_default(rot_factor); break"
|
||||
bind $w <Right> "winset $w; knob -i ay \$mged_default(rot_factor); break"
|
||||
bind $w <Up> "winset $w; knob -i ax -\$mged_default(rot_factor); break"
|
||||
bind $w <Shift-Down> "winset $w; knob -i aZ -\$mged_default(tran_factor); break"
|
||||
bind $w <Shift-Left> "winset $w; knob -i aX \$mged_default(tran_factor); break"
|
||||
bind $w <Shift-Right> "winset $w; knob -i aX -\$mged_default(tran_factor); break"
|
||||
bind $w <Shift-Up> "winset $w; knob -i aZ \$mged_default(tran_factor); break"
|
||||
|
||||
# function keys (settings)
|
||||
bind $w <F1> "winset $w; dm set depthcue !; update_gui $w depthcue \[dm set depthcue\]; break"
|
||||
bind $w <F2> "winset $w; dm set zclip !; update_gui $w zclip \[dm set zclip\]; break"
|
||||
bind $w <F3> "winset $w; set perspective_mode !; update_gui $w perspective_mode \$perspective_mode; break"
|
||||
bind $w <F4> "winset $w; dm set zbuffer !; update_gui $w zbuffer \[dm set zbuffer\]; break"
|
||||
bind $w <F5> "winset $w; dm set lighting !; update_gui $w lighting \[dm set lighting\]; break"
|
||||
bind $w <F6> "winset $w; set toggle_perspective !; break"
|
||||
bind $w <F7> "winset $w; set faceplate !; update_gui $w faceplate \$faceplate; break"
|
||||
bind $w <F8> "winset $w; set orig_gui !; update_gui $w orig_gui \$orig_gui; break"
|
||||
bind $w <F9> "toggle_forward_key_bindings $w; update_gui $w forward_keys \$forwarding_key($w); break"
|
||||
bind $w <F12> "winset $w; knob zero; break"
|
||||
|
||||
# interrupt
|
||||
bind $w <Escape> "winset $w; reset_everything $w; break"
|
||||
|
||||
# Throw away other key events
|
||||
bind $w <KeyPress> {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
proc reset_everything { w } {
|
||||
global mged_gui
|
||||
|
||||
@@ -75,6 +202,274 @@ proc reset_everything { w } {
|
||||
set_mouse_behavior $id
|
||||
}
|
||||
|
||||
proc set_forward_keys { w val } {
|
||||
global forwarding_key
|
||||
|
||||
set forwarding_key($w) $val
|
||||
if {$forwarding_key($w)} {
|
||||
forward_key_bindings $w
|
||||
} else {
|
||||
default_key_bindings $w
|
||||
}
|
||||
}
|
||||
|
||||
proc toggle_forward_key_bindings { w } {
|
||||
global forwarding_key
|
||||
|
||||
if {$forwarding_key($w)} {
|
||||
default_key_bindings $w
|
||||
set forwarding_key($w) 0
|
||||
} else {
|
||||
forward_key_bindings $w
|
||||
set forwarding_key($w) 1
|
||||
}
|
||||
}
|
||||
|
||||
proc forward_key_bindings { w } {
|
||||
set id [get_player_id_dm $w]
|
||||
|
||||
if {$id == "mged"} {
|
||||
return
|
||||
}
|
||||
|
||||
# First, unset the default key bindings
|
||||
bind $w a {}
|
||||
bind $w e {}
|
||||
bind $w m {}
|
||||
bind $w v {}
|
||||
bind $w i {}
|
||||
bind $w I {}
|
||||
bind $w p {}
|
||||
bind $w 2 {}
|
||||
bind $w 3 {}
|
||||
bind $w 4 {}
|
||||
bind $w 5 {}
|
||||
bind $w 6 {}
|
||||
bind $w 7 {}
|
||||
bind $w f {}
|
||||
bind $w t {}
|
||||
bind $w b {}
|
||||
bind $w l {}
|
||||
bind $w N {}
|
||||
bind $w r {}
|
||||
bind $w R {}
|
||||
bind $w s {}
|
||||
bind $w o {}
|
||||
bind $w q {}
|
||||
bind $w u {}
|
||||
bind $w c {}
|
||||
bind $w A {}
|
||||
bind $w P {}
|
||||
bind $w S {}
|
||||
bind $w <F1> {}
|
||||
bind $w <F2> {}
|
||||
bind $w <F3> {}
|
||||
bind $w <F4> {}
|
||||
bind $w <F5> {}
|
||||
bind $w <F6> {}
|
||||
bind $w <F7> {}
|
||||
bind $w <F8> {}
|
||||
bind $w <F12> {}
|
||||
|
||||
bind $w <Left> {}
|
||||
bind $w <Right> {}
|
||||
bind $w <Down> {}
|
||||
bind $w <Up> {}
|
||||
bind $w <Shift-Left> {}
|
||||
bind $w <Shift-Right> {}
|
||||
bind $w <Shift-Down> {}
|
||||
bind $w <Shift-Up> {}
|
||||
bind $w <Control-Shift-Left> {}
|
||||
bind $w <Control-Shift-Right> {}
|
||||
bind $w <Control-Shift-Down> {}
|
||||
bind $w <Control-Shift-Up> {}
|
||||
|
||||
bind $w <Control-n> {}
|
||||
bind $w <Control-p> {}
|
||||
bind $w <Control-t> {}
|
||||
|
||||
bind $w <Escape> {}
|
||||
|
||||
# The focus commands in the binding below are necessary to insure
|
||||
# that .$id.t gets the event.
|
||||
bind $w <KeyPress> "\
|
||||
focus .$id.t;\
|
||||
set mged_gui(.$id.t,insert_char_flag) 1;\
|
||||
event generate .$id.t <KeyPress> -state %s -keysym %K;\
|
||||
set mged_gui(.$id.t,insert_char_flag) 0;\
|
||||
focus %W;\
|
||||
break"
|
||||
}
|
||||
|
||||
proc default_mouse_bindings { w } {
|
||||
global no_focus_hack
|
||||
global transform
|
||||
global tcl_platform
|
||||
|
||||
# default button bindings
|
||||
if {![regexp {^[0-9]+} $::tcl_platform(osVersion) osMajorVersion]} {
|
||||
set osMajorVersion 0
|
||||
}
|
||||
|
||||
if {[info exists no_focus_hack] && $no_focus_hack} {
|
||||
bind $w <1> "winset $w; zoom 0.5; break"
|
||||
bind $w <2> "winset $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
|
||||
bind $w <3> "winset $w; zoom 2.0; break"
|
||||
|
||||
# Mouse wheel fun with Tcl/Tk - see https://wiki.tcl-lang.org/page/mousewheel
|
||||
bind $w <MouseWheel> "winset $w; focus $w; if {%D < 0} {zoom 0.9} else {zoom 1.1}"
|
||||
bind $w <Button-4> {event generate [focus] <MouseWheel> -delta 120}
|
||||
bind $w <Button-5> {event generate [focus] <MouseWheel> -delta -120}
|
||||
|
||||
if {$::tcl_platform(os) == "Darwin"} {
|
||||
# Mac option-key emulates mouse 2 (doesn't work?)
|
||||
bind $w <Option-ButtonPress-1> "winset $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
|
||||
# Mac command-key emulates mouse 3
|
||||
bind $w <Mod2-ButtonPress-1> "winset $w; zoom 2.0; break"
|
||||
}
|
||||
} else {
|
||||
bind $w <1> "winset $w; focus $w; zoom 0.5; break"
|
||||
bind $w <2> "winset $w; focus $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
|
||||
bind $w <3> "winset $w; focus $w; zoom 2.0; break"
|
||||
|
||||
# Mouse wheel fun with Tcl/Tk - see https://wiki.tcl-lang.org/page/mousewheel
|
||||
bind $w <MouseWheel> "winset $w; focus $w; if {%D < 0} {zoom 0.9} else {zoom 1.1}; break"
|
||||
bind $w <Button-4> {event generate [focus] <MouseWheel> -delta 120}
|
||||
bind $w <Button-5> {event generate [focus] <MouseWheel> -delta -120}
|
||||
|
||||
if {$::tcl_platform(os) == "Darwin"} {
|
||||
# Mac command-key emulates mouse 2 (doesn't work?)
|
||||
bind $w <Option-ButtonPress-1> "winset $w; focus $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
|
||||
# Mac command-key emulates mouse 3
|
||||
bind $w <Mod2-ButtonPress-1> "winset $w; focus $w; zoom 2.0; break"
|
||||
}
|
||||
}
|
||||
|
||||
bind $w <ButtonRelease> "winset $w; dm idle; break"
|
||||
bind $w <Motion> "winset $w; if {\[dm type\] == \"tkswrast\"} {dm motion %x %y}; break"
|
||||
bind $w <KeyRelease-Control_L> "winset $w; dm idle; break"
|
||||
bind $w <KeyRelease-Control_R> "winset $w; dm idle; break"
|
||||
bind $w <KeyRelease-Shift_L> "winset $w; dm idle; break"
|
||||
bind $w <KeyRelease-Shift_R> "winset $w; dm idle; break"
|
||||
bind $w <KeyRelease-Alt_L> "winset $w; dm idle; break"
|
||||
bind $w <KeyRelease-Alt_R> "winset $w; dm idle; break"
|
||||
|
||||
if ![catch {adc draw} result] {
|
||||
set adcflag $result
|
||||
} else {
|
||||
set adcflag 0
|
||||
}
|
||||
|
||||
if {$adcflag == "1" && $transform == "a"} {
|
||||
bind $w <Shift-ButtonPress-1> "winset $w; dm adc t %x %y; \
|
||||
shift_grip_hints $w \"Translate ADC\"; break"
|
||||
bind $w <Shift-ButtonPress-2> "winset $w; dm adc t %x %y; \
|
||||
shift_grip_hints $w \"Translate ADC\"; break"
|
||||
bind $w <Shift-ButtonPress-3> "winset $w; dm adc d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
|
||||
bind $w <Control-ButtonPress-1> "winset $w; dm adc 1 %x %y; \
|
||||
shift_grip_hints $w \"Rotate Angle 1\"; break"
|
||||
bind $w <Control-ButtonPress-2> "winset $w; dm adc 2 %x %y; \
|
||||
shift_grip_hints $w \"Rotate Angle 2\"; break"
|
||||
bind $w <Control-ButtonPress-3> "winset $w; dm adc d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
|
||||
bind $w <Shift-Control-ButtonPress-1> "winset $w; dm adc d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
bind $w <Shift-Control-ButtonPress-2> "winset $w; dm adc d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
bind $w <Shift-Control-ButtonPress-3> "winset $w; dm adc d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
|
||||
#constrained adc defaults
|
||||
bind $w <Alt-Shift-ButtonPress-1> "winset $w; dm con a x %x %y; \
|
||||
shift_grip_hints $w \"X Translate ADC\"; break"
|
||||
bind $w <Alt-Shift-ButtonPress-2> "winset $w; dm con a y %x %y; \
|
||||
shift_grip_hints $w \"Y Translate ADC\"; break"
|
||||
bind $w <Alt-Shift-ButtonPress-3> "winset $w; dm con a d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
|
||||
bind $w <Alt-Control-ButtonPress-1> "winset $w; dm con a 1 %x %y; \
|
||||
shift_grip_hints $w \"Rotate Angle 1\"; break"
|
||||
bind $w <Alt-Control-ButtonPress-2> "winset $w; dm con a 2 %x %y; \
|
||||
shift_grip_hints $w \"Rotate Angle 2\"; break"
|
||||
bind $w <Alt-Control-ButtonPress-3> "winset $w; dm con a d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
|
||||
bind $w <Alt-Shift-Control-ButtonPress-1> "winset $w; dm con a d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
bind $w <Alt-Shift-Control-ButtonPress-2> "winset $w; dm con a d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
bind $w <Alt-Shift-Control-ButtonPress-3> "winset $w; dm con a d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
} else {
|
||||
bind $w <Shift-ButtonPress-1> "winset $w; dm am t %x %y; \
|
||||
shift_grip_hints $w Translate; break"
|
||||
bind $w <Shift-ButtonPress-2> "winset $w; dm am t %x %y; \
|
||||
shift_grip_hints $w Translate; break"
|
||||
bind $w <Shift-ButtonPress-3> "winset $w; dm am t %x %y; \
|
||||
shift_grip_hints $w Translate; break"
|
||||
|
||||
bind $w <Control-ButtonPress-1> "winset $w; dm am r %x %y; \
|
||||
shift_grip_hints $w Rotate; break"
|
||||
bind $w <Control-ButtonPress-2> "winset $w; dm am r %x %y; \
|
||||
shift_grip_hints $w Rotate; break"
|
||||
bind $w <Control-ButtonPress-3> "winset $w; dm am r %x %y; \
|
||||
shift_grip_hints $w Rotate; break"
|
||||
|
||||
bind $w <Shift-Control-ButtonPress-1> "winset $w; dm am s %x %y; \
|
||||
shift_grip_hints $w Scale/Zoom; break"
|
||||
bind $w <Shift-Control-ButtonPress-2> "winset $w; dm am s %x %y; \
|
||||
shift_grip_hints $w Scale/Zoom; break"
|
||||
bind $w <Shift-Control-ButtonPress-3> "winset $w; dm am s %x %y; \
|
||||
shift_grip_hints $w Scale/Zoom; break"
|
||||
|
||||
#constrained defaults
|
||||
bind $w <Alt-Shift-ButtonPress-1> "winset $w; dm con t x %x %y; \
|
||||
shift_grip_hints $w \"X Translation\"; break"
|
||||
bind $w <Alt-Shift-ButtonPress-2> "winset $w; dm con t y %x %y; \
|
||||
shift_grip_hints $w \"Y Translation\"; break"
|
||||
bind $w <Alt-Shift-ButtonPress-3> "winset $w; dm con t z %x %y; \
|
||||
shift_grip_hints $w \"Z Translation\"; break"
|
||||
|
||||
bind $w <Alt-Control-ButtonPress-1> "winset $w; dm con r x %x %y; \
|
||||
shift_grip_hints $w \"X Rotation\"; break"
|
||||
bind $w <Alt-Control-ButtonPress-2> "winset $w; dm con r y %x %y; \
|
||||
shift_grip_hints $w \"Y Rotation\"; break"
|
||||
bind $w <Alt-Control-ButtonPress-3> "winset $w; dm con r z %x %y; \
|
||||
shift_grip_hints $w \"Z Rotation\"; break"
|
||||
|
||||
bind $w <Alt-Shift-Control-ButtonPress-1> "winset $w; dm con s x %x %y; \
|
||||
scale_shift_grip_hints $w X; break"
|
||||
bind $w <Alt-Shift-Control-ButtonPress-2> "winset $w; dm con s y %x %y; \
|
||||
scale_shift_grip_hints $w Y; break"
|
||||
bind $w <Alt-Shift-Control-ButtonPress-3> "winset $w; dm con s z %x %y; \
|
||||
scale_shift_grip_hints $w Z; break"
|
||||
}
|
||||
}
|
||||
|
||||
proc shift_grip_hints { w hint } {
|
||||
global mged_display
|
||||
global mged_gui
|
||||
global win_to_id
|
||||
|
||||
if ![info exists win_to_id($w)] {
|
||||
return
|
||||
}
|
||||
|
||||
set id $win_to_id($w)
|
||||
set mged_gui($id,illum_label) $hint
|
||||
}
|
||||
|
||||
proc scale_shift_grip_hints { w axis } {
|
||||
if {[status state] == "OBJ EDIT"} {
|
||||
shift_grip_hints $w "$axis Scale"
|
||||
} else {
|
||||
shift_grip_hints $w "Scale/Zoom"
|
||||
}
|
||||
}
|
||||
|
||||
proc update_gui { w vname val } {
|
||||
global mged_players
|
||||
global mged_gui
|
||||
@@ -87,14 +482,6 @@ proc update_gui { w vname val } {
|
||||
}
|
||||
}
|
||||
|
||||
# Dummy stubs to prevent legacy scripts from failing if they try to invoke them
|
||||
proc default_key_bindings { w } {}
|
||||
proc default_mouse_bindings { w } {}
|
||||
proc forward_key_bindings { w } {}
|
||||
proc toggle_forward_key_bindings { w } {}
|
||||
proc set_forward_keys { w val } {}
|
||||
proc shift_grip_hints { w hint } {}
|
||||
proc scale_shift_grip_hints { w axis } {}
|
||||
|
||||
# Local Variables:
|
||||
# mode: Tcl
|
||||
|
||||
@@ -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