Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ab8c379e9 | ||
|
|
c12e55973a | ||
|
|
1968ba02a3 | ||
|
|
40621be317 | ||
|
|
92ba4caa42 | ||
|
|
ef75af075a |
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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"};
|
||||
}
|
||||
@@ -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
|
||||
================================================================================
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user