Compare commits

..
6 Commits
Author SHA1 Message Date
Clifford Yapp 1a7ea7976f backend consistency work 2026-09-18 18:02:31 +00:00
Clifford Yapp ec4347e7d3 Start prototyping a system for defining bindings in a toolkit agnostic way. 2026-09-18 17:53:25 +00:00
Clifford Yapp 21e034ee34 Rename so we don't end up conflicting with an MSVC header 2026-09-18 16:13:27 +00:00
Clifford Yapp 8f81f27d92 librt: correct winding for explicit DSP B-C cell cuts
The explicit ULlr permutation inverted both terrain triangle normals.
The cell intersection state machine uses normal direction to classify
entry and exit, so top-to-side rays could disappear.

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

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

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

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

Add optional collection of ray identifiers, paired entry and exit
hit points, normals, and partition thickness. This gives
reconstruction callers structured source evidence while preserving
lightweight area and volume estimation. Link librt to the thread
target and extend the Crofton tests to verify that offset calls
exactly reproduce the uninterrupted stream.
2026-09-18 14:48:05 +00:00
Clifford Yapp 17601916f4 Use static diagonal transformation in IGES test 2026-09-18 14:02:22 +00:00
42 changed files with 2119 additions and 4379 deletions
+164
View File
@@ -0,0 +1,164 @@
/* 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
*/
+3
View File
@@ -444,6 +444,9 @@ 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 */
+46
View File
@@ -227,6 +227,32 @@ struct rt_crofton_params {
double time_ms; /**< wall-clock time budget (ms); 0 = disabled */
};
/** One solid partition observed by a Crofton ray. */
struct rt_crofton_segment {
point_t in_point;
vect_t in_normal;
point_t out_point;
vect_t out_normal;
fastf_t thickness;
size_t ray_id;
};
/**
* Structured samples and estimates produced by rt_crofton_collect().
* Release @p segments with rt_crofton_result_free().
*/
struct rt_crofton_result {
struct rt_crofton_segment *segments;
size_t segment_count;
size_t ray_count;
size_t crossing_count;
double surface_area;
double volume;
};
/** Initialize an rt_crofton_result before first use. */
#define RT_CROFTON_RESULT_INIT { NULL, 0, 0, 0, 0.0, 0.0 }
/**
* Run the Cauchy-Crofton ray-sampling estimator on an already-prepared
@@ -337,6 +363,26 @@ struct rt_crofton_params {
*/
RT_EXPORT extern int rt_crofton_shoot(double *out_surf_area, double *out_volume, point_t *out_aabb_min, point_t *out_aabb_max, point_t out_obb[8], point_t **out_points, size_t *out_point_count, struct rt_i *rtip, const struct rt_crofton_params *params, const fastf_t *bbox_min, const fastf_t *bbox_max);
/**
* Run Crofton sampling while preserving oriented entry/exit pairs.
*
* @p result must be initialized with RT_CROFTON_RESULT_INIT. On success it
* owns the returned segment array. @p rtip must already be prepared.
* @p ray_offset skips that many samples in the deterministic sequence and
* labels the returned rays starting at that value. This lets successive
* calls use disjoint validation streams.
*/
RT_EXPORT extern int rt_crofton_collect(struct rt_crofton_result *result,
struct rt_i *rtip,
const struct rt_crofton_params *params,
size_t ray_offset,
const fastf_t *bbox_min,
const fastf_t *bbox_max);
/** Release storage owned by a structured Crofton result. */
RT_EXPORT extern void rt_crofton_result_free(
struct rt_crofton_result *result);
/**
* Cauchy-Crofton surface-area and/or volume estimator for a primitive.
+2 -4
View File
@@ -17,10 +17,8 @@ brlcad_addexec(asc-nmg nmg/asc-nmg.c "libwdb;${M_LIBRARY}" FOLDER Conv)
brlcad_addexec(bot_dump bot_dump.c "libged;librt" FOLDER Conv)
verbose_add_subdirectory("src/conv" comgeom)
set(
comgeom-g_old_SRCS
comgeom-g_SRCS
comgeom/cvt.c
comgeom/f2a.c
comgeom/mat.c
@@ -29,7 +27,7 @@ set(
comgeom/solid.c
comgeom/tools.c
)
brlcad_addexec(comgeom-g_old "${comgeom-g_old_SRCS}" "libwdb;libbn;libbu;${M_LIBRARY}" FOLDER Conv)
brlcad_addexec(comgeom-g "${comgeom-g_SRCS}" "libwdb;libbn;libbu;${M_LIBRARY}" FOLDER Conv)
brlcad_addexec(conv-vg2g conv-vg2g.c "libbu" FOLDER Conv)
-28
View File
@@ -1,28 +0,0 @@
set(comgeom_srcs
ComGeomConverter.cpp
ComGeomReader.cpp
RegionManager.cpp
SolidParser.cpp
main.cpp
)
brlcad_addexec(comgeom-g "${comgeom_srcs}" "libwdb;librt;libnmg;libbu" FOLDER Conv)
target_include_directories(comgeom-g BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
cmakefiles(
CMakeLists.txt
COMGEOM_V1.txt
COMGEOM_versions.txt
ComGeomConverter.h
ComGeomReader.h
RegionManager.h
SolidParser.h
bot_to_arbn_pipeline_thoughts.txt
)
# Local Variables:
# tab-width: 8
# mode: cmake
# indent-tabs-mode: t
# End:
# ex: shiftwidth=2 tabstop=8
-498
View File
@@ -1,498 +0,0 @@
COM-GEOM v1 FORMAT DEFINITION FOR THE GIFT CODE
Based on BRL 1975 Manuals (Vol. I: BRL Report 1802; Vol. II: ARBRL-TR-02189)
---------------------------------------------------------------------
SECTION 0. GENERAL PRINCIPLES
1. Coordinate System
- Right-handed Cartesian system (X,Y,Z).
- Origin is user-defined (e.g., turret datum intersection for a tank).
- All dimensional values (coordinates, lengths, radii) use a single unit family
declared on the Title Card (columns 79-80): IN, FT, CM, M, or MM (default
IN).
2. Data Organization (Target Description)
A COM-GEOM data set consists of (in order):
1) Title Card
2) Target Specification Card
3) Solid Table (one or more cards per solid)
4) Region Table (one or more cards per region; ends with "-1" sentinel card)
5) (Optional) Region RPP Table
6) Region Identification Table
7) Program Option / Control Cards (for GIFT processing and output runs)
3. Numeric Formats
Unless otherwise noted:
- Integers are right- or left-adjusted in their column ranges.
- Floating point fields: F10.5 (classic) or F10.n where width 10 columns,
decimal point explicit.
- Blank where a "+" intersection operator is implied.
4. Combination Logic (Region Construction)
Given solids S1, S2 ...
- Intersection: adjacency (implicit "+"). Example: 1 2 3 means S1 intersect S2
intersect S3.
- Subtraction: prefix "-" before a solid number: ... -7 means subtract volume
of S7 from accumulated set.
- Union: keyword OR splits disjoint boolean branches: BranchA OR BranchB.
- Dummy region: region card with only region number and no solids (assigned
special identification code 111).
- Recommended practice (per BRL guidance): Avoid OR on Region cards; instead
assign identical identification (Item) codes to multiple simple regions.
5. Air Space and Items
Each region is either:
- Item (component) region: has item code (e.g., armor plate, wheel).
- Air region: has air space code (e.g., crew compartment air).
Rules:
- Item regions must not overlap (beyond TOL).
- Air regions may overlap other air regions with same air code.
- Air regions must NOT overlap air regions with different air codes (beyond
TOL).
- Air regions may overlap item regions (they represent enclosing void).
6. Tolerances (Control Card)
- Overlap tolerance TOL: maximum allowable spatial overlap distance between
regions before flagged as error.
- Line-of-sight gap tolerance TOLLOS: maximum axial gap along a ray ignored
when assembling ray intersection chains.
7. End Sentinel & Codes
- Void Space Code: numeric code inserted where no component.
- Phantom Item Code: code assigned to "phantom armor" or shielding constructs
generated during checking.
- End-of-ray Signal Code: code marking termination of component stack for a ray.
8. Storage Model
MASTER-ASTER equivalence array holds all processed geometry. Size NDQ must be
large enough; formula given (Section 11 of Vol II) reproduced below (see
Section 7).
---------------------------------------------------------------------
SECTION 1. TITLE CARD
Columns (1-80):
1-60: Free text title (target name/description).
61-78: (Usually blank or ancillary description).
79-80: Units code (IN, FT, CM, M, MM). If blank defaults to IN.
Example:
SAMPLE LIGHT ARMORED CAR WITH DRIVER IN
---------------------------------------------------------------------
SECTION 2. TARGET SPECIFICATION CARD
Format: (Counts of solids / regions / surrounding RPPs)
Columns (fixed interpretation from manuals):
1-10 Number of Solids (NSOLID)
11-20 Number of Regions (NREGION, expected or max)
21-30 Number of Surrounding RPPs (if used) (NRPP) or MAX Regions (context dependent)
31-40 MAX pointers or storage (implementation field) - historically used internally
Remaining columns may be reserved or blank.
If a dedicated second card exists (variant implementations) it repeats extended capacity fields.
Example:
20 20 1
---------------------------------------------------------------------
SECTION 3. SOLID TABLE
Each solid has a unique consecutive integer ID starting at 1.
GENERAL CARD FORMAT (all solids):
Columns:
1-3 Solid Number (integer)
4-6 Solid Symbol (3 chars): RPP BOX RAW ARB ARS ELL SPH RCC REC TRC TEC TOR
7 Variant / Vertex Count / Option flag (for ARB, ARS, ELL1) else blank
11-70 Numeric fields (F10.5 blocks)
71-80 Comment (recommended: name or purpose)
3.1 Rectangular Parallelepiped (RPP)
No explicit vertex; define bounding box aligned to global axes.
Fields (F10.5 each):
XMIN, XMAX, YMIN, YMAX, ZMIN, ZMAX.
Example:
6 RPP 20.0000 23.0000 -10.0000 10.0000 28.0000 46.0000 BODY
3.2 BOX (Arbitrarily oriented)
Vertex V and three mutually perpendicular edge vectors H, W, D.
Fields:
Vx Vy Vz Hx Hy Hz Wx Wy Wz Dx Dy Dz
Example:
1 BOX 21.5000 37.0000 0.0000 0.0000 45.0000 11.0000 40.0000 0.0000 0.0000 11.0000 0.0000 0.0000 STEERING WHEEL
3.3 RAW (Right Angle Wedge)
Vertex V; two perpendicular legs H,W in wedge plane; depth D.
Fields:
Vx Vy Vz Hx Hy Hz Wx Wy Wz Dx Dy Dz
Example:
12 RAW -75.0000 -36.0000 12.0000 60.0000 12.0000 0.0000 60.0000 12.0000 0.0000 -60.0000 12.0000 0.0000 FENDER
3.4 ARB (Convex Polyhedron) - ARB4 / ARB5 / ARB6 / ARB7 / ARB8
Column 7 = number of vertices (4-8).
Card sequence: Enough cards to list all vertices (each vertex: X Y Z).
One vertex triple per F10.5 block; continue across cards.
Face set is internally inferred by GIFT according to vertex count pattern (see manual).
Example ARB5 (two cards if needed):
3 ARB5 33.5000 37.0000 12.0000 46.0000 -75.0000 -75.0000 20.0000 50.0000 0.0000 ...
(continuation)
3.5 ARS (Triangular Surfaced Polyhedron)
Column 7: M (number of curves)
Column 11-20: N (points per curve)
Then M x N vertex points laid out curve by curve (each curve on one or more cards).
Repeated vertices allowed; triangles formed between successive curves (see Vol I Fig. 13).
Example:
11 ARS7 (points...) (comments)
3.6 ELL / ELL1 (Ellipsoid of Revolution)
Two input modes:
(a) ELL: Foci F1, F2 and major axis length L.
Fields: F1x F1y F1z F2x F2y F2z L
(b) ELL1 (Column 7 = 1): Center V, semi-major axis vector A, radius R of equatorial circle.
Fields: Vx Vy Vz Ax Ay Az R
Example:
14 ELL1 24.0000 52.0000 0.0000 35.0000 0.0000 20.0000 7.5000 DRIVER CHEST
3.7 SPH (Sphere)
Center V and radius R.
Fields: Vx Vy Vz R
Example:
15 SPH 45.0000 -30.0000 -30.0000 7.5000 HEAD
3.8 RCC (Right Circular Cylinder)
Center of one circular base (vertex V), height vector H, radius R.
Fields: Vx Vy Vz Hx Hy Hz R
Example:
16 RCC -30.0000 -70.0000 -30.0000 48.0000 24.0000 0.0000 13.0000 LEG
3.9 REC (Right Elliptical Cylinder)
Center V, height vector H, semi-major axis vector A, semi-minor axis vector B (both in base plane).
Fields: Vx Vy Vz Hx Hy Hz Ax Ay Az Bx By Bz
Example:
17 REC -30.0000 -30.0000 -30.0000 48.0000 24.0000 0.0000 13.0000 13.0000 0.0000 5.0000 0.0000 0.0000 ARM
3.10 TRC (Truncated Circular Cone)
Base center V, height vector H, base radius R1, top radius R2.
Fields: Vx Vy Vz Hx Hy Hz R1 R2
Example:
18 TRC -30.0000 -30.0000 -70.0000 48.0000 24.0000 0.0000 13.0000 9.0000 TORSO LOWER
3.11 TEC (Truncated Elliptic Cone)
Base center V, height vector H, base semi-major A, base semi-minor B, ratio P (BaseEllipse / TopEllipse).
Fields: Vx Vy Vz Hx Hy Hz Ax Ay Az Bx By Bz P
Example:
19 TEC -30.0000 -70.0000 0.0000 48.0000 24.0000 0.0000 24.0000 0.0000 20.0000 12.0000 0.0000 10.0000 1.5000
3.12 TOR (Torus)
Center V, normal vector N (to torus plane), major radius R1 (centerline), tube radius R2.
Fields: Vx Vy Vz Nx Ny Nz R1 R2
Example:
20 TOR 21.5000 37.0000 0.0000 0.0000 45.0000 11.0000 40.0000 11.0000 STEERING RIM
---------------------------------------------------------------------
SECTION 4. REGION TABLE
Card Format (per region):
Columns:
1-2 Blank (recommended)
3-5 Region Number (integer, consecutive starting at 1)
6 Blank
7-8 "OR" if union logic is used (introductory OR for region; optional but required if any OR appears later)
9-69 Sequence of signed solid references (2-3 digit solid numbers with optional leading minus sign; blanks imply "+" intersection). Space or "OR" may appear to break into disjoint branches.
70-80 Comment (e.g., semantic label, region definition subset index)
Grammar rules:
- Intersection: adjacency (implicit +).
- Subtraction: a leading "-" before the solid number.
- Union: OR separating disjoint boolean expressions.
- End of Region Table: card with region number -1 in columns 4-5 (" -1" Card).
Example (two-card region expression 1 + 2 - 3 OR 4 + 5 - 6):
Card 1:
___111 OR 1 2 -3 4
Card 2:
___111 5 -6
Dummy region (no solids):
___012 (columns 9-69 blank) (Region 12 dummy)
Sentinel:
____-1
Best Practice:
Prefer multiple simple non-OR regions sharing a common identification code (see Section 5) over complex OR expressions.
---------------------------------------------------------------------
SECTION 5. REGION IDENTIFICATION TABLE
Each card assigns either an Item Code (component) or Air Space Code to a region.
Format:
Columns:
1-2 Blank
3-5 Region Number
6-10 Blank or separator
11-15 Item or Air Space Code (integer)
16-69 Optional additional codes or reserved blanks
70-80 Comment (verbal description + region formula)
Special Codes:
- 01 Exterior air / Surrounding void (example usage; manuals show 02,03,05 for interior categories).
- 02 Crew compartment air
- 03 Passenger compartment air
- 05 Engine compartment air
- 09 (DO NOT USE) reserved
- 111 Dummy region code
- 501 Track region (may be remapped to 502 for track edge by GIFT processing)
- 999 Forbidden as a component code
Item Code Ranges (per Table 3 of manual):
1- 99 Personnel & miscellaneous interior
100-199 Armor & structural
200-299 Powertrain / engine group
300-399 Suspension / drive
400-499 Mobility auxiliaries
500-599 Track / running gear (501 special)
600-699 Armament
700-799 Fire control
800-899 Communications / electronics
900-998 Ammunition
If a region is air: its code is an air space code; otherwise an item code.
Example:
__001 40 STEERING WHEEL 1-2
__002 100 STEERING SHAFT 2
__012 111 DUMMY
__019 02 INTERIOR AIR 19
__020 02 INTERIOR AIR 20
---------------------------------------------------------------------
SECTION 6. REGION RPP TABLE (OPTIONAL)
Purpose: Override automatically computed bounding boxes (per region).
Card Format:
Columns:
1-10 Region Number
11-20 XMIN
21-30 XMAX
31-40 YMIN
41-50 YMAX
51-60 ZMIN
61-70 ZMAX
71-80 Comment
Omit dummy regions (those with identification code 111).
If absent for a region, GIFT computes bounding RPP automatically.
Example:
1 20.0000 23.0000 -10.0000 10.0000 28.0000 46.0000
2 15.0000 21.5000 -11.0000 11.0000 28.0000 46.0000
---------------------------------------------------------------------
SECTION 7. STORAGE FORMULA (MASTER-ASTER SIZE NDQ)
Minimum NDQ (memory words) must satisfy largest of:
1) LDES - 1
2) 6 * (NSOLID) + LREGON - 1
3) K * (NREGION) + LRIN - 1 (K = 5 CDC, 8 UNIVAC, 11 IBM)
Plus if alphanumeric region descriptions stored:
NDQ >= LDES + (K - 1) * NREGION - 1
Detailed formula from Volume II (for precise sizing):
TOTAL STORAGE =
4 * (#Solids)
+ 7 * (#RCC)
+12 * (#REC)
+ 8 * (#TRC)
+15 * (#TEC)
+ 4 * (#SPH)
+12 * (#ELL/ELL1)
+11 * (#TOR)
+ 6 * (#RPP)
+15 * (#BOX)
+12 * (#RAW)
+ (ARB Storage)
+ (Non-convex ARS Storage)
+ (Convex ARS Storage)
+14 * (#Regions)
+ (#Region Descriptors)
+ 6
+ (Alphanumeric Storage optional)
ARB Storage per ARB:
4 * (#faces) + 1
Non-convex ARS:
88 + 4 * (L + 2 * N * M)
where L = #points read directly
M = #curves
N = #points per curve
Convex ARS:
4 * (#non-degenerate faces) + 1
Region Descriptors:
Count of non-zero entries in columns 9-69 of Region Table.
Alphanumeric storage per region:
CDC=4, UNIVAC=7, IBM=10 words.
---------------------------------------------------------------------
SECTION 8. CONTROL (PROGRAM OPTION) CARD (GIFT EXECUTION)
Columns (classic Control Card from Vol II):
1 Use processed geometry from binary Unit 4 (0=no, non-zero=yes)
2 Write processed geometry to binary Unit 4 (0=no, non-zero=yes)
3-5 FORTRAN unit number for raw COM-GEOM input (0 or 5 = Unit 5)
6 Print MASTER-ASTER array (debug) if non-zero
7 Print Ordered Region Identification Table if non-zero
8 Print Ordered Region RPP Equivalent Table if non-zero
9 Print Solid RPP Equivalent Table if non-zero
10 Suppress printing of Solid/Region tables if non-zero (No Print)
11-30 Reserved / (legacy fields)
31-40 Overlap tolerance TOL (F10)
41-50 Line-of-sight gap tolerance TOLLOS (F10)
51-55 Void Space Code (integer; default 1 if 0)
56-60 Phantom Item Code (default 111 if 0)
61-65 End-of-ray Signal Code (default 9 if 0)
66-80 Comments
Example:
0 1 5 0 1 1 0 0.010000 0.010000 1 111 9 RUN A
---------------------------------------------------------------------
SECTION 9. VALIDATION & RULES SUMMARY
1) Solid Numbers: Consecutive starting at 1. No gaps.
2) Region Numbers: Consecutive starting at 1; end sentinel: region -1 card.
3) Dummy Regions: Allowed; must carry identification code 111; no solids.
4) Item Regions: Must not overlap beyond TOL.
5) Air Regions: May overlap same air code; not different air codes beyond TOL.
6) Negative Descriptors: Subtraction only applies to immediately preceding accumulated set in branch.
7) OR Branches: Disjoint boolean unions; minimized use recommended.
8) Tolerances: Set to reflect modeling precision; typical TOL=TOLLOS=0.01 in same units.
9) Special Codes: Avoid 999, 09, ensure proper use of 111, 501/502.
---------------------------------------------------------------------
SECTION 10. MINIMAL WORKED EXAMPLE
Title:
SAMPLE LIGHT VEHICLE WITH DRIVER IN
Target Specification:
20 20 1
Solid (excerpt):
1 BOX 21.5000 37.0000 0.0000 0.0000 45.0000 11.0000 40.0000 0.0000 0.0000 11.0000 0.0000 0.0000 STEERING WHEEL
2 TOR 21.5000 37.0000 0.0000 0.0000 45.0000 11.0000 40.0000 11.0000 RIM
3 SPH 33.5000 37.0000 12.0000 7.5000 HEAD
...
20 RCC -30.0000 -70.0000 -30.0000 48.0000 24.0000 0.0000 13.0000 LEG
Region Table (excerpt):
1 1 2 (STEERING WHEEL ASSEMBLY: BOX intersect TOR)
2 3 (HEAD)
3 4 -5 6 (ARM: Intersection of solids 4 & 6 minus 5)
...
12 (Dummy)
...
End: -1
Region Identification:
1 40 STEERING WHEEL 1 2
2 100 STEERING SHAFT 3
3 100 ARM (UPPER) 4 -5 6
12 111 DUMMY
19 02 INTERIOR AIR
20 02 INTERIOR AIR
Region RPP (optional):
1 20.0 23.0 -10.0 10.0 28.0 46.0
2 32.0 37.5 -11.0 11.0 28.0 46.0
...
Control Card:
0 1 5 0 1 0 0 0 0.010000 0.010000 1 111 9 STD RUN
---------------------------------------------------------------------
SECTION 11. COMMON PITFALLS AND RECOMMENDATIONS
1) Overlapping Items: Resolve by subtracting one from the other or adjusting tolerance; large overlaps are modeling errors, not tolerance issues.
2) Excessive OR Usage: Replace with multiple regions sharing identification codes to simplify ray tracing and checking.
3) Insufficient NDQ: Use storage formula; double NDQ if "NO MORE ROOM" messages appear after >75% of Solid Table processed.
4) Dummy Regions: Keep for numbering alignment only; always code 111.
5) Precision: If coordinate acquisition is from drawings with limited precision, set TOL/TOLLOS to at least the drawing resolution.
6) ARS Conversions: For legacy MAGIC/SHOTGEN conversions using ARS, verify point ordering and duplication to preserve intended triangular mesh.
7) TEC Ratio P: Ensure P > 1; if P < 1 code will treat cone as inverted (swap top/base); better to input consistent orientation.
---------------------------------------------------------------------
SECTION 12. DIFFERENCES VS. OTHER ERA GEOMETRY FORMATS
MAGIC / SHOTGEN vs COM-GEOM v1:
- COM-GEOM v1 uses boolean combinatorial solids with explicit card columns.
- Emphasizes minimal OR, encourages identification grouping.
- Tolerances (TOL/TOLLOS) user-adjustable on Control Card (MAGIC earlier versions required source edits).
- Storage formulas explicitly published; user responsible for NDQ sizing (contrast with more opaque internal allocation in other codes).
---------------------------------------------------------------------
SECTION 13. QUICK REFERENCE TABLES
13.1 Solid Symbols and Required Fields
Symbol | Type | Fields (Order)
RPP | Axis-aligned box | XMIN XMAX YMIN YMAX ZMIN ZMAX
BOX | Oriented box | Vx Vy Vz Hx Hy Hz Wx Wy Wz Dx Dy Dz
RAW | Right wedge | Vx Vy Vz Hx Hy Hz Wx Wy Wz Dx Dy Dz
ARB4-8 | Convex polyhedron | Vertex triples (count in col 7)
ARS | Triangulated polyhedron | M, N then M x N vertex triples
ELL | Ellipsoid (foci) | F1x F1y F1z F2x F2y F2z L
ELL1 | Ellipsoid (axis) | Vx Vy Vz Ax Ay Az R (col 7=1)
SPH | Sphere | Vx Vy Vz R
RCC | Right circular cylinder | Vx Vy Vz Hx Hy Hz R
REC | Right elliptical cylinder | Vx Vy Vz Hx Hy Hz Ax Ay Az Bx By Bz
TRC | Truncated circular cone | Vx Vy Vz Hx Hy Hz R1 R2
TEC | Truncated elliptic cone | Vx Vy Vz Hx Hy Hz Ax Ay Az Bx By Bz P
TOR | Torus | Vx Vy Vz Nx Ny Nz R1 R2
13.2 Special Identification Codes
111 Dummy region
501 Track (may become 502)
01,02,03,05 Air categories (examples)
999 Forbidden component code
09 Forbidden air code
---------------------------------------------------------------------
SECTION 14. EXTENSIBILITY NOTES
To add new solid types (historical practice for later internal variants):
- Reserve a new 3-char symbol in columns 4-6.
- Define fixed F10.5 field sequence and document vertex or vector semantics.
- Update MASTER-ASTER storage calculator with new per-solid word count.
- Maintain backward compatibility by keeping existing card column positions.
---------------------------------------------------------------------
SECTION 15. VALIDATION CHECKLIST
Before running GIFT:
[ ] Title card unit code set.
[ ] NSOLID equals number of solid definitions.
[ ] NREGION equals number of region definitions (excluding -1 sentinel).
[ ] Solid numbers consecutive from 1.
[ ] Region numbers consecutive from 1; -1 end card present.
[ ] Region Identification codes valid (no 999, no 09).
[ ] Dummy regions have 111 code and no solids.
[ ] NDQ sized using formula (or rule of thumb 45*NSOLID + margin).
[ ] TOL/TOLLOS appropriate for measurement precision.
[ ] Void / Phantom / End-of-ray codes non-zero or rely on defaults (1,111,9).
[ ] Optional Region RPP Table consistent (no dummy region entries).
[ ] No unintended overlaps (test with CHECK option).
[ ] Use CHECK / TESTG GIFT options for geometry validation before production runs.
---------------------------------------------------------------------
End of COM-GEOM v1 ASCII specification.
-350
View File
@@ -1,350 +0,0 @@
# COMGEOM Versions (1967–present)
This is a survey of the COMGEOM geometry input language across its lifecycle,
from the earliest MAGI/MAGIC card decks through the stable v1 and the tokenized
v4/v5 formats. It highlights what changed between versions, when those changes
occurred, and provides practical guidance for recognizing and migrating old
decks.
Quick summary: v0 (pre‑v1) was the early MAGIC card style with implicit
intersections and “OR” unions; v1 formalized strict punched‑card columns; v2/v3
were transitional internal experiments (partial free‑field parsing,
parentheses, generalized cones); v4 is the fully tokenized, parenthesized
grammar; v5 added attribute/metadata lines atop v4.
---
## Timeline
- 1967–1974: Pre‑formal MAGIC/MAGI “v0”
- 12 primitive shapes; implicit intersection (adjacency), `OR` for unions; no parentheses
- Dual ellipsoid input forms; 5‑card ARB; optional TESTG/VOLUM/AREA sections
- Region IDs (3‑digit components / 2‑digit spaces) already in place
- 1974–1976: COMGEOM v1 (formal punched‑card format)
- Strict column positions; fixed parameter counts per primitive
- Region table layout (cards 16A/16B) and `-1` terminator card
- Error taxonomy; RPP enclosure required
- 1977–1981: Transitional v2 → v3 (internal, not publicly specified)
- v2: Column‑first with free‑field fallback; early generalized cones; explicit `+` tokens begin to appear
- v3: Free‑field first; limited parentheses; explicit `+ - UNION/OR`; auto bounding boxes begin to appear
- 1982–1984: COMGEOM v4 (stable token grammar)
- Whitespace/tokenized parsing; parentheses; decoupled geometry vs. region structure
- 1985+: COMGEOM v5 (v4 + attributes)
- Attribute/metadata lines: material, aircode, color, region parameters; same boolean grammar as v4
Modern translators (e.g., `comgeom-g`) typically support v1, v4, and v5. Other
versions would require manual adaptation.
---
## Summary of Noteworthy Changes
|------------|---------|--------------------------------------|------------------------------------------------|
| Era | Version | Context | Noteworthy Changes |
|------------|---------|--------------------------------------|------------------------------------------------|
| 1967–1970 | v0 | Early MAGI/MAGIC on CDC/BRL | 12 primitives; adjacency = intersection; |
| | | | `-` subtract; `OR` unions; no parentheses; |
| | | | TEC permits oblique height vectors |
|------------|---------|--------------------------------------|------------------------------------------------|
| 1970–1974 | v0 | MAGIC manuals | Canonical 5‑card ARB; dual ellipsoid input |
| | | | (foci vs center form); optional |
| | | | TESTG/VOLUM/AREA cards; strict(er) carding |
|------------|---------|--------------------------------------|------------------------------------------------|
| 1974–1976 | v1 | GIFT integration; CDC/FORTRAN | Formal fixed‑columns per primitive; |
| | | | region table cards (16A/16B); `-1` end marker; |
| | | | RPP enclosure required |
|------------|---------|--------------------------------------|------------------------------------------------|
| 1977–1979 | v2 | Internal expansion | Column parsing w/ free‑field fallback; |
| | | | explicit `+` tokens; early generalized cones; |
| | | | torus parameter ordering fixes |
|------------|---------|--------------------------------------|------------------------------------------------|
| 1979–1981 | v3 | Move to machine‑independent parsing | Whitespace tokenization; shallow parentheses; |
| | | | explicit `+ - UNION/OR`; auto RPP; |
| | | | early ARB face validation |
|------------|---------|--------------------------------------|------------------------------------------------|
| 1982–1984 | v4 | Consolidation into BRL‑CAD pipeline | Fully tokenized whitespace grammar; |
| | | | stable parentheses; clean geometry/structure |
| | | | separation |
|------------|---------|--------------------------------------|------------------------------------------------|
| 1985+ | v5 | Attribute/metadata enrichment | Adds attribute lines (material, aircode, |
| | | | color, region params); same v4 |
| | | | geometry/boolean grammar |
|------------|---------|--------------------------------------|------------------------------------------------|
Provenance: v0 details are drawn from the 1967 MAGI report and the MAGIC JTCG
User Manual (61 JTCG/ME‑71‑7‑1). v1 is documented in BRL Report 1802 and
ARBRL‑TR‑02189. v2/v3 are reconstructed from tool comments and translator
heuristics. v4/v5 are reflected in BRL‑CAD-era translators and user guides.
---
## Primitive set by version
The early “MAGIC twelve” primitives were established by 1970 and remained the
backbone across versions:
- RPP (axis‑aligned box), BOX (oriented box), SPH, RCC (right circular
cylinder), REC (right elliptical cylinder), TRC (truncated right angle cone),
TEC (truncated elliptic cone; height vector need not be perpendicular to base),
ELL (ellipsoid of revolution), RAW/WED (right angle wedge), ARB (arbitrary
convex polyhedron), ARS (arbitrary curved surface), TOR (torus)
| Primitive | v0 (MAGIC) | v1 | v2/v3 (notes) | v4 | v5 |
|-----------|------------|----|------------------------------------|----|----|
| RPP | ✓ | ✓ | Auto‑derive allowed (v3) | ✓ | ✓ |
| BOX | ✓ | ✓ | | ✓ | ✓ |
| SPH | ✓ | ✓ | | ✓ | ✓ |
| RCC | ✓ | ✓ | Param order normalized (v3) | ✓ | ✓ |
| REC | ✓ | ✓ | | ✓ | ✓ |
| TRC | ✓ | ✓ | Distinct from TEC clarified (v3) | ✓ | ✓ |
| TEC | ✓ | ✓ | Pre‑TGC experiments | ✓ | ✓ |
| ELL | ✓ (dual) | ✓ | Dual input retained | ✓ | ✓ |
| RAW/WED | ✓ | ✓ | | ✓ | ✓ |
| ARB | ✓ (5‑card) | ✓ | Face validation begins (v3) | ✓ | ✓ |
| ARS | ✓ | ✓ | Same semantics | ✓ | ✓ |
| TOR | ✓ | ✓ | Minor<Major enforcement tightened | ✓ | ✓ |
Notes:
- ARB in v0/v1 is the “MAGIC 5‑card” convex polyhedron: 8 vertices then 6 faces
by ordinal indices; BRL‑CAD later canonicalizes to `arb8`.
- Ellipsoid dual input (foci+major length or center+axis+equatorial radius) is
an early quirk that persists; translators normalize internally.
- TEC (truncated elliptic cone) allows a non‑perpendicular height vector as
early as v0; generalized cone (TGC) is a later BRL‑CAD primitive outside
COMGEOM proper.
---
## Boolean and region semantics across versions
### v0 (pre‑v1)
- Intersection: implied by adjacency of body references (`Region = 1 2` means
inside both 1 and 2)
- Subtraction: leading minus (`-3` means outside body 3)
- Union: `OR` between clauses (`Region = (1 -2) OR (4 -5)`)
- No parentheses: evaluation is effectively left‑to‑right by clause; `OR` lumps
subregions
- “Buttressing” rule: where two bodies share a surface, each region must negate
the other body to avoid ambiguous boundaries (e.g., `+2 -3` vs `+3 -2`)
Example (v0 style):
```
200 = 2 -3
300 = 3 -2
```
### v1
- Same operators, strict fixed columns:
- Region number cols 1–5, optional `OR` marker (IA(1)), then up to 9
operator+body tokens per card
- Continuation cards permitted; `-1` in cols 1–5 terminates region table
- Parentheses still not part of the grammar; grouping via `OR` card positions
### v2 / v3 (transitional)
- v2: Column parsing with free‑field fallback; explicit `+` tokens increasingly
required in ambiguous decks
- v3: Free‑field tokenization; limited parentheses; `UNION`/`U` recognized
synonyms for `OR`
- Automatic bounding boxes if RPP omitted (conventional runs)
### v4 / v5
- Full token grammar; parentheses supported; explicit `+ - OR UNION` with
predictable precedence
- Geometry declarations cleanly separated from boolean structure; comments
supported
Comparative snippets:
- v0/v1 region equation (fixed columns):
```
001 +2 -3
002 +3 -2
-1
```
- v4/v5 region expression (tokenized):
```
region 001: +2 -3
region 002: +3 -2
```
- v4/v5 with grouping:
```
region 010: ( +2 -3 ) OR ( +4 -5 )
```
---
## Card / field structure changes
### v0 (MAGIC card decks)
- Mixed adherence to columns
- Multi‑card primitives:
- BOX: Vx Vy Vz / Ax Ay Az / Bx By Bz / Cx Cy Cz (2 cards)
- RCC/REC/TRC/RAW: 2 cards; TEC: 3 cards; ARB: 5 cards; ARS: 2 +
M*ceil((N+1)/2) cards
- Region identification card per region: region number + component code or
space code + description
### v1
- Formal fixed‑column definitions per primitive; field counts validated
- RPP mandatory enclosure
- Region table formatting codified; `-1` terminator
- Error messages categorized (fatal vs non‑fatal)
### v2/v3 (reconstructed)
- Free‑field parsing introduced to accept large/overflow coordinates
- Whole‑line comments (`#` or `!`) accepted in some parsers (internal)
- Early “DUMMY” region recognition; improved error diagnostics
### v4/v5
- Whitespace/token parsing only; columns ignored
- v5 adds attribute/metadata lines (see next section)
---
## Attributes and metadata
- v0/v1: Region identification via numeric codes only (3‑digit component;
2‑digit space). Materials/colors external or implicit.
- v5: Attribute lines augment region definitions:
- `material=Steel`, `aircode=02`, `color=0/255/0`, `los=...`, `density=...`
- Name=value or token style, parsed separately from geometry/boolean
---
## Recognizing versions in the wild (cheat sheet)
- Likely v0 (MAGIC):
- Cards referencing TESTG/VOLUM/AREA sections
- Primitive blocks matching MAGIC manual’s 2/3/5‑card counts and ordering
- Region tables with `-1` terminator but no parentheses anywhere; `OR`
appears literally
- Ellipsoid blocks with foci pair + major length
- v1:
- Strict columns; values neatly aligned; region table with IA(1) `OR` markers
- RPP enclosure present and required
- v2/v3 (transitional):
- Mixture of column and free‑field lines; occasional parentheses; `UNION`
token present
- Comments starting `#` or `!` in legacy sources; generalized cone parameters
pre‑TGC
- v4:
- Whitespace tokens; parentheses; clean separation of bodies from boolean
structure; no attribute lines
- v5:
- Same as v4 plus attribute/metadata lines (e.g., `material=`, `aircode=`,
`color=`)
---
## Migration guidance
1. Inventory and validate
- Count per‑primitive parameters; flag lines with wrong cardinality
- Verify region table completeness: every point belongs to exactly one
region; buttressing negations present
2. Convert v0 → v1
- Pad to fixed columns; split multi‑card primitives correctly
- Keep dual ellipsoid inputs; normalize internally if supported
3. Convert v1 → v4/v5
- Translate region boolean expressions to explicit tokens with parentheses
- Add attributes (v5): material, aircode (01 external, 02 crew, 05 engine,
09 terminus), region properties
4. Validate by ray tracing
- Reproduce small sample angles from manual output (cell listings,
obliquities, LOS/normal thickness) to confirm equivalence
Pitfalls:
- Ellipsoid dual form mapping; ensure axis vector direction/length matches
major axis semantics
- TEC/TRC confusion in transitional decks; verify height vector
perpendicularity (TEC: may be oblique)
- ARB face index blocks: ensure 6 faces present; repeat a face for 4/5‑face
forms (per v1 storage) or convert to `arb8`
---
## Examples
### v0 style (MAGIC region)
```
15 +15 -17 -14 -16
16 +15 +16 -14 -17 -21 -22
-1
```
### v1 fixed columns (same semantics)
```
00015 +15 -17 -14 -16
00016 +15 +16 -14 -17 -21 -22
-1
```
### v4/v5 tokenized with grouping
```
region 15: (+15 -17 -14 -16)
region 16: ((+15 +16 -14 -17) OR (-21 -22)) # if logically intended
```
### v5 attributes
```
region 15: (+15 -17 -14 -16)
material=Steel
aircode=02
color=128/128/128
```
---
## Region identification codes (early and stable)
Components (3 digits):
- 001–099 Internal components
- 100–199 Armor
- 200–299 Fuel (often reused in later targets)
- 300–399 Misc external
- 400–499 Gun components
- 500–599 Track suspension (501 reserved for track; edge hits become 502)
- 600–699 Wheel suspension
- 700–799 Power train
- 800–899 Miscellaneous
- 900–998 Unused
- 999 Ground/soil
Spaces (2 digits):
- 01 External air
- 02 Crew compartment air
- 05 Engine compartment air
- 09 No further target
---
## References
- MAGI (1967): Early geometric description technique for vulnerability analysis
(MAGI‑6701)
- MAGIC User Manual (JTCG/ME 61 JTCG/ME‑71‑7‑1; ‑7‑2‑1; ‑7‑2‑2): Combinatorial
geometry, body/region cards, optional QA routines, sample problems
- BRL Report 1802; ARBRL‑TR‑02189: Formal COMGEOM v1 card specifications
- BRL‑CAD documentation: Primitive mappings (e.g., `arb8`, TGC) and later
pipeline translators
---
## Appendix: Field counts per primitive (v0/v1 reference)
- RPP: 6 scalars (Xmin Xmax Ymin Ymax Zmin Zmax)
- BOX: 12 scalars (Vx Vy Vz Ax Ay Az Bx By Bz Cx Cy Cz) over 2 cards
- SPH: 4 scalars (Cx Cy Cz R)
- RCC: 7 scalars over 2 cards (Vx Vy Vz Hx Hy Hz R)
- REC: 12 scalars over 2 cards (Vx Vy Vz Hx Hy Hz R1x R1y R1z R2x R2y R2z)
- TRC: 8 scalars over 2 cards (Vx Vy Vz Hx Hy Hz R1 R2)
- ELL: (foci Fl(3) F2(3) + L) OR (center V(3) + A(3) + equatorial R)
- RAW/WED: 12 scalars over 2 cards (V(3) + leg A(3) + leg B(3) + width C(3); A and B interchangeable)
- ARB: 5 cards: vertices 1–2 on card A; pairs (3,4), (5,6), (7,8) on cards B; faces (6 x 4 ordinals) on card C
- TEC: 13 scalars over 3 cards (V(3) H(3) A(3) B(3) P)
- ARS: 2 header integers (M curves, N points) + M * ceil((N+1)/2) cards of point pairs
-186
View File
@@ -1,186 +0,0 @@
#include "ComGeomConverter.h"
#include <cstdio>
#include <cstring>
#include "ComGeomReader.h"
#include "SolidParser.h"
#include "RegionManager.h"
ComGeomConverter::ComGeomConverter(const std::string &in,
const std::string &out,
const ConverterOptions &opts)
: m_inPath(in), m_outPath(out), m_opts(opts)
{}
ComGeomConverter::~ComGeomConverter() {
if (m_in) fclose(m_in);
if (m_out) wdb_close(m_out);
}
bool ComGeomConverter::openInput() {
if (!bu_file_exists(m_inPath.c_str(), NULL)) {
std::perror(m_inPath.c_str());
return false;
}
m_in = fopen(m_inPath.c_str(), "rb");
if (!m_in) {
std::perror(m_inPath.c_str());
return false;
}
return true;
}
bool ComGeomConverter::openOutput() {
if (bu_file_exists(m_outPath.c_str(), NULL))
bu_log("Warning: appending to existing database\n");
m_out = wdb_fopen(m_outPath.c_str());
if (!m_out) {
std::perror(m_outPath.c_str());
return false;
}
return true;
}
bool ComGeomConverter::writeDbHeader(const std::string &title, const std::string &units) {
if (mk_conversion(units.c_str()) < 0) {
std::printf("WARNING: unknown units '%s', using inches\n", units.c_str());
(void)mk_conversion("in");
}
if (mk_id_units(m_out, title.c_str(), units.c_str()) < 0) {
std::printf("Unable to write database ID, units='%s'\n", units.c_str());
return false;
}
return true;
}
int ComGeomConverter::run() {
if (m_opts.version != 0 && m_opts.version != 1 && m_opts.version != 4 && m_opts.version != 5) {
std::fprintf(stderr,"version %d not supported\n", m_opts.version);
return 1;
}
if (!openInput() || !openOutput()) return 1;
std::printf("Reading version %d COMGEOM file\n", m_opts.version);
/* Title & units */
std::string title, units;
{
ComGeomReader r(m_in);
auto l = r.next("title card");
if (!l) { std::printf("Empty input file: no title record\n"); return 10; }
std::string_view sv(l->s);
if (m_opts.version == 0) {
title = ComGeomReader::rstrip(sv);
ComGeomReader::trimLeadingSpacesInplace(title);
units = "in";
} else if (m_opts.version == 1) {
title = ComGeomReader::rstrip(sv);
ComGeomReader::trimLeadingSpacesInplace(title);
units = "in";
} else {
units.assign(sv.substr(0,2));
title.assign(sv.substr(3));
ComGeomReader::trimLeadingSpacesInplace(title);
title = ComGeomReader::rstrip(title);
units = ComGeomReader::rstrip(units);
ComGeomReader::toLowerInplace(units);
}
std::printf("Title: %s\nUnits: %s\n", title.c_str(), units.c_str());
}
if (!writeDbHeader(title, units)) return 1;
int solTotal=0, regTotal=0;
{
ComGeomReader r(m_in);
if (m_opts.version == 0) {
// Attempt single read first
auto l = r.next("v0 constraints candidate");
if (l) {
std::string_view sv(l->s);
solTotal = ComGeomReader::fieldInt(sv, 30, 10);
regTotal = ComGeomReader::fieldInt(sv, 40, 10);
}
if (solTotal <= 0 || regTotal <= 0) {
// Scan up to 50 lines to find plausible NBODY/NRMAX pair
solTotal = regTotal = 0;
for (int scan=0; scan<50; ++scan) {
auto cand = r.next("scan constraints");
if (!cand) break;
std::string_view sv2(cand->s);
int nb = ComGeomReader::fieldInt(sv2, 30, 10);
int nr = ComGeomReader::fieldInt(sv2, 40, 10);
if (nb > 0 && nr > 0) {
solTotal = nb;
regTotal = nr;
break;
}
}
if (solTotal == 0 || regTotal == 0) {
std::printf("Warning: v0 NBODY/NRMAX not found, dynamic sizing will be used\n");
solTotal = 0; // dynamic
regTotal = 0;
}
}
} else if (m_opts.version == 1) {
solTotal = regTotal = 9999;
} else {
auto l = r.next("control card");
if (!l) { std::printf("No control card .... STOP\n"); return 10; }
std::string_view sv(l->s);
if (m_opts.version == 4) {
solTotal = ComGeomReader::fieldInt(sv, 0, 20);
regTotal = ComGeomReader::fieldInt(sv, 20, 10);
} else {
solTotal = ComGeomReader::fieldInt(sv, 0, 5);
regTotal = ComGeomReader::fieldInt(sv, 5, 5);
}
}
if (m_opts.verbose)
std::printf("Expecting %d solids, %d regions\n", solTotal, regTotal);
}
/* Solids */
{
if (m_opts.verbose) std::puts("Primitive table");
ComGeomReader r(m_in);
SolidParserOptions sopts{m_opts.version,m_opts.verbose,m_opts.suffix};
sopts.legacy_trunc = m_opts.legacy_truncate_names;
SolidParser sp(r, m_out, sopts);
sp.setSolidsExpected(solTotal);
while (true) {
auto res = sp.parseNext();
if (res.status == SolidParseResult::Eof) {
if (m_opts.verbose)
std::printf("\nprocessed %d of %d solids\n\n", sp.solidsProcessed(), solTotal);
if (solTotal > 0 && sp.solidsProcessed() < solTotal && m_opts.version > 1) {
std::printf("some solids are missing, aborting\n");
return 1;
}
break;
}
if (res.status == SolidParseResult::Error) {
std::fprintf(stderr,"error converting solid %d: %s\n",
sp.solidsProcessed(), res.message.c_str());
}
}
}
/* Regions */
{
if (m_opts.verbose) std::puts("\nRegion table");
ComGeomReader r(m_in);
RegionManagerOptions ropts;
ropts.version = m_opts.version;
ropts.verbose = m_opts.verbose;
ropts.suffix = m_opts.suffix;
ropts.regTotal = regTotal;
ropts.legacy_trunc = m_opts.legacy_truncate_names;
RegionManager rm(r, m_out, ropts);
if (!rm.parseRegions()) return 10;
if (m_opts.verbose) std::puts("\nGroups");
if (!rm.writeGroups()) return 10;
if (m_opts.verbose) std::puts("");
}
return 0;
}
-41
View File
@@ -1,41 +0,0 @@
#ifndef BRLCAD_COMGEOM_CONVERTER_H
#define BRLCAD_COMGEOM_CONVERTER_H
#include "common.h"
#include <string>
extern "C" {
#include "wdb.h"
#include "bu/file.h"
#include "bu/log.h"
#include "bu/app.h"
}
struct ConverterOptions {
int version = 5;
int verbose = 0;
std::string suffix;
bool legacy_truncate_names = false; // enable legacy suffix truncation (max 13 chars)
};
class ComGeomConverter {
public:
ComGeomConverter(const std::string &inputPath,
const std::string &outputPath,
const ConverterOptions &opts);
~ComGeomConverter();
int run();
private:
bool openInput();
bool openOutput();
bool writeDbHeader(const std::string &title, const std::string &units);
private:
std::string m_inPath, m_outPath;
ConverterOptions m_opts;
FILE *m_in = nullptr;
rt_wdb *m_out = nullptr;
};
#endif
-76
View File
@@ -1,76 +0,0 @@
#include "ComGeomReader.h"
#include <cstdlib>
#include <cctype>
#include <algorithm>
ComGeomReader::ComGeomReader(FILE *fp) : m_fp(fp) {}
ComGeomReader::~ComGeomReader() = default;
std::optional<ComGeomReader::Line> ComGeomReader::next(const char * /*title*/) {
if (!m_fp) return std::nullopt;
std::string s;
int c = 0;
while ((c = fgetc(m_fp)) != EOF) {
if (c == '\r') {
int nc = fgetc(m_fp);
if (nc != '\n' && nc != EOF) ungetc(nc, m_fp);
break;
}
if (c == '\n') break;
s.push_back(static_cast<char>(c));
}
if (c == EOF && s.empty())
return std::nullopt;
m_line++;
return Line{std::move(s), m_line};
}
int ComGeomReader::fieldInt(std::string_view sv, size_t start, size_t len) {
if (start >= sv.size()) return 0;
size_t end = std::min(sv.size(), start + len);
// Work on the field window only
size_t b = start, e = end;
// Trim leading whitespace
while (b < e && std::isspace(static_cast<unsigned char>(sv[b]))) b++;
// Trim trailing whitespace
while (e > b && std::isspace(static_cast<unsigned char>(sv[e-1]))) e--;
if (b >= e) return 0;
// Note that we preserve internal spaces to match the behavior of the
// original C code - "1 23" would be read as 1 rather than 123. Whether
// or not this is really desirable behavior is a question - i.e. whether
// there is a valid use case where a line would make the above separation
// with intent in the COMGEOM formats - but for now we match legacy code.
std::string token(sv.substr(b, e - b));
return std::atoi(token.c_str());
}
double ComGeomReader::fieldDouble(std::string_view sv, size_t start, size_t len) {
if (start >= sv.size()) return 0.0;
size_t end = std::min(sv.size(), start + len);
// Work on the field window only
size_t b = start, e = end;
// Trim leading whitespace
while (b < e && std::isspace(static_cast<unsigned char>(sv[b]))) b++;
// Trim trailing whitespace
while (e > b && std::isspace(static_cast<unsigned char>(sv[e-1]))) e--;
if (b >= e) return 0.0;
std::string token(sv.substr(b, e - b)); // preserve internal spaces to match legacy C
return std::atof(token.c_str());
}
std::string ComGeomReader::rstrip(std::string_view s) {
size_t end = s.size();
while (end > 0 && s[end-1] == ' ') end--;
return std::string(s.substr(0, end));
}
void ComGeomReader::toLowerInplace(std::string &s) {
for (auto &c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
void ComGeomReader::trimLeadingSpacesInplace(std::string &s) {
size_t i = 0;
while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) i++;
if (i) s.erase(0, i);
}
-38
View File
@@ -1,38 +0,0 @@
#ifndef BRLCAD_COMGEOM_READER_H
#define BRLCAD_COMGEOM_READER_H
#include "common.h"
#include <cstdio>
#include <string>
#include <optional>
#include <string_view>
class ComGeomReader {
public:
struct Line {
std::string s;
size_t number = 0;
std::string_view view() const { return s; }
};
explicit ComGeomReader(FILE *fp);
~ComGeomReader();
std::optional<Line> next(const char *title);
static int fieldInt(std::string_view sv, size_t start, size_t len);
static double fieldDouble(std::string_view sv, size_t start, size_t len);
static std::string rstrip(std::string_view s);
static void toLowerInplace(std::string &s);
static void trimLeadingSpacesInplace(std::string &s);
size_t lineNo() const { return m_line; }
private:
FILE *m_fp = nullptr;
size_t m_line = 0;
};
#endif
-424
View File
@@ -1,424 +0,0 @@
#include "RegionManager.h"
#include <cstdio>
#include <cstring>
#include <utility>
RegionManager::RegionManager(ComGeomReader &reader, rt_wdb *outfp, const RegionManagerOptions &opts)
: m_r(reader), m_out(outfp), m_opts(opts)
{
// For dynamic v0 decks (regTotal == 0) preallocate provisional capacity
size_t allocCount = static_cast<size_t>(m_opts.regTotal + 1);
if (m_opts.version == 0 && m_opts.regTotal == 0) {
allocCount = 4097; // provisional region cap
}
m_regionHeads.resize(allocCount);
for (auto &wm : m_regionHeads) {
WMEMBER_INIT(&wm);
}
groupInit();
}
RegionManager::~RegionManager() {
for (auto &wm : m_regionHeads) {
if (wm.wm_name) {
bu_free(wm.wm_name, "region wm_name");
wm.wm_name = nullptr;
}
}
for (int i = 0; i < m_groupCount; ++i) {
if (m_groups[i].head.wm_name) {
bu_free(m_groups[i].head.wm_name, "group wm_name");
m_groups[i].head.wm_name = nullptr;
}
}
}
std::string RegionManager::makeRegionName(int n) const {
std::string suf = m_opts.suffix;
if (m_opts.legacy_trunc && suf.size() > 13) suf.resize(13);
char buf[64];
std::snprintf(buf, sizeof(buf), "r%d%s", n, suf.c_str());
return std::string(buf);
}
void RegionManager::colPrint(const std::string &s) {
if (!m_opts.verbose) return;
std::fputs(s.c_str(), stdout);
m_colCur += s.size();
while (m_colCur < 78 && (m_colCur % 10)) {
putchar(' ');
++m_colCur;
}
if (m_colCur >= 78) {
std::fputc('\n', stdout);
m_colCur = 0;
}
}
/* ---------------- v0 (MAGIC) region table with OR semantics ---------------- */
bool RegionManager::parseRegionTableV0() {
auto first = m_r.next("v0 region card");
if (!first) { std::fprintf(stderr,"v0 region: EOF\n"); return false; }
std::string current = first->s;
int lastRegNum = 0;
while (true) {
int reg_num = ComGeomReader::fieldInt(current, 0, 5);
if (reg_num < 0) break;
if (reg_num == 0) {
auto nl = m_r.next("v0 region card");
if (!nl) break;
current = nl->s;
continue;
}
// resize if needed (dynamic)
if (reg_num >= static_cast<int>(m_regionHeads.size())) {
std::fprintf(stderr,"v0 region number %d exceeds provisional allocation\n", reg_num);
return false;
}
lastRegNum = reg_num;
wmember &head = m_regionHeads[static_cast<size_t>(reg_num)];
if (!head.wm_name) head.wm_name = bu_strdup(makeRegionName(reg_num).c_str());
struct Term { int body; bool subtract; };
std::vector<std::vector<Term>> clauses;
clauses.emplace_back();
auto parseLine = [&](const std::string &line) {
for (int i=0;i<9;++i) {
size_t slot = 6 + i*7;
if (slot + 7 > line.size()) break;
std::string field = line.substr(slot, 7);
std::string op = field.substr(0,2);
std::string numstr = field.substr(2);
ComGeomReader::trimLeadingSpacesInplace(op);
numstr = ComGeomReader::rstrip(numstr);
ComGeomReader::trimLeadingSpacesInplace(numstr);
if (numstr.empty()) continue;
int bnum = std::atoi(numstr.c_str());
if (bnum == 0) continue;
if (op == "OR" || op == "or") {
if (!clauses.back().empty()) clauses.emplace_back();
continue;
}
bool subtract = (bnum < 0);
if (subtract) bnum = -bnum;
clauses.back().push_back({bnum, subtract});
}
};
parseLine(current);
while (true) {
auto nextLine = m_r.next("v0 region card");
if (!nextLine) break;
int newRegTest = ComGeomReader::fieldInt(nextLine->s, 0, 5);
std::string trimmed = ComGeomReader::rstrip(nextLine->s);
std::string t2 = trimmed; ComGeomReader::trimLeadingSpacesInplace(t2);
if (t2 == "end") {
if (m_opts.regTotal == 0) m_opts.regTotal = reg_num;
break;
}
if (newRegTest < 0) { // negative sentinel
if (m_opts.regTotal == 0) m_opts.regTotal = reg_num;
current = nextLine->s;
break;
}
if (newRegTest > 0) { // new region starts
current = nextLine->s;
break;
}
parseLine(nextLine->s);
}
int clauseIdx = 0;
for (auto &cl : clauses) {
if (cl.empty()) continue;
wmember clauseHead; WMEMBER_INIT(&clauseHead);
char cname[64]; std::snprintf(cname, sizeof(cname), "r%d_c%d", reg_num, ++clauseIdx);
clauseHead.wm_name = bu_strdup(cname);
bool firstTerm = true;
for (auto &t : cl) {
std::string memberName = std::string("s") + std::to_string(t.body) + m_opts.suffix;
int op;
if (firstTerm) {
op = WMOP_UNION; // baseline
firstTerm = false;
} else {
op = t.subtract ? WMOP_SUBTRACT : WMOP_INTERSECT;
}
if (!mk_addmember(memberName.c_str(), &clauseHead.l, nullptr, op)) {
std::fprintf(stderr,"v0 mk_addmember failed clause %s member %s\n", cname, memberName.c_str());
}
}
mk_comb(m_out, clauseHead.wm_name, &clauseHead.l, 0,
"", "", nullptr, 0,0,0,0,0,0,0);
if (!mk_addmember(clauseHead.wm_name, &head.l, nullptr, WMOP_UNION)) {
std::fprintf(stderr,"v0 mk_addmember region %d clause %s\n", reg_num, clauseHead.wm_name);
}
}
if (m_opts.verbose) colPrint(head.wm_name);
int sentinelTest = ComGeomReader::fieldInt(current, 0, 5);
if (sentinelTest < 0) break;
}
if (m_opts.regTotal == 0) m_opts.regTotal = lastRegNum;
return true;
}
bool RegionManager::parseRegions() {
if (m_opts.version == 0) {
if (!parseRegionTableV0()) return false;
for (int i=1;i<=m_opts.regTotal && i < static_cast<int>(m_regionHeads.size());++i) registerRegion(i,0,0,0,0);
return true;
}
if (!parseRegionTable()) return false;
if (m_opts.version == 1) {
for (int i = 1; i <= m_opts.regTotal; ++i) {
registerRegion(i, 0, 0, 0, 0);
}
return true;
}
return parseIdentTable();
}
bool RegionManager::parseRegionTable() {
auto first = m_r.next("region card");
if (!first) {
std::fprintf(stderr, "getregion: EOF\n");
return false;
}
if (ComGeomReader::fieldInt(first->s, 0, 5) != 1) {
std::fprintf(stderr, "First region card not #1\n");
return false;
}
std::string current = first->s;
while (true) {
int reg_num = ComGeomReader::fieldInt(current, 0, 5);
if (reg_num < 0) {
return true;
}
if (reg_num > m_opts.regTotal) {
std::fprintf(stderr, "%d regions > claimed %d\n", reg_num, m_opts.regTotal);
return false;
}
wmember &head = m_regionHeads[static_cast<size_t>(reg_num)];
if (!head.wm_name) {
head.wm_name = bu_strdup(makeRegionName(reg_num).c_str());
}
while (true) {
size_t base = (m_opts.version == 1) ? 10 : 6;
for (int i = 0; i < 9; ++i) {
size_t off = base + i * 7;
if (off + 7 > current.size()) break;
char token[8] = {0};
int k = 0;
for (int j = 2; j < 7; ++j) {
if (off + j >= current.size()) break;
char c = current[off + j];
if (!std::isspace(static_cast<unsigned char>(c))) {
token[k++] = c;
}
}
if (k == 0) continue;
int inst_num = std::atoi(token);
if (inst_num == 0) continue;
int op = WMOP_UNION;
bool region_ref = false;
if (m_opts.version == 5) {
char flag = (off + 1 < current.size()) ? current[off + 1] : ' ';
if (flag == 'g' || flag == 'G') {
region_ref = true;
} else if (flag == 'R' || flag == 'r') {
op = WMOP_UNION;
} else {
if (inst_num < 0) {
op = WMOP_SUBTRACT;
inst_num = -inst_num;
} else {
op = WMOP_INTERSECT;
}
}
} else {
char flag = (off + 1 < current.size()) ? current[off + 1] : ' ';
if (flag == ' ') {
if (inst_num < 0) {
op = WMOP_SUBTRACT;
inst_num = -inst_num;
} else {
op = WMOP_INTERSECT;
}
} else {
op = WMOP_UNION;
}
}
std::string suf = m_opts.suffix;
if (m_opts.legacy_trunc && suf.size() > 13) suf.resize(13);
std::string memberName = region_ref
? makeRegionName(inst_num)
: (std::string("s") + std::to_string(inst_num) + suf);
if (!mk_addmember(memberName.c_str(), &head.l, nullptr, op)) {
std::fprintf(stderr, "mk_addmember failed for region %d member '%s'\n",
reg_num, memberName.c_str());
}
}
auto nextLine = m_r.next("region card");
if (!nextLine) {
std::fprintf(stderr, "getregion: EOF\n");
return false;
}
std::string trimmed = ComGeomReader::rstrip(nextLine->s);
ComGeomReader::trimLeadingSpacesInplace(trimmed);
if (trimmed == "end") {
m_opts.regTotal = reg_num;
return true;
}
int possible = ComGeomReader::fieldInt(nextLine->s, 0, 5);
if (possible != 0) {
current = nextLine->s;
break;
}
current = nextLine->s;
}
if (m_opts.verbose) colPrint(head.wm_name);
}
}
bool RegionManager::parseIdentTable() {
if (m_opts.version == 4) {
(void)m_r.next("blank card");
}
if (m_opts.verbose) std::puts("\nRegion ident table");
while (true) {
auto line = m_r.next("ident card");
if (!line) return true;
if (line->s.empty()) return true;
int reg_num, id, air, mat, los;
if (m_opts.version == 5) {
reg_num = ComGeomReader::fieldInt(line->s, 0, 5);
id = ComGeomReader::fieldInt(line->s, 5, 5);
air = ComGeomReader::fieldInt(line->s, 10, 5);
mat = ComGeomReader::fieldInt(line->s, 15, 5);
los = ComGeomReader::fieldInt(line->s, 20, 5);
} else {
reg_num = ComGeomReader::fieldInt(line->s, 0, 10);
id = ComGeomReader::fieldInt(line->s, 10, 10);
air = ComGeomReader::fieldInt(line->s, 20, 10);
mat = ComGeomReader::fieldInt(line->s, 74, 3);
los = ComGeomReader::fieldInt(line->s, 77, 3);
}
if (reg_num <= 0) return true;
registerRegion(reg_num, id, air, mat, los);
}
}
void RegionManager::registerRegion(int regNum, int id, int air, int mat, int los) {
if (regNum <= 0 || regNum >= static_cast<int>(m_regionHeads.size())) return;
wmember &head = m_regionHeads[static_cast<size_t>(regNum)];
if (BU_LIST_IS_EMPTY(&head.l)) {
if (m_opts.verbose && head.wm_name) {
colPrint(std::string("(") + head.wm_name + ")");
}
return;
}
int ret = mk_comb(m_out, head.wm_name, &head.l, 1,
"", "", nullptr,
id, air, mat, los, 0, 0, 1);
if (ret < 0) {
std::fprintf(stderr, "mk_comb failed for region %d (%s)\n", regNum, head.wm_name);
} else {
groupAdd(id, head.wm_name);
if (m_opts.verbose) colPrint(head.wm_name);
}
}
void RegionManager::groupInit() {
struct Def { const char *n; int lo; int hi; } defs[] = {
{"g00",0,0},{"g0",1,99},{"g1",100,199},{"g2",200,299},{"g3",300,399},
{"g4",400,499},{"g5",500,599},{"g6",600,699},{"g7",700,799},{"g8",800,899},
{"g9",900,999},{"g10",1000,1999},{"g11",2000,2999},{"g12",3000,3999},{"g13",4000,4999},
{"g14",5000,5999},{"g15",6000,6999},{"g16",7000,7999},{"g17",8000,8999},{"g18",9000,9999},
{"g19",10000,32767}
};
for (const auto &d : defs) {
if (m_groupCount >= kMaxGroups) {
std::fprintf(stderr, "Too many groups (limit %d)\n", kMaxGroups);
break;
}
groupRegister(d.n, d.lo, d.hi);
}
}
void RegionManager::groupRegister(const char *name, int lo, int hi) {
Group &g = m_groups[m_groupCount];
WMEMBER_INIT(&g.head);
std::string suf = m_opts.suffix;
if (m_opts.legacy_trunc && suf.size() > 13) suf.resize(13);
std::string full = std::string(name) + suf;
g.head.wm_name = bu_strdup(full.c_str());
g.lo = lo;
g.hi = hi;
g.used = false;
++m_groupCount;
}
void RegionManager::groupAdd(int identVal, const char *regionName) {
for (int i = m_groupCount - 1; i >= 0; --i) {
if (identVal < m_groups[i].lo || identVal > m_groups[i].hi) continue;
if (!mk_addmember(regionName, &m_groups[i].head.l, nullptr, WMOP_UNION)) {
std::fprintf(stderr, "mk_addmember failed adding region '%s' to group '%s'\n",
regionName, m_groups[i].head.wm_name);
} else {
m_groups[i].used = true;
}
return;
}
std::fprintf(stderr, "Unable to find group for ident %d (region %s)\n", identVal, regionName);
}
bool RegionManager::writeGroups() {
wmember allHead;
WMEMBER_INIT(&allHead);
allHead.wm_name = const_cast<char *>("all.g");
for (int i = 0; i < m_groupCount; ++i) {
Group &g = m_groups[i];
if (BU_LIST_IS_EMPTY(&g.head.l)) continue;
int ret = mk_comb(m_out, g.head.wm_name, &g.head.l, 0,
nullptr, nullptr, nullptr,
0, 0, 0, 0, 0, 0, 0);
if (ret < 0) {
std::fprintf(stderr, "mk_comb (group) failed for '%s'\n", g.head.wm_name);
continue;
}
if (!mk_addmember(g.head.wm_name, &allHead.l, nullptr, WMOP_UNION)) {
std::fprintf(stderr, "mk_addmember failed adding '%s' to all.g\n", g.head.wm_name);
}
if (m_opts.verbose) colPrint(g.head.wm_name);
}
int retAll = mk_comb(m_out, "all.g", &allHead.l, 0,
nullptr, nullptr, nullptr,
0, 0, 0, 0, 0, 0, 0);
if (retAll < 0) {
std::fprintf(stderr, "mk_comb failed for all.g\n");
return false;
}
return true;
}
-74
View File
@@ -1,74 +0,0 @@
#pragma once
#include "common.h"
#include <array>
#include <string>
#include <vector>
extern "C" {
#include "vmath.h"
#include "raytrace.h"
#include "wdb.h"
}
#include "ComGeomReader.h"
struct RegionManagerOptions {
int version = 5;
long verbose = 0;
std::string suffix;
int regTotal = 0;
bool legacy_trunc = false; // optional legacy suffix truncation (max 13 chars)
};
/*
* Modernized RegionManager:
* - Uses WMEMBER_INIT for region and group heads (matches wdb.h)
* - Avoids container reallocation for group heads (std::array)
* - Relies on mk_addmember for node allocation and mk_comb/mk_lfcomb
* to free membership lists (per wdb.h contract)
* - No manual traversal freeing of member nodes (use mk_freemembers only
* for temporary aggregation lists we explicitly build, e.g. "all.g" head)
* - Stable addresses: region heads stored in std::vector sized once,
* group heads in fixed array
*/
class RegionManager {
public:
RegionManager(ComGeomReader &reader, rt_wdb *outfp, const RegionManagerOptions &opts);
~RegionManager();
bool parseRegions(); // read region membership cards
bool writeGroups(); // emit group combinations + all.g
private:
std::string makeRegionName(int n) const;
void colPrint(const std::string &s);
bool parseRegionTable();
bool parseRegionTableV0();
bool parseIdentTable();
void registerRegion(int regNum, int id, int air, int mat, int los);
void groupInit();
void groupRegister(const char *name, int lo, int hi);
void groupAdd(int identVal, const char *regionName);
private:
ComGeomReader &m_r;
rt_wdb *m_out = nullptr;
RegionManagerOptions m_opts;
std::vector<wmember> m_regionHeads; // index by region number (1..regTotal)
size_t m_colCur = 0;
struct Group {
wmember head; // combination head (membership list)
int lo = 0;
int hi = 0;
bool used = false; // whether at least one region fell into range
};
static constexpr int kMaxGroups = 21;
std::array<Group, kMaxGroups> m_groups;
int m_groupCount = 0;
};
-830
View File
@@ -1,830 +0,0 @@
#include "SolidParser.h"
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
/* Helper: build min/max from 6 RPP values */
static inline void vset_minmax(fastf_t *min, fastf_t *max, const fastf_t *dd) {
VSET(min, dd[0], dd[2], dd[4]);
VSET(max, dd[1], dd[3], dd[5]);
}
SolidParser::SolidParser(ComGeomReader &reader, rt_wdb *outfp, const SolidParserOptions &opts)
: m_r(reader), m_out(outfp), m_opts(opts)
{}
std::string SolidParser::makeSolidName(int n) const {
std::string suf = m_opts.suffix;
if (m_opts.legacy_trunc && suf.size() > 13) suf.resize(13);
char buf[64] = {0};
std::snprintf(buf, sizeof(buf), "s%d%s", n, suf.c_str());
return std::string(buf);
}
void SolidParser::colPrint(const std::string &s) {
if (!m_opts.verbose) return;
std::fputs(s.c_str(), stdout);
m_colCur += s.size();
while (m_colCur < 78 && (m_colCur % 10)) {
putchar(' ');
++m_colCur;
}
if (m_colCur >= 78) {
std::fputc('\n', stdout);
m_colCur = 0;
}
}
bool SolidParser::parseSolidHeader(const std::string &line, int &solidNum,
std::string &typeLower, std::string &lineOutForData) {
std::string_view sv(line);
std::string solidType;
switch (m_opts.version) {
case 5:
solidNum = ComGeomReader::fieldInt(sv, 0, 5);
solidType = ComGeomReader::rstrip(sv.substr(5, 5));
break;
case 4:
solidNum = ComGeomReader::fieldInt(sv, 0, 3);
solidType = ComGeomReader::rstrip(sv.substr(3, 4));
break;
case 1:
solidNum = ComGeomReader::fieldInt(sv, 5, 4);
solidType = ComGeomReader::rstrip(sv.substr(2, 3));
break;
default:
return false;
}
ComGeomReader::toLowerInplace(solidType);
typeLower = solidType;
lineOutForData = line;
return true;
}
bool SolidParser::getSolData(std::vector<fastf_t> &data, int num, int solidNum,
bool /*firstCardAlreadyRead*/, std::string lineBuf) {
data.clear();
data.reserve(num);
int cd = 1;
std::string line = std::move(lineBuf);
while (num > 0) {
std::string_view sv(line);
if (cd != 1) {
if ((m_opts.version == 5 && sv.size() > 5 && sv[5] != ' ') ||
(m_opts.version == 4 && sv.size() > 3 && sv[3] != ' ')) {
std::fprintf(stderr, "solid %d (continuation) card %d non-blank\n", solidNum, cd);
return false;
}
}
int j = std::min(num, 6);
for (int i = 0; i < j; ++i) {
data.push_back(static_cast<fastf_t>(ComGeomReader::fieldDouble(sv, 10 + i*10, 10)));
}
num -= j;
if (num <= 0) break;
auto l = m_r.next("solid continuation card");
if (!l) {
std::fprintf(stderr, "too few cards for solid %d\n", solidNum);
return false;
}
line = l->s;
++cd;
}
return true;
}
bool SolidParser::getXSolData(std::vector<fastf_t> &data, int num, int solidNum) {
data.clear();
data.reserve(num);
int cd = 1;
while (num > 0) {
auto l = m_r.next("x solid card");
if (!l) {
std::fprintf(stderr, "too few cards for solid %d\n", solidNum);
return false;
}
std::string_view sv(l->s);
if (cd != 1) {
if ((m_opts.version == 5 && sv.size() > 5 && sv[5] != ' ') ||
(m_opts.version == 4 && sv.size() > 3 && sv[3] != ' ')) {
std::fprintf(stderr, "solid %d (continuation) card %d non-blank\n", solidNum, cd);
return false;
}
}
int j = std::min(num, 6);
for (int i = 0; i < j; ++i) {
data.push_back(static_cast<fastf_t>(ComGeomReader::fieldDouble(sv, 10 + i*10, 10)));
}
num -= j;
++cd;
}
return true;
}
bool SolidParser::isIntegerField(std::string_view sv, size_t start, size_t len, int &value) const {
if (start >= sv.size()) { value = 0; return false; }
size_t end = std::min(sv.size(), start + len);
size_t b = start;
while (b < end && std::isspace(static_cast<unsigned char>(sv[b]))) b++;
if (b >= end) { value = 0; return false; }
bool hasDigit=false; bool hasDot=false;
for (size_t i=b;i<end;++i) {
char c = sv[i];
if (std::isspace(static_cast<unsigned char>(c))) break;
if (c == '.') hasDot = true;
if (std::isdigit(static_cast<unsigned char>(c))) hasDigit = true;
else if (c=='-' || c=='+') continue;
else { value=0; return false; }
}
if (!hasDigit || hasDot) { value=0; return false; }
std::string token(sv.substr(b, end-b));
value = std::atoi(token.c_str());
return true;
}
bool SolidParser::isLikelyFaceCard(const std::string &line) const {
// Heuristic: first four 10-char fields each parse as small integers (1..8) & no decimal points.
int vals=0;
for (int f=0; f<4; ++f) {
int v=0;
if (!isIntegerField(line, 10 + f*10, 10, v)) return false;
if (v < -8 || v > 8 || v==0) return false;
vals++;
}
return (vals==4);
}
/* ---------------- Primitive Handlers ---------------- */
SolidParseResult SolidParser::handle_ars(const std::string &name, const std::string &firstLine) {
std::string_view sv(firstLine);
int ncurves = ComGeomReader::fieldInt(sv, 10, 10);
int pts_per_curve = ComGeomReader::fieldInt(sv, 20, 10);
if (ncurves <= 0 || pts_per_curve <= 0)
return {SolidParseResult::Error, "ars invalid counts"};
std::vector<std::vector<fastf_t>> curves(ncurves);
for (int i = 0; i < ncurves; ++i) {
curves[i].resize(static_cast<size_t>(pts_per_curve) * 3);
if (!getXSolData(curves[i], pts_per_curve * 3, m_solWork))
return {SolidParseResult::Error, "ars curve data read failure"};
}
std::vector<fastf_t*> ptrs(ncurves);
for (int i = 0; i < ncurves; ++i) ptrs[i] = curves[i].data();
int ret = mk_ars(m_out, name.c_str(), ncurves, pts_per_curve, ptrs.data());
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_ars failed" : ""};
}
SolidParseResult SolidParser::handle_rpp(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 6) return {SolidParseResult::Error, "rpp needs 6 values"};
fastf_t min[3], max[3];
vset_minmax(min, max, dd.data());
int ret = mk_rpp(m_out, name.c_str(), min, max);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_rpp failed" : ""};
}
SolidParseResult SolidParser::handle_box(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 12) return {SolidParseResult::Error, "box needs 12 values"};
point_t v[8];
const fastf_t *D0 = &dd[0], *D1=&dd[3], *D2=&dd[6], *D3=&dd[9];
VMOVE(v[0], D0);
VADD2(v[1], D0, D2);
VADD3(v[2], D0, D2, D1);
VADD2(v[3], D0, D1);
VADD2(v[4], D0, D3);
VADD3(v[5], D0, D3, D2);
VADD4(v[6], D0, D3, D2, D1);
VADD3(v[7], D0, D3, D1);
int ret = mk_arb8(m_out, name.c_str(), &v[0][0]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_arb8(box) failed" : ""};
}
SolidParseResult SolidParser::handle_raw_wed(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 12) return {SolidParseResult::Error, "raw/wed needs 12 values"};
point_t v[8];
const fastf_t *D0=&dd[0], *D1=&dd[3], *D2=&dd[6], *D3=&dd[9];
VMOVE(v[0], D0);
VADD2(v[1], D0, D2);
VMOVE(v[2], v[1]);
VADD2(v[3], D0, D1);
VADD2(v[4], D0, D3);
VADD3(v[5], D0, D3, D2);
VMOVE(v[6], v[5]);
VADD3(v[7], D0, D3, D1);
int ret = mk_arb8(m_out, name.c_str(), &v[0][0]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_arb8(raw/wed) failed" : ""};
}
SolidParseResult SolidParser::handle_rvw(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 7) return {SolidParseResult::Error, "rvw needs 7 values"};
vect_t V; VMOVE(V, &dd[0]);
double a2=dd[3], theta=dd[4], phi=dd[5], h2=dd[6];
double angle1 = (phi + theta - 90.0) * DEG2RAD;
double angle2 = (phi + theta) * DEG2RAD;
double a2theta = a2 * std::tan(theta * DEG2RAD);
vect_t a,b,c;
VSET(a, a2theta * std::cos(angle1), a2theta * std::sin(angle1), 0.0);
VSET(b, -a2 * std::cos(angle2), -a2 * std::sin(angle2), 0.0);
VSET(c, 0.0, 0.0, h2);
point_t vert[8];
VSUB2(vert[0], V, b);
VMOVE(vert[1], V);
VMOVE(vert[2], vert[1]);
VADD2(vert[3], vert[0], a);
VADD2(vert[4], vert[0], c);
VADD2(vert[5], vert[1], c);
VMOVE(vert[6], vert[5]);
VADD2(vert[7], vert[3], c);
int ret = mk_arb8(m_out, name.c_str(), &vert[0][0]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_arb8(rvw) failed" : ""};
}
SolidParseResult SolidParser::handle_arw(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 12) return {SolidParseResult::Error, "arw needs 12 values"};
point_t v[8];
const fastf_t *D0=&dd[0], *D1=&dd[3], *D2=&dd[6], *D3=&dd[9];
VMOVE(v[0], D0);
VADD2(v[1], D0, D2);
VADD3(v[2], D0, D2, D3);
VADD2(v[3], D0, D3);
VADD2(v[4], D0, D1);
VMOVE(v[5], v[4]);
VADD3(v[6], D0, D1, D3);
VMOVE(v[7], v[6]);
int ret = mk_arb8(m_out, name.c_str(), &v[0][0]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_arb8(arw) failed" : ""};
}
/* ---------- MAGIC v0 ARB generic handler ---------- */
SolidParseResult SolidParser::handle_arb_magic(const std::string &name, const std::string &firstLine) {
// Multi-card ARB parsing:
// Collect vertex triples until a face card is detected (integer ordinals 1..8).
std::vector<fastf_t> vertexDoubles;
std::string line = firstLine;
int maxVertexTriples = 8;
// Include data from first line if it contains numeric fields (after header)
auto consumeVertexCard = [&](const std::string &card) {
std::string_view sv(card);
for (int i=0;i<6;++i) { // up to 6 fields (i.e., 2 triples per card typical)
double val = ComGeomReader::fieldDouble(sv, 10 + i*10, 10);
// Heuristic: consider any numeric (including 0) as part of vertex stream until a face card appears
vertexDoubles.push_back(static_cast<fastf_t>(val));
if ((int)vertexDoubles.size()/3 >= maxVertexTriples) break;
}
};
// First card may have header only; attempt to parse numeric fields
consumeVertexCard(line);
// Read continuation cards until face card or limit reached
while ((int)vertexDoubles.size()/3 < maxVertexTriples) {
auto next = m_r.next("arb card");
if (!next) break;
if (isLikelyFaceCard(next->s)) {
line = next->s;
break;
}
consumeVertexCard(next->s);
}
int vertexCount = static_cast<int>(vertexDoubles.size()/3);
if (vertexCount < 4) return {SolidParseResult::Error, "arb v0 insufficient vertices"};
// Build point array
std::vector<fastf_t> verts(vertexCount*3);
std::copy(vertexDoubles.begin(), vertexDoubles.begin()+vertexCount*3, verts.begin());
// Read face cards (up to 6 faces)
std::vector<std::vector<int>> faces;
while (true) {
if (!isLikelyFaceCard(line)) {
auto peek = m_r.next("arb maybe face");
if (!peek) break;
line = peek->s;
if (!isLikelyFaceCard(line)) break;
}
std::string_view sv(line);
std::vector<int> f;
for (int i=0;i<4;++i) {
int v=0;
if (isIntegerField(sv, 10 + i*10, 10, v) && v != 0) {
f.push_back(v);
}
}
if (!f.empty()) faces.push_back(f);
auto next = m_r.next("arb face next");
if (!next) break;
if (!isLikelyFaceCard(next->s)) {
line = next->s; // pass to main dispatcher for next solid
break;
}
line = next->s;
if ((int)faces.size() >= 6) break;
}
// Validate face indices
for (const auto &f : faces) {
for (int idx : f) {
int absIdx = std::abs(idx);
if (absIdx < 1 || absIdx > vertexCount)
return {SolidParseResult::Error, "arb face index out of range"};
}
}
// Convert based on vertex count
SolidParseResult result;
if (vertexCount == 4) {
result = handle_arb4(name, verts);
} else if (vertexCount == 5) {
result = handle_arb5(name, verts);
} else if (vertexCount == 6) {
result = handle_arb6(name, verts);
} else if (vertexCount == 7) {
result = handle_arb7(name, verts);
} else if (vertexCount == 8) {
result = handle_arb8(name, verts);
} else {
return {SolidParseResult::Error, "arb v0 unsupported vertex count"};
}
if (result.status != SolidParseResult::Ok) {
// Fallback to ARBN if vertex-based conversion fails
std::fprintf(stderr, "arb v0 vertex conversion failed, attempting arbn fallback\n");
// Build plane equations from faces using indices (if enough faces)
if (faces.size() >= 4) {
// Simple plane extraction: pick triple for each face (first three indices)
std::vector<plane_t> eqn(faces.size());
for (size_t i=0;i<faces.size();++i) {
int a = std::abs(faces[i][0]) - 1;
int b = std::abs(faces[i][1]) - 1;
int c = std::abs(faces[i][2]) - 1;
if (a<0||b<0||c<0 || a>=vertexCount||b>=vertexCount||c>=vertexCount) continue;
plane_t pl;
point_t pa, pb, pc;
VMOVE(pa, &verts[a*3]);
VMOVE(pb, &verts[b*3]);
VMOVE(pc, &verts[c*3]);
struct bn_tol tol;
tol.magic = BN_TOL_MAGIC;
tol.dist = 0.0005;
tol.dist_sq = tol.dist * tol.dist;
tol.perp = 1e-6;
tol.para = 1 - tol.perp;
if (bg_make_plane_3pnts(pl, pa, pb, pc, &tol) == 0) {
VMOVE(eqn[i], pl);
}
}
int ret = mk_arbn(m_out, name.c_str(), (int)faces.size(), eqn.data());
if (ret < 0) return {SolidParseResult::Error, "arbn fallback failed"};
return {SolidParseResult::Ok, ""};
}
}
return result;
}
SolidParseResult SolidParser::handle_arb4(const std::string &name, const std::vector<fastf_t> &dd4) {
if (dd4.size() < 12) return {SolidParseResult::Error, "arb4 needs 12 values"};
int ret = mk_arb4(m_out, name.c_str(), dd4.data());
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_arb4 failed" : ""};
}
SolidParseResult SolidParser::handle_arb5(const std::string &name, const std::vector<fastf_t> &dd5) {
if (dd5.size() < 15) return {SolidParseResult::Error, "arb5 needs 15 values"};
std::vector<fastf_t> dd_full(24);
std::copy(dd5.begin(), dd5.begin()+15, dd_full.begin());
const fastf_t *v4 = &dd5[12];
for (int rep=5; rep<=7; ++rep) std::copy(v4, v4+3, dd_full.begin()+rep*3);
int ret = mk_arb8(m_out, name.c_str(), dd_full.data());
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_arb8(arb5) failed" : ""};
}
SolidParseResult SolidParser::handle_arb6(const std::string &name, const std::vector<fastf_t> &dd6) {
if (dd6.size() < 18) return {SolidParseResult::Error, "arb6 needs 18 values"};
std::vector<fastf_t> dd_full(24);
std::copy(dd6.begin(), dd6.begin()+18, dd_full.begin());
const fastf_t *D4=&dd6[12];
const fastf_t *D5=&dd6[15];
std::copy(D4, D4+3, dd_full.begin()+5*3); // D(5)=D(4)
std::copy(D5, D5+3, dd_full.begin()+6*3); // D(6)=original D(5)
std::copy(D5, D5+3, dd_full.begin()+7*3); // D(7)=original D(5)
int ret = mk_arb8(m_out, name.c_str(), dd_full.data());
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_arb8(arb6) failed" : ""};
}
SolidParseResult SolidParser::handle_arb7(const std::string &name, const std::vector<fastf_t> &dd7) {
if (dd7.size() < 21) return {SolidParseResult::Error, "arb7 needs 21 values"};
std::vector<fastf_t> dd_full(24);
std::copy(dd7.begin(), dd7.begin()+21, dd_full.begin());
const fastf_t *D4=&dd7[12];
std::copy(D4, D4+3, dd_full.begin()+7*3); // D(7)=D(4)
int ret = mk_arb8(m_out, name.c_str(), dd_full.data());
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_arb8(arb7) failed" : ""};
}
SolidParseResult SolidParser::handle_arb8(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 24) return {SolidParseResult::Error, "arb8 needs 24 values"};
int ret = mk_arb8(m_out, name.c_str(), dd.data());
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_arb8 failed" : ""};
}
SolidParseResult SolidParser::handle_rcc(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 7) return {SolidParseResult::Error, "rcc needs 7 values"};
int ret = mk_rcc(m_out, name.c_str(), dd.data(), dd.data()+3, dd[6]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_rcc failed" : ""};
}
SolidParseResult SolidParser::handle_rec(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 12) return {SolidParseResult::Error, "rec needs 12 values"};
int ret = mk_tgc(m_out, name.c_str(), dd.data(), dd.data()+3, dd.data()+6, dd.data()+9, dd.data()+6, dd.data()+9);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_tgc(rec) failed" : ""};
}
SolidParseResult SolidParser::handle_trc(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 8) return {SolidParseResult::Error, "trc needs 8 values"};
int ret = mk_trc_h(m_out, name.c_str(), dd.data(), dd.data()+3, dd[6], dd[7]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_trc_h failed" : ""};
}
SolidParseResult SolidParser::handle_tec(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 13) return {SolidParseResult::Error, "tec needs 13 values"};
double p = dd[12];
if (NEAR_ZERO(p, SMALL)) return {SolidParseResult::Error, "tec p cannot be zero"};
vect_t topA, topB;
double invp = 1.0/p;
VSCALE(topA, dd.data()+6, invp);
VSCALE(topB, dd.data()+9, invp);
int ret = mk_tgc(m_out, name.c_str(), dd.data(), dd.data()+3, dd.data()+6, dd.data()+9, topA, topB);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_tgc(tec) failed" : ""};
}
SolidParseResult SolidParser::handle_tgc(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 14) return {SolidParseResult::Error, "tgc needs 14 values"};
vect_t topA, topB;
double magBaseA = MAGNITUDE(dd.data()+6);
double magBaseB = MAGNITUDE(dd.data()+9);
if (magBaseA < SMALL || magBaseB < SMALL) return {SolidParseResult::Error,"tgc zero-length A or B"};
VSCALE(topA, dd.data()+6, dd[12]/magBaseA);
VSCALE(topB, dd.data()+9, dd[13]/magBaseB);
int ret = mk_tgc(m_out, name.c_str(), dd.data(), dd.data()+3, dd.data()+6, dd.data()+9, topA, topB);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_tgc failed" : ""};
}
SolidParseResult SolidParser::handle_sph(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 4) return {SolidParseResult::Error, "sph needs 4 values"};
int ret = mk_sph(m_out, name.c_str(), dd.data(), dd[3]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_sph failed" : ""};
}
SolidParseResult SolidParser::handle_wir(const std::string &name, const std::string &firstLine) {
int numpts = ComGeomReader::fieldInt(firstLine, 8, 2);
if (numpts <= 0) return {SolidParseResult::Error, "wir invalid point count"};
int total = numpts*3 + 1;
std::vector<fastf_t> raw;
if (!getSolData(raw, total, m_solWork, true, firstLine))
return {SolidParseResult::Error, "wir data read failure"};
if ((int)raw.size() < total) return {SolidParseResult::Error, "wir insufficient data"};
double radius = raw.back();
if (radius <= 0.0) return {SolidParseResult::Error, "wir invalid radius"};
double diameter = radius * 2.0;
struct bu_list head;
BU_LIST_INIT(&head);
for (int i=0;i<numpts;++i) {
struct wdb_pipe_pnt *ps;
BU_ALLOC(ps, struct wdb_pipe_pnt);
ps->l.magic = WDB_PIPESEG_MAGIC;
VMOVE(ps->pp_coord, &raw[i*3]);
ps->pp_id = 0;
ps->pp_od = diameter;
ps->pp_bendradius = diameter;
BU_LIST_INSERT(&head, &ps->l);
}
int ret = mk_pipe(m_out, name.c_str(), &head);
mk_pipe_free(&head);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_pipe failed" : ""};
}
SolidParseResult SolidParser::handle_rpc(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 10) return {SolidParseResult::Error, "rpc needs 10 values"};
int ret = mk_rpc(m_out, name.c_str(), dd.data(), dd.data()+3, dd.data()+6, dd[9]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_rpc failed" : ""};
}
SolidParseResult SolidParser::handle_rhc(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 11) return {SolidParseResult::Error, "rhc needs 11 values"};
int ret = mk_rhc(m_out, name.c_str(), dd.data(), dd.data()+3, dd.data()+6, dd[9], dd[10]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_rhc failed" : ""};
}
SolidParseResult SolidParser::handle_epa(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 11) return {SolidParseResult::Error, "epa needs 11 values"};
int ret = mk_epa(m_out, name.c_str(), dd.data(), dd.data()+3, dd.data()+6, dd[9], dd[10]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_epa failed" : ""};
}
SolidParseResult SolidParser::handle_ehy(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 12) return {SolidParseResult::Error, "ehy needs 12 values"};
int ret = mk_ehy(m_out, name.c_str(), dd.data(), dd.data()+3, dd.data()+6, dd[9], dd[10], dd[11]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_ehy failed" : ""};
}
SolidParseResult SolidParser::handle_eto(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 11) return {SolidParseResult::Error, "eto needs 11 values"};
int ret = mk_eto(m_out, name.c_str(), dd.data(), dd.data()+3, dd.data()+6, dd[9], dd[10]);
return {ret < 0 ? SolidParseResult::Error : SolidParseResult::Ok, ret < 0 ? "mk_eto failed" : ""};
}
SolidParseResult SolidParser::handle_v4_ell_transform(const std::string &name, const std::vector<fastf_t> &foci) {
if (foci.size() < 7) return {SolidParseResult::Error, "v4 ell needs 7 values"};
vect_t F1,F2; VMOVE(F1,&foci[0]); VMOVE(F2,&foci[3]);
double L = foci[6];
vect_t V; VADD2SCALE(V,F1,F2,0.5);
vect_t work; VSUB2(work,F2,F1);
double m1 = MAGNITUDE(work);
if (m1 < SMALL) return {SolidParseResult::Error,"degenerate ell"};
double a_scale = (L*0.5)/m1;
vect_t baseA; VSCALE(baseA, work, a_scale);
double r = std::sqrt(MAGSQ(baseA) - (m1*0.5)*(m1*0.5));
if (!(r>0.0)) return {SolidParseResult::Error,"invalid ell radius"};
vect_t Vp{V[0]+M_PI, V[1]+M_PI, V[2]+M_PI};
vect_t B; VCROSS(B,Vp,baseA); double mb=MAGNITUDE(B); if (mb<SMALL) return {SolidParseResult::Error,"degenerate B"};
VSCALE(B,B,r/mb);
vect_t C; VCROSS(C,baseA,B); double mc=MAGNITUDE(C); if (mc<SMALL) return {SolidParseResult::Error,"degenerate C"};
VSCALE(C,C,r/mc);
int ret = mk_ell(m_out, name.c_str(), V, baseA, B, C);
return {ret<0?SolidParseResult::Error:SolidParseResult::Ok, ret<0?"mk_ell failed":""};
}
SolidParseResult SolidParser::handle_ell(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 7) return {SolidParseResult::Error, "ell needs 7 values"};
vect_t V, A; VMOVE(V,&dd[0]); VMOVE(A,&dd[3]);
double r = dd[6]; if (r <= 0.0) return {SolidParseResult::Error,"invalid ell radius"};
vect_t Vp{V[0]+M_PI,V[1]+M_PI,V[2]+M_PI};
vect_t B; VCROSS(B,Vp,A); double mb=MAGNITUDE(B); if (mb<SMALL) return {SolidParseResult::Error,"degenerate B"};
VSCALE(B,B,r/mb);
vect_t C; VCROSS(C,A,B); double mc=MAGNITUDE(C); if (mc<SMALL) return {SolidParseResult::Error,"degenerate C"};
VSCALE(C,C,r/mc);
int ret = mk_ell(m_out, name.c_str(), V, A, B, C);
return {ret<0?SolidParseResult::Error:SolidParseResult::Ok, ret<0?"mk_ell failed":""};
}
SolidParseResult SolidParser::handle_ellg(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 12) return {SolidParseResult::Error, "ellg needs 12 values"};
int ret = mk_ell(m_out, name.c_str(), dd.data(), dd.data()+3, dd.data()+6, dd.data()+9);
return {ret<0?SolidParseResult::Error:SolidParseResult::Ok, ret<0?"mk_ell failed":""};
}
SolidParseResult SolidParser::handle_tor(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 8) return {SolidParseResult::Error, "tor needs 8 values"};
int ret = mk_tor(m_out, name.c_str(), dd.data(), dd.data()+3, dd[6], dd[7]);
return {ret<0?SolidParseResult::Error:SolidParseResult::Ok, ret<0?"mk_tor failed":""};
}
SolidParseResult SolidParser::handle_haf(const std::string &name, const std::vector<fastf_t> &dd) {
if (dd.size() < 4) return {SolidParseResult::Error, "haf needs 4 values"};
vect_t N; VMOVE(N,&dd[0]);
double magN = MAGNITUDE(N);
if (magN < SMALL) return {SolidParseResult::Error,"haf normal too small"};
int ret = mk_half(m_out, name.c_str(), N, -dd[3]);
return {ret<0?SolidParseResult::Error:SolidParseResult::Ok, ret<0?"mk_half failed":""};
}
SolidParseResult SolidParser::handle_arbn(const std::string &name, const std::string &firstLine) {
std::string_view sv(firstLine);
int npt = ComGeomReader::fieldInt(sv, 10 + 0*10, 10);
int npe = ComGeomReader::fieldInt(sv, 10 + 1*10, 10);
int neq = ComGeomReader::fieldInt(sv, 10 + 2*10, 10);
int nae = ComGeomReader::fieldInt(sv, 10 + 3*10, 10);
int nface = npe + neq + nae;
if (npt < 1) return {SolidParseResult::Error, "arbn requires >=1 point"};
if (nface < 1) return {SolidParseResult::Error, "arbn requires >=1 face"};
std::vector<plane_t> eqn(static_cast<size_t>(nface));
std::vector<int> used(static_cast<size_t>(nface), 0);
std::vector<fastf_t> input_points;
input_points.reserve(npt*3);
struct bn_tol tol;
tol.magic = BN_TOL_MAGIC;
tol.dist = 0.0005;
tol.dist_sq = tol.dist * tol.dist;
tol.perp = 1e-6;
tol.para = 1 - tol.perp;
{ // vertex points
std::vector<fastf_t> pts;
if (!getXSolData(pts, npt*3, m_solWork))
return {SolidParseResult::Error, "arbn vertex read failure"};
input_points = std::move(pts);
}
int cur_eq = 0;
int symm = 0;
/* Planes from triples */
for (int i=0;i<npe;i+=6) {
auto l = m_r.next("arbn vertex point indices");
if (!l) return {SolidParseResult::Error,"arbn EOF (point indices)"};
std::string_view lv(l->s);
for (int j=0;j<6 && (i+j)<npe;++j) {
int q = ComGeomReader::fieldInt(lv, 10 + j*10 + 0, 4);
int r = ComGeomReader::fieldInt(lv, 10 + j*10 + 4, 3);
int s = ComGeomReader::fieldInt(lv, 10 + j*10 + 7, 3);
if (q==0 || r==0 || s==0) continue;
point_t a,b,c;
auto fetch = [&](int idx, point_t &out){
int abs_idx = std::abs(idx)-1;
VMOVE(out, &input_points[abs_idx*3]);
if (idx < 0) { out[Y] = -out[Y]; symm = 1; }
};
fetch(q,a); fetch(r,b); fetch(s,c);
if (bg_make_plane_3pnts(eqn[cur_eq], a, b, c, &tol) < 0) {
if (m_opts.verbose) std::fprintf(stderr,"arbn degenerate 3p plane ignored\n");
continue;
}
++cur_eq;
}
}
/* Planes from equations */
for (int i=0;i<neq;++i) {
auto l = m_r.next("arbn plane equation card");
if (!l) return {SolidParseResult::Error,"arbn EOF (equation)"};
std::string_view lv(l->s);
eqn[cur_eq][X] = ComGeomReader::fieldDouble(lv, 10, 10);
eqn[cur_eq][Y] = ComGeomReader::fieldDouble(lv, 20, 10);
eqn[cur_eq][Z] = ComGeomReader::fieldDouble(lv, 30, 10);
eqn[cur_eq][W] = ComGeomReader::fieldDouble(lv, 40, 10);
double scale = MAGNITUDE(eqn[cur_eq]);
if (scale < SMALL) {
if (m_opts.verbose) std::fprintf(stderr,"arbn plane normal too small (eqn)\n");
continue;
}
scale = 1.0/scale;
VSCALE(eqn[cur_eq], eqn[cur_eq], scale);
eqn[cur_eq][W] *= scale;
++cur_eq;
}
/* Planes from az/el */
for (int i=0;i<nae;i+=2) {
auto l = m_r.next("arbn az/el card");
if (!l) return {SolidParseResult::Error,"arbn EOF (az/el)"};
std::string_view lv(l->s);
for (int j=0;j<2 && (i+j)<nae;++j) {
double az = ComGeomReader::fieldDouble(lv, 10 + j*30 + 0*10, 10) * DEG2RAD;
double el = ComGeomReader::fieldDouble(lv, 10 + j*30 + 1*10, 10) * DEG2RAD;
int vert_no = ComGeomReader::fieldInt(lv, 10 + j*30 + 2*10, 10);
if (vert_no == 0) break;
double cos_el = std::cos(el);
eqn[cur_eq][X] = std::cos(az) * cos_el;
eqn[cur_eq][Y] = std::sin(az) * cos_el;
eqn[cur_eq][Z] = std::sin(el);
point_t pt;
int abs_idx = std::abs(vert_no)-1;
VMOVE(pt, &input_points[abs_idx*3]);
if (vert_no < 0) pt[Y] = -pt[Y];
eqn[cur_eq][W] = VDOT(pt, eqn[cur_eq]);
++cur_eq;
}
}
if (cur_eq != nface)
return {SolidParseResult::Error, "arbn face count mismatch"};
point_t cent; VSETALL(cent, 0);
for (int i=0;i<npt;++i) VADD2(cent, cent, &input_points[i*3]);
VSCALE(cent, cent, 1.0/static_cast<double>(npt));
if (symm) cent[Y] = 0.0;
constexpr double DIST_TOL = 1.0e-8;
for (int i=0;i<nface;++i) {
double dist = VDOT(eqn[i], cent) - eqn[i][W];
if (dist < -DIST_TOL) continue;
if (dist > DIST_TOL) {
VREVERSE(eqn[i], eqn[i]);
eqn[i][W] = -eqn[i][W];
} else {
return {SolidParseResult::Error,"arbn centroid lies on face"};
}
}
std::vector<double> vertex;
int last_vertex = 0;
for (int i=0;i<nface-2;++i) {
for (int j=i+1;j<nface-1;++j) {
double dot = VDOT(eqn[i], eqn[j]);
if (!NEAR_ZERO(dot, 0.999999)) continue;
for (int k=j+1;k<nface;++k) {
point_t pt;
if (bg_make_pnt_3planes(pt, eqn[i], eqn[j], eqn[k]) < 0) continue;
bool outside=false;
for (int m=0;m<nface;++m) {
if (m==i||m==j||m==k) continue;
if (VDOT(pt, eqn[m]) - eqn[m][W] > DIST_TOL) { outside=true; break; }
}
if (outside) continue;
bool duplicate=false;
for (int m=0;m<last_vertex;++m) {
vect_t dv; VSUB2(dv, pt, &vertex[m*3]);
if (MAGSQ(dv) < 1.0e-10) { duplicate=true; break; }
}
if (duplicate) continue;
vertex.resize((last_vertex+1)*3);
VMOVE(&vertex[last_vertex*3], pt);
++last_vertex;
used[i]++; used[j]++; used[k]++;
}
}
}
for (int i=0;i<nface;++i)
if (used[i]==0)
return {SolidParseResult::Error,"arbn face unused (not convex)"};
int ret = mk_arbn(m_out, name.c_str(), nface, eqn.data());
return {ret<0?SolidParseResult::Error:SolidParseResult::Ok, ret<0?"mk_arbn failed":""};
}
/* ---------------- Dispatcher ---------------- */
SolidParseResult SolidParser::parseNext() {
if (m_solWork >= m_solTotal && m_solTotal > 0) {
return {SolidParseResult::Eof, ""};
}
auto l = m_r.next("solid card");
if (!l) return {SolidParseResult::Eof, ""};
int solidNum = 0;
std::string type, firstLine = l->s;
if (!parseSolidHeader(l->s, solidNum, type, firstLine)) {
if (m_solWork >= m_solTotal) return {SolidParseResult::Eof, ""};
return {SolidParseResult::Error, "invalid solid header"};
}
// v1 end sentinel handled in region code; in solids, "end" terminates
if (type == "end") {
return {SolidParseResult::Eof, ""};
}
if (m_opts.version == 5) {
int expectedNext = m_solWork + 1;
if (solidNum != expectedNext) {
// proceed but warn
std::fprintf(stderr,
"warning: solid numbering mismatch: expected %d, got %d - consuming this solid to maintain alignment\n",
expectedNext, solidNum);
}
}
// Always advance processed count; names use sequential counter
m_solWork++;
std::string name = makeSolidName(m_solWork);
colPrint(name);
std::vector<fastf_t> dd;
if (type == "ars") return handle_ars(name, firstLine);
if (type == "rpp") { if(!getSolData(dd,6,solidNum,true,firstLine)) return {SolidParseResult::Error,"rpp data read"}; return handle_rpp(name,dd); }
if (type == "box") { if(!getSolData(dd,12,solidNum,true,firstLine)) return {SolidParseResult::Error,"box data read"}; return handle_box(name,dd); }
if (type == "raw" || type == "wed") { if(!getSolData(dd,12,solidNum,true,firstLine)) return {SolidParseResult::Error,"raw/wed data read"}; return handle_raw_wed(name,dd); }
if (type == "rvw") { if(!getSolData(dd,7,solidNum,true,firstLine)) return {SolidParseResult::Error,"rvw data read"}; return handle_rvw(name,dd); }
if (type == "arw") { if(!getSolData(dd,12,solidNum,true,firstLine)) return {SolidParseResult::Error,"arw data read"}; return handle_arw(name,dd); }
if (m_opts.version == 0 && type == "arb") return handle_arb_magic(name, firstLine);
if (type == "arb4") { if(!getSolData(dd,12,solidNum,true,firstLine)) return {SolidParseResult::Error,"arb4 data read"}; return handle_arb4(name,dd); }
if (type == "arb5") { if(!getSolData(dd,15,solidNum,true,firstLine)) return {SolidParseResult::Error,"arb5 data read"}; return handle_arb5(name,dd); }
if (type == "arb6") { if(!getSolData(dd,18,solidNum,true,firstLine)) return {SolidParseResult::Error,"arb6 data read"}; return handle_arb6(name,dd); }
if (type == "arb7") { if(!getSolData(dd,21,solidNum,true,firstLine)) return {SolidParseResult::Error,"arb7 data read"}; return handle_arb7(name,dd); }
if (type == "arb8") { if(!getSolData(dd,24,solidNum,true,firstLine)) return {SolidParseResult::Error,"arb8 data read"}; return handle_arb8(name,dd); }
if (type == "rcc") { if(!getSolData(dd,7,solidNum,true,firstLine)) return {SolidParseResult::Error,"rcc data read"}; return handle_rcc(name,dd); }
if (type == "rec") { if(!getSolData(dd,12,solidNum,true,firstLine)) return {SolidParseResult::Error,"rec data read"}; return handle_rec(name,dd); }
if (type == "trc") { if(!getSolData(dd,8,solidNum,true,firstLine)) return {SolidParseResult::Error,"trc data read"}; return handle_trc(name,dd); }
if (type == "tec") { if(!getSolData(dd,13,solidNum,true,firstLine)) return {SolidParseResult::Error,"tec data read"}; return handle_tec(name,dd); }
if (type == "tgc") { if(!getSolData(dd,14,solidNum,true,firstLine)) return {SolidParseResult::Error,"tgc data read"}; return handle_tgc(name,dd); }
if (type == "sph") { if(!getSolData(dd,4,solidNum,true,firstLine)) return {SolidParseResult::Error,"sph data read"}; return handle_sph(name,dd); }
if (type.rfind("wir",0)==0) return handle_wir(name, firstLine);
if (type == "rpc") { if(!getSolData(dd,10,solidNum,true,firstLine)) return {SolidParseResult::Error,"rpc data read"}; return handle_rpc(name,dd); }
if (type == "rhc") { if(!getSolData(dd,11,solidNum,true,firstLine)) return {SolidParseResult::Error,"rhc data read"}; return handle_rhc(name,dd); }
if (type == "epa") { if(!getSolData(dd,11,solidNum,true,firstLine)) return {SolidParseResult::Error,"epa data read"}; return handle_epa(name,dd); }
if (type == "ehy") { if(!getSolData(dd,12,solidNum,true,firstLine)) return {SolidParseResult::Error,"ehy data read"}; return handle_ehy(name,dd); }
if (type == "eto") { if(!getSolData(dd,11,solidNum,true,firstLine)) return {SolidParseResult::Error,"eto data read"}; return handle_eto(name,dd); }
if (type == "ellg") { if(!getSolData(dd,12,solidNum,true,firstLine)) return {SolidParseResult::Error,"ellg data read"}; return handle_ellg(name,dd); }
if (type == "ell" || type == "ell1") {
if (m_opts.version <=4 && type=="ell") {
if (!getSolData(dd,7,solidNum,true,firstLine)) return {SolidParseResult::Error,"v4 ell data read"};
return handle_v4_ell_transform(name, dd);
} else {
if (!getSolData(dd,7,solidNum,true,firstLine)) return {SolidParseResult::Error,"ell data read"};
return handle_ell(name, dd);
}
}
if (type == "tor") { if(!getSolData(dd,8,solidNum,true,firstLine)) return {SolidParseResult::Error,"tor data read"}; return handle_tor(name,dd); }
if (type == "haf") { if(!getSolData(dd,4,solidNum,true,firstLine)) return {SolidParseResult::Error,"haf data read"}; return handle_haf(name,dd); }
if (type == "arbn") return handle_arbn(name, firstLine);
std::fprintf(stderr, "getsolid: unsupported type '%s'\n", type.c_str());
return {SolidParseResult::Error, "unsupported solid"};
}
-93
View File
@@ -1,93 +0,0 @@
#ifndef BRLCAD_SOLID_PARSER_H
#define BRLCAD_SOLID_PARSER_H
#include "common.h"
#include <string>
#include <vector>
#include <optional>
extern "C" {
#include "vmath.h"
#include "raytrace.h"
#include "wdb.h"
#include "bg.h"
}
#include "ComGeomReader.h"
struct SolidParseResult {
enum Status { Ok, Eof, Error } status = Error;
std::string message;
};
struct SolidParserOptions {
int version = 5;
int verbose = 0;
std::string suffix;
bool legacy_trunc = false;
};
class SolidParser {
public:
SolidParser(ComGeomReader &reader, rt_wdb *outfp, const SolidParserOptions &opts);
SolidParseResult parseNext();
int solidsExpected() const { return m_solTotal; }
int solidsProcessed() const { return m_solWork; }
void setSolidsExpected(int c) { m_solTotal = c; }
private:
std::string makeSolidName(int n) const;
void colPrint(const std::string &s);
bool parseSolidHeader(const std::string &line, int &solidNum,
std::string &typeLower, std::string &lineOutForData);
bool getSolData(std::vector<fastf_t> &data, int num, int solidNum,
bool firstCardAlreadyRead, std::string lineBuf);
bool getXSolData(std::vector<fastf_t> &data, int num, int solidNum);
/* Helpers */
bool isIntegerField(std::string_view sv, size_t start, size_t len, int &value) const;
bool isLikelyFaceCard(const std::string &line) const;
/* Primitive handlers */
SolidParseResult handle_ars(const std::string &name, const std::string &firstLine);
SolidParseResult handle_rpp(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_box(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_raw_wed(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_rvw(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_arw(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_arb_magic(const std::string &name, const std::string &firstLine);
SolidParseResult handle_arb4(const std::string &name, const std::vector<fastf_t> &dd4);
SolidParseResult handle_arb5(const std::string &name, const std::vector<fastf_t> &dd5);
SolidParseResult handle_arb6(const std::string &name, const std::vector<fastf_t> &dd6);
SolidParseResult handle_arb7(const std::string &name, const std::vector<fastf_t> &dd7);
SolidParseResult handle_arb8(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_rcc(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_rec(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_trc(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_tec(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_tgc(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_sph(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_wir(const std::string &name, const std::string &firstLine);
SolidParseResult handle_rpc(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_rhc(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_epa(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_ehy(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_eto(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_v4_ell_transform(const std::string &name, const std::vector<fastf_t> &foci);
SolidParseResult handle_ell(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_ellg(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_tor(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_haf(const std::string &name, const std::vector<fastf_t> &dd);
SolidParseResult handle_arbn(const std::string &name, const std::string &firstLine);
private:
ComGeomReader &m_r;
rt_wdb *m_out = nullptr;
SolidParserOptions m_opts;
int m_solTotal = 0;
int m_solWork = 0;
size_t m_colCur = 0;
};
#endif
@@ -1,376 +0,0 @@
================================================================================
BRL-CAD BoT → COMGEOM ARBN Export Pipeline
RLM (Robust Low-Poly Meshing Inspired) + HACD/V-HACD Convex Decomposition
================================================================================
Purpose:
--------
Provide a clear, implementation-oriented blueprint for converting arbitrary
triangle meshes (BOTs) into a set of convex COMGEOM ARBN solids suitable for
region assembly. This document is intended both for human developers and as
context for future GPT-assisted sessions.
Context:
--------
COMGEOM does not have a native arbitrary triangle mesh primitive. ARBN
(primitives defined by intersection of half-spaces / planes) can represent
convex polyhedra only. Arbitrary non-convex BOT geometry must be decomposed
into convex parts, each described as an ARBN. To reduce complexity and noise,
we first simplify and planarize the mesh with an RLM-inspired stage, then
decompose using HACD/V-HACD, and finally produce ARBNs.
High-Level Pipeline Stages:
---------------------------
1. Mesh Acquisition & Validation
2. RLM-Inspired Simplification (Planar Clustering & Polygon Extraction)
3. Optional Primitive Recognition (Planes, Cylinders, Cones) (future enhancement)
4. Convex Decomposition (HACD/V-HACD or fallback hull partitioning)
5. Convex Part → Plane Set Conversion
6. ARBN Construction via Incremental Half-Space Clipping
7. Region Assembly (OR union of convex ARBN solids)
8. Quality Assurance (QA) & Reporting
9. Output COMGEOM Deck (version selectable: v1 / v4 / v5)
10. Optional JSON summary and statistics
Key Design Goals:
-----------------
- Produce clean, planar-faced convex solids minimizing plane count.
- Heal small cracks, near-coplanar face fragmentation, and sliver triangles.
- Provide tunable tolerances for clustering, plane fitting, snapping, and decomposition.
- Maintain sharp features (edges with large dihedral angles).
- Scale to large meshes (hundreds of thousands of triangles) via segmentation.
Terminology:
------------
BOT : BRL-CAD triangle mesh primitive.
RLM-inspired : Robust Low-Poly Meshing style operations (plane clustering, feature retention).
HACD/V-HACD : Hierarchical Approximate Convex Decomposition algorithms.
ARBN : COMGEOM arbitrary N-plane convex solid.
Plane cluster : Group of triangles forming an approximate planar patch.
Convex part : Output of HACD or grouping guaranteed to be convex.
Clipper : Incremental half-space intersection algorithm producing ARBN vertices.
Region : COMGEOM Boolean combination (union/intersection/subtraction) of solids.
Assumptions:
------------
- BOT input is either closed and manifold or can be repaired (hole fill / thickening).
- Decomposition tools are available (HACD or V-HACD library; otherwise fallback convex hull).
- Numeric tolerances are chosen relative to model scale (bbox diagonal).
- Complexity controls (max planes per ARBN, max clusters, max convex parts) prevent runaway resource usage.
Environment / Flags (Proposed):
-------------------------------
--simplify=none|rlm : Enable RLM-stage planar clustering.
--rlm-angle=deg : Angle threshold for normal clustering (default 2.0).
--rlm-dist=scale : Distance tolerance for plane residual (default 1e-3 * bbox_diag).
--rlm-snap=scale : Vertex snap tolerance (default 5e-4 * bbox_diag).
--rlm-sharp=deg : Dihedral angle above which edges are preserved (default 30).
--rlm-min-area=scale^2 : Minimum polygon area; discard degenerates.
--hacd=on|off : Enable HACD decomposition stage.
--hacd-concavity=value : HACD concavity parameter.
--hacd-max-parts=N : Max number of convex parts.
--hacd-min-volume=scale^3 : Minimum part volume threshold.
--decomp-order=pre|post : RLM before or after HACD (default pre).
--max-planes-per-arbn=N : Safety cap (default 512).
--arbn-hash=on|off : Spatial hash for vertex deduplication (default on).
--arbn-stats=path : Stats dump file.
--export-version=1|4|5 : COMGEOM dialect (default 5).
--json=path : Emit JSON export report.
--material-map=file.json : Attribute enrichment mapping.
--color-map=file.json : Region color mapping.
Stage 1: Mesh Acquisition & Validation
--------------------------------------
Inputs: BOT primitive (triangle list).
Steps:
1. Load triangles; compute bounding box.
2. Verify manifold conditions:
- Each edge referenced by 1 or 2 faces.
- No zero-area triangles.
3. If open surface and user requests solid export:
- Either abort or apply thickening (offset + stitch) – optional future feature.
4. Deduplicate vertices (hash by quantized position).
5. Compute per-face normals and edge dihedral angles.
Outputs:
- Clean triangle list, adjacency (half-edge or edge-to-face map).
- BBox, scale metrics.
Stage 2: RLM-Inspired Simplification
------------------------------------
Goal: Merge nearly coplanar triangles into planar clusters.
Algorithm Sketch:
1. Seed clusters: each triangle forms initial cluster with its plane.
2. Normal clustering: merge cluster pairs if angle between planes < rlm-angle AND distance between centroids projects within rlm-dist residual.
3. Plane fitting: For each cluster, fit plane via PCA/least squares to vertex set. Recompute residuals.
4. Split clusters where residual > rlm-dist or non-manifold boundaries arise.
5. Extract boundary polygon:
- Collect boundary edges (adjacent triangle only once).
- Project boundary vertices to plane.
- Compute 2D convex (or general boundary) hull; remove interior points.
6. Vertex snapping:
- Project vertices onto fitted plane.
- Merge vertices within rlm-snap tolerance.
7. Sharp edges retention:
- Do not merge clusters across edges with dihedral > rlm-sharp.
8. Degeneracy filtering:
- Discard polygons with area < rlm-min-area or internal angle < min_angle (~5°).
9. Optional: Triangulate polygon if required by downstream (for HACD only if non-convex planar patch).
Outputs:
- Reduced set of planar patches (clusters) with cleaned boundary polygons.
- Mapping from original faces to cluster.
Stage 3: Optional Primitive Recognition (Future Enhancement)
------------------------------------------------------------
Idea: Detect canonical primitives (planes, cylinders, spheres, cones) before clustering to reduce complexity:
- RANSAC or multi-pass fitting.
- Replace large planar clusters with a BOX or RPP if axis-aligned.
- Defer for initial implementation.
Stage 4: Convex Decomposition (HACD/V-HACD)
-------------------------------------------
If mesh (post-RLM) remains non-convex:
1. Run HACD on simplified mesh.
- Inputs: tolerance (concavity), max parts, min volume.
2. Obtain convex parts (triangle subsets).
3. For each convex part:
- Optionally re-run a localized RLM stage again (Mode: post-hacd).
4. If HACD disabled:
- Option hull-only: compute global convex hull (loss of concavity details).
- Reject or warn if non-convex and no decomposition path chosen.
Outputs:
- Collection of convex parts (each either a polygonal shell).
Stage 5: Convex Part → Plane Set Conversion
-------------------------------------------
For each convex part:
1. Compute convex hull of part (even if already convex; double-check).
2. Extract hull faces; each face yields a plane (normal outward, distance).
3. Normalize planes; cluster near-duplicates (angle & offset tolerance).
4. Verify face consistency (no inverted normals; positive volume).
5. Cap plane count; if > max-planes-per-arbn → either split, relax tolerance, or fallback to legacy enumeration (warning).
Outputs:
- Plane list per convex part.
Stage 6: ARBN Construction
--------------------------
Use incremental half-space clipping (implemented arbn_clip):
1. Seed bounding polyhedron from plane offsets (derived radius).
2. For each plane in part:
- Clip polyhedron (spatial hash for vertex reuse).
- Abort if volume becomes zero (log error; fallback to hull triangulation?).
3. Gather final vertex set and face loops.
4. Record stats (input planes, duplicates removed, final face/vertex count).
Outputs:
- ARBN solid definition: plane equations only (COMGEOM stores planes; vertex enumeration implicit).
- Optional internal triangulation only for QA (not stored in deck).
Stage 7: Region Assembly
------------------------
Single region for entire original BOT:
- Boolean semantics: Region = OR of all ARBN solids derived from decomposition.
- If decomposition yields components with distinct attributes (e.g., materials), create multiple regions (each corresponding to an attribute class).
- For COMGEOM v1: multiple cards with OR semantics; for v4/v5: parenthesized expression: ( +S1 ) OR ( +S2 ) OR ...
Stage 8: QA & Validation
------------------------
Checks:
1. Volume > 0 and matches (within tolerance) original mesh volume (approx. via voxel sampling or Monte Carlo).
2. Surface area near original (after simplification) minus sliver removal.
3. Watertightness: ray shoot tests (random directions) see consistent in/out transitions.
4. Plane count sanity (no explosion).
5. Overlaps: sampled points belong to exactly one convex part unless intended union.
Stage 9: Output COMGEOM Deck
----------------------------
For each ARBN:
- Solid table entries listing plane equations (v1 fixed columns or v5 token).
Region table:
- Union of solids, optional attributes.
Ident table:
- Region codes, space codes, materials, LOS (v5 attribute lines or numeric fields).
Attribute enhancement (v5):
- material=, color=R/G/B, aircode=, density= if available.
Stage 10: Reporting
-------------------
JSON report keys:
{
"input": { "triangles": N, "bbox": [...], "scale": S },
"rlm": { "clusters": C, "merged_faces": F, "avg_residual": R },
"hacd": { "enabled": true, "parts": P, "concavity": X },
"arbn": { "solids": S, "avg_planes": A, "max_planes": M },
"region": { "count": Rg, "boolean": "OR" },
"stats": { "warnings": W, "errors": E },
"timing": { "rlm_ms": t1, "hacd_ms": t2, "arbn_ms": t3 }
}
Parameter Tuning Guidelines:
----------------------------
angle_tol (cluster) : 1–2 degrees (start small; larger increases merging).
distance_tol (plane fit) : 1e-3 × bbox_diag (reduce for high precision parts).
snap_tol (vertex merge) : 5e-4 × bbox_diag (balance healing vs distortion).
sharp_threshold : 30 degrees (increase to preserve more edges).
hacd_concavity : 0.002–0.01 typical; lower → more parts, higher → fewer.
max_planes_per_arbn : 256–512 recommended safety.
min_face_area : 1e-10 × bbox_diag^2 (filter numerical artifacts).
min_part_volume : 1e-9 × bbox_diag^3 (discard tiny fragments).
Error Handling Strategy:
------------------------
- Non-manifold input → attempt repair; if fails, abort with message.
- Excessive plane count → split cluster or relax clustering thresholds.
- Zero-volume ARBN creation → fallback to convex hull or mark as skipped.
- HACD failure → fallback to hull-only mode with warning.
- Tolerance misconfiguration → auto-suggest adjustments in JSON report.
Performance Considerations:
---------------------------
- Spatial hashing for vertex dedup in ARBN clipper (O(1) average insert/find).
- Parallelization possibilities:
- RLM clustering per spatial block / octree node.
- HACD usually internal multi-threading (depending on library).
- ARBN construction per convex part in parallel threads.
- Memory caps: early discard of clusters under min_face_area prevents growth.
- Streaming: flush solids incrementally to output files.
Security / Robustness Notes:
----------------------------
- Validate all user-supplied tolerances; clamp unreasonable values.
- Protect against extremely large mesh inputs by early bounding checks.
- Consistent use of relative tolerances tied to model scale to avoid numeric collapse.
Interfaces & Extensibility:
---------------------------
Future flags:
--primitive-detect=on/off : Pre-pass for cylinders/cones/spheres.
--thicken-open-mesh=scale : Convert open surfaces to solids.
--exact-predicates=on/off : Introduce robust orientation tests.
--parallel=N : Explicit thread count.
Data Structures (Minimal Outline):
----------------------------------
struct RLMCluster {
vect_t plane_normal;
double plane_offset;
double rms_error;
size_t *face_indices;
size_t face_count;
int sharp_boundary; /* flag */
point_t *boundary_vertices;
size_t boundary_count;
};
struct ConvexPart {
size_t *triangle_indices;
size_t tri_count;
plane_t *planes;
size_t plane_count;
};
struct ARBNSolid {
plane_t *planes;
size_t plane_count;
/* optional stats */
};
Pseudo-Code Summary:
--------------------
function export_bot_to_comgeom(bot, params):
triangles = load(bot)
validate(triangles)
if params.simplify == RLM:
clusters = rlm_cluster(triangles, params)
triangles = rebuild_from_clusters(clusters) // optional
parts = (params.hacd ? hacd_decompose(triangles, params) : [triangles])
solids = []
for part in parts:
planes = extract_planes(part, params)
arbn = build_arbn(planes, params)
if arbn.valid: solids.append(arbn)
region = build_region_union(solids)
write_comgeom(solids, region, params)
write_report(params, stats)
Potential Failure Scenarios & Responses:
----------------------------------------
1. Input mesh massively non-manifold:
- Response: abort early; suggest repair tool invocation.
2. RLM merges too aggressively (loss of moderate detail):
- Response: output warning; advise lower angle_tol.
3. HACD produces thousands of tiny parts:
- Response: raise concavity tolerance; filter by min_part_volume.
4. ARBN plane overflow:
- Response: split part along largest variance axis; recursively process.
5. Output region expression too large:
- Response: chunk region definitions or group solids logically by type.
Testing Strategy:
-----------------
Unit:
- Plane fitting accuracy (synthetic coplanar sets).
- Clustering stability under noise.
- ARBN clipper invariants (volume positive, faces planar).
Integration:
- BOT sample models (simple, moderate, complex).
- Comparison of original vs simplified volumes (Monte Carlo sampling).
- Performance benchmarking (timings for each stage).
Regression:
- Fuzz small random perturbations in vertex positions; ensure pipeline stable.
- Large mesh stress (e.g., 250k triangles) within acceptable memory/time.
JSON Report Example:
--------------------
{
"input": { "triangles": 50234, "bbox": [ [0,0,0], [10,5,7] ], "scale": 12.247 },
"rlm": { "clusters": 340, "merged_faces": 48000, "avg_residual": 0.00072 },
"hacd": { "enabled": true, "parts": 128, "concavity": 0.005 },
"arbn": { "solids": 128, "avg_planes": 18, "max_planes": 42 },
"region": { "count": 1, "boolean": "OR" },
"stats": { "warnings": 3, "errors": 0 },
"timing": { "rlm_ms": 1834, "hacd_ms": 2975, "arbn_ms": 622 }
}
Open Questions (For Future Iteration):
--------------------------------------
- Should we incorporate curved primitive detection before RLM to reduce plane counts further?
- Acceptable loss thresholds for feature simplification (volume delta %, area delta %)?
- Deterministic mode (fixed random seeds) for reproducible CI outputs?
- Support for per-material segmentation feeding separate region definitions?
Recommended Initial Implementation Order:
----------------------------------------
Phase 1: BOT validation + adjacency + dihedral computation
Phase 2: Basic RLM clustering (angle + residual) + boundary extraction
Phase 3: HACD integration (API wrapper) + parameter mapping
Phase 4: Plane extraction + ARBN clipper integration
Phase 5: Region assembly + COMGEOM writing (v5 first)
Phase 6: JSON reporting + stats instrumentation
Phase 7: Tuning + advanced cluster splitting + QA benchmarks
Phase 8: Optional primitive recognition and improved heuristics
Risk Summary:
-------------
- Over-simplification: mitigated by exposing user parameters and reporting cluster stats.
- Performance bottlenecks: spatial hashing, early culling, parallel cluster operations.
- Numerical instability: per-stage tolerance scaling; failover to hull approximations.
- Large memory footprints: streaming cluster processing and releasing intermediate buffers.
References (Conceptual Inspirations):
-------------------------------------
- Robust Low-Poly Meshing (RLM) paper (site referenced).
- HACD / V-HACD algorithms (hierarchical approximate convex decomposition).
- Plane clustering & PCA fitting (standard computational geometry).
- Incremental half-space intersection for convex polyhedra.
- Shewchuk robust predicates (optional future exact arithmetic upgrade).
- RANSAC-based primitive detection (future enhancements).
End of Document
================================================================================
-122
View File
@@ -1,122 +0,0 @@
#include "common.h"
#include <string>
#include <vector>
#include <cstdio>
#include <cstdlib>
extern "C" {
#include "bu/app.h"
#include "bu/opt.h"
#include "bu/vls.h"
}
#include "ComGeomConverter.h"
/* Custom arg processor to copy a string argument into a std::string */
static int
opt_set_string(struct bu_vls *msg, size_t argc, const char **argv, void *set_var)
{
BU_OPT_CHECK_ARGV0(msg, argc, argv, "string");
if (!set_var) return -1;
std::string *sp = static_cast<std::string *>(set_var);
*sp = argv[0];
return 1;
}
struct CliOptions {
ConverterOptions conv;
int show_help = 0;
int legacy_truncate_names = 0;
std::vector<std::string> positionals;
};
static void usage(const char *argv0, const struct bu_opt_desc *opt_defs)
{
std::fprintf(stderr,
"Usage: %s [options] input_file output_file\n\n", argv0);
char *help = bu_opt_describe(opt_defs, NULL);
if (help) {
std::fprintf(stderr, "%s\n", help);
bu_free(help, "help string");
}
std::fprintf(stderr,
"\nExamples:\n"
" %s model.cg model.g # convert using defaults (version 5)\n"
" %s -v 0 in_magic.cg out.g # convert MAGIC-era v0 deck\n"
" %s -v 4 -s _cg4 in.cg out.g # convert v4, append suffix to names\n"
" %s -t -s _longsuffix in.cg out.g # legacy suffix truncation (13 chars)\n"
" %s -d 2 input.cg out.g # verbose debug level 2\n",
argv0, argv0, argv0, argv0, argv0);
}
int main(int argc, const char **argv)
{
bu_setprogname(argv[0]);
CliOptions co;
struct bu_opt_desc opt_defs[7];
BU_OPT(opt_defs[0], "h", "help", "", NULL, &co.show_help, "Show help and exit.");
BU_OPT(opt_defs[1], "v", "version", "N", &bu_opt_int, &co.conv.version, "Set COMGEOM input version (0,1,4,5; default 5).");
BU_OPT(opt_defs[2], "d", "debug", "N", &bu_opt_int, &co.conv.verbose, "Set verbosity/debug level (integer).");
BU_OPT(opt_defs[3], "s", "suffix", "STR", opt_set_string, &co.conv.suffix, "Append name suffix to all generated objects.");
BU_OPT(opt_defs[4], "V", "verbose", "", &bu_opt_incr_long,&co.conv.verbose, "Increment verbosity (may be repeated).");
BU_OPT(opt_defs[5], "t", "legacy-truncate-names", "", NULL, &co.legacy_truncate_names, "Enable legacy name truncation (suffix max 13 chars).");
BU_OPT_NULL(opt_defs[6]);
std::vector<const char *> argvec;
argvec.reserve(static_cast<size_t>(argc - 1));
for (int i = 1; i < argc; ++i) argvec.push_back(argv[i]);
struct bu_vls msgs = BU_VLS_INIT_ZERO;
int ret = bu_opt_parse(&msgs, argvec.size(), argvec.data(), opt_defs);
if (ret < 0) {
if (BU_VLS_IS_INITIALIZED(&msgs) && bu_vls_strlen(&msgs) > 0)
std::fprintf(stderr, "%s", bu_vls_addr(&msgs));
usage(argv[0], opt_defs);
bu_vls_free(&msgs);
return 1;
}
size_t unused = static_cast<size_t>(ret);
for (size_t i = 0; i < unused; ++i)
co.positionals.emplace_back(argvec[i]);
if (co.show_help) {
usage(argv[0], opt_defs);
bu_vls_free(&msgs);
return 0;
}
if (co.positionals.size() != 2) {
std::fprintf(stderr,
"Error: expected 2 positional arguments (input_file output_file), got %zu.\n",
co.positionals.size());
usage(argv[0], opt_defs);
bu_vls_free(&msgs);
return 1;
}
if (co.conv.version != 0 && co.conv.version != 1 && co.conv.version != 4 && co.conv.version != 5) {
std::fprintf(stderr, "Error: unsupported COMGEOM version %d (valid: 0,1,4,5).\n",
co.conv.version);
usage(argv[0], opt_defs);
bu_vls_free(&msgs);
return 1;
}
if (BU_VLS_IS_INITIALIZED(&msgs) && bu_vls_strlen(&msgs) > 0 && co.conv.verbose)
std::fprintf(stderr, "%s", bu_vls_addr(&msgs));
bu_vls_free(&msgs);
const std::string inputPath = co.positionals[0];
const std::string outputPath = co.positionals[1];
co.conv.legacy_truncate_names = (co.legacy_truncate_names != 0);
ComGeomConverter converter(inputPath, outputPath, co.conv);
return converter.run();
}
+2 -1
View File
@@ -41,6 +41,7 @@ set(
badmagic.c
bitv.c
bomb.c
binding.cpp
booleanize.c
bu_init.cpp
cache.cpp
@@ -234,7 +235,7 @@ set(
mime.cmake
parallel.h
parallel_cpp11thread.cpp
process.h
process_private.h
sha1.h
struetype.h
vfont.h
+362
View File
@@ -0,0 +1,362 @@
/* 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
*/
+1 -1
View File
@@ -49,7 +49,7 @@
#include "bu/str.h"
#include "bu/datetime.h"
#include "bu/vls.h"
#include "./process.h"
#include "./process_private.h"
#ifndef HAVE_KILL
# include <TlHelp32.h>
@@ -1,4 +1,4 @@
/* P R O C E S S . H
/* P R O C E S S _ P R I V A T E . H
* BRL-CAD
*
* Copyright (c) 2013-2026 United States Government as represented by
@@ -20,8 +20,8 @@
/* Private libbu process related definitions shared between multiple files */
#ifndef LIBBU_PROCESS_H
#define LIBBU_PROCESS_H
#ifndef LIBBU_PROCESS_PRIVATE_H
#define LIBBU_PROCESS_PRIVATE_H
/* Apparently the termination code on Windows is arbitrary and somewhat
* ill-defined - see https://bugs.python.org/issue31863 for more info. Try our
@@ -29,7 +29,7 @@
* abort identification. */
#define BU_MSVC_ABORT_EXIT 97101 /* ascii - a == 97 && e == 101 */
#endif /* LIBBU_PROCESS_H */
#endif /* LIBBU_PROCESS_PRIVATE_H */
/*
* Local Variables:
+2
View File
@@ -10,6 +10,7 @@ set(
test_bomb.c
test_badmagic.c
test_basename.c
test_binding.cpp
test_bitv.c
test_booleanize.c
test_cache.cpp
@@ -122,6 +123,7 @@ 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)
+109
View File
@@ -0,0 +1,109 @@
/* 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
*/
+22
View File
@@ -0,0 +1,22 @@
{
"contexts": {
"fb": {
"bindings": [
{
"match": {
"type": "mouse-press",
"button": 2
},
"action": "fb_close_window"
},
{
"match": {
"type": "key-press",
"key": "Escape"
},
"action": "fb_invalid_action"
}
]
}
}
}
+6
View File
@@ -38,6 +38,11 @@ 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)
@@ -165,3 +170,4 @@ cmakefiles(${libdm_ignore_files})
# indent-tabs-mode: t
# End:
# ex: shiftwidth=2 tabstop=8
+1
View File
@@ -2925,6 +2925,7 @@ 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. */
+59
View File
@@ -0,0 +1,59 @@
{
"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" }
]
}
}
}
+12
View File
@@ -66,10 +66,22 @@ 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;
+14
View File
@@ -29,6 +29,7 @@
#include <cctype>
#include <cstring>
#include <map>
#include "bu/binding.h"
#include <set>
#include <string>
#include <cstdio>
@@ -62,6 +63,13 @@ 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;
}
@@ -528,6 +536,12 @@ 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;
}
+25
View File
@@ -0,0 +1,25 @@
{
"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"
}
}
+54
View File
@@ -53,6 +53,7 @@
#include "./include/private.h"
#include "icv.h"
#include "dm.h"
#include "bu/binding.h"
struct fb *fb_get(void)
@@ -258,6 +259,59 @@ 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)
+6
View File
@@ -0,0 +1,6 @@
{
"actions": {
"fb_print_pixel_rgb": "Print RGB values of pixel under cursor",
"fb_close_window": "Close framebuffer window"
}
}
+171 -72
View File
@@ -61,6 +61,10 @@
#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
@@ -774,6 +778,52 @@ 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)
@@ -781,76 +831,122 @@ 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);
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;
}
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;
}
}
}
@@ -1021,9 +1117,10 @@ 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();
/*
* First, attempt to determine operating mode for this open,
* based upon the "unit number" or flags.
@@ -1214,7 +1311,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;
ButtonPressMask | ButtonReleaseMask | PointerMotionMask | StructureNotifyMask;
swa.colormap = OGL(ifp)->xcmap;
#define XCreateWindowDebug(display, parent, x, y, width, height, border_width, depth, class, visual, valuemask, attributes) \
@@ -1381,6 +1478,8 @@ 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);
@@ -188,8 +188,7 @@ test_uncertain()
require(result.shells.empty() || (result.shells.front().orientation == ShellOrientation::Indeterminate &&
!result.shells.front().corrected), "invalid parameter-loop senses caused an incorrect face repair");
auto flat = box();
ON_Xform flatten;
flatten.DiagonalTransformation(1.0);
ON_Xform flatten = ON_Xform::DiagonalTransformation(1.0);
flatten[2][2] = 1.0e-13;
require(flat->Transform(flatten), "could not construct thin shell");
result = repair_brep_orientation(*flat);
+241 -246
View File
@@ -37,6 +37,12 @@ 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;
@@ -68,247 +74,256 @@ restore_drag_bounds_update(struct bview *v, int refresh_bounds)
drag_update_ts.erase(v);
}
// TODO - look into QShortcut, see if it might be a better way
// to manage this
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, "]");
}
int CADkeyPressEvent(struct bview *v, int UNUSED(x_prev), int UNUSED(y_prev), QKeyEvent *k)
{
QTCAD_EVENT("keyPress", 1);
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;
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);
}
return 0;
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;
}
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();
if (!v)
return 0;
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 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;
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;
}
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;
if (!v) return 0;
qtcad_init_bindings();
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)
cx = (double)e->x();
cy = (double)e->y();
double cx = e->x();
double cy = e->y();
#else
cx = e->position().x();
cy = e->position().y();
double cx = e->position().x();
double cy = e->position().y();
#endif
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;
if ((fabs(cx - x_press) > 10) || (fabs(cy - y_press) > 10)) 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;
}
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::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);
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;
}
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 (!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;
if (x_prev == -INT_MAX) return 0;
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)
@@ -319,53 +334,33 @@ int CADmouseMoveEvent(struct bview *v, int x_prev, int y_prev, QMouseEvent *e, i
int dy = e->position().y() - y_prev;
#endif
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);
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;
}
int CADwheelEvent(struct bview *v, QWheelEvent *e)
{
QTCAD_EVENT("mouseWheel", 1);
if (!v)
return 0;
if (!v) return 0;
qtcad_init_bindings();
QPoint delta = e->angleDelta();
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);
int mdelta = -1 * delta.y() / 8;
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:
+1
View File
@@ -427,6 +427,7 @@ endif(BRLCAD_ENABLE_SPR)
set(LIBRT_PUBLIC_LIBS ${librt_deps} OPENNURBS::OPENNURBS)
set(
LIBRT_PRIVATE_LIBS
Threads::Threads
${OPENCL_LIBS}
mmesh::mmesh
REGEX::REGEX
+469 -203
View File
@@ -45,24 +45,28 @@
* intersecting closed surface), and total_chord is the sum of solid
* segment lengths.
*
* References:
* Li et al. (2003), "Using low-discrepancy sequences and the Crofton
* formula to compute surface areas of geometric models",
* Computer-Aided Design 35, 771-782.
*
* Liu et al. (2010), "Surface area estimation of digitized 3D objects
* using quasi-Monte Carlo methods", Pattern Recognition 43, 3900-3909.
* Chord endpoints are independent pseudorandom points on the bounding
* sphere. Reinitializing the generator with a fixed seed makes sampling
* reproducible without changing the ordinary Monte Carlo convergence
* behavior on which the estimator's stability checks rely.
*/
#include "common.h"
#include <chrono>
#include <climits>
#include <condition_variable>
#include <cstdint>
#include <exception>
#include <mutex>
#include <random>
#include <stdlib.h>
#include <math.h>
#include <random>
#include <stdexcept>
#include <string.h>
#include <thread>
#include <time.h>
#include <vector>
#include "vmath.h"
#include "bu/log.h"
@@ -100,8 +104,15 @@
#define RT_CROFTON_STABILITY_MIN_WINDOWS 2u
/** Fixed seed for reproducible estimates in tests and cross-platform runs. */
#define RT_CROFTON_RNG_SEED 0x9e3779b97f4a7c15ULL
#define RT_CROFTON_RNG_SEED UINT64_C(0x9e3779b97f4a7c15)
/** Two sphere coordinates for each of two chord endpoints. */
#define RT_CROFTON_RANDOM_VALUES_PER_RAY 4u
/** Convert the high 53 random bits exactly to a double in [0, 1). */
static constexpr unsigned int CROFTON_RANDOM_SHIFT = 11u;
static constexpr double CROFTON_RANDOM_SCALE =
1.0 / 9007199254740992.0;
/* ------------------------------------------------------------------ */
/* Internal types */
@@ -110,6 +121,7 @@
struct crofton_ray {
point_t r_pt;
vect_t r_dir;
size_t id;
};
struct crofton_shared {
@@ -117,14 +129,19 @@ struct crofton_shared {
size_t total_crossings; /* in+out hit events */
double total_chord; /* solid segment length sum (mm) */
size_t total_rays; /* rays fired (hits + misses) */
size_t ray_offset; /* first identifier in this sample stream */
point_t *points;
size_t point_count;
size_t point_capacity;
struct rt_crofton_segment *segments;
size_t segment_count;
size_t segment_capacity;
bool collect_points;
bool collect_segments;
};
struct crofton_worker_data {
struct application *ap; /* per-CPU application struct */
struct application ap; /* per-CPU application struct */
struct crofton_ray *rays; /* shared ray array (read-only) */
size_t start;
size_t end;
@@ -135,6 +152,10 @@ struct crofton_worker_data {
point_t *local_points;
size_t local_point_count;
size_t local_point_capacity;
struct rt_crofton_segment *local_segments;
size_t local_segment_count;
size_t local_segment_capacity;
size_t current_ray;
};
@@ -143,46 +164,88 @@ struct crofton_worker_data {
/* ------------------------------------------------------------------ */
static int
crofton_hit(struct application *ap, struct partition *PartHeadp, struct seg *UNUSED(segs))
crofton_hit(struct application *ap, struct partition *PartHeadp,
struct seg *UNUSED(segs))
{
struct crofton_worker_data *wd = (struct crofton_worker_data *)ap->a_uptr;
struct crofton_worker_data *worker =
(struct crofton_worker_data *)ap->a_uptr;
size_t crossings = 0;
double chord = 0.0;
double chord = 0.0;
struct partition *pp;
for (pp = PartHeadp->pt_forw; pp != PartHeadp; pp = pp->pt_forw) {
/* Each partition contributes an in-hit and an out-hit */
crossings += 2;
chord += pp->pt_outhit->hit_dist - pp->pt_inhit->hit_dist;
if (wd->shared->collect_points) {
if (wd->local_point_count + 2 > wd->local_point_capacity) {
const size_t initial_capacity = 1024;
size_t new_capacity = wd->local_point_capacity ?
2 * wd->local_point_capacity : initial_capacity;
while (new_capacity < wd->local_point_count + 2)
new_capacity *= 2;
wd->local_points = (point_t *)bu_realloc(wd->local_points,
new_capacity * sizeof(point_t), "Crofton worker hit points");
wd->local_point_capacity = new_capacity;
}
VJOIN1(wd->local_points[wd->local_point_count], ap->a_ray.r_pt,
pp->pt_inhit->hit_dist, ap->a_ray.r_dir);
wd->local_point_count++;
VJOIN1(wd->local_points[wd->local_point_count], ap->a_ray.r_pt,
pp->pt_outhit->hit_dist, ap->a_ray.r_dir);
wd->local_point_count++;
}
struct partition *partition;
for (partition = PartHeadp->pt_forw; partition != PartHeadp;
partition = partition->pt_forw) {
fastf_t thickness = partition->pt_outhit->hit_dist -
partition->pt_inhit->hit_dist;
crossings += 2;
chord += thickness;
if (worker->shared->collect_points) {
if (worker->local_point_count + 2 >
worker->local_point_capacity) {
const size_t initial_capacity = 1024;
size_t new_capacity = worker->local_point_capacity ?
2 * worker->local_point_capacity : initial_capacity;
while (new_capacity < worker->local_point_count + 2)
new_capacity *= 2;
worker->local_points = (point_t *)bu_realloc(
worker->local_points,
new_capacity * sizeof(point_t),
"Crofton worker hit points");
worker->local_point_capacity = new_capacity;
}
VJOIN1(worker->local_points[worker->local_point_count],
ap->a_ray.r_pt, partition->pt_inhit->hit_dist,
ap->a_ray.r_dir);
worker->local_point_count++;
VJOIN1(worker->local_points[worker->local_point_count],
ap->a_ray.r_pt, partition->pt_outhit->hit_dist,
ap->a_ray.r_dir);
worker->local_point_count++;
}
if (worker->shared->collect_segments) {
if (worker->local_segment_count + 1 >
worker->local_segment_capacity) {
const size_t initial_capacity = 512;
size_t new_capacity = worker->local_segment_capacity ?
2 * worker->local_segment_capacity : initial_capacity;
worker->local_segments =
(struct rt_crofton_segment *)bu_realloc(
worker->local_segments,
new_capacity * sizeof(struct rt_crofton_segment),
"Crofton worker segments");
worker->local_segment_capacity = new_capacity;
}
struct rt_crofton_segment *sample =
&worker->local_segments[worker->local_segment_count];
struct soltab *in_solid =
partition->pt_inseg->seg_stp;
struct soltab *out_solid =
partition->pt_outseg->seg_stp;
VJOIN1(sample->in_point, ap->a_ray.r_pt,
partition->pt_inhit->hit_dist, ap->a_ray.r_dir);
VJOIN1(sample->out_point, ap->a_ray.r_pt,
partition->pt_outhit->hit_dist, ap->a_ray.r_dir);
RT_HIT_NORMAL(sample->in_normal, partition->pt_inhit,
in_solid, &ap->a_ray, partition->pt_inflip);
RT_HIT_NORMAL(sample->out_normal, partition->pt_outhit,
out_solid, &ap->a_ray, partition->pt_outflip);
sample->thickness = thickness;
sample->ray_id = worker->rays[worker->current_ray].id;
if (!VNEAR_ZERO(sample->in_normal, VUNITIZE_TOL) &&
!VNEAR_ZERO(sample->out_normal, VUNITIZE_TOL))
worker->local_segment_count++;
}
}
wd->local_crossings += crossings;
wd->local_chord += chord;
wd->local_rays += 1;
worker->local_crossings += crossings;
worker->local_chord += chord;
worker->local_rays++;
return 1;
}
static int
crofton_miss(struct application *ap)
{
@@ -193,159 +256,273 @@ crofton_miss(struct application *ap)
}
/* ------------------------------------------------------------------ */
/* Parallel worker */
/* ------------------------------------------------------------------ */
static void
crofton_worker(int id, void *data)
{
struct crofton_worker_data *wd = &((struct crofton_worker_data *)data)[id - 1];
struct application *ap = wd->ap;
struct crofton_ray *rays = wd->rays;
for (size_t i = wd->start; i < wd->end; i++) {
VMOVE(ap->a_ray.r_pt, rays[i].r_pt);
VMOVE(ap->a_ray.r_dir, rays[i].r_dir);
/* r_dir is already unit-length (set during ray generation) */
rt_shootray(ap);
}
}
/* ------------------------------------------------------------------ */
/* Point / ray generation */
/* ------------------------------------------------------------------ */
static double
crofton_rand01(std::mt19937_64 &rng)
crofton_random01(std::mt19937_64 &rng)
{
return std::generate_canonical<double, 53>(rng);
return static_cast<double>(rng() >> CROFTON_RANDOM_SHIFT) *
CROFTON_RANDOM_SCALE;
}
static void
random_point_on_sphere(double radius, const point_t center, point_t out, std::mt19937_64 &rng)
random_point_on_sphere(double radius, const point_t center, double u,
double v, point_t point)
{
double theta = 2.0 * M_PI * crofton_rand01(rng);
double phi = acos(2.0 * crofton_rand01(rng) - 1.0);
double sp = sin(phi);
out[X] = center[X] + radius * sp * cos(theta);
out[Y] = center[Y] + radius * sp * sin(theta);
out[Z] = center[Z] + radius * cos(phi);
double z = 1.0 - 2.0 * u;
double radial = sqrt(std::max(0.0, 1.0 - z * z));
double angle = 2.0 * M_PI * v;
point[X] = center[X] + radius * radial * cos(angle);
point[Y] = center[Y] + radius * radial * sin(angle);
point[Z] = center[Z] + radius * z;
}
/**
* Generate @p nrays chord rays from 2*nrays random points on the
* bounding sphere. Each ray connects one randomly chosen point to
* another.
* Generate independent random chord endpoints. Pairing consecutive IID
* sphere points has the same distribution as shuffling a point array before
* pairing, but consumes exactly four random values per ray. That fixed
* stride lets callers continue the deterministic stream with ray_offset.
*/
static void
generate_rays(struct crofton_ray *rays, size_t nrays,
double radius, const point_t center, std::mt19937_64 &rng)
generate_rays(struct crofton_ray *rays, size_t ray_count, size_t first_id,
double radius, const point_t center, std::mt19937_64 &rng)
{
size_t npts = nrays * 2;
point_t *pts = (point_t *)bu_calloc(npts, sizeof(point_t), "crofton pts");
for (size_t i = 0; i < npts; i++)
random_point_on_sphere(radius, center, pts[i], rng);
/* Shuffle so pairing is random */
for (size_t i = npts - 1; i > 0; i--) {
size_t j = (size_t)(crofton_rand01(rng) * (i + 1));
if (j > i) j = i;
point_t tmp;
VMOVE(tmp, pts[i]);
VMOVE(pts[i], pts[j]);
VMOVE(pts[j], tmp);
for (size_t i = 0; i < ray_count; i++) {
point_t endpoint;
random_point_on_sphere(radius, center, crofton_random01(rng),
crofton_random01(rng), rays[i].r_pt);
random_point_on_sphere(radius, center, crofton_random01(rng),
crofton_random01(rng), endpoint);
VSUB2(rays[i].r_dir, endpoint, rays[i].r_pt);
VUNITIZE(rays[i].r_dir);
rays[i].id = first_id + i;
}
for (size_t i = 0; i < nrays; i++) {
VMOVE(rays[i].r_pt, pts[i * 2]);
VSUB2(rays[i].r_dir, pts[i * 2 + 1], pts[i * 2]);
VUNITIZE(rays[i].r_dir);
}
bu_free(pts, "crofton pts");
}
/* ------------------------------------------------------------------ */
/* Reusable parallel workers */
/* ------------------------------------------------------------------ */
class CroftonWorkerPool
{
public:
CroftonWorkerPool(const struct application *ap_template,
struct resource *resources, size_t worker_count) :
count(worker_count), data(worker_count)
{
for (size_t i = 0; i < count; i++) {
data[i].ap = *ap_template;
data[i].ap.a_resource = &resources[i];
data[i].shared = NULL;
data[i].ap.a_uptr = &data[i];
}
try {
executor = std::thread(&CroftonWorkerPool::run_workers, this);
} catch (...) {
free_worker_points();
throw;
}
std::unique_lock<std::mutex> lock(mutex);
if (!workers_started.wait_for(lock, worker_start_timeout,
[this]() { return registered == count; })) {
stopping = true;
work_ready.notify_all();
lock.unlock();
executor.join();
free_worker_points();
throw std::runtime_error("worker startup timed out");
}
}
~CroftonWorkerPool()
{
stop_and_join();
free_worker_points();
}
CroftonWorkerPool(const CroftonWorkerPool &) = delete;
CroftonWorkerPool &operator=(const CroftonWorkerPool &) = delete;
void run(struct crofton_ray *rays, size_t nrays,
struct crofton_shared *shared)
{
const size_t rays_per_worker = nrays / count;
std::unique_lock<std::mutex> lock(mutex);
for (size_t i = 0; i < count; i++) {
struct crofton_worker_data &wd = data[i];
wd.rays = rays;
wd.start = i * rays_per_worker;
wd.end = (i == count - 1) ? nrays : (i + 1) * rays_per_worker;
wd.shared = shared;
wd.local_crossings = 0;
wd.local_chord = 0.0;
wd.local_rays = 0;
wd.local_point_count = 0;
wd.local_segment_count = 0;
}
completed = 0;
generation++;
work_ready.notify_all();
work_done.wait(lock, [this]() { return completed == count; });
}
const std::vector<struct crofton_worker_data> &worker_data() const
{
return data;
}
private:
/* Native thread creation should complete promptly. A bounded startup wait
* turns partial bu_parallel startup into a reported failure, not a hang. */
static constexpr std::chrono::seconds worker_start_timeout{5};
static void worker_entry(int UNUSED(id), void *context)
{
CroftonWorkerPool *pool = static_cast<CroftonWorkerPool *>(context);
size_t slot = 0;
{
std::lock_guard<std::mutex> lock(pool->mutex);
slot = pool->registered++;
if (pool->registered == pool->count)
pool->workers_started.notify_one();
}
pool->worker(slot);
}
void run_workers()
{
bu_parallel(worker_entry, count, this);
}
void worker(size_t slot)
{
size_t observed_generation = 0;
std::unique_lock<std::mutex> lock(mutex);
while (true) {
work_ready.wait(lock, [this, observed_generation]() {
return stopping || generation != observed_generation;
});
if (stopping)
return;
observed_generation = generation;
struct crofton_worker_data *wd = &data[slot];
lock.unlock();
for (size_t i = wd->start; i < wd->end; i++) {
wd->current_ray = i;
VMOVE(wd->ap.a_ray.r_pt, wd->rays[i].r_pt);
VMOVE(wd->ap.a_ray.r_dir, wd->rays[i].r_dir);
rt_shootray(&wd->ap);
}
lock.lock();
completed++;
if (completed == count)
work_done.notify_one();
}
}
void stop_and_join()
{
{
std::lock_guard<std::mutex> lock(mutex);
stopping = true;
work_ready.notify_all();
}
if (executor.joinable())
executor.join();
}
void free_worker_points()
{
for (struct crofton_worker_data &wd : data) {
if (wd.local_points)
bu_free(wd.local_points, "Crofton worker hit points");
if (wd.local_segments)
bu_free(wd.local_segments, "Crofton worker segments");
wd.local_points = NULL;
wd.local_segments = NULL;
}
}
size_t count;
std::vector<struct crofton_worker_data> data;
std::thread executor;
std::mutex mutex;
std::condition_variable workers_started;
std::condition_variable work_ready;
std::condition_variable work_done;
size_t registered = 0;
size_t generation = 0;
size_t completed = 0;
bool stopping = false;
};
/* ------------------------------------------------------------------ */
/* One iteration: fire nrays, accumulate into shared */
/* ------------------------------------------------------------------ */
static void
do_one_iteration(struct application *ap_template,
struct resource *resources,
size_t nrays,
double radius,
const point_t center,
struct crofton_shared *shared,
std::mt19937_64 &rng)
do_one_iteration(CroftonWorkerPool &pool, size_t ray_count, double radius,
const point_t center, struct crofton_shared *shared,
std::mt19937_64 &rng)
{
struct crofton_ray *rays = (struct crofton_ray *)bu_calloc(
nrays, sizeof(struct crofton_ray), "crofton rays");
ray_count, sizeof(struct crofton_ray), "Crofton rays");
generate_rays(rays, ray_count, shared->ray_offset + shared->total_rays,
radius, center, rng);
pool.run(rays, ray_count, shared);
generate_rays(rays, nrays, radius, center, rng);
for (const struct crofton_worker_data &worker : pool.worker_data()) {
shared->total_crossings += worker.local_crossings;
shared->total_chord += worker.local_chord;
shared->total_rays += worker.local_rays;
size_t ncpus = bu_avail_cpus();
if (ncpus < 1) ncpus = 1;
if (ncpus > MAX_PSW) ncpus = MAX_PSW;
if (worker.local_point_count) {
const size_t needed =
shared->point_count + worker.local_point_count;
if (needed > shared->point_capacity) {
size_t new_capacity = shared->point_capacity ?
2 * shared->point_capacity : needed;
if (new_capacity < needed)
new_capacity = needed;
shared->points = (point_t *)bu_realloc(shared->points,
new_capacity * sizeof(point_t),
"Crofton hit points");
shared->point_capacity = new_capacity;
}
memcpy(&shared->points[shared->point_count],
worker.local_points,
worker.local_point_count * sizeof(point_t));
shared->point_count = needed;
}
struct crofton_worker_data *wdata = (struct crofton_worker_data *)bu_calloc(
ncpus, sizeof(struct crofton_worker_data), "crofton wdata");
size_t per_cpu = nrays / ncpus;
for (size_t i = 0; i < ncpus; i++) {
struct application *a = (struct application *)bu_calloc(
1, sizeof(struct application), "crofton app");
*a = *ap_template; /* struct copy */
a->a_resource = &resources[i];
struct crofton_worker_data *wd = &wdata[i];
wd->ap = a;
wd->rays = rays;
wd->start = i * per_cpu;
wd->end = (i == ncpus - 1) ? nrays : (i + 1) * per_cpu;
wd->shared = shared;
a->a_uptr = wd;
if (worker.local_segment_count) {
const size_t needed =
shared->segment_count + worker.local_segment_count;
if (needed > shared->segment_capacity) {
size_t new_capacity = shared->segment_capacity ?
2 * shared->segment_capacity : needed;
if (new_capacity < needed)
new_capacity = needed;
shared->segments =
(struct rt_crofton_segment *)bu_realloc(
shared->segments,
new_capacity * sizeof(struct rt_crofton_segment),
"Crofton segments");
shared->segment_capacity = new_capacity;
}
memcpy(&shared->segments[shared->segment_count],
worker.local_segments,
worker.local_segment_count *
sizeof(struct rt_crofton_segment));
shared->segment_count = needed;
}
}
bu_parallel(crofton_worker, (int)ncpus, (void *)wdata);
for (size_t i = 0; i < ncpus; i++) {
shared->total_crossings += wdata[i].local_crossings;
shared->total_chord += wdata[i].local_chord;
shared->total_rays += wdata[i].local_rays;
if (wdata[i].local_point_count) {
const size_t needed = shared->point_count + wdata[i].local_point_count;
if (needed > shared->point_capacity) {
size_t new_capacity = shared->point_capacity ?
2 * shared->point_capacity : needed;
if (new_capacity < needed)
new_capacity = needed;
shared->points = (point_t *)bu_realloc(shared->points,
new_capacity * sizeof(point_t), "Crofton hit points");
shared->point_capacity = new_capacity;
}
memcpy(&shared->points[shared->point_count], wdata[i].local_points,
wdata[i].local_point_count * sizeof(point_t));
shared->point_count = needed;
}
}
for (size_t i = 0; i < ncpus; i++) {
if (wdata[i].local_points)
bu_free(wdata[i].local_points, "Crofton worker hit points");
bu_free(wdata[i].ap, "crofton app");
}
bu_free(wdata, "crofton wdata");
bu_free(rays, "crofton rays");
bu_free(rays, "Crofton rays");
}
/* ------------------------------------------------------------------ */
/* Public API: rt_crofton_shoot */
/* ------------------------------------------------------------------ */
@@ -375,7 +552,8 @@ do_one_iteration(struct application *ap_template,
* sampling (>= 0) on success; -1 on bad arguments.
*/
static int
crofton_shoot_impl(point_t **out_points,
crofton_shoot_impl(struct rt_crofton_result *out_result,
point_t **out_points,
size_t *out_point_count,
double *out_surf_area,
double *out_volume,
@@ -384,13 +562,22 @@ crofton_shoot_impl(point_t **out_points,
point_t out_obb[8],
struct rt_i *rtip,
const struct rt_crofton_params *params,
size_t ray_offset,
const fastf_t *bbox_min,
const fastf_t *bbox_max)
{
if (!rtip || (!out_surf_area && !out_volume && !out_aabb_min && !out_obb && !out_points) ||
((out_points == NULL) != (out_point_count == NULL)) ||
((out_aabb_min == NULL) != (out_aabb_max == NULL)))
return -1;
if (!rtip ||
(!out_result && !out_surf_area && !out_volume && !out_aabb_min &&
!out_obb && !out_points) ||
((out_points == NULL) != (out_point_count == NULL)) ||
((out_aabb_min == NULL) != (out_aabb_max == NULL)) ||
(out_result && out_result->segments) ||
ray_offset > UINT64_MAX / RT_CROFTON_RANDOM_VALUES_PER_RAY)
return -1;
if (out_result) {
struct rt_crofton_result empty = RT_CROFTON_RESULT_INIT;
*out_result = empty;
}
if (out_points) {
*out_points = NULL;
*out_point_count = 0;
@@ -473,10 +660,14 @@ crofton_shoot_impl(point_t **out_points,
if (!use_default && max_rays > 0 && stability_mm <= 0.0 && time_ms <= 0.0)
batch = max_rays; /* single-iteration mode */
size_t ncpus = bu_avail_cpus();
if (ncpus < 1) ncpus = 1;
if (ncpus > MAX_PSW) ncpus = MAX_PSW;
/* ---- Initialize per-CPU resources ---- */
struct resource *resources = (struct resource *)bu_calloc(
MAX_PSW, sizeof(struct resource), "crofton resources");
for (int i = 0; i < MAX_PSW; i++)
ncpus, sizeof(struct resource), "crofton resources");
for (size_t i = 0; i < ncpus; i++)
rt_init_resource(&resources[i], i, rtip);
/* ---- Set up application template ---- */
@@ -494,10 +685,32 @@ crofton_shoot_impl(point_t **out_points,
/* ---- Shared accumulator ---- */
struct crofton_shared shared;
memset(&shared, 0, sizeof(shared));
shared.collect_points = out_points != NULL || out_aabb_min != NULL || out_obb != NULL;
const bool stabilize_surface = out_surf_area != NULL || shared.collect_points;
shared.ray_offset = ray_offset;
shared.collect_points =
out_points != NULL || out_aabb_min != NULL || out_obb != NULL;
shared.collect_segments = out_result != NULL;
const bool stabilize_surface =
out_surf_area != NULL || out_result != NULL ||
shared.collect_points;
CroftonWorkerPool *pool = NULL;
try {
pool = new CroftonWorkerPool(&ap, resources, ncpus);
} catch (const std::exception &e) {
bu_log("rt_crofton: unable to start worker pool: %s\n", e.what());
for (size_t i = 0; i < ncpus; i++) {
if (resources[i].re_magic == RESOURCE_MAGIC) {
rt_clean_resource_basic(rtip, &resources[i]);
BU_PTBL_SET(&rtip->rti_resources, i, NULL);
}
}
bu_free(resources, "crofton resources");
return -1;
}
std::mt19937_64 rng(RT_CROFTON_RNG_SEED);
rng.discard(static_cast<uint64_t>(ray_offset) *
RT_CROFTON_RANDOM_VALUES_PER_RAY);
const double FOUR_PI = 4.0 * M_PI;
const double PI = M_PI;
@@ -521,7 +734,7 @@ crofton_shoot_impl(point_t **out_points,
curr_rays = batch;
}
do_one_iteration(&ap, resources, curr_rays, R, center, &shared, rng);
do_one_iteration(*pool, curr_rays, R, center, &shared, rng);
iteration++;
if (shared.total_rays == 0) break;
@@ -562,9 +775,12 @@ crofton_shoot_impl(point_t **out_points,
} else {
/* ---- Parametric loop: n_rays / stability_mm / time_ms ---- */
double prev_r_sa = -1.0, prev_r_v = -1.0;
double prev_sa = -1.0, prev_v = -1.0;
size_t total_fired = 0;
size_t stable_windows = 0;
int64_t t0 = (time_ms > 0.0) ? bu_gettime() : 0;
const bool use_relative_stability = out_result && time_ms > 0.0 &&
stability_mm <= 0.0;
for (;;) {
/* Time-budget check before firing */
@@ -581,7 +797,7 @@ crofton_shoot_impl(point_t **out_points,
if (fire > remaining) fire = remaining;
}
do_one_iteration(&ap, resources, fire, R, center, &shared, rng);
do_one_iteration(*pool, fire, R, center, &shared, rng);
total_fired += fire;
if (shared.total_rays == 0) break;
@@ -594,27 +810,39 @@ crofton_shoot_impl(point_t **out_points,
/ (double)shared.total_rays;
/* Stability check */
if (stability_mm > 0.0 &&
if ((stability_mm > 0.0 || use_relative_stability) &&
shared.total_rays >= RT_CROFTON_STABILITY_MIN_RAYS &&
shared.total_crossings >= RT_CROFTON_STABILITY_MIN_CROSSINGS) {
double r_sa = (curr_est_sa > 0.0) ? sqrt(curr_est_sa * INV_4PI) : 0.0;
double r_v = (curr_est_v > 0.0) ? cbrt(curr_est_v * INV_4PI3) : 0.0;
if (prev_r_sa >= 0.0) {
int sa_ok = (!stabilize_surface) ||
fabs(r_sa - prev_r_sa) < stability_mm;
int v_ok = (!out_volume) ||
fabs(r_v - prev_r_v) < stability_mm;
if (sa_ok && v_ok) {
stable_windows++;
if (stable_windows >= RT_CROFTON_STABILITY_MIN_WINDOWS)
break;
} else {
stable_windows = 0;
}
bool sa_ok = false;
bool v_ok = false;
if (use_relative_stability) {
sa_ok = !stabilize_surface || (prev_sa > 0.0 &&
fabs(curr_est_sa - prev_sa) / prev_sa * 100.0 <=
RT_CROFTON_DEFAULT_THRESHOLD);
v_ok = !out_volume || (prev_v > 0.0 &&
fabs(curr_est_v - prev_v) / prev_v * 100.0 <=
RT_CROFTON_DEFAULT_THRESHOLD);
prev_sa = curr_est_sa;
prev_v = curr_est_v;
} else {
double r_sa = (curr_est_sa > 0.0) ?
sqrt(curr_est_sa * INV_4PI) : 0.0;
double r_v = (curr_est_v > 0.0) ?
cbrt(curr_est_v * INV_4PI3) : 0.0;
sa_ok = !stabilize_surface || (prev_r_sa >= 0.0 &&
fabs(r_sa - prev_r_sa) < stability_mm);
v_ok = !out_volume || (prev_r_v >= 0.0 &&
fabs(r_v - prev_r_v) < stability_mm);
prev_r_sa = r_sa;
prev_r_v = r_v;
}
if (sa_ok && v_ok) {
stable_windows++;
if (stable_windows >= RT_CROFTON_STABILITY_MIN_WINDOWS)
break;
} else {
stable_windows = 0;
}
prev_r_sa = r_sa;
prev_r_v = r_v;
}
/* Time-budget check after firing */
@@ -624,8 +852,16 @@ crofton_shoot_impl(point_t **out_points,
}
}
if (out_surf_area) *out_surf_area = curr_est_sa;
if (out_volume) *out_volume = curr_est_v;
if (out_surf_area)
*out_surf_area = curr_est_sa;
if (out_volume)
*out_volume = curr_est_v;
if (out_result) {
out_result->surface_area = curr_est_sa;
out_result->volume = curr_est_v;
out_result->ray_count = shared.total_rays;
out_result->crossing_count = shared.total_crossings;
}
if (out_aabb_min) {
for (size_t i = 0; i < shared.point_count; ++i)
VMINMAX(*out_aabb_min, *out_aabb_max, shared.points[i]);
@@ -641,11 +877,19 @@ crofton_shoot_impl(point_t **out_points,
bounds_ret = -1;
}
if (out_points && !bounds_ret) {
*out_points = shared.points;
*out_point_count = shared.point_count;
*out_points = shared.points;
*out_point_count = shared.point_count;
} else if (shared.points) {
bu_free(shared.points, "Crofton hit points");
bu_free(shared.points, "Crofton hit points");
}
if (out_result && !bounds_ret) {
out_result->segments = shared.segments;
out_result->segment_count = shared.segment_count;
} else if (shared.segments) {
bu_free(shared.segments, "Crofton segments");
}
delete pool;
/* Clean each resource and NULL out its slot in rtip->rti_resources.
* This is necessary because crofton_from_ip calls rt_i_destroy(rtip)
@@ -657,7 +901,7 @@ crofton_shoot_impl(point_t **out_points,
*
* By setting the slot to NULL we let rt_i_destroy's cleanup skip it,
* and then we can safely bu_free the resources array. */
for (int i = 0; i < MAX_PSW; i++) {
for (size_t i = 0; i < ncpus; i++) {
if (resources[i].re_magic == RESOURCE_MAGIC) {
rt_clean_resource_basic(rtip, &resources[i]);
BU_PTBL_SET(&rtip->rti_resources, i, NULL);
@@ -681,9 +925,31 @@ rt_crofton_shoot(double *out_surf_area, double *out_volume,
const struct rt_crofton_params *params,
const fastf_t *bbox_min, const fastf_t *bbox_max)
{
return crofton_shoot_impl(out_points, out_point_count, out_surf_area,
out_volume, out_aabb_min, out_aabb_max, out_obb, rtip, params,
bbox_min, bbox_max);
return crofton_shoot_impl(NULL, out_points, out_point_count,
out_surf_area, out_volume, out_aabb_min, out_aabb_max, out_obb,
rtip, params, 0, bbox_min, bbox_max);
}
int
rt_crofton_collect(struct rt_crofton_result *result, struct rt_i *rtip,
const struct rt_crofton_params *params, size_t ray_offset,
const fastf_t *bbox_min, const fastf_t *bbox_max)
{
if (!result)
return -1;
return crofton_shoot_impl(result, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, rtip, params, ray_offset, bbox_min, bbox_max);
}
void
rt_crofton_result_free(struct rt_crofton_result *result)
{
if (!result)
return;
if (result->segments)
bu_free(result->segments, "Crofton segments");
struct rt_crofton_result empty = RT_CROFTON_RESULT_INIT;
*result = empty;
}
@@ -821,7 +1087,7 @@ crofton_from_ip_n(const struct rt_db_internal *ip,
double sa = 0.0;
double vol = 0.0;
(void)rt_crofton_shoot(&sa, &vol, NULL, NULL, NULL, NULL, NULL,
rtip, params, NULL, NULL);
rtip, params, NULL, NULL);
if (out_sa) *out_sa = sa;
if (out_vol) *out_vol = vol;
+9 -59
View File
@@ -1543,8 +1543,7 @@ permute_cell(point_t A,
struct dsp_specific *dsp,
struct dsp_rpp *dsp_rpp)
{
int x, y;
point_t tmp;
#ifdef FULL_DSP_DEBUGGING
if (RT_G_DEBUG & RT_DEBUG_HF) {
@@ -1558,10 +1557,8 @@ permute_cell(point_t A,
switch (dsp->dsp_i.dsp_cuttype) {
case DSP_CUT_DIR_llUR:
return DSP_CUT_DIR_llUR;
break;
case DSP_CUT_DIR_ADAPT: {
point_t tmp;
size_t cx = (size_t)dsp_rpp->dsp_min[X];
size_t cy = (size_t)dsp_rpp->dsp_min[Y];
@@ -1570,28 +1567,8 @@ permute_cell(point_t A,
dsp_rpp->dsp_min[X],
dsp_rpp->dsp_min[Y]);
/*
* We look at the points in the diagonal next cells to
* determine the curvature along each diagonal of this
* cell. This cell is divided into two triangles by
* cutting across the cell in the direction of least
* curvature.
*
* * * * *
* \ /
* \C D/
* * *--* *
* |\/|
* |/\|
* * *--* *
* /A B\
* / \
* * * * *
*/
/* leaf-cell precondition: the rpp spans exactly one grid unit, so
* (cx, cy) plus the cell counts fully describe the curvature
* stencil that rt_dsp_cell_cut() recomputes.
/* The rpp spans one grid cell; use its indices to select the
* lower-curvature diagonal.
*/
BU_ASSERT(dsp_rpp->dsp_max[X] == dsp_rpp->dsp_min[X] + 1);
BU_ASSERT(dsp_rpp->dsp_max[Y] == dsp_rpp->dsp_min[Y] + 1);
@@ -1599,15 +1576,16 @@ permute_cell(point_t A,
if (rt_dsp_cell_cut(&dsp->dsp_i, cx, cy,
(size_t)dsp->xsiz, (size_t)dsp->ysiz)
== DSP_CUT_DIR_llUR) {
/* A-D cut is fine, no need to permute */
if (RT_G_DEBUG & RT_DEBUG_HF)
bu_log("A-D cut\n");
return DSP_CUT_DIR_llUR;
}
/* prefer the B-C cut */
}
/* fall through */
case DSP_CUT_DIR_ULlr:
/* Both B-C modes need the same corner order. The top triangles
* must keep outward (+Z) normals for the entry/exit state machine.
*/
VMOVE(tmp, A);
VMOVE(A, B);
VMOVE(B, D);
@@ -1615,35 +1593,7 @@ permute_cell(point_t A,
VMOVE(C, tmp);
if (RT_G_DEBUG & RT_DEBUG_HF)
bu_log("B-C cut\n");
return DSP_CUT_DIR_ULlr;
break;
}
case DSP_CUT_DIR_ULlr:
/* assign the values for the corner points
*
* D----C
* | |
* | |
* | |
* B----A
*/
x = dsp_rpp->dsp_min[X];
y = dsp_rpp->dsp_min[Y];
VSET(B, x, y, DSP(&dsp->dsp_i, x, y));
x = dsp_rpp->dsp_max[X];
VSET(A, x, y, DSP(&dsp->dsp_i, x, y));
y = dsp_rpp->dsp_max[Y];
VSET(C, x, y, DSP(&dsp->dsp_i, x, y));
x = dsp_rpp->dsp_min[X];
VSET(D, x, y, DSP(&dsp->dsp_i, x, y));
return DSP_CUT_DIR_ULlr;
break;
}
bu_log("%s:%d Unknown DSP cut direction: %d\n",
__FILE__, __LINE__, dsp->dsp_i.dsp_cuttype);
+108
View File
@@ -43,6 +43,18 @@ rel_err(double estimated, double exact)
}
static int
crofton_segments_equal(const struct rt_crofton_segment *left,
const struct rt_crofton_segment *right)
{
return left->ray_id == right->ray_id &&
NEAR_EQUAL(left->thickness, right->thickness, SMALL_FASTF) &&
VNEAR_EQUAL(left->in_point, right->in_point, SMALL_FASTF) &&
VNEAR_EQUAL(left->in_normal, right->in_normal, SMALL_FASTF) &&
VNEAR_EQUAL(left->out_point, right->out_point, SMALL_FASTF) &&
VNEAR_EQUAL(left->out_normal, right->out_normal, SMALL_FASTF);
}
static int
verify_crofton_estimates(void)
{
@@ -379,6 +391,102 @@ run_convergence_case(struct db_i *dbip,
&sample_points, &sample_count,
rtip, &p, NULL, NULL);
run_sec = (double)(bu_gettime() - t0) / 1000000.0;
if (i == 0) {
struct rt_crofton_result samples = RT_CROFTON_RESULT_INIT;
const size_t ray_offset = 4096u;
const size_t continuation_rays = 256u;
struct rt_crofton_params sample_params =
{ray_offset + continuation_rays, 0.0, 0.0};
int sample_ret = rt_crofton_collect(&samples, rtip,
&sample_params, 0, NULL, NULL);
int full_samples_valid = sample_ret > 0 && samples.segments &&
samples.segment_count * 2 == samples.crossing_count &&
samples.ray_count == sample_params.n_rays;
if (!full_samples_valid) {
printf(" %-24s invalid structured Crofton output\\n", label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < samples.segment_count; sample_index++) {
struct rt_crofton_segment *segment =
&samples.segments[sample_index];
if (segment->thickness <= 0.0 ||
!NEAR_EQUAL(DIST_PNT_PNT(segment->in_point,
segment->out_point), segment->thickness, RT_LEN_TOL) ||
!NEAR_EQUAL(MAGNITUDE(segment->in_normal), 1.0,
VUNITIZE_TOL) ||
!NEAR_EQUAL(MAGNITUDE(segment->out_normal), 1.0,
VUNITIZE_TOL) ||
segment->ray_id >= samples.ray_count) {
printf(" %-24s invalid structured segment\\n", label);
failures++;
break;
}
}
}
struct rt_crofton_result offset_samples =
RT_CROFTON_RESULT_INIT;
struct rt_crofton_params offset_params =
{continuation_rays, 0.0, 0.0};
sample_ret = rt_crofton_collect(&offset_samples, rtip,
&offset_params, ray_offset, NULL, NULL);
int offset_samples_valid = sample_ret > 0 &&
offset_samples.ray_count == offset_params.n_rays;
if (!offset_samples_valid) {
printf(" %-24s invalid offset Crofton output\\n", label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < offset_samples.segment_count;
sample_index++) {
size_t ray_id = offset_samples.segments[sample_index].ray_id;
if (ray_id < ray_offset ||
ray_id >= ray_offset + offset_params.n_rays) {
printf(" %-24s invalid offset ray identifier\\n",
label);
failures++;
break;
}
}
}
if (full_samples_valid && offset_samples_valid) {
size_t suffix_start = 0;
while (suffix_start < samples.segment_count &&
samples.segments[suffix_start].ray_id < ray_offset)
suffix_start++;
size_t suffix_count = samples.segment_count - suffix_start;
if (suffix_count != offset_samples.segment_count) {
printf(" %-24s random stream continuation count mismatch\\n",
label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < suffix_count; sample_index++) {
const struct rt_crofton_segment *full_segment =
&samples.segments[suffix_start + sample_index];
const struct rt_crofton_segment *offset_segment =
&offset_samples.segments[sample_index];
if (!crofton_segments_equal(full_segment, offset_segment)) {
printf(" %-24s random stream continuation mismatch\\n",
label);
failures++;
break;
}
}
}
}
rt_crofton_result_free(&samples);
if (samples.segments || samples.segment_count)
failures++;
rt_crofton_result_free(&offset_samples);
if (offset_samples.segments || offset_samples.segment_count)
failures++;
}
rt_i_destroy(rtip);
if (elapsed_total_sec)
+36 -21
View File
@@ -414,13 +414,20 @@ flat_dsp_analytic(uint32_t xcnt, uint32_t ycnt, unsigned short h,
/* Build an in-memory DB, prep, and run comparison */
/* ------------------------------------------------------------------ */
enum dsp_ray_check_mode {
DSP_RAY_CHECK_NONE,
DSP_RAY_CHECK_FLAT,
DSP_RAY_CHECK_SMOOTH,
DSP_RAY_CHECK_BC_CUT
};
struct dsp_case {
const char *label;
uint32_t xcnt, ycnt;
const unsigned short *buf;
int cuttype;
int smooth;
int ray_checks;
enum dsp_ray_check_mode ray_checks;
/* stom diagonal (identity: dx=dy=dz=1): */
double dx, dy, dz;
/* analytic SA/vol (-1 if not available): */
@@ -550,7 +557,7 @@ check_smooth_normal(const struct dsp_case *tc, struct soltab *stp)
static int
run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
{
if (!tc->ray_checks)
if (tc->ray_checks == DSP_RAY_CHECK_NONE)
return 0;
int failures = 0;
@@ -562,16 +569,14 @@ run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
double x_extent = (tc->xcnt - 1) * tc->dx;
double y_extent = (tc->ycnt - 1) * tc->dy;
/* ray_checks == 1 is reserved for flat DSP cases, so buf[0] is the
* expected terrain height for the deterministic vertical/side rays.
*/
double top_z = (double)tc->buf[0] * tc->dz;
double expected_vertical_in = 10.0;
double expected_vertical_out = expected_vertical_in + top_z;
point_t o;
vect_t d;
if (tc->ray_checks == 1) {
if (tc->ray_checks == DSP_RAY_CHECK_FLAT) {
/* Flat cases use buf[0] as the terrain height for direct rays. */
double top_z = (double)tc->buf[0] * tc->dz;
double expected_vertical_in = 10.0;
double expected_vertical_out = expected_vertical_in + top_z;
VSET(o, 0.5 * x_extent, 0.5 * y_extent, top_z + 10.0);
VSET(d, 0.0, 0.0, -1.0);
failures += check_direct_ray("vertical down flat DSP", stp, rtip,
@@ -598,12 +603,22 @@ run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
failures += check_direct_ray("vertical up flat DSP", stp, rtip,
o, d, 1, expected_vertical_in,
expected_vertical_out, 0);
} else if (tc->ray_checks == 2) {
} else if (tc->ray_checks == DSP_RAY_CHECK_SMOOTH) {
VSET(o, 0.5 * x_extent, 0.5 * y_extent, 10000.0);
VSET(d, 0.0, 0.0, -1.0);
failures += check_direct_ray("smooth-mode direct vertical shot", stp, rtip,
o, d, 1, -1.0, -1.0, 0);
failures += check_smooth_normal(tc, stp);
} else if (tc->ray_checks == DSP_RAY_CHECK_BC_CUT) {
/* In cell (3,3), the B-C triangle rises from 315 at y=3.25 by
* 68 per x. A horizontal ray at z=330 enters at x=3+15/68
* and exits through the outer wall at x=4. This also checks
* that the explicit B-C cut gives the top an outward normal.
*/
VSET(o, 2.5, 3.25, 330.0);
VSET(d, 1.0, 0.0, 0.0);
failures += check_direct_ray("explicit B-C top-to-side shot", stp, rtip,
o, d, 1, 0.5 + 15.0 / 68.0, 1.5, 1);
}
return failures;
@@ -782,7 +797,7 @@ test_synthetic(void)
struct dsp_case tc = {
"flat 5x5 h=100 (4x4 cells, identity stom)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 1, 1.0, 1.0, 1.0, asa, avol
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_FLAT, 1.0, 1.0, 1.0, asa, avol
};
failures += run_inmem_case(&tc);
}
@@ -798,7 +813,7 @@ test_synthetic(void)
struct dsp_case tc = {
"flat 10x10 h=200 (9x9 cells)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, asa, avol
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, asa, avol
};
failures += run_inmem_case(&tc);
}
@@ -814,7 +829,7 @@ test_synthetic(void)
struct dsp_case tc = {
"flat 10x10 h=50 (stom dx=5 dy=5 dz=2)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 1, 5.0, 5.0, 2.0, asa, avol
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_FLAT, 5.0, 5.0, 2.0, asa, avol
};
failures += run_inmem_case(&tc);
}
@@ -829,7 +844,7 @@ test_synthetic(void)
struct dsp_case tc = {
"ramp 9x9 (h=100+20x+10y)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -844,7 +859,7 @@ test_synthetic(void)
struct dsp_case tc = {
"asymmetric 5x5 explicit ULlr cut",
GW, GH, buf, DSP_CUT_DIR_ULlr, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_ULlr, 0, DSP_RAY_CHECK_BC_CUT, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -859,7 +874,7 @@ test_synthetic(void)
struct dsp_case tc = {
"asymmetric 5x5 adaptive cut",
GW, GH, buf, DSP_CUT_DIR_ADAPT, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_ADAPT, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -874,7 +889,7 @@ test_synthetic(void)
struct dsp_case tc = {
"smooth=1 ramp normal check",
GW, GH, buf, DSP_CUT_DIR_llUR, 1, 2, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 1, DSP_RAY_CHECK_SMOOTH, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -889,7 +904,7 @@ test_synthetic(void)
struct dsp_case tc = {
"smooth=2 ramp normal check",
GW, GH, buf, DSP_CUT_DIR_llUR, 2, 2, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 2, DSP_RAY_CHECK_SMOOTH, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -908,7 +923,7 @@ test_synthetic(void)
struct dsp_case tc = {
"sinusoidal 16x16",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -928,7 +943,7 @@ test_synthetic(void)
struct dsp_case tc = {
"complex 33x33 wave",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
bu_free(buf, "33x33 buf");
@@ -971,7 +986,7 @@ test_synthetic(void)
struct dsp_case tc = {
"larger 129x129 mixed ramp/wave terrain",
grid_width, grid_height, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
grid_width, grid_height, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
bu_free(sin_x, "129x129 sin x");
+180 -234
View File
@@ -49,6 +49,7 @@
#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"
@@ -2824,6 +2825,148 @@ 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(&current_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(&current_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(&current_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(&current_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)
{
@@ -2832,6 +2975,35 @@ 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(&current_top->to_gedp->go_name),
@@ -2848,240 +3020,14 @@ to_init_default_bindings(struct bview *gdvp)
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
/* Mouse Bindings */
bu_vls_printf(&bindings, "bind %s <2> {%s vslew %s %%x %%y; focus %s; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_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(&current_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(&current_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(&current_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(&current_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(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name),
bu_vls_addr(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
Tcl_Eval(current_top->to_interp, bu_vls_addr(&bindings));
/* 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(&current_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(&current_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(&current_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(&current_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(&current_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&bindings));
}
}
bu_vls_free(&bindings);
+9 -396
View File
@@ -49,12 +49,8 @@ proc mged_bind_dm { w } {
bind $w <Enter> [list mged_activate_dm $w]
#default mouse bindings
default_mouse_bindings $w
# Load standardized JSON bindings for mged
#default key bindings
set forwarding_key($w) 0
default_key_bindings $w
}
proc print_return_val str {
@@ -64,129 +60,6 @@ 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
@@ -202,274 +75,6 @@ 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
@@ -482,6 +87,14 @@ 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