Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a7ea7976f | ||
|
|
ec4347e7d3 | ||
|
|
21e034ee34 | ||
|
|
8f81f27d92 | ||
|
|
d729a7d3f2 | ||
|
|
17601916f4 |
@@ -0,0 +1,164 @@
|
||||
/* B I N D I N G . H
|
||||
* BRL-CAD
|
||||
*
|
||||
* Copyright (c) 2026 United States Government as represented by
|
||||
* the U.S. Army Research Laboratory.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public License
|
||||
* version 2.1 as published by the Free Software Foundation.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this file; see the file named COPYING for more
|
||||
* information.
|
||||
*/
|
||||
|
||||
/** @addtogroup bu_binding
|
||||
* @brief
|
||||
* Universal input binding schema and action dispatch.
|
||||
*/
|
||||
/** @{ */
|
||||
/** @file bu/binding.h */
|
||||
|
||||
#ifndef BU_BINDING_H
|
||||
#define BU_BINDING_H
|
||||
|
||||
#include "common.h"
|
||||
#include "bu/defines.h"
|
||||
#include "bu/vls.h"
|
||||
|
||||
__BEGIN_DECLS
|
||||
|
||||
/* Standardized Attribute Keys (for W3C validation logic) */
|
||||
#define BU_BINDING_ATTR_TYPE "type"
|
||||
#define BU_BINDING_ATTR_KEY "key" /* Values MUST be W3C standard strings */
|
||||
#define BU_BINDING_ATTR_BUTTON "button" /* Values MUST be W3C button integers */
|
||||
#define BU_BINDING_ATTR_MODIFIERS "modifiers"
|
||||
|
||||
/* Standardized Event Types */
|
||||
#define BU_BINDING_EVENT_KEY_PRESS "key-press"
|
||||
#define BU_BINDING_EVENT_KEY_RELEASE "key-release"
|
||||
#define BU_BINDING_EVENT_MOUSE_PRESS "mouse-press"
|
||||
#define BU_BINDING_EVENT_MOUSE_RELEASE "mouse-release"
|
||||
#define BU_BINDING_EVENT_MOUSE_MOTION "mouse-motion"
|
||||
#define BU_BINDING_EVENT_MOUSE_SCROLL "mouse-scroll"
|
||||
|
||||
/**
|
||||
* @brief Action callback signature
|
||||
*
|
||||
* Invoked when an incoming event matches a loaded binding.
|
||||
* The callback receives the full JSON string of the triggering event.
|
||||
*
|
||||
* @param event_json Raw JSON string describing the incoming event
|
||||
* @param registered_data Opaque pointer passed during registration
|
||||
* @param event_data Opaque pointer passed during event processing
|
||||
*/
|
||||
typedef void (*bu_action_func_t)(const char *event_json, void *registered_data, void *event_data);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief Load JSON bindings from a file
|
||||
*
|
||||
* Parses a JSON configuration specifying bindings for various contexts.
|
||||
* Performs two-phase validation:
|
||||
* 1. Checks standard fields (like W3C key strings) for correctness.
|
||||
* 2. Invokes context-specific validation hooks (if registered).
|
||||
*
|
||||
* @param json_path Path to the JSON file
|
||||
* @return 0 on success, <0 on error
|
||||
*/
|
||||
BU_EXPORT extern int bu_binding_load(const char *json_path);
|
||||
|
||||
/**
|
||||
* @brief Load JSON bindings from a raw string
|
||||
*
|
||||
* Parses a JSON configuration string specifying bindings for various contexts.
|
||||
* Allows applications to embed overriding bindings natively without external files.
|
||||
*
|
||||
* @param json_string Raw JSON string containing bindings
|
||||
* @return 0 on success, <0 on error
|
||||
*/
|
||||
BU_EXPORT extern int bu_binding_load_str(const char *json_string);
|
||||
|
||||
/**
|
||||
* @brief Register a JSON schema for a specific context
|
||||
*
|
||||
* Defines the allowed vocabulary for a context using a JSON schema file.
|
||||
* The schema file is lazily parsed during bu_binding_load().
|
||||
*
|
||||
* @param context The interaction context (e.g., "dm", "fb")
|
||||
* @param json_path Absolute path to the schema JSON file
|
||||
*/
|
||||
BU_EXPORT extern void bu_binding_register_schema(const char *context, const char *json_path);
|
||||
|
||||
/**
|
||||
* @brief Augment an existing schema with a custom action
|
||||
*
|
||||
* @param context The interaction context (e.g., "dm")
|
||||
* @param action_name The new abstract action string
|
||||
* @param description Human-readable description of the action
|
||||
*/
|
||||
BU_EXPORT extern void bu_binding_schema_add_action(const char *context, const char *action_name, const char *description);
|
||||
|
||||
/**
|
||||
* @brief Remove a custom action from an existing schema
|
||||
*
|
||||
* @param context The interaction context (e.g., "dm")
|
||||
* @param action_name The abstract action string to remove
|
||||
*/
|
||||
BU_EXPORT extern void bu_binding_schema_remove_action(const char *context, const char *action_name);
|
||||
|
||||
/**
|
||||
* @brief Register a C callback for a semantic action
|
||||
*
|
||||
* @param action_name The abstract action string (e.g., "fb_close_window")
|
||||
* @param func The C function to invoke when the action is triggered
|
||||
* @param client_data Opaque pointer passed to the callback
|
||||
*/
|
||||
BU_EXPORT extern void bu_binding_register_action(const char *action_name, bu_action_func_t func, void *client_data);
|
||||
|
||||
/**
|
||||
* @brief Process an incoming native event
|
||||
*
|
||||
* Matches the event JSON against the loaded bindings for the given context.
|
||||
* If a matching action is found and a handler is registered for that action,
|
||||
* the callback is invoked.
|
||||
*
|
||||
* @param context The interaction context (e.g., "view3d", "fb_raw")
|
||||
* @param event_json A JSON string containing the event attributes
|
||||
* @param event_data Opaque pointer passed to the callback representing instance state
|
||||
* @return 1 if an action was triggered and handled, 0 otherwise
|
||||
*/
|
||||
BU_EXPORT extern int bu_binding_process_event(const char *context, const char *event_json, void *event_data);
|
||||
|
||||
/**
|
||||
* @brief Check if a specific action is bound in a given context
|
||||
*
|
||||
* Use this to verify that an application has provided a binding for an essential action.
|
||||
*
|
||||
* @param context The interaction context (e.g., "view3d", "fb_raw")
|
||||
* @param action_name The abstract action string (e.g., "view_mouse_drag_rotate")
|
||||
* @return 1 if bound, 0 if not
|
||||
*/
|
||||
BU_EXPORT extern int bu_binding_action_is_bound(const char *context, const char *action_name);
|
||||
|
||||
__END_DECLS
|
||||
|
||||
#endif /* BU_BINDING_H */
|
||||
|
||||
/** @} */
|
||||
/*
|
||||
* Local Variables:
|
||||
* mode: C
|
||||
* tab-width: 8
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "stroustrup"
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||
@@ -444,6 +444,9 @@ DM_EXPORT extern int fb_get_interactive(const struct fb *ifp);
|
||||
*/
|
||||
DM_EXPORT extern int fb_next_event(struct fb *ifp, struct fb_event *e);
|
||||
|
||||
/** Dispatch an application-facing event through the framebuffer bindings. */
|
||||
DM_EXPORT extern int fb_process_event_bindings(struct fb *ifp, const struct fb_event *e);
|
||||
|
||||
|
||||
/* Library entry points */
|
||||
|
||||
|
||||
@@ -227,6 +227,32 @@ struct rt_crofton_params {
|
||||
double time_ms; /**< wall-clock time budget (ms); 0 = disabled */
|
||||
};
|
||||
|
||||
/** One solid partition observed by a Crofton ray. */
|
||||
struct rt_crofton_segment {
|
||||
point_t in_point;
|
||||
vect_t in_normal;
|
||||
point_t out_point;
|
||||
vect_t out_normal;
|
||||
fastf_t thickness;
|
||||
size_t ray_id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Structured samples and estimates produced by rt_crofton_collect().
|
||||
* Release @p segments with rt_crofton_result_free().
|
||||
*/
|
||||
struct rt_crofton_result {
|
||||
struct rt_crofton_segment *segments;
|
||||
size_t segment_count;
|
||||
size_t ray_count;
|
||||
size_t crossing_count;
|
||||
double surface_area;
|
||||
double volume;
|
||||
};
|
||||
|
||||
/** Initialize an rt_crofton_result before first use. */
|
||||
#define RT_CROFTON_RESULT_INIT { NULL, 0, 0, 0, 0.0, 0.0 }
|
||||
|
||||
|
||||
/**
|
||||
* Run the Cauchy-Crofton ray-sampling estimator on an already-prepared
|
||||
@@ -337,6 +363,26 @@ struct rt_crofton_params {
|
||||
*/
|
||||
RT_EXPORT extern int rt_crofton_shoot(double *out_surf_area, double *out_volume, point_t *out_aabb_min, point_t *out_aabb_max, point_t out_obb[8], point_t **out_points, size_t *out_point_count, struct rt_i *rtip, const struct rt_crofton_params *params, const fastf_t *bbox_min, const fastf_t *bbox_max);
|
||||
|
||||
/**
|
||||
* Run Crofton sampling while preserving oriented entry/exit pairs.
|
||||
*
|
||||
* @p result must be initialized with RT_CROFTON_RESULT_INIT. On success it
|
||||
* owns the returned segment array. @p rtip must already be prepared.
|
||||
* @p ray_offset skips that many samples in the deterministic sequence and
|
||||
* labels the returned rays starting at that value. This lets successive
|
||||
* calls use disjoint validation streams.
|
||||
*/
|
||||
RT_EXPORT extern int rt_crofton_collect(struct rt_crofton_result *result,
|
||||
struct rt_i *rtip,
|
||||
const struct rt_crofton_params *params,
|
||||
size_t ray_offset,
|
||||
const fastf_t *bbox_min,
|
||||
const fastf_t *bbox_max);
|
||||
|
||||
/** Release storage owned by a structured Crofton result. */
|
||||
RT_EXPORT extern void rt_crofton_result_free(
|
||||
struct rt_crofton_result *result);
|
||||
|
||||
|
||||
/**
|
||||
* Cauchy-Crofton surface-area and/or volume estimator for a primitive.
|
||||
|
||||
@@ -45,14 +45,6 @@ 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
|
||||
)
|
||||
|
||||
@@ -1,619 +0,0 @@
|
||||
#!/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())
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/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())
|
||||
@@ -1,501 +0,0 @@
|
||||
#!/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())
|
||||
@@ -1,939 +0,0 @@
|
||||
#!/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())
|
||||
@@ -1,288 +0,0 @@
|
||||
#!/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())
|
||||
@@ -1,449 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,178 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,508 +0,0 @@
|
||||
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.
|
||||
@@ -41,6 +41,7 @@ set(
|
||||
badmagic.c
|
||||
bitv.c
|
||||
bomb.c
|
||||
binding.cpp
|
||||
booleanize.c
|
||||
bu_init.cpp
|
||||
cache.cpp
|
||||
@@ -234,7 +235,7 @@ set(
|
||||
mime.cmake
|
||||
parallel.h
|
||||
parallel_cpp11thread.cpp
|
||||
process.h
|
||||
process_private.h
|
||||
sha1.h
|
||||
struetype.h
|
||||
vfont.h
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
/* B I N D I N G . C P P
|
||||
* BRL-CAD
|
||||
*
|
||||
* Copyright (c) 2026 United States Government as represented by
|
||||
* the U.S. Army Research Laboratory.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public License
|
||||
* version 2.1 as published by the Free Software Foundation.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this file; see the file named COPYING for more
|
||||
* information.
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "bu/binding.h"
|
||||
#include "bu/log.h"
|
||||
#include "bu/malloc.h"
|
||||
#include "bu/str.h"
|
||||
#include "bu/vls.h"
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
/*
|
||||
* Internal data structures to hold the parsed bindings and callbacks
|
||||
*/
|
||||
struct ActionCallback {
|
||||
bu_action_func_t func;
|
||||
void *client_data;
|
||||
};
|
||||
|
||||
struct BindingDef {
|
||||
json match;
|
||||
std::string action;
|
||||
};
|
||||
|
||||
/* Global state for libbu bindings */
|
||||
static std::map<std::string, std::vector<BindingDef>>& get_contexts() {
|
||||
static auto* ptr = new std::map<std::string, std::vector<BindingDef>>();
|
||||
return *ptr;
|
||||
}
|
||||
static std::map<std::string, ActionCallback>& get_action_registry() {
|
||||
static auto* ptr = new std::map<std::string, ActionCallback>();
|
||||
return *ptr;
|
||||
}
|
||||
static std::map<std::string, std::string>& get_schema_paths() {
|
||||
static auto* ptr = new std::map<std::string, std::string>();
|
||||
return *ptr;
|
||||
}
|
||||
static std::map<std::string, std::set<std::string>>& get_schemas() {
|
||||
static auto* ptr = new std::map<std::string, std::set<std::string>>();
|
||||
return *ptr;
|
||||
}
|
||||
|
||||
static void
|
||||
ensure_schema_loaded(const std::string& ctx_name)
|
||||
{
|
||||
if (get_schemas().find(ctx_name) != get_schemas().end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = get_schema_paths().find(ctx_name);
|
||||
if (it == get_schema_paths().end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::ifstream file(it->second);
|
||||
if (!file.is_open()) {
|
||||
bu_log("ensure_schema_loaded: Failed to open schema %s\n", it->second.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
json j;
|
||||
try {
|
||||
file >> j;
|
||||
} catch (const json::parse_error& e) {
|
||||
bu_log("ensure_schema_loaded: JSON parse error in schema %s: %s\n", it->second.c_str(), e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
std::set<std::string> valid_actions;
|
||||
if (j.contains("actions") && j["actions"].is_object()) {
|
||||
for (auto el = j["actions"].begin(); el != j["actions"].end(); ++el) {
|
||||
valid_actions.insert(el.key());
|
||||
}
|
||||
}
|
||||
get_schemas()[ctx_name] = valid_actions;
|
||||
}
|
||||
|
||||
/* W3C Key validation placeholder. A full implementation would check against
|
||||
* an exhaustive list of W3C standard keys (e.g., "Escape", "ArrowUp").
|
||||
* For demonstration, we'll just implement a simple sanity check.
|
||||
*/
|
||||
static int
|
||||
validate_w3c_key(const std::string& key)
|
||||
{
|
||||
/* Example: Reject completely upper-case strings that might be native C macros */
|
||||
if (key.length() > 1 && key == "RETURN") return 0;
|
||||
if (key.length() > 1 && key == "SPACE") return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* W3C Button validation. Values 0, 1, 2, 3, 4 are standard. */
|
||||
static int
|
||||
validate_w3c_button(int button)
|
||||
{
|
||||
return (button >= 0 && button <= 4);
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
bu_binding_load(const char *json_path)
|
||||
{
|
||||
if (!json_path) return -1;
|
||||
|
||||
std::ifstream file(json_path);
|
||||
if (!file.is_open()) {
|
||||
bu_log("bu_binding_load: Failed to open %s\n", json_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
return bu_binding_load_str(buffer.str().c_str());
|
||||
}
|
||||
|
||||
int
|
||||
bu_binding_load_str(const char *json_string)
|
||||
{
|
||||
if (!json_string) return -1;
|
||||
|
||||
json j;
|
||||
try {
|
||||
j = json::parse(json_string);
|
||||
} catch (const json::parse_error& e) {
|
||||
bu_log("bu_binding_load_str: JSON parse error: %s\n", e.what());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!j.contains("contexts") || !j["contexts"].is_object()) {
|
||||
bu_log("bu_binding_load_str: Invalid JSON structure (missing 'contexts' object)\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (auto it = j["contexts"].begin(); it != j["contexts"].end(); ++it) {
|
||||
std::string ctx_name = it.key();
|
||||
const auto& ctx_obj = it.value();
|
||||
|
||||
if (!ctx_obj.contains("bindings") || !ctx_obj["bindings"].is_array()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<BindingDef> defs;
|
||||
|
||||
ensure_schema_loaded(ctx_name);
|
||||
bool has_schema = (get_schemas().find(ctx_name) != get_schemas().end());
|
||||
|
||||
for (const auto& binding : ctx_obj["bindings"]) {
|
||||
if (!binding.contains("match") || !binding.contains("action")) {
|
||||
bu_log("bu_binding_load_str: Binding missing 'match' or 'action' in context %s. Skipping.\n", ctx_name.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
const json& match = binding["match"];
|
||||
std::string action = binding["action"].get<std::string>();
|
||||
|
||||
/* Phase 1 Validation: Check W3C standard compliance */
|
||||
int is_valid = 1;
|
||||
if (match.contains(BU_BINDING_ATTR_KEY)) {
|
||||
if (!match[BU_BINDING_ATTR_KEY].is_string() || !validate_w3c_key(match[BU_BINDING_ATTR_KEY].get<std::string>())) {
|
||||
bu_log("bu_binding_load_str: Invalid W3C key '%s' in context %s. Binding rejected.\n",
|
||||
match[BU_BINDING_ATTR_KEY].dump().c_str(), ctx_name.c_str());
|
||||
is_valid = 0;
|
||||
}
|
||||
}
|
||||
if (match.contains(BU_BINDING_ATTR_BUTTON)) {
|
||||
if (!match[BU_BINDING_ATTR_BUTTON].is_number_integer() || !validate_w3c_button(match[BU_BINDING_ATTR_BUTTON].get<int>())) {
|
||||
bu_log("bu_binding_load_str: Invalid W3C button '%s' in context %s. Binding rejected.\n",
|
||||
match[BU_BINDING_ATTR_BUTTON].dump().c_str(), ctx_name.c_str());
|
||||
is_valid = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_valid) continue;
|
||||
|
||||
/* Phase 2 Validation: Schema constraints */
|
||||
if (has_schema) {
|
||||
if (get_schemas()[ctx_name].find(action) == get_schemas()[ctx_name].end()) {
|
||||
bu_log("bu_binding_load_str: Binding for action '%s' rejected by schema for context '%s'.\n",
|
||||
action.c_str(), ctx_name.c_str());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
BindingDef def;
|
||||
def.match = match;
|
||||
def.action = action;
|
||||
get_contexts()[ctx_name].insert(get_contexts()[ctx_name].begin(), def);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
bu_binding_register_schema(const char *context, const char *json_path)
|
||||
{
|
||||
if (!context || !json_path) return;
|
||||
get_schema_paths()[std::string(context)] = std::string(json_path);
|
||||
}
|
||||
|
||||
void
|
||||
bu_binding_schema_add_action(const char *context, const char *action_name, const char *description)
|
||||
{
|
||||
if (!context || !action_name) return;
|
||||
std::string ctx(context);
|
||||
ensure_schema_loaded(ctx);
|
||||
get_schemas()[ctx].insert(std::string(action_name));
|
||||
}
|
||||
|
||||
void
|
||||
bu_binding_schema_remove_action(const char *context, const char *action_name)
|
||||
{
|
||||
if (!context || !action_name) return;
|
||||
std::string ctx(context);
|
||||
ensure_schema_loaded(ctx);
|
||||
get_schemas()[ctx].erase(std::string(action_name));
|
||||
}
|
||||
|
||||
void
|
||||
bu_binding_register_action(const char *action_name, bu_action_func_t func, void *client_data)
|
||||
{
|
||||
if (!action_name || !func) return;
|
||||
|
||||
ActionCallback cb;
|
||||
cb.func = func;
|
||||
cb.client_data = client_data;
|
||||
|
||||
get_action_registry()[std::string(action_name)] = cb;
|
||||
}
|
||||
|
||||
/* Helper to check if the json 'event' contains all the key-value pairs required by 'match' */
|
||||
static int
|
||||
is_match(const json& match, const json& event)
|
||||
{
|
||||
for (auto it = match.begin(); it != match.end(); ++it) {
|
||||
if (!event.contains(it.key())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* If it's an array (like modifiers), we need to ensure all elements in 'match' exist in 'event' */
|
||||
if (it.value().is_array()) {
|
||||
if (!event[it.key()].is_array()) return 0;
|
||||
|
||||
for (const auto& req_mod : it.value()) {
|
||||
int found = 0;
|
||||
for (const auto& ev_mod : event[it.key()]) {
|
||||
if (ev_mod == req_mod) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) return 0;
|
||||
}
|
||||
}
|
||||
/* Otherwise, simple equality */
|
||||
else if (event[it.key()] != it.value()) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
bu_binding_process_event(const char *context, const char *event_json, void *event_data)
|
||||
{
|
||||
if (!context || !event_json) return 0;
|
||||
|
||||
std::string ctx_name(context);
|
||||
auto it = get_contexts().find(ctx_name);
|
||||
if (it == get_contexts().end()) {
|
||||
return 0; /* Context not found */
|
||||
}
|
||||
|
||||
const char *json_str = event_json;
|
||||
|
||||
json event_json_obj;
|
||||
try {
|
||||
event_json_obj = json::parse(json_str);
|
||||
} catch (const json::parse_error& e) {
|
||||
bu_log("bu_binding_process_event: Invalid event JSON: %s\n", e.what());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!event_json_obj.is_object()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const auto& def : it->second) {
|
||||
if (is_match(def.match, event_json_obj)) {
|
||||
/* Match found */
|
||||
auto reg_it = get_action_registry().find(def.action);
|
||||
if (reg_it != get_action_registry().end()) {
|
||||
reg_it->second.func(event_json, reg_it->second.client_data, event_data);
|
||||
return 1;
|
||||
} else {
|
||||
/* We have a binding, but no callback registered for this action */
|
||||
bu_log("Warning: Event mapped to action '%s', but no callback is registered.\n", def.action.c_str());
|
||||
return 1; /* Still mark as handled since it matched a binding */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0; /* Unhandled */
|
||||
}
|
||||
|
||||
extern "C" int
|
||||
bu_binding_action_is_bound(const char *context, const char *action_name)
|
||||
{
|
||||
if (!context || !action_name) return 0;
|
||||
|
||||
std::string ctx(context);
|
||||
std::string act(action_name);
|
||||
|
||||
auto it = get_contexts().find(ctx);
|
||||
if (it == get_contexts().end()) return 0;
|
||||
|
||||
for (const auto& def : it->second) {
|
||||
if (def.action == act) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Local Variables:
|
||||
* mode: C++
|
||||
* tab-width: 8
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "stroustrup"
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||
+1
-1
@@ -49,7 +49,7 @@
|
||||
#include "bu/str.h"
|
||||
#include "bu/datetime.h"
|
||||
#include "bu/vls.h"
|
||||
#include "./process.h"
|
||||
#include "./process_private.h"
|
||||
|
||||
#ifndef HAVE_KILL
|
||||
# include <TlHelp32.h>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* P R O C E S S . H
|
||||
/* P R O C E S S _ P R I V A T E . H
|
||||
* BRL-CAD
|
||||
*
|
||||
* Copyright (c) 2013-2026 United States Government as represented by
|
||||
@@ -20,8 +20,8 @@
|
||||
|
||||
/* Private libbu process related definitions shared between multiple files */
|
||||
|
||||
#ifndef LIBBU_PROCESS_H
|
||||
#define LIBBU_PROCESS_H
|
||||
#ifndef LIBBU_PROCESS_PRIVATE_H
|
||||
#define LIBBU_PROCESS_PRIVATE_H
|
||||
|
||||
/* Apparently the termination code on Windows is arbitrary and somewhat
|
||||
* ill-defined - see https://bugs.python.org/issue31863 for more info. Try our
|
||||
@@ -29,7 +29,7 @@
|
||||
* abort identification. */
|
||||
#define BU_MSVC_ABORT_EXIT 97101 /* ascii - a == 97 && e == 101 */
|
||||
|
||||
#endif /* LIBBU_PROCESS_H */
|
||||
#endif /* LIBBU_PROCESS_PRIVATE_H */
|
||||
|
||||
/*
|
||||
* Local Variables:
|
||||
@@ -10,6 +10,7 @@ set(
|
||||
test_bomb.c
|
||||
test_badmagic.c
|
||||
test_basename.c
|
||||
test_binding.cpp
|
||||
test_bitv.c
|
||||
test_booleanize.c
|
||||
test_cache.cpp
|
||||
@@ -122,6 +123,7 @@ brlcad_addexec(bu_gethostname test_gethostname.c libbu TEST)
|
||||
brlcad_add_test(NAME bu_argv COMMAND bu_test test_argv)
|
||||
brlcad_add_test(NAME bu_backtrace COMMAND bu_test test_backtrace)
|
||||
brlcad_add_test(NAME bu_bomb COMMAND bu_test test_bomb "$<TARGET_FILE:bu_test>")
|
||||
brlcad_add_test(NAME bu_binding COMMAND bu_test test_binding "${CMAKE_CURRENT_SOURCE_DIR}/test_bindings.json")
|
||||
brlcad_add_test(NAME bu_dir COMMAND bu_test test_dir)
|
||||
brlcad_add_test(NAME bu_list_empty COMMAND bu_test test_list 0)
|
||||
brlcad_add_test(NAME bu_list_single COMMAND bu_test test_list 1)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/* T E S T _ B I N D I N G . C P P
|
||||
* BRL-CAD
|
||||
*
|
||||
* Copyright (c) 2026 United States Government as represented by
|
||||
* the U.S. Army Research Laboratory.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public License
|
||||
* version 2.1 as published by the Free Software Foundation.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this file; see the file named COPYING for more
|
||||
* information.
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "bu/binding.h"
|
||||
#include "bu/vls.h"
|
||||
|
||||
int action_triggered = 0;
|
||||
|
||||
void mock_action_handler(const char *event_json, void *registered_data, void *event_data) {
|
||||
action_triggered = 1;
|
||||
printf("Action handler triggered with JSON: %s\n", event_json);
|
||||
}
|
||||
|
||||
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
bu_setprogname(argv[0]);
|
||||
|
||||
if (argc < 2) {
|
||||
printf("Usage: %s <path_to_test_bindings.json>\n", argv[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *json_path = argv[1];
|
||||
|
||||
/* Register schema to restrict allowed actions */
|
||||
bu_binding_schema_add_action("fb", "fb_zoom", "Zoom");
|
||||
bu_binding_schema_add_action("fb", "fb_close_window", "Close Window");
|
||||
|
||||
/* Register hooks before loading JSON */
|
||||
bu_binding_register_action("fb_close_window", mock_action_handler, NULL);
|
||||
|
||||
printf("Loading bindings from %s\n", json_path);
|
||||
if (bu_binding_load(json_path) < 0) {
|
||||
printf("Failed to load bindings.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Test 1: Simulate a matching event */
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-press\", \"button\": 2}");
|
||||
|
||||
action_triggered = 0;
|
||||
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), NULL);
|
||||
|
||||
if (!action_triggered) {
|
||||
printf("Test 1 Failed: Expected action was not triggered.\n");
|
||||
return -1;
|
||||
}
|
||||
printf("Test 1 Passed: Action triggered successfully.\n");
|
||||
|
||||
/* Test 2: Simulate a non-matching event */
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-press\", \"button\": 1}");
|
||||
action_triggered = 0;
|
||||
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), NULL);
|
||||
|
||||
if (action_triggered) {
|
||||
printf("Test 2 Failed: Action should not have been triggered.\n");
|
||||
return -1;
|
||||
}
|
||||
printf("Test 2 Passed: Non-matching event correctly ignored.\n");
|
||||
|
||||
/* Test 3: The Escape key binding should have been rejected by the validator */
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"key-press\", \"key\": \"Escape\"}");
|
||||
action_triggered = 0;
|
||||
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), NULL);
|
||||
|
||||
if (action_triggered) {
|
||||
printf("Test 3 Failed: Escape binding should have been filtered by the validator.\n");
|
||||
return -1;
|
||||
}
|
||||
printf("Test 3 Passed: Validator correctly rejected binding at load time.\n");
|
||||
|
||||
bu_vls_free(&ev_json);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Local Variables:
|
||||
* mode: C++
|
||||
* tab-width: 8
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "stroustrup"
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"contexts": {
|
||||
"fb": {
|
||||
"bindings": [
|
||||
{
|
||||
"match": {
|
||||
"type": "mouse-press",
|
||||
"button": 2
|
||||
},
|
||||
"action": "fb_close_window"
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"type": "key-press",
|
||||
"key": "Escape"
|
||||
},
|
||||
"action": "fb_invalid_action"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,11 @@ set_property(
|
||||
PROPERTY COMPILE_DEFINITIONS "DM_PLUGIN_SUFFIX=\"${CMAKE_SHARED_LIBRARY_SUFFIX}\""
|
||||
)
|
||||
|
||||
# Files related to key/mouse binding management
|
||||
brlcad_manage_files(dm_bindings.json ${DATA_DIR}/bindings)
|
||||
brlcad_manage_files(dm_schema.json ${DATA_DIR}/bindings)
|
||||
brlcad_manage_files(fb_schema.json ${DATA_DIR}/bindings)
|
||||
|
||||
brlcad_find_package(PNG REQUIRED)
|
||||
find_package(OpenSSL QUIET)
|
||||
|
||||
@@ -165,3 +170,4 @@ cmakefiles(${libdm_ignore_files})
|
||||
# indent-tabs-mode: t
|
||||
# End:
|
||||
# ex: shiftwidth=2 tabstop=8
|
||||
|
||||
|
||||
@@ -2925,6 +2925,7 @@ X24_handle_event(struct fb *ifp, XEvent *event)
|
||||
case ButtonPress:
|
||||
{
|
||||
int button = (int) event->xbutton.button;
|
||||
int w3c_button = 0;
|
||||
|
||||
/* In interactive mode, report the event to the application
|
||||
* rather than consuming it here. */
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"contexts": {
|
||||
"fb": {
|
||||
"description": "Standard interactions for raw framebuffers (rt, fbserv)",
|
||||
"bindings": [
|
||||
{
|
||||
"match": {
|
||||
"type": "mouse-press",
|
||||
"button": 1
|
||||
},
|
||||
"action": "fb_print_pixel_rgb"
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"type": "mouse-press",
|
||||
"button": 2
|
||||
},
|
||||
"action": "fb_close_window"
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"type": "key-press",
|
||||
"key": "Escape"
|
||||
},
|
||||
"action": "fb_close_window"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dm": {
|
||||
"description": "Standard bindings for the Geometry Editor (qged & mged)",
|
||||
"bindings": [
|
||||
{ "match": { "type": "key-press", "key": "A" }, "action": "view_toggle_adc" },
|
||||
{ "match": { "type": "key-press", "key": "M" }, "action": "view_toggle_model_axes" },
|
||||
{ "match": { "type": "key-press", "key": "V" }, "action": "view_toggle_view_axes" },
|
||||
{ "match": { "type": "key-press", "key": "2" }, "action": "view_2" },
|
||||
{ "match": { "type": "key-press", "key": "3" }, "action": "view_3" },
|
||||
{ "match": { "type": "key-press", "key": "4" }, "action": "view_4" },
|
||||
{ "match": { "type": "key-press", "key": "5" }, "action": "view_5" },
|
||||
{ "match": { "type": "key-press", "key": "6" }, "action": "view_6" },
|
||||
{ "match": { "type": "key-press", "key": "7" }, "action": "view_7" },
|
||||
{ "match": { "type": "key-press", "key": "F" }, "action": "view_front" },
|
||||
{ "match": { "type": "key-press", "key": "T" }, "action": "view_top" },
|
||||
{ "match": { "type": "key-press", "key": "B" }, "action": "view_bottom" },
|
||||
{ "match": { "type": "key-press", "key": "L" }, "action": "view_left" },
|
||||
{ "match": { "type": "key-press", "key": "R", "modifiers": ["Shift"] }, "action": "view_rear" },
|
||||
{ "match": { "type": "key-press", "key": "R" }, "action": "view_right" },
|
||||
{ "match": { "type": "mouse-press", "button": 0 }, "action": "view_mouse_press_left" },
|
||||
{ "match": { "type": "mouse-release", "button": 0 }, "action": "view_mouse_release_left" },
|
||||
{ "match": { "type": "mouse-release", "button": 2 }, "action": "view_mouse_release_right" },
|
||||
{ "match": { "type": "mouse-release", "button": 1 }, "action": "view_mouse_release_middle" },
|
||||
{ "match": { "type": "mouse-move", "button": 0, "modifiers": ["Control"] }, "action": "view_mouse_drag_rotate" },
|
||||
{ "match": { "type": "mouse-move", "button": 0, "modifiers": ["Shift"] }, "action": "view_mouse_drag_translate" },
|
||||
{ "match": { "type": "mouse-move", "button": 0, "modifiers": ["Control", "Shift"] }, "action": "view_mouse_drag_scale" },
|
||||
{ "match": { "type": "mouse-move", "button": 0 }, "action": "view_mouse_drag_adjust" },
|
||||
{ "match": { "type": "mouse-wheel" }, "action": "view_mouse_wheel_scale" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,10 +66,22 @@ dm_init_msgs()
|
||||
}
|
||||
|
||||
|
||||
#include "bu/binding.h"
|
||||
|
||||
static void
|
||||
libdm_init(void)
|
||||
{
|
||||
bu_vls_init(&init_msgs);
|
||||
|
||||
const char *dm_schema = bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "dm_schema.json", NULL);
|
||||
if (dm_schema) {
|
||||
bu_binding_register_schema("dm", dm_schema);
|
||||
}
|
||||
|
||||
const char *fb_schema = bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "fb_schema.json", NULL);
|
||||
if (fb_schema) {
|
||||
bu_binding_register_schema("fb", fb_schema);
|
||||
}
|
||||
|
||||
const char *ppath = bu_dir(NULL, 0, BU_DIR_LIBEXEC, "dm", NULL);
|
||||
char **dm_filenames;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include "bu/binding.h"
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
@@ -62,6 +63,13 @@ dm_open(void *ctx, void *interp, const char *type, int argc, const char *argv[])
|
||||
|
||||
const struct dm *d = d_it->second;
|
||||
struct dm *dmp = d->i->dm_open(ctx, interp, argc, argv);
|
||||
|
||||
char *bindings_path = (char *)bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "dm_bindings.json", NULL);
|
||||
if (bindings_path) {
|
||||
bu_binding_load(bindings_path);
|
||||
bu_free(bindings_path, "bindings_path");
|
||||
}
|
||||
|
||||
return dmp;
|
||||
}
|
||||
|
||||
@@ -528,6 +536,12 @@ found_interface:
|
||||
|
||||
return FB_NULL;
|
||||
}
|
||||
char *bindings_path = (char *)bu_dir(NULL, 0, BU_DIR_DATA, "bindings", "dm_bindings.json", NULL);
|
||||
if (bindings_path) {
|
||||
bu_binding_load(bindings_path);
|
||||
bu_free(bindings_path, "bindings_path");
|
||||
}
|
||||
|
||||
return ifp;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"actions": {
|
||||
"view_toggle_adc": "Toggle Angle Distance Cursor",
|
||||
"view_toggle_model_axes": "Toggle Model Axes",
|
||||
"view_toggle_view_axes": "Toggle View Axes",
|
||||
"view_2": "Set View 2",
|
||||
"view_3": "Set View 3",
|
||||
"view_4": "Set View 4",
|
||||
"view_5": "Set View 5",
|
||||
"view_6": "Set View 6",
|
||||
"view_7": "Set View 7",
|
||||
"view_front": "Set Front View",
|
||||
"view_top": "Set Top View",
|
||||
"view_bottom": "Set Bottom View",
|
||||
"view_left": "Set Left View",
|
||||
"view_right": "Set Right View",
|
||||
"view_rear": "Set Rear View",
|
||||
"view_mouse_drag_rotate": "Rotate view via mouse drag",
|
||||
"view_mouse_drag_translate": "Translate view via mouse drag",
|
||||
"view_mouse_drag_scale": "Scale view via mouse drag",
|
||||
"view_mouse_wheel_scale": "Scale view via mouse wheel",
|
||||
"view_mouse_press_left": "Left mouse press action",
|
||||
"view_mouse_press_right": "Right mouse press action"
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@
|
||||
#include "./include/private.h"
|
||||
#include "icv.h"
|
||||
#include "dm.h"
|
||||
#include "bu/binding.h"
|
||||
|
||||
|
||||
struct fb *fb_get(void)
|
||||
@@ -258,6 +259,59 @@ long fb_poll_rate(struct fb *ifp)
|
||||
return ifp->i->if_poll_refresh_rate;
|
||||
}
|
||||
|
||||
static int
|
||||
fb_event_button_to_w3c(int button)
|
||||
{
|
||||
return (button >= 1 && button <= 3) ? button - 1 : button;
|
||||
}
|
||||
|
||||
int
|
||||
fb_process_event_bindings(struct fb *ifp, const struct fb_event *event)
|
||||
{
|
||||
struct bu_vls event_json = BU_VLS_INIT_ZERO;
|
||||
int handled = 0;
|
||||
|
||||
if (!ifp || !event)
|
||||
return 0;
|
||||
|
||||
switch (event->type) {
|
||||
case FB_EVENT_BUTTON_PRESS:
|
||||
bu_vls_sprintf(&event_json,
|
||||
"{\"type\": \"mouse-press\", \"button\": %d, \"x\": %d, \"y\": %d}",
|
||||
fb_event_button_to_w3c(event->button), event->x, event->y);
|
||||
break;
|
||||
case FB_EVENT_BUTTON_RELEASE:
|
||||
bu_vls_sprintf(&event_json,
|
||||
"{\"type\": \"mouse-release\", \"button\": %d, \"x\": %d, \"y\": %d}",
|
||||
fb_event_button_to_w3c(event->button), event->x, event->y);
|
||||
break;
|
||||
case FB_EVENT_MOTION:
|
||||
bu_vls_sprintf(&event_json,
|
||||
"{\"type\": \"mouse-move\", \"x\": %d, \"y\": %d}",
|
||||
event->x, event->y);
|
||||
break;
|
||||
case FB_EVENT_KEY_PRESS:
|
||||
if (event->keycode == 27) {
|
||||
bu_vls_sprintf(&event_json, "{\"type\": \"key-press\", \"key\": \"Escape\"}");
|
||||
} else if (event->keycode >= 32 && event->keycode <= 126) {
|
||||
bu_vls_sprintf(&event_json,
|
||||
"{\"type\": \"key-press\", \"key\": \"%c\"}",
|
||||
event->keycode);
|
||||
}
|
||||
break;
|
||||
case FB_EVENT_CLOSE:
|
||||
bu_vls_sprintf(&event_json, "{\"type\": \"key-press\", \"key\": \"Escape\"}");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (bu_vls_strlen(&event_json) > 0)
|
||||
handled = bu_binding_process_event("fb", bu_vls_cstr(&event_json), ifp);
|
||||
bu_vls_free(&event_json);
|
||||
return handled;
|
||||
}
|
||||
|
||||
void fb_set_interactive(struct fb *ifp, int on)
|
||||
{
|
||||
if (!ifp)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"actions": {
|
||||
"fb_print_pixel_rgb": "Print RGB values of pixel under cursor",
|
||||
"fb_close_window": "Close framebuffer window"
|
||||
}
|
||||
}
|
||||
+171
-72
@@ -61,6 +61,10 @@
|
||||
#include "dm.h"
|
||||
#include "./fb_ogl.h"
|
||||
|
||||
#include "bu/binding.h"
|
||||
#include "bu/file.h"
|
||||
#include <X11/keysym.h>
|
||||
|
||||
extern struct fb ogl_interface;
|
||||
|
||||
#define DIRECT_COLOR_VISUAL_ALLOWED 0
|
||||
@@ -774,6 +778,52 @@ ogl_configureWindow(struct fb *ifp, int width, int height)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
ogl_action_fb_print_pixel_rgb(const struct bu_vls *event_json, void *registered_data, void *event_data)
|
||||
{
|
||||
struct fb *ifp = (struct fb *)event_data;
|
||||
int x = -1, y = -1;
|
||||
char *p;
|
||||
|
||||
if (!ifp) return;
|
||||
|
||||
if ((p = strstr(bu_vls_cstr(event_json), "\"x\":"))) {
|
||||
sscanf(p + 4, "%d", &x);
|
||||
}
|
||||
if ((p = strstr(bu_vls_cstr(event_json), "\"y\":"))) {
|
||||
sscanf(p + 4, "%d", &y);
|
||||
}
|
||||
|
||||
if (x < 0 || y < 0) {
|
||||
fb_log("No RGB (outside image viewport)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t memidx = (y * WIN(ifp)->mi_pixwidth) * sizeof(struct fb_pixel);
|
||||
struct fb_pixel *oglp = (struct fb_pixel *)&ifp->i->if_mem[memidx];
|
||||
|
||||
fb_log("At image (%d, %d), real RGB=(%3d %3d %3d)\n",
|
||||
x, y, (int)oglp[x].red, (int)oglp[x].green, (int)oglp[x].blue);
|
||||
}
|
||||
|
||||
static void
|
||||
ogl_action_fb_close_window(const struct bu_vls *event_json, void *registered_data, void *event_data)
|
||||
{
|
||||
struct fb *ifp = (struct fb *)event_data;
|
||||
if (ifp) OGL(ifp)->alive = 0;
|
||||
}
|
||||
|
||||
static int bindings_loaded = 0;
|
||||
static void
|
||||
ogl_init_bindings(void)
|
||||
{
|
||||
if (bindings_loaded) return;
|
||||
bindings_loaded = 1;
|
||||
|
||||
|
||||
bu_binding_register_action("fb_print_pixel_rgb", ogl_action_fb_print_pixel_rgb, NULL);
|
||||
bu_binding_register_action("fb_close_window", ogl_action_fb_close_window, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
ogl_do_event(struct fb *ifp)
|
||||
@@ -781,76 +831,122 @@ ogl_do_event(struct fb *ifp)
|
||||
XEvent event;
|
||||
|
||||
while (XCheckWindowEvent(OGL(ifp)->dispp, OGL(ifp)->wind,
|
||||
OGL(ifp)->event_mask, &event)) {
|
||||
switch (event.type) {
|
||||
case Expose:
|
||||
if (!OGL(ifp)->use_ext_ctrl)
|
||||
expose_callback(ifp);
|
||||
break;
|
||||
case ButtonPress:
|
||||
{
|
||||
int button = (int) event.xbutton.button;
|
||||
if (button == Button1) {
|
||||
/* Check for single button mouse remap.
|
||||
* ctrl-1 => 2
|
||||
* meta-1 => 3
|
||||
* cmdkey => 3
|
||||
*/
|
||||
if (event.xbutton.state & ControlMask) {
|
||||
button = Button2;
|
||||
} else if (event.xbutton.state & Mod1Mask) {
|
||||
button = Button3;
|
||||
} else if (event.xbutton.state & Mod2Mask) {
|
||||
button = Button3;
|
||||
}
|
||||
}
|
||||
|
||||
switch (button) {
|
||||
case Button1:
|
||||
break;
|
||||
case Button2:
|
||||
{
|
||||
int x, y;
|
||||
register struct fb_pixel *oglp;
|
||||
|
||||
x = event.xbutton.x;
|
||||
y = ifp->i->if_height - event.xbutton.y - 1;
|
||||
|
||||
if (x < 0 || y < 0) {
|
||||
fb_log("No RGB (outside image viewport)\n");
|
||||
break;
|
||||
}
|
||||
|
||||
size_t memidx = (y * WIN(ifp)->mi_pixwidth) * sizeof(struct fb_pixel);
|
||||
oglp = (struct fb_pixel *)&ifp->i->if_mem[memidx];
|
||||
|
||||
fb_log("At image (%d, %d), real RGB=(%3d %3d %3d)\n",
|
||||
x, y, (int)oglp[x].red, (int)oglp[x].green, (int)oglp[x].blue);
|
||||
|
||||
break;
|
||||
}
|
||||
case Button3:
|
||||
OGL(ifp)->alive = 0;
|
||||
break;
|
||||
default:
|
||||
fb_log("unhandled mouse event\n");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ConfigureNotify:
|
||||
{
|
||||
XConfigureEvent *conf = (XConfigureEvent *)&event;
|
||||
|
||||
if (conf->width == OGL(ifp)->win_width &&
|
||||
conf->height == OGL(ifp)->win_height)
|
||||
return;
|
||||
|
||||
ogl_configureWindow(ifp, conf->width, conf->height);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
OGL(ifp)->event_mask, &event)) {
|
||||
switch (event.type) {
|
||||
case Expose:
|
||||
if (!OGL(ifp)->use_ext_ctrl)
|
||||
expose_callback(ifp);
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev = {FB_EVENT_EXPOSE, 0, 0, 0, 0, 0};
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
}
|
||||
break;
|
||||
case ButtonPress:
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev;
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = FB_EVENT_BUTTON_PRESS;
|
||||
ev.button = (int)event.xbutton.button;
|
||||
ev.x = event.xbutton.x;
|
||||
ev.y = ifp->i->if_height - event.xbutton.y - 1;
|
||||
ev.state = (int)event.xbutton.state;
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
} else {
|
||||
int button = (int)event.xbutton.button;
|
||||
int w3c_button = 0;
|
||||
if (button == Button1) {
|
||||
if (event.xbutton.state & ControlMask) button = Button2;
|
||||
else if (event.xbutton.state & Mod1Mask) button = Button3;
|
||||
else if (event.xbutton.state & Mod2Mask) button = Button3;
|
||||
}
|
||||
if (button == Button1) w3c_button = 0;
|
||||
else if (button == Button2) w3c_button = 1;
|
||||
else if (button == Button3) w3c_button = 2;
|
||||
else w3c_button = button;
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json,
|
||||
"{\"type\": \"mouse-press\", \"button\": %d, \"x\": %d, \"y\": %d}",
|
||||
w3c_button, event.xbutton.x,
|
||||
ifp->i->if_height - event.xbutton.y - 1);
|
||||
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), ifp);
|
||||
bu_vls_free(&ev_json);
|
||||
}
|
||||
break;
|
||||
case ButtonRelease:
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev;
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = FB_EVENT_BUTTON_RELEASE;
|
||||
ev.button = (int)event.xbutton.button;
|
||||
ev.x = event.xbutton.x;
|
||||
ev.y = ifp->i->if_height - event.xbutton.y - 1;
|
||||
ev.state = (int)event.xbutton.state;
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
}
|
||||
break;
|
||||
case MotionNotify:
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev;
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = FB_EVENT_MOTION;
|
||||
ev.x = event.xmotion.x;
|
||||
ev.y = ifp->i->if_height - event.xmotion.y - 1;
|
||||
ev.state = (int)event.xmotion.state;
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
}
|
||||
break;
|
||||
case KeyPress:
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev;
|
||||
char keybuf[8] = {0};
|
||||
KeySym keysym;
|
||||
int n;
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = FB_EVENT_KEY_PRESS;
|
||||
n = XLookupString(&event.xkey, keybuf, sizeof(keybuf), &keysym, NULL);
|
||||
ev.keycode = (n > 0) ? (unsigned char)keybuf[0] : (int)keysym;
|
||||
ev.state = (int)event.xkey.state;
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
} else {
|
||||
KeySym keysym = XLookupKeysym(&event.xkey, 0);
|
||||
char *key_str = XKeysymToString(keysym);
|
||||
if (key_str) {
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json,
|
||||
"{\"type\": \"key-press\", \"key\": \"%s\"}", key_str);
|
||||
bu_binding_process_event("fb", bu_vls_cstr(&ev_json), ifp);
|
||||
bu_vls_free(&ev_json);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ClientMessage:
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev = {FB_EVENT_CLOSE, 0, 0, 0, 0, 0};
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
} else {
|
||||
OGL(ifp)->alive = 0;
|
||||
}
|
||||
break;
|
||||
case ConfigureNotify:
|
||||
{
|
||||
XConfigureEvent *conf = (XConfigureEvent *)&event;
|
||||
if (conf->width == OGL(ifp)->win_width &&
|
||||
conf->height == OGL(ifp)->win_height)
|
||||
break;
|
||||
ogl_configureWindow(ifp, conf->width, conf->height);
|
||||
if (fb_get_interactive(ifp)) {
|
||||
struct fb_event ev;
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = FB_EVENT_RESIZE;
|
||||
ev.x = conf->width;
|
||||
ev.y = conf->height;
|
||||
fb_enqueue_event(ifp, &ev);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1021,9 +1117,10 @@ fb_ogl_open(struct fb *ifp, const char *file, int width, int height)
|
||||
int mode, i;
|
||||
long valuemask;
|
||||
XSetWindowAttributes swa;
|
||||
|
||||
FB_CK_FB(ifp->i);
|
||||
|
||||
ogl_init_bindings();
|
||||
|
||||
/*
|
||||
* First, attempt to determine operating mode for this open,
|
||||
* based upon the "unit number" or flags.
|
||||
@@ -1214,7 +1311,7 @@ fb_ogl_open(struct fb *ifp, const char *file, int width, int height)
|
||||
OGL(ifp)->vip->screen);
|
||||
swa.event_mask = OGL(ifp)->event_mask =
|
||||
ExposureMask | KeyPressMask | KeyReleaseMask |
|
||||
ButtonPressMask | ButtonReleaseMask;
|
||||
ButtonPressMask | ButtonReleaseMask | PointerMotionMask | StructureNotifyMask;
|
||||
swa.colormap = OGL(ifp)->xcmap;
|
||||
|
||||
#define XCreateWindowDebug(display, parent, x, y, width, height, border_width, depth, class, visual, valuemask, attributes) \
|
||||
@@ -1381,6 +1478,8 @@ ogl_open_existing(struct fb *ifp, int width, int height, struct fb_platform_spec
|
||||
{
|
||||
struct ogl_fb_info *ogl_internal = (struct ogl_fb_info *)fb_p->data;
|
||||
BU_CKMAG(fb_p, FB_OGL_MAGIC, "ogl framebuffer");
|
||||
|
||||
ogl_init_bindings();
|
||||
return open_existing(ifp, ogl_internal->dpy, ogl_internal->win,
|
||||
ogl_internal->cmap, ogl_internal->vip, width, height, ogl_internal->glxc,
|
||||
ogl_internal->double_buffer, ogl_internal->soft_cmap);
|
||||
|
||||
@@ -188,8 +188,7 @@ test_uncertain()
|
||||
require(result.shells.empty() || (result.shells.front().orientation == ShellOrientation::Indeterminate &&
|
||||
!result.shells.front().corrected), "invalid parameter-loop senses caused an incorrect face repair");
|
||||
auto flat = box();
|
||||
ON_Xform flatten;
|
||||
flatten.DiagonalTransformation(1.0);
|
||||
ON_Xform flatten = ON_Xform::DiagonalTransformation(1.0);
|
||||
flatten[2][2] = 1.0e-13;
|
||||
require(flat->Transform(flatten), "could not construct thin shell");
|
||||
result = repair_brep_orientation(*flat);
|
||||
|
||||
+241
-246
@@ -37,6 +37,12 @@ extern "C" {
|
||||
#include "qtcad/defines.h"
|
||||
#include "bindings.h"
|
||||
|
||||
extern "C" {
|
||||
#include "bu/binding.h"
|
||||
#include "bu/file.h"
|
||||
#include "bu/log.h"
|
||||
}
|
||||
|
||||
typedef void (*bounds_update_t)(struct bview *);
|
||||
static std::unordered_map<struct bview *, bounds_update_t> drag_bounds_updates;
|
||||
static std::unordered_map<struct bview *, long long> drag_update_ts;
|
||||
@@ -68,247 +74,256 @@ restore_drag_bounds_update(struct bview *v, int refresh_bounds)
|
||||
drag_update_ts.erase(v);
|
||||
}
|
||||
|
||||
// TODO - look into QShortcut, see if it might be a better way
|
||||
// to manage this
|
||||
static int bindings_loaded = 0;
|
||||
|
||||
static void qtcad_action_toggle_adc(const char *, void *, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (v) v->gv_s->gv_adc.draw = !v->gv_s->gv_adc.draw;
|
||||
}
|
||||
static void qtcad_action_toggle_model_axes(const char *, void *, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (v) v->gv_s->gv_model_axes.draw = !v->gv_s->gv_model_axes.draw;
|
||||
}
|
||||
static void qtcad_action_toggle_view_axes(const char *, void *, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (v) v->gv_s->gv_view_axes.draw = !v->gv_s->gv_view_axes.draw;
|
||||
}
|
||||
|
||||
static void qtcad_action_view_set(const char *, void *registered_data, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
const char *aet_str = (const char *)registered_data;
|
||||
if (v && aet_str) {
|
||||
bn_decode_vect(v->gv_aet, aet_str);
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
}
|
||||
}
|
||||
|
||||
static void qtcad_action_mouse_release(const char *event_json, void *, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (!v) return;
|
||||
|
||||
int cx = 0, cy = 0, mode = 0, dx = 1, dy = 1;
|
||||
char *p;
|
||||
if ((p = strstr(event_json, "\"cx\":"))) sscanf(p + 5, "%d", &cx);
|
||||
if ((p = strstr(event_json, "\"cy\":"))) sscanf(p + 5, "%d", &cy);
|
||||
if ((p = strstr(event_json, "\"mode\":"))) sscanf(p + 7, "%d", &mode);
|
||||
|
||||
int button = 0;
|
||||
if ((p = strstr(event_json, "\"button\":"))) sscanf(p + 9, "%d", &button);
|
||||
|
||||
unsigned long long view_flags = BV_IDLE;
|
||||
if (button == 0) { // Left
|
||||
if (mode != BV_CENTER) { view_flags = BV_SCALE; dx = 10; dy = 5; }
|
||||
else { view_flags = BV_CENTER; dx = cx; dy = cy; }
|
||||
} else if (button == 2) { // Right
|
||||
if (mode == BV_CENTER) return;
|
||||
view_flags = BV_SCALE; dx = 1; dy = 2;
|
||||
} else if (button == 1) { // Middle
|
||||
view_flags = BV_CENTER; dx = cx; dy = cy;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
point_t keypt = VINIT_ZERO;
|
||||
bv_adjust(v, dx, dy, keypt, 0, view_flags);
|
||||
}
|
||||
|
||||
static void qtcad_action_adjust_view(const char *event_json, void *registered_data, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (!v) return;
|
||||
|
||||
int dx = 0, dy = 0, mode = 0;
|
||||
int view_flags = (int)(intptr_t)registered_data;
|
||||
char *p;
|
||||
|
||||
if ((p = strstr(event_json, "\"dx\":"))) sscanf(p + 5, "%d", &dx);
|
||||
if ((p = strstr(event_json, "\"dy\":"))) sscanf(p + 5, "%d", &dy);
|
||||
if ((p = strstr(event_json, "\"mode\":"))) sscanf(p + 7, "%d", &mode);
|
||||
|
||||
if (view_flags == 0) { // dynamic fallback
|
||||
view_flags = mode;
|
||||
if (mode == BV_CENTER) view_flags = BV_SCALE;
|
||||
}
|
||||
|
||||
if (view_flags == BV_SCALE) {
|
||||
int mdelta = (abs(dx) > abs(dy)) ? dx : -dy;
|
||||
int f = (int)(2*100*(double)abs(mdelta)/(double)v->gv_height);
|
||||
if (mdelta > 0) { dy = 101 + f; dx = 100; }
|
||||
else { dy = 99 - f; dx = 100; }
|
||||
}
|
||||
|
||||
point_t center;
|
||||
MAT_DELTAS_GET_NEG(center, v->gv_center);
|
||||
|
||||
if (view_flags & (BV_ROT | BV_TRANS | BV_SCALE))
|
||||
suspend_drag_bounds_update(v);
|
||||
|
||||
bv_adjust(v, dx, dy, center, 0, view_flags);
|
||||
}
|
||||
|
||||
static void qtcad_action_wheel_scale(const char *event_json, void *, void *event_data) {
|
||||
struct bview *v = (struct bview *)event_data;
|
||||
if (!v) return;
|
||||
|
||||
int delta = 0;
|
||||
char *p;
|
||||
if ((p = strstr(event_json, "\"delta\":"))) sscanf(p + 8, "%d", &delta);
|
||||
|
||||
int dx = 100 + delta;
|
||||
int dy = 100;
|
||||
point_t origin = VINIT_ZERO;
|
||||
bv_adjust(v, dx, dy, origin, 0, BV_SCALE);
|
||||
}
|
||||
|
||||
extern "C" QCAD_EXPORT int qtcad_load_bindings(const char *json_path) {
|
||||
if (bu_binding_load(json_path) < 0) {
|
||||
bu_log("libqtcad: Failed to load bindings from %s\n", json_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!bu_binding_action_is_bound("dm", "view_toggle_adc") ||
|
||||
!bu_binding_action_is_bound("dm", "view_2")) {
|
||||
bu_log("libqtcad Validation Failed: Provided bindings missing required standard actions.\n");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void qtcad_init_bindings(void) {
|
||||
if (bindings_loaded) return;
|
||||
bindings_loaded = 1;
|
||||
|
||||
bu_binding_register_action("view_toggle_adc", qtcad_action_toggle_adc, NULL);
|
||||
bu_binding_register_action("view_toggle_model_axes", qtcad_action_toggle_model_axes, NULL);
|
||||
bu_binding_register_action("view_toggle_view_axes", qtcad_action_toggle_view_axes, NULL);
|
||||
|
||||
bu_binding_register_action("view_2", qtcad_action_view_set, (void*)"35 -25 0");
|
||||
bu_binding_register_action("view_3", qtcad_action_view_set, (void*)"35 25 0");
|
||||
bu_binding_register_action("view_4", qtcad_action_view_set, (void*)"45 45 0");
|
||||
bu_binding_register_action("view_5", qtcad_action_view_set, (void*)"145 25 0");
|
||||
bu_binding_register_action("view_6", qtcad_action_view_set, (void*)"215 25 0");
|
||||
bu_binding_register_action("view_7", qtcad_action_view_set, (void*)"325 25 0");
|
||||
bu_binding_register_action("view_front", qtcad_action_view_set, (void*)"0 0 0");
|
||||
bu_binding_register_action("view_top", qtcad_action_view_set, (void*)"270 90 0");
|
||||
bu_binding_register_action("view_bottom", qtcad_action_view_set, (void*)"270 -90 0");
|
||||
bu_binding_register_action("view_left", qtcad_action_view_set, (void*)"90 0 0");
|
||||
bu_binding_register_action("view_rear", qtcad_action_view_set, (void*)"180 0 0");
|
||||
bu_binding_register_action("view_right", qtcad_action_view_set, (void*)"270 0 0");
|
||||
|
||||
bu_binding_register_action("view_mouse_release_left", qtcad_action_mouse_release, NULL);
|
||||
bu_binding_register_action("view_mouse_release_right", qtcad_action_mouse_release, NULL);
|
||||
bu_binding_register_action("view_mouse_release_middle", qtcad_action_mouse_release, NULL);
|
||||
|
||||
bu_binding_register_action("view_mouse_drag_rotate", qtcad_action_adjust_view, (void*)(intptr_t)BV_ROT);
|
||||
bu_binding_register_action("view_mouse_drag_translate", qtcad_action_adjust_view, (void*)(intptr_t)BV_TRANS);
|
||||
bu_binding_register_action("view_mouse_drag_scale", qtcad_action_adjust_view, (void*)(intptr_t)BV_SCALE);
|
||||
bu_binding_register_action("view_mouse_drag_adjust", qtcad_action_adjust_view, (void*)(intptr_t)0);
|
||||
bu_binding_register_action("view_mouse_wheel_scale", qtcad_action_wheel_scale, NULL);
|
||||
}
|
||||
|
||||
static void append_modifiers(struct bu_vls *vls, QInputEvent *e) {
|
||||
if (!e->modifiers()) return;
|
||||
bu_vls_printf(vls, ", \"modifiers\": [");
|
||||
int added = 0;
|
||||
if (e->modifiers().testFlag(Qt::ShiftModifier)) { bu_vls_printf(vls, "\"Shift\""); added=1; }
|
||||
if (e->modifiers().testFlag(Qt::ControlModifier)) { if(added) bu_vls_printf(vls, ", "); bu_vls_printf(vls, "\"Control\""); added=1; }
|
||||
if (e->modifiers().testFlag(Qt::AltModifier)) { if(added) bu_vls_printf(vls, ", "); bu_vls_printf(vls, "\"Alt\""); }
|
||||
bu_vls_printf(vls, "]");
|
||||
}
|
||||
|
||||
int CADkeyPressEvent(struct bview *v, int UNUSED(x_prev), int UNUSED(y_prev), QKeyEvent *k)
|
||||
{
|
||||
QTCAD_EVENT("keyPress", 1);
|
||||
if (!v)
|
||||
return 0;
|
||||
#if 0
|
||||
QString kstr = QKeySequence(k->key()).toString();
|
||||
bu_log("%s\n", kstr.toStdString().c_str());
|
||||
#endif
|
||||
switch (k->key()) {
|
||||
case 'A':
|
||||
v->gv_s->gv_adc.draw = !v->gv_s->gv_adc.draw;
|
||||
return 1;
|
||||
case 'M':
|
||||
v->gv_s->gv_model_axes.draw = !v->gv_s->gv_model_axes.draw;
|
||||
return 1;
|
||||
case 'V':
|
||||
v->gv_s->gv_view_axes.draw = !v->gv_s->gv_view_axes.draw;
|
||||
return 1;
|
||||
case '2':
|
||||
bn_decode_vect(v->gv_aet, "35 -25 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case '3':
|
||||
bn_decode_vect(v->gv_aet, "35 25 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case '4':
|
||||
bn_decode_vect(v->gv_aet, "45 45 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case '5':
|
||||
bn_decode_vect(v->gv_aet, "145 25 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case '6':
|
||||
bn_decode_vect(v->gv_aet, "215 25 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case '7':
|
||||
bn_decode_vect(v->gv_aet, "325 25 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case 'F':
|
||||
bn_decode_vect(v->gv_aet, "0 0 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case 'T':
|
||||
bn_decode_vect(v->gv_aet, "270 90 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case 'B':
|
||||
bn_decode_vect(v->gv_aet, "270 -90 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case 'L':
|
||||
bn_decode_vect(v->gv_aet, "90 0 0");
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
case 'R':
|
||||
if (k->modifiers().testFlag(Qt::ShiftModifier) == true) {
|
||||
bn_decode_vect(v->gv_aet, "180 0 0");
|
||||
} else {
|
||||
bn_decode_vect(v->gv_aet, "270 0 0");
|
||||
}
|
||||
bv_mat_aet(v);
|
||||
bv_update(v);
|
||||
return 1;
|
||||
default:
|
||||
break;
|
||||
if (!v) return 0;
|
||||
qtcad_init_bindings();
|
||||
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
char key_str[8] = {0};
|
||||
if (k->key() >= 0x20 && k->key() <= 0x7E) {
|
||||
key_str[0] = (char)k->key();
|
||||
} else {
|
||||
QString kstr = QKeySequence(k->key()).toString();
|
||||
strncpy(key_str, kstr.toStdString().c_str(), 7);
|
||||
}
|
||||
return 0;
|
||||
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"key-press\", \"key\": \"%s\"", key_str);
|
||||
append_modifiers(&ev_json, k);
|
||||
bu_vls_printf(&ev_json, "}");
|
||||
|
||||
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
|
||||
bu_vls_free(&ev_json);
|
||||
return handled;
|
||||
}
|
||||
|
||||
int CADmousePressEvent(struct bview *v, int UNUSED(x_prev), int UNUSED(y_prev), QMouseEvent *e)
|
||||
{
|
||||
QTCAD_EVENT("mousePress", 1);
|
||||
if (!v) return 0;
|
||||
qtcad_init_bindings();
|
||||
|
||||
if (!v)
|
||||
return 0;
|
||||
int button = -1;
|
||||
if (e->buttons().testFlag(Qt::LeftButton)) button = 0;
|
||||
else if (e->buttons().testFlag(Qt::MiddleButton)) button = 1;
|
||||
else if (e->buttons().testFlag(Qt::RightButton)) button = 2;
|
||||
if (button < 0) return 0;
|
||||
|
||||
// If we're intending the mouse motion to do the work,
|
||||
// then the press has to be a no-op. If we're going
|
||||
// to do configurable key bindings, this will take some
|
||||
// thought - if we want unmodded left button to be a
|
||||
// rotation, and Ctrl+Left to do something else, these
|
||||
// checks are all going to have to be exact-flag-combo-only
|
||||
// actions.
|
||||
if (e->modifiers()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (e->buttons().testFlag(Qt::LeftButton)) {
|
||||
//bu_log("Press Left\n");
|
||||
}
|
||||
if (e->buttons().testFlag(Qt::RightButton)) {
|
||||
//bu_log("Press Right\n");
|
||||
}
|
||||
if (e->buttons().testFlag(Qt::MiddleButton)) {
|
||||
//bu_log("Press Middle\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-press\", \"button\": %d", button);
|
||||
append_modifiers(&ev_json, e);
|
||||
bu_vls_printf(&ev_json, "}");
|
||||
|
||||
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
|
||||
bu_vls_free(&ev_json);
|
||||
return handled;
|
||||
}
|
||||
|
||||
int CADmouseReleaseEvent(struct bview *v, double x_press, double y_press, int UNUSED(x_prev), int UNUSED(y_prev), QMouseEvent *e, int mode)
|
||||
{
|
||||
QTCAD_EVENT("mouseRelease", 1);
|
||||
|
||||
if (!v)
|
||||
return 0;
|
||||
|
||||
if (!v) return 0;
|
||||
qtcad_init_bindings();
|
||||
restore_drag_bounds_update(v, 1);
|
||||
|
||||
// If we're intending the mouse motion to do the work,
|
||||
// then the release has to be a no-op. If we're going
|
||||
// to do configurable key bindings, this will take some
|
||||
// thought - if we want unmodded left button to be a
|
||||
// rotation, and Ctrl+Left to do something else, these
|
||||
// checks are all going to have to be exact-flag-combo-only
|
||||
// actions.
|
||||
if (e->modifiers()) {
|
||||
return 0;
|
||||
}
|
||||
if (e->buttons().testFlag(Qt::LeftButton) || e->buttons().testFlag(Qt::RightButton)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double cx, cy;
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
cx = (double)e->x();
|
||||
cy = (double)e->y();
|
||||
double cx = e->x();
|
||||
double cy = e->y();
|
||||
#else
|
||||
cx = e->position().x();
|
||||
cy = e->position().y();
|
||||
double cx = e->position().x();
|
||||
double cy = e->position().y();
|
||||
#endif
|
||||
if ((fabs(cx - x_press) > 10) || (fabs(cy - y_press) > 10))
|
||||
return 0;
|
||||
|
||||
int dx = 1;
|
||||
int dy = 1;
|
||||
unsigned long long view_flags = BV_IDLE;
|
||||
if ((fabs(cx - x_press) > 10) || (fabs(cy - y_press) > 10)) return 0;
|
||||
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
//bu_log("Release Left\n");
|
||||
if (mode != BV_CENTER) {
|
||||
view_flags = BV_SCALE;
|
||||
dx = 10;
|
||||
dy = 5;
|
||||
} else {
|
||||
view_flags = BV_CENTER;
|
||||
dx = (int)cx;
|
||||
dy = (int)cy;
|
||||
}
|
||||
}
|
||||
if (e->button() == Qt::RightButton) {
|
||||
//bu_log("Release Right\n");
|
||||
if (mode == BV_CENTER)
|
||||
return 0;
|
||||
view_flags = BV_SCALE;
|
||||
dx = 1;
|
||||
dy = 2;
|
||||
}
|
||||
int button = -1;
|
||||
if (e->button() == Qt::LeftButton) button = 0;
|
||||
else if (e->button() == Qt::MiddleButton) button = 1;
|
||||
else if (e->button() == Qt::RightButton) button = 2;
|
||||
if (button < 0) return 0;
|
||||
|
||||
if (e->button() == Qt::MiddleButton) {
|
||||
//bu_log("Release Center\n");
|
||||
view_flags = BV_CENTER;
|
||||
dx = (int)cx;
|
||||
dy = (int)cy;
|
||||
}
|
||||
|
||||
point_t keypt = VINIT_ZERO;
|
||||
return bv_adjust(v, dx, dy, keypt, 0, view_flags);
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-release\", \"button\": %d, \"cx\": %d, \"cy\": %d, \"mode\": %d", button, (int)cx, (int)cy, mode);
|
||||
append_modifiers(&ev_json, e);
|
||||
bu_vls_printf(&ev_json, "}");
|
||||
|
||||
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
|
||||
bu_vls_free(&ev_json);
|
||||
return handled;
|
||||
}
|
||||
|
||||
int CADmouseMoveEvent(struct bview *v, int x_prev, int y_prev, QMouseEvent *e, int mode)
|
||||
{
|
||||
QTCAD_EVENT("mouseMove", 2);
|
||||
if (!v) return 0;
|
||||
qtcad_init_bindings();
|
||||
|
||||
if (!v)
|
||||
return 0;
|
||||
|
||||
unsigned long long view_flags = BV_IDLE;
|
||||
|
||||
if (x_prev == -INT_MAX) {
|
||||
//x_prev = e->x();
|
||||
//y_prev = e->y();
|
||||
return 0;
|
||||
}
|
||||
|
||||
view_flags = mode;
|
||||
if (mode == BV_CENTER)
|
||||
view_flags = BV_SCALE;
|
||||
|
||||
if (e->buttons().testFlag(Qt::LeftButton)) {
|
||||
//bu_log("Left\n");
|
||||
|
||||
if (e->modifiers().testFlag(Qt::ControlModifier)) {
|
||||
//bu_log("Ctrl+Left\n");
|
||||
view_flags = BV_ROT;
|
||||
}
|
||||
|
||||
if (e->modifiers().testFlag(Qt::ShiftModifier)) {
|
||||
//bu_log("Shift+Left\n");
|
||||
view_flags = BV_TRANS;
|
||||
}
|
||||
|
||||
if (e->modifiers().testFlag(Qt::ShiftModifier) && e->modifiers().testFlag(Qt::ControlModifier)) {
|
||||
//bu_log("Ctrl+Shift+Left\n");
|
||||
view_flags = BV_SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
if (e->buttons().testFlag(Qt::MiddleButton)) {
|
||||
//bu_log("Middle\n");
|
||||
}
|
||||
|
||||
if (e->buttons().testFlag(Qt::RightButton)) {
|
||||
//bu_log("Right\n");
|
||||
}
|
||||
|
||||
if (!e->buttons().testFlag(Qt::LeftButton))
|
||||
return 0;
|
||||
if (x_prev == -INT_MAX) return 0;
|
||||
if (!e->buttons().testFlag(Qt::LeftButton)) return 0;
|
||||
|
||||
long long now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
std::unordered_map<struct bview *, long long>::iterator ts_it = drag_update_ts.find(v);
|
||||
if (ts_it != drag_update_ts.end() && (now_ms - ts_it->second) < drag_update_interval_ms)
|
||||
return -1;
|
||||
if (ts_it != drag_update_ts.end() && (now_ms - ts_it->second) < drag_update_interval_ms) return -1;
|
||||
drag_update_ts[v] = now_ms;
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
@@ -319,53 +334,33 @@ int CADmouseMoveEvent(struct bview *v, int x_prev, int y_prev, QMouseEvent *e, i
|
||||
int dy = e->position().y() - y_prev;
|
||||
#endif
|
||||
|
||||
if (view_flags == BV_SCALE) {
|
||||
// Build in some sensitivity to how much the mouse moved when doing
|
||||
// a motion based scale
|
||||
int mdelta = (abs(dx) > abs(dy)) ? dx : -dy;
|
||||
int f = (int)(2*100*(double)abs(mdelta)/(double)v->gv_height);
|
||||
|
||||
if (mdelta > 0) {
|
||||
dy = 101 + f;
|
||||
dx = 100;
|
||||
} else {
|
||||
dy = 99 - f;
|
||||
dx = 100;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO - the key point and the mode/flags are all hardcoded
|
||||
// right now, but eventually for shift grips they will need to
|
||||
// respond to the various mod keys. The intent is to set flags
|
||||
// based on which mod keys are set to allow bv_adjust to
|
||||
// do the correct math.
|
||||
point_t center;
|
||||
MAT_DELTAS_GET_NEG(center, v->gv_center);
|
||||
|
||||
if (view_flags & (BV_ROT | BV_TRANS | BV_SCALE))
|
||||
suspend_drag_bounds_update(v);
|
||||
|
||||
return bv_adjust(v, dx, dy, center, 0, view_flags);
|
||||
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-move\", \"button\": 0, \"dx\": %d, \"dy\": %d, \"mode\": %d", dx, dy, mode);
|
||||
append_modifiers(&ev_json, e);
|
||||
bu_vls_printf(&ev_json, "}");
|
||||
|
||||
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
|
||||
bu_vls_free(&ev_json);
|
||||
return handled;
|
||||
}
|
||||
|
||||
int CADwheelEvent(struct bview *v, QWheelEvent *e)
|
||||
{
|
||||
QTCAD_EVENT("mouseWheel", 1);
|
||||
|
||||
if (!v)
|
||||
return 0;
|
||||
if (!v) return 0;
|
||||
qtcad_init_bindings();
|
||||
|
||||
QPoint delta = e->angleDelta();
|
||||
int mdelta = -1* delta.y() / 8;
|
||||
|
||||
int dx = 100 + mdelta;
|
||||
int dy = 100;
|
||||
|
||||
point_t origin = VINIT_ZERO;
|
||||
return bv_adjust(v, dx, dy, origin, 0, BV_SCALE);
|
||||
int mdelta = -1 * delta.y() / 8;
|
||||
|
||||
struct bu_vls ev_json = BU_VLS_INIT_ZERO;
|
||||
bu_vls_sprintf(&ev_json, "{\"type\": \"mouse-wheel\", \"delta\": %d", mdelta);
|
||||
append_modifiers(&ev_json, e);
|
||||
bu_vls_printf(&ev_json, "}");
|
||||
|
||||
int handled = bu_binding_process_event("dm", bu_vls_cstr(&ev_json), v);
|
||||
bu_vls_free(&ev_json);
|
||||
return handled;
|
||||
}
|
||||
|
||||
// Local Variables:
|
||||
|
||||
@@ -427,6 +427,7 @@ endif(BRLCAD_ENABLE_SPR)
|
||||
set(LIBRT_PUBLIC_LIBS ${librt_deps} OPENNURBS::OPENNURBS)
|
||||
set(
|
||||
LIBRT_PRIVATE_LIBS
|
||||
Threads::Threads
|
||||
${OPENCL_LIBS}
|
||||
mmesh::mmesh
|
||||
REGEX::REGEX
|
||||
|
||||
+469
-203
@@ -45,24 +45,28 @@
|
||||
* intersecting closed surface), and total_chord is the sum of solid
|
||||
* segment lengths.
|
||||
*
|
||||
* References:
|
||||
* Li et al. (2003), "Using low-discrepancy sequences and the Crofton
|
||||
* formula to compute surface areas of geometric models",
|
||||
* Computer-Aided Design 35, 771-782.
|
||||
*
|
||||
* Liu et al. (2010), "Surface area estimation of digitized 3D objects
|
||||
* using quasi-Monte Carlo methods", Pattern Recognition 43, 3900-3909.
|
||||
* Chord endpoints are independent pseudorandom points on the bounding
|
||||
* sphere. Reinitializing the generator with a fixed seed makes sampling
|
||||
* reproducible without changing the ordinary Monte Carlo convergence
|
||||
* behavior on which the estimator's stability checks rely.
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <string.h>
|
||||
#include <thread>
|
||||
#include <time.h>
|
||||
#include <vector>
|
||||
|
||||
#include "vmath.h"
|
||||
#include "bu/log.h"
|
||||
@@ -100,8 +104,15 @@
|
||||
#define RT_CROFTON_STABILITY_MIN_WINDOWS 2u
|
||||
|
||||
/** Fixed seed for reproducible estimates in tests and cross-platform runs. */
|
||||
#define RT_CROFTON_RNG_SEED 0x9e3779b97f4a7c15ULL
|
||||
#define RT_CROFTON_RNG_SEED UINT64_C(0x9e3779b97f4a7c15)
|
||||
|
||||
/** Two sphere coordinates for each of two chord endpoints. */
|
||||
#define RT_CROFTON_RANDOM_VALUES_PER_RAY 4u
|
||||
|
||||
/** Convert the high 53 random bits exactly to a double in [0, 1). */
|
||||
static constexpr unsigned int CROFTON_RANDOM_SHIFT = 11u;
|
||||
static constexpr double CROFTON_RANDOM_SCALE =
|
||||
1.0 / 9007199254740992.0;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Internal types */
|
||||
@@ -110,6 +121,7 @@
|
||||
struct crofton_ray {
|
||||
point_t r_pt;
|
||||
vect_t r_dir;
|
||||
size_t id;
|
||||
};
|
||||
|
||||
struct crofton_shared {
|
||||
@@ -117,14 +129,19 @@ struct crofton_shared {
|
||||
size_t total_crossings; /* in+out hit events */
|
||||
double total_chord; /* solid segment length sum (mm) */
|
||||
size_t total_rays; /* rays fired (hits + misses) */
|
||||
size_t ray_offset; /* first identifier in this sample stream */
|
||||
point_t *points;
|
||||
size_t point_count;
|
||||
size_t point_capacity;
|
||||
struct rt_crofton_segment *segments;
|
||||
size_t segment_count;
|
||||
size_t segment_capacity;
|
||||
bool collect_points;
|
||||
bool collect_segments;
|
||||
};
|
||||
|
||||
struct crofton_worker_data {
|
||||
struct application *ap; /* per-CPU application struct */
|
||||
struct application ap; /* per-CPU application struct */
|
||||
struct crofton_ray *rays; /* shared ray array (read-only) */
|
||||
size_t start;
|
||||
size_t end;
|
||||
@@ -135,6 +152,10 @@ struct crofton_worker_data {
|
||||
point_t *local_points;
|
||||
size_t local_point_count;
|
||||
size_t local_point_capacity;
|
||||
struct rt_crofton_segment *local_segments;
|
||||
size_t local_segment_count;
|
||||
size_t local_segment_capacity;
|
||||
size_t current_ray;
|
||||
};
|
||||
|
||||
|
||||
@@ -143,46 +164,88 @@ struct crofton_worker_data {
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int
|
||||
crofton_hit(struct application *ap, struct partition *PartHeadp, struct seg *UNUSED(segs))
|
||||
crofton_hit(struct application *ap, struct partition *PartHeadp,
|
||||
struct seg *UNUSED(segs))
|
||||
{
|
||||
struct crofton_worker_data *wd = (struct crofton_worker_data *)ap->a_uptr;
|
||||
|
||||
struct crofton_worker_data *worker =
|
||||
(struct crofton_worker_data *)ap->a_uptr;
|
||||
size_t crossings = 0;
|
||||
double chord = 0.0;
|
||||
double chord = 0.0;
|
||||
|
||||
struct partition *pp;
|
||||
for (pp = PartHeadp->pt_forw; pp != PartHeadp; pp = pp->pt_forw) {
|
||||
/* Each partition contributes an in-hit and an out-hit */
|
||||
crossings += 2;
|
||||
chord += pp->pt_outhit->hit_dist - pp->pt_inhit->hit_dist;
|
||||
if (wd->shared->collect_points) {
|
||||
if (wd->local_point_count + 2 > wd->local_point_capacity) {
|
||||
const size_t initial_capacity = 1024;
|
||||
size_t new_capacity = wd->local_point_capacity ?
|
||||
2 * wd->local_point_capacity : initial_capacity;
|
||||
while (new_capacity < wd->local_point_count + 2)
|
||||
new_capacity *= 2;
|
||||
wd->local_points = (point_t *)bu_realloc(wd->local_points,
|
||||
new_capacity * sizeof(point_t), "Crofton worker hit points");
|
||||
wd->local_point_capacity = new_capacity;
|
||||
}
|
||||
VJOIN1(wd->local_points[wd->local_point_count], ap->a_ray.r_pt,
|
||||
pp->pt_inhit->hit_dist, ap->a_ray.r_dir);
|
||||
wd->local_point_count++;
|
||||
VJOIN1(wd->local_points[wd->local_point_count], ap->a_ray.r_pt,
|
||||
pp->pt_outhit->hit_dist, ap->a_ray.r_dir);
|
||||
wd->local_point_count++;
|
||||
}
|
||||
struct partition *partition;
|
||||
for (partition = PartHeadp->pt_forw; partition != PartHeadp;
|
||||
partition = partition->pt_forw) {
|
||||
fastf_t thickness = partition->pt_outhit->hit_dist -
|
||||
partition->pt_inhit->hit_dist;
|
||||
crossings += 2;
|
||||
chord += thickness;
|
||||
|
||||
if (worker->shared->collect_points) {
|
||||
if (worker->local_point_count + 2 >
|
||||
worker->local_point_capacity) {
|
||||
const size_t initial_capacity = 1024;
|
||||
size_t new_capacity = worker->local_point_capacity ?
|
||||
2 * worker->local_point_capacity : initial_capacity;
|
||||
while (new_capacity < worker->local_point_count + 2)
|
||||
new_capacity *= 2;
|
||||
worker->local_points = (point_t *)bu_realloc(
|
||||
worker->local_points,
|
||||
new_capacity * sizeof(point_t),
|
||||
"Crofton worker hit points");
|
||||
worker->local_point_capacity = new_capacity;
|
||||
}
|
||||
VJOIN1(worker->local_points[worker->local_point_count],
|
||||
ap->a_ray.r_pt, partition->pt_inhit->hit_dist,
|
||||
ap->a_ray.r_dir);
|
||||
worker->local_point_count++;
|
||||
VJOIN1(worker->local_points[worker->local_point_count],
|
||||
ap->a_ray.r_pt, partition->pt_outhit->hit_dist,
|
||||
ap->a_ray.r_dir);
|
||||
worker->local_point_count++;
|
||||
}
|
||||
|
||||
if (worker->shared->collect_segments) {
|
||||
if (worker->local_segment_count + 1 >
|
||||
worker->local_segment_capacity) {
|
||||
const size_t initial_capacity = 512;
|
||||
size_t new_capacity = worker->local_segment_capacity ?
|
||||
2 * worker->local_segment_capacity : initial_capacity;
|
||||
worker->local_segments =
|
||||
(struct rt_crofton_segment *)bu_realloc(
|
||||
worker->local_segments,
|
||||
new_capacity * sizeof(struct rt_crofton_segment),
|
||||
"Crofton worker segments");
|
||||
worker->local_segment_capacity = new_capacity;
|
||||
}
|
||||
|
||||
struct rt_crofton_segment *sample =
|
||||
&worker->local_segments[worker->local_segment_count];
|
||||
struct soltab *in_solid =
|
||||
partition->pt_inseg->seg_stp;
|
||||
struct soltab *out_solid =
|
||||
partition->pt_outseg->seg_stp;
|
||||
VJOIN1(sample->in_point, ap->a_ray.r_pt,
|
||||
partition->pt_inhit->hit_dist, ap->a_ray.r_dir);
|
||||
VJOIN1(sample->out_point, ap->a_ray.r_pt,
|
||||
partition->pt_outhit->hit_dist, ap->a_ray.r_dir);
|
||||
RT_HIT_NORMAL(sample->in_normal, partition->pt_inhit,
|
||||
in_solid, &ap->a_ray, partition->pt_inflip);
|
||||
RT_HIT_NORMAL(sample->out_normal, partition->pt_outhit,
|
||||
out_solid, &ap->a_ray, partition->pt_outflip);
|
||||
sample->thickness = thickness;
|
||||
sample->ray_id = worker->rays[worker->current_ray].id;
|
||||
if (!VNEAR_ZERO(sample->in_normal, VUNITIZE_TOL) &&
|
||||
!VNEAR_ZERO(sample->out_normal, VUNITIZE_TOL))
|
||||
worker->local_segment_count++;
|
||||
}
|
||||
}
|
||||
|
||||
wd->local_crossings += crossings;
|
||||
wd->local_chord += chord;
|
||||
wd->local_rays += 1;
|
||||
|
||||
worker->local_crossings += crossings;
|
||||
worker->local_chord += chord;
|
||||
worker->local_rays++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
crofton_miss(struct application *ap)
|
||||
{
|
||||
@@ -193,159 +256,273 @@ crofton_miss(struct application *ap)
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Parallel worker */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void
|
||||
crofton_worker(int id, void *data)
|
||||
{
|
||||
struct crofton_worker_data *wd = &((struct crofton_worker_data *)data)[id - 1];
|
||||
struct application *ap = wd->ap;
|
||||
struct crofton_ray *rays = wd->rays;
|
||||
|
||||
for (size_t i = wd->start; i < wd->end; i++) {
|
||||
VMOVE(ap->a_ray.r_pt, rays[i].r_pt);
|
||||
VMOVE(ap->a_ray.r_dir, rays[i].r_dir);
|
||||
/* r_dir is already unit-length (set during ray generation) */
|
||||
rt_shootray(ap);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Point / ray generation */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static double
|
||||
crofton_rand01(std::mt19937_64 &rng)
|
||||
crofton_random01(std::mt19937_64 &rng)
|
||||
{
|
||||
return std::generate_canonical<double, 53>(rng);
|
||||
return static_cast<double>(rng() >> CROFTON_RANDOM_SHIFT) *
|
||||
CROFTON_RANDOM_SCALE;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
random_point_on_sphere(double radius, const point_t center, point_t out, std::mt19937_64 &rng)
|
||||
random_point_on_sphere(double radius, const point_t center, double u,
|
||||
double v, point_t point)
|
||||
{
|
||||
double theta = 2.0 * M_PI * crofton_rand01(rng);
|
||||
double phi = acos(2.0 * crofton_rand01(rng) - 1.0);
|
||||
double sp = sin(phi);
|
||||
out[X] = center[X] + radius * sp * cos(theta);
|
||||
out[Y] = center[Y] + radius * sp * sin(theta);
|
||||
out[Z] = center[Z] + radius * cos(phi);
|
||||
double z = 1.0 - 2.0 * u;
|
||||
double radial = sqrt(std::max(0.0, 1.0 - z * z));
|
||||
double angle = 2.0 * M_PI * v;
|
||||
point[X] = center[X] + radius * radial * cos(angle);
|
||||
point[Y] = center[Y] + radius * radial * sin(angle);
|
||||
point[Z] = center[Z] + radius * z;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate @p nrays chord rays from 2*nrays random points on the
|
||||
* bounding sphere. Each ray connects one randomly chosen point to
|
||||
* another.
|
||||
* Generate independent random chord endpoints. Pairing consecutive IID
|
||||
* sphere points has the same distribution as shuffling a point array before
|
||||
* pairing, but consumes exactly four random values per ray. That fixed
|
||||
* stride lets callers continue the deterministic stream with ray_offset.
|
||||
*/
|
||||
static void
|
||||
generate_rays(struct crofton_ray *rays, size_t nrays,
|
||||
double radius, const point_t center, std::mt19937_64 &rng)
|
||||
generate_rays(struct crofton_ray *rays, size_t ray_count, size_t first_id,
|
||||
double radius, const point_t center, std::mt19937_64 &rng)
|
||||
{
|
||||
size_t npts = nrays * 2;
|
||||
|
||||
point_t *pts = (point_t *)bu_calloc(npts, sizeof(point_t), "crofton pts");
|
||||
for (size_t i = 0; i < npts; i++)
|
||||
random_point_on_sphere(radius, center, pts[i], rng);
|
||||
|
||||
/* Shuffle so pairing is random */
|
||||
for (size_t i = npts - 1; i > 0; i--) {
|
||||
size_t j = (size_t)(crofton_rand01(rng) * (i + 1));
|
||||
if (j > i) j = i;
|
||||
point_t tmp;
|
||||
VMOVE(tmp, pts[i]);
|
||||
VMOVE(pts[i], pts[j]);
|
||||
VMOVE(pts[j], tmp);
|
||||
for (size_t i = 0; i < ray_count; i++) {
|
||||
point_t endpoint;
|
||||
random_point_on_sphere(radius, center, crofton_random01(rng),
|
||||
crofton_random01(rng), rays[i].r_pt);
|
||||
random_point_on_sphere(radius, center, crofton_random01(rng),
|
||||
crofton_random01(rng), endpoint);
|
||||
VSUB2(rays[i].r_dir, endpoint, rays[i].r_pt);
|
||||
VUNITIZE(rays[i].r_dir);
|
||||
rays[i].id = first_id + i;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < nrays; i++) {
|
||||
VMOVE(rays[i].r_pt, pts[i * 2]);
|
||||
VSUB2(rays[i].r_dir, pts[i * 2 + 1], pts[i * 2]);
|
||||
VUNITIZE(rays[i].r_dir);
|
||||
}
|
||||
|
||||
bu_free(pts, "crofton pts");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Reusable parallel workers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
class CroftonWorkerPool
|
||||
{
|
||||
public:
|
||||
CroftonWorkerPool(const struct application *ap_template,
|
||||
struct resource *resources, size_t worker_count) :
|
||||
count(worker_count), data(worker_count)
|
||||
{
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
data[i].ap = *ap_template;
|
||||
data[i].ap.a_resource = &resources[i];
|
||||
data[i].shared = NULL;
|
||||
data[i].ap.a_uptr = &data[i];
|
||||
}
|
||||
|
||||
try {
|
||||
executor = std::thread(&CroftonWorkerPool::run_workers, this);
|
||||
} catch (...) {
|
||||
free_worker_points();
|
||||
throw;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
if (!workers_started.wait_for(lock, worker_start_timeout,
|
||||
[this]() { return registered == count; })) {
|
||||
stopping = true;
|
||||
work_ready.notify_all();
|
||||
lock.unlock();
|
||||
executor.join();
|
||||
free_worker_points();
|
||||
throw std::runtime_error("worker startup timed out");
|
||||
}
|
||||
}
|
||||
|
||||
~CroftonWorkerPool()
|
||||
{
|
||||
stop_and_join();
|
||||
free_worker_points();
|
||||
}
|
||||
|
||||
CroftonWorkerPool(const CroftonWorkerPool &) = delete;
|
||||
CroftonWorkerPool &operator=(const CroftonWorkerPool &) = delete;
|
||||
|
||||
void run(struct crofton_ray *rays, size_t nrays,
|
||||
struct crofton_shared *shared)
|
||||
{
|
||||
const size_t rays_per_worker = nrays / count;
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
struct crofton_worker_data &wd = data[i];
|
||||
wd.rays = rays;
|
||||
wd.start = i * rays_per_worker;
|
||||
wd.end = (i == count - 1) ? nrays : (i + 1) * rays_per_worker;
|
||||
wd.shared = shared;
|
||||
wd.local_crossings = 0;
|
||||
wd.local_chord = 0.0;
|
||||
wd.local_rays = 0;
|
||||
wd.local_point_count = 0;
|
||||
wd.local_segment_count = 0;
|
||||
}
|
||||
completed = 0;
|
||||
generation++;
|
||||
work_ready.notify_all();
|
||||
work_done.wait(lock, [this]() { return completed == count; });
|
||||
}
|
||||
|
||||
const std::vector<struct crofton_worker_data> &worker_data() const
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
private:
|
||||
/* Native thread creation should complete promptly. A bounded startup wait
|
||||
* turns partial bu_parallel startup into a reported failure, not a hang. */
|
||||
static constexpr std::chrono::seconds worker_start_timeout{5};
|
||||
|
||||
static void worker_entry(int UNUSED(id), void *context)
|
||||
{
|
||||
CroftonWorkerPool *pool = static_cast<CroftonWorkerPool *>(context);
|
||||
size_t slot = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pool->mutex);
|
||||
slot = pool->registered++;
|
||||
if (pool->registered == pool->count)
|
||||
pool->workers_started.notify_one();
|
||||
}
|
||||
pool->worker(slot);
|
||||
}
|
||||
|
||||
void run_workers()
|
||||
{
|
||||
bu_parallel(worker_entry, count, this);
|
||||
}
|
||||
|
||||
void worker(size_t slot)
|
||||
{
|
||||
size_t observed_generation = 0;
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
while (true) {
|
||||
work_ready.wait(lock, [this, observed_generation]() {
|
||||
return stopping || generation != observed_generation;
|
||||
});
|
||||
if (stopping)
|
||||
return;
|
||||
|
||||
observed_generation = generation;
|
||||
struct crofton_worker_data *wd = &data[slot];
|
||||
lock.unlock();
|
||||
for (size_t i = wd->start; i < wd->end; i++) {
|
||||
wd->current_ray = i;
|
||||
VMOVE(wd->ap.a_ray.r_pt, wd->rays[i].r_pt);
|
||||
VMOVE(wd->ap.a_ray.r_dir, wd->rays[i].r_dir);
|
||||
rt_shootray(&wd->ap);
|
||||
}
|
||||
lock.lock();
|
||||
completed++;
|
||||
if (completed == count)
|
||||
work_done.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
void stop_and_join()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
stopping = true;
|
||||
work_ready.notify_all();
|
||||
}
|
||||
if (executor.joinable())
|
||||
executor.join();
|
||||
}
|
||||
|
||||
void free_worker_points()
|
||||
{
|
||||
for (struct crofton_worker_data &wd : data) {
|
||||
if (wd.local_points)
|
||||
bu_free(wd.local_points, "Crofton worker hit points");
|
||||
if (wd.local_segments)
|
||||
bu_free(wd.local_segments, "Crofton worker segments");
|
||||
wd.local_points = NULL;
|
||||
wd.local_segments = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
size_t count;
|
||||
std::vector<struct crofton_worker_data> data;
|
||||
std::thread executor;
|
||||
std::mutex mutex;
|
||||
std::condition_variable workers_started;
|
||||
std::condition_variable work_ready;
|
||||
std::condition_variable work_done;
|
||||
size_t registered = 0;
|
||||
size_t generation = 0;
|
||||
size_t completed = 0;
|
||||
bool stopping = false;
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* One iteration: fire nrays, accumulate into shared */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void
|
||||
do_one_iteration(struct application *ap_template,
|
||||
struct resource *resources,
|
||||
size_t nrays,
|
||||
double radius,
|
||||
const point_t center,
|
||||
struct crofton_shared *shared,
|
||||
std::mt19937_64 &rng)
|
||||
do_one_iteration(CroftonWorkerPool &pool, size_t ray_count, double radius,
|
||||
const point_t center, struct crofton_shared *shared,
|
||||
std::mt19937_64 &rng)
|
||||
{
|
||||
struct crofton_ray *rays = (struct crofton_ray *)bu_calloc(
|
||||
nrays, sizeof(struct crofton_ray), "crofton rays");
|
||||
ray_count, sizeof(struct crofton_ray), "Crofton rays");
|
||||
generate_rays(rays, ray_count, shared->ray_offset + shared->total_rays,
|
||||
radius, center, rng);
|
||||
pool.run(rays, ray_count, shared);
|
||||
|
||||
generate_rays(rays, nrays, radius, center, rng);
|
||||
for (const struct crofton_worker_data &worker : pool.worker_data()) {
|
||||
shared->total_crossings += worker.local_crossings;
|
||||
shared->total_chord += worker.local_chord;
|
||||
shared->total_rays += worker.local_rays;
|
||||
|
||||
size_t ncpus = bu_avail_cpus();
|
||||
if (ncpus < 1) ncpus = 1;
|
||||
if (ncpus > MAX_PSW) ncpus = MAX_PSW;
|
||||
if (worker.local_point_count) {
|
||||
const size_t needed =
|
||||
shared->point_count + worker.local_point_count;
|
||||
if (needed > shared->point_capacity) {
|
||||
size_t new_capacity = shared->point_capacity ?
|
||||
2 * shared->point_capacity : needed;
|
||||
if (new_capacity < needed)
|
||||
new_capacity = needed;
|
||||
shared->points = (point_t *)bu_realloc(shared->points,
|
||||
new_capacity * sizeof(point_t),
|
||||
"Crofton hit points");
|
||||
shared->point_capacity = new_capacity;
|
||||
}
|
||||
memcpy(&shared->points[shared->point_count],
|
||||
worker.local_points,
|
||||
worker.local_point_count * sizeof(point_t));
|
||||
shared->point_count = needed;
|
||||
}
|
||||
|
||||
struct crofton_worker_data *wdata = (struct crofton_worker_data *)bu_calloc(
|
||||
ncpus, sizeof(struct crofton_worker_data), "crofton wdata");
|
||||
|
||||
size_t per_cpu = nrays / ncpus;
|
||||
|
||||
for (size_t i = 0; i < ncpus; i++) {
|
||||
struct application *a = (struct application *)bu_calloc(
|
||||
1, sizeof(struct application), "crofton app");
|
||||
*a = *ap_template; /* struct copy */
|
||||
a->a_resource = &resources[i];
|
||||
|
||||
struct crofton_worker_data *wd = &wdata[i];
|
||||
wd->ap = a;
|
||||
wd->rays = rays;
|
||||
wd->start = i * per_cpu;
|
||||
wd->end = (i == ncpus - 1) ? nrays : (i + 1) * per_cpu;
|
||||
wd->shared = shared;
|
||||
a->a_uptr = wd;
|
||||
if (worker.local_segment_count) {
|
||||
const size_t needed =
|
||||
shared->segment_count + worker.local_segment_count;
|
||||
if (needed > shared->segment_capacity) {
|
||||
size_t new_capacity = shared->segment_capacity ?
|
||||
2 * shared->segment_capacity : needed;
|
||||
if (new_capacity < needed)
|
||||
new_capacity = needed;
|
||||
shared->segments =
|
||||
(struct rt_crofton_segment *)bu_realloc(
|
||||
shared->segments,
|
||||
new_capacity * sizeof(struct rt_crofton_segment),
|
||||
"Crofton segments");
|
||||
shared->segment_capacity = new_capacity;
|
||||
}
|
||||
memcpy(&shared->segments[shared->segment_count],
|
||||
worker.local_segments,
|
||||
worker.local_segment_count *
|
||||
sizeof(struct rt_crofton_segment));
|
||||
shared->segment_count = needed;
|
||||
}
|
||||
}
|
||||
|
||||
bu_parallel(crofton_worker, (int)ncpus, (void *)wdata);
|
||||
|
||||
for (size_t i = 0; i < ncpus; i++) {
|
||||
shared->total_crossings += wdata[i].local_crossings;
|
||||
shared->total_chord += wdata[i].local_chord;
|
||||
shared->total_rays += wdata[i].local_rays;
|
||||
if (wdata[i].local_point_count) {
|
||||
const size_t needed = shared->point_count + wdata[i].local_point_count;
|
||||
if (needed > shared->point_capacity) {
|
||||
size_t new_capacity = shared->point_capacity ?
|
||||
2 * shared->point_capacity : needed;
|
||||
if (new_capacity < needed)
|
||||
new_capacity = needed;
|
||||
shared->points = (point_t *)bu_realloc(shared->points,
|
||||
new_capacity * sizeof(point_t), "Crofton hit points");
|
||||
shared->point_capacity = new_capacity;
|
||||
}
|
||||
memcpy(&shared->points[shared->point_count], wdata[i].local_points,
|
||||
wdata[i].local_point_count * sizeof(point_t));
|
||||
shared->point_count = needed;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ncpus; i++) {
|
||||
if (wdata[i].local_points)
|
||||
bu_free(wdata[i].local_points, "Crofton worker hit points");
|
||||
bu_free(wdata[i].ap, "crofton app");
|
||||
}
|
||||
bu_free(wdata, "crofton wdata");
|
||||
bu_free(rays, "crofton rays");
|
||||
bu_free(rays, "Crofton rays");
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Public API: rt_crofton_shoot */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -375,7 +552,8 @@ do_one_iteration(struct application *ap_template,
|
||||
* sampling (>= 0) on success; -1 on bad arguments.
|
||||
*/
|
||||
static int
|
||||
crofton_shoot_impl(point_t **out_points,
|
||||
crofton_shoot_impl(struct rt_crofton_result *out_result,
|
||||
point_t **out_points,
|
||||
size_t *out_point_count,
|
||||
double *out_surf_area,
|
||||
double *out_volume,
|
||||
@@ -384,13 +562,22 @@ crofton_shoot_impl(point_t **out_points,
|
||||
point_t out_obb[8],
|
||||
struct rt_i *rtip,
|
||||
const struct rt_crofton_params *params,
|
||||
size_t ray_offset,
|
||||
const fastf_t *bbox_min,
|
||||
const fastf_t *bbox_max)
|
||||
{
|
||||
if (!rtip || (!out_surf_area && !out_volume && !out_aabb_min && !out_obb && !out_points) ||
|
||||
((out_points == NULL) != (out_point_count == NULL)) ||
|
||||
((out_aabb_min == NULL) != (out_aabb_max == NULL)))
|
||||
return -1;
|
||||
if (!rtip ||
|
||||
(!out_result && !out_surf_area && !out_volume && !out_aabb_min &&
|
||||
!out_obb && !out_points) ||
|
||||
((out_points == NULL) != (out_point_count == NULL)) ||
|
||||
((out_aabb_min == NULL) != (out_aabb_max == NULL)) ||
|
||||
(out_result && out_result->segments) ||
|
||||
ray_offset > UINT64_MAX / RT_CROFTON_RANDOM_VALUES_PER_RAY)
|
||||
return -1;
|
||||
if (out_result) {
|
||||
struct rt_crofton_result empty = RT_CROFTON_RESULT_INIT;
|
||||
*out_result = empty;
|
||||
}
|
||||
if (out_points) {
|
||||
*out_points = NULL;
|
||||
*out_point_count = 0;
|
||||
@@ -473,10 +660,14 @@ crofton_shoot_impl(point_t **out_points,
|
||||
if (!use_default && max_rays > 0 && stability_mm <= 0.0 && time_ms <= 0.0)
|
||||
batch = max_rays; /* single-iteration mode */
|
||||
|
||||
size_t ncpus = bu_avail_cpus();
|
||||
if (ncpus < 1) ncpus = 1;
|
||||
if (ncpus > MAX_PSW) ncpus = MAX_PSW;
|
||||
|
||||
/* ---- Initialize per-CPU resources ---- */
|
||||
struct resource *resources = (struct resource *)bu_calloc(
|
||||
MAX_PSW, sizeof(struct resource), "crofton resources");
|
||||
for (int i = 0; i < MAX_PSW; i++)
|
||||
ncpus, sizeof(struct resource), "crofton resources");
|
||||
for (size_t i = 0; i < ncpus; i++)
|
||||
rt_init_resource(&resources[i], i, rtip);
|
||||
|
||||
/* ---- Set up application template ---- */
|
||||
@@ -494,10 +685,32 @@ crofton_shoot_impl(point_t **out_points,
|
||||
/* ---- Shared accumulator ---- */
|
||||
struct crofton_shared shared;
|
||||
memset(&shared, 0, sizeof(shared));
|
||||
shared.collect_points = out_points != NULL || out_aabb_min != NULL || out_obb != NULL;
|
||||
const bool stabilize_surface = out_surf_area != NULL || shared.collect_points;
|
||||
shared.ray_offset = ray_offset;
|
||||
shared.collect_points =
|
||||
out_points != NULL || out_aabb_min != NULL || out_obb != NULL;
|
||||
shared.collect_segments = out_result != NULL;
|
||||
const bool stabilize_surface =
|
||||
out_surf_area != NULL || out_result != NULL ||
|
||||
shared.collect_points;
|
||||
|
||||
CroftonWorkerPool *pool = NULL;
|
||||
try {
|
||||
pool = new CroftonWorkerPool(&ap, resources, ncpus);
|
||||
} catch (const std::exception &e) {
|
||||
bu_log("rt_crofton: unable to start worker pool: %s\n", e.what());
|
||||
for (size_t i = 0; i < ncpus; i++) {
|
||||
if (resources[i].re_magic == RESOURCE_MAGIC) {
|
||||
rt_clean_resource_basic(rtip, &resources[i]);
|
||||
BU_PTBL_SET(&rtip->rti_resources, i, NULL);
|
||||
}
|
||||
}
|
||||
bu_free(resources, "crofton resources");
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::mt19937_64 rng(RT_CROFTON_RNG_SEED);
|
||||
rng.discard(static_cast<uint64_t>(ray_offset) *
|
||||
RT_CROFTON_RANDOM_VALUES_PER_RAY);
|
||||
|
||||
const double FOUR_PI = 4.0 * M_PI;
|
||||
const double PI = M_PI;
|
||||
@@ -521,7 +734,7 @@ crofton_shoot_impl(point_t **out_points,
|
||||
curr_rays = batch;
|
||||
}
|
||||
|
||||
do_one_iteration(&ap, resources, curr_rays, R, center, &shared, rng);
|
||||
do_one_iteration(*pool, curr_rays, R, center, &shared, rng);
|
||||
iteration++;
|
||||
|
||||
if (shared.total_rays == 0) break;
|
||||
@@ -562,9 +775,12 @@ crofton_shoot_impl(point_t **out_points,
|
||||
} else {
|
||||
/* ---- Parametric loop: n_rays / stability_mm / time_ms ---- */
|
||||
double prev_r_sa = -1.0, prev_r_v = -1.0;
|
||||
double prev_sa = -1.0, prev_v = -1.0;
|
||||
size_t total_fired = 0;
|
||||
size_t stable_windows = 0;
|
||||
int64_t t0 = (time_ms > 0.0) ? bu_gettime() : 0;
|
||||
const bool use_relative_stability = out_result && time_ms > 0.0 &&
|
||||
stability_mm <= 0.0;
|
||||
|
||||
for (;;) {
|
||||
/* Time-budget check before firing */
|
||||
@@ -581,7 +797,7 @@ crofton_shoot_impl(point_t **out_points,
|
||||
if (fire > remaining) fire = remaining;
|
||||
}
|
||||
|
||||
do_one_iteration(&ap, resources, fire, R, center, &shared, rng);
|
||||
do_one_iteration(*pool, fire, R, center, &shared, rng);
|
||||
total_fired += fire;
|
||||
|
||||
if (shared.total_rays == 0) break;
|
||||
@@ -594,27 +810,39 @@ crofton_shoot_impl(point_t **out_points,
|
||||
/ (double)shared.total_rays;
|
||||
|
||||
/* Stability check */
|
||||
if (stability_mm > 0.0 &&
|
||||
if ((stability_mm > 0.0 || use_relative_stability) &&
|
||||
shared.total_rays >= RT_CROFTON_STABILITY_MIN_RAYS &&
|
||||
shared.total_crossings >= RT_CROFTON_STABILITY_MIN_CROSSINGS) {
|
||||
double r_sa = (curr_est_sa > 0.0) ? sqrt(curr_est_sa * INV_4PI) : 0.0;
|
||||
double r_v = (curr_est_v > 0.0) ? cbrt(curr_est_v * INV_4PI3) : 0.0;
|
||||
|
||||
if (prev_r_sa >= 0.0) {
|
||||
int sa_ok = (!stabilize_surface) ||
|
||||
fabs(r_sa - prev_r_sa) < stability_mm;
|
||||
int v_ok = (!out_volume) ||
|
||||
fabs(r_v - prev_r_v) < stability_mm;
|
||||
if (sa_ok && v_ok) {
|
||||
stable_windows++;
|
||||
if (stable_windows >= RT_CROFTON_STABILITY_MIN_WINDOWS)
|
||||
break;
|
||||
} else {
|
||||
stable_windows = 0;
|
||||
}
|
||||
bool sa_ok = false;
|
||||
bool v_ok = false;
|
||||
if (use_relative_stability) {
|
||||
sa_ok = !stabilize_surface || (prev_sa > 0.0 &&
|
||||
fabs(curr_est_sa - prev_sa) / prev_sa * 100.0 <=
|
||||
RT_CROFTON_DEFAULT_THRESHOLD);
|
||||
v_ok = !out_volume || (prev_v > 0.0 &&
|
||||
fabs(curr_est_v - prev_v) / prev_v * 100.0 <=
|
||||
RT_CROFTON_DEFAULT_THRESHOLD);
|
||||
prev_sa = curr_est_sa;
|
||||
prev_v = curr_est_v;
|
||||
} else {
|
||||
double r_sa = (curr_est_sa > 0.0) ?
|
||||
sqrt(curr_est_sa * INV_4PI) : 0.0;
|
||||
double r_v = (curr_est_v > 0.0) ?
|
||||
cbrt(curr_est_v * INV_4PI3) : 0.0;
|
||||
sa_ok = !stabilize_surface || (prev_r_sa >= 0.0 &&
|
||||
fabs(r_sa - prev_r_sa) < stability_mm);
|
||||
v_ok = !out_volume || (prev_r_v >= 0.0 &&
|
||||
fabs(r_v - prev_r_v) < stability_mm);
|
||||
prev_r_sa = r_sa;
|
||||
prev_r_v = r_v;
|
||||
}
|
||||
if (sa_ok && v_ok) {
|
||||
stable_windows++;
|
||||
if (stable_windows >= RT_CROFTON_STABILITY_MIN_WINDOWS)
|
||||
break;
|
||||
} else {
|
||||
stable_windows = 0;
|
||||
}
|
||||
prev_r_sa = r_sa;
|
||||
prev_r_v = r_v;
|
||||
}
|
||||
|
||||
/* Time-budget check after firing */
|
||||
@@ -624,8 +852,16 @@ crofton_shoot_impl(point_t **out_points,
|
||||
}
|
||||
}
|
||||
|
||||
if (out_surf_area) *out_surf_area = curr_est_sa;
|
||||
if (out_volume) *out_volume = curr_est_v;
|
||||
if (out_surf_area)
|
||||
*out_surf_area = curr_est_sa;
|
||||
if (out_volume)
|
||||
*out_volume = curr_est_v;
|
||||
if (out_result) {
|
||||
out_result->surface_area = curr_est_sa;
|
||||
out_result->volume = curr_est_v;
|
||||
out_result->ray_count = shared.total_rays;
|
||||
out_result->crossing_count = shared.total_crossings;
|
||||
}
|
||||
if (out_aabb_min) {
|
||||
for (size_t i = 0; i < shared.point_count; ++i)
|
||||
VMINMAX(*out_aabb_min, *out_aabb_max, shared.points[i]);
|
||||
@@ -641,11 +877,19 @@ crofton_shoot_impl(point_t **out_points,
|
||||
bounds_ret = -1;
|
||||
}
|
||||
if (out_points && !bounds_ret) {
|
||||
*out_points = shared.points;
|
||||
*out_point_count = shared.point_count;
|
||||
*out_points = shared.points;
|
||||
*out_point_count = shared.point_count;
|
||||
} else if (shared.points) {
|
||||
bu_free(shared.points, "Crofton hit points");
|
||||
bu_free(shared.points, "Crofton hit points");
|
||||
}
|
||||
if (out_result && !bounds_ret) {
|
||||
out_result->segments = shared.segments;
|
||||
out_result->segment_count = shared.segment_count;
|
||||
} else if (shared.segments) {
|
||||
bu_free(shared.segments, "Crofton segments");
|
||||
}
|
||||
|
||||
delete pool;
|
||||
|
||||
/* Clean each resource and NULL out its slot in rtip->rti_resources.
|
||||
* This is necessary because crofton_from_ip calls rt_i_destroy(rtip)
|
||||
@@ -657,7 +901,7 @@ crofton_shoot_impl(point_t **out_points,
|
||||
*
|
||||
* By setting the slot to NULL we let rt_i_destroy's cleanup skip it,
|
||||
* and then we can safely bu_free the resources array. */
|
||||
for (int i = 0; i < MAX_PSW; i++) {
|
||||
for (size_t i = 0; i < ncpus; i++) {
|
||||
if (resources[i].re_magic == RESOURCE_MAGIC) {
|
||||
rt_clean_resource_basic(rtip, &resources[i]);
|
||||
BU_PTBL_SET(&rtip->rti_resources, i, NULL);
|
||||
@@ -681,9 +925,31 @@ rt_crofton_shoot(double *out_surf_area, double *out_volume,
|
||||
const struct rt_crofton_params *params,
|
||||
const fastf_t *bbox_min, const fastf_t *bbox_max)
|
||||
{
|
||||
return crofton_shoot_impl(out_points, out_point_count, out_surf_area,
|
||||
out_volume, out_aabb_min, out_aabb_max, out_obb, rtip, params,
|
||||
bbox_min, bbox_max);
|
||||
return crofton_shoot_impl(NULL, out_points, out_point_count,
|
||||
out_surf_area, out_volume, out_aabb_min, out_aabb_max, out_obb,
|
||||
rtip, params, 0, bbox_min, bbox_max);
|
||||
}
|
||||
|
||||
int
|
||||
rt_crofton_collect(struct rt_crofton_result *result, struct rt_i *rtip,
|
||||
const struct rt_crofton_params *params, size_t ray_offset,
|
||||
const fastf_t *bbox_min, const fastf_t *bbox_max)
|
||||
{
|
||||
if (!result)
|
||||
return -1;
|
||||
return crofton_shoot_impl(result, NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
NULL, rtip, params, ray_offset, bbox_min, bbox_max);
|
||||
}
|
||||
|
||||
void
|
||||
rt_crofton_result_free(struct rt_crofton_result *result)
|
||||
{
|
||||
if (!result)
|
||||
return;
|
||||
if (result->segments)
|
||||
bu_free(result->segments, "Crofton segments");
|
||||
struct rt_crofton_result empty = RT_CROFTON_RESULT_INIT;
|
||||
*result = empty;
|
||||
}
|
||||
|
||||
|
||||
@@ -821,7 +1087,7 @@ crofton_from_ip_n(const struct rt_db_internal *ip,
|
||||
double sa = 0.0;
|
||||
double vol = 0.0;
|
||||
(void)rt_crofton_shoot(&sa, &vol, NULL, NULL, NULL, NULL, NULL,
|
||||
rtip, params, NULL, NULL);
|
||||
rtip, params, NULL, NULL);
|
||||
|
||||
if (out_sa) *out_sa = sa;
|
||||
if (out_vol) *out_vol = vol;
|
||||
|
||||
@@ -1543,8 +1543,7 @@ permute_cell(point_t A,
|
||||
struct dsp_specific *dsp,
|
||||
struct dsp_rpp *dsp_rpp)
|
||||
{
|
||||
int x, y;
|
||||
|
||||
point_t tmp;
|
||||
|
||||
#ifdef FULL_DSP_DEBUGGING
|
||||
if (RT_G_DEBUG & RT_DEBUG_HF) {
|
||||
@@ -1558,10 +1557,8 @@ permute_cell(point_t A,
|
||||
switch (dsp->dsp_i.dsp_cuttype) {
|
||||
case DSP_CUT_DIR_llUR:
|
||||
return DSP_CUT_DIR_llUR;
|
||||
break;
|
||||
|
||||
case DSP_CUT_DIR_ADAPT: {
|
||||
point_t tmp;
|
||||
size_t cx = (size_t)dsp_rpp->dsp_min[X];
|
||||
size_t cy = (size_t)dsp_rpp->dsp_min[Y];
|
||||
|
||||
@@ -1570,28 +1567,8 @@ permute_cell(point_t A,
|
||||
dsp_rpp->dsp_min[X],
|
||||
dsp_rpp->dsp_min[Y]);
|
||||
|
||||
/*
|
||||
* We look at the points in the diagonal next cells to
|
||||
* determine the curvature along each diagonal of this
|
||||
* cell. This cell is divided into two triangles by
|
||||
* cutting across the cell in the direction of least
|
||||
* curvature.
|
||||
*
|
||||
* * * * *
|
||||
* \ /
|
||||
* \C D/
|
||||
* * *--* *
|
||||
* |\/|
|
||||
* |/\|
|
||||
* * *--* *
|
||||
* /A B\
|
||||
* / \
|
||||
* * * * *
|
||||
*/
|
||||
|
||||
/* leaf-cell precondition: the rpp spans exactly one grid unit, so
|
||||
* (cx, cy) plus the cell counts fully describe the curvature
|
||||
* stencil that rt_dsp_cell_cut() recomputes.
|
||||
/* The rpp spans one grid cell; use its indices to select the
|
||||
* lower-curvature diagonal.
|
||||
*/
|
||||
BU_ASSERT(dsp_rpp->dsp_max[X] == dsp_rpp->dsp_min[X] + 1);
|
||||
BU_ASSERT(dsp_rpp->dsp_max[Y] == dsp_rpp->dsp_min[Y] + 1);
|
||||
@@ -1599,15 +1576,16 @@ permute_cell(point_t A,
|
||||
if (rt_dsp_cell_cut(&dsp->dsp_i, cx, cy,
|
||||
(size_t)dsp->xsiz, (size_t)dsp->ysiz)
|
||||
== DSP_CUT_DIR_llUR) {
|
||||
/* A-D cut is fine, no need to permute */
|
||||
if (RT_G_DEBUG & RT_DEBUG_HF)
|
||||
bu_log("A-D cut\n");
|
||||
|
||||
return DSP_CUT_DIR_llUR;
|
||||
|
||||
}
|
||||
|
||||
/* prefer the B-C cut */
|
||||
}
|
||||
/* fall through */
|
||||
case DSP_CUT_DIR_ULlr:
|
||||
/* Both B-C modes need the same corner order. The top triangles
|
||||
* must keep outward (+Z) normals for the entry/exit state machine.
|
||||
*/
|
||||
VMOVE(tmp, A);
|
||||
VMOVE(A, B);
|
||||
VMOVE(B, D);
|
||||
@@ -1615,35 +1593,7 @@ permute_cell(point_t A,
|
||||
VMOVE(C, tmp);
|
||||
if (RT_G_DEBUG & RT_DEBUG_HF)
|
||||
bu_log("B-C cut\n");
|
||||
|
||||
return DSP_CUT_DIR_ULlr;
|
||||
|
||||
break;
|
||||
}
|
||||
case DSP_CUT_DIR_ULlr:
|
||||
/* assign the values for the corner points
|
||||
*
|
||||
* D----C
|
||||
* | |
|
||||
* | |
|
||||
* | |
|
||||
* B----A
|
||||
*/
|
||||
x = dsp_rpp->dsp_min[X];
|
||||
y = dsp_rpp->dsp_min[Y];
|
||||
VSET(B, x, y, DSP(&dsp->dsp_i, x, y));
|
||||
|
||||
x = dsp_rpp->dsp_max[X];
|
||||
VSET(A, x, y, DSP(&dsp->dsp_i, x, y));
|
||||
|
||||
y = dsp_rpp->dsp_max[Y];
|
||||
VSET(C, x, y, DSP(&dsp->dsp_i, x, y));
|
||||
|
||||
x = dsp_rpp->dsp_min[X];
|
||||
VSET(D, x, y, DSP(&dsp->dsp_i, x, y));
|
||||
|
||||
return DSP_CUT_DIR_ULlr;
|
||||
break;
|
||||
}
|
||||
bu_log("%s:%d Unknown DSP cut direction: %d\n",
|
||||
__FILE__, __LINE__, dsp->dsp_i.dsp_cuttype);
|
||||
|
||||
@@ -43,6 +43,18 @@ rel_err(double estimated, double exact)
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
crofton_segments_equal(const struct rt_crofton_segment *left,
|
||||
const struct rt_crofton_segment *right)
|
||||
{
|
||||
return left->ray_id == right->ray_id &&
|
||||
NEAR_EQUAL(left->thickness, right->thickness, SMALL_FASTF) &&
|
||||
VNEAR_EQUAL(left->in_point, right->in_point, SMALL_FASTF) &&
|
||||
VNEAR_EQUAL(left->in_normal, right->in_normal, SMALL_FASTF) &&
|
||||
VNEAR_EQUAL(left->out_point, right->out_point, SMALL_FASTF) &&
|
||||
VNEAR_EQUAL(left->out_normal, right->out_normal, SMALL_FASTF);
|
||||
}
|
||||
|
||||
static int
|
||||
verify_crofton_estimates(void)
|
||||
{
|
||||
@@ -379,6 +391,102 @@ run_convergence_case(struct db_i *dbip,
|
||||
&sample_points, &sample_count,
|
||||
rtip, &p, NULL, NULL);
|
||||
run_sec = (double)(bu_gettime() - t0) / 1000000.0;
|
||||
|
||||
if (i == 0) {
|
||||
struct rt_crofton_result samples = RT_CROFTON_RESULT_INIT;
|
||||
const size_t ray_offset = 4096u;
|
||||
const size_t continuation_rays = 256u;
|
||||
struct rt_crofton_params sample_params =
|
||||
{ray_offset + continuation_rays, 0.0, 0.0};
|
||||
int sample_ret = rt_crofton_collect(&samples, rtip,
|
||||
&sample_params, 0, NULL, NULL);
|
||||
int full_samples_valid = sample_ret > 0 && samples.segments &&
|
||||
samples.segment_count * 2 == samples.crossing_count &&
|
||||
samples.ray_count == sample_params.n_rays;
|
||||
if (!full_samples_valid) {
|
||||
printf(" %-24s invalid structured Crofton output\\n", label);
|
||||
failures++;
|
||||
} else {
|
||||
for (size_t sample_index = 0;
|
||||
sample_index < samples.segment_count; sample_index++) {
|
||||
struct rt_crofton_segment *segment =
|
||||
&samples.segments[sample_index];
|
||||
if (segment->thickness <= 0.0 ||
|
||||
!NEAR_EQUAL(DIST_PNT_PNT(segment->in_point,
|
||||
segment->out_point), segment->thickness, RT_LEN_TOL) ||
|
||||
!NEAR_EQUAL(MAGNITUDE(segment->in_normal), 1.0,
|
||||
VUNITIZE_TOL) ||
|
||||
!NEAR_EQUAL(MAGNITUDE(segment->out_normal), 1.0,
|
||||
VUNITIZE_TOL) ||
|
||||
segment->ray_id >= samples.ray_count) {
|
||||
printf(" %-24s invalid structured segment\\n", label);
|
||||
failures++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct rt_crofton_result offset_samples =
|
||||
RT_CROFTON_RESULT_INIT;
|
||||
struct rt_crofton_params offset_params =
|
||||
{continuation_rays, 0.0, 0.0};
|
||||
sample_ret = rt_crofton_collect(&offset_samples, rtip,
|
||||
&offset_params, ray_offset, NULL, NULL);
|
||||
int offset_samples_valid = sample_ret > 0 &&
|
||||
offset_samples.ray_count == offset_params.n_rays;
|
||||
if (!offset_samples_valid) {
|
||||
printf(" %-24s invalid offset Crofton output\\n", label);
|
||||
failures++;
|
||||
} else {
|
||||
for (size_t sample_index = 0;
|
||||
sample_index < offset_samples.segment_count;
|
||||
sample_index++) {
|
||||
size_t ray_id = offset_samples.segments[sample_index].ray_id;
|
||||
if (ray_id < ray_offset ||
|
||||
ray_id >= ray_offset + offset_params.n_rays) {
|
||||
printf(" %-24s invalid offset ray identifier\\n",
|
||||
label);
|
||||
failures++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (full_samples_valid && offset_samples_valid) {
|
||||
size_t suffix_start = 0;
|
||||
while (suffix_start < samples.segment_count &&
|
||||
samples.segments[suffix_start].ray_id < ray_offset)
|
||||
suffix_start++;
|
||||
size_t suffix_count = samples.segment_count - suffix_start;
|
||||
|
||||
if (suffix_count != offset_samples.segment_count) {
|
||||
printf(" %-24s random stream continuation count mismatch\\n",
|
||||
label);
|
||||
failures++;
|
||||
} else {
|
||||
for (size_t sample_index = 0;
|
||||
sample_index < suffix_count; sample_index++) {
|
||||
const struct rt_crofton_segment *full_segment =
|
||||
&samples.segments[suffix_start + sample_index];
|
||||
const struct rt_crofton_segment *offset_segment =
|
||||
&offset_samples.segments[sample_index];
|
||||
if (!crofton_segments_equal(full_segment, offset_segment)) {
|
||||
printf(" %-24s random stream continuation mismatch\\n",
|
||||
label);
|
||||
failures++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rt_crofton_result_free(&samples);
|
||||
if (samples.segments || samples.segment_count)
|
||||
failures++;
|
||||
rt_crofton_result_free(&offset_samples);
|
||||
if (offset_samples.segments || offset_samples.segment_count)
|
||||
failures++;
|
||||
}
|
||||
rt_i_destroy(rtip);
|
||||
|
||||
if (elapsed_total_sec)
|
||||
|
||||
@@ -414,13 +414,20 @@ flat_dsp_analytic(uint32_t xcnt, uint32_t ycnt, unsigned short h,
|
||||
/* Build an in-memory DB, prep, and run comparison */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
enum dsp_ray_check_mode {
|
||||
DSP_RAY_CHECK_NONE,
|
||||
DSP_RAY_CHECK_FLAT,
|
||||
DSP_RAY_CHECK_SMOOTH,
|
||||
DSP_RAY_CHECK_BC_CUT
|
||||
};
|
||||
|
||||
struct dsp_case {
|
||||
const char *label;
|
||||
uint32_t xcnt, ycnt;
|
||||
const unsigned short *buf;
|
||||
int cuttype;
|
||||
int smooth;
|
||||
int ray_checks;
|
||||
enum dsp_ray_check_mode ray_checks;
|
||||
/* stom diagonal (identity: dx=dy=dz=1): */
|
||||
double dx, dy, dz;
|
||||
/* analytic SA/vol (-1 if not available): */
|
||||
@@ -550,7 +557,7 @@ check_smooth_normal(const struct dsp_case *tc, struct soltab *stp)
|
||||
static int
|
||||
run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
|
||||
{
|
||||
if (!tc->ray_checks)
|
||||
if (tc->ray_checks == DSP_RAY_CHECK_NONE)
|
||||
return 0;
|
||||
|
||||
int failures = 0;
|
||||
@@ -562,16 +569,14 @@ run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
|
||||
|
||||
double x_extent = (tc->xcnt - 1) * tc->dx;
|
||||
double y_extent = (tc->ycnt - 1) * tc->dy;
|
||||
/* ray_checks == 1 is reserved for flat DSP cases, so buf[0] is the
|
||||
* expected terrain height for the deterministic vertical/side rays.
|
||||
*/
|
||||
double top_z = (double)tc->buf[0] * tc->dz;
|
||||
double expected_vertical_in = 10.0;
|
||||
double expected_vertical_out = expected_vertical_in + top_z;
|
||||
point_t o;
|
||||
vect_t d;
|
||||
|
||||
if (tc->ray_checks == 1) {
|
||||
if (tc->ray_checks == DSP_RAY_CHECK_FLAT) {
|
||||
/* Flat cases use buf[0] as the terrain height for direct rays. */
|
||||
double top_z = (double)tc->buf[0] * tc->dz;
|
||||
double expected_vertical_in = 10.0;
|
||||
double expected_vertical_out = expected_vertical_in + top_z;
|
||||
VSET(o, 0.5 * x_extent, 0.5 * y_extent, top_z + 10.0);
|
||||
VSET(d, 0.0, 0.0, -1.0);
|
||||
failures += check_direct_ray("vertical down flat DSP", stp, rtip,
|
||||
@@ -598,12 +603,22 @@ run_prepped_ray_checks(const struct dsp_case *tc, struct rt_i *rtip)
|
||||
failures += check_direct_ray("vertical up flat DSP", stp, rtip,
|
||||
o, d, 1, expected_vertical_in,
|
||||
expected_vertical_out, 0);
|
||||
} else if (tc->ray_checks == 2) {
|
||||
} else if (tc->ray_checks == DSP_RAY_CHECK_SMOOTH) {
|
||||
VSET(o, 0.5 * x_extent, 0.5 * y_extent, 10000.0);
|
||||
VSET(d, 0.0, 0.0, -1.0);
|
||||
failures += check_direct_ray("smooth-mode direct vertical shot", stp, rtip,
|
||||
o, d, 1, -1.0, -1.0, 0);
|
||||
failures += check_smooth_normal(tc, stp);
|
||||
} else if (tc->ray_checks == DSP_RAY_CHECK_BC_CUT) {
|
||||
/* In cell (3,3), the B-C triangle rises from 315 at y=3.25 by
|
||||
* 68 per x. A horizontal ray at z=330 enters at x=3+15/68
|
||||
* and exits through the outer wall at x=4. This also checks
|
||||
* that the explicit B-C cut gives the top an outward normal.
|
||||
*/
|
||||
VSET(o, 2.5, 3.25, 330.0);
|
||||
VSET(d, 1.0, 0.0, 0.0);
|
||||
failures += check_direct_ray("explicit B-C top-to-side shot", stp, rtip,
|
||||
o, d, 1, 0.5 + 15.0 / 68.0, 1.5, 1);
|
||||
}
|
||||
|
||||
return failures;
|
||||
@@ -782,7 +797,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"flat 5x5 h=100 (4x4 cells, identity stom)",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 1, 1.0, 1.0, 1.0, asa, avol
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_FLAT, 1.0, 1.0, 1.0, asa, avol
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -798,7 +813,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"flat 10x10 h=200 (9x9 cells)",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, asa, avol
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, asa, avol
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -814,7 +829,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"flat 10x10 h=50 (stom dx=5 dy=5 dz=2)",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 1, 5.0, 5.0, 2.0, asa, avol
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_FLAT, 5.0, 5.0, 2.0, asa, avol
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -829,7 +844,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"ramp 9x9 (h=100+20x+10y)",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -844,7 +859,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"asymmetric 5x5 explicit ULlr cut",
|
||||
GW, GH, buf, DSP_CUT_DIR_ULlr, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_ULlr, 0, DSP_RAY_CHECK_BC_CUT, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -859,7 +874,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"asymmetric 5x5 adaptive cut",
|
||||
GW, GH, buf, DSP_CUT_DIR_ADAPT, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_ADAPT, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -874,7 +889,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"smooth=1 ramp normal check",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 1, 2, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 1, DSP_RAY_CHECK_SMOOTH, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -889,7 +904,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"smooth=2 ramp normal check",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 2, 2, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 2, DSP_RAY_CHECK_SMOOTH, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -908,7 +923,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"sinusoidal 16x16",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
}
|
||||
@@ -928,7 +943,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"complex 33x33 wave",
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
GW, GH, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
bu_free(buf, "33x33 buf");
|
||||
@@ -971,7 +986,7 @@ test_synthetic(void)
|
||||
|
||||
struct dsp_case tc = {
|
||||
"larger 129x129 mixed ramp/wave terrain",
|
||||
grid_width, grid_height, buf, DSP_CUT_DIR_llUR, 0, 0, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
grid_width, grid_height, buf, DSP_CUT_DIR_llUR, 0, DSP_RAY_CHECK_NONE, 1.0, 1.0, 1.0, -1.0, -1.0
|
||||
};
|
||||
failures += run_inmem_case(&tc);
|
||||
bu_free(sin_x, "129x129 sin x");
|
||||
|
||||
+180
-234
@@ -49,6 +49,7 @@
|
||||
#include "bu/path.h"
|
||||
#include "bu/process.h"
|
||||
#include "bu/units.h"
|
||||
#include "bu/binding.h"
|
||||
#include "vmath.h"
|
||||
#include "rt/db4.h"
|
||||
#include "rt/geom.h"
|
||||
@@ -2824,6 +2825,148 @@ to_data_vZ(struct ged *gedp,
|
||||
}
|
||||
|
||||
|
||||
static void tclcad_action_eval_aet(const struct bu_vls *ev, void *reg, void *event_data) {
|
||||
struct bview *gdvp = (struct bview *)event_data;
|
||||
const char *aet_args = (const char *)reg;
|
||||
struct bu_vls cmd = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&cmd, "%s aet %s %s", bu_vls_cstr(¤t_top->to_gedp->go_name), bu_vls_cstr(&gdvp->gv_name), aet_args);
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
|
||||
bu_vls_free(&cmd);
|
||||
}
|
||||
|
||||
static void tclcad_action_mouse_drag(const struct bu_vls *ev, void *reg, void *event_data) {
|
||||
struct bview *gdvp = (struct bview *)event_data;
|
||||
const char *cmd_name = (const char *)reg;
|
||||
int cx = gdvp->gv_prevMouseX;
|
||||
int cy = gdvp->gv_prevMouseY;
|
||||
char *p;
|
||||
if ((p = strstr(bu_vls_cstr(ev), "\"cx\":"))) sscanf(p + 5, "%d", &cx);
|
||||
if ((p = strstr(bu_vls_cstr(ev), "\"cy\":"))) sscanf(p + 5, "%d", &cy);
|
||||
|
||||
struct bu_vls cmd = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&cmd, "%s %s %s %d %d", bu_vls_cstr(¤t_top->to_gedp->go_name), cmd_name, bu_vls_cstr(&gdvp->gv_name), cx, cy);
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
|
||||
bu_vls_free(&cmd);
|
||||
}
|
||||
|
||||
static void tclcad_action_wheel_scale(const struct bu_vls *ev, void *reg, void *event_data) {
|
||||
struct bview *gdvp = (struct bview *)event_data;
|
||||
int delta = 0;
|
||||
char *p;
|
||||
if ((p = strstr(bu_vls_cstr(ev), "\"delta\":"))) sscanf(p + 8, "%d", &delta);
|
||||
const char *sf = (delta > 0) ? "1.1" : "0.9";
|
||||
struct bu_vls cmd = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&cmd, "%s zoom %s %s", bu_vls_cstr(¤t_top->to_gedp->go_name), bu_vls_cstr(&gdvp->gv_name), sf);
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
|
||||
bu_vls_free(&cmd);
|
||||
}
|
||||
|
||||
static void tclcad_action_mouse_click(const struct bu_vls *ev, void *reg, void *event_data) {
|
||||
struct bview *gdvp = (struct bview *)event_data;
|
||||
const char *cmd_format = (const char *)reg;
|
||||
int cx = gdvp->gv_prevMouseX;
|
||||
int cy = gdvp->gv_prevMouseY;
|
||||
char *p;
|
||||
if ((p = strstr(bu_vls_cstr(ev), "\"cx\":"))) sscanf(p + 5, "%d", &cx);
|
||||
if ((p = strstr(bu_vls_cstr(ev), "\"cy\":"))) sscanf(p + 5, "%d", &cy);
|
||||
|
||||
struct bu_vls cmd = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&cmd, cmd_format, bu_vls_cstr(¤t_top->to_gedp->go_name), bu_vls_cstr(&gdvp->gv_name), cx, cy);
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&cmd));
|
||||
bu_vls_free(&cmd);
|
||||
}
|
||||
|
||||
static int bindings_loaded_mged = 0;
|
||||
|
||||
static int tclcad_load_bindings_cmd(ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]) {
|
||||
if (argc < 2) {
|
||||
Tcl_AppendResult(interp, "wrong # args: should be \"", argv[0], " <json_path>\"", NULL);
|
||||
return TCL_ERROR;
|
||||
}
|
||||
|
||||
char *path = (char *)bu_dir(NULL, 0, BU_DIR_DATA, argv[1], NULL);
|
||||
if (!path) {
|
||||
Tcl_AppendResult(interp, "Failed to resolve path: ", argv[1], NULL);
|
||||
return TCL_ERROR;
|
||||
}
|
||||
|
||||
if (bu_binding_load(path) < 0) {
|
||||
bu_free(path, "path");
|
||||
Tcl_AppendResult(interp, "Failed to load bindings from: ", argv[1], NULL);
|
||||
return TCL_ERROR;
|
||||
}
|
||||
bu_free(path, "path");
|
||||
|
||||
/* Validate that application provided required bindings */
|
||||
if (!bu_binding_action_is_bound("dm", "view_mouse_drag_rotate") ||
|
||||
!bu_binding_action_is_bound("dm", "view_mouse_drag_translate") ||
|
||||
!bu_binding_action_is_bound("dm", "view_mouse_drag_scale")) {
|
||||
Tcl_AppendResult(interp, "Validation Failed: The provided bindings do not map inputs for view_mouse_drag_rotate/translate/scale. These are required standard actions.", NULL);
|
||||
return TCL_ERROR;
|
||||
}
|
||||
|
||||
return TCL_OK;
|
||||
}
|
||||
|
||||
static int tclcad_dispatch_event_cmd(ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]) {
|
||||
if (argc < 8) return TCL_OK;
|
||||
struct tclcad_obj *to = (struct tclcad_obj *)clientData;
|
||||
struct bview *gdvp = bv_set_find_view(&to->to_gedp->ged_views, argv[2]);
|
||||
if (!gdvp) return TCL_OK;
|
||||
|
||||
int x = atoi(argv[5]);
|
||||
int y = atoi(argv[6]);
|
||||
int state = atoi(argv[7]);
|
||||
|
||||
struct bu_vls ev = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&ev, "{\"type\": \"%s\"", argv[3]);
|
||||
|
||||
if (!strcmp(argv[3], "key-press")) {
|
||||
char key_str[16] = {0};
|
||||
bu_strlcpy(key_str, argv[4], sizeof(key_str));
|
||||
if (strlen(key_str) == 1 && key_str[0] >= 'a' && key_str[0] <= 'z') {
|
||||
key_str[0] = key_str[0] - 'a' + 'A';
|
||||
}
|
||||
bu_vls_printf(&ev, ", \"key\": \"%s\"", key_str);
|
||||
} else if (!strcmp(argv[3], "mouse-wheel")) {
|
||||
int delta = (argc > 8) ? atoi(argv[8]) : 0;
|
||||
int mdelta = -1 * delta / 120;
|
||||
bu_vls_printf(&ev, ", \"delta\": %d", mdelta);
|
||||
} else {
|
||||
int btn = atoi(argv[4]) - 1;
|
||||
if (!strcmp(argv[3], "mouse-move")) {
|
||||
if (state & 0x100) btn = 0;
|
||||
else if (state & 0x200) btn = 1;
|
||||
else if (state & 0x400) btn = 2;
|
||||
else btn = -1;
|
||||
|
||||
int dx = x - gdvp->gv_prevMouseX;
|
||||
int dy = y - gdvp->gv_prevMouseY;
|
||||
bu_vls_printf(&ev, ", \"dx\": %d, \"dy\": %d, \"mode\": 0", dx, dy);
|
||||
}
|
||||
if (btn >= 0) bu_vls_printf(&ev, ", \"button\": %d", btn);
|
||||
bu_vls_printf(&ev, ", \"cx\": %d, \"cy\": %d", x, y);
|
||||
}
|
||||
|
||||
if (state & (1 | 4 | 8)) {
|
||||
bu_vls_printf(&ev, ", \"modifiers\": [");
|
||||
int added = 0;
|
||||
if (state & 1) { bu_vls_printf(&ev, "\"Shift\""); added=1; }
|
||||
if (state & 4) { if(added) bu_vls_printf(&ev, ", "); bu_vls_printf(&ev, "\"Control\""); added=1; }
|
||||
if (state & 8) { if(added) bu_vls_printf(&ev, ", "); bu_vls_printf(&ev, "\"Alt\""); }
|
||||
bu_vls_printf(&ev, "]");
|
||||
}
|
||||
bu_vls_printf(&ev, "}");
|
||||
|
||||
bu_binding_process_event("dm", bu_vls_cstr(&ev), gdvp);
|
||||
|
||||
gdvp->gv_prevMouseX = x;
|
||||
gdvp->gv_prevMouseY = y;
|
||||
|
||||
bu_vls_free(&ev);
|
||||
return TCL_OK;
|
||||
}
|
||||
|
||||
static void
|
||||
to_init_default_bindings(struct bview *gdvp)
|
||||
{
|
||||
@@ -2832,6 +2975,35 @@ to_init_default_bindings(struct bview *gdvp)
|
||||
if (dm_get_pathname((struct dm *)gdvp->dmp)) {
|
||||
struct bu_vls *pathvls = dm_get_pathname((struct dm *)gdvp->dmp);
|
||||
if (pathvls) {
|
||||
if (!bindings_loaded_mged) {
|
||||
bindings_loaded_mged = 1;
|
||||
|
||||
|
||||
bu_binding_register_action("view_2", tclcad_action_eval_aet, (void*)"35 -25 0");
|
||||
bu_binding_register_action("view_3", tclcad_action_eval_aet, (void*)"35 25 0");
|
||||
bu_binding_register_action("view_4", tclcad_action_eval_aet, (void*)"45 45 0");
|
||||
bu_binding_register_action("view_5", tclcad_action_eval_aet, (void*)"145 25 0");
|
||||
bu_binding_register_action("view_6", tclcad_action_eval_aet, (void*)"215 25 0");
|
||||
bu_binding_register_action("view_7", tclcad_action_eval_aet, (void*)"325 25 0");
|
||||
bu_binding_register_action("view_front", tclcad_action_eval_aet, (void*)"0 0 0");
|
||||
bu_binding_register_action("view_top", tclcad_action_eval_aet, (void*)"270 90 0");
|
||||
bu_binding_register_action("view_bottom", tclcad_action_eval_aet, (void*)"270 -90 0");
|
||||
bu_binding_register_action("view_left", tclcad_action_eval_aet, (void*)"90 0 0");
|
||||
bu_binding_register_action("view_rear", tclcad_action_eval_aet, (void*)"180 0 0");
|
||||
bu_binding_register_action("view_right", tclcad_action_eval_aet, (void*)"270 0 0");
|
||||
|
||||
bu_binding_register_action("view_mouse_drag_rotate", tclcad_action_mouse_drag, (void*)"mouse_rot");
|
||||
bu_binding_register_action("view_mouse_drag_translate", tclcad_action_mouse_drag, (void*)"mouse_trans");
|
||||
bu_binding_register_action("view_mouse_drag_scale", tclcad_action_mouse_drag, (void*)"mouse_scale");
|
||||
bu_binding_register_action("view_mouse_wheel_scale", tclcad_action_wheel_scale, NULL);
|
||||
|
||||
bu_binding_register_action("view_mouse_press_left", tclcad_action_mouse_click, (void*)"%s zoom %s 0.5");
|
||||
bu_binding_register_action("view_mouse_press_right", tclcad_action_mouse_click, (void*)"%s zoom %s 2.0");
|
||||
|
||||
Tcl_CreateCommand(current_top->to_interp, "tclcad_dispatch_event", tclcad_dispatch_event_cmd, (ClientData)current_top, (Tcl_CmdDeleteProc *)NULL);
|
||||
Tcl_CreateCommand(current_top->to_interp, "tclcad_load_bindings", tclcad_load_bindings_cmd, (ClientData)current_top, (Tcl_CmdDeleteProc *)NULL);
|
||||
}
|
||||
|
||||
bu_vls_printf(&bindings, "bind %s <Configure> {%s configure %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
@@ -2848,240 +3020,14 @@ to_init_default_bindings(struct bview *gdvp)
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Mouse Bindings */
|
||||
bu_vls_printf(&bindings, "bind %s <2> {%s vslew %s %%x %%y; focus %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name),
|
||||
bu_vls_addr(pathvls));
|
||||
bu_vls_printf(&bindings, "bind %s <1> {%s zoom %s 0.5; focus %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name),
|
||||
bu_vls_addr(pathvls));
|
||||
bu_vls_printf(&bindings, "bind %s <3> {%s zoom %s 2.0; focus %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name),
|
||||
bu_vls_addr(pathvls));
|
||||
bu_vls_printf(&bindings, "bind %s <4> {%s zoom %s 1.1; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <5> {%s zoom %s 0.9; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <MouseWheel> {if {%%D < 0} {%s zoom %s 0.9} else {%s zoom %s 1.1}; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Idle Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <ButtonRelease> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Control_L> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Control_R> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Shift_L> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Shift_R> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Alt_L> {%s idle_mode %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <KeyRelease-Alt_R> {%s idle_mode %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Rotate Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <Control-ButtonRelease-1> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-ButtonPress-1> {%s rotate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-ButtonPress-2> {%s rotate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-ButtonPress-3> {%s rotate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Translate Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-ButtonRelease-1> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-ButtonPress-1> {%s translate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-ButtonPress-2> {%s translate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-ButtonPress-3> {%s translate_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Scale Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonRelease-1> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonPress-1> {%s scale_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonPress-2> {%s scale_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Shift-ButtonPress-3> {%s scale_mode %s %%x %%y}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Constrained Rotate Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonRelease-1> {%s idle_mode %s}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonPress-1> {%s constrain_rmode %s x %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonPress-2> {%s constrain_rmode %s y %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Control-Lock-ButtonPress-3> {%s constrain_rmode %s z %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Constrained Translate Mode */
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonRelease-1> {%s idle_mode %s; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonPress-1> {%s constrain_tmode %s x %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonPress-2> {%s constrain_tmode %s y %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Shift-Lock-ButtonPress-3> {%s constrain_tmode %s z %%x %%y; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
/* Key Bindings */
|
||||
bu_vls_printf(&bindings, "bind %s 3 {%s aet %s 35 25; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s 4 {%s aet %s 45 45; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s f {%s aet %s 0 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s F {%s aet %s 0 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s R {%s aet %s 180 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s r {%s aet %s 270 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s l {%s aet %s 90 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s L {%s aet %s 90 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s t {%s aet %s 270 90; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s T {%s aet %s 270 90; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s b {%s aet %s 270 -90; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s B {%s aet %s 270 -90; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s + {%s zoom %s 2.0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s = {%s zoom %s 2.0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s _ {%s zoom %s 0.5; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s - {%s zoom %s 0.5; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Key-Left> {%s rot %s -v 0 1 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Key-Right> {%s rot %s -v 0 -1 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Key-Up> {%s rot %s -v 1 0 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Key-Down> {%s rot %s -v -1 0 0; break}; ",
|
||||
bu_vls_addr(pathvls),
|
||||
bu_vls_addr(¤t_top->to_gedp->go_name),
|
||||
bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_addr(&bindings));
|
||||
/* Dispatch all events to bu_binding */
|
||||
bu_vls_printf(&bindings, "bind %s <Any-KeyPress> {tclcad_dispatch_event %%W %s %s key-press %%K %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(¤t_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Any-ButtonPress> {tclcad_dispatch_event %%W %s %s mouse-press %%b %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(¤t_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Any-ButtonRelease> {tclcad_dispatch_event %%W %s %s mouse-release %%b %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(¤t_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <Any-Motion> {tclcad_dispatch_event %%W %s %s mouse-move 0 %%x %%y %%s; break}; ", bu_vls_addr(pathvls), bu_vls_addr(¤t_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
|
||||
bu_vls_printf(&bindings, "bind %s <MouseWheel> {tclcad_dispatch_event %%W %s %s mouse-wheel 0 %%x %%y %%s %%D; break}; ", bu_vls_addr(pathvls), bu_vls_addr(¤t_top->to_gedp->go_name), bu_vls_addr(&gdvp->gv_name));
|
||||
|
||||
Tcl_Eval(current_top->to_interp, bu_vls_cstr(&bindings));
|
||||
}
|
||||
}
|
||||
bu_vls_free(&bindings);
|
||||
|
||||
@@ -49,12 +49,8 @@ proc mged_bind_dm { w } {
|
||||
|
||||
bind $w <Enter> [list mged_activate_dm $w]
|
||||
|
||||
#default mouse bindings
|
||||
default_mouse_bindings $w
|
||||
# Load standardized JSON bindings for mged
|
||||
|
||||
#default key bindings
|
||||
set forwarding_key($w) 0
|
||||
default_key_bindings $w
|
||||
}
|
||||
|
||||
proc print_return_val str {
|
||||
@@ -64,129 +60,6 @@ proc print_return_val str {
|
||||
}
|
||||
}
|
||||
|
||||
if ![info exists mged_default(dm_key_bindings)] {
|
||||
set mged_default(dm_key_bindings) "\tKey Sequence\t\tBehavior
|
||||
\ta\t\t\ttoggle angle distance cursor (ADC)
|
||||
\te\t\t\ttoggle edit axes
|
||||
\tm\t\t\ttoggle model axes
|
||||
\tv\t\t\ttoggle view axes
|
||||
\ti\t\t\tadvance illumination pointer forward
|
||||
\tI\t\t\tadvance illumination pointer backward
|
||||
\tp\t\t\tsimulate mouse press (i.e. to pick a solid)
|
||||
\t3\t\t\tview - ae 35 25
|
||||
\t4\t\t\tview - ae 45 45
|
||||
\tf\t\t\tfront view
|
||||
\tt\t\t\ttop view
|
||||
\tb\t\t\tbottom view
|
||||
\tl\t\t\tleft view
|
||||
\tr\t\t\tright view
|
||||
\tR\t\t\trear view
|
||||
\ts\t\t\tenter solid illumination state
|
||||
\to\t\t\tenter object illumination state
|
||||
\tq\t\t\treject edit
|
||||
\tu\t\t\tzero knobs and sliders
|
||||
\t<F1>\t\t\ttoggle depthcue
|
||||
\t<F2>\t\t\ttoggle zclip
|
||||
\t<F3>\t\t\ttoggle perspective
|
||||
\t<F4>\t\t\ttoggle zbuffer
|
||||
\t<F5>\t\t\ttoggle lighting
|
||||
\t<F6>\t\t\ttoggle perspective angle
|
||||
\t<F7>\t\t\ttoggle faceplate
|
||||
\t<F8>\t\t\ttoggle faceplate GUI
|
||||
\t<F9>\t\t\ttoggle keystroke forwarding
|
||||
\t<F12>\t\t\tzero knobs
|
||||
\t<Left>\t\t\tabsolute rotate about y axis
|
||||
\t<Right>\t\t\tabsolute rotate about y axis
|
||||
\t<Down>\t\t\tabsolute rotate about x axis
|
||||
\t<Up>\t\t\tabsolute rotate about x axis
|
||||
\t<Shift-Left>\t\tabsolute translate in X direction
|
||||
\t<Shift-Right>\t\tabsolute translate in X direction
|
||||
\t<Shift-Down>\t\tabsolute translate in Z direction
|
||||
\t<Shift-Up>\t\tabsolute translate in Z direction
|
||||
\t<Control-Shift-Left>\tabsolute rotate about z axis
|
||||
\t<Control-Shift-Right>\tabsolute rotate about z axis
|
||||
\t<Control-Shift-Down>\tabsolute translate in Y direction
|
||||
\t<Control-Shift-Up>\tabsolute translate in Y direction
|
||||
\t<Control-n>\t\tgoto next view
|
||||
\t<Control-p>\t\tgoto previous view
|
||||
\t<Control-t>\t\ttoggle between the current view and the last view"
|
||||
}
|
||||
|
||||
proc default_key_bindings { w } {
|
||||
# common view shortcuts
|
||||
bind $w 2 "winset $w; ae 35 -25; break"
|
||||
bind $w 3 "winset $w; press 35,25; break"
|
||||
bind $w 4 "winset $w; press 45,45; break"
|
||||
bind $w 5 "winset $w; ae 145 25; break"
|
||||
bind $w 6 "winset $w; ae 215 25; break"
|
||||
bind $w 7 "winset $w; ae 325 25; break"
|
||||
|
||||
# default key shortcuts
|
||||
bind $w a "winset $w; adc; break"
|
||||
bind $w b "winset $w; press bottom; break"
|
||||
bind $w c "open_cmd_win id_0"
|
||||
bind $w e "winset $w; rset ax edit_draw !; update_gui $w edit_draw \[rset ax edit_draw\]; break"
|
||||
bind $w f "winset $w; press front; break"
|
||||
bind $w i "winset $w; aip f; break"
|
||||
bind $w l "winset $w; press left; break"
|
||||
bind $w m "winset $w; rset ax model_draw !; update_gui $w model_draw \[rset ax model_draw\]; break"
|
||||
bind $w o "winset $w; press oill; break"
|
||||
bind $w p "winset $w; M 1 0 0; break"
|
||||
bind $w q "winset $w; press reject; break"
|
||||
bind $w r "winset $w; press right; break"
|
||||
bind $w s "winset $w; press sill; break"
|
||||
bind $w t "winset $w; press top; break"
|
||||
bind $w u "winset $w; svb; break"
|
||||
bind $w v "winset $w; rset ax view_draw !; update_gui $w view_draw \[rset ax view_draw\]; break"
|
||||
|
||||
# default shift+key shortcuts
|
||||
bind $w A "winset $w; press accept; break"
|
||||
bind $w I "winset $w; aip b; break"
|
||||
bind $w N "winset $w; puts \[nirt -b\]; break"
|
||||
bind $w P "winset $w; catch {sed_apply}; break"
|
||||
bind $w R "winset $w; press rear; break"
|
||||
bind $w S "winset $w; catch {sed_reset}; break"
|
||||
|
||||
# default control+key shortcuts
|
||||
bind $w <Control-n> "winset $w; _mged_view_ring next; break"
|
||||
bind $w <Control-p> "winset $w; _mged_view_ring prev; break"
|
||||
bind $w <Control-t> "winset $w; _mged_view_ring toggle; break"
|
||||
|
||||
# shift grips navigation
|
||||
bind $w <Control-Shift-Down> "winset $w; knob -i aY \$mged_default(tran_factor); break"
|
||||
bind $w <Control-Shift-Left> "winset $w; knob -i az \$mged_default(rot_factor); break"
|
||||
bind $w <Control-Shift-Right> "winset $w; knob -i az -\$mged_default(rot_factor); break"
|
||||
bind $w <Control-Shift-Up> "winset $w; knob -i aY -\$mged_default(tran_factor); break"
|
||||
bind $w <Down> "winset $w; knob -i ax \$mged_default(rot_factor); break"
|
||||
bind $w <Left> "winset $w; knob -i ay -\$mged_default(rot_factor); break"
|
||||
bind $w <Right> "winset $w; knob -i ay \$mged_default(rot_factor); break"
|
||||
bind $w <Up> "winset $w; knob -i ax -\$mged_default(rot_factor); break"
|
||||
bind $w <Shift-Down> "winset $w; knob -i aZ -\$mged_default(tran_factor); break"
|
||||
bind $w <Shift-Left> "winset $w; knob -i aX \$mged_default(tran_factor); break"
|
||||
bind $w <Shift-Right> "winset $w; knob -i aX -\$mged_default(tran_factor); break"
|
||||
bind $w <Shift-Up> "winset $w; knob -i aZ \$mged_default(tran_factor); break"
|
||||
|
||||
# function keys (settings)
|
||||
bind $w <F1> "winset $w; dm set depthcue !; update_gui $w depthcue \[dm set depthcue\]; break"
|
||||
bind $w <F2> "winset $w; dm set zclip !; update_gui $w zclip \[dm set zclip\]; break"
|
||||
bind $w <F3> "winset $w; set perspective_mode !; update_gui $w perspective_mode \$perspective_mode; break"
|
||||
bind $w <F4> "winset $w; dm set zbuffer !; update_gui $w zbuffer \[dm set zbuffer\]; break"
|
||||
bind $w <F5> "winset $w; dm set lighting !; update_gui $w lighting \[dm set lighting\]; break"
|
||||
bind $w <F6> "winset $w; set toggle_perspective !; break"
|
||||
bind $w <F7> "winset $w; set faceplate !; update_gui $w faceplate \$faceplate; break"
|
||||
bind $w <F8> "winset $w; set orig_gui !; update_gui $w orig_gui \$orig_gui; break"
|
||||
bind $w <F9> "toggle_forward_key_bindings $w; update_gui $w forward_keys \$forwarding_key($w); break"
|
||||
bind $w <F12> "winset $w; knob zero; break"
|
||||
|
||||
# interrupt
|
||||
bind $w <Escape> "winset $w; reset_everything $w; break"
|
||||
|
||||
# Throw away other key events
|
||||
bind $w <KeyPress> {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
proc reset_everything { w } {
|
||||
global mged_gui
|
||||
|
||||
@@ -202,274 +75,6 @@ proc reset_everything { w } {
|
||||
set_mouse_behavior $id
|
||||
}
|
||||
|
||||
proc set_forward_keys { w val } {
|
||||
global forwarding_key
|
||||
|
||||
set forwarding_key($w) $val
|
||||
if {$forwarding_key($w)} {
|
||||
forward_key_bindings $w
|
||||
} else {
|
||||
default_key_bindings $w
|
||||
}
|
||||
}
|
||||
|
||||
proc toggle_forward_key_bindings { w } {
|
||||
global forwarding_key
|
||||
|
||||
if {$forwarding_key($w)} {
|
||||
default_key_bindings $w
|
||||
set forwarding_key($w) 0
|
||||
} else {
|
||||
forward_key_bindings $w
|
||||
set forwarding_key($w) 1
|
||||
}
|
||||
}
|
||||
|
||||
proc forward_key_bindings { w } {
|
||||
set id [get_player_id_dm $w]
|
||||
|
||||
if {$id == "mged"} {
|
||||
return
|
||||
}
|
||||
|
||||
# First, unset the default key bindings
|
||||
bind $w a {}
|
||||
bind $w e {}
|
||||
bind $w m {}
|
||||
bind $w v {}
|
||||
bind $w i {}
|
||||
bind $w I {}
|
||||
bind $w p {}
|
||||
bind $w 2 {}
|
||||
bind $w 3 {}
|
||||
bind $w 4 {}
|
||||
bind $w 5 {}
|
||||
bind $w 6 {}
|
||||
bind $w 7 {}
|
||||
bind $w f {}
|
||||
bind $w t {}
|
||||
bind $w b {}
|
||||
bind $w l {}
|
||||
bind $w N {}
|
||||
bind $w r {}
|
||||
bind $w R {}
|
||||
bind $w s {}
|
||||
bind $w o {}
|
||||
bind $w q {}
|
||||
bind $w u {}
|
||||
bind $w c {}
|
||||
bind $w A {}
|
||||
bind $w P {}
|
||||
bind $w S {}
|
||||
bind $w <F1> {}
|
||||
bind $w <F2> {}
|
||||
bind $w <F3> {}
|
||||
bind $w <F4> {}
|
||||
bind $w <F5> {}
|
||||
bind $w <F6> {}
|
||||
bind $w <F7> {}
|
||||
bind $w <F8> {}
|
||||
bind $w <F12> {}
|
||||
|
||||
bind $w <Left> {}
|
||||
bind $w <Right> {}
|
||||
bind $w <Down> {}
|
||||
bind $w <Up> {}
|
||||
bind $w <Shift-Left> {}
|
||||
bind $w <Shift-Right> {}
|
||||
bind $w <Shift-Down> {}
|
||||
bind $w <Shift-Up> {}
|
||||
bind $w <Control-Shift-Left> {}
|
||||
bind $w <Control-Shift-Right> {}
|
||||
bind $w <Control-Shift-Down> {}
|
||||
bind $w <Control-Shift-Up> {}
|
||||
|
||||
bind $w <Control-n> {}
|
||||
bind $w <Control-p> {}
|
||||
bind $w <Control-t> {}
|
||||
|
||||
bind $w <Escape> {}
|
||||
|
||||
# The focus commands in the binding below are necessary to insure
|
||||
# that .$id.t gets the event.
|
||||
bind $w <KeyPress> "\
|
||||
focus .$id.t;\
|
||||
set mged_gui(.$id.t,insert_char_flag) 1;\
|
||||
event generate .$id.t <KeyPress> -state %s -keysym %K;\
|
||||
set mged_gui(.$id.t,insert_char_flag) 0;\
|
||||
focus %W;\
|
||||
break"
|
||||
}
|
||||
|
||||
proc default_mouse_bindings { w } {
|
||||
global no_focus_hack
|
||||
global transform
|
||||
global tcl_platform
|
||||
|
||||
# default button bindings
|
||||
if {![regexp {^[0-9]+} $::tcl_platform(osVersion) osMajorVersion]} {
|
||||
set osMajorVersion 0
|
||||
}
|
||||
|
||||
if {[info exists no_focus_hack] && $no_focus_hack} {
|
||||
bind $w <1> "winset $w; zoom 0.5; break"
|
||||
bind $w <2> "winset $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
|
||||
bind $w <3> "winset $w; zoom 2.0; break"
|
||||
|
||||
# Mouse wheel fun with Tcl/Tk - see https://wiki.tcl-lang.org/page/mousewheel
|
||||
bind $w <MouseWheel> "winset $w; focus $w; if {%D < 0} {zoom 0.9} else {zoom 1.1}"
|
||||
bind $w <Button-4> {event generate [focus] <MouseWheel> -delta 120}
|
||||
bind $w <Button-5> {event generate [focus] <MouseWheel> -delta -120}
|
||||
|
||||
if {$::tcl_platform(os) == "Darwin"} {
|
||||
# Mac option-key emulates mouse 2 (doesn't work?)
|
||||
bind $w <Option-ButtonPress-1> "winset $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
|
||||
# Mac command-key emulates mouse 3
|
||||
bind $w <Mod2-ButtonPress-1> "winset $w; zoom 2.0; break"
|
||||
}
|
||||
} else {
|
||||
bind $w <1> "winset $w; focus $w; zoom 0.5; break"
|
||||
bind $w <2> "winset $w; focus $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
|
||||
bind $w <3> "winset $w; focus $w; zoom 2.0; break"
|
||||
|
||||
# Mouse wheel fun with Tcl/Tk - see https://wiki.tcl-lang.org/page/mousewheel
|
||||
bind $w <MouseWheel> "winset $w; focus $w; if {%D < 0} {zoom 0.9} else {zoom 1.1}; break"
|
||||
bind $w <Button-4> {event generate [focus] <MouseWheel> -delta 120}
|
||||
bind $w <Button-5> {event generate [focus] <MouseWheel> -delta -120}
|
||||
|
||||
if {$::tcl_platform(os) == "Darwin"} {
|
||||
# Mac command-key emulates mouse 2 (doesn't work?)
|
||||
bind $w <Option-ButtonPress-1> "winset $w; focus $w; set tmpstr \[dm m %x %y\]; print_return_val \$tmpstr; break"
|
||||
# Mac command-key emulates mouse 3
|
||||
bind $w <Mod2-ButtonPress-1> "winset $w; focus $w; zoom 2.0; break"
|
||||
}
|
||||
}
|
||||
|
||||
bind $w <ButtonRelease> "winset $w; dm idle; break"
|
||||
bind $w <Motion> "winset $w; if {\[dm type\] == \"tkswrast\"} {dm motion %x %y}; break"
|
||||
bind $w <KeyRelease-Control_L> "winset $w; dm idle; break"
|
||||
bind $w <KeyRelease-Control_R> "winset $w; dm idle; break"
|
||||
bind $w <KeyRelease-Shift_L> "winset $w; dm idle; break"
|
||||
bind $w <KeyRelease-Shift_R> "winset $w; dm idle; break"
|
||||
bind $w <KeyRelease-Alt_L> "winset $w; dm idle; break"
|
||||
bind $w <KeyRelease-Alt_R> "winset $w; dm idle; break"
|
||||
|
||||
if ![catch {adc draw} result] {
|
||||
set adcflag $result
|
||||
} else {
|
||||
set adcflag 0
|
||||
}
|
||||
|
||||
if {$adcflag == "1" && $transform == "a"} {
|
||||
bind $w <Shift-ButtonPress-1> "winset $w; dm adc t %x %y; \
|
||||
shift_grip_hints $w \"Translate ADC\"; break"
|
||||
bind $w <Shift-ButtonPress-2> "winset $w; dm adc t %x %y; \
|
||||
shift_grip_hints $w \"Translate ADC\"; break"
|
||||
bind $w <Shift-ButtonPress-3> "winset $w; dm adc d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
|
||||
bind $w <Control-ButtonPress-1> "winset $w; dm adc 1 %x %y; \
|
||||
shift_grip_hints $w \"Rotate Angle 1\"; break"
|
||||
bind $w <Control-ButtonPress-2> "winset $w; dm adc 2 %x %y; \
|
||||
shift_grip_hints $w \"Rotate Angle 2\"; break"
|
||||
bind $w <Control-ButtonPress-3> "winset $w; dm adc d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
|
||||
bind $w <Shift-Control-ButtonPress-1> "winset $w; dm adc d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
bind $w <Shift-Control-ButtonPress-2> "winset $w; dm adc d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
bind $w <Shift-Control-ButtonPress-3> "winset $w; dm adc d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
|
||||
#constrained adc defaults
|
||||
bind $w <Alt-Shift-ButtonPress-1> "winset $w; dm con a x %x %y; \
|
||||
shift_grip_hints $w \"X Translate ADC\"; break"
|
||||
bind $w <Alt-Shift-ButtonPress-2> "winset $w; dm con a y %x %y; \
|
||||
shift_grip_hints $w \"Y Translate ADC\"; break"
|
||||
bind $w <Alt-Shift-ButtonPress-3> "winset $w; dm con a d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
|
||||
bind $w <Alt-Control-ButtonPress-1> "winset $w; dm con a 1 %x %y; \
|
||||
shift_grip_hints $w \"Rotate Angle 1\"; break"
|
||||
bind $w <Alt-Control-ButtonPress-2> "winset $w; dm con a 2 %x %y; \
|
||||
shift_grip_hints $w \"Rotate Angle 2\"; break"
|
||||
bind $w <Alt-Control-ButtonPress-3> "winset $w; dm con a d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
|
||||
bind $w <Alt-Shift-Control-ButtonPress-1> "winset $w; dm con a d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
bind $w <Alt-Shift-Control-ButtonPress-2> "winset $w; dm con a d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
bind $w <Alt-Shift-Control-ButtonPress-3> "winset $w; dm con a d %x %y; \
|
||||
shift_grip_hints $w \"Translate Tick Distance\"; break"
|
||||
} else {
|
||||
bind $w <Shift-ButtonPress-1> "winset $w; dm am t %x %y; \
|
||||
shift_grip_hints $w Translate; break"
|
||||
bind $w <Shift-ButtonPress-2> "winset $w; dm am t %x %y; \
|
||||
shift_grip_hints $w Translate; break"
|
||||
bind $w <Shift-ButtonPress-3> "winset $w; dm am t %x %y; \
|
||||
shift_grip_hints $w Translate; break"
|
||||
|
||||
bind $w <Control-ButtonPress-1> "winset $w; dm am r %x %y; \
|
||||
shift_grip_hints $w Rotate; break"
|
||||
bind $w <Control-ButtonPress-2> "winset $w; dm am r %x %y; \
|
||||
shift_grip_hints $w Rotate; break"
|
||||
bind $w <Control-ButtonPress-3> "winset $w; dm am r %x %y; \
|
||||
shift_grip_hints $w Rotate; break"
|
||||
|
||||
bind $w <Shift-Control-ButtonPress-1> "winset $w; dm am s %x %y; \
|
||||
shift_grip_hints $w Scale/Zoom; break"
|
||||
bind $w <Shift-Control-ButtonPress-2> "winset $w; dm am s %x %y; \
|
||||
shift_grip_hints $w Scale/Zoom; break"
|
||||
bind $w <Shift-Control-ButtonPress-3> "winset $w; dm am s %x %y; \
|
||||
shift_grip_hints $w Scale/Zoom; break"
|
||||
|
||||
#constrained defaults
|
||||
bind $w <Alt-Shift-ButtonPress-1> "winset $w; dm con t x %x %y; \
|
||||
shift_grip_hints $w \"X Translation\"; break"
|
||||
bind $w <Alt-Shift-ButtonPress-2> "winset $w; dm con t y %x %y; \
|
||||
shift_grip_hints $w \"Y Translation\"; break"
|
||||
bind $w <Alt-Shift-ButtonPress-3> "winset $w; dm con t z %x %y; \
|
||||
shift_grip_hints $w \"Z Translation\"; break"
|
||||
|
||||
bind $w <Alt-Control-ButtonPress-1> "winset $w; dm con r x %x %y; \
|
||||
shift_grip_hints $w \"X Rotation\"; break"
|
||||
bind $w <Alt-Control-ButtonPress-2> "winset $w; dm con r y %x %y; \
|
||||
shift_grip_hints $w \"Y Rotation\"; break"
|
||||
bind $w <Alt-Control-ButtonPress-3> "winset $w; dm con r z %x %y; \
|
||||
shift_grip_hints $w \"Z Rotation\"; break"
|
||||
|
||||
bind $w <Alt-Shift-Control-ButtonPress-1> "winset $w; dm con s x %x %y; \
|
||||
scale_shift_grip_hints $w X; break"
|
||||
bind $w <Alt-Shift-Control-ButtonPress-2> "winset $w; dm con s y %x %y; \
|
||||
scale_shift_grip_hints $w Y; break"
|
||||
bind $w <Alt-Shift-Control-ButtonPress-3> "winset $w; dm con s z %x %y; \
|
||||
scale_shift_grip_hints $w Z; break"
|
||||
}
|
||||
}
|
||||
|
||||
proc shift_grip_hints { w hint } {
|
||||
global mged_display
|
||||
global mged_gui
|
||||
global win_to_id
|
||||
|
||||
if ![info exists win_to_id($w)] {
|
||||
return
|
||||
}
|
||||
|
||||
set id $win_to_id($w)
|
||||
set mged_gui($id,illum_label) $hint
|
||||
}
|
||||
|
||||
proc scale_shift_grip_hints { w axis } {
|
||||
if {[status state] == "OBJ EDIT"} {
|
||||
shift_grip_hints $w "$axis Scale"
|
||||
} else {
|
||||
shift_grip_hints $w "Scale/Zoom"
|
||||
}
|
||||
}
|
||||
|
||||
proc update_gui { w vname val } {
|
||||
global mged_players
|
||||
global mged_gui
|
||||
@@ -482,6 +87,14 @@ proc update_gui { w vname val } {
|
||||
}
|
||||
}
|
||||
|
||||
# Dummy stubs to prevent legacy scripts from failing if they try to invoke them
|
||||
proc default_key_bindings { w } {}
|
||||
proc default_mouse_bindings { w } {}
|
||||
proc forward_key_bindings { w } {}
|
||||
proc toggle_forward_key_bindings { w } {}
|
||||
proc set_forward_keys { w val } {}
|
||||
proc shift_grip_hints { w hint } {}
|
||||
proc scale_shift_grip_hints { w axis } {}
|
||||
|
||||
# Local Variables:
|
||||
# mode: Tcl
|
||||
|
||||
Reference in New Issue
Block a user