Compare commits

..
1 Commits
Author SHA1 Message Date
Clifford Yapp bc4837815c Stash the processing scripts and notes for later 2026-09-18 10:12:20 -04:00
38 changed files with 4901 additions and 2116 deletions
-164
View File
@@ -1,164 +0,0 @@
/* B I N D I N G . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @addtogroup bu_binding
* @brief
* Universal input binding schema and action dispatch.
*/
/** @{ */
/** @file bu/binding.h */
#ifndef BU_BINDING_H
#define BU_BINDING_H
#include "common.h"
#include "bu/defines.h"
#include "bu/vls.h"
__BEGIN_DECLS
/* Standardized Attribute Keys (for W3C validation logic) */
#define BU_BINDING_ATTR_TYPE "type"
#define BU_BINDING_ATTR_KEY "key" /* Values MUST be W3C standard strings */
#define BU_BINDING_ATTR_BUTTON "button" /* Values MUST be W3C button integers */
#define BU_BINDING_ATTR_MODIFIERS "modifiers"
/* Standardized Event Types */
#define BU_BINDING_EVENT_KEY_PRESS "key-press"
#define BU_BINDING_EVENT_KEY_RELEASE "key-release"
#define BU_BINDING_EVENT_MOUSE_PRESS "mouse-press"
#define BU_BINDING_EVENT_MOUSE_RELEASE "mouse-release"
#define BU_BINDING_EVENT_MOUSE_MOTION "mouse-motion"
#define BU_BINDING_EVENT_MOUSE_SCROLL "mouse-scroll"
/**
* @brief Action callback signature
*
* Invoked when an incoming event matches a loaded binding.
* The callback receives the full JSON string of the triggering event.
*
* @param event_json Raw JSON string describing the incoming event
* @param registered_data Opaque pointer passed during registration
* @param event_data Opaque pointer passed during event processing
*/
typedef void (*bu_action_func_t)(const char *event_json, void *registered_data, void *event_data);
/**
* @brief Load JSON bindings from a file
*
* Parses a JSON configuration specifying bindings for various contexts.
* Performs two-phase validation:
* 1. Checks standard fields (like W3C key strings) for correctness.
* 2. Invokes context-specific validation hooks (if registered).
*
* @param json_path Path to the JSON file
* @return 0 on success, <0 on error
*/
BU_EXPORT extern int bu_binding_load(const char *json_path);
/**
* @brief Load JSON bindings from a raw string
*
* Parses a JSON configuration string specifying bindings for various contexts.
* Allows applications to embed overriding bindings natively without external files.
*
* @param json_string Raw JSON string containing bindings
* @return 0 on success, <0 on error
*/
BU_EXPORT extern int bu_binding_load_str(const char *json_string);
/**
* @brief Register a JSON schema for a specific context
*
* Defines the allowed vocabulary for a context using a JSON schema file.
* The schema file is lazily parsed during bu_binding_load().
*
* @param context The interaction context (e.g., "dm", "fb")
* @param json_path Absolute path to the schema JSON file
*/
BU_EXPORT extern void bu_binding_register_schema(const char *context, const char *json_path);
/**
* @brief Augment an existing schema with a custom action
*
* @param context The interaction context (e.g., "dm")
* @param action_name The new abstract action string
* @param description Human-readable description of the action
*/
BU_EXPORT extern void bu_binding_schema_add_action(const char *context, const char *action_name, const char *description);
/**
* @brief Remove a custom action from an existing schema
*
* @param context The interaction context (e.g., "dm")
* @param action_name The abstract action string to remove
*/
BU_EXPORT extern void bu_binding_schema_remove_action(const char *context, const char *action_name);
/**
* @brief Register a C callback for a semantic action
*
* @param action_name The abstract action string (e.g., "fb_close_window")
* @param func The C function to invoke when the action is triggered
* @param client_data Opaque pointer passed to the callback
*/
BU_EXPORT extern void bu_binding_register_action(const char *action_name, bu_action_func_t func, void *client_data);
/**
* @brief Process an incoming native event
*
* Matches the event JSON against the loaded bindings for the given context.
* If a matching action is found and a handler is registered for that action,
* the callback is invoked.
*
* @param context The interaction context (e.g., "view3d", "fb_raw")
* @param event_json A JSON string containing the event attributes
* @param event_data Opaque pointer passed to the callback representing instance state
* @return 1 if an action was triggered and handled, 0 otherwise
*/
BU_EXPORT extern int bu_binding_process_event(const char *context, const char *event_json, void *event_data);
/**
* @brief Check if a specific action is bound in a given context
*
* Use this to verify that an application has provided a binding for an essential action.
*
* @param context The interaction context (e.g., "view3d", "fb_raw")
* @param action_name The abstract action string (e.g., "view_mouse_drag_rotate")
* @return 1 if bound, 0 if not
*/
BU_EXPORT extern int bu_binding_action_is_bound(const char *context, const char *action_name);
__END_DECLS
#endif /* BU_BINDING_H */
/** @} */
/*
* Local Variables:
* mode: C
* tab-width: 8
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
-3
View File
@@ -444,9 +444,6 @@ DM_EXPORT extern int fb_get_interactive(const struct fb *ifp);
*/
DM_EXPORT extern int fb_next_event(struct fb *ifp, struct fb_event *e);
/** Dispatch an application-facing event through the framebuffer bindings. */
DM_EXPORT extern int fb_process_event_bindings(struct fb *ifp, const struct fb_event *e);
/* Library entry points */
-46
View File
@@ -227,32 +227,6 @@ struct rt_crofton_params {
double time_ms; /**< wall-clock time budget (ms); 0 = disabled */
};
/** One solid partition observed by a Crofton ray. */
struct rt_crofton_segment {
point_t in_point;
vect_t in_normal;
point_t out_point;
vect_t out_normal;
fastf_t thickness;
size_t ray_id;
};
/**
* Structured samples and estimates produced by rt_crofton_collect().
* Release @p segments with rt_crofton_result_free().
*/
struct rt_crofton_result {
struct rt_crofton_segment *segments;
size_t segment_count;
size_t ray_count;
size_t crossing_count;
double surface_area;
double volume;
};
/** Initialize an rt_crofton_result before first use. */
#define RT_CROFTON_RESULT_INIT { NULL, 0, 0, 0, 0.0, 0.0 }
/**
* Run the Cauchy-Crofton ray-sampling estimator on an already-prepared
@@ -363,26 +337,6 @@ struct rt_crofton_result {
*/
RT_EXPORT extern int rt_crofton_shoot(double *out_surf_area, double *out_volume, point_t *out_aabb_min, point_t *out_aabb_max, point_t out_obb[8], point_t **out_points, size_t *out_point_count, struct rt_i *rtip, const struct rt_crofton_params *params, const fastf_t *bbox_min, const fastf_t *bbox_max);
/**
* Run Crofton sampling while preserving oriented entry/exit pairs.
*
* @p result must be initialized with RT_CROFTON_RESULT_INIT. On success it
* owns the returned segment array. @p rtip must already be prepared.
* @p ray_offset skips that many samples in the deterministic sequence and
* labels the returned rays starting at that value. This lets successive
* calls use disjoint validation streams.
*/
RT_EXPORT extern int rt_crofton_collect(struct rt_crofton_result *result,
struct rt_i *rtip,
const struct rt_crofton_params *params,
size_t ray_offset,
const fastf_t *bbox_min,
const fastf_t *bbox_max);
/** Release storage owned by a structured Crofton result. */
RT_EXPORT extern void rt_crofton_result_free(
struct rt_crofton_result *result);
/**
* Cauchy-Crofton surface-area and/or volume estimator for a primitive.
+8
View File
@@ -45,6 +45,14 @@ cmakefiles(
asciiquack/vendor/cxxopts.hpp
db2adoc/db2adoc.py
db2adoc/db2adoc.xsl
brep_corpus_audit.py
step_corpus_capture.py
step_corpus_run.py
step_corpus_summary.py
step_corpus_verify.py
step_external_corpus_expectations.json
step_local_external_corpus_expectations.json
step_dependency_subset.py
inactvhdrs/CMakeLists.txt
inactvhdrs/inactvhdrs.cpp
)
+619
View File
@@ -0,0 +1,619 @@
#!/usr/bin/env python3
"""Audit BRep wireframe and fast shaded realizations in a directory of .g files.
The companion ``brep-audit`` executable does one BRep per process. This
driver inventories each database, runs those isolated checks, checkpoints
every result to JSON Lines, and writes JSON and Markdown summaries. Geometry
findings do not make the driver fail: they are the intended work list.
"""
import argparse
import datetime
import json
import os
import re
import signal
import subprocess
import sys
import tempfile
import time
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
FORMAT = "brlcad-brep-corpus-audit-v1"
HELPER_FORMAT = "brlcad-brep-realization-audit-v1"
def file_identity(path):
try:
info = path.stat()
return {"size": info.st_size, "mtime_ns": info.st_mtime_ns}
except OSError:
return None
def text_tail(value, limit=8000):
return (value or "")[-limit:]
def last_phase(stderr):
phases = re.findall(r"^brep-audit: phase=([a-z]+)$", stderr or "", re.MULTILINE)
return phases[-1] if phases else None
def process_rss_bytes(pid):
"""Read current Linux RSS, returning zero when it is unavailable."""
try:
with open("/proc/{}/status".format(pid), "r", encoding="ascii") as stream:
for line in stream:
if line.startswith("VmRSS:"):
return int(line.split()[1]) * 1024
except (OSError, ValueError, IndexError):
pass
return 0
def kill_process(process):
try:
if os.name == "posix":
os.killpg(process.pid, signal.SIGKILL)
else:
process.kill()
except (OSError, ProcessLookupError):
pass
def file_tail(stream, limit):
stream.flush()
size = os.fstat(stream.fileno()).st_size
stream.seek(max(0, size - limit))
return stream.read().decode("utf-8", errors="replace"), size > limit
def run_process(command, timeout, max_rss_mib=0):
"""Run and monitor one helper, retaining enough detail for diagnostics."""
started = time.monotonic()
with tempfile.TemporaryFile() as stdout_file, \
tempfile.TemporaryFile() as stderr_file:
try:
process = subprocess.Popen(
command,
stdout=stdout_file,
stderr=stderr_file,
start_new_session=(os.name == "posix"),
)
except OSError as error:
return {
"returncode": 127,
"stdout": "",
"stdout_truncated": False,
"stderr": str(error),
"elapsed_seconds": time.monotonic() - started,
"observed_peak_rss_bytes": 0,
"termination_reason": "launch_error",
}
peak_rss = 0
termination_reason = None
while True:
elapsed = time.monotonic() - started
remaining = timeout - elapsed
if remaining <= 0:
termination_reason = "timeout"
kill_process(process)
process.wait()
break
try:
process.wait(timeout=min(0.1, remaining))
peak_rss = max(peak_rss, process_rss_bytes(process.pid))
break
except subprocess.TimeoutExpired:
rss = process_rss_bytes(process.pid)
peak_rss = max(peak_rss, rss)
if max_rss_mib and rss > max_rss_mib * 1024 * 1024:
termination_reason = "rss_limit"
kill_process(process)
process.wait()
break
stdout, stdout_truncated = file_tail(
stdout_file, 16 * 1024 * 1024
)
stderr, _ = file_tail(stderr_file, 64 * 1024)
return {
"returncode": process.returncode,
"stdout": stdout,
"stdout_truncated": stdout_truncated,
"stderr": stderr,
"elapsed_seconds": time.monotonic() - started,
"observed_peak_rss_bytes": peak_rss,
"termination_reason": termination_reason,
}
def synthetic_record(database, object_name, status, issue, run, signature):
return {
"format": HELPER_FORMAT,
"scope": "brep" if object_name else "database",
"database": str(database),
"object": object_name,
"status": status,
"reference": None,
"wireframe": None,
"shaded": None,
"issues": [issue],
"audit_signature": signature,
"database_identity": file_identity(database),
"process": {
"return_code": run["returncode"],
"elapsed_seconds": run["elapsed_seconds"],
"observed_peak_rss_bytes": run["observed_peak_rss_bytes"],
"termination_reason": run["termination_reason"],
"last_phase": last_phase(run["stderr"]),
"stdout_tail": text_tail(run["stdout"]),
"stderr_tail": text_tail(run["stderr"]),
},
}
def parse_helper_json(output):
for line in reversed(output.splitlines()):
if not line.lstrip().startswith("{"):
continue
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict):
return value
return None
def audit_one(helper, database, object_name, args, signature):
command = [
str(helper),
"--ratio-min", str(args.ratio_min),
"--ratio-max", str(args.ratio_max),
"--tess-abs", str(args.tess_abs),
"--tess-rel", str(args.tess_rel),
"--tess-norm", str(args.tess_norm),
]
if args.address_space_limit_mib:
command.extend([
"--memory-limit-mib", str(args.address_space_limit_mib)
])
command.extend([str(database), object_name])
run = run_process(command, args.timeout, args.max_rss_mib)
if run["termination_reason"] == "timeout":
phase = last_phase(run["stderr"])
return synthetic_record(
database, object_name, "timeout",
"{}_timeout".format(phase) if phase else "process_timeout",
run, signature
)
if run["termination_reason"] == "rss_limit":
phase = last_phase(run["stderr"])
return synthetic_record(
database, object_name, "memory_limit",
"{}_rss_limit_exceeded".format(phase)
if phase else "process_rss_limit_exceeded",
run, signature
)
if run["termination_reason"] == "launch_error":
return synthetic_record(
database, object_name, "error", "helper_launch_failed", run, signature
)
record = parse_helper_json(run["stdout"])
if record is None:
issue = (
"process_signal_{}".format(-run["returncode"])
if run["returncode"] is not None and run["returncode"] < 0
else "helper_output_not_json"
)
return synthetic_record(
database, object_name, "error", issue, run, signature
)
if record.get("format") != HELPER_FORMAT:
return synthetic_record(
database, object_name, "error", "unexpected_helper_format",
run, signature
)
record["scope"] = "brep"
record["audit_signature"] = signature
record["database_identity"] = file_identity(database)
record["process"] = {
"return_code": run["returncode"],
"elapsed_seconds": run["elapsed_seconds"],
"observed_peak_rss_bytes": run["observed_peak_rss_bytes"],
"termination_reason": None,
"last_phase": last_phase(run["stderr"]),
"stdout_tail": "",
"stderr_tail": text_tail(run["stderr"]),
}
expected_return = 0 if record.get("status") == "ok" else 1
if run["returncode"] != expected_return:
record["status"] = "error"
record.setdefault("issues", []).append("helper_exit_status_mismatch")
return record
def list_breps(helper, database, timeout, signature):
run = run_process([str(helper), "--list", str(database)], timeout)
if run["termination_reason"] == "timeout":
return [], synthetic_record(
database, "", "timeout", "brep_inventory_timeout", run, signature
)
if run["returncode"] != 0:
return [], synthetic_record(
database, "", "error", "brep_inventory_failed", run, signature
)
if run["stdout_truncated"]:
return [], synthetic_record(
database, "", "error", "brep_inventory_output_too_large",
run, signature
)
names = sorted(set(line.strip() for line in run["stdout"].splitlines()
if line.strip()))
return names, None
def git_state(start):
try:
revision = subprocess.check_output(
["git", "-C", str(start), "rev-parse", "HEAD"],
stderr=subprocess.DEVNULL, text=True,
).strip()
dirty = bool(subprocess.check_output(
["git", "-C", str(start), "status", "--porcelain"],
stderr=subprocess.DEVNULL, text=True,
).strip())
return revision, dirty
except (OSError, subprocess.CalledProcessError):
return "unknown", None
def read_checkpoint(path, signature):
records = {}
if not path.is_file():
return records
with path.open("r", encoding="utf-8") as stream:
for number, line in enumerate(stream, 1):
try:
record = json.loads(line)
except json.JSONDecodeError:
print("ignoring malformed checkpoint line {} in {}".format(
number, path), file=sys.stderr)
continue
if record.get("audit_signature") != signature:
continue
records[(record.get("database", ""), record.get("object", ""))] = record
return records
def all_issues(record):
issues = list(record.get("issues") or [])
for mode in ("wireframe", "shaded"):
result = record.get(mode)
if result:
issues.extend(result.get("issues") or [])
return list(dict.fromkeys(str(issue) for issue in issues))
def format_vector(values):
if not values:
return "-"
return " × ".join("{:.5g}".format(value) for value in values)
def format_geometry(result, primitive):
if not result:
return "-"
ratios = result.get("dimension_ratios")
ratio_text = ""
if ratios:
ratio_text = "; ratios " + "/".join(
"-" if value is None else "{:.3g}".format(value)
for value in ratios
)
return "{} points, {} {}{}".format(
result.get("vertices", 0), result.get("primitives", 0),
primitive, ratio_text,
)
def markdown_escape(value):
return str(value).replace("|", "\\|").replace("\n", "<br>")
def worklist_markdown(summary, input_directory):
totals = summary["totals"]
lines = [
"# BRep realization audit",
"",
"Revision: `{}`{}".format(
summary["revision"], " (dirty)" if summary.get("dirty") else ""
),
"",
(
"{} databases, {} BReps: {} okay, {} geometry findings, "
"{} timeouts, {} memory-limit stops, and {} process errors."
).format(
totals["databases"], totals["breps"], totals["ok"], totals["fail"],
totals["timeout"], totals["memory_limit"], totals["error"],
),
"",
(
"Accepted generated/reference dimension ratios: {}–{}. "
"Shaded tessellation tolerance: abs {}, rel {}, norm {}."
).format(
summary["options"]["ratio_min"], summary["options"]["ratio_max"],
summary["options"]["tess_abs"], summary["options"]["tess_rel"],
summary["options"]["tess_norm"],
),
"",
"## Work list",
"",
]
findings = [
record for record in summary["results"]
if record.get("status") != "ok"
]
if not findings:
lines.append("No findings.")
return "\n".join(lines) + "\n"
lines.extend([
"| Database | BRep | Status | Reference dimensions | Wireframe | Shaded | Peak RSS MiB | Issues |",
"|---|---|---|---|---|---|---:|---|",
])
for record in findings:
database = Path(record.get("database", ""))
try:
database_name = str(database.relative_to(input_directory))
except ValueError:
database_name = str(database)
wire = record.get("wireframe")
shaded = record.get("shaded")
peaks = [
int(result.get("peak_rss_bytes") or 0)
for result in (wire, shaded) if result
]
process = record.get("process") or {}
peaks.append(int(process.get("observed_peak_rss_bytes") or 0))
reference = record.get("reference") or {}
lines.append("| {} | {} | {} | {} | {} | {} | {:.1f} | {} |".format(
markdown_escape(database_name),
markdown_escape(record.get("object") or "(inventory)"),
markdown_escape(record.get("status", "error")),
markdown_escape(format_vector(reference.get("dimensions"))),
markdown_escape(format_geometry(wire, "segments")),
markdown_escape(format_geometry(shaded, "triangles")),
max(peaks, default=0) / (1024.0 * 1024.0),
markdown_escape(", ".join(all_issues(record)) or "unspecified"),
))
return "\n".join(lines) + "\n"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input_directory", type=Path)
parser.add_argument("output_directory", type=Path)
parser.add_argument(
"--brep-audit", type=Path, default=Path(".build/bin/brep-audit")
)
parser.add_argument("-j", "--jobs", type=int, default=1,
help="BRep processes to run concurrently (default: 1)")
parser.add_argument("--timeout", type=float, default=300.0,
help="seconds allowed per BRep")
parser.add_argument("--list-timeout", type=float, default=120.0,
help="seconds allowed to inventory one database")
parser.add_argument("--ratio-min", type=float, default=0.5)
parser.add_argument("--ratio-max", type=float, default=2.0)
parser.add_argument("--tess-abs", type=float, default=0.0)
parser.add_argument("--tess-rel", type=float, default=0.01)
parser.add_argument("--tess-norm", type=float, default=0.0)
parser.add_argument(
"--max-rss-mib", type=int, default=0,
help="kill a BRep helper above this observed RSS (zero disables)"
)
parser.add_argument(
"--address-space-limit-mib", type=int, default=0,
help="set the helper RLIMIT_AS in MiB (zero disables)"
)
parser.add_argument("--only-file",
help="regular expression matched against .g filenames")
parser.add_argument("--only-object",
help="regular expression matched against BRep names")
parser.add_argument("--resume", action="store_true",
help="retain checkpointed results with identical options")
args = parser.parse_args()
if args.jobs < 1:
parser.error("--jobs must be positive")
if args.timeout <= 0 or args.list_timeout <= 0:
parser.error("timeouts must be positive")
if args.ratio_min <= 0 or args.ratio_max < args.ratio_min:
parser.error("dimension ratio limits are invalid")
if min(args.tess_abs, args.tess_rel, args.tess_norm) < 0:
parser.error("tessellation tolerances cannot be negative")
if args.max_rss_mib < 0 or args.address_space_limit_mib < 0:
parser.error("memory limits cannot be negative")
input_directory = args.input_directory.resolve()
output_directory = args.output_directory.resolve()
helper = args.brep_audit.resolve()
if not helper.is_file():
parser.error("brep-audit not found: {}".format(helper))
file_matcher = re.compile(args.only_file) if args.only_file else None
object_matcher = re.compile(args.only_object) if args.only_object else None
databases = sorted(
path.resolve() for path in input_directory.iterdir()
if path.is_file() and path.suffix.lower() == ".g" and
(not file_matcher or file_matcher.search(path.name))
)
if not databases:
parser.error("no matching .g files in {}".format(input_directory))
options = {
"ratio_min": args.ratio_min,
"ratio_max": args.ratio_max,
"tess_abs": args.tess_abs,
"tess_rel": args.tess_rel,
"tess_norm": args.tess_norm,
"timeout": args.timeout,
"list_timeout": args.list_timeout,
"max_rss_mib": args.max_rss_mib,
"address_space_limit_mib": args.address_space_limit_mib,
"only_file": args.only_file,
"only_object": args.only_object,
"helper": str(helper),
"helper_size": helper.stat().st_size,
"helper_mtime_ns": helper.stat().st_mtime_ns,
}
signature = options
output_directory.mkdir(parents=True, exist_ok=True)
checkpoint_path = output_directory / "brep-audit-results.jsonl"
records = (
read_checkpoint(checkpoint_path, signature) if args.resume else {}
)
tasks = []
for index, database in enumerate(databases, 1):
print("[inventory {}/{}] {}".format(
index, len(databases), database.name), flush=True)
names, error_record = list_breps(
helper, database, args.list_timeout, signature
)
if error_record:
for key in list(records):
if key[0] == str(database):
records.pop(key)
records[(str(database), "")] = error_record
continue
records.pop((str(database), ""), None)
if object_matcher:
names = [name for name in names if object_matcher.search(name)]
current_names = set(names)
identity = file_identity(database)
for key, record in list(records.items()):
if key[0] != str(database):
continue
if key[1] not in current_names or \
record.get("database_identity") != identity:
records.pop(key)
tasks.extend((database, name) for name in names)
# Rewrite a resumed checkpoint with only the selected run signature, then
# append each new result immediately so an interrupted corpus run is usable.
with checkpoint_path.open("w", encoding="utf-8") as checkpoint:
for record in sorted(
records.values(),
key=lambda item: (item.get("database", ""), item.get("object", ""))):
checkpoint.write(json.dumps(record, sort_keys=True) + "\n")
checkpoint.flush()
pending = [
task for task in tasks if (str(task[0]), task[1]) not in records
]
resumed = len(tasks) - len(pending)
if resumed:
print("resuming {} completed BRep records".format(resumed), flush=True)
completed = 0
with checkpoint_path.open("a", encoding="utf-8") as checkpoint:
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
futures = {
pool.submit(
audit_one, helper, database, object_name, args, signature
): (database, object_name)
for database, object_name in pending
}
for future in as_completed(futures):
database, object_name = futures[future]
try:
record = future.result()
except Exception as error: # Preserve the rest of a corpus run.
run = {
"returncode": 125,
"stdout": "",
"stderr": repr(error),
"elapsed_seconds": 0.0,
"observed_peak_rss_bytes": 0,
"termination_reason": "driver_error",
}
record = synthetic_record(
database, object_name, "error",
"audit_driver_exception", run, signature
)
records[(str(database), object_name)] = record
checkpoint.write(json.dumps(record, sort_keys=True) + "\n")
checkpoint.flush()
completed += 1
print("[audit {}/{}] {}:{} {}".format(
completed, len(pending), database.name, object_name,
record["status"],
), flush=True)
results = sorted(
records.values(),
key=lambda item: (item.get("database", ""), item.get("object", "")),
)
statuses = Counter(
record.get("status", "error")
for record in results if record.get("scope") == "brep"
)
database_errors = [
record for record in results if record.get("scope") == "database"
]
revision, dirty = git_state(Path(__file__).resolve().parents[2])
summary = {
"format": FORMAT,
"generated_utc": datetime.datetime.now(
datetime.timezone.utc
).isoformat(),
"revision": revision,
"dirty": dirty,
"input_directory": str(input_directory),
"output_directory": str(output_directory),
"helper": str(helper),
"options": options,
"totals": {
"databases": len(databases),
"database_inventory_errors": len(database_errors),
"breps": sum(statuses.values()),
"ok": statuses["ok"],
"fail": statuses["fail"],
"timeout": statuses["timeout"],
"memory_limit": statuses["memory_limit"],
"error": statuses["error"] + len(database_errors),
},
"results": results,
}
summary_path = output_directory / "brep-audit-summary.json"
worklist_path = output_directory / "brep-audit-worklist.md"
summary_path.write_text(
json.dumps(summary, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
worklist_path.write_text(
worklist_markdown(summary, input_directory), encoding="utf-8"
)
totals = summary["totals"]
print(
"audited {} BReps: {} okay; {} work items; reports in {}".format(
totals["breps"], totals["ok"],
totals["breps"] - totals["ok"] + len(database_errors),
output_directory,
)
)
return 0
if __name__ == "__main__":
sys.exit(main())
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""Capture standard swrast overview images for converted STEP databases.
Each database is opened in its own gsh process so the display manager and GED
state are isolated. The two modes are written to separate directories and
use the database filename (without its final ``.g``) as the image name.
"""
import argparse
import json
import os
import re
import subprocess
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
def image_stem(path):
return path.name[:-2] if path.name.lower().endswith(".g") else path.stem
def capture(gsh, model, output, mode, azimuth, elevation, timeout):
output.mkdir(parents=True, exist_ok=True)
image = output / (image_stem(model) + ".png")
try:
tops = subprocess.run(
[str(gsh), "--new-cmds", str(model), "tops"],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
timeout=timeout,
).stdout.splitlines()
except subprocess.TimeoutExpired:
return {"model": str(model), "mode": mode, "image": str(image),
"returncode": 124, "complete": False,
"log": "timed out while listing top-level objects"}
tops = [line.strip().rstrip("/") for line in tops
if line.strip().endswith("/") and line.strip().count("/") == 1]
if not tops:
return {"model": str(model), "mode": mode, "image": str(image),
"returncode": 1, "complete": False,
"log": "database has no top-level objects"}
commands = [
"dm attach swrast SWDM",
"draw -m{} {}".format(mode, " ".join(tops)),
"autoview",
"ae {} {}".format(azimuth, elevation),
"screengrab -D SWDM {}".format(image),
"quit",
]
try:
proc = subprocess.run(
[str(gsh), "--new-cmds", str(model)],
input="\n".join(commands) + "\n",
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=timeout,
)
except subprocess.TimeoutExpired as error:
return {"model": str(model), "mode": mode, "image": str(image),
"returncode": 124, "complete": False,
"log": str(error.stdout or "")[-4000:] + "\ntimed out during capture"}
return {
"model": str(model),
"mode": mode,
"image": str(image),
"returncode": proc.returncode,
"complete": proc.returncode == 0 and image.is_file() and image.stat().st_size > 0,
"log": proc.stdout[-4000:],
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input_directory", type=Path)
parser.add_argument("output_directory", type=Path)
parser.add_argument("--gsh", type=Path, default=Path(".build/bin/gsh"))
parser.add_argument("--jobs", type=int, default=min(2, os.cpu_count() or 1))
parser.add_argument("--timeout", type=int, default=300,
help="maximum seconds per model and view")
parser.add_argument("--mode", choices=("wireframe", "shaded", "both"), default="both")
parser.add_argument("--only", help="regular expression matched against model names")
parser.add_argument("--azimuth", type=float, default=35.0)
parser.add_argument("--elevation", type=float, default=25.0)
args = parser.parse_args()
if args.jobs < 1:
parser.error("--jobs must be positive")
if args.timeout < 1:
parser.error("--timeout must be positive")
if not args.gsh.is_file():
parser.error("gsh not found: {}".format(args.gsh))
matcher = re.compile(args.only) if args.only else None
models = sorted(path for path in args.input_directory.glob("*.g")
if not matcher or matcher.search(path.name))
if not models:
parser.error("no converted .g models in {}".format(args.input_directory))
modes = [("wireframe", 0), ("shaded", 1)] if args.mode == "both" else [
(args.mode, 0 if args.mode == "wireframe" else 1)
]
jobs = []
for name, mode in modes:
target = args.output_directory / name
jobs.extend((model, target, mode) for model in models)
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
results = list(pool.map(
lambda item: capture(args.gsh, item[0], item[1], item[2],
args.azimuth, args.elevation, args.timeout), jobs
))
args.output_directory.mkdir(parents=True, exist_ok=True)
manifest = {
"format": "brlcad-step-corpus-capture-v1",
"azimuth": args.azimuth,
"elevation": args.elevation,
"jobs": args.jobs,
"results": results,
}
(args.output_directory / "capture-manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
failed = [item for item in results if not item["complete"]]
print("captured {}/{} images".format(len(results) - len(failed), len(results)))
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
+501
View File
@@ -0,0 +1,501 @@
#!/usr/bin/env python3
"""Run the schema-dispatching BRL-CAD STEP importer over an external corpus.
Unknown schemas are recorded in the run manifest but are not passed to the
converter. Each supported input gets a structured report, a diagnostic log,
and a tab-separated status record consumed by ``step_corpus_summary.py``.
Nested input directories are scanned recursively, and existing complete
records may be resumed.
"""
import argparse
import datetime
import hashlib
import json
import os
import platform
import re
import signal
import subprocess
import sys
import time
from pathlib import Path
STEP_SUFFIXES = {".stp", ".step", ".p21"}
INCOMPLETE_PROCESS_STATUSES = {124, 130, 137, 143}
def schema_names(path):
with path.open("rb") as stream:
header = stream.read(2 * 1024 * 1024).decode("latin-1", errors="replace")
match = re.search(
r"FILE_SCHEMA\s*\(\s*\((.*?)\)\s*\)", header,
re.IGNORECASE | re.DOTALL,
)
if not match:
return []
return [
value.replace("''", "'").upper()
for value in re.findall(r"'((?:''|[^'])*)'", match.group(1))
]
def schema_family(names):
"""Mirror the public host's supported-family and interim-profile routing."""
families = set()
legacy_ap203e2 = {
"CONFIGURATION_CONTROL_3D_DESIGN_MIM_LF",
"CCD_CLA_GVP_AST",
"CCD_CLA_GVP_AST_ASD",
}
interim_companions = (
"GEOMETRIC_VALIDATION_PROPERTIES_MIM",
"GEOMETRIC_VALIDATION_PROPERTY_REPRESENTATION_MIM",
"SHAPE_APPEARANCE_LAYER_MIM",
"SHAPE_APPEARANCE_LAYERS_MIM",
)
for name in names:
if "AP203_CONFIGURATION_CONTROLLED" in name or any(
name.startswith(alias) for alias in legacy_ap203e2):
families.add("AP203e2")
elif "AUTOMOTIVE_DESIGN" in name:
families.add("AP214")
elif "CONFIG_CONTROL_DESIGN" in name:
families.add("AP203")
elif "AP242" in name or "MANAGED_MODEL_BASED_3D_ENGINEERING" in name:
families.add("AP242")
if families == {"AP203"} and any(
name.startswith(companion)
for name in names for companion in interim_companions):
return "AP203e2"
return next(iter(families)) if len(families) == 1 else "unsupported"
def artifact_stem(path, input_directory):
relative = path.relative_to(input_directory).as_posix()
readable = re.sub(r"[^A-Za-z0-9_.-]+", "_", relative)
# Sanitizing path separators and punctuation is not injective. Retain a
# readable prefix while making nested and similarly named inputs safe.
identity = hashlib.sha256(relative.encode("utf-8")).hexdigest()[:12]
return "{}__{}_".format(readable, identity)
def legacy_artifact_stem(path, input_directory):
"""Return the pre-v2 stem so an in-progress corpus can be resumed."""
relative = path.relative_to(input_directory).as_posix()
return re.sub(r"[^A-Za-z0-9_.-]+", "_", relative) + "_"
def completed_status(path):
try:
status = int(path.read_text(encoding="utf-8").split("\t", 1)[0])
except (OSError, ValueError):
return False
return status not in INCOMPLETE_PROCESS_STATUSES
def synthetic_report(input_path, status, message):
return {
"format": "brlcad-step-import-report-v1",
"input": str(input_path),
"output": "",
"exit_status": status,
"coverage": {
"geometry_attempted": 0,
"geometry_written": 0,
"geometry_skipped": 0,
},
"validation": {
"invalid_breps": 0,
"output_failures": 0,
"repairs": 0,
"inferred_curves": 0,
},
"diagnostics": [{
"severity": "error",
"entity_id": 0,
"entity_type": "",
"attribute": "",
"message": message,
"count": 1,
"aggregated": False,
}],
"timings_us": {},
"peak_rss_bytes": 0,
"skipped_items": [],
"corpus_run_incomplete": True,
}
def file_sha256(path):
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def executable_identity(path):
stat = path.stat()
return {
"path": str(path),
"size": stat.st_size,
"mtime_ns": stat.st_mtime_ns,
"sha256": file_sha256(path),
}
def schema_plugin_paths(step_g):
"""Locate build/install-tree schema modules used by the plugin host."""
plugin_directory = step_g.parent.parent / "libexec" / "step"
if not plugin_directory.is_dir():
return []
return sorted(
path for path in plugin_directory.glob("libstep-schema-*")
if path.is_file()
)
def schema_plugin_identities(step_g):
"""Identify build/install-tree schema modules used by the plugin host."""
return [executable_identity(path) for path in schema_plugin_paths(step_g)]
def linked_library_identities(executables, runtime_root):
"""Hash resolved runtime libraries supplied by the selected build tree.
Converter and plugin hashes do not detect a rebuilt geometry or STEPcode
shared library. On platforms with ``ldd``, retain every resolved library
below the executable's build/install root so a long run cannot be mistaken
for a homogeneous baseline after one of those dependencies changes.
"""
libraries = {}
for executable in executables:
try:
result = subprocess.run(
["ldd", str(executable)], check=False, capture_output=True,
text=True,
)
except OSError:
return []
if result.returncode:
continue
for line in result.stdout.splitlines():
match = re.search(r"=>\s+(/\S+)", line)
if not match:
match = re.match(r"\s*(/\S+)", line)
if not match:
continue
path = Path(match.group(1)).resolve()
try:
path.relative_to(runtime_root)
except ValueError:
continue
if path.is_file():
libraries[str(path)] = path
return [executable_identity(libraries[name]) for name in sorted(libraries)]
def host_snapshot():
"""Capture lightweight run context without adding a monitoring dependency."""
snapshot = {
"utc": datetime.datetime.now(datetime.timezone.utc).replace(
microsecond=0
).isoformat(),
}
try:
snapshot["load_average"] = list(os.getloadavg())
except (AttributeError, OSError):
pass
try:
values = {}
with Path("/proc/meminfo").open("r", encoding="ascii") as stream:
for line in stream:
key, value = line.split(":", 1)
if key in ("MemTotal", "MemAvailable"):
values[key] = int(value.split()[0]) * 1024
if values:
snapshot["memory_bytes"] = values
except (OSError, ValueError):
pass
return snapshot
def cpu_model():
try:
with Path("/proc/cpuinfo").open("r", encoding="ascii") as stream:
for line in stream:
if line.lower().startswith("model name"):
return line.split(":", 1)[1].strip()
except (OSError, IndexError):
pass
return platform.processor()
def write_manifest(path, manifest):
"""Atomically preserve progress so an interrupted long run is auditable."""
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
temporary.replace(path)
def terminate_process(process, force=False):
try:
if os.name == "nt":
process.kill() if force else process.terminate()
else:
os.killpg(process.pid, signal.SIGKILL if force else signal.SIGTERM)
except ProcessLookupError:
pass
def run_converter(command, log, timeout):
"""Run one converter and terminate its complete process group on timeout."""
process = subprocess.Popen(
command, stdout=log, stderr=subprocess.STDOUT,
start_new_session=os.name != "nt",
)
try:
return process.wait(timeout=timeout if timeout > 0 else None), False
except subprocess.TimeoutExpired:
terminate_process(process)
try:
process.wait(timeout=30)
except subprocess.TimeoutExpired:
terminate_process(process, force=True)
process.wait()
return 124, True
except BaseException:
terminate_process(process)
try:
process.wait(timeout=30)
except subprocess.TimeoutExpired:
terminate_process(process, force=True)
process.wait()
raise
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input_directory", type=Path)
parser.add_argument("output_directory", type=Path)
parser.add_argument("--step-g", type=Path, default=Path(".build/bin/step-g"))
parser.add_argument("-j", "--jobs", type=int, default=min(12, os.cpu_count() or 1),
help="geometry workers inside each converter")
parser.add_argument("--previous", type=Path,
help="previous corpus-summary.json for trend deltas")
parser.add_argument("--expectations", type=Path,
help="checksum-pinned corpus expectation manifest")
parser.add_argument("--allow-missing-expectations", action="store_true",
help="allow expectation entries absent from a filtered run")
parser.add_argument("--run-id", help="stable label for this run")
parser.add_argument("--resume", action="store_true")
parser.add_argument("--exact", action="store_true")
parser.add_argument("--timeout", type=float, default=0.0,
help="outer wall-clock limit per file in seconds (0 disables)")
parser.add_argument("--stall-timeout", type=float,
help="override step-g's no-progress timeout in seconds")
parser.add_argument(
"--only", help="regular expression matched against relative input paths"
)
parser.add_argument("--list", action="store_true",
help="classify inputs without running converters")
args = parser.parse_args()
if args.jobs < 1 or args.timeout < 0:
parser.error("--jobs must be positive and --timeout must be nonnegative")
if args.stall_timeout is not None and args.stall_timeout <= 0:
parser.error("--stall-timeout must be positive")
input_directory = args.input_directory.resolve()
output_directory = args.output_directory.resolve()
output_directory.mkdir(parents=True, exist_ok=True)
matcher = re.compile(args.only) if args.only else None
inputs = sorted(
path for path in input_directory.rglob("*")
if path.is_file() and path.suffix.lower() in STEP_SUFFIXES and
(not matcher or matcher.search(path.relative_to(input_directory).as_posix()))
)
if not inputs:
parser.error("no matching STEP files in {}".format(input_directory))
run_id = args.run_id or datetime.datetime.now(
datetime.timezone.utc
).strftime("%Y%m%dT%H%M%SZ")
step_g = args.step_g.resolve()
if not step_g.is_file():
parser.error("step-g not found: {}".format(step_g))
plugin_paths = schema_plugin_paths(step_g)
runtime_root = step_g.parent.parent.resolve()
manifest = {
"format": "brlcad-step-corpus-run-v1",
"run_id": run_id,
"started_utc": datetime.datetime.now(datetime.timezone.utc).replace(
microsecond=0
).isoformat(),
"host": {
"hostname": platform.node(),
"platform": platform.platform(),
"machine": platform.machine(),
"cpu_model": cpu_model(),
"logical_cpus": os.cpu_count(),
"initial_snapshot": host_snapshot(),
},
"input_directory": str(input_directory),
"output_directory": str(output_directory),
"options": {
"jobs": args.jobs,
"exact": args.exact,
"recursive": True,
"timeout_seconds": args.timeout,
"stall_timeout_seconds": args.stall_timeout,
},
"step_g": executable_identity(step_g),
"schema_plugins": [executable_identity(path) for path in plugin_paths],
"linked_libraries": linked_library_identities(
[step_g] + plugin_paths, runtime_root
),
"expectations": str(args.expectations.resolve()) if args.expectations else None,
"files": [],
}
manifest_path = output_directory / "run-manifest.json"
write_manifest(manifest_path, manifest)
for index, input_path in enumerate(inputs, 1):
relative_input = input_path.relative_to(input_directory).as_posix()
declared_schemas = schema_names(input_path)
declared_schema = declared_schemas[0] if declared_schemas else ""
family = schema_family(declared_schemas)
record = {
"input": str(input_path),
"declared_schema": declared_schema,
"declared_schemas": declared_schemas,
"schema": family,
}
if args.list:
print("{}\t{}\t{}".format(
family, ";".join(declared_schemas), relative_input
))
manifest["files"].append(record)
write_manifest(manifest_path, manifest)
continue
input_stat = input_path.stat()
record["input_identity"] = {
"size": input_stat.st_size,
"mtime_ns": input_stat.st_mtime_ns,
"sha256": file_sha256(input_path),
}
if family == "unsupported":
record["result"] = "skipped"
record["reason"] = "unrecognized schema"
manifest["files"].append(record)
write_manifest(manifest_path, manifest)
print("[{}/{}] skip {} ({})".format(
index, len(inputs), relative_input, family
))
continue
stem = artifact_stem(input_path, input_directory)
report_path = output_directory / (stem + ".json")
log_path = output_directory / (stem + ".log")
status_path = output_directory / (stem + ".status")
if args.resume:
legacy_stem = legacy_artifact_stem(input_path, input_directory)
legacy_report = output_directory / (legacy_stem + ".json")
legacy_status = output_directory / (legacy_stem + ".status")
if legacy_report.is_file() and completed_status(legacy_status):
stem = legacy_stem
report_path = legacy_report
log_path = output_directory / (stem + ".log")
status_path = legacy_status
if args.resume and report_path.is_file() and completed_status(status_path):
record["result"] = "resumed"
record["report"] = report_path.name
manifest["files"].append(record)
write_manifest(manifest_path, manifest)
print("[{}/{}] keep {}".format(index, len(inputs), relative_input))
continue
command = [
str(step_g), "-D", "-j", str(args.jobs),
"--report", str(report_path),
]
if args.exact:
command.append("--exact")
if args.stall_timeout is not None:
command.extend(["--stall-timeout", str(args.stall_timeout)])
command.append(str(input_path))
print("[{}/{}] {} {}".format(index, len(inputs), family, relative_input),
flush=True)
record.update({"result": "running", "host_before": host_snapshot()})
manifest["files"].append(record)
write_manifest(manifest_path, manifest)
started = time.monotonic()
with log_path.open("wb") as log:
try:
status, timed_out = run_converter(command, log, args.timeout)
message = "converter exited without writing a structured report"
if timed_out:
message = "converter exceeded the {:.1f}-second outer timeout".format(
args.timeout
)
except OSError as error:
status = 127
timed_out = False
message = "could not execute converter: {}".format(error)
elapsed = time.monotonic() - started
if not report_path.is_file():
report_path.write_text(
json.dumps(synthetic_report(input_path, status, message), indent=2) + "\n",
encoding="utf-8",
)
status_path.write_text(
"{}\t{:.3f}\t{}\t{}\n".format(status, elapsed, family, input_path),
encoding="utf-8",
)
record.update({
"result": "completed",
"process_exit_status": status,
"elapsed_seconds": elapsed,
"timed_out": timed_out,
"report": report_path.name,
"log": log_path.name,
"host_after": host_snapshot(),
})
write_manifest(manifest_path, manifest)
manifest["finished_utc"] = datetime.datetime.now(
datetime.timezone.utc
).replace(microsecond=0).isoformat()
manifest["host"]["final_snapshot"] = host_snapshot()
write_manifest(manifest_path, manifest)
if not args.list:
summary_command = [
sys.executable,
str(Path(__file__).with_name("step_corpus_summary.py")),
str(output_directory),
"--run-id", run_id,
]
if args.previous:
summary_command.extend(["--previous", str(args.previous.resolve())])
summary_status = subprocess.call(summary_command)
if summary_status or not args.expectations:
return summary_status
verify_command = [
sys.executable,
str(Path(__file__).with_name("step_corpus_verify.py")),
str(output_directory),
str(args.expectations.resolve()),
]
if args.allow_missing_expectations:
verify_command.append("--allow-missing")
return subprocess.call(verify_command)
return 0
if __name__ == "__main__":
sys.exit(main())
+939
View File
@@ -0,0 +1,939 @@
#!/usr/bin/env python3
"""Summarize one BRL-CAD STEP converter corpus run and compare it to another.
The corpus runner writes one ``NAME.status`` record and one ``NAME.json``
converter report per input. A status record is tab separated::
exit_status<TAB>elapsed_seconds<TAB>schema<TAB>input_path
This utility intentionally consumes the converters' structured reports rather
than scraping diagnostics. It writes a durable JSON record suitable for trend
plots and a compact Markdown companion for review.
"""
import argparse
import datetime
import json
import math
import os
import statistics
import subprocess
import sys
from collections import Counter
from pathlib import Path
FORMAT = "brlcad-step-corpus-summary-v2"
INCOMPLETE_PROCESS_STATUSES = {124, 130, 137, 143}
def run_complete(item):
"""Return whether an input reached a normal converter terminal state."""
if "run_complete" in item:
return bool(item["run_complete"])
return int(item.get("process_exit_status", 0)) not in INCOMPLETE_PROCESS_STATUSES
def read_json(path):
with path.open("r", encoding="utf-8") as stream:
return json.load(stream)
def git_state(start):
try:
revision = subprocess.check_output(
["git", "-C", str(start), "rev-parse", "HEAD"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
dirty = bool(
subprocess.check_output(
["git", "-C", str(start), "status", "--porcelain"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
)
return revision, dirty
except (OSError, subprocess.CalledProcessError):
return "unknown", None
def failure_category(reason):
text = (reason or "").lower()
if any(word in text for word in ("budget", "expired", "timed out", "timeout")):
return "work_budget"
if any(word in text for word in (
"not solid", "non-solid", "solid validation", "validate as a solid"
)):
return "non_solid"
if "structural" in text:
return "structural"
if "exact" in text or "conversion failed" in text:
return "exact_conversion"
if "opennurbs" in text:
return "structural"
if "no importer" in text or "unsupported" in text:
return "unsupported"
if "output" in text or "database" in text or "write" in text:
return "output"
return "other"
def parse_status(path):
fields = path.read_text(encoding="utf-8").rstrip("\n").split("\t", 3)
if len(fields) != 4:
raise ValueError("{} is not a four-field corpus status record".format(path))
return {
"process_exit_status": int(fields[0]),
"elapsed_seconds": float(fields[1]),
"schema": fields[2],
"input": fields[3],
}
def occurrence_depth(occurrences):
"""Return the maximum product-assembly depth represented by occurrences."""
children = {}
vertices = set()
for occurrence in occurrences:
parent = int(occurrence.get("parent_product_id") or 0)
child = int(occurrence.get("child_product_id") or 0)
if parent <= 0 or child <= 0:
continue
children.setdefault(parent, set()).add(child)
vertices.update((parent, child))
active = set()
memo = {}
def visit(vertex):
if vertex in memo:
return memo[vertex]
if vertex in active:
# A cycle is invalid, but retain a useful finite metric and let the
# converter's structured diagnostics report the underlying issue.
return 0
active.add(vertex)
depth = max((1 + visit(child)
for child in children.get(vertex, ())), default=0)
active.remove(vertex)
memo[vertex] = depth
return depth
return max((visit(vertex) for vertex in vertices), default=0)
def report_record(status_path):
status = parse_status(status_path)
report_path = status_path.with_suffix(".json")
if not report_path.is_file():
raise ValueError("missing report paired with {}".format(status_path))
report = read_json(report_path)
coverage = report.get("coverage") or {}
validation = report.get("validation") or {}
timings = report.get("timings_us") or {}
performance = report.get("performance") or {}
calibration = performance.get("calibration") or {}
stage_timings = performance.get("stages") or {}
skipped = report.get("skipped_items") or []
occurrences = report.get("occurrence_details") or []
configuration_records = report.get("configuration_records") or []
representation_coverage = report.get("representation_coverage") or []
attempted = int(coverage.get("geometry_attempted") or 0)
written = int(coverage.get("geometry_written") or 0)
skipped_count = int(coverage.get("geometry_skipped") or len(skipped))
categories = Counter(failure_category(item.get("reason")) for item in skipped)
invalid_breps = int(validation.get("invalid_breps") or 0)
invalid_breps_written = int(validation.get("invalid_breps_written") or 0)
invalid_breps_rejected = int(validation.get("invalid_breps_rejected") or 0)
output_failures = int(validation.get("output_failures") or 0)
inferred_curves = int(validation.get("inferred_curves") or 0)
if invalid_breps_written:
categories["invalid_brep_preserved"] += invalid_breps_written
if output_failures:
categories["output"] += output_failures
configuration_types = Counter(
item.get("type") or "UNKNOWN" for item in configuration_records
)
representation_statuses = Counter(
item.get("status") or "unknown" for item in representation_coverage
)
converter_status = int(
report.get("exit_status", status["process_exit_status"])
)
intentionally_empty = (
status["process_exit_status"] == 6 and converter_status == 6 and
attempted == 0 and bool(representation_coverage) and
all(item.get("status") == "intentionally_non_geometric"
for item in representation_coverage)
)
input_path = str(report.get("input") or status["input"])
result = {
"name": os.path.basename(input_path),
"input": input_path,
"schema": status["schema"],
"report": report_path.name,
"process_exit_status": status["process_exit_status"],
"converter_exit_status": converter_status,
"run_complete": status["process_exit_status"] not in
INCOMPLETE_PROCESS_STATUSES,
"elapsed_seconds": status["elapsed_seconds"],
"geometry_attempted": attempted,
"geometry_written": written,
"geometry_skipped": skipped_count,
"success_fraction": (float(written) / attempted) if attempted else 0.0,
"products": int(coverage.get("products") or 0),
"occurrences": int(coverage.get("occurrences") or 0),
"assembly_usage_count": len(report.get("assembly_usages") or []),
"occurrence_detail_count": len(occurrences),
"occurrence_max_depth": occurrence_depth(occurrences),
"occurrence_methods": sorted({
item.get("shape_method") or "unknown" for item in occurrences
}),
"styles_extracted": int(coverage.get("styles_extracted") or 0),
"styles_applied": int(coverage.get("styles_applied") or 0),
"layers_extracted": int(coverage.get("layers_extracted") or 0),
"materials_extracted": int(coverage.get("materials_extracted") or 0),
"properties_extracted": int(coverage.get("properties_extracted") or 0),
"configuration_record_count": len(configuration_records),
"configuration_record_types": dict(sorted(configuration_types.items())),
"configuration_metadata_count": len(
report.get("configuration_metadata") or {}
),
"product_alternative_count": len(
report.get("product_alternatives") or []
),
"usage_substitute_count": len(report.get("usage_substitutes") or []),
"entity_type_counts": dict(sorted(
(coverage.get("entity_counts") or {}).items()
)),
"unsupported_entity_counts": dict(sorted(
(coverage.get("unsupported_counts") or {}).items()
)),
"representation_status_counts": dict(sorted(
representation_statuses.items()
)),
"clean": status["process_exit_status"] not in
INCOMPLETE_PROCESS_STATUSES and attempted > 0 and
written == attempted and
converter_status == 0 and
invalid_breps_written == 0 and output_failures == 0 and
inferred_curves == 0,
"intentionally_empty": intentionally_empty,
"invalid_breps": invalid_breps,
"invalid_breps_written": invalid_breps_written,
"invalid_breps_rejected": invalid_breps_rejected,
"output_failures": output_failures,
"repairs": int(validation.get("repairs") or 0),
"inferred_curves": inferred_curves,
"tolerance_mm": validation.get("tolerance_mm"),
"peak_rss_bytes": int(report.get("peak_rss_bytes") or 0),
"load_us": int(timings.get("load") or 0),
"index_us": int(timings.get("index") or 0),
"inventory_us": int(timings.get("inventory") or 0),
"convert_us": int(timings.get("convert") or 0),
"calibration": {
"queries": int(calibration.get("queries") or 0),
"elapsed_us": int(calibration.get("elapsed_us") or 0),
"scalar_queries_per_second": float(
calibration.get("scalar_queries_per_second") or 0.0
),
"parallel_queries_per_second": float(
calibration.get("parallel_queries_per_second") or 0.0
),
},
"stage_timings_us": {
name: int(values.get("total_us") or 0)
for name, values in sorted(stage_timings.items())
},
"failure_categories": dict(sorted(categories.items())),
"skipped_items": [
{
"entity_id": item.get("entity_id"),
"entity_type": item.get("entity_type"),
"reason": item.get("reason"),
"category": failure_category(item.get("reason")),
}
for item in skipped
],
}
return result
def totals(files):
by_schema = {}
for schema in sorted(set(item["schema"] for item in files)):
by_schema[schema] = totals_base(
[item for item in files if item["schema"] == schema]
)
result = totals_base(files)
result["by_schema"] = by_schema
return result
def totals_base(files):
completed = [item for item in files if run_complete(item)]
incomplete = [item for item in files if not run_complete(item)]
process_statuses = Counter(
str(item["process_exit_status"]) for item in completed
)
converter_statuses = Counter(
str(item["converter_exit_status"]) for item in completed
)
attempted = sum(item["geometry_attempted"] for item in completed)
written = sum(item["geometry_written"] for item in completed)
categories = Counter()
entity_types = Counter()
unsupported_types = Counter()
configuration_types = Counter()
representation_statuses = Counter()
occurrence_methods = set()
for item in completed:
categories.update(item["failure_categories"])
entity_types.update(item.get("entity_type_counts") or {})
unsupported_types.update(item.get("unsupported_entity_counts") or {})
configuration_types.update(item.get("configuration_record_types") or {})
representation_statuses.update(
item.get("representation_status_counts") or {}
)
occurrence_methods.update(item.get("occurrence_methods") or [])
return {
"files": len(files),
"completed_files": len(completed),
"inconclusive_files": len(incomplete),
"clean_files": sum(1 for item in completed if item["clean"]),
"intentionally_empty_files": sum(
1 for item in completed if item.get("intentionally_empty")
),
"process_exit_status_counts": dict(sorted(process_statuses.items())),
"converter_exit_status_counts": dict(sorted(converter_statuses.items())),
"geometry_attempted": attempted,
"geometry_written": written,
"geometry_skipped": sum(item["geometry_skipped"] for item in completed),
"success_fraction": (float(written) / attempted) if attempted else 0.0,
"products": sum(item.get("products", 0) for item in completed),
"occurrences": sum(item.get("occurrences", 0) for item in completed),
"assembly_usage_count": sum(
item.get("assembly_usage_count", 0) for item in completed
),
"occurrence_detail_count": sum(
item.get("occurrence_detail_count", 0) for item in completed
),
"occurrence_max_depth": max(
(item.get("occurrence_max_depth", 0) for item in completed),
default=0,
),
"occurrence_methods": sorted(occurrence_methods),
"styles_extracted": sum(
item.get("styles_extracted", 0) for item in completed
),
"styles_applied": sum(
item.get("styles_applied", 0) for item in completed
),
"layers_extracted": sum(
item.get("layers_extracted", 0) for item in completed
),
"materials_extracted": sum(
item.get("materials_extracted", 0) for item in completed
),
"properties_extracted": sum(
item.get("properties_extracted", 0) for item in completed
),
"configuration_record_count": sum(
item.get("configuration_record_count", 0) for item in completed
),
"configuration_metadata_count": sum(
item.get("configuration_metadata_count", 0) for item in completed
),
"product_alternative_count": sum(
item.get("product_alternative_count", 0) for item in completed
),
"usage_substitute_count": sum(
item.get("usage_substitute_count", 0) for item in completed
),
"invalid_breps": sum(
item.get("invalid_breps", 0) for item in completed
),
"invalid_breps_written": sum(
item.get("invalid_breps_written", 0) for item in completed
),
"invalid_breps_rejected": sum(
item.get("invalid_breps_rejected", 0) for item in completed
),
"output_failures": sum(
item.get("output_failures", 0) for item in completed
),
"repairs": sum(item.get("repairs", 0) for item in completed),
"inferred_curves": sum(
item.get("inferred_curves", 0) for item in completed
),
"elapsed_seconds": sum(item["elapsed_seconds"] for item in completed),
"load_seconds": sum(item.get("load_us", 0) for item in completed) / 1.0e6,
"index_seconds": sum(item.get("index_us", 0) for item in completed) / 1.0e6,
"inventory_seconds": sum(
item.get("inventory_us", 0) for item in completed
) / 1.0e6,
"convert_seconds": sum(
item.get("convert_us", 0) for item in completed
) / 1.0e6,
"inconclusive_elapsed_seconds": sum(
item["elapsed_seconds"] for item in incomplete
),
"observed_inconclusive_geometry_attempted": sum(
item["geometry_attempted"] for item in incomplete
),
"observed_inconclusive_geometry_written": sum(
item["geometry_written"] for item in incomplete
),
"observed_inconclusive_geometry_skipped": sum(
item["geometry_skipped"] for item in incomplete
),
"peak_file_rss_bytes": max(
(item["peak_rss_bytes"] for item in files), default=0
),
"entity_type_counts": dict(sorted(entity_types.items())),
"unsupported_entity_counts": dict(sorted(unsupported_types.items())),
"configuration_record_types": dict(sorted(configuration_types.items())),
"representation_status_counts": dict(
sorted(representation_statuses.items())
),
"failure_categories": dict(sorted(categories.items())),
}
def positive_ratio(current, previous):
return (float(current) / float(previous)) if previous > 0 else None
def median_ratio(current_files, previous_files, field):
ratios = [
positive_ratio(current.get(field, 0), previous.get(field, 0))
for current, previous in zip(current_files, previous_files)
]
ratios = [value for value in ratios if value is not None and value > 0]
return statistics.median(ratios) if ratios else None
def geometric_mean_ratio(current_files, previous_files, field):
ratios = [
positive_ratio(current.get(field, 0), previous.get(field, 0))
for current, previous in zip(current_files, previous_files)
]
ratios = [value for value in ratios if value is not None and value > 0]
return math.exp(sum(math.log(value) for value in ratios) / len(ratios)) \
if ratios else None
def performance_comparison(current_files, previous_files):
current = totals_base(current_files)
previous = totals_base(previous_files)
fields = {
"elapsed": "elapsed_seconds",
"load": "load_seconds",
"convert": "convert_seconds",
"peak_file_rss": "peak_file_rss_bytes",
}
aggregate_ratios = {
label: positive_ratio(current[field], previous[field])
for label, field in fields.items()
}
per_file_fields = {
"elapsed": "elapsed_seconds",
"load": "load_us",
"convert": "convert_us",
"peak_rss": "peak_rss_bytes",
}
return {
"comparable_files": len(current_files),
"current": {field: current[field] for field in fields.values()},
"previous": {field: previous[field] for field in fields.values()},
"aggregate_ratios": aggregate_ratios,
"median_file_ratios": {
label: median_ratio(current_files, previous_files, field)
for label, field in per_file_fields.items()
},
"geometric_mean_file_ratios": {
label: geometric_mean_ratio(current_files, previous_files, field)
for label, field in per_file_fields.items()
},
}
def converter_options(configuration):
options = (configuration or {}).get("options") or {}
return {
"jobs": options.get("jobs"),
"exact": bool(options.get("exact", False)),
"stall_timeout_seconds": options.get("stall_timeout_seconds"),
}
def converter_identity(configuration):
configuration = configuration or {}
executable = configuration.get("step_g") or {}
plugins = configuration.get("schema_plugins") or []
libraries = configuration.get("linked_libraries") or []
return {
"host": executable.get("sha256"),
"schema_plugins": [
(os.path.basename(item.get("path") or ""), item.get("sha256"))
for item in plugins
],
"linked_libraries": [
(os.path.basename(item.get("path") or ""), item.get("sha256"))
for item in libraries
],
}
def host_identity(configuration):
host = (configuration or {}).get("host") or {}
return {
key: host.get(key)
for key in ("hostname", "platform", "machine", "cpu_model",
"logical_cpus")
}
def host_observations(manifest):
snapshots = []
host = manifest.get("host") or {}
for name in ("initial_snapshot", "final_snapshot"):
if isinstance(host.get(name), dict):
snapshots.append(host[name])
for item in manifest.get("files", []):
for name in ("host_before", "host_after"):
if isinstance(item.get(name), dict):
snapshots.append(item[name])
loads = [item.get("load_average") for item in snapshots]
loads = [item for item in loads if isinstance(item, list) and len(item) == 3]
available = [
(item.get("memory_bytes") or {}).get("MemAvailable")
for item in snapshots
]
available = [int(value) for value in available if value is not None]
result = {"snapshot_count": len(snapshots)}
if loads:
labels = ("one_minute", "five_minutes", "fifteen_minutes")
result["load_average"] = {
label: {
"minimum": min(float(item[index]) for item in loads),
"mean": statistics.mean(float(item[index]) for item in loads),
"maximum": max(float(item[index]) for item in loads),
}
for index, label in enumerate(labels)
}
if available:
result["available_memory_bytes"] = {
"minimum": min(available),
"maximum": max(available),
}
return result
def compare(current_files, previous, current_configuration=None,
previous_configuration=None):
if not previous:
return None
old_files = {item["input"]: item for item in previous.get("files", [])}
changes = []
comparable_current = []
comparable_previous = []
for current in current_files:
old = old_files.get(current["input"])
if not run_complete(current):
changes.append({
"input": current["input"],
"change": "inconclusive",
"process_exit_status": current["process_exit_status"],
})
continue
if not old:
changes.append({"input": current["input"], "change": "new"})
continue
if not run_complete(old):
changes.append({"input": current["input"],
"change": "previous_inconclusive"})
continue
comparable_current.append(current)
comparable_previous.append(old)
written_delta = current["geometry_written"] - old.get("geometry_written", 0)
skipped_delta = current["geometry_skipped"] - old.get("geometry_skipped", 0)
inferred_delta = current.get("inferred_curves", 0) - \
old.get("inferred_curves", 0)
old_skips = {
(item.get("entity_id"), item.get("entity_type"), item.get("reason"))
for item in old.get("skipped_items", [])
}
new_skips = {
(item.get("entity_id"), item.get("entity_type"), item.get("reason"))
for item in current.get("skipped_items", [])
}
if (written_delta or skipped_delta or inferred_delta or
bool(old.get("clean")) != current["clean"] or
old_skips != new_skips):
changes.append({
"input": current["input"],
"written_delta": written_delta,
"skipped_delta": skipped_delta,
"inferred_curves_delta": inferred_delta,
"became_clean": current["clean"] and not old.get("clean"),
"regressed_from_clean": bool(old.get("clean")) and not current["clean"],
"fixed_skips": [list(item) for item in sorted(old_skips - new_skips)],
"new_skips": [list(item) for item in sorted(new_skips - old_skips)],
})
previous_totals = totals(comparable_previous)
current_totals = totals(comparable_current)
input_hash_pairs = [
(current.get("input_sha256"), old.get("input_sha256"))
for current, old in zip(comparable_current, comparable_previous)
]
input_hashes_verified = bool(input_hash_pairs) and all(
current and old and current == old
for current, old in input_hash_pairs
)
options_match = bool(
current_configuration and previous_configuration
) and converter_options(current_configuration) == converter_options(
previous_configuration
)
hosts_match = bool(
current_configuration and previous_configuration
) and host_identity(current_configuration) == host_identity(
previous_configuration
)
current_observations = (
current_configuration or {}
).get("host_observations") or {}
previous_observations = (
previous_configuration or {}
).get("host_observations") or {}
loads_recorded = bool(
current_observations.get("load_average") and
previous_observations.get("load_average")
)
return {
"previous_run_id": previous.get("run_id"),
"geometry_written_delta": current_totals["geometry_written"] -
int(previous_totals.get("geometry_written") or 0),
"geometry_skipped_delta": current_totals["geometry_skipped"] -
int(previous_totals.get("geometry_skipped") or 0),
"clean_files_delta": current_totals["clean_files"] -
int(previous_totals.get("clean_files") or 0),
"regressed_clean_files": [
item["input"] for item in changes if item.get("regressed_from_clean")
],
"newly_clean_files": [
item["input"] for item in changes if item.get("became_clean")
],
"inconclusive_files": [
item["input"] for item in changes
if item.get("change") == "inconclusive"
],
"configuration": {
"current_options": (current_configuration or {}).get("options"),
"previous_options": (previous_configuration or {}).get("options"),
"converter_options_match": options_match,
"host_identity_match": hosts_match,
"input_hashes_verified": input_hashes_verified,
"load_observations_recorded": loads_recorded,
"timing_context_comparable": options_match and hosts_match and
input_hashes_verified and
loads_recorded,
"executable_match": bool(
(current_configuration or {}).get("step_g") and
(previous_configuration or {}).get("step_g")
) and converter_identity(current_configuration) ==
converter_identity(previous_configuration),
"current_outer_timeout_seconds": (
(current_configuration or {}).get("options") or {}
).get("timeout_seconds"),
"previous_outer_timeout_seconds": (
(previous_configuration or {}).get("options") or {}
).get("timeout_seconds"),
"current_host_identity": host_identity(current_configuration),
"previous_host_identity": host_identity(previous_configuration),
"current_host_observations": current_observations,
"previous_host_observations": previous_observations,
},
"performance": performance_comparison(
comparable_current, comparable_previous
),
"changes": changes,
}
def run_configuration(run_directory):
path = Path(run_directory) / "run-manifest.json"
if not path.is_file():
return None
manifest = read_json(path)
return {
"manifest": str(path.resolve()),
"options": manifest.get("options"),
"step_g": manifest.get("step_g"),
"schema_plugins": manifest.get("schema_plugins"),
"linked_libraries": manifest.get("linked_libraries"),
"host": manifest.get("host"),
"host_observations": host_observations(manifest),
"started_utc": manifest.get("started_utc"),
"finished_utc": manifest.get("finished_utc"),
}
def attach_input_identities(files, run_directory):
path = Path(run_directory) / "run-manifest.json"
if not path.is_file():
return
manifest = read_json(path)
identities = {
record.get("input"): record.get("input_identity") or {}
for record in manifest.get("files") or []
}
for item in files:
identity = identities.get(item["input"]) or {}
if identity.get("sha256"):
item["input_sha256"] = identity["sha256"]
item["input_size"] = identity.get("size")
def markdown(summary):
total = summary["totals"]
def count_list(values):
return ", ".join(
"{} {}".format(count, name)
for name, count in sorted(
values.items(), key=lambda item: (-item[1], item[0])
)
)
lines = [
"# STEP corpus run {}".format(summary["run_id"]),
"",
"Revision: `{}`{}".format(
summary["revision"], " (dirty)" if summary.get("dirty") else ""
),
"",
"| Schema | Files clean | Intentional empty | Geometry written | Success |",
"|---|---:|---:|---:|---:|",
]
for schema, values in sorted(total["by_schema"].items()):
lines.append("| {} | {}/{} | {} | {}/{} | {:.2%} |".format(
schema, values["clean_files"], values["completed_files"],
values["intentionally_empty_files"],
values["geometry_written"], values["geometry_attempted"],
values["success_fraction"],
))
lines.append("| Combined | {}/{} | {} | {}/{} | {:.2%} |".format(
total["clean_files"], total["completed_files"],
total["intentionally_empty_files"], total["geometry_written"],
total["geometry_attempted"], total["success_fraction"],
))
lines.extend([
"",
"Assembly coverage: {} product(s), {} occurrence(s), maximum depth "
"{}, using {}.".format(
total["products"], total["occurrences"],
total["occurrence_max_depth"],
", ".join(total["occurrence_methods"]) or "no occurrence method",
),
"Presentation and metadata: {} style(s) extracted, {} applied, {} "
"layer(s), {} material(s), {} property value(s), and {} retained "
"configuration record(s).".format(
total["styles_extracted"], total["styles_applied"],
total["layers_extracted"], total["materials_extracted"],
total["properties_extracted"],
total["configuration_record_count"],
),
"Validation: {} invalid B-Rep(s), {} preserved and {} rejected; {} "
"bounded repair(s) and {} tagged curve inference(s) applied.".format(
total["invalid_breps"], total["invalid_breps_written"],
total["invalid_breps_rejected"],
total["repairs"],
total["inferred_curves"],
),
])
if total["failure_categories"]:
lines.append("Geometry omission categories: {}.".format(
count_list(total["failure_categories"])
))
lines.append("Converter terminal statuses: {}.".format(
count_list(total["converter_exit_status_counts"])
))
if total["unsupported_entity_counts"]:
lines.append("Unsupported STEP entity inventory: {}.".format(
count_list(total["unsupported_entity_counts"])
))
observations = ((summary.get("run_configuration") or {}).get(
"host_observations") or {})
one_minute = (observations.get("load_average") or {}).get("one_minute")
if one_minute:
lines.extend([
"",
"Host one-minute load over {} boundary samples: {:.2f} mean "
"({:.2f}-{:.2f}).".format(
observations["snapshot_count"], one_minute["mean"],
one_minute["minimum"], one_minute["maximum"],
),
])
if total["inconclusive_files"]:
lines.append("")
lines.append(
"Inconclusive (timed out or interrupted): {} file(s); partial "
"geometry is excluded from totals and deltas.".format(
total["inconclusive_files"]
)
)
lines.extend([
"", "## Per file", "",
"| Schema | Input | Status | Written | Skipped | Inferred | Reported load s | "
"Reported convert s | Wall s | Peak RSS MiB |",
"|---|---|---|---:|---:|---:|---:|---:|---:|---:|",
])
for item in summary["files"]:
lines.append("| {} | {} | {} | {}/{} | {} | {} | {:.1f} | {:.1f} | {:.1f} | {:.1f} |".format(
item["schema"], item["name"].replace("|", "\\|"),
"clean" if item["clean"] else
("intentional empty" if item.get("intentionally_empty") else
("partial" if run_complete(item) else "inconclusive")),
item["geometry_written"], item["geometry_attempted"],
item["geometry_skipped"], item.get("inferred_curves", 0),
item["load_us"] / 1.0e6,
item["convert_us"] / 1.0e6, item["elapsed_seconds"],
item["peak_rss_bytes"] / (1024.0 * 1024.0),
))
delta = summary.get("delta")
if delta:
lines.extend([
"", "## Delta", "",
"Written geometry: {:+d}; skipped geometry: {:+d}; clean files: {:+d}.".format(
delta["geometry_written_delta"], delta["geometry_skipped_delta"],
delta["clean_files_delta"],
),
])
if delta["regressed_clean_files"]:
lines.append("Regressed clean files: {}.".format(
", ".join(delta["regressed_clean_files"])
))
if delta["inconclusive_files"]:
lines.append("Not compared because the run was incomplete: {}.".format(
", ".join(delta["inconclusive_files"])
))
performance = delta.get("performance") or {}
ratios = performance.get("aggregate_ratios") or {}
if performance.get("comparable_files"):
lines.extend([
"",
"Performance over {} comparable files (current/previous): "
"wall {:.3f}x and peak-file RSS {:.3f}x. Reported phase "
"ratios are load {:.3f}x and conversion {:.3f}x; phase "
"boundaries may differ across binaries.".format(
performance["comparable_files"],
ratios.get("elapsed") or 0.0,
ratios.get("peak_file_rss") or 0.0,
ratios.get("load") or 0.0,
ratios.get("convert") or 0.0,
),
])
configuration = delta.get("configuration") or {}
if not configuration.get("converter_options_match"):
lines.append(
"Run options differ or could not be verified; performance "
"ratios are informational only."
)
if not configuration.get("host_identity_match"):
lines.append(
"Host identity differs or could not be verified; timing "
"ratios are not controlled benchmarks."
)
if not configuration.get("input_hashes_verified"):
lines.append(
"Input hashes differ or were not recorded in both runs; "
"timing ratios are informational only."
)
if not configuration.get("load_observations_recorded"):
lines.append(
"Both runs did not record host-load observations; "
"timing ratios are not controlled benchmarks."
)
else:
current_load = configuration["current_host_observations"][
"load_average"
]["one_minute"]["mean"]
previous_load = configuration["previous_host_observations"][
"load_average"
]["one_minute"]["mean"]
lines.append(
"Recorded one-minute load mean was {:.2f} current versus "
"{:.2f} previous.".format(current_load, previous_load)
)
if configuration.get("timing_context_comparable"):
lines.append(
"Host, input hashes, converter options, and host-load "
"telemetry are comparable; unrecorded background activity "
"may still affect timings."
)
return "\n".join(lines) + "\n"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("run_directory", type=Path)
parser.add_argument("--previous", type=Path,
help="previous corpus-summary.json for delta reporting")
parser.add_argument("--output", type=Path,
help="output JSON (default: RUN_DIRECTORY/corpus-summary.json)")
parser.add_argument("--run-id", help="stable run label; defaults to directory name")
parser.add_argument("--no-markdown", action="store_true")
args = parser.parse_args()
run_directory = args.run_directory.resolve()
status_paths = sorted(run_directory.glob("*.status"))
if not status_paths:
parser.error("{} contains no *.status corpus records".format(run_directory))
try:
files = [report_record(path) for path in status_paths]
except (OSError, ValueError, json.JSONDecodeError) as error:
print("step_corpus_summary.py: {}".format(error), file=sys.stderr)
return 2
attach_input_identities(files, run_directory)
revision, dirty = git_state(Path(__file__).resolve().parents[2])
previous = read_json(args.previous) if args.previous else None
current_configuration = run_configuration(run_directory)
previous_configuration = run_configuration(
previous.get("run_directory")
) if previous and previous.get("run_directory") else None
generated = datetime.datetime.now(datetime.timezone.utc).replace(
microsecond=0
).isoformat()
summary = {
"format": FORMAT,
"run_id": args.run_id or run_directory.name,
"generated_utc": generated,
"run_directory": str(run_directory),
"revision": revision,
"dirty": dirty,
"run_configuration": current_configuration,
"totals": totals(files),
"files": files,
}
summary["delta"] = compare(
files, previous, current_configuration, previous_configuration
)
output = args.output or (run_directory / "corpus-summary.json")
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8") as stream:
json.dump(summary, stream, indent=2, sort_keys=True)
stream.write("\n")
if not args.no_markdown:
output.with_suffix(".md").write_text(markdown(summary), encoding="utf-8")
return 0
if __name__ == "__main__":
sys.exit(main())
+288
View File
@@ -0,0 +1,288 @@
#!/usr/bin/env python3
"""Verify a recorded STEP corpus run against checksum-pinned expectations.
External STEP samples are often valuable but not suitable for copying into
BRL-CAD. This tool keeps their evidence reproducible without copying the
files: an expectation manifest records the source, accepted local filenames,
SHA-256 digest, and report assertions. The verifier consumes the structured
reports and run manifest produced by ``step_corpus_run.py``.
"""
import argparse
import hashlib
import json
import sys
from pathlib import Path
FORMAT = "brlcad-step-corpus-verification-v1"
def read_json(path):
with path.open("r", encoding="utf-8") as stream:
return json.load(stream)
def file_sha256(path):
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def dotted_value(document, path):
value = document
for component in path.split("."):
if not isinstance(value, dict) or component not in value:
raise KeyError(path)
value = value[component]
return value
def occurrence_depth(occurrences):
children = {}
vertices = set()
for occurrence in occurrences:
parent = int(occurrence.get("parent_product_id") or 0)
child = int(occurrence.get("child_product_id") or 0)
if parent <= 0 or child <= 0:
continue
children.setdefault(parent, set()).add(child)
vertices.update((parent, child))
active = set()
memo = {}
def visit(vertex):
if vertex in memo:
return memo[vertex]
if vertex in active:
raise ValueError("occurrence product graph contains a cycle")
active.add(vertex)
depth = max((1 + visit(child) for child in children.get(vertex, ())),
default=0)
active.remove(vertex)
memo[vertex] = depth
return depth
return max((visit(vertex) for vertex in vertices), default=0)
def derived_values(report):
occurrences = report.get("occurrence_details") or []
metadata = report.get("product_metadata") or []
materials = [
material
for product in metadata
for material in product.get("materials") or []
]
material_properties = [
prop
for material in materials
for prop in material.get("properties") or []
]
material_numeric_values = sorted({
value
for prop in material_properties
for value in prop.get("values") or []
if isinstance(value, (int, float))
})
return {
"assembly_usage_count": len(report.get("assembly_usages") or []),
"occurrence_detail_count": len(occurrences),
"occurrence_max_depth": occurrence_depth(occurrences),
"occurrence_methods": sorted({
item.get("shape_method") or "" for item in occurrences
}),
"skipped_entity_ids": sorted({
int(item.get("entity_id") or 0)
for item in report.get("skipped_items") or []
if int(item.get("entity_id") or 0) > 0
}),
"product_metadata_count": len(metadata),
"material_count": len(materials),
"material_identifiers": sorted({
material.get("identifier") or "" for material in materials
}),
"material_names": sorted({
material.get("name") or "" for material in materials
}),
"material_property_count": len(material_properties),
"material_property_names": sorted({
prop.get("name") or "" for prop in material_properties
}),
"material_property_units": sorted({
prop.get("units") or "" for prop in material_properties
if prop.get("units")
}),
"material_property_numeric_values": material_numeric_values,
"validation_property_count": sum(
len(product.get("validation_properties") or [])
for product in metadata
),
}
def compare_expected(report, expected):
combined = dict(report)
combined["derived"] = derived_values(report)
checks = []
errors = []
for path, wanted in sorted((expected.get("equals") or {}).items()):
try:
actual = dotted_value(combined, path)
except KeyError:
errors.append("{} is missing".format(path))
continue
checks.append(path)
if actual != wanted:
errors.append("{} is {!r}, expected {!r}".format(
path, actual, wanted
))
for path, wanted in sorted((expected.get("minimum") or {}).items()):
try:
actual = dotted_value(combined, path)
except KeyError:
errors.append("{} is missing".format(path))
continue
checks.append(path)
if not isinstance(actual, (int, float)) or actual < wanted:
errors.append("{} is {!r}, expected at least {!r}".format(
path, actual, wanted
))
return checks, errors, combined["derived"]
def markdown(result):
lines = [
"# STEP external corpus verification",
"",
"Expectation manifest: `{}`".format(result["expectations"]),
"",
"| Input | Source/producer | Result | Checks |",
"|---|---|---:|---:|",
]
for item in result["files"]:
source = item.get("producer") or item.get("source_page") or ""
lines.append("| {} | {} | {} | {} |".format(
item["name"].replace("|", "\\|"),
source.replace("|", "\\|"),
item["result"], len(item.get("checks") or []),
))
for error in item.get("errors") or []:
lines.append(" - **{}:** {}".format(item["name"], error))
lines.extend([
"",
"Verified {}/{} present files; {} manifest entries were missing.".format(
result["passed_files"], result["present_files"],
result["missing_files"],
),
])
return "\n".join(lines) + "\n"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("run_directory", type=Path)
parser.add_argument("expectations", type=Path)
parser.add_argument("--allow-missing", action="store_true",
help="do not fail for expectation entries absent from the run")
parser.add_argument("--output", type=Path,
help="output JSON (default: RUN/corpus-verification.json)")
args = parser.parse_args()
run_directory = args.run_directory.resolve()
manifest_path = run_directory / "run-manifest.json"
if not manifest_path.is_file():
parser.error("{} does not contain run-manifest.json".format(run_directory))
expectations_path = args.expectations.resolve()
run = read_json(manifest_path)
expectations = read_json(expectations_path)
if expectations.get("format") != "brlcad-step-corpus-expectations-v1":
parser.error("unsupported expectation manifest format")
run_files = {}
for record in run.get("files") or []:
input_name = Path(record.get("input") or "").name
if input_name:
run_files[input_name] = record
results = []
passed = 0
present = 0
missing = 0
for expectation in expectations.get("files") or []:
names = [expectation["name"]] + list(expectation.get("aliases") or [])
record = next((run_files[name] for name in names if name in run_files), None)
item = {
"name": expectation["name"],
"source_page": expectation.get("source_page"),
"producer": expectation.get("producer"),
"checks": [],
"errors": [],
}
if record is None:
missing += 1
item["result"] = "missing"
if not args.allow_missing:
item["errors"].append("input was not present in the corpus run")
results.append(item)
continue
present += 1
input_path = Path(record["input"])
actual_hash = file_sha256(input_path) if input_path.is_file() else None
item["input"] = str(input_path)
item["sha256"] = actual_hash
item["checks"].append("sha256")
recorded_hash = (record.get("input_identity") or {}).get("sha256")
if recorded_hash and recorded_hash != actual_hash:
item["errors"].append(
"input changed after the run manifest recorded its SHA-256"
)
if actual_hash != expectation.get("sha256"):
item["errors"].append("SHA-256 is {}, expected {}".format(
actual_hash, expectation.get("sha256")
))
report_name = record.get("report")
report_path = run_directory / report_name if report_name else None
if not report_path or not report_path.is_file():
item["errors"].append("structured converter report is missing")
else:
report = read_json(report_path)
checks, errors, derived = compare_expected(
report, expectation.get("expected") or {}
)
item["checks"].extend(checks)
item["errors"].extend(errors)
item["derived"] = derived
item["report"] = str(report_path)
item["result"] = "pass" if not item["errors"] else "fail"
if item["result"] == "pass":
passed += 1
results.append(item)
failed = sum(1 for item in results if item.get("errors"))
result = {
"format": FORMAT,
"expectations": str(expectations_path),
"run_directory": str(run_directory),
"passed_files": passed,
"present_files": present,
"missing_files": missing,
"failed_files": failed,
"files": results,
}
output = args.output or (run_directory / "corpus-verification.json")
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n",
encoding="utf-8")
output.with_suffix(".md").write_text(markdown(result), encoding="utf-8")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())
+449
View File
@@ -0,0 +1,449 @@
#!/usr/bin/env python3
"""Extract a small, forward-complete STEP geometry subset.
This helper is intended for local diagnosis and for identifying the minimal
geometry that a project-owned synthetic regression test must reproduce. It
copies the dependency closure of one or more STEP face entities and wraps them
in an OPEN_SHELL product, retaining the source entity numbers so converter
diagnostics remain directly comparable to the original file. A diagnostic
--closed-shell mode instead uses CLOSED_SHELL/MANIFOLD_SOLID_BREP so reductions
can exercise topology rules which only apply to asserted solids. A face subset
is not necessarily a valid closed shell, so that mode is for failure reduction,
not evidence that the extracted result is valid geometry. The output keeps the
input model's provenance and is not automatically suitable for committing or
redistribution.
"""
import argparse
import re
from pathlib import Path
ADVANCED_FACE = re.compile(
r"^[ \t]*#[0-9]+[ \t]*=[ \t]*ADVANCED_FACE[ \t]*\(",
re.IGNORECASE,
)
FILE_SCHEMA = re.compile(
r"FILE_SCHEMA\s*\(\s*\(\s*'([^']+)'", re.IGNORECASE | re.DOTALL
)
ENTITY_TYPE = re.compile(
r"^[ \t]*#[0-9]+[ \t]*=[ \t]*([A-Z0-9_]+)[ \t]*\(",
re.IGNORECASE,
)
def instance_end(text, start):
"""Return the first record-ending semicolon outside strings/comments."""
in_string = False
in_comment = False
offset = start
while offset < len(text):
current = text[offset]
following = text[offset + 1] if offset + 1 < len(text) else ""
if in_comment:
if current == "*" and following == "/":
in_comment = False
offset += 2
continue
elif in_string:
if current == "'":
if following == "'":
offset += 2
continue
in_string = False
elif current == "/" and following == "*":
in_comment = True
offset += 2
continue
elif current == "'":
in_string = True
elif current == ";":
return offset + 1
offset += 1
raise ValueError("unterminated STEP instance starting at byte {}".format(start))
def instances(text):
"""Return Part 21 instances without recognizing text in strings/comments."""
result = {}
offset = 0
line_start = True
in_string = False
in_comment = False
while offset < len(text):
current = text[offset]
following = text[offset + 1] if offset + 1 < len(text) else ""
if in_comment:
if current == "*" and following == "/":
in_comment = False
offset += 2
continue
elif in_string:
if current == "'":
if following == "'":
offset += 2
continue
in_string = False
elif current == "/" and following == "*":
in_comment = True
offset += 2
continue
elif current == "'":
in_string = True
elif current == "\n" or current == "\r":
line_start = True
elif line_start and current in " \t":
pass
elif line_start and current == "#":
digit_end = offset + 1
while digit_end < len(text) and text[digit_end].isdigit():
digit_end += 1
separator = digit_end
while separator < len(text) and text[separator] in " \t":
separator += 1
if digit_end > offset + 1 and separator < len(text) and \
text[separator] == "=":
entity_id = int(text[offset + 1:digit_end])
end = instance_end(text, offset)
if entity_id in result:
raise ValueError(
"duplicate STEP entity #{}".format(entity_id)
)
result[entity_id] = text[offset:end].strip()
offset = end
line_start = False
continue
line_start = False
else:
line_start = False
offset += 1
return result
def references(record):
"""Yield entity references outside Part 21 strings and comments."""
offset = 0
in_string = False
in_comment = False
while offset < len(record):
current = record[offset]
following = record[offset + 1] if offset + 1 < len(record) else ""
if in_comment:
if current == "*" and following == "/":
in_comment = False
offset += 2
continue
elif in_string:
if current == "'":
if following == "'":
offset += 2
continue
in_string = False
elif current == "/" and following == "*":
in_comment = True
offset += 2
continue
elif current == "'":
in_string = True
elif current == "#":
digit_end = offset + 1
while digit_end < len(record) and record[digit_end].isdigit():
digit_end += 1
if digit_end > offset + 1:
yield int(record[offset + 1:digit_end])
offset = digit_end
continue
offset += 1
def dependency_closure(records, roots):
pending = list(roots)
selected = set()
while pending:
entity_id = pending.pop()
if entity_id in selected:
continue
record = records.get(entity_id)
if record is None:
raise ValueError("STEP entity #{} is not present".format(entity_id))
selected.add(entity_id)
for referenced_id in references(record):
if referenced_id != entity_id and referenced_id not in selected:
pending.append(referenced_id)
return selected
def record_type(record):
match = ENTITY_TYPE.match(record)
return match.group(1).upper() if match else ""
def adjacent_faces(records, faces):
"""Return ADVANCED_FACE entities sharing an EDGE_CURVE with faces."""
reverse = {}
for entity_id, record in records.items():
for referenced_id in references(record):
reverse.setdefault(referenced_id, set()).add(entity_id)
edge_curves = {
entity_id
for entity_id in dependency_closure(records, faces)
if record_type(records[entity_id]) == "EDGE_CURVE"
}
oriented_edges = {
entity_id
for edge_id in edge_curves
for entity_id in reverse.get(edge_id, ())
if record_type(records[entity_id]) == "ORIENTED_EDGE"
}
edge_loops = {
entity_id
for oriented_id in oriented_edges
for entity_id in reverse.get(oriented_id, ())
if record_type(records[entity_id]) == "EDGE_LOOP"
}
face_bounds = {
entity_id
for loop_id in edge_loops
for entity_id in reverse.get(loop_id, ())
if record_type(records[entity_id]) in {
"FACE_BOUND", "FACE_OUTER_BOUND"
}
}
return {
entity_id
for bound_id in face_bounds
for entity_id in reverse.get(bound_id, ())
if record_type(records[entity_id]) == "ADVANCED_FACE"
}
def quoted(value):
return value.replace("'", "''")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path)
parser.add_argument("output", type=Path)
parser.add_argument(
"--face",
type=int,
action="append",
default=[],
help="ADVANCED_FACE entity ID to retain (repeatable)",
)
parser.add_argument(
"--face-range",
action="append",
default=[],
metavar="FIRST:LAST",
help="inclusive contiguous ADVANCED_FACE ID range to retain (repeatable)",
)
parser.add_argument(
"--face-neighbor-depth",
type=int,
default=0,
metavar="N",
help=(
"also retain ADVANCED_FACE neighbors reachable through N layers "
"of shared EDGE_CURVEs"
),
)
parser.add_argument("--name", default="reduced_step_regression")
parser.add_argument(
"--context",
type=int,
help=(
"source GEOMETRIC_REPRESENTATION_CONTEXT entity ID to preserve; "
"recommended when source angle or length units are not SI defaults"
),
)
parser.add_argument(
"--uncertainty",
type=float,
default=1.0e-6,
help="fixture uncertainty in millimetres (default: 1e-6)",
)
parser.add_argument(
"--closed-shell",
action="store_true",
help=(
"wrap the face subset as CLOSED_SHELL/MANIFOLD_SOLID_BREP "
"instead of OPEN_SHELL/SHELL_BASED_SURFACE_MODEL"
),
)
args = parser.parse_args()
faces = list(args.face)
for face_range in args.face_range:
fields = face_range.split(":", 1)
if len(fields) != 2:
parser.error("--face-range must be FIRST:LAST")
try:
first, last = (int(field) for field in fields)
except ValueError:
parser.error("--face-range must contain integer entity IDs")
if first > last:
parser.error("--face-range FIRST must not exceed LAST")
faces.extend(range(first, last + 1))
faces = sorted(set(faces))
if not faces:
parser.error("at least one --face or --face-range is required")
if args.face_neighbor_depth < 0:
parser.error("--face-neighbor-depth must be nonnegative")
source = args.input.read_text(encoding="latin-1")
records = instances(source)
for entity_id in faces:
record = records.get(entity_id)
if record is None:
parser.error("STEP entity #{} is not present".format(entity_id))
if not ADVANCED_FACE.match(record):
parser.error(
"STEP entity #{} is not an ADVANCED_FACE".format(entity_id)
)
expanded_faces = set(faces)
frontier = set(faces)
for _ in range(args.face_neighbor_depth):
neighbors = adjacent_faces(records, frontier) - expanded_faces
if not neighbors:
break
expanded_faces.update(neighbors)
frontier = neighbors
faces = sorted(expanded_faces)
closure_roots = list(faces)
if args.context is not None:
closure_roots.append(args.context)
selected = dependency_closure(records, closure_roots)
schema_match = FILE_SCHEMA.search(source)
schema = (
schema_match.group(1)
if schema_match
else "AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 1 }"
)
next_id = max(records) + 1
wrapper_ids = list(range(next_id, next_id + 18))
(
shell_id,
model_id,
length_id,
angle_id,
solid_angle_id,
uncertainty_id,
context_id,
application_id,
protocol_id,
product_context_id,
definition_context_id,
product_id,
formation_id,
definition_id,
definition_shape_id,
representation_id,
shape_definition_id,
_unused_id,
) = wrapper_ids
safe_name = quoted(args.name)
face_refs = ",".join("#{}".format(entity_id) for entity_id in faces)
if args.closed_shell:
wrapper = [
"#{}=CLOSED_SHELL('{} shell',({}));".format(
shell_id, safe_name, face_refs
),
"#{}=MANIFOLD_SOLID_BREP('{} model',#{});".format(
model_id, safe_name, shell_id
),
]
else:
wrapper = [
"#{}=OPEN_SHELL('{} shell',({}));".format(
shell_id, safe_name, face_refs
),
"#{}=SHELL_BASED_SURFACE_MODEL('{} model',(#{}));".format(
model_id, safe_name, shell_id
),
]
if args.context is None:
wrapper.extend(
[
"#{}=(LENGTH_UNIT() NAMED_UNIT(*) "
"SI_UNIT(.MILLI.,.METRE.));".format(length_id),
"#{}=(NAMED_UNIT(*) PLANE_ANGLE_UNIT() "
"SI_UNIT($,.RADIAN.));".format(angle_id),
"#{}=(NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) "
"SOLID_ANGLE_UNIT());".format(solid_angle_id),
"#{}=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE({:.17g}),"
"#{},'distance_accuracy_value','maximum gap value');".format(
uncertainty_id, args.uncertainty, length_id
),
"#{}=(GEOMETRIC_REPRESENTATION_CONTEXT(3) "
"GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{})) "
"GLOBAL_UNIT_ASSIGNED_CONTEXT((#{},#{},#{})) "
"REPRESENTATION_CONTEXT('fixture','3D'));".format(
context_id,
uncertainty_id,
length_id,
angle_id,
solid_angle_id,
),
]
)
representation_context_id = (
args.context if args.context is not None else context_id
)
wrapper.extend(
[
"#{}=APPLICATION_CONTEXT('automotive design');".format(application_id),
"#{}=APPLICATION_PROTOCOL_DEFINITION("
"'fixture','automotive_design',2010,#{});".format(
protocol_id, application_id
),
"#{}=PRODUCT_CONTEXT('',#{},'mechanical');".format(
product_context_id, application_id
),
"#{}=PRODUCT_DEFINITION_CONTEXT('part definition',#{},'design');".format(
definition_context_id, application_id
),
"#{}=PRODUCT('{}','{}','reduced corpus regression fixture',(#{}));".format(
product_id, safe_name, safe_name, product_context_id
),
"#{}=PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE("
"'1','',#{},.MADE.);".format(formation_id, product_id),
"#{}=PRODUCT_DEFINITION('design','',#{},#{});".format(
definition_id, formation_id, definition_context_id
),
"#{}=PRODUCT_DEFINITION_SHAPE('','',#{});".format(
definition_shape_id, definition_id
),
"#{}={}('',(#{}),#{});".format(
representation_id,
"ADVANCED_BREP_SHAPE_REPRESENTATION" if args.closed_shell else
"MANIFOLD_SURFACE_SHAPE_REPRESENTATION",
model_id,
representation_context_id,
),
"#{}=SHAPE_DEFINITION_REPRESENTATION(#{},#{});".format(
shape_definition_id, definition_shape_id, representation_id
),
]
)
lines = [
"ISO-10303-21;",
"HEADER;",
"FILE_DESCRIPTION(('BRL-CAD reduced corpus regression fixture'),'2;1');",
"FILE_NAME('{}','',('BRL-CAD'),('BRL-CAD'),'','','');".format(safe_name),
"FILE_SCHEMA(('{}'));".format(quoted(schema)),
"ENDSEC;",
"DATA;",
]
lines.extend(records[entity_id] for entity_id in sorted(selected))
lines.extend(wrapper)
lines.extend(("ENDSEC;", "END-ISO-10303-21;", ""))
args.output.write_text("\n".join(lines), encoding="latin-1")
if __name__ == "__main__":
main()
@@ -0,0 +1,178 @@
{
"format": "brlcad-step-corpus-expectations-v1",
"description": "Checksum-pinned external samples used for independent producer qualification without copying their exchange files into BRL-CAD.",
"redistribution": "The linked STEP Tools pages state All Rights Reserved, while the STEPcode collective work is distributed under its 3-clause BSD COPYING terms. Obtain each sample from its stated source; this manifest does not independently grant redistribution rights.",
"files": [
{
"name": "cubsomcy.stp",
"aliases": ["ap203_cubsomcy.stp"],
"sha256": "eb8fb40f56523fb154b435066f7b2719a88e15e30d0b8d8e6d01dd99339ddcf1",
"source_page": "https://www.steptools.com/docs/stpfiles/ap203/index.html",
"producer": "CATIA / Alcatel Space Industries",
"expected": {
"equals": {
"exit_status": 0,
"coverage.products": 1,
"coverage.occurrences": 0,
"coverage.geometry_attempted": 2,
"coverage.geometry_written": 2,
"coverage.geometry_skipped": 0,
"coverage.outcome": "complete",
"coverage.entity_counts.ADVANCED_BREP_SHAPE_REPRESENTATION": 1,
"coverage.entity_counts.FACETED_BREP_SHAPE_REPRESENTATION": 1,
"derived.occurrence_max_depth": 0,
"derived.skipped_entity_ids": []
}
}
},
{
"name": "moon_buggy_asm.stp",
"aliases": ["ap203_moon_buggy_asm.stp"],
"sha256": "2afaaf50cd7ec15e36367be0a1e41730e7af5440ed974b62a0a21cf6346289dd",
"source_page": "https://www.steptools.com/docs/stpfiles/ap203/index.html",
"producer": "STEPNet / EDS Unigraphics 10.5",
"expected": {
"equals": {
"exit_status": 1,
"coverage.products": 20,
"coverage.occurrences": 23,
"coverage.geometry_attempted": 12,
"coverage.geometry_written": 9,
"coverage.geometry_skipped": 3,
"coverage.outcome": "partial",
"coverage.entity_counts.MAPPED_ITEM": 23,
"derived.assembly_usage_count": 23,
"derived.occurrence_detail_count": 23,
"derived.occurrence_max_depth": 4,
"derived.occurrence_methods": ["mapped"],
"derived.skipped_entity_ids": [32060, 32220, 32820]
}
}
},
{
"name": "123Block_Color.stp",
"aliases": ["ap203e2_123Block_Color.stp"],
"sha256": "4bbdd752fd3c4e7ebfee633da210b9e3df62c583a604e874ca5e9c36545d2be4",
"source_page": "https://www.steptools.com/docs/stpfiles/ap203e2/index.html",
"producer": "CATIA / Boeing",
"expected": {
"equals": {
"exit_status": 0,
"coverage.products": 1,
"coverage.geometry_attempted": 1,
"coverage.geometry_written": 1,
"coverage.geometry_skipped": 0,
"coverage.outcome": "complete",
"coverage.styles_extracted": 1,
"coverage.styles_applied": 1,
"derived.skipped_entity_ids": []
}
}
},
{
"name": "boxy_with_linearsize.stp",
"aliases": ["ap203e2_boxy_with_linearsize.stp"],
"sha256": "e120b982c5bb5c8b62c99bb03d5ae46c16d9200fcb53be6521c201be98ead6d6",
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
"producer": "STEP-NC Machine / STEP Tools, Inc.",
"expected": {
"equals": {
"exit_status": 3,
"coverage.products": 1,
"coverage.geometry_attempted": 1,
"coverage.geometry_written": 0,
"coverage.geometry_skipped": 1,
"coverage.outcome": "failed",
"coverage.entity_counts.DATUM": 3,
"coverage.entity_counts.PLUS_MINUS_TOLERANCE": 1,
"derived.skipped_entity_ids": [26]
}
}
},
{
"name": "as1-ac-214.stp",
"aliases": ["ap214_as1-ac-214.stp"],
"sha256": "1bb1a0e55dc4a0169e329529eb520596690f8745077c1bdd2f9fa41fa5a2aa95",
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
"producer": "AutoCAD / STEPnet",
"expected": {
"equals": {
"exit_status": 0,
"coverage.products": 9,
"coverage.occurrences": 13,
"coverage.geometry_attempted": 5,
"coverage.geometry_written": 5,
"coverage.geometry_skipped": 0,
"coverage.outcome": "complete",
"coverage.styles_extracted": 5,
"coverage.styles_applied": 5,
"coverage.layers_extracted": 1,
"derived.assembly_usage_count": 13,
"derived.occurrence_detail_count": 13,
"derived.occurrence_max_depth": 3,
"derived.occurrence_methods": ["referenced"],
"derived.skipped_entity_ids": []
}
}
},
{
"name": "as1-ug-214.stp",
"aliases": ["ap214_as1-ug-214.stp"],
"sha256": "53c5069f5d6b59435eb32ac743cc82e53bd3607954c94785d989d579d0e29426",
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
"producer": "Unigraphics / STEPnet",
"expected": {
"equals": {
"exit_status": 0,
"coverage.products": 9,
"coverage.occurrences": 13,
"coverage.geometry_attempted": 5,
"coverage.geometry_written": 5,
"coverage.geometry_skipped": 0,
"coverage.outcome": "complete",
"coverage.styles_extracted": 5,
"coverage.styles_applied": 5,
"coverage.layers_extracted": 3,
"coverage.properties_extracted": 42,
"derived.assembly_usage_count": 13,
"derived.occurrence_detail_count": 13,
"derived.occurrence_max_depth": 3,
"derived.occurrence_methods": ["referenced"],
"derived.validation_property_count": 42,
"derived.skipped_entity_ids": []
}
}
},
{
"name": "dm1-id-214.stp",
"sha256": "25e727cdc4d738064da7907d604bd4ceeb4e941c6fdc201f3df04c0b94671661",
"source_page": "STEPcode data/ap214e3/dm1-id-214.stp (3-clause BSD collective work)",
"producer": "I-DEAS Master Series 9 / SDRC",
"expected": {
"equals": {
"exit_status": 0,
"coverage.products": 7,
"coverage.occurrences": 7,
"coverage.geometry_attempted": 3,
"coverage.geometry_written": 3,
"coverage.geometry_skipped": 0,
"coverage.outcome": "complete",
"coverage.styles_extracted": 3,
"coverage.styles_applied": 3,
"coverage.materials_extracted": 3,
"coverage.properties_extracted": 22,
"coverage.entity_counts.MAKE_FROM_USAGE_OPTION": 3,
"derived.assembly_usage_count": 7,
"derived.occurrence_detail_count": 7,
"derived.occurrence_max_depth": 1,
"derived.occurrence_methods": ["referenced"],
"derived.material_count": 3,
"derived.material_identifiers": ["AMS 4928", "AMS 5613", "AMS 5662"],
"derived.material_names": ["Greek Ascoloy", "Inconel 718", "Titanium 6-4"],
"derived.validation_property_count": 19,
"derived.skipped_entity_ids": []
}
}
}
]
}
@@ -0,0 +1,46 @@
{
"format": "brlcad-step-corpus-expectations-v1",
"description": "Checksum-pinned independent-producer samples from locally retained, non-redistributed corpora whose original download pages were not recorded.",
"redistribution": "These records provide reproducible local qualification evidence only. They do not grant permission to copy the exchange files into BRL-CAD or redistribute them.",
"files": [
{
"name": "Leopard2-A6.step",
"sha256": "ef8f21f6454f7d952b619ed1e41dab824f31f393e81a3e138392c53605a0ab94",
"source_page": "Local non-redistributed GrabCAD corpus; original download page not recorded",
"producer": "Autodesk Inventor 2018 / ST-Developer v17",
"expected": {
"equals": {
"exit_status": 1,
"coverage.products": 14,
"coverage.occurrences": 338,
"coverage.geometry_attempted": 31,
"coverage.geometry_written": 31,
"coverage.geometry_skipped": 0,
"coverage.outcome": "partial",
"coverage.styles_extracted": 2425,
"coverage.styles_applied": 31,
"coverage.materials_extracted": 13,
"coverage.properties_extracted": 26,
"coverage.properties_invalid": 13,
"validation.invalid_breps": 1,
"validation.invalid_breps_written": 1,
"validation.invalid_breps_rejected": 0,
"derived.assembly_usage_count": 338,
"derived.occurrence_detail_count": 338,
"derived.occurrence_max_depth": 1,
"derived.occurrence_methods": ["referenced"],
"derived.product_metadata_count": 13,
"derived.material_count": 13,
"derived.material_identifiers": [""],
"derived.material_names": ["Generic", "SAE 1020"],
"derived.material_property_count": 26,
"derived.material_property_names": ["Generic", "SAE 1020", "density measure"],
"derived.material_property_units": ["gram^1*centimetre^3"],
"derived.material_property_numeric_values": [1, 7.84],
"derived.validation_property_count": 0,
"derived.skipped_entity_ids": []
}
}
}
]
}
+508
View File
@@ -0,0 +1,508 @@
Remaining STEP Import/Export Work
=================================
Status: 2026-08-05
Purpose
-------
This is the current implementation and qualification plan for the public
step-g and g-step converters. It replaces recommended_practices_todo.txt,
save_invalid_plan.txt, and step_refactor.txt. It is not a development
changelog: completed work is summarized only to define the supported baseline,
and the detailed sections describe work which remains.
Status terms used below are:
* COMPLETE -- implemented and covered by focused tests;
* ACTIVE -- being qualified or reduced from real inputs;
* REMAINING -- useful work with a defined scope;
* BLOCKED -- requires a missing prerequisite and must not be approximated; and
* DEFERRED -- outside the present converter objective.
A feature is complete only when schema-valid fixtures, permissive and strict
policy tests, and structured report assertions prove that geometry or semantics
did not disappear. Independent-producer evidence is required wherever a
BRL-CAD export and re-import could otherwise repeat the same mistake.
Supported Baseline
------------------
Architecture and schemas
~~~~~~~~~~~~~~~~~~~~~~~~
The public converter architecture is COMPLETE:
* step-g and g-step are schema-dispatching hosts;
* AP203 edition 1, AP203 edition 2, AP214 edition 3, and AP242 editions 1
through 4 are bidirectional plugins;
* AP242 generated bindings remain edition-specific because the entity sets and
some Part 21 physical layouts differ;
* the AP242 export alias targets edition 4, while automatic import probes
editions 4 through 1;
* common conversion behavior is schema-neutral and schema-only behavior is
under ap203, ap203e2, ap214, or ap242; and
* obsolete schema-specific public converter programs are not built.
Explicit --schema ap242e1 through ap242e4 selection remains useful when a file
uses only the editions' common physical subset. Interim identifiers
CONFIGURATION_CONTROL_3D_DESIGN_MIM_LF, CCD_CLA_GVP_AST, and
CCD_CLA_GVP_AST_ASD route to AP203e2. Supported multi-schema AP203e2 headers
also route correctly; unknown companion schemas remain visible, and genuinely
ambiguous supported-AP combinations fail rather than guessing.
The STEPcode lazy reader supports multiple DATA sections, SCOPE,
source-ordered complex components, bounded diagnostics, type census output,
and schema-independent retention of unknown configuration records. AP203
Recommended Practice 8 fixtures cover referenced, mapped, and replicated
shapes, multi-level hierarchy, SHUO and usage semantics, multiple SDR forms,
wireframes, configuration data, and SCOPE.
STEPcode and build tractability
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
BRL-CAD uses STEPcode's compact generated API v2, packed schema modules,
precomputed attribute layouts, bounded unity chunks, and generated source
manifests. Entity and type chunk limits are independently tunable; 64:8 is the
qualified default. STEP plugins retain optimized compilation but disable LTO.
On the qualification host, a serial AP242e4 binding build without LTO fell
from 8m25s and 6.22 GB peak RSS at 64:64 to 6m12s and 1.33 GB at 64:8. NIST
CTC 03 showed no measurable conversion benefit from LTO.
The importer is split into schema-neutral common build units for session and
cache handling, document traversal, job scheduling, geometric sets, topology,
periodic bands, pullback, seam repair, loop repair, validation, and
finalization. Schema plugins consume those objects directly, preserving
static Factory registration without a new runtime ABI. A local geometry
repair edit therefore recompiles its topical unit and relinks only requested
plugins. Revisit these boundaries only in response to a measured dependency,
ownership, or rebuild problem.
Default policy and loss accounting
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The user-facing default is deliberately permissive:
* safe topology repair is enabled;
* exact and strict modes are disabled;
* storage-safe invalid BReps are preserved and explicitly tagged;
* useful partial output is published with status 1 and a structured report;
* --strict is transactional; and
* --reject-invalid-objs restores invalid-BRep rejection.
Recovery order is source-faithful construction, bounded safe repair, tagged
modeling inference for a proven source contradiction, marked storage-safe
invalid fallback, and finally omission. Scale-bounded inference is attempted
only in a detached whole-item retry after the ordinary candidate fails. It is
committed only when either:
* the rebuilt BRep passes normal structural and solidness validation; or
* permissive invalid preservation proves that a structurally safe open result
corresponds one-for-one to source EDGE_CURVEs with exactly one authored
ORIENTED_EDGE use. Such a result remains invalid and is tagged
`step:invalidity=authored_open_topology`; it is never presented as a solid.
Inference is disabled by exact, strict, no-repair, and reject-invalid policy.
Reports and affected database objects retain source edge identifiers,
discrepancy, safe and inference limits, declared tolerance, inference kind,
and explanatory detail. Rejected inference transactions do not contaminate
ordinary results or diagnostics.
Every product-bound representation, representation item, selected BRL-CAD
object, and reachable child receives a handled, intentionally non-geometric,
skipped, malformed, unsupported, or failed disposition. Import and export
distinguish complete, partial, empty, and failed outcomes. The pre-conversion
entity census describes the input and does not imply conversion support.
A preserved invalid BRep is not, by itself, proof of bad authoring. Unless an
independent topology proof establishes a source contradiction, it remains an
unresolved converter result tagged `step:geometry_status=invalid_preserved`
and `step:source_validity=unresolved`. It forces a partial result and remains
in the failure ratchet.
Geometry
~~~~~~~~
The following geometry paths are COMPLETE for their documented subsets:
* rigid product hierarchies, repeated occurrences, transforms, and shared
product/occurrence emission in every supported schema;
* optional native CSG import/export in AP203e2, AP214, and AP242, including
supported Boolean trees, exact mapped primitives, BRep leaves, transforms,
and RECTANGULAR_PYRAMID;
* exact compatible extruded and revolved area/face solids and linear or
circular SWEPT_DISK_SOLID in AP203e2, AP214, and AP242;
* AP242 triangulated faces, simple/complex surface sets, shells, and solids,
including strips, fans, pnindex, normals, welding, closure, and winding;
* analytic and spline BRep import, periodic and singular topology, voids,
bounded repair, transactional inference, and explicit invalid fallback;
* solid-BRep export/re-import for sphere, cylinder, cone, torus, box, open
NURBS faces with voids, rational Cobb surfaces, and BREP_WITH_VOIDS;
* mated librt BRep fallback for EHY, EPA, HYP, PART, PIPE, EXTRUDE, REVOLVE,
solid BOT, closed ARS, and supported NMG input;
* sketch recovery for reordered or reversed segments, zero-length segments,
inner loops, lines, circles, arcs, Beziers, and rational/non-rational NURBS;
and
* mixed GEOMETRIC_SET curves, points, and CURVE_BOUNDED_SURFACEs with partial
set loss accounting and shared bounded pullback validation.
The detailed native-CSG primitive and operator matrix is maintained in
src/conv/step/README.csg.md. AP203e1 has no native-CSG path; non-union AP203e1
trees are BLOCKED on reliable Boolean evaluation.
STEP B-spline curves and surfaces share exact conversion from EXPRESS full knot
vectors to OpenNURBS endpoint-truncated storage. Valid unclamped closed
surfaces are clamped and only numerically equal seam controls are normalized,
subject to dense before/after audits.
Presentation, units, configuration, and PMI
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
AP203e2, AP214, and AP242 share colour, transparency, style, layer, material,
property, validation-value, and recursive unit-graph extraction. Export
round-trips product identity and revision, presentation, multiple materials,
typed properties, known SI and exact conversion units, derived units,
occurrence labels, and reference designators. AP203e1 explicitly reports
presentation or property requests it cannot author.
One constrained safe-mode repair handles an identified producer defect where
a positive density's complete unit graph is mass^1 times length^3 instead of
mass^1 times length^-3. The value is retained, the denominator dimension is
corrected, provenance is recorded, and exact, strict, and no-repair modes
reject the graph. This is not a general unit-guessing rule.
--output-units, --angle-units, and --uncertainty form one schema-neutral export
policy covering BReps, native CSG, cone angles, and rigid occurrence
translations.
Configuration data is retained as a typed reference graph with exact Part 21
right-hand sides. Cross-schema authoring covers approvals, provenance,
assignments, effectivity, alternatives, substitutes, categories, people,
organizations, addresses, security classifications, documents, date/time
chains, and their legal schema-specific assignment forms.
DATUM supports point, line, plane, frame, and bounded-target forms, semantic
roles, axes, dimensions, identifiers, validation, transforms, and versioned
legacy-compatible database storage. ANNOT supports model-space planes,
per-segment roles and styles, UTF-8 text, runtime TrueType outlines,
transforms, and legacy screen-space compatibility. The hardened private
struetype code is shared from libbu; LGPL OSIFont remains runtime data and is
not compiled into a binary.
AP242 PMI stage 1 retains connected semantic, presentation, association, and
dependency records with source values, component order, references, product
ownership, and native disposition. Exactly resolved point, axis, and plane
datums become DATUM objects. Supported graphical content becomes model-space
ANNOT geometry; ambiguous content remains semantic-only. Malformed
presentation is partial in permissive mode and transactional in strict mode.
Safety and test infrastructure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
BRL-CAD and STEPcode support ASan+UBSan and a separate TSan mode. Release and
ASan+UBSan have covered the complete STEP/AP range; focused parallel, failure,
native-CSG, and BRep-export paths have run under TSan. On the qualification
host TSan must run through `setarch x86_64 -R` because its runtime otherwise
fails before main. Repeated -j8 versus -j1 output comparison remains part of
the determinism gate.
Validation Snapshot
-------------------
In-tree tests include success, malformed, partial, strict, round-trip,
sanitizer, and output-validity cases across every supported schema. The last
complete focused Release STEP matrix contains 197 tests. Official NIST MBE PMI
files are stored under db/nist with documented provenance. Five AP203 MIM
files exercise AP203e2, and 17 AP242 files span editions 1 through 4.
Checksum-pinned, non-redistributed STEP Tools and STEPcode samples cover CATIA,
Unigraphics, AutoCAD, I-DEAS, STEPnet, and STEP Tools producers. Important
independent modern-schema evidence currently includes:
* AP242 Leopard2-A6: 31/31 geometry items, 14 products, 338 occurrences,
2,425 styles, 13 material identities, and 26 material values;
* AP242 Open Source Rover: six product levels, 713 products, 1,390 rigid
occurrences, and 569 applied styles; and
* AP203e2 ROTATOR_ASM: eight product levels, 126 participating products, 404
usages, 671 product-property records, and 3,991 styles.
The last complete unified corpus baseline is the 1,138-file dry run at
`.build/step-corpus-full-j8-validity-ladder-20260804-post-speaker`:
* schemas: 616 AP203, 375 AP214, four AP203e2, and 143 AP242 files;
* files: 1,057 clean; process statuses were 1,064 complete, 69 useful partial,
three intentionally empty, one failed, and one SIGSEGV;
* geometry: 41,765 of 41,992 attempted items written (99.46 percent), 221 exact
omissions, and six unsupported-representation omissions;
* invalid fallback: 386 structurally invalid but storage-safe BReps preserved,
none rejected;
* aggregate wall/conversion time: 36,796.2/34,678.9 seconds; and
* maximum per-file RSS: 3.62 GB.
This remains the comparison baseline, not the current expected result. The
crash and several omission groups have since been fixed in selected working
runs. Its timing delta from the preceding baseline is not a controlled
regression result because host load and available memory differed materially.
Preserve both manifests and compare future runs only under similar load.
The corpus runner records hashes, schemas, converter/plugin/shared-library
identities, reports, logs, wall time, peak RSS, host load, memory availability,
geometry outcomes, hierarchy depth, metadata, and failure categories. Dry
mode still parses, constructs, validates, and repairs geometry; it omits only
database serialization. Historical `.g` files are comparative artifacts, not
correctness oracles, and must remain intact.
Selected post-baseline qualifications establish:
* cargo ship now terminates normally; its two remaining selected omissions are
explicit missing required FACE_SURFACE references;
* hAVYARD now terminates normally after rejected seam transactions reacquire
topology owned by the restored BRep; a direct owned lifetime fixture remains
to be added;
* SNAP #801290 is a valid solid after a face-scoped horn-torus keyhole repair;
* all ten historical T90 omissions serialize: six valid solids, two
source-open BReps tagged `authored_open_topology`, and two storage-safe
partial fallbacks whose contradictions exceed bounded inference limits;
* paired 135-file AP203/AP242 Recurta runs write all 139 attempted items per
schema as valid BReps, with three tagged inferences per schema; and
* the selected 421-face speaker solid now produces a validated 419-face solid
through a whole-item shared-spur transaction.
The T90 serialized selection is recorded in
`.build/t90_omissions_final_write.json`; it attempted and wrote ten of ten,
skipped none, and explicitly preserved four invalid objects. The file-level
partial status is intentional and does not represent an omission.
Two measured performance targets remain useful:
* Recurta `10012.step`, 992 faces: 78.7 seconds in the final qualification;
profiling attributes most time to periodic trim-orientation repair and
repeated transaction work, not preflight construction; and
* chess king, 13,234 faces: about 616.8 seconds and 2.92 GB in its selected
clean run.
Remaining Work in Recommended Order
-----------------------------------
1. Close the current corpus ratchet -- ACTIVE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Complete selected work before starting another expensive unified run:
1. Add a compact rejected-seam-transaction lifetime fixture, qualify it under
ASan+UBSan, and retain the complete hAVYARD real-file check.
2. Sample and reduce the largest post-baseline omission sets:
* Garrett: 49;
* 2-litre engine: 49, including 48 GEOMETRIC_SET items;
* Open Source Rover: 16;
* Epoxy granite VMC: seven; and
* BLANCO: five.
3. Retest the former 58-omission GEOMETRIC_SET group with the completed bounded
surface path. The independent b/c/d compatibility files are already
qualified except for six d-wireframe boundary/pcurve disagreements.
4. Reduce the Leopard2-A6 export/re-import failure at STEP edge #460691. One
of 31 exported BReps fails while splitting a pcurve whose candidate
fragment is 36.4476 model units from its subedge. This is a geometry
interoperability problem, not schema selection or assembly loss.
5. Retain MOUSE as a source edge/surface mismatch unless independent geometry
proves a narrower correction.
6. Revisit T90 #20240 and #20307 only if independent geometry supports a
narrower reconstruction. Their marked partial results are preferable to
inventing a false solid.
7. Remove repeated full-BRep scans and copies from large periodic repair
transactions while preserving exact rollback behavior.
8. Run focused Release, ASan+UBSan, TSan where applicable, and -j1/-j8
determinism gates. Then run the complete 1,138-file qualification.
Corpus fixes require a compact reproducer, permissive and strict assertions,
structured report coverage, and a selected real-file check. Do not introduce
broad repair heuristics solely to improve corpus percentages.
The next unified run is acceptable only if every file reaches a normal terminal
report; there is no crash, hang, schema-load failure, corrupt output, or
unexplained zero-geometry result; and all omissions and invalid fallbacks are
grouped by owned root cause.
2. Expand independent AP203e2/AP242 evidence -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Locate and pin independent examples for:
* AP203e2 material and property association variants;
* AP242 materials beyond the Inventor MAKE_FROM_USAGE_OPTION pattern;
* mixed representation contexts and large multiple-product files; and
* AP203e2/AP242 sweeps, NURBS, shells, voids, and mixed BRep/CSG assemblies.
Promote small distributable reductions into in-tree fixtures. For external
inputs record source, producer, SHA-256, and structured expectations. A
BRL-CAD-authored round trip alone cannot close this item.
3. Complete AP242 semantic PMI and MBD -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Stage 1 graph retention and exact native point/axis/plane/curve creation are
complete. Remaining interpretation and authoring work is to:
* normalize dimension type/value, tolerance type and magnitude,
material-condition modifiers, datum-reference compartments, and zones;
* preserve stable product, occurrence, face, edge, and vertex target links;
* distinguish semantic-only, graphical-only, and validated combined PMI;
* map representable text, leaders, extension/dimension lines, arrowheads,
fills, area targets, and supported symbols;
* explicitly report graphical content which cannot be mapped faithfully;
* author retained semantic and supported presentation records in AP242; and
* qualify against NIST PMI and independent AP242 files, not only hand-authored
fixtures.
An ANNOT outline is not proof that original text or tolerance semantics were
recovered. Do not infer missing semantics from graphical similarity.
4. Complete tessellated coverage and optional export -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add faithful mappings for tessellated point, curve, wire, geometric-set, and
repositioned-item forms. Add explicit optional AP242 BOT export using the
shared unit and uncertainty policy. Tessellation must not silently replace
the eventual evaluated-BRep default for analytic solids.
5. Broaden existing-BRep interoperability -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Reduce the six d-wireframe boundary/pcurve disagreements and independently
classify each as an authored tolerance violation or adapter defect. Preserve
faithful partial-set output until that proof exists; do not stretch pcurves or
widen model uncertainty merely to reach complete counts.
Expand independent-producer coverage for rational and non-rational NURBS,
open shells, nested BREP_WITH_VOIDS, reversed proxies/faces, difficult trims,
periodic and singular surfaces, and closed curves. Define a legal policy for
one BRL-CAD object containing disconnected material volumes. Add wireframe
and surface-curve export for databases without face topology.
Verification must include STEPcode readback, strict BRL-CAD re-import,
topology and material-ray comparison, and an independent STEP validator or
consumer where available. Textual identity is insufficient.
Sketch containment still needs a complete loop hierarchy for disjoint outer
regions and nested material islands. REVOLVE should split profiles at sharp
C0/tangent discontinuities when needed to avoid pathological wireframe and CDT
refinement.
DSP, deprecated HF, EBM, and VOL are a separate scalability and fidelity
project. They require bounded-error patching or faithful faceting, discrete
boundary simplification, source-semantic ray/material comparisons, and hard
validity, memory, and complexity limits. A single high-degree DSP surface or
unbounded voxel-to-BRep expansion is not an acceptable general solution.
6. Complete product and configuration mappings -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Define scoped mappings and loss policy for region identifiers,
material/shader strings, and selected arbitrary BRL-CAD attributes. Do not
conflate opaque attributes with typed STEP properties.
Broaden normalized named-unit conveniences only where schemas agree; the
recursive retained graph remains authoritative. Extend authoring for
remaining configuration identities, relationships, complex addresses, and
application edges only when required source values exist. Expand
import-export-import tests to multiple products, occurrences, shared complex
effectivities, and schema-specific graphs.
7. Resolve assembly and native-CSG edge cases -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Define explicit policy for scale, mirror, and future non-rigid occurrence
transforms. Preserve or explicitly report nested combination identity inside
one CSG expression; STEP CSG_SELECT cannot directly use another CSG_SOLID as
an operand. Expand stable naming and independent multi-level mixed BRep/CSG
evidence across AP203e2, AP214, and AP242.
NOT and guard/no-op Boolean nodes have no lossless finite STEP CSG encoding.
AP242 CYCLIDE_SEGMENT_SOLID remains unsupported because recognizing a faithful
BRL-CAD PIPE subset requires additional geometric semantics; similar
dimensions alone are insufficient.
8. Implement remaining exact sweeps -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add nonlinear swept-disk directrices only when OpenNURBS can construct a
faithful pipe with validated frame continuity and self-intersection handling.
Ellipse, spline, and composite-curve cases remain. Implement drafted and
trimmed face sweeps only after referenced termination, offset, trim intent,
and per-edge draft can be evaluated exactly. Inherited depth or angle is not
a faithful fallback.
Blocked and Deferred Work
-------------------------
Evaluated-BRep default export -- BLOCKED
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The broadly interoperable default should eventually evaluate arbitrary
Boolean trees into final BReps. Current Boolean evaluation is not reliable
enough. Continue evaluator work and validation; when it is ready, make
evaluated BRep the default without weakening optional exact native CSG.
STEPcode upstreaming -- DEFERRED maintenance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The generated-binding scalability work required by step-g and g-step is
complete. Remaining STEPcode work is upstream preparation and maintenance:
retain API v1 compatibility where required, and address correctness, cache,
generation, or resource defects when measured. Reopen generator scalability
only for a demonstrated regression or a new schema exceeding current limits.
Additional application protocols -- DEFERRED
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Do not build every ISO SMRL schema speculatively. AP224 machining features and
AP238 STEP-NC are the strongest future geometry/manufacturing candidates once
representative inputs and mapping goals exist. AP209 analysis and AP210
electronics require domain-specific semantics. AP232 and AP239 are primarily
metadata protocols and are not geometry targets merely because they share
integrated resources.
ISO SMRL and historical STEPmod selection, provenance, and redistribution
policy are documented in STEP(5).
Definition of Done
------------------
Every new decoder, emitter, or repair must include, as applicable:
1. an independent hand-authored or external fixture;
2. a supported case and a malformed or explicitly unsupported case;
3. permissive and transactional strict assertions;
4. structured report checks proving exhaustive loss accounting;
5. schema-specific coverage where legal alternatives or physical layouts
differ;
6. round-trip geometry, topology, semantic, and unit checks;
7. Release, sanitizer, and deterministic parallel tests;
8. selected corpus reruns for every affected real file; and
9. matching step-g, g-step, STEP(5), CSG matrix, and plan documentation.
The overall STEP effort is broadly robust when:
* every input and selected output object is exhaustively accounted for;
* the unified corpus has no crash, hang, schema-load failure, corrupt output,
or unexplained zero-geometry result;
* AP203e2 and AP242 have independent producer evidence comparable to the
relevant AP203/AP214 baseline;
* supported tessellation, rigid assemblies, presentation, configuration, and
PMI round-trip with explicit losses;
* existing BReps interoperate across analytic, NURBS, periodic, singular,
shell, void, orientation, and trim cases with independent consumers;
* native CSG remains optional, exact, and explicit about unsupported content;
* parallel, sanitizer, deterministic-output, performance, and memory gates
remain green; and
* reliable Boolean evaluation enables evaluated-BRep default export without
regressing native CSG.
+1 -2
View File
@@ -41,7 +41,6 @@ set(
badmagic.c
bitv.c
bomb.c
binding.cpp
booleanize.c
bu_init.cpp
cache.cpp
@@ -235,7 +234,7 @@ set(
mime.cmake
parallel.h
parallel_cpp11thread.cpp
process_private.h
process.h
sha1.h
struetype.h
vfont.h
-362
View File
@@ -1,362 +0,0 @@
/* B I N D I N G . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
#include "common.h"
#include <string>
#include <vector>
#include <map>
#include <set>
#include <fstream>
#include <sstream>
#include "bu/binding.h"
#include "bu/log.h"
#include "bu/malloc.h"
#include "bu/str.h"
#include "bu/vls.h"
#include "json.hpp"
using json = nlohmann::json;
/*
* Internal data structures to hold the parsed bindings and callbacks
*/
struct ActionCallback {
bu_action_func_t func;
void *client_data;
};
struct BindingDef {
json match;
std::string action;
};
/* Global state for libbu bindings */
static std::map<std::string, std::vector<BindingDef>>& get_contexts() {
static auto* ptr = new std::map<std::string, std::vector<BindingDef>>();
return *ptr;
}
static std::map<std::string, ActionCallback>& get_action_registry() {
static auto* ptr = new std::map<std::string, ActionCallback>();
return *ptr;
}
static std::map<std::string, std::string>& get_schema_paths() {
static auto* ptr = new std::map<std::string, std::string>();
return *ptr;
}
static std::map<std::string, std::set<std::string>>& get_schemas() {
static auto* ptr = new std::map<std::string, std::set<std::string>>();
return *ptr;
}
static void
ensure_schema_loaded(const std::string& ctx_name)
{
if (get_schemas().find(ctx_name) != get_schemas().end()) {
return;
}
auto it = get_schema_paths().find(ctx_name);
if (it == get_schema_paths().end()) {
return;
}
std::ifstream file(it->second);
if (!file.is_open()) {
bu_log("ensure_schema_loaded: Failed to open schema %s\n", it->second.c_str());
return;
}
json j;
try {
file >> j;
} catch (const json::parse_error& e) {
bu_log("ensure_schema_loaded: JSON parse error in schema %s: %s\n", it->second.c_str(), e.what());
return;
}
std::set<std::string> valid_actions;
if (j.contains("actions") && j["actions"].is_object()) {
for (auto el = j["actions"].begin(); el != j["actions"].end(); ++el) {
valid_actions.insert(el.key());
}
}
get_schemas()[ctx_name] = valid_actions;
}
/* W3C Key validation placeholder. A full implementation would check against
* an exhaustive list of W3C standard keys (e.g., "Escape", "ArrowUp").
* For demonstration, we'll just implement a simple sanity check.
*/
static int
validate_w3c_key(const std::string& key)
{
/* Example: Reject completely upper-case strings that might be native C macros */
if (key.length() > 1 && key == "RETURN") return 0;
if (key.length() > 1 && key == "SPACE") return 0;
return 1;
}
/* W3C Button validation. Values 0, 1, 2, 3, 4 are standard. */
static int
validate_w3c_button(int button)
{
return (button >= 0 && button <= 4);
}
int
bu_binding_load(const char *json_path)
{
if (!json_path) return -1;
std::ifstream file(json_path);
if (!file.is_open()) {
bu_log("bu_binding_load: Failed to open %s\n", json_path);
return -1;
}
std::stringstream buffer;
buffer << file.rdbuf();
return bu_binding_load_str(buffer.str().c_str());
}
int
bu_binding_load_str(const char *json_string)
{
if (!json_string) return -1;
json j;
try {
j = json::parse(json_string);
} catch (const json::parse_error& e) {
bu_log("bu_binding_load_str: JSON parse error: %s\n", e.what());
return -1;
}
if (!j.contains("contexts") || !j["contexts"].is_object()) {
bu_log("bu_binding_load_str: Invalid JSON structure (missing 'contexts' object)\n");
return -1;
}
for (auto it = j["contexts"].begin(); it != j["contexts"].end(); ++it) {
std::string ctx_name = it.key();
const auto& ctx_obj = it.value();
if (!ctx_obj.contains("bindings") || !ctx_obj["bindings"].is_array()) {
continue;
}
std::vector<BindingDef> defs;
ensure_schema_loaded(ctx_name);
bool has_schema = (get_schemas().find(ctx_name) != get_schemas().end());
for (const auto& binding : ctx_obj["bindings"]) {
if (!binding.contains("match") || !binding.contains("action")) {
bu_log("bu_binding_load_str: Binding missing 'match' or 'action' in context %s. Skipping.\n", ctx_name.c_str());
continue;
}
const json& match = binding["match"];
std::string action = binding["action"].get<std::string>();
/* Phase 1 Validation: Check W3C standard compliance */
int is_valid = 1;
if (match.contains(BU_BINDING_ATTR_KEY)) {
if (!match[BU_BINDING_ATTR_KEY].is_string() || !validate_w3c_key(match[BU_BINDING_ATTR_KEY].get<std::string>())) {
bu_log("bu_binding_load_str: Invalid W3C key '%s' in context %s. Binding rejected.\n",
match[BU_BINDING_ATTR_KEY].dump().c_str(), ctx_name.c_str());
is_valid = 0;
}
}
if (match.contains(BU_BINDING_ATTR_BUTTON)) {
if (!match[BU_BINDING_ATTR_BUTTON].is_number_integer() || !validate_w3c_button(match[BU_BINDING_ATTR_BUTTON].get<int>())) {
bu_log("bu_binding_load_str: Invalid W3C button '%s' in context %s. Binding rejected.\n",
match[BU_BINDING_ATTR_BUTTON].dump().c_str(), ctx_name.c_str());
is_valid = 0;
}
}
if (!is_valid) continue;
/* Phase 2 Validation: Schema constraints */
if (has_schema) {
if (get_schemas()[ctx_name].find(action) == get_schemas()[ctx_name].end()) {
bu_log("bu_binding_load_str: Binding for action '%s' rejected by schema for context '%s'.\n",
action.c_str(), ctx_name.c_str());
continue;
}
}
BindingDef def;
def.match = match;
def.action = action;
get_contexts()[ctx_name].insert(get_contexts()[ctx_name].begin(), def);
}
}
return 0;
}
void
bu_binding_register_schema(const char *context, const char *json_path)
{
if (!context || !json_path) return;
get_schema_paths()[std::string(context)] = std::string(json_path);
}
void
bu_binding_schema_add_action(const char *context, const char *action_name, const char *description)
{
if (!context || !action_name) return;
std::string ctx(context);
ensure_schema_loaded(ctx);
get_schemas()[ctx].insert(std::string(action_name));
}
void
bu_binding_schema_remove_action(const char *context, const char *action_name)
{
if (!context || !action_name) return;
std::string ctx(context);
ensure_schema_loaded(ctx);
get_schemas()[ctx].erase(std::string(action_name));
}
void
bu_binding_register_action(const char *action_name, bu_action_func_t func, void *client_data)
{
if (!action_name || !func) return;
ActionCallback cb;
cb.func = func;
cb.client_data = client_data;
get_action_registry()[std::string(action_name)] = cb;
}
/* Helper to check if the json 'event' contains all the key-value pairs required by 'match' */
static int
is_match(const json& match, const json& event)
{
for (auto it = match.begin(); it != match.end(); ++it) {
if (!event.contains(it.key())) {
return 0;
}
/* If it's an array (like modifiers), we need to ensure all elements in 'match' exist in 'event' */
if (it.value().is_array()) {
if (!event[it.key()].is_array()) return 0;
for (const auto& req_mod : it.value()) {
int found = 0;
for (const auto& ev_mod : event[it.key()]) {
if (ev_mod == req_mod) {
found = 1;
break;
}
}
if (!found) return 0;
}
}
/* Otherwise, simple equality */
else if (event[it.key()] != it.value()) {
return 0;
}
}
return 1;
}
int
bu_binding_process_event(const char *context, const char *event_json, void *event_data)
{
if (!context || !event_json) return 0;
std::string ctx_name(context);
auto it = get_contexts().find(ctx_name);
if (it == get_contexts().end()) {
return 0; /* Context not found */
}
const char *json_str = event_json;
json event_json_obj;
try {
event_json_obj = json::parse(json_str);
} catch (const json::parse_error& e) {
bu_log("bu_binding_process_event: Invalid event JSON: %s\n", e.what());
return 0;
}
if (!event_json_obj.is_object()) {
return 0;
}
for (const auto& def : it->second) {
if (is_match(def.match, event_json_obj)) {
/* Match found */
auto reg_it = get_action_registry().find(def.action);
if (reg_it != get_action_registry().end()) {
reg_it->second.func(event_json, reg_it->second.client_data, event_data);
return 1;
} else {
/* We have a binding, but no callback registered for this action */
bu_log("Warning: Event mapped to action '%s', but no callback is registered.\n", def.action.c_str());
return 1; /* Still mark as handled since it matched a binding */
}
}
}
return 0; /* Unhandled */
}
extern "C" int
bu_binding_action_is_bound(const char *context, const char *action_name)
{
if (!context || !action_name) return 0;
std::string ctx(context);
std::string act(action_name);
auto it = get_contexts().find(ctx);
if (it == get_contexts().end()) return 0;
for (const auto& def : it->second) {
if (def.action == act) {
return 1;
}
}
return 0;
}
/*
* Local Variables:
* mode: C++
* tab-width: 8
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+1 -1
View File
@@ -49,7 +49,7 @@
#include "bu/str.h"
#include "bu/datetime.h"
#include "bu/vls.h"
#include "./process_private.h"
#include "./process.h"
#ifndef HAVE_KILL
# include <TlHelp32.h>
@@ -1,4 +1,4 @@
/* P R O C E S S _ P R I V A T E . H
/* P R O C E S S . H
* BRL-CAD
*
* Copyright (c) 2013-2026 United States Government as represented by
@@ -20,8 +20,8 @@
/* Private libbu process related definitions shared between multiple files */
#ifndef LIBBU_PROCESS_PRIVATE_H
#define LIBBU_PROCESS_PRIVATE_H
#ifndef LIBBU_PROCESS_H
#define LIBBU_PROCESS_H
/* Apparently the termination code on Windows is arbitrary and somewhat
* ill-defined - see https://bugs.python.org/issue31863 for more info. Try our
@@ -29,7 +29,7 @@
* abort identification. */
#define BU_MSVC_ABORT_EXIT 97101 /* ascii - a == 97 && e == 101 */
#endif /* LIBBU_PROCESS_PRIVATE_H */
#endif /* LIBBU_PROCESS_H */
/*
* Local Variables:
-2
View File
@@ -10,7 +10,6 @@ set(
test_bomb.c
test_badmagic.c
test_basename.c
test_binding.cpp
test_bitv.c
test_booleanize.c
test_cache.cpp
@@ -123,7 +122,6 @@ brlcad_addexec(bu_gethostname test_gethostname.c libbu TEST)
brlcad_add_test(NAME bu_argv COMMAND bu_test test_argv)
brlcad_add_test(NAME bu_backtrace COMMAND bu_test test_backtrace)
brlcad_add_test(NAME bu_bomb COMMAND bu_test test_bomb "$<TARGET_FILE:bu_test>")
brlcad_add_test(NAME bu_binding COMMAND bu_test test_binding "${CMAKE_CURRENT_SOURCE_DIR}/test_bindings.json")
brlcad_add_test(NAME bu_dir COMMAND bu_test test_dir)
brlcad_add_test(NAME bu_list_empty COMMAND bu_test test_list 0)
brlcad_add_test(NAME bu_list_single COMMAND bu_test test_list 1)
-109
View File
@@ -1,109 +0,0 @@
/* T E S T _ B I N D I N G . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
#include "common.h"
#include <stdio.h>
#include <string.h>
#include "bu/binding.h"
#include "bu/vls.h"
int action_triggered = 0;
void mock_action_handler(const char *event_json, void *registered_data, void *event_data) {
action_triggered = 1;
printf("Action handler triggered with JSON: %s\n", event_json);
}
int main(int argc, char *argv[]) {
bu_setprogname(argv[0]);
if (argc < 2) {
printf("Usage: %s <path_to_test_bindings.json>\n", argv[0]);
return -1;
}
const char *json_path = argv[1];
/* Register schema to restrict allowed actions */
bu_binding_schema_add_action("fb", "fb_zoom", "Zoom");
bu_binding_schema_add_action("fb", "fb_close_window", "Close Window");
/* Register hooks before loading JSON */
bu_binding_register_action("fb_close_window", mock_action_handler, NULL);
printf("Loading bindings from %s\n", json_path);
if (bu_binding_load(json_path) < 0) {
printf("Failed to load bindings.\n");
return -1;
}
/* Test 1: Simulate a matching event */
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-press\", \"button\": 2}");
action_triggered = 0;
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), NULL);
if (!action_triggered) {
printf("Test 1 Failed: Expected action was not triggered.\n");
return -1;
}
printf("Test 1 Passed: Action triggered successfully.\n");
/* Test 2: Simulate a non-matching event */
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-press\", \"button\": 1}");
action_triggered = 0;
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), NULL);
if (action_triggered) {
printf("Test 2 Failed: Action should not have been triggered.\n");
return -1;
}
printf("Test 2 Passed: Non-matching event correctly ignored.\n");
/* Test 3: The Escape key binding should have been rejected by the validator */
bu_vls_sprintf(&ev_json, "{\"type\": \"key-press\", \"key\": \"Escape\"}");
action_triggered = 0;
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), NULL);
if (action_triggered) {
printf("Test 3 Failed: Escape binding should have been filtered by the validator.\n");
return -1;
}
printf("Test 3 Passed: Validator correctly rejected binding at load time.\n");
bu_vls_free(&ev_json);
return 0;
}
/*
* Local Variables:
* mode: C++
* tab-width: 8
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
-22
View File
@@ -1,22 +0,0 @@
{
"contexts": {
"fb": {
"bindings": [
{
"match": {
"type": "mouse-press",
"button": 2
},
"action": "fb_close_window"
},
{
"match": {
"type": "key-press",
"key": "Escape"
},
"action": "fb_invalid_action"
}
]
}
}
}
-6
View File
@@ -38,11 +38,6 @@ set_property(
PROPERTY COMPILE_DEFINITIONS "DM_PLUGIN_SUFFIX=\"${CMAKE_SHARED_LIBRARY_SUFFIX}\""
)
# Files related to key/mouse binding management
brlcad_manage_files(dm_bindings.json ${DATA_DIR}/bindings)
brlcad_manage_files(dm_schema.json ${DATA_DIR}/bindings)
brlcad_manage_files(fb_schema.json ${DATA_DIR}/bindings)
brlcad_find_package(PNG REQUIRED)
find_package(OpenSSL QUIET)
@@ -170,4 +165,3 @@ cmakefiles(${libdm_ignore_files})
# indent-tabs-mode: t
# End:
# ex: shiftwidth=2 tabstop=8
-1
View File
@@ -2925,7 +2925,6 @@ X24_handle_event(struct fb *ifp, XEvent *event)
case ButtonPress:
{
int button = (int) event->xbutton.button;
int w3c_button = 0;
/* In interactive mode, report the event to the application
* rather than consuming it here. */
-59
View File
@@ -1,59 +0,0 @@
{
"contexts": {
"fb": {
"description": "Standard interactions for raw framebuffers (rt, fbserv)",
"bindings": [
{
"match": {
"type": "mouse-press",
"button": 1
},
"action": "fb_print_pixel_rgb"
},
{
"match": {
"type": "mouse-press",
"button": 2
},
"action": "fb_close_window"
},
{
"match": {
"type": "key-press",
"key": "Escape"
},
"action": "fb_close_window"
}
]
},
"dm": {
"description": "Standard bindings for the Geometry Editor (qged & mged)",
"bindings": [
{ "match": { "type": "key-press", "key": "A" }, "action": "view_toggle_adc" },
{ "match": { "type": "key-press", "key": "M" }, "action": "view_toggle_model_axes" },
{ "match": { "type": "key-press", "key": "V" }, "action": "view_toggle_view_axes" },
{ "match": { "type": "key-press", "key": "2" }, "action": "view_2" },
{ "match": { "type": "key-press", "key": "3" }, "action": "view_3" },
{ "match": { "type": "key-press", "key": "4" }, "action": "view_4" },
{ "match": { "type": "key-press", "key": "5" }, "action": "view_5" },
{ "match": { "type": "key-press", "key": "6" }, "action": "view_6" },
{ "match": { "type": "key-press", "key": "7" }, "action": "view_7" },
{ "match": { "type": "key-press", "key": "F" }, "action": "view_front" },
{ "match": { "type": "key-press", "key": "T" }, "action": "view_top" },
{ "match": { "type": "key-press", "key": "B" }, "action": "view_bottom" },
{ "match": { "type": "key-press", "key": "L" }, "action": "view_left" },
{ "match": { "type": "key-press", "key": "R", "modifiers": ["Shift"] }, "action": "view_rear" },
{ "match": { "type": "key-press", "key": "R" }, "action": "view_right" },
{ "match": { "type": "mouse-press", "button": 0 }, "action": "view_mouse_press_left" },
{ "match": { "type": "mouse-release", "button": 0 }, "action": "view_mouse_release_left" },
{ "match": { "type": "mouse-release", "button": 2 }, "action": "view_mouse_release_right" },
{ "match": { "type": "mouse-release", "button": 1 }, "action": "view_mouse_release_middle" },
{ "match": { "type": "mouse-move", "button": 0, "modifiers": ["Control"] }, "action": "view_mouse_drag_rotate" },
{ "match": { "type": "mouse-move", "button": 0, "modifiers": ["Shift"] }, "action": "view_mouse_drag_translate" },
{ "match": { "type": "mouse-move", "button": 0, "modifiers": ["Control", "Shift"] }, "action": "view_mouse_drag_scale" },
{ "match": { "type": "mouse-move", "button": 0 }, "action": "view_mouse_drag_adjust" },
{ "match": { "type": "mouse-wheel" }, "action": "view_mouse_wheel_scale" }
]
}
}
}
-12
View File
@@ -66,22 +66,10 @@ dm_init_msgs()
}
#include "bu/binding.h"
static void
libdm_init(void)
{
bu_vls_init(&init_msgs);
const char *dm_schema = bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "dm_schema.json", NULL);
if (dm_schema) {
bu_binding_register_schema("dm", dm_schema);
}
const char *fb_schema = bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "fb_schema.json", NULL);
if (fb_schema) {
bu_binding_register_schema("fb", fb_schema);
}
const char *ppath = bu_dir(NULL, 0, BU_DIR_LIBEXEC, "dm", NULL);
char **dm_filenames;
-14
View File
@@ -29,7 +29,6 @@
#include <cctype>
#include <cstring>
#include <map>
#include "bu/binding.h"
#include <set>
#include <string>
#include <cstdio>
@@ -63,13 +62,6 @@ dm_open(void *ctx, void *interp, const char *type, int argc, const char *argv[])
const struct dm *d = d_it->second;
struct dm *dmp = d->i->dm_open(ctx, interp, argc, argv);
char *bindings_path = (char *)bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "dm_bindings.json", NULL);
if (bindings_path) {
bu_binding_load(bindings_path);
bu_free(bindings_path, "bindings_path");
}
return dmp;
}
@@ -536,12 +528,6 @@ found_interface:
return FB_NULL;
}
char *bindings_path = (char *)bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "dm_bindings.json", NULL);
if (bindings_path) {
bu_binding_load(bindings_path);
bu_free(bindings_path, "bindings_path");
}
return ifp;
}
-25
View File
@@ -1,25 +0,0 @@
{
"actions": {
"view_toggle_adc": "Toggle Angle Distance Cursor",
"view_toggle_model_axes": "Toggle Model Axes",
"view_toggle_view_axes": "Toggle View Axes",
"view_2": "Set View 2",
"view_3": "Set View 3",
"view_4": "Set View 4",
"view_5": "Set View 5",
"view_6": "Set View 6",
"view_7": "Set View 7",
"view_front": "Set Front View",
"view_top": "Set Top View",
"view_bottom": "Set Bottom View",
"view_left": "Set Left View",
"view_right": "Set Right View",
"view_rear": "Set Rear View",
"view_mouse_drag_rotate": "Rotate view via mouse drag",
"view_mouse_drag_translate": "Translate view via mouse drag",
"view_mouse_drag_scale": "Scale view via mouse drag",
"view_mouse_wheel_scale": "Scale view via mouse wheel",
"view_mouse_press_left": "Left mouse press action",
"view_mouse_press_right": "Right mouse press action"
}
}
-54
View File
@@ -53,7 +53,6 @@
#include "./include/private.h"
#include "icv.h"
#include "dm.h"
#include "bu/binding.h"
struct fb *fb_get(void)
@@ -259,59 +258,6 @@ long fb_poll_rate(struct fb *ifp)
return ifp->i->if_poll_refresh_rate;
}
static int
fb_event_button_to_w3c(int button)
{
return (button >= 1 && button <= 3) ? button - 1 : button;
}
int
fb_process_event_bindings(struct fb *ifp, const struct fb_event *event)
{
struct bu_vls event_json = BU_VLS_INIT_ZERO;
int handled = 0;
if (!ifp || !event)
return 0;
switch (event->type) {
case FB_EVENT_BUTTON_PRESS:
bu_vls_sprintf(&event_json,
"{\"type\": \"mouse-press\", \"button\": %d, \"x\": %d, \"y\": %d}",
fb_event_button_to_w3c(event->button), event->x, event->y);
break;
case FB_EVENT_BUTTON_RELEASE:
bu_vls_sprintf(&event_json,
"{\"type\": \"mouse-release\", \"button\": %d, \"x\": %d, \"y\": %d}",
fb_event_button_to_w3c(event->button), event->x, event->y);
break;
case FB_EVENT_MOTION:
bu_vls_sprintf(&event_json,
"{\"type\": \"mouse-move\", \"x\": %d, \"y\": %d}",
event->x, event->y);
break;
case FB_EVENT_KEY_PRESS:
if (event->keycode == 27) {
bu_vls_sprintf(&event_json, "{\"type\": \"key-press\", \"key\": \"Escape\"}");
} else if (event->keycode >= 32 && event->keycode <= 126) {
bu_vls_sprintf(&event_json,
"{\"type\": \"key-press\", \"key\": \"%c\"}",
event->keycode);
}
break;
case FB_EVENT_CLOSE:
bu_vls_sprintf(&event_json, "{\"type\": \"key-press\", \"key\": \"Escape\"}");
break;
default:
break;
}
if (bu_vls_strlen(&event_json) > 0)
handled = bu_binding_process_event("fb", bu_vls_cstr(&event_json), ifp);
bu_vls_free(&event_json);
return handled;
}
void fb_set_interactive(struct fb *ifp, int on)
{
if (!ifp)
-6
View File
@@ -1,6 +0,0 @@
{
"actions": {
"fb_print_pixel_rgb": "Print RGB values of pixel under cursor",
"fb_close_window": "Close framebuffer window"
}
}
+72 -171
View File
@@ -61,10 +61,6 @@
#include "dm.h"
#include "./fb_ogl.h"
#include "bu/binding.h"
#include "bu/file.h"
#include <X11/keysym.h>
extern struct fb ogl_interface;
#define DIRECT_COLOR_VISUAL_ALLOWED 0
@@ -778,52 +774,6 @@ ogl_configureWindow(struct fb *ifp, int width, int height)
return 0;
}
static void
ogl_action_fb_print_pixel_rgb(const struct bu_vls *event_json, void *registered_data, void *event_data)
{
struct fb *ifp = (struct fb *)event_data;
int x = -1, y = -1;
char *p;
if (!ifp) return;
if ((p = strstr(bu_vls_cstr(event_json), "\"x\":"))) {
sscanf(p + 4, "%d", &x);
}
if ((p = strstr(bu_vls_cstr(event_json), "\"y\":"))) {
sscanf(p + 4, "%d", &y);
}
if (x < 0 || y < 0) {
fb_log("No RGB (outside image viewport)\n");
return;
}
size_t memidx = (y * WIN(ifp)->mi_pixwidth) * sizeof(struct fb_pixel);
struct fb_pixel *oglp = (struct fb_pixel *)&ifp->i->if_mem[memidx];
fb_log("At image (%d, %d), real RGB=(%3d %3d %3d)\n",
x, y, (int)oglp[x].red, (int)oglp[x].green, (int)oglp[x].blue);
}
static void
ogl_action_fb_close_window(const struct bu_vls *event_json, void *registered_data, void *event_data)
{
struct fb *ifp = (struct fb *)event_data;
if (ifp) OGL(ifp)->alive = 0;
}
static int bindings_loaded = 0;
static void
ogl_init_bindings(void)
{
if (bindings_loaded) return;
bindings_loaded = 1;
bu_binding_register_action("fb_print_pixel_rgb", ogl_action_fb_print_pixel_rgb, NULL);
bu_binding_register_action("fb_close_window", ogl_action_fb_close_window, NULL);
}
static void
ogl_do_event(struct fb *ifp)
@@ -831,122 +781,76 @@ ogl_do_event(struct fb *ifp)
XEvent event;
while (XCheckWindowEvent(OGL(ifp)->dispp, OGL(ifp)->wind,
OGL(ifp)->event_mask, &event)) {
switch (event.type) {
case Expose:
if (!OGL(ifp)->use_ext_ctrl)
expose_callback(ifp);
if (fb_get_interactive(ifp)) {
struct fb_event ev = {FB_EVENT_EXPOSE, 0, 0, 0, 0, 0};
fb_enqueue_event(ifp, &ev);
}
break;
case ButtonPress:
if (fb_get_interactive(ifp)) {
struct fb_event ev;
memset(&ev, 0, sizeof(ev));
ev.type = FB_EVENT_BUTTON_PRESS;
ev.button = (int)event.xbutton.button;
ev.x = event.xbutton.x;
ev.y = ifp->i->if_height - event.xbutton.y - 1;
ev.state = (int)event.xbutton.state;
fb_enqueue_event(ifp, &ev);
} else {
int button = (int)event.xbutton.button;
int w3c_button = 0;
if (button == Button1) {
if (event.xbutton.state & ControlMask) button = Button2;
else if (event.xbutton.state & Mod1Mask) button = Button3;
else if (event.xbutton.state & Mod2Mask) button = Button3;
}
if (button == Button1) w3c_button = 0;
else if (button == Button2) w3c_button = 1;
else if (button == Button3) w3c_button = 2;
else w3c_button = button;
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&ev_json,
"{\"type\": \"mouse-press\", \"button\": %d, \"x\": %d, \"y\": %d}",
w3c_button, event.xbutton.x,
ifp->i->if_height - event.xbutton.y - 1);
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), ifp);
bu_vls_free(&ev_json);
}
break;
case ButtonRelease:
if (fb_get_interactive(ifp)) {
struct fb_event ev;
memset(&ev, 0, sizeof(ev));
ev.type = FB_EVENT_BUTTON_RELEASE;
ev.button = (int)event.xbutton.button;
ev.x = event.xbutton.x;
ev.y = ifp->i->if_height - event.xbutton.y - 1;
ev.state = (int)event.xbutton.state;
fb_enqueue_event(ifp, &ev);
}
break;
case MotionNotify:
if (fb_get_interactive(ifp)) {
struct fb_event ev;
memset(&ev, 0, sizeof(ev));
ev.type = FB_EVENT_MOTION;
ev.x = event.xmotion.x;
ev.y = ifp->i->if_height - event.xmotion.y - 1;
ev.state = (int)event.xmotion.state;
fb_enqueue_event(ifp, &ev);
}
break;
case KeyPress:
if (fb_get_interactive(ifp)) {
struct fb_event ev;
char keybuf[8] = {0};
KeySym keysym;
int n;
memset(&ev, 0, sizeof(ev));
ev.type = FB_EVENT_KEY_PRESS;
n = XLookupString(&event.xkey, keybuf, sizeof(keybuf), &keysym, NULL);
ev.keycode = (n > 0) ? (unsigned char)keybuf[0] : (int)keysym;
ev.state = (int)event.xkey.state;
fb_enqueue_event(ifp, &ev);
} else {
KeySym keysym = XLookupKeysym(&event.xkey, 0);
char *key_str = XKeysymToString(keysym);
if (key_str) {
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&ev_json,
"{\"type\": \"key-press\", \"key\": \"%s\"}", key_str);
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), ifp);
bu_vls_free(&ev_json);
}
}
break;
case ClientMessage:
if (fb_get_interactive(ifp)) {
struct fb_event ev = {FB_EVENT_CLOSE, 0, 0, 0, 0, 0};
fb_enqueue_event(ifp, &ev);
} else {
OGL(ifp)->alive = 0;
}
break;
case ConfigureNotify:
{
XConfigureEvent *conf = (XConfigureEvent *)&event;
if (conf->width == OGL(ifp)->win_width &&
conf->height == OGL(ifp)->win_height)
break;
ogl_configureWindow(ifp, conf->width, conf->height);
if (fb_get_interactive(ifp)) {
struct fb_event ev;
memset(&ev, 0, sizeof(ev));
ev.type = FB_EVENT_RESIZE;
ev.x = conf->width;
ev.y = conf->height;
fb_enqueue_event(ifp, &ev);
}
}
break;
default:
break;
}
OGL(ifp)->event_mask, &event)) {
switch (event.type) {
case Expose:
if (!OGL(ifp)->use_ext_ctrl)
expose_callback(ifp);
break;
case ButtonPress:
{
int button = (int) event.xbutton.button;
if (button == Button1) {
/* Check for single button mouse remap.
* ctrl-1 => 2
* meta-1 => 3
* cmdkey => 3
*/
if (event.xbutton.state & ControlMask) {
button = Button2;
} else if (event.xbutton.state & Mod1Mask) {
button = Button3;
} else if (event.xbutton.state & Mod2Mask) {
button = Button3;
}
}
switch (button) {
case Button1:
break;
case Button2:
{
int x, y;
register struct fb_pixel *oglp;
x = event.xbutton.x;
y = ifp->i->if_height - event.xbutton.y - 1;
if (x < 0 || y < 0) {
fb_log("No RGB (outside image viewport)\n");
break;
}
size_t memidx = (y * WIN(ifp)->mi_pixwidth) * sizeof(struct fb_pixel);
oglp = (struct fb_pixel *)&ifp->i->if_mem[memidx];
fb_log("At image (%d, %d), real RGB=(%3d %3d %3d)\n",
x, y, (int)oglp[x].red, (int)oglp[x].green, (int)oglp[x].blue);
break;
}
case Button3:
OGL(ifp)->alive = 0;
break;
default:
fb_log("unhandled mouse event\n");
break;
}
break;
}
case ConfigureNotify:
{
XConfigureEvent *conf = (XConfigureEvent *)&event;
if (conf->width == OGL(ifp)->win_width &&
conf->height == OGL(ifp)->win_height)
return;
ogl_configureWindow(ifp, conf->width, conf->height);
}
default:
break;
}
}
}
@@ -1117,9 +1021,8 @@ fb_ogl_open(struct fb *ifp, const char *file, int width, int height)
int mode, i;
long valuemask;
XSetWindowAttributes swa;
FB_CK_FB(ifp->i);
ogl_init_bindings();
FB_CK_FB(ifp->i);
/*
* First, attempt to determine operating mode for this open,
@@ -1311,7 +1214,7 @@ fb_ogl_open(struct fb *ifp, const char *file, int width, int height)
OGL(ifp)->vip->screen);
swa.event_mask = OGL(ifp)->event_mask =
ExposureMask | KeyPressMask | KeyReleaseMask |
ButtonPressMask | ButtonReleaseMask | PointerMotionMask | StructureNotifyMask;
ButtonPressMask | ButtonReleaseMask;
swa.colormap = OGL(ifp)->xcmap;
#define XCreateWindowDebug(display, parent, x, y, width, height, border_width, depth, class, visual, valuemask, attributes) \
@@ -1478,8 +1381,6 @@ ogl_open_existing(struct fb *ifp, int width, int height, struct fb_platform_spec
{
struct ogl_fb_info *ogl_internal = (struct ogl_fb_info *)fb_p->data;
BU_CKMAG(fb_p, FB_OGL_MAGIC, "ogl framebuffer");
ogl_init_bindings();
return open_existing(ifp, ogl_internal->dpy, ogl_internal->win,
ogl_internal->cmap, ogl_internal->vip, width, height, ogl_internal->glxc,
ogl_internal->double_buffer, ogl_internal->soft_cmap);
@@ -188,7 +188,8 @@ test_uncertain()
require(result.shells.empty() || (result.shells.front().orientation == ShellOrientation::Indeterminate &&
!result.shells.front().corrected), "invalid parameter-loop senses caused an incorrect face repair");
auto flat = box();
ON_Xform flatten = ON_Xform::DiagonalTransformation(1.0);
ON_Xform flatten;
flatten.DiagonalTransformation(1.0);
flatten[2][2] = 1.0e-13;
require(flat->Transform(flatten), "could not construct thin shell");
result = repair_brep_orientation(*flat);
+246 -241
View File
@@ -37,12 +37,6 @@ extern "C" {
#include "qtcad/defines.h"
#include "bindings.h"
extern "C" {
#include "bu/binding.h"
#include "bu/file.h"
#include "bu/log.h"
}
typedef void (*bounds_update_t)(struct bview *);
static std::unordered_map<struct bview *, bounds_update_t> drag_bounds_updates;
static std::unordered_map<struct bview *, long long> drag_update_ts;
@@ -74,256 +68,247 @@ restore_drag_bounds_update(struct bview *v, int refresh_bounds)
drag_update_ts.erase(v);
}
static int bindings_loaded = 0;
static void qtcad_action_toggle_adc(const char *, void *, void *event_data) {
struct bview *v = (struct bview *)event_data;
if (v) v->gv_s->gv_adc.draw = !v->gv_s->gv_adc.draw;
}
static void qtcad_action_toggle_model_axes(const char *, void *, void *event_data) {
struct bview *v = (struct bview *)event_data;
if (v) v->gv_s->gv_model_axes.draw = !v->gv_s->gv_model_axes.draw;
}
static void qtcad_action_toggle_view_axes(const char *, void *, void *event_data) {
struct bview *v = (struct bview *)event_data;
if (v) v->gv_s->gv_view_axes.draw = !v->gv_s->gv_view_axes.draw;
}
static void qtcad_action_view_set(const char *, void *registered_data, void *event_data) {
struct bview *v = (struct bview *)event_data;
const char *aet_str = (const char *)registered_data;
if (v && aet_str) {
bn_decode_vect(v->gv_aet, aet_str);
bv_mat_aet(v);
bv_update(v);
}
}
static void qtcad_action_mouse_release(const char *event_json, void *, void *event_data) {
struct bview *v = (struct bview *)event_data;
if (!v) return;
int cx = 0, cy = 0, mode = 0, dx = 1, dy = 1;
char *p;
if ((p = strstr(event_json, "\"cx\":"))) sscanf(p + 5, "%d", &cx);
if ((p = strstr(event_json, "\"cy\":"))) sscanf(p + 5, "%d", &cy);
if ((p = strstr(event_json, "\"mode\":"))) sscanf(p + 7, "%d", &mode);
int button = 0;
if ((p = strstr(event_json, "\"button\":"))) sscanf(p + 9, "%d", &button);
unsigned long long view_flags = BV_IDLE;
if (button == 0) { // Left
if (mode != BV_CENTER) { view_flags = BV_SCALE; dx = 10; dy = 5; }
else { view_flags = BV_CENTER; dx = cx; dy = cy; }
} else if (button == 2) { // Right
if (mode == BV_CENTER) return;
view_flags = BV_SCALE; dx = 1; dy = 2;
} else if (button == 1) { // Middle
view_flags = BV_CENTER; dx = cx; dy = cy;
} else {
return;
}
point_t keypt = VINIT_ZERO;
bv_adjust(v, dx, dy, keypt, 0, view_flags);
}
static void qtcad_action_adjust_view(const char *event_json, void *registered_data, void *event_data) {
struct bview *v = (struct bview *)event_data;
if (!v) return;
int dx = 0, dy = 0, mode = 0;
int view_flags = (int)(intptr_t)registered_data;
char *p;
if ((p = strstr(event_json, "\"dx\":"))) sscanf(p + 5, "%d", &dx);
if ((p = strstr(event_json, "\"dy\":"))) sscanf(p + 5, "%d", &dy);
if ((p = strstr(event_json, "\"mode\":"))) sscanf(p + 7, "%d", &mode);
if (view_flags == 0) { // dynamic fallback
view_flags = mode;
if (mode == BV_CENTER) view_flags = BV_SCALE;
}
if (view_flags == BV_SCALE) {
int mdelta = (abs(dx) > abs(dy)) ? dx : -dy;
int f = (int)(2*100*(double)abs(mdelta)/(double)v->gv_height);
if (mdelta > 0) { dy = 101 + f; dx = 100; }
else { dy = 99 - f; dx = 100; }
}
point_t center;
MAT_DELTAS_GET_NEG(center, v->gv_center);
if (view_flags & (BV_ROT | BV_TRANS | BV_SCALE))
suspend_drag_bounds_update(v);
bv_adjust(v, dx, dy, center, 0, view_flags);
}
static void qtcad_action_wheel_scale(const char *event_json, void *, void *event_data) {
struct bview *v = (struct bview *)event_data;
if (!v) return;
int delta = 0;
char *p;
if ((p = strstr(event_json, "\"delta\":"))) sscanf(p + 8, "%d", &delta);
int dx = 100 + delta;
int dy = 100;
point_t origin = VINIT_ZERO;
bv_adjust(v, dx, dy, origin, 0, BV_SCALE);
}
extern "C" QCAD_EXPORT int qtcad_load_bindings(const char *json_path) {
if (bu_binding_load(json_path) < 0) {
bu_log("libqtcad: Failed to load bindings from %s\n", json_path);
return -1;
}
if (!bu_binding_action_is_bound("dm", "view_toggle_adc") ||
!bu_binding_action_is_bound("dm", "view_2")) {
bu_log("libqtcad Validation Failed: Provided bindings missing required standard actions.\n");
return -1;
}
return 0;
}
static void qtcad_init_bindings(void) {
if (bindings_loaded) return;
bindings_loaded = 1;
bu_binding_register_action("view_toggle_adc", qtcad_action_toggle_adc, NULL);
bu_binding_register_action("view_toggle_model_axes", qtcad_action_toggle_model_axes, NULL);
bu_binding_register_action("view_toggle_view_axes", qtcad_action_toggle_view_axes, NULL);
bu_binding_register_action("view_2", qtcad_action_view_set, (void*)"35 -25 0");
bu_binding_register_action("view_3", qtcad_action_view_set, (void*)"35 25 0");
bu_binding_register_action("view_4", qtcad_action_view_set, (void*)"45 45 0");
bu_binding_register_action("view_5", qtcad_action_view_set, (void*)"145 25 0");
bu_binding_register_action("view_6", qtcad_action_view_set, (void*)"215 25 0");
bu_binding_register_action("view_7", qtcad_action_view_set, (void*)"325 25 0");
bu_binding_register_action("view_front", qtcad_action_view_set, (void*)"0 0 0");
bu_binding_register_action("view_top", qtcad_action_view_set, (void*)"270 90 0");
bu_binding_register_action("view_bottom", qtcad_action_view_set, (void*)"270 -90 0");
bu_binding_register_action("view_left", qtcad_action_view_set, (void*)"90 0 0");
bu_binding_register_action("view_rear", qtcad_action_view_set, (void*)"180 0 0");
bu_binding_register_action("view_right", qtcad_action_view_set, (void*)"270 0 0");
bu_binding_register_action("view_mouse_release_left", qtcad_action_mouse_release, NULL);
bu_binding_register_action("view_mouse_release_right", qtcad_action_mouse_release, NULL);
bu_binding_register_action("view_mouse_release_middle", qtcad_action_mouse_release, NULL);
bu_binding_register_action("view_mouse_drag_rotate", qtcad_action_adjust_view, (void*)(intptr_t)BV_ROT);
bu_binding_register_action("view_mouse_drag_translate", qtcad_action_adjust_view, (void*)(intptr_t)BV_TRANS);
bu_binding_register_action("view_mouse_drag_scale", qtcad_action_adjust_view, (void*)(intptr_t)BV_SCALE);
bu_binding_register_action("view_mouse_drag_adjust", qtcad_action_adjust_view, (void*)(intptr_t)0);
bu_binding_register_action("view_mouse_wheel_scale", qtcad_action_wheel_scale, NULL);
}
static void append_modifiers(struct bu_vls *vls, QInputEvent *e) {
if (!e->modifiers()) return;
bu_vls_printf(vls, ", \"modifiers\": [");
int added = 0;
if (e->modifiers().testFlag(Qt::ShiftModifier)) { bu_vls_printf(vls, "\"Shift\""); added=1; }
if (e->modifiers().testFlag(Qt::ControlModifier)) { if(added) bu_vls_printf(vls, ", "); bu_vls_printf(vls, "\"Control\""); added=1; }
if (e->modifiers().testFlag(Qt::AltModifier)) { if(added) bu_vls_printf(vls, ", "); bu_vls_printf(vls, "\"Alt\""); }
bu_vls_printf(vls, "]");
}
// TODO - look into QShortcut, see if it might be a better way
// to manage this
int CADkeyPressEvent(struct bview *v, int UNUSED(x_prev), int UNUSED(y_prev), QKeyEvent *k)
{
QTCAD_EVENT("keyPress", 1);
if (!v) return 0;
qtcad_init_bindings();
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
char key_str[8] = {0};
if (k->key() >= 0x20 && k->key() <= 0x7E) {
key_str[0] = (char)k->key();
} else {
QString kstr = QKeySequence(k->key()).toString();
strncpy(key_str, kstr.toStdString().c_str(), 7);
if (!v)
return 0;
#if 0
QString kstr = QKeySequence(k->key()).toString();
bu_log("%s\n", kstr.toStdString().c_str());
#endif
switch (k->key()) {
case 'A':
v->gv_s->gv_adc.draw = !v->gv_s->gv_adc.draw;
return 1;
case 'M':
v->gv_s->gv_model_axes.draw = !v->gv_s->gv_model_axes.draw;
return 1;
case 'V':
v->gv_s->gv_view_axes.draw = !v->gv_s->gv_view_axes.draw;
return 1;
case '2':
bn_decode_vect(v->gv_aet, "35 -25 0");
bv_mat_aet(v);
bv_update(v);
return 1;
case '3':
bn_decode_vect(v->gv_aet, "35 25 0");
bv_mat_aet(v);
bv_update(v);
return 1;
case '4':
bn_decode_vect(v->gv_aet, "45 45 0");
bv_mat_aet(v);
bv_update(v);
return 1;
case '5':
bn_decode_vect(v->gv_aet, "145 25 0");
bv_mat_aet(v);
bv_update(v);
return 1;
case '6':
bn_decode_vect(v->gv_aet, "215 25 0");
bv_mat_aet(v);
bv_update(v);
return 1;
case '7':
bn_decode_vect(v->gv_aet, "325 25 0");
bv_mat_aet(v);
bv_update(v);
return 1;
case 'F':
bn_decode_vect(v->gv_aet, "0 0 0");
bv_mat_aet(v);
bv_update(v);
return 1;
case 'T':
bn_decode_vect(v->gv_aet, "270 90 0");
bv_mat_aet(v);
bv_update(v);
return 1;
case 'B':
bn_decode_vect(v->gv_aet, "270 -90 0");
bv_mat_aet(v);
bv_update(v);
return 1;
case 'L':
bn_decode_vect(v->gv_aet, "90 0 0");
bv_mat_aet(v);
bv_update(v);
return 1;
case 'R':
if (k->modifiers().testFlag(Qt::ShiftModifier) == true) {
bn_decode_vect(v->gv_aet, "180 0 0");
} else {
bn_decode_vect(v->gv_aet, "270 0 0");
}
bv_mat_aet(v);
bv_update(v);
return 1;
default:
break;
}
bu_vls_sprintf(&ev_json, "{\"type\": \"key-press\", \"key\": \"%s\"", key_str);
append_modifiers(&ev_json, k);
bu_vls_printf(&ev_json, "}");
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
bu_vls_free(&ev_json);
return handled;
return 0;
}
int CADmousePressEvent(struct bview *v, int UNUSED(x_prev), int UNUSED(y_prev), QMouseEvent *e)
{
QTCAD_EVENT("mousePress", 1);
if (!v) return 0;
qtcad_init_bindings();
int button = -1;
if (e->buttons().testFlag(Qt::LeftButton)) button = 0;
else if (e->buttons().testFlag(Qt::MiddleButton)) button = 1;
else if (e->buttons().testFlag(Qt::RightButton)) button = 2;
if (button < 0) return 0;
if (!v)
return 0;
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-press\", \"button\": %d", button);
append_modifiers(&ev_json, e);
bu_vls_printf(&ev_json, "}");
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
bu_vls_free(&ev_json);
return handled;
// If we're intending the mouse motion to do the work,
// then the press has to be a no-op. If we're going
// to do configurable key bindings, this will take some
// thought - if we want unmodded left button to be a
// rotation, and Ctrl+Left to do something else, these
// checks are all going to have to be exact-flag-combo-only
// actions.
if (e->modifiers()) {
return 0;
}
if (e->buttons().testFlag(Qt::LeftButton)) {
//bu_log("Press Left\n");
}
if (e->buttons().testFlag(Qt::RightButton)) {
//bu_log("Press Right\n");
}
if (e->buttons().testFlag(Qt::MiddleButton)) {
//bu_log("Press Middle\n");
}
return 0;
}
int CADmouseReleaseEvent(struct bview *v, double x_press, double y_press, int UNUSED(x_prev), int UNUSED(y_prev), QMouseEvent *e, int mode)
{
QTCAD_EVENT("mouseRelease", 1);
if (!v) return 0;
qtcad_init_bindings();
if (!v)
return 0;
restore_drag_bounds_update(v, 1);
// If we're intending the mouse motion to do the work,
// then the release has to be a no-op. If we're going
// to do configurable key bindings, this will take some
// thought - if we want unmodded left button to be a
// rotation, and Ctrl+Left to do something else, these
// checks are all going to have to be exact-flag-combo-only
// actions.
if (e->modifiers()) {
return 0;
}
if (e->buttons().testFlag(Qt::LeftButton) || e->buttons().testFlag(Qt::RightButton)) {
return 0;
}
double cx, cy;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
double cx = e->x();
double cy = e->y();
cx = (double)e->x();
cy = (double)e->y();
#else
double cx = e->position().x();
double cy = e->position().y();
cx = e->position().x();
cy = e->position().y();
#endif
if ((fabs(cx - x_press) > 10) || (fabs(cy - y_press) > 10))
return 0;
if ((fabs(cx - x_press) > 10) || (fabs(cy - y_press) > 10)) return 0;
int dx = 1;
int dy = 1;
unsigned long long view_flags = BV_IDLE;
int button = -1;
if (e->button() == Qt::LeftButton) button = 0;
else if (e->button() == Qt::MiddleButton) button = 1;
else if (e->button() == Qt::RightButton) button = 2;
if (button < 0) return 0;
if (e->button() == Qt::LeftButton) {
//bu_log("Release Left\n");
if (mode != BV_CENTER) {
view_flags = BV_SCALE;
dx = 10;
dy = 5;
} else {
view_flags = BV_CENTER;
dx = (int)cx;
dy = (int)cy;
}
}
if (e->button() == Qt::RightButton) {
//bu_log("Release Right\n");
if (mode == BV_CENTER)
return 0;
view_flags = BV_SCALE;
dx = 1;
dy = 2;
}
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-release\", \"button\": %d, \"cx\": %d, \"cy\": %d, \"mode\": %d", button, (int)cx, (int)cy, mode);
append_modifiers(&ev_json, e);
bu_vls_printf(&ev_json, "}");
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
bu_vls_free(&ev_json);
return handled;
if (e->button() == Qt::MiddleButton) {
//bu_log("Release Center\n");
view_flags = BV_CENTER;
dx = (int)cx;
dy = (int)cy;
}
point_t keypt = VINIT_ZERO;
return bv_adjust(v, dx, dy, keypt, 0, view_flags);
}
int CADmouseMoveEvent(struct bview *v, int x_prev, int y_prev, QMouseEvent *e, int mode)
{
QTCAD_EVENT("mouseMove", 2);
if (!v) return 0;
qtcad_init_bindings();
if (x_prev == -INT_MAX) return 0;
if (!e->buttons().testFlag(Qt::LeftButton)) return 0;
if (!v)
return 0;
unsigned long long view_flags = BV_IDLE;
if (x_prev == -INT_MAX) {
//x_prev = e->x();
//y_prev = e->y();
return 0;
}
view_flags = mode;
if (mode == BV_CENTER)
view_flags = BV_SCALE;
if (e->buttons().testFlag(Qt::LeftButton)) {
//bu_log("Left\n");
if (e->modifiers().testFlag(Qt::ControlModifier)) {
//bu_log("Ctrl+Left\n");
view_flags = BV_ROT;
}
if (e->modifiers().testFlag(Qt::ShiftModifier)) {
//bu_log("Shift+Left\n");
view_flags = BV_TRANS;
}
if (e->modifiers().testFlag(Qt::ShiftModifier) && e->modifiers().testFlag(Qt::ControlModifier)) {
//bu_log("Ctrl+Shift+Left\n");
view_flags = BV_SCALE;
}
}
if (e->buttons().testFlag(Qt::MiddleButton)) {
//bu_log("Middle\n");
}
if (e->buttons().testFlag(Qt::RightButton)) {
//bu_log("Right\n");
}
if (!e->buttons().testFlag(Qt::LeftButton))
return 0;
long long now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
std::unordered_map<struct bview *, long long>::iterator ts_it = drag_update_ts.find(v);
if (ts_it != drag_update_ts.end() && (now_ms - ts_it->second) < drag_update_interval_ms) return -1;
if (ts_it != drag_update_ts.end() && (now_ms - ts_it->second) < drag_update_interval_ms)
return -1;
drag_update_ts[v] = now_ms;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
@@ -334,33 +319,53 @@ int CADmouseMoveEvent(struct bview *v, int x_prev, int y_prev, QMouseEvent *e, i
int dy = e->position().y() - y_prev;
#endif
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-move\", \"button\": 0, \"dx\": %d, \"dy\": %d, \"mode\": %d", dx, dy, mode);
append_modifiers(&ev_json, e);
bu_vls_printf(&ev_json, "}");
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
bu_vls_free(&ev_json);
return handled;
if (view_flags == BV_SCALE) {
// Build in some sensitivity to how much the mouse moved when doing
// a motion based scale
int mdelta = (abs(dx) > abs(dy)) ? dx : -dy;
int f = (int)(2*100*(double)abs(mdelta)/(double)v->gv_height);
if (mdelta > 0) {
dy = 101 + f;
dx = 100;
} else {
dy = 99 - f;
dx = 100;
}
}
// TODO - the key point and the mode/flags are all hardcoded
// right now, but eventually for shift grips they will need to
// respond to the various mod keys. The intent is to set flags
// based on which mod keys are set to allow bv_adjust to
// do the correct math.
point_t center;
MAT_DELTAS_GET_NEG(center, v->gv_center);
if (view_flags & (BV_ROT | BV_TRANS | BV_SCALE))
suspend_drag_bounds_update(v);
return bv_adjust(v, dx, dy, center, 0, view_flags);
}
int CADwheelEvent(struct bview *v, QWheelEvent *e)
{
QTCAD_EVENT("mouseWheel", 1);
if (!v) return 0;
qtcad_init_bindings();
if (!v)
return 0;
QPoint delta = e->angleDelta();
int mdelta = -1 * delta.y() / 8;
int mdelta = -1* delta.y() / 8;
int dx = 100 + mdelta;
int dy = 100;
point_t origin = VINIT_ZERO;
return bv_adjust(v, dx, dy, origin, 0, BV_SCALE);
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-wheel\", \"delta\": %d", mdelta);
append_modifiers(&ev_json, e);
bu_vls_printf(&ev_json, "}");
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
bu_vls_free(&ev_json);
return handled;
}
// Local Variables:
-1
View File
@@ -427,7 +427,6 @@ endif(BRLCAD_ENABLE_SPR)
set(LIBRT_PUBLIC_LIBS ${librt_deps} OPENNURBS::OPENNURBS)
set(
LIBRT_PRIVATE_LIBS
Threads::Threads
${OPENCL_LIBS}
mmesh::mmesh
REGEX::REGEX
+202 -468
View File
@@ -45,28 +45,24 @@
* intersecting closed surface), and total_chord is the sum of solid
* segment lengths.
*
* Chord endpoints are independent pseudorandom points on the bounding
* sphere. Reinitializing the generator with a fixed seed makes sampling
* reproducible without changing the ordinary Monte Carlo convergence
* behavior on which the estimator's stability checks rely.
* References:
* Li et al. (2003), "Using low-discrepancy sequences and the Crofton
* formula to compute surface areas of geometric models",
* Computer-Aided Design 35, 771-782.
*
* Liu et al. (2010), "Surface area estimation of digitized 3D objects
* using quasi-Monte Carlo methods", Pattern Recognition 43, 3900-3909.
*/
#include "common.h"
#include <chrono>
#include <climits>
#include <condition_variable>
#include <cstdint>
#include <exception>
#include <mutex>
#include <random>
#include <stdlib.h>
#include <math.h>
#include <stdexcept>
#include <random>
#include <string.h>
#include <thread>
#include <time.h>
#include <vector>
#include "vmath.h"
#include "bu/log.h"
@@ -104,15 +100,8 @@
#define RT_CROFTON_STABILITY_MIN_WINDOWS 2u
/** Fixed seed for reproducible estimates in tests and cross-platform runs. */
#define RT_CROFTON_RNG_SEED UINT64_C(0x9e3779b97f4a7c15)
#define RT_CROFTON_RNG_SEED 0x9e3779b97f4a7c15ULL
/** Two sphere coordinates for each of two chord endpoints. */
#define RT_CROFTON_RANDOM_VALUES_PER_RAY 4u
/** Convert the high 53 random bits exactly to a double in [0, 1). */
static constexpr unsigned int CROFTON_RANDOM_SHIFT = 11u;
static constexpr double CROFTON_RANDOM_SCALE =
1.0 / 9007199254740992.0;
/* ------------------------------------------------------------------ */
/* Internal types */
@@ -121,7 +110,6 @@ static constexpr double CROFTON_RANDOM_SCALE =
struct crofton_ray {
point_t r_pt;
vect_t r_dir;
size_t id;
};
struct crofton_shared {
@@ -129,19 +117,14 @@ struct crofton_shared {
size_t total_crossings; /* in+out hit events */
double total_chord; /* solid segment length sum (mm) */
size_t total_rays; /* rays fired (hits + misses) */
size_t ray_offset; /* first identifier in this sample stream */
point_t *points;
size_t point_count;
size_t point_capacity;
struct rt_crofton_segment *segments;
size_t segment_count;
size_t segment_capacity;
bool collect_points;
bool collect_segments;
};
struct crofton_worker_data {
struct application ap; /* per-CPU application struct */
struct application *ap; /* per-CPU application struct */
struct crofton_ray *rays; /* shared ray array (read-only) */
size_t start;
size_t end;
@@ -152,10 +135,6 @@ struct crofton_worker_data {
point_t *local_points;
size_t local_point_count;
size_t local_point_capacity;
struct rt_crofton_segment *local_segments;
size_t local_segment_count;
size_t local_segment_capacity;
size_t current_ray;
};
@@ -164,88 +143,46 @@ struct crofton_worker_data {
/* ------------------------------------------------------------------ */
static int
crofton_hit(struct application *ap, struct partition *PartHeadp,
struct seg *UNUSED(segs))
crofton_hit(struct application *ap, struct partition *PartHeadp, struct seg *UNUSED(segs))
{
struct crofton_worker_data *worker =
(struct crofton_worker_data *)ap->a_uptr;
struct crofton_worker_data *wd = (struct crofton_worker_data *)ap->a_uptr;
size_t crossings = 0;
double chord = 0.0;
double chord = 0.0;
struct partition *partition;
for (partition = PartHeadp->pt_forw; partition != PartHeadp;
partition = partition->pt_forw) {
fastf_t thickness = partition->pt_outhit->hit_dist -
partition->pt_inhit->hit_dist;
crossings += 2;
chord += thickness;
if (worker->shared->collect_points) {
if (worker->local_point_count + 2 >
worker->local_point_capacity) {
const size_t initial_capacity = 1024;
size_t new_capacity = worker->local_point_capacity ?
2 * worker->local_point_capacity : initial_capacity;
while (new_capacity < worker->local_point_count + 2)
new_capacity *= 2;
worker->local_points = (point_t *)bu_realloc(
worker->local_points,
new_capacity * sizeof(point_t),
"Crofton worker hit points");
worker->local_point_capacity = new_capacity;
}
VJOIN1(worker->local_points[worker->local_point_count],
ap->a_ray.r_pt, partition->pt_inhit->hit_dist,
ap->a_ray.r_dir);
worker->local_point_count++;
VJOIN1(worker->local_points[worker->local_point_count],
ap->a_ray.r_pt, partition->pt_outhit->hit_dist,
ap->a_ray.r_dir);
worker->local_point_count++;
}
if (worker->shared->collect_segments) {
if (worker->local_segment_count + 1 >
worker->local_segment_capacity) {
const size_t initial_capacity = 512;
size_t new_capacity = worker->local_segment_capacity ?
2 * worker->local_segment_capacity : initial_capacity;
worker->local_segments =
(struct rt_crofton_segment *)bu_realloc(
worker->local_segments,
new_capacity * sizeof(struct rt_crofton_segment),
"Crofton worker segments");
worker->local_segment_capacity = new_capacity;
}
struct rt_crofton_segment *sample =
&worker->local_segments[worker->local_segment_count];
struct soltab *in_solid =
partition->pt_inseg->seg_stp;
struct soltab *out_solid =
partition->pt_outseg->seg_stp;
VJOIN1(sample->in_point, ap->a_ray.r_pt,
partition->pt_inhit->hit_dist, ap->a_ray.r_dir);
VJOIN1(sample->out_point, ap->a_ray.r_pt,
partition->pt_outhit->hit_dist, ap->a_ray.r_dir);
RT_HIT_NORMAL(sample->in_normal, partition->pt_inhit,
in_solid, &ap->a_ray, partition->pt_inflip);
RT_HIT_NORMAL(sample->out_normal, partition->pt_outhit,
out_solid, &ap->a_ray, partition->pt_outflip);
sample->thickness = thickness;
sample->ray_id = worker->rays[worker->current_ray].id;
if (!VNEAR_ZERO(sample->in_normal, VUNITIZE_TOL) &&
!VNEAR_ZERO(sample->out_normal, VUNITIZE_TOL))
worker->local_segment_count++;
}
struct partition *pp;
for (pp = PartHeadp->pt_forw; pp != PartHeadp; pp = pp->pt_forw) {
/* Each partition contributes an in-hit and an out-hit */
crossings += 2;
chord += pp->pt_outhit->hit_dist - pp->pt_inhit->hit_dist;
if (wd->shared->collect_points) {
if (wd->local_point_count + 2 > wd->local_point_capacity) {
const size_t initial_capacity = 1024;
size_t new_capacity = wd->local_point_capacity ?
2 * wd->local_point_capacity : initial_capacity;
while (new_capacity < wd->local_point_count + 2)
new_capacity *= 2;
wd->local_points = (point_t *)bu_realloc(wd->local_points,
new_capacity * sizeof(point_t), "Crofton worker hit points");
wd->local_point_capacity = new_capacity;
}
VJOIN1(wd->local_points[wd->local_point_count], ap->a_ray.r_pt,
pp->pt_inhit->hit_dist, ap->a_ray.r_dir);
wd->local_point_count++;
VJOIN1(wd->local_points[wd->local_point_count], ap->a_ray.r_pt,
pp->pt_outhit->hit_dist, ap->a_ray.r_dir);
wd->local_point_count++;
}
}
worker->local_crossings += crossings;
worker->local_chord += chord;
worker->local_rays++;
wd->local_crossings += crossings;
wd->local_chord += chord;
wd->local_rays += 1;
return 1;
}
static int
crofton_miss(struct application *ap)
{
@@ -256,273 +193,159 @@ crofton_miss(struct application *ap)
}
/* ------------------------------------------------------------------ */
/* Parallel worker */
/* ------------------------------------------------------------------ */
static void
crofton_worker(int id, void *data)
{
struct crofton_worker_data *wd = &((struct crofton_worker_data *)data)[id - 1];
struct application *ap = wd->ap;
struct crofton_ray *rays = wd->rays;
for (size_t i = wd->start; i < wd->end; i++) {
VMOVE(ap->a_ray.r_pt, rays[i].r_pt);
VMOVE(ap->a_ray.r_dir, rays[i].r_dir);
/* r_dir is already unit-length (set during ray generation) */
rt_shootray(ap);
}
}
/* ------------------------------------------------------------------ */
/* Point / ray generation */
/* ------------------------------------------------------------------ */
static double
crofton_random01(std::mt19937_64 &rng)
crofton_rand01(std::mt19937_64 &rng)
{
return static_cast<double>(rng() >> CROFTON_RANDOM_SHIFT) *
CROFTON_RANDOM_SCALE;
return std::generate_canonical<double, 53>(rng);
}
static void
random_point_on_sphere(double radius, const point_t center, double u,
double v, point_t point)
random_point_on_sphere(double radius, const point_t center, point_t out, std::mt19937_64 &rng)
{
double z = 1.0 - 2.0 * u;
double radial = sqrt(std::max(0.0, 1.0 - z * z));
double angle = 2.0 * M_PI * v;
point[X] = center[X] + radius * radial * cos(angle);
point[Y] = center[Y] + radius * radial * sin(angle);
point[Z] = center[Z] + radius * z;
double theta = 2.0 * M_PI * crofton_rand01(rng);
double phi = acos(2.0 * crofton_rand01(rng) - 1.0);
double sp = sin(phi);
out[X] = center[X] + radius * sp * cos(theta);
out[Y] = center[Y] + radius * sp * sin(theta);
out[Z] = center[Z] + radius * cos(phi);
}
/**
* Generate independent random chord endpoints. Pairing consecutive IID
* sphere points has the same distribution as shuffling a point array before
* pairing, but consumes exactly four random values per ray. That fixed
* stride lets callers continue the deterministic stream with ray_offset.
* Generate @p nrays chord rays from 2*nrays random points on the
* bounding sphere. Each ray connects one randomly chosen point to
* another.
*/
static void
generate_rays(struct crofton_ray *rays, size_t ray_count, size_t first_id,
double radius, const point_t center, std::mt19937_64 &rng)
generate_rays(struct crofton_ray *rays, size_t nrays,
double radius, const point_t center, std::mt19937_64 &rng)
{
for (size_t i = 0; i < ray_count; i++) {
point_t endpoint;
random_point_on_sphere(radius, center, crofton_random01(rng),
crofton_random01(rng), rays[i].r_pt);
random_point_on_sphere(radius, center, crofton_random01(rng),
crofton_random01(rng), endpoint);
VSUB2(rays[i].r_dir, endpoint, rays[i].r_pt);
VUNITIZE(rays[i].r_dir);
rays[i].id = first_id + i;
size_t npts = nrays * 2;
point_t *pts = (point_t *)bu_calloc(npts, sizeof(point_t), "crofton pts");
for (size_t i = 0; i < npts; i++)
random_point_on_sphere(radius, center, pts[i], rng);
/* Shuffle so pairing is random */
for (size_t i = npts - 1; i > 0; i--) {
size_t j = (size_t)(crofton_rand01(rng) * (i + 1));
if (j > i) j = i;
point_t tmp;
VMOVE(tmp, pts[i]);
VMOVE(pts[i], pts[j]);
VMOVE(pts[j], tmp);
}
for (size_t i = 0; i < nrays; i++) {
VMOVE(rays[i].r_pt, pts[i * 2]);
VSUB2(rays[i].r_dir, pts[i * 2 + 1], pts[i * 2]);
VUNITIZE(rays[i].r_dir);
}
bu_free(pts, "crofton pts");
}
/* ------------------------------------------------------------------ */
/* Reusable parallel workers */
/* One iteration: fire nrays, accumulate into shared */
/* ------------------------------------------------------------------ */
class CroftonWorkerPool
{
public:
CroftonWorkerPool(const struct application *ap_template,
struct resource *resources, size_t worker_count) :
count(worker_count), data(worker_count)
{
for (size_t i = 0; i < count; i++) {
data[i].ap = *ap_template;
data[i].ap.a_resource = &resources[i];
data[i].shared = NULL;
data[i].ap.a_uptr = &data[i];
}
try {
executor = std::thread(&CroftonWorkerPool::run_workers, this);
} catch (...) {
free_worker_points();
throw;
}
std::unique_lock<std::mutex> lock(mutex);
if (!workers_started.wait_for(lock, worker_start_timeout,
[this]() { return registered == count; })) {
stopping = true;
work_ready.notify_all();
lock.unlock();
executor.join();
free_worker_points();
throw std::runtime_error("worker startup timed out");
}
}
~CroftonWorkerPool()
{
stop_and_join();
free_worker_points();
}
CroftonWorkerPool(const CroftonWorkerPool &) = delete;
CroftonWorkerPool &operator=(const CroftonWorkerPool &) = delete;
void run(struct crofton_ray *rays, size_t nrays,
struct crofton_shared *shared)
{
const size_t rays_per_worker = nrays / count;
std::unique_lock<std::mutex> lock(mutex);
for (size_t i = 0; i < count; i++) {
struct crofton_worker_data &wd = data[i];
wd.rays = rays;
wd.start = i * rays_per_worker;
wd.end = (i == count - 1) ? nrays : (i + 1) * rays_per_worker;
wd.shared = shared;
wd.local_crossings = 0;
wd.local_chord = 0.0;
wd.local_rays = 0;
wd.local_point_count = 0;
wd.local_segment_count = 0;
}
completed = 0;
generation++;
work_ready.notify_all();
work_done.wait(lock, [this]() { return completed == count; });
}
const std::vector<struct crofton_worker_data> &worker_data() const
{
return data;
}
private:
/* Native thread creation should complete promptly. A bounded startup wait
* turns partial bu_parallel startup into a reported failure, not a hang. */
static constexpr std::chrono::seconds worker_start_timeout{5};
static void worker_entry(int UNUSED(id), void *context)
{
CroftonWorkerPool *pool = static_cast<CroftonWorkerPool *>(context);
size_t slot = 0;
{
std::lock_guard<std::mutex> lock(pool->mutex);
slot = pool->registered++;
if (pool->registered == pool->count)
pool->workers_started.notify_one();
}
pool->worker(slot);
}
void run_workers()
{
bu_parallel(worker_entry, count, this);
}
void worker(size_t slot)
{
size_t observed_generation = 0;
std::unique_lock<std::mutex> lock(mutex);
while (true) {
work_ready.wait(lock, [this, observed_generation]() {
return stopping || generation != observed_generation;
});
if (stopping)
return;
observed_generation = generation;
struct crofton_worker_data *wd = &data[slot];
lock.unlock();
for (size_t i = wd->start; i < wd->end; i++) {
wd->current_ray = i;
VMOVE(wd->ap.a_ray.r_pt, wd->rays[i].r_pt);
VMOVE(wd->ap.a_ray.r_dir, wd->rays[i].r_dir);
rt_shootray(&wd->ap);
}
lock.lock();
completed++;
if (completed == count)
work_done.notify_one();
}
}
void stop_and_join()
{
{
std::lock_guard<std::mutex> lock(mutex);
stopping = true;
work_ready.notify_all();
}
if (executor.joinable())
executor.join();
}
void free_worker_points()
{
for (struct crofton_worker_data &wd : data) {
if (wd.local_points)
bu_free(wd.local_points, "Crofton worker hit points");
if (wd.local_segments)
bu_free(wd.local_segments, "Crofton worker segments");
wd.local_points = NULL;
wd.local_segments = NULL;
}
}
size_t count;
std::vector<struct crofton_worker_data> data;
std::thread executor;
std::mutex mutex;
std::condition_variable workers_started;
std::condition_variable work_ready;
std::condition_variable work_done;
size_t registered = 0;
size_t generation = 0;
size_t completed = 0;
bool stopping = false;
};
static void
do_one_iteration(CroftonWorkerPool &pool, size_t ray_count, double radius,
const point_t center, struct crofton_shared *shared,
std::mt19937_64 &rng)
do_one_iteration(struct application *ap_template,
struct resource *resources,
size_t nrays,
double radius,
const point_t center,
struct crofton_shared *shared,
std::mt19937_64 &rng)
{
struct crofton_ray *rays = (struct crofton_ray *)bu_calloc(
ray_count, sizeof(struct crofton_ray), "Crofton rays");
generate_rays(rays, ray_count, shared->ray_offset + shared->total_rays,
radius, center, rng);
pool.run(rays, ray_count, shared);
nrays, sizeof(struct crofton_ray), "crofton rays");
for (const struct crofton_worker_data &worker : pool.worker_data()) {
shared->total_crossings += worker.local_crossings;
shared->total_chord += worker.local_chord;
shared->total_rays += worker.local_rays;
generate_rays(rays, nrays, radius, center, rng);
if (worker.local_point_count) {
const size_t needed =
shared->point_count + worker.local_point_count;
if (needed > shared->point_capacity) {
size_t new_capacity = shared->point_capacity ?
2 * shared->point_capacity : needed;
if (new_capacity < needed)
new_capacity = needed;
shared->points = (point_t *)bu_realloc(shared->points,
new_capacity * sizeof(point_t),
"Crofton hit points");
shared->point_capacity = new_capacity;
}
memcpy(&shared->points[shared->point_count],
worker.local_points,
worker.local_point_count * sizeof(point_t));
shared->point_count = needed;
}
size_t ncpus = bu_avail_cpus();
if (ncpus < 1) ncpus = 1;
if (ncpus > MAX_PSW) ncpus = MAX_PSW;
if (worker.local_segment_count) {
const size_t needed =
shared->segment_count + worker.local_segment_count;
if (needed > shared->segment_capacity) {
size_t new_capacity = shared->segment_capacity ?
2 * shared->segment_capacity : needed;
if (new_capacity < needed)
new_capacity = needed;
shared->segments =
(struct rt_crofton_segment *)bu_realloc(
shared->segments,
new_capacity * sizeof(struct rt_crofton_segment),
"Crofton segments");
shared->segment_capacity = new_capacity;
}
memcpy(&shared->segments[shared->segment_count],
worker.local_segments,
worker.local_segment_count *
sizeof(struct rt_crofton_segment));
shared->segment_count = needed;
}
struct crofton_worker_data *wdata = (struct crofton_worker_data *)bu_calloc(
ncpus, sizeof(struct crofton_worker_data), "crofton wdata");
size_t per_cpu = nrays / ncpus;
for (size_t i = 0; i < ncpus; i++) {
struct application *a = (struct application *)bu_calloc(
1, sizeof(struct application), "crofton app");
*a = *ap_template; /* struct copy */
a->a_resource = &resources[i];
struct crofton_worker_data *wd = &wdata[i];
wd->ap = a;
wd->rays = rays;
wd->start = i * per_cpu;
wd->end = (i == ncpus - 1) ? nrays : (i + 1) * per_cpu;
wd->shared = shared;
a->a_uptr = wd;
}
bu_free(rays, "Crofton rays");
bu_parallel(crofton_worker, (int)ncpus, (void *)wdata);
for (size_t i = 0; i < ncpus; i++) {
shared->total_crossings += wdata[i].local_crossings;
shared->total_chord += wdata[i].local_chord;
shared->total_rays += wdata[i].local_rays;
if (wdata[i].local_point_count) {
const size_t needed = shared->point_count + wdata[i].local_point_count;
if (needed > shared->point_capacity) {
size_t new_capacity = shared->point_capacity ?
2 * shared->point_capacity : needed;
if (new_capacity < needed)
new_capacity = needed;
shared->points = (point_t *)bu_realloc(shared->points,
new_capacity * sizeof(point_t), "Crofton hit points");
shared->point_capacity = new_capacity;
}
memcpy(&shared->points[shared->point_count], wdata[i].local_points,
wdata[i].local_point_count * sizeof(point_t));
shared->point_count = needed;
}
}
for (size_t i = 0; i < ncpus; i++) {
if (wdata[i].local_points)
bu_free(wdata[i].local_points, "Crofton worker hit points");
bu_free(wdata[i].ap, "crofton app");
}
bu_free(wdata, "crofton wdata");
bu_free(rays, "crofton rays");
}
/* ------------------------------------------------------------------ */
/* Public API: rt_crofton_shoot */
/* ------------------------------------------------------------------ */
@@ -552,8 +375,7 @@ do_one_iteration(CroftonWorkerPool &pool, size_t ray_count, double radius,
* sampling (>= 0) on success; -1 on bad arguments.
*/
static int
crofton_shoot_impl(struct rt_crofton_result *out_result,
point_t **out_points,
crofton_shoot_impl(point_t **out_points,
size_t *out_point_count,
double *out_surf_area,
double *out_volume,
@@ -562,22 +384,13 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
point_t out_obb[8],
struct rt_i *rtip,
const struct rt_crofton_params *params,
size_t ray_offset,
const fastf_t *bbox_min,
const fastf_t *bbox_max)
{
if (!rtip ||
(!out_result && !out_surf_area && !out_volume && !out_aabb_min &&
!out_obb && !out_points) ||
((out_points == NULL) != (out_point_count == NULL)) ||
((out_aabb_min == NULL) != (out_aabb_max == NULL)) ||
(out_result && out_result->segments) ||
ray_offset > UINT64_MAX / RT_CROFTON_RANDOM_VALUES_PER_RAY)
return -1;
if (out_result) {
struct rt_crofton_result empty = RT_CROFTON_RESULT_INIT;
*out_result = empty;
}
if (!rtip || (!out_surf_area && !out_volume && !out_aabb_min && !out_obb && !out_points) ||
((out_points == NULL) != (out_point_count == NULL)) ||
((out_aabb_min == NULL) != (out_aabb_max == NULL)))
return -1;
if (out_points) {
*out_points = NULL;
*out_point_count = 0;
@@ -660,14 +473,10 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
if (!use_default && max_rays > 0 && stability_mm <= 0.0 && time_ms <= 0.0)
batch = max_rays; /* single-iteration mode */
size_t ncpus = bu_avail_cpus();
if (ncpus < 1) ncpus = 1;
if (ncpus > MAX_PSW) ncpus = MAX_PSW;
/* ---- Initialize per-CPU resources ---- */
struct resource *resources = (struct resource *)bu_calloc(
ncpus, sizeof(struct resource), "crofton resources");
for (size_t i = 0; i < ncpus; i++)
MAX_PSW, sizeof(struct resource), "crofton resources");
for (int i = 0; i < MAX_PSW; i++)
rt_init_resource(&resources[i], i, rtip);
/* ---- Set up application template ---- */
@@ -685,32 +494,10 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
/* ---- Shared accumulator ---- */
struct crofton_shared shared;
memset(&shared, 0, sizeof(shared));
shared.ray_offset = ray_offset;
shared.collect_points =
out_points != NULL || out_aabb_min != NULL || out_obb != NULL;
shared.collect_segments = out_result != NULL;
const bool stabilize_surface =
out_surf_area != NULL || out_result != NULL ||
shared.collect_points;
CroftonWorkerPool *pool = NULL;
try {
pool = new CroftonWorkerPool(&ap, resources, ncpus);
} catch (const std::exception &e) {
bu_log("rt_crofton: unable to start worker pool: %s\n", e.what());
for (size_t i = 0; i < ncpus; i++) {
if (resources[i].re_magic == RESOURCE_MAGIC) {
rt_clean_resource_basic(rtip, &resources[i]);
BU_PTBL_SET(&rtip->rti_resources, i, NULL);
}
}
bu_free(resources, "crofton resources");
return -1;
}
shared.collect_points = out_points != NULL || out_aabb_min != NULL || out_obb != NULL;
const bool stabilize_surface = out_surf_area != NULL || shared.collect_points;
std::mt19937_64 rng(RT_CROFTON_RNG_SEED);
rng.discard(static_cast<uint64_t>(ray_offset) *
RT_CROFTON_RANDOM_VALUES_PER_RAY);
const double FOUR_PI = 4.0 * M_PI;
const double PI = M_PI;
@@ -734,7 +521,7 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
curr_rays = batch;
}
do_one_iteration(*pool, curr_rays, R, center, &shared, rng);
do_one_iteration(&ap, resources, curr_rays, R, center, &shared, rng);
iteration++;
if (shared.total_rays == 0) break;
@@ -775,12 +562,9 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
} else {
/* ---- Parametric loop: n_rays / stability_mm / time_ms ---- */
double prev_r_sa = -1.0, prev_r_v = -1.0;
double prev_sa = -1.0, prev_v = -1.0;
size_t total_fired = 0;
size_t stable_windows = 0;
int64_t t0 = (time_ms > 0.0) ? bu_gettime() : 0;
const bool use_relative_stability = out_result && time_ms > 0.0 &&
stability_mm <= 0.0;
for (;;) {
/* Time-budget check before firing */
@@ -797,7 +581,7 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
if (fire > remaining) fire = remaining;
}
do_one_iteration(*pool, fire, R, center, &shared, rng);
do_one_iteration(&ap, resources, fire, R, center, &shared, rng);
total_fired += fire;
if (shared.total_rays == 0) break;
@@ -810,39 +594,27 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
/ (double)shared.total_rays;
/* Stability check */
if ((stability_mm > 0.0 || use_relative_stability) &&
if (stability_mm > 0.0 &&
shared.total_rays >= RT_CROFTON_STABILITY_MIN_RAYS &&
shared.total_crossings >= RT_CROFTON_STABILITY_MIN_CROSSINGS) {
bool sa_ok = false;
bool v_ok = false;
if (use_relative_stability) {
sa_ok = !stabilize_surface || (prev_sa > 0.0 &&
fabs(curr_est_sa - prev_sa) / prev_sa * 100.0 <=
RT_CROFTON_DEFAULT_THRESHOLD);
v_ok = !out_volume || (prev_v > 0.0 &&
fabs(curr_est_v - prev_v) / prev_v * 100.0 <=
RT_CROFTON_DEFAULT_THRESHOLD);
prev_sa = curr_est_sa;
prev_v = curr_est_v;
} else {
double r_sa = (curr_est_sa > 0.0) ?
sqrt(curr_est_sa * INV_4PI) : 0.0;
double r_v = (curr_est_v > 0.0) ?
cbrt(curr_est_v * INV_4PI3) : 0.0;
sa_ok = !stabilize_surface || (prev_r_sa >= 0.0 &&
fabs(r_sa - prev_r_sa) < stability_mm);
v_ok = !out_volume || (prev_r_v >= 0.0 &&
fabs(r_v - prev_r_v) < stability_mm);
prev_r_sa = r_sa;
prev_r_v = r_v;
}
if (sa_ok && v_ok) {
stable_windows++;
if (stable_windows >= RT_CROFTON_STABILITY_MIN_WINDOWS)
break;
} else {
stable_windows = 0;
double r_sa = (curr_est_sa > 0.0) ? sqrt(curr_est_sa * INV_4PI) : 0.0;
double r_v = (curr_est_v > 0.0) ? cbrt(curr_est_v * INV_4PI3) : 0.0;
if (prev_r_sa >= 0.0) {
int sa_ok = (!stabilize_surface) ||
fabs(r_sa - prev_r_sa) < stability_mm;
int v_ok = (!out_volume) ||
fabs(r_v - prev_r_v) < stability_mm;
if (sa_ok && v_ok) {
stable_windows++;
if (stable_windows >= RT_CROFTON_STABILITY_MIN_WINDOWS)
break;
} else {
stable_windows = 0;
}
}
prev_r_sa = r_sa;
prev_r_v = r_v;
}
/* Time-budget check after firing */
@@ -852,16 +624,8 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
}
}
if (out_surf_area)
*out_surf_area = curr_est_sa;
if (out_volume)
*out_volume = curr_est_v;
if (out_result) {
out_result->surface_area = curr_est_sa;
out_result->volume = curr_est_v;
out_result->ray_count = shared.total_rays;
out_result->crossing_count = shared.total_crossings;
}
if (out_surf_area) *out_surf_area = curr_est_sa;
if (out_volume) *out_volume = curr_est_v;
if (out_aabb_min) {
for (size_t i = 0; i < shared.point_count; ++i)
VMINMAX(*out_aabb_min, *out_aabb_max, shared.points[i]);
@@ -877,19 +641,11 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
bounds_ret = -1;
}
if (out_points && !bounds_ret) {
*out_points = shared.points;
*out_point_count = shared.point_count;
*out_points = shared.points;
*out_point_count = shared.point_count;
} else if (shared.points) {
bu_free(shared.points, "Crofton hit points");
bu_free(shared.points, "Crofton hit points");
}
if (out_result && !bounds_ret) {
out_result->segments = shared.segments;
out_result->segment_count = shared.segment_count;
} else if (shared.segments) {
bu_free(shared.segments, "Crofton segments");
}
delete pool;
/* Clean each resource and NULL out its slot in rtip->rti_resources.
* This is necessary because crofton_from_ip calls rt_i_destroy(rtip)
@@ -901,7 +657,7 @@ crofton_shoot_impl(struct rt_crofton_result *out_result,
*
* By setting the slot to NULL we let rt_i_destroy's cleanup skip it,
* and then we can safely bu_free the resources array. */
for (size_t i = 0; i < ncpus; i++) {
for (int i = 0; i < MAX_PSW; i++) {
if (resources[i].re_magic == RESOURCE_MAGIC) {
rt_clean_resource_basic(rtip, &resources[i]);
BU_PTBL_SET(&rtip->rti_resources, i, NULL);
@@ -925,31 +681,9 @@ rt_crofton_shoot(double *out_surf_area, double *out_volume,
const struct rt_crofton_params *params,
const fastf_t *bbox_min, const fastf_t *bbox_max)
{
return crofton_shoot_impl(NULL, out_points, out_point_count,
out_surf_area, out_volume, out_aabb_min, out_aabb_max, out_obb,
rtip, params, 0, bbox_min, bbox_max);
}
int
rt_crofton_collect(struct rt_crofton_result *result, struct rt_i *rtip,
const struct rt_crofton_params *params, size_t ray_offset,
const fastf_t *bbox_min, const fastf_t *bbox_max)
{
if (!result)
return -1;
return crofton_shoot_impl(result, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, rtip, params, ray_offset, bbox_min, bbox_max);
}
void
rt_crofton_result_free(struct rt_crofton_result *result)
{
if (!result)
return;
if (result->segments)
bu_free(result->segments, "Crofton segments");
struct rt_crofton_result empty = RT_CROFTON_RESULT_INIT;
*result = empty;
return crofton_shoot_impl(out_points, out_point_count, out_surf_area,
out_volume, out_aabb_min, out_aabb_max, out_obb, rtip, params,
bbox_min, bbox_max);
}
@@ -1087,7 +821,7 @@ crofton_from_ip_n(const struct rt_db_internal *ip,
double sa = 0.0;
double vol = 0.0;
(void)rt_crofton_shoot(&sa, &vol, NULL, NULL, NULL, NULL, NULL,
rtip, params, NULL, NULL);
rtip, params, NULL, NULL);
if (out_sa) *out_sa = sa;
if (out_vol) *out_vol = vol;
+59 -9
View File
@@ -1543,7 +1543,8 @@ permute_cell(point_t A,
struct dsp_specific *dsp,
struct dsp_rpp *dsp_rpp)
{
point_t tmp;
int x, y;
#ifdef FULL_DSP_DEBUGGING
if (RT_G_DEBUG & RT_DEBUG_HF) {
@@ -1557,8 +1558,10 @@ permute_cell(point_t A,
switch (dsp->dsp_i.dsp_cuttype) {
case DSP_CUT_DIR_llUR:
return DSP_CUT_DIR_llUR;
break;
case DSP_CUT_DIR_ADAPT: {
point_t tmp;
size_t cx = (size_t)dsp_rpp->dsp_min[X];
size_t cy = (size_t)dsp_rpp->dsp_min[Y];
@@ -1567,8 +1570,28 @@ permute_cell(point_t A,
dsp_rpp->dsp_min[X],
dsp_rpp->dsp_min[Y]);
/* The rpp spans one grid cell; use its indices to select the
* lower-curvature diagonal.
/*
* We look at the points in the diagonal next cells to
* determine the curvature along each diagonal of this
* cell. This cell is divided into two triangles by
* cutting across the cell in the direction of least
* curvature.
*
* * * * *
* \ /
* \C D/
* * *--* *
* |\/|
* |/\|
* * *--* *
* /A B\
* / \
* * * * *
*/
/* leaf-cell precondition: the rpp spans exactly one grid unit, so
* (cx, cy) plus the cell counts fully describe the curvature
* stencil that rt_dsp_cell_cut() recomputes.
*/
BU_ASSERT(dsp_rpp->dsp_max[X] == dsp_rpp->dsp_min[X] + 1);
BU_ASSERT(dsp_rpp->dsp_max[Y] == dsp_rpp->dsp_min[Y] + 1);
@@ -1576,16 +1599,15 @@ permute_cell(point_t A,
if (rt_dsp_cell_cut(&dsp->dsp_i, cx, cy,
(size_t)dsp->xsiz, (size_t)dsp->ysiz)
== DSP_CUT_DIR_llUR) {
/* A-D cut is fine, no need to permute */
if (RT_G_DEBUG & RT_DEBUG_HF)
bu_log("A-D cut\n");
return DSP_CUT_DIR_llUR;
}
}
/* fall through */
case DSP_CUT_DIR_ULlr:
/* Both B-C modes need the same corner order. The top triangles
* must keep outward (+Z) normals for the entry/exit state machine.
*/
/* prefer the B-C cut */
VMOVE(tmp, A);
VMOVE(A, B);
VMOVE(B, D);
@@ -1593,7 +1615,35 @@ permute_cell(point_t A,
VMOVE(C, tmp);
if (RT_G_DEBUG & RT_DEBUG_HF)
bu_log("B-C cut\n");
return DSP_CUT_DIR_ULlr;
break;
}
case DSP_CUT_DIR_ULlr:
/* assign the values for the corner points
*
* D----C
* | |
* | |
* | |
* B----A
*/
x = dsp_rpp->dsp_min[X];
y = dsp_rpp->dsp_min[Y];
VSET(B, x, y, DSP(&dsp->dsp_i, x, y));
x = dsp_rpp->dsp_max[X];
VSET(A, x, y, DSP(&dsp->dsp_i, x, y));
y = dsp_rpp->dsp_max[Y];
VSET(C, x, y, DSP(&dsp->dsp_i, x, y));
x = dsp_rpp->dsp_min[X];
VSET(D, x, y, DSP(&dsp->dsp_i, x, y));
return DSP_CUT_DIR_ULlr;
break;
}
bu_log("%s:%d Unknown DSP cut direction: %d\n",
__FILE__, __LINE__, dsp->dsp_i.dsp_cuttype);
-108
View File
@@ -43,18 +43,6 @@ rel_err(double estimated, double exact)
}
static int
crofton_segments_equal(const struct rt_crofton_segment *left,
const struct rt_crofton_segment *right)
{
return left->ray_id == right->ray_id &&
NEAR_EQUAL(left->thickness, right->thickness, SMALL_FASTF) &&
VNEAR_EQUAL(left->in_point, right->in_point, SMALL_FASTF) &&
VNEAR_EQUAL(left->in_normal, right->in_normal, SMALL_FASTF) &&
VNEAR_EQUAL(left->out_point, right->out_point, SMALL_FASTF) &&
VNEAR_EQUAL(left->out_normal, right->out_normal, SMALL_FASTF);
}
static int
verify_crofton_estimates(void)
{
@@ -391,102 +379,6 @@ run_convergence_case(struct db_i *dbip,
&sample_points, &sample_count,
rtip, &p, NULL, NULL);
run_sec = (double)(bu_gettime() - t0) / 1000000.0;
if (i == 0) {
struct rt_crofton_result samples = RT_CROFTON_RESULT_INIT;
const size_t ray_offset = 4096u;
const size_t continuation_rays = 256u;
struct rt_crofton_params sample_params =
{ray_offset + continuation_rays, 0.0, 0.0};
int sample_ret = rt_crofton_collect(&samples, rtip,
&sample_params, 0, NULL, NULL);
int full_samples_valid = sample_ret > 0 && samples.segments &&
samples.segment_count * 2 == samples.crossing_count &&
samples.ray_count == sample_params.n_rays;
if (!full_samples_valid) {
printf(" %-24s invalid structured Crofton output\\n", label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < samples.segment_count; sample_index++) {
struct rt_crofton_segment *segment =
&samples.segments[sample_index];
if (segment->thickness <= 0.0 ||
!NEAR_EQUAL(DIST_PNT_PNT(segment->in_point,
segment->out_point), segment->thickness, RT_LEN_TOL) ||
!NEAR_EQUAL(MAGNITUDE(segment->in_normal), 1.0,
VUNITIZE_TOL) ||
!NEAR_EQUAL(MAGNITUDE(segment->out_normal), 1.0,
VUNITIZE_TOL) ||
segment->ray_id >= samples.ray_count) {
printf(" %-24s invalid structured segment\\n", label);
failures++;
break;
}
}
}
struct rt_crofton_result offset_samples =
RT_CROFTON_RESULT_INIT;
struct rt_crofton_params offset_params =
{continuation_rays, 0.0, 0.0};
sample_ret = rt_crofton_collect(&offset_samples, rtip,
&offset_params, ray_offset, NULL, NULL);
int offset_samples_valid = sample_ret > 0 &&
offset_samples.ray_count == offset_params.n_rays;
if (!offset_samples_valid) {
printf(" %-24s invalid offset Crofton output\\n", label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < offset_samples.segment_count;
sample_index++) {
size_t ray_id = offset_samples.segments[sample_index].ray_id;
if (ray_id < ray_offset ||
ray_id >= ray_offset + offset_params.n_rays) {
printf(" %-24s invalid offset ray identifier\\n",
label);
failures++;
break;
}
}
}
if (full_samples_valid && offset_samples_valid) {
size_t suffix_start = 0;
while (suffix_start < samples.segment_count &&
samples.segments[suffix_start].ray_id < ray_offset)
suffix_start++;
size_t suffix_count = samples.segment_count - suffix_start;
if (suffix_count != offset_samples.segment_count) {
printf(" %-24s random stream continuation count mismatch\\n",
label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < suffix_count; sample_index++) {
const struct rt_crofton_segment *full_segment =
&samples.segments[suffix_start + sample_index];
const struct rt_crofton_segment *offset_segment =
&offset_samples.segments[sample_index];
if (!crofton_segments_equal(full_segment, offset_segment)) {
printf(" %-24s random stream continuation mismatch\\n",
label);
failures++;
break;
}
}
}
}
rt_crofton_result_free(&samples);
if (samples.segments || samples.segment_count)
failures++;
rt_crofton_result_free(&offset_samples);
if (offset_samples.segments || offset_samples.segment_count)
failures++;
}
rt_i_destroy(rtip);
if (elapsed_total_sec)
+21 -36
View File
@@ -414,20 +414,13 @@ flat_dsp_analytic(uint32_t xcnt, uint32_t ycnt, unsigned short h,
/* Build an in-memory DB, prep, and run comparison */
/* ------------------------------------------------------------------ */
enum dsp_ray_check_mode {
DSP_RAY_CHECK_NONE,
DSP_RAY_CHECK_FLAT,
DSP_RAY_CHECK_SMOOTH,
DSP_RAY_CHECK_BC_CUT
};
struct dsp_case {
const char *label;
uint32_t xcnt, ycnt;
const unsigned short *buf;
int cuttype;
int smooth;
enum dsp_ray_check_mode ray_checks;
int ray_checks;
/* stom diagonal (identity: dx=dy=dz=1): */
double dx, dy, dz;
/* analytic SA/vol (-1 if not available): */
@@ -557,7 +550,7 @@ check_smooth_normal(const struct dsp_case *tc, struct soltab *stp)
static int
run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
{
if (tc->ray_checks == DSP_RAY_CHECK_NONE)
if (!tc->ray_checks)
return 0;
int failures = 0;
@@ -569,14 +562,16 @@ run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
double x_extent = (tc->xcnt - 1) * tc->dx;
double y_extent = (tc->ycnt - 1) * tc->dy;
/* ray_checks == 1 is reserved for flat DSP cases, so buf[0] is the
* expected terrain height for the deterministic vertical/side rays.
*/
double top_z = (double)tc->buf[0] * tc->dz;
double expected_vertical_in = 10.0;
double expected_vertical_out = expected_vertical_in + top_z;
point_t o;
vect_t d;
if (tc->ray_checks == DSP_RAY_CHECK_FLAT) {
/* Flat cases use buf[0] as the terrain height for direct rays. */
double top_z = (double)tc->buf[0] * tc->dz;
double expected_vertical_in = 10.0;
double expected_vertical_out = expected_vertical_in + top_z;
if (tc->ray_checks == 1) {
VSET(o, 0.5 * x_extent, 0.5 * y_extent, top_z + 10.0);
VSET(d, 0.0, 0.0, -1.0);
failures += check_direct_ray("vertical down flat DSP", stp, rtip,
@@ -603,22 +598,12 @@ run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
failures += check_direct_ray("vertical up flat DSP", stp, rtip,
o, d, 1, expected_vertical_in,
expected_vertical_out, 0);
} else if (tc->ray_checks == DSP_RAY_CHECK_SMOOTH) {
} else if (tc->ray_checks == 2) {
VSET(o, 0.5 * x_extent, 0.5 * y_extent, 10000.0);
VSET(d, 0.0, 0.0, -1.0);
failures += check_direct_ray("smooth-mode direct vertical shot", stp, rtip,
o, d, 1, -1.0, -1.0, 0);
failures += check_smooth_normal(tc, stp);
} else if (tc->ray_checks == DSP_RAY_CHECK_BC_CUT) {
/* In cell (3,3), the B-C triangle rises from 315 at y=3.25 by
* 68 per x. A horizontal ray at z=330 enters at x=3+15/68
* and exits through the outer wall at x=4. This also checks
* that the explicit B-C cut gives the top an outward normal.
*/
VSET(o, 2.5, 3.25, 330.0);
VSET(d, 1.0, 0.0, 0.0);
failures += check_direct_ray("explicit B-C top-to-side shot", stp, rtip,
o, d, 1, 0.5 + 15.0 / 68.0, 1.5, 1);
}
return failures;
@@ -797,7 +782,7 @@ test_synthetic(void)
struct dsp_case tc = {
"flat 5x5 h=100 (4x4 cells, identity stom)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_FLAT, 1.0, 1.0, 1.0, asa, avol
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 1, 1.0, 1.0, 1.0, asa, avol
};
failures += run_inmem_case(&tc);
}
@@ -813,7 +798,7 @@ test_synthetic(void)
struct dsp_case tc = {
"flat 10x10 h=200 (9x9 cells)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, asa, avol
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, asa, avol
};
failures += run_inmem_case(&tc);
}
@@ -829,7 +814,7 @@ test_synthetic(void)
struct dsp_case tc = {
"flat 10x10 h=50 (stom dx=5 dy=5 dz=2)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_FLAT, 5.0, 5.0, 2.0, asa, avol
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 1, 5.0, 5.0, 2.0, asa, avol
};
failures += run_inmem_case(&tc);
}
@@ -844,7 +829,7 @@ test_synthetic(void)
struct dsp_case tc = {
"ramp 9x9 (h=100+20x+10y)",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -859,7 +844,7 @@ test_synthetic(void)
struct dsp_case tc = {
"asymmetric 5x5 explicit ULlr cut",
GW, GH, buf, DSP_CUT_DIR_ULlr, 0, DSP_RAY_CHECK_BC_CUT, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_ULlr, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -874,7 +859,7 @@ test_synthetic(void)
struct dsp_case tc = {
"asymmetric 5x5 adaptive cut",
GW, GH, buf, DSP_CUT_DIR_ADAPT, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_ADAPT, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -889,7 +874,7 @@ test_synthetic(void)
struct dsp_case tc = {
"smooth=1 ramp normal check",
GW, GH, buf, DSP_CUT_DIR_llUR, 1, DSP_RAY_CHECK_SMOOTH, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 1, 2, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -904,7 +889,7 @@ test_synthetic(void)
struct dsp_case tc = {
"smooth=2 ramp normal check",
GW, GH, buf, DSP_CUT_DIR_llUR, 2, DSP_RAY_CHECK_SMOOTH, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 2, 2, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -923,7 +908,7 @@ test_synthetic(void)
struct dsp_case tc = {
"sinusoidal 16x16",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
}
@@ -943,7 +928,7 @@ test_synthetic(void)
struct dsp_case tc = {
"complex 33x33 wave",
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
bu_free(buf, "33x33 buf");
@@ -986,7 +971,7 @@ test_synthetic(void)
struct dsp_case tc = {
"larger 129x129 mixed ramp/wave terrain",
grid_width, grid_height, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
grid_width, grid_height, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
};
failures += run_inmem_case(&tc);
bu_free(sin_x, "129x129 sin x");
+234 -180
View File
@@ -49,7 +49,6 @@
#include "bu/path.h"
#include "bu/process.h"
#include "bu/units.h"
#include "bu/binding.h"
#include "vmath.h"
#include "rt/db4.h"
#include "rt/geom.h"
@@ -2825,148 +2824,6 @@ to_data_vZ(struct ged *gedp,
}
static void tclcad_action_eval_aet(const struct bu_vls *ev, void *reg, void *event_data) {
struct bview *gdvp = (struct bview *)event_data;
const char *aet_args = (const char *)reg;
struct bu_vls cmd = BU_VLS_INIT_ZERO;
bu_vls_printf(&cmd, "%s aet %s %s", bu_vls_cstr(&current_top->to_gedp->go_name), bu_vls_cstr(&gdvp->gv_name), aet_args);
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
bu_vls_free(&cmd);
}
static void tclcad_action_mouse_drag(const struct bu_vls *ev, void *reg, void *event_data) {
struct bview *gdvp = (struct bview *)event_data;
const char *cmd_name = (const char *)reg;
int cx = gdvp->gv_prevMouseX;
int cy = gdvp->gv_prevMouseY;
char *p;
if ((p = strstr(bu_vls_cstr(ev), "\"cx\":"))) sscanf(p + 5, "%d", &cx);
if ((p = strstr(bu_vls_cstr(ev), "\"cy\":"))) sscanf(p + 5, "%d", &cy);
struct bu_vls cmd = BU_VLS_INIT_ZERO;
bu_vls_printf(&cmd, "%s %s %s %d %d", bu_vls_cstr(&current_top->to_gedp->go_name), cmd_name, bu_vls_cstr(&gdvp->gv_name), cx, cy);
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
bu_vls_free(&cmd);
}
static void tclcad_action_wheel_scale(const struct bu_vls *ev, void *reg, void *event_data) {
struct bview *gdvp = (struct bview *)event_data;
int delta = 0;
char *p;
if ((p = strstr(bu_vls_cstr(ev), "\"delta\":"))) sscanf(p + 8, "%d", &delta);
const char *sf = (delta > 0) ? "1.1" : "0.9";
struct bu_vls cmd = BU_VLS_INIT_ZERO;
bu_vls_printf(&cmd, "%s zoom %s %s", bu_vls_cstr(&current_top->to_gedp->go_name), bu_vls_cstr(&gdvp->gv_name), sf);
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
bu_vls_free(&cmd);
}
static void tclcad_action_mouse_click(const struct bu_vls *ev, void *reg, void *event_data) {
struct bview *gdvp = (struct bview *)event_data;
const char *cmd_format = (const char *)reg;
int cx = gdvp->gv_prevMouseX;
int cy = gdvp->gv_prevMouseY;
char *p;
if ((p = strstr(bu_vls_cstr(ev), "\"cx\":"))) sscanf(p + 5, "%d", &cx);
if ((p = strstr(bu_vls_cstr(ev), "\"cy\":"))) sscanf(p + 5, "%d", &cy);
struct bu_vls cmd = BU_VLS_INIT_ZERO;
bu_vls_printf(&cmd, cmd_format, bu_vls_cstr(&current_top->to_gedp->go_name), bu_vls_cstr(&gdvp->gv_name), cx, cy);
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
bu_vls_free(&cmd);
}
static int bindings_loaded_mged = 0;
static int tclcad_load_bindings_cmd(ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]) {
if (argc < 2) {
Tcl_AppendResult(interp, "wrong # args: should be \"", argv[0], " <json_path>\"", NULL);
return TCL_ERROR;
}
char *path = (char *)bu_dir(NULL, 0, BU_DIR_DATA, argv[1], NULL);
if (!path) {
Tcl_AppendResult(interp, "Failed to resolve path: ", argv[1], NULL);
return TCL_ERROR;
}
if (bu_binding_load(path) < 0) {
bu_free(path, "path");
Tcl_AppendResult(interp, "Failed to load bindings from: ", argv[1], NULL);
return TCL_ERROR;
}
bu_free(path, "path");
/* Validate that application provided required bindings */
if (!bu_binding_action_is_bound("dm", "view_mouse_drag_rotate") ||
!bu_binding_action_is_bound("dm", "view_mouse_drag_translate") ||
!bu_binding_action_is_bound("dm", "view_mouse_drag_scale")) {
Tcl_AppendResult(interp, "Validation Failed: The provided bindings do not map inputs for view_mouse_drag_rotate/translate/scale. These are required standard actions.", NULL);
return TCL_ERROR;
}
return TCL_OK;
}
static int tclcad_dispatch_event_cmd(ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]) {
if (argc < 8) return TCL_OK;
struct tclcad_obj *to = (struct tclcad_obj *)clientData;
struct bview *gdvp = bv_set_find_view(&to->to_gedp->ged_views, argv[2]);
if (!gdvp) return TCL_OK;
int x = atoi(argv[5]);
int y = atoi(argv[6]);
int state = atoi(argv[7]);
struct bu_vls ev = BU_VLS_INIT_ZERO;
bu_vls_printf(&ev, "{\"type\": \"%s\"", argv[3]);
if (!strcmp(argv[3], "key-press")) {
char key_str[16] = {0};
bu_strlcpy(key_str, argv[4], sizeof(key_str));
if (strlen(key_str) == 1 && key_str[0] >= 'a' && key_str[0] <= 'z') {
key_str[0] = key_str[0] - 'a' + 'A';
}
bu_vls_printf(&ev, ", \"key\": \"%s\"", key_str);
} else if (!strcmp(argv[3], "mouse-wheel")) {
int delta = (argc > 8) ? atoi(argv[8]) : 0;
int mdelta = -1 * delta / 120;
bu_vls_printf(&ev, ", \"delta\": %d", mdelta);
} else {
int btn = atoi(argv[4]) - 1;
if (!strcmp(argv[3], "mouse-move")) {
if (state & 0x100) btn = 0;
else if (state & 0x200) btn = 1;
else if (state & 0x400) btn = 2;
else btn = -1;
int dx = x - gdvp->gv_prevMouseX;
int dy = y - gdvp->gv_prevMouseY;
bu_vls_printf(&ev, ", \"dx\": %d, \"dy\": %d, \"mode\": 0", dx, dy);
}
if (btn >= 0) bu_vls_printf(&ev, ", \"button\": %d", btn);
bu_vls_printf(&ev, ", \"cx\": %d, \"cy\": %d", x, y);
}
if (state & (1 | 4 | 8)) {
bu_vls_printf(&ev, ", \"modifiers\": [");
int added = 0;
if (state & 1) { bu_vls_printf(&ev, "\"Shift\""); added=1; }
if (state & 4) { if(added) bu_vls_printf(&ev, ", "); bu_vls_printf(&ev, "\"Control\""); added=1; }
if (state & 8) { if(added) bu_vls_printf(&ev, ", "); bu_vls_printf(&ev, "\"Alt\""); }
bu_vls_printf(&ev, "]");
}
bu_vls_printf(&ev, "}");
bu_binding_process_event("dm", bu_vls_cstr(&ev), gdvp);
gdvp->gv_prevMouseX = x;
gdvp->gv_prevMouseY = y;
bu_vls_free(&ev);
return TCL_OK;
}
static void
to_init_default_bindings(struct bview *gdvp)
{
@@ -2975,35 +2832,6 @@ to_init_default_bindings(struct bview *gdvp)
if (dm_get_pathname((struct dm *)gdvp->dmp)) {
struct bu_vls *pathvls = dm_get_pathname((struct dm *)gdvp->dmp);
if (pathvls) {
if (!bindings_loaded_mged) {
bindings_loaded_mged = 1;
bu_binding_register_action("view_2", tclcad_action_eval_aet, (void*)"35 -25 0");
bu_binding_register_action("view_3", tclcad_action_eval_aet, (void*)"35 25 0");
bu_binding_register_action("view_4", tclcad_action_eval_aet, (void*)"45 45 0");
bu_binding_register_action("view_5", tclcad_action_eval_aet, (void*)"145 25 0");
bu_binding_register_action("view_6", tclcad_action_eval_aet, (void*)"215 25 0");
bu_binding_register_action("view_7", tclcad_action_eval_aet, (void*)"325 25 0");
bu_binding_register_action("view_front", tclcad_action_eval_aet, (void*)"0 0 0");
bu_binding_register_action("view_top", tclcad_action_eval_aet, (void*)"270 90 0");
bu_binding_register_action("view_bottom", tclcad_action_eval_aet, (void*)"270 -90 0");
bu_binding_register_action("view_left", tclcad_action_eval_aet, (void*)"90 0 0");
bu_binding_register_action("view_rear", tclcad_action_eval_aet, (void*)"180 0 0");
bu_binding_register_action("view_right", tclcad_action_eval_aet, (void*)"270 0 0");
bu_binding_register_action("view_mouse_drag_rotate", tclcad_action_mouse_drag, (void*)"mouse_rot");
bu_binding_register_action("view_mouse_drag_translate", tclcad_action_mouse_drag, (void*)"mouse_trans");
bu_binding_register_action("view_mouse_drag_scale", tclcad_action_mouse_drag, (void*)"mouse_scale");
bu_binding_register_action("view_mouse_wheel_scale", tclcad_action_wheel_scale, NULL);
bu_binding_register_action("view_mouse_press_left", tclcad_action_mouse_click, (void*)"%s zoom %s 0.5");
bu_binding_register_action("view_mouse_press_right", tclcad_action_mouse_click, (void*)"%s zoom %s 2.0");
Tcl_CreateCommand(current_top->to_interp, "tclcad_dispatch_event", tclcad_dispatch_event_cmd, (ClientData)current_top, (Tcl_CmdDeleteProc *)NULL);
Tcl_CreateCommand(current_top->to_interp, "tclcad_load_bindings", tclcad_load_bindings_cmd, (ClientData)current_top, (Tcl_CmdDeleteProc *)NULL);
}
bu_vls_printf(&bindings, "bind %s <Configure> {%s configure %s; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
@@ -3020,14 +2848,240 @@ to_init_default_bindings(struct bview *gdvp)
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
/* Dispatch all events to bu_binding */
bu_vls_printf(&bindings, "bind %s <Any-KeyPress> {tclcad_dispatch_event %%W %s %s key-press %%K %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(&current_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Any-ButtonPress> {tclcad_dispatch_event %%W %s %s mouse-press %%b %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(&current_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Any-ButtonRelease> {tclcad_dispatch_event %%W %s %s mouse-release %%b %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(&current_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Any-Motion> {tclcad_dispatch_event %%W %s %s mouse-move 0 %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(&current_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <MouseWheel> {tclcad_dispatch_event %%W %s %s mouse-wheel 0 %%x %%y %%s %%D; break}; ", bu_vls_addr(pathvls), bu_vls_addr(&current_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&bindings));
/* Mouse Bindings */
bu_vls_printf(&bindings, "bind %s <2> {%s vslew %s %%x %%y; focus %s; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name),
bu_vls_addr(pathvls));
bu_vls_printf(&bindings, "bind %s <1> {%s zoom %s 0.5; focus %s; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name),
bu_vls_addr(pathvls));
bu_vls_printf(&bindings, "bind %s <3> {%s zoom %s 2.0; focus %s; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name),
bu_vls_addr(pathvls));
bu_vls_printf(&bindings, "bind %s <4> {%s zoom %s 1.1; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <5> {%s zoom %s 0.9; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <MouseWheel> {if {%%D < 0} {%s zoom %s 0.9} else {%s zoom %s 1.1}; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
/* Idle Mode */
bu_vls_printf(&bindings, "bind %s <ButtonRelease> {%s idle_mode %s}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <KeyRelease-Control_L> {%s idle_mode %s}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <KeyRelease-Control_R> {%s idle_mode %s}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <KeyRelease-Shift_L> {%s idle_mode %s}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <KeyRelease-Shift_R> {%s idle_mode %s}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <KeyRelease-Alt_L> {%s idle_mode %s; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <KeyRelease-Alt_R> {%s idle_mode %s; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
/* Rotate Mode */
bu_vls_printf(&bindings, "bind %s <Control-ButtonRelease-1> {%s idle_mode %s}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Control-ButtonPress-1> {%s rotate_mode %s %%x %%y}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Control-ButtonPress-2> {%s rotate_mode %s %%x %%y}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Control-ButtonPress-3> {%s rotate_mode %s %%x %%y}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
/* Translate Mode */
bu_vls_printf(&bindings, "bind %s <Shift-ButtonRelease-1> {%s idle_mode %s}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Shift-ButtonPress-1> {%s translate_mode %s %%x %%y}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Shift-ButtonPress-2> {%s translate_mode %s %%x %%y}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Shift-ButtonPress-3> {%s translate_mode %s %%x %%y}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
/* Scale Mode */
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonRelease-1> {%s idle_mode %s}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonPress-1> {%s scale_mode %s %%x %%y}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonPress-2> {%s scale_mode %s %%x %%y}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonPress-3> {%s scale_mode %s %%x %%y}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
/* Constrained Rotate Mode */
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonRelease-1> {%s idle_mode %s}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonPress-1> {%s constrain_rmode %s x %%x %%y; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonPress-2> {%s constrain_rmode %s y %%x %%y; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonPress-3> {%s constrain_rmode %s z %%x %%y; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
/* Constrained Translate Mode */
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonRelease-1> {%s idle_mode %s; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonPress-1> {%s constrain_tmode %s x %%x %%y; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonPress-2> {%s constrain_tmode %s y %%x %%y; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonPress-3> {%s constrain_tmode %s z %%x %%y; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
/* Key Bindings */
bu_vls_printf(&bindings, "bind %s 3 {%s aet %s 35 25; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s 4 {%s aet %s 45 45; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s f {%s aet %s 0 0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s F {%s aet %s 0 0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s R {%s aet %s 180 0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s r {%s aet %s 270 0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s l {%s aet %s 90 0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s L {%s aet %s 90 0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s t {%s aet %s 270 90; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s T {%s aet %s 270 90; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s b {%s aet %s 270 -90; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s B {%s aet %s 270 -90; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s + {%s zoom %s 2.0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s = {%s zoom %s 2.0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s _ {%s zoom %s 0.5; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s - {%s zoom %s 0.5; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Key-Left> {%s rot %s -v 0 1 0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Key-Right> {%s rot %s -v 0 -1 0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Key-Up> {%s rot %s -v 1 0 0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
bu_vls_printf(&bindings, "bind %s <Key-Down> {%s rot %s -v -1 0 0; break}; ",
bu_vls_addr(pathvls),
bu_vls_addr(&current_top->to_gedp->go_name),
bu_vls_addr(&gdvp->gv_name));
Tcl_Eval(current_top->to_interp, bu_vls_addr(&bindings));
}
}
bu_vls_free(&bindings);
+396 -9
View File
@@ -49,8 +49,12 @@ proc mged_bind_dm { w } {
bind $w <Enter> [list mged_activate_dm $w]
# Load standardized JSON bindings for mged
#default mouse bindings
default_mouse_bindings $w
#default key bindings
set forwarding_key($w) 0
default_key_bindings $w
}
proc print_return_val str {
@@ -60,6 +64,129 @@ proc print_return_val str {
}
}
if ![info exists mged_default(dm_key_bindings)] {
set mged_default(dm_key_bindings) "\tKey Sequence\t\tBehavior
\ta\t\t\ttoggle angle distance cursor (ADC)
\te\t\t\ttoggle edit axes
\tm\t\t\ttoggle model axes
\tv\t\t\ttoggle view axes
\ti\t\t\tadvance illumination pointer forward
\tI\t\t\tadvance illumination pointer backward
\tp\t\t\tsimulate mouse press (i.e. to pick a solid)
\t3\t\t\tview - ae 35 25
\t4\t\t\tview - ae 45 45
\tf\t\t\tfront view
\tt\t\t\ttop view
\tb\t\t\tbottom view
\tl\t\t\tleft view
\tr\t\t\tright view
\tR\t\t\trear view
\ts\t\t\tenter solid illumination state
\to\t\t\tenter object illumination state
\tq\t\t\treject edit
\tu\t\t\tzero knobs and sliders
\t<F1>\t\t\ttoggle depthcue
\t<F2>\t\t\ttoggle zclip
\t<F3>\t\t\ttoggle perspective
\t<F4>\t\t\ttoggle zbuffer
\t<F5>\t\t\ttoggle lighting
\t<F6>\t\t\ttoggle perspective angle
\t<F7>\t\t\ttoggle faceplate
\t<F8>\t\t\ttoggle faceplate GUI
\t<F9>\t\t\ttoggle keystroke forwarding
\t<F12>\t\t\tzero knobs
\t<Left>\t\t\tabsolute rotate about y axis
\t<Right>\t\t\tabsolute rotate about y axis
\t<Down>\t\t\tabsolute rotate about x axis
\t<Up>\t\t\tabsolute rotate about x axis
\t<Shift-Left>\t\tabsolute translate in X direction
\t<Shift-Right>\t\tabsolute translate in X direction
\t<Shift-Down>\t\tabsolute translate in Z direction
\t<Shift-Up>\t\tabsolute translate in Z direction
\t<Control-Shift-Left>\tabsolute rotate about z axis
\t<Control-Shift-Right>\tabsolute rotate about z axis
\t<Control-Shift-Down>\tabsolute translate in Y direction
\t<Control-Shift-Up>\tabsolute translate in Y direction
\t<Control-n>\t\tgoto next view
\t<Control-p>\t\tgoto previous view
\t<Control-t>\t\ttoggle between the current view and the last view"
}
proc default_key_bindings { w } {
# common view shortcuts
bind $w 2 "winset $w; ae 35 -25; break"
bind $w 3 "winset $w; press 35,25; break"
bind $w 4 "winset $w; press 45,45; break"
bind $w 5 "winset $w; ae 145 25; break"
bind $w 6 "winset $w; ae 215 25; break"
bind $w 7 "winset $w; ae 325 25; break"
# default key shortcuts
bind $w a "winset $w; adc; break"
bind $w b "winset $w; press bottom; break"
bind $w c "open_cmd_win id_0"
bind $w e "winset $w; rset ax edit_draw !; update_gui $w edit_draw \[rset ax edit_draw\]; break"
bind $w f "winset $w; press front; break"
bind $w i "winset $w; aip f; break"
bind $w l "winset $w; press left; break"
bind $w m "winset $w; rset ax model_draw !; update_gui $w model_draw \[rset ax model_draw\]; break"
bind $w o "winset $w; press oill; break"
bind $w p "winset $w; M 1 0 0; break"
bind $w q "winset $w; press reject; break"
bind $w r "winset $w; press right; break"
bind $w s "winset $w; press sill; break"
bind $w t "winset $w; press top; break"
bind $w u "winset $w; svb; break"
bind $w v "winset $w; rset ax view_draw !; update_gui $w view_draw \[rset ax view_draw\]; break"
# default shift+key shortcuts
bind $w A "winset $w; press accept; break"
bind $w I "winset $w; aip b; break"
bind $w N "winset $w; puts \[nirt -b\]; break"
bind $w P "winset $w; catch {sed_apply}; break"
bind $w R "winset $w; press rear; break"
bind $w S "winset $w; catch {sed_reset}; break"
# default control+key shortcuts
bind $w <Control-n> "winset $w; _mged_view_ring next; break"
bind $w <Control-p> "winset $w; _mged_view_ring prev; break"
bind $w <Control-t> "winset $w; _mged_view_ring toggle; break"
# shift grips navigation
bind $w <Control-Shift-Down> "winset $w; knob -i aY \$mged_default(tran_factor); break"
bind $w <Control-Shift-Left> "winset $w; knob -i az \$mged_default(rot_factor); break"
bind $w <Control-Shift-Right> "winset $w; knob -i az -\$mged_default(rot_factor); break"
bind $w <Control-Shift-Up> "winset $w; knob -i aY -\$mged_default(tran_factor); break"
bind $w <Down> "winset $w; knob -i ax \$mged_default(rot_factor); break"
bind $w <Left> "winset $w; knob -i ay -\$mged_default(rot_factor); break"
bind $w <Right> "winset $w; knob -i ay \$mged_default(rot_factor); break"
bind $w <Up> "winset $w; knob -i ax -\$mged_default(rot_factor); break"
bind $w <Shift-Down> "winset $w; knob -i aZ -\$mged_default(tran_factor); break"
bind $w <Shift-Left> "winset $w; knob -i aX \$mged_default(tran_factor); break"
bind $w <Shift-Right> "winset $w; knob -i aX -\$mged_default(tran_factor); break"
bind $w <Shift-Up> "winset $w; knob -i aZ \$mged_default(tran_factor); break"
# function keys (settings)
bind $w <F1> "winset $w; dm set depthcue !; update_gui $w depthcue \[dm set depthcue\]; break"
bind $w <F2> "winset $w; dm set zclip !; update_gui $w zclip \[dm set zclip\]; break"
bind $w <F3> "winset $w; set perspective_mode !; update_gui $w perspective_mode \$perspective_mode; break"
bind $w <F4> "winset $w; dm set zbuffer !; update_gui $w zbuffer \[dm set zbuffer\]; break"
bind $w <F5> "winset $w; dm set lighting !; update_gui $w lighting \[dm set lighting\]; break"
bind $w <F6> "winset $w; set toggle_perspective !; break"
bind $w <F7> "winset $w; set faceplate !; update_gui $w faceplate \$faceplate; break"
bind $w <F8> "winset $w; set orig_gui !; update_gui $w orig_gui \$orig_gui; break"
bind $w <F9> "toggle_forward_key_bindings $w; update_gui $w forward_keys \$forwarding_key($w); break"
bind $w <F12> "winset $w; knob zero; break"
# interrupt
bind $w <Escape> "winset $w; reset_everything $w; break"
# Throw away other key events
bind $w <KeyPress> {
break
}
}
proc reset_everything { w } {
global mged_gui
@@ -75,6 +202,274 @@ proc reset_everything { w } {
set_mouse_behavior $id
}
proc set_forward_keys { w val } {
global forwarding_key
set forwarding_key($w) $val
if {$forwarding_key($w)} {
forward_key_bindings $w
} else {
default_key_bindings $w
}
}
proc toggle_forward_key_bindings { w } {
global forwarding_key
if {$forwarding_key($w)} {
default_key_bindings $w
set forwarding_key($w) 0
} else {
forward_key_bindings $w
set forwarding_key($w) 1
}
}
proc forward_key_bindings { w } {
set id [get_player_id_dm $w]
if {$id == "mged"} {
return
}
# First, unset the default key bindings
bind $w a {}
bind $w e {}
bind $w m {}
bind $w v {}
bind $w i {}
bind $w I {}
bind $w p {}
bind $w 2 {}
bind $w 3 {}
bind $w 4 {}
bind $w 5 {}
bind $w 6 {}
bind $w 7 {}
bind $w f {}
bind $w t {}
bind $w b {}
bind $w l {}
bind $w N {}
bind $w r {}
bind $w R {}
bind $w s {}
bind $w o {}
bind $w q {}
bind $w u {}
bind $w c {}
bind $w A {}
bind $w P {}
bind $w S {}
bind $w <F1> {}
bind $w <F2> {}
bind $w <F3> {}
bind $w <F4> {}
bind $w <F5> {}
bind $w <F6> {}
bind $w <F7> {}
bind $w <F8> {}
bind $w <F12> {}
bind $w <Left> {}
bind $w <Right> {}
bind $w <Down> {}
bind $w <Up> {}
bind $w <Shift-Left> {}
bind $w <Shift-Right> {}
bind $w <Shift-Down> {}
bind $w <Shift-Up> {}
bind $w <Control-Shift-Left> {}
bind $w <Control-Shift-Right> {}
bind $w <Control-Shift-Down> {}
bind $w <Control-Shift-Up> {}
bind $w <Control-n> {}
bind $w <Control-p> {}
bind $w <Control-t> {}
bind $w <Escape> {}
# The focus commands in the binding below are necessary to insure
# that .$id.t gets the event.
bind $w <KeyPress> "\
focus .$id.t;\
set mged_gui(.$id.t,insert_char_flag) 1;\
event generate .$id.t <KeyPress> -state %s -keysym %K;\
set mged_gui(.$id.t,insert_char_flag) 0;\
focus %W;\
break"
}
proc default_mouse_bindings { w } {
global no_focus_hack
global transform
global tcl_platform
# default button bindings
if {![regexp {^[0-9]+} $::tcl_platform(osVersion) osMajorVersion]} {
set osMajorVersion 0
}
if {[info exists no_focus_hack] && $no_focus_hack} {
bind $w <1> "winset $w; zoom 0.5; break"
bind $w <2> "winset $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
bind $w <3> "winset $w; zoom 2.0; break"
# Mouse wheel fun with Tcl/Tk - see https://wiki.tcl-lang.org/page/mousewheel
bind $w <MouseWheel> "winset $w; focus $w; if {%D < 0} {zoom 0.9} else {zoom 1.1}"
bind $w <Button-4> {event generate [focus] <MouseWheel> -delta 120}
bind $w <Button-5> {event generate [focus] <MouseWheel> -delta -120}
if {$::tcl_platform(os) == "Darwin"} {
# Mac option-key emulates mouse 2 (doesn't work?)
bind $w <Option-ButtonPress-1> "winset $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
# Mac command-key emulates mouse 3
bind $w <Mod2-ButtonPress-1> "winset $w; zoom 2.0; break"
}
} else {
bind $w <1> "winset $w; focus $w; zoom 0.5; break"
bind $w <2> "winset $w; focus $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
bind $w <3> "winset $w; focus $w; zoom 2.0; break"
# Mouse wheel fun with Tcl/Tk - see https://wiki.tcl-lang.org/page/mousewheel
bind $w <MouseWheel> "winset $w; focus $w; if {%D < 0} {zoom 0.9} else {zoom 1.1}; break"
bind $w <Button-4> {event generate [focus] <MouseWheel> -delta 120}
bind $w <Button-5> {event generate [focus] <MouseWheel> -delta -120}
if {$::tcl_platform(os) == "Darwin"} {
# Mac command-key emulates mouse 2 (doesn't work?)
bind $w <Option-ButtonPress-1> "winset $w; focus $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
# Mac command-key emulates mouse 3
bind $w <Mod2-ButtonPress-1> "winset $w; focus $w; zoom 2.0; break"
}
}
bind $w <ButtonRelease> "winset $w; dm idle; break"
bind $w <Motion> "winset $w; if {\[dm type\] == \"tkswrast\"} {dm motion %x %y}; break"
bind $w <KeyRelease-Control_L> "winset $w; dm idle; break"
bind $w <KeyRelease-Control_R> "winset $w; dm idle; break"
bind $w <KeyRelease-Shift_L> "winset $w; dm idle; break"
bind $w <KeyRelease-Shift_R> "winset $w; dm idle; break"
bind $w <KeyRelease-Alt_L> "winset $w; dm idle; break"
bind $w <KeyRelease-Alt_R> "winset $w; dm idle; break"
if ![catch {adc draw} result] {
set adcflag $result
} else {
set adcflag 0
}
if {$adcflag == "1" && $transform == "a"} {
bind $w <Shift-ButtonPress-1> "winset $w; dm adc t %x %y; \
shift_grip_hints $w \"Translate ADC\"; break"
bind $w <Shift-ButtonPress-2> "winset $w; dm adc t %x %y; \
shift_grip_hints $w \"Translate ADC\"; break"
bind $w <Shift-ButtonPress-3> "winset $w; dm adc d %x %y; \
shift_grip_hints $w \"Translate Tick Distance\"; break"
bind $w <Control-ButtonPress-1> "winset $w; dm adc 1 %x %y; \
shift_grip_hints $w \"Rotate Angle 1\"; break"
bind $w <Control-ButtonPress-2> "winset $w; dm adc 2 %x %y; \
shift_grip_hints $w \"Rotate Angle 2\"; break"
bind $w <Control-ButtonPress-3> "winset $w; dm adc d %x %y; \
shift_grip_hints $w \"Translate Tick Distance\"; break"
bind $w <Shift-Control-ButtonPress-1> "winset $w; dm adc d %x %y; \
shift_grip_hints $w \"Translate Tick Distance\"; break"
bind $w <Shift-Control-ButtonPress-2> "winset $w; dm adc d %x %y; \
shift_grip_hints $w \"Translate Tick Distance\"; break"
bind $w <Shift-Control-ButtonPress-3> "winset $w; dm adc d %x %y; \
shift_grip_hints $w \"Translate Tick Distance\"; break"
#constrained adc defaults
bind $w <Alt-Shift-ButtonPress-1> "winset $w; dm con a x %x %y; \
shift_grip_hints $w \"X Translate ADC\"; break"
bind $w <Alt-Shift-ButtonPress-2> "winset $w; dm con a y %x %y; \
shift_grip_hints $w \"Y Translate ADC\"; break"
bind $w <Alt-Shift-ButtonPress-3> "winset $w; dm con a d %x %y; \
shift_grip_hints $w \"Translate Tick Distance\"; break"
bind $w <Alt-Control-ButtonPress-1> "winset $w; dm con a 1 %x %y; \
shift_grip_hints $w \"Rotate Angle 1\"; break"
bind $w <Alt-Control-ButtonPress-2> "winset $w; dm con a 2 %x %y; \
shift_grip_hints $w \"Rotate Angle 2\"; break"
bind $w <Alt-Control-ButtonPress-3> "winset $w; dm con a d %x %y; \
shift_grip_hints $w \"Translate Tick Distance\"; break"
bind $w <Alt-Shift-Control-ButtonPress-1> "winset $w; dm con a d %x %y; \
shift_grip_hints $w \"Translate Tick Distance\"; break"
bind $w <Alt-Shift-Control-ButtonPress-2> "winset $w; dm con a d %x %y; \
shift_grip_hints $w \"Translate Tick Distance\"; break"
bind $w <Alt-Shift-Control-ButtonPress-3> "winset $w; dm con a d %x %y; \
shift_grip_hints $w \"Translate Tick Distance\"; break"
} else {
bind $w <Shift-ButtonPress-1> "winset $w; dm am t %x %y; \
shift_grip_hints $w Translate; break"
bind $w <Shift-ButtonPress-2> "winset $w; dm am t %x %y; \
shift_grip_hints $w Translate; break"
bind $w <Shift-ButtonPress-3> "winset $w; dm am t %x %y; \
shift_grip_hints $w Translate; break"
bind $w <Control-ButtonPress-1> "winset $w; dm am r %x %y; \
shift_grip_hints $w Rotate; break"
bind $w <Control-ButtonPress-2> "winset $w; dm am r %x %y; \
shift_grip_hints $w Rotate; break"
bind $w <Control-ButtonPress-3> "winset $w; dm am r %x %y; \
shift_grip_hints $w Rotate; break"
bind $w <Shift-Control-ButtonPress-1> "winset $w; dm am s %x %y; \
shift_grip_hints $w Scale/Zoom; break"
bind $w <Shift-Control-ButtonPress-2> "winset $w; dm am s %x %y; \
shift_grip_hints $w Scale/Zoom; break"
bind $w <Shift-Control-ButtonPress-3> "winset $w; dm am s %x %y; \
shift_grip_hints $w Scale/Zoom; break"
#constrained defaults
bind $w <Alt-Shift-ButtonPress-1> "winset $w; dm con t x %x %y; \
shift_grip_hints $w \"X Translation\"; break"
bind $w <Alt-Shift-ButtonPress-2> "winset $w; dm con t y %x %y; \
shift_grip_hints $w \"Y Translation\"; break"
bind $w <Alt-Shift-ButtonPress-3> "winset $w; dm con t z %x %y; \
shift_grip_hints $w \"Z Translation\"; break"
bind $w <Alt-Control-ButtonPress-1> "winset $w; dm con r x %x %y; \
shift_grip_hints $w \"X Rotation\"; break"
bind $w <Alt-Control-ButtonPress-2> "winset $w; dm con r y %x %y; \
shift_grip_hints $w \"Y Rotation\"; break"
bind $w <Alt-Control-ButtonPress-3> "winset $w; dm con r z %x %y; \
shift_grip_hints $w \"Z Rotation\"; break"
bind $w <Alt-Shift-Control-ButtonPress-1> "winset $w; dm con s x %x %y; \
scale_shift_grip_hints $w X; break"
bind $w <Alt-Shift-Control-ButtonPress-2> "winset $w; dm con s y %x %y; \
scale_shift_grip_hints $w Y; break"
bind $w <Alt-Shift-Control-ButtonPress-3> "winset $w; dm con s z %x %y; \
scale_shift_grip_hints $w Z; break"
}
}
proc shift_grip_hints { w hint } {
global mged_display
global mged_gui
global win_to_id
if ![info exists win_to_id($w)] {
return
}
set id $win_to_id($w)
set mged_gui($id,illum_label) $hint
}
proc scale_shift_grip_hints { w axis } {
if {[status state] == "OBJ EDIT"} {
shift_grip_hints $w "$axis Scale"
} else {
shift_grip_hints $w "Scale/Zoom"
}
}
proc update_gui { w vname val } {
global mged_players
global mged_gui
@@ -87,14 +482,6 @@ proc update_gui { w vname val } {
}
}
# Dummy stubs to prevent legacy scripts from failing if they try to invoke them
proc default_key_bindings { w } {}
proc default_mouse_bindings { w } {}
proc forward_key_bindings { w } {}
proc toggle_forward_key_bindings { w } {}
proc set_forward_keys { w val } {}
proc shift_grip_hints { w hint } {}
proc scale_shift_grip_hints { w axis } {}
# Local Variables:
# mode: Tcl