Compare commits

...
6 Commits
Author SHA1 Message Date
Clifford Yapp 1ab8c379e9 Stash some thoughts about what to do with BoTs. 2026-09-18 10:12:00 -04:00
Clifford Yapp c12e55973a Stash some info gleaned from old reports and manuals. 2026-09-18 10:12:00 -04:00
Clifford Yapp 1968ba02a3 Quick stab at adding some support for pre-GIFT COMGEOM
Only known surviving examples would be fuzzy printouts in old report scans -
would need really good OCR work to get those set up to actually test, so in the
meantime just checkpointing the V0 work to revisit later.
2026-09-18 10:12:00 -04:00
Clifford Yapp 40621be317 Various minor adjustments 2026-09-18 10:12:00 -04:00
Clifford Yapp 92ba4caa42 Use correct type for opt target 2026-09-18 10:12:00 -04:00
Clifford Yapp ef75af075a Initial checkpoint of C++ rework of comgeom-g 2026-09-18 10:12:00 -04:00
14 changed files with 3140 additions and 2 deletions
+4 -2
View File
@@ -17,8 +17,10 @@ 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_SRCS
comgeom-g_old_SRCS
comgeom/cvt.c
comgeom/f2a.c
comgeom/mat.c
@@ -27,7 +29,7 @@ set(
comgeom/solid.c
comgeom/tools.c
)
brlcad_addexec(comgeom-g "${comgeom-g_SRCS}" "libwdb;libbn;libbu;${M_LIBRARY}" FOLDER Conv)
brlcad_addexec(comgeom-g_old "${comgeom-g_old_SRCS}" "libwdb;libbn;libbu;${M_LIBRARY}" FOLDER Conv)
brlcad_addexec(conv-vg2g conv-vg2g.c "libbu" FOLDER Conv)
+28
View File
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,498 @@
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
@@ -0,0 +1,350 @@
# 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
@@ -0,0 +1,186 @@
#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
@@ -0,0 +1,41 @@
#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
@@ -0,0 +1,76 @@
#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
@@ -0,0 +1,38 @@
#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
@@ -0,0 +1,424 @@
#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
@@ -0,0 +1,74 @@
#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
@@ -0,0 +1,830 @@
#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
@@ -0,0 +1,93 @@
#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
@@ -0,0 +1,376 @@
================================================================================
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
@@ -0,0 +1,122 @@
#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();
}