Compare commits

...

108 Commits

Author SHA1 Message Date
Bruno Postle 92f9bd5938 Fix null reference bind in header parsing
references_to_resolve is never set while parsing header
entities, so binding a reference to it was UB, caught by
UBSan on any file with a header.

Generated with the assistance of an AI coding tool.
2026-07-18 00:39:25 +01:00
Andrej730 71a598e63a ifcopenshell.file: small wording fix 2026-07-17 21:55:22 +05:00
Andrej730 3d8115ebc5 ifcopenshell.file: drop workarounds for older builds
Introduced in aeed371 and it's been a while.
2026-07-17 21:55:22 +05:00
Ryan Schultz b5a0f1fc74 Bonsai: allow cross-family class reassignment for spatial elements with geometry (#8665)
The Reassign Class operator refused to reassign an element to a different
IFC product family unless it was an IfcElement <-> IfcElementType swap, so a
piece of geometry mistakenly hosted on IfcSite could not be turned into
IfcFurniture even though root.reassign_class handles it fine.

Loosen the guard: only block the case that actually matters - a spatial
element (IfcSpatialElement / IfcSpatialStructureElement for IFC2X3) with no
geometry, which would be a real containment-hierarchy container rather than
a stray modelled object. Everything else reassigns freely.

Closes #8664

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 18:57:11 -05:00
Petru Conduraru 25441bd816 Bonsai: make 'has openings' representation error actionable (#8108)
When converting a wall representation to a parametric extrusion via the
Representation Utilities buttons, an element that has openings would report
"has openings - representation cannot be updated" and stop, without telling
the user there is an ALT+click path that bakes the openings into the new
representation. Point the message at that path so the error is actionable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:35:46 +02:00
Petru Conduraru 65811ac7c9 Bonsai: place auto-generated opening boundaries at their real position (#8237) (#8311)
* Bonsai: place auto-generated opening boundaries at their real position #8237

auto_generate_boundaries (single-space mode) built each opening/filling boundary
from the opening's LOCAL geometry (get_vertices) but first did
mat.translation = (0, 0, 0) on its placement matrix. Because the vertices are
local, that placement translation is exactly what carries the opening to its
real location, so zeroing it collapsed every window/door boundary onto the
origin. This is why the auto path misplaced window boundaries while the
single-element path (create_element_boundary) placed them correctly, as
@MDHering observed with the two modes. Keep the full placement matrix.

Verified on the reporter's file: the opening's real placement is (0.1, 1.5, 1.0);
a vertex went from (0.6, 0, 0) under the old code to (0.7, 1.5, 1.0) with the fix,
i.e. moved by exactly the (0.1, 1.5, 1.0) that was being discarded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove superfluous comment from #8237 fix

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: CyrilWaechter <cyril@biminsight.ch>
2026-07-16 20:17:02 +02:00
Andrej730 d9d1824886 test-package: drop stale comment
This information is already documented in maintanence.rst.
2026-07-16 19:03:35 +05:00
Andrej730 16e5f18553 Bump build 3e7b739 -> 821cf7b
Just to test everything is working with the changes from the last month.
2026-07-16 18:59:44 +05:00
Andrej730 b7a9b7bc5a test-package: assert BUILD_COMMIT is a 7-char short SHA 2026-07-16 18:56:44 +05:00
Andrej730 e14397058d test-package: verify build URLs with HEAD requests instead of scraping listing page 2026-07-16 18:53:54 +05:00
Andrej730 f0117c60b3 stub: sync added/removed symbols 2026-07-16 18:09:32 +05:00
Andrej730 0e5223a30d stub: add missing arrange_polygon_settings (158756e921) 2026-07-16 18:00:55 +05:00
Andrej730 bc41ff78f4 stub: drop abstract_arrangement (158756e921)
And also gnore delete_same_facet_edge_pairs as it's more of an interanl API.
2026-07-16 18:00:47 +05:00
Andrej730 9123d8c183 stub: add missing entity.inverse_attributes 2026-07-16 17:35:52 +05:00
Andrej730 ffd939508c ci-lint: run ty-bonsai and ty-ios as separate steps
So if one fails, it wouldn't block another.
Noticed by Stephen in d5e890bccd
2026-07-16 17:28:24 +05:00
Andrej730 9213b31235 logger: reuse logger_or_root, dedupe optional-logger-arg pattern 2026-07-16 17:28:24 +05:00
Andrej730 2155e3206f logger: use Logger* instead of Logger& to propagate signature using swig
See the comment in IfcLogger.h explaining this.
2026-07-16 17:28:24 +05:00
Andrej730 9e0c6cf524 util.schema: dedupe inline schema resolution logic 2026-07-16 17:28:23 +05:00
Andrej730 d5dc069b2f util.schema: fix geometry_classes_introduced_after using wrong IFC4X3 schema
It was passing `IFC4X3` directly to `schema_by_name` which is expecting
schema identifier (e.g. IFC4X3_ADD2, not IFC4X3 allowed by `IFC_SCHEMA`
- IFC4X3 is one of the IFC4X3 iterations while it was in development,
not the final one).

Noticed by tests failing:
FAILED
test/util/test_schema.py::TestGeometryClassesIntroducedAfter::test_ifc4x3_to_ifc2x3_is_superset_of_ifc4_to_ifc2x3
- RuntimeError: No schema named IFC4X3
FAILED
test/util/test_schema.py::TestGeometryClassesIntroducedAfter::test_ifc4_to_ifc4x3_is_empty
- RuntimeError: No schema named IFC4X3
2026-07-16 17:28:23 +05:00
Andrej730 821cf7b671 ifcparse: replace std::to_chars to fix mac build (ee2b357d7) 2026-07-16 11:57:37 +05:00
Andrej730 e01979187a build-all: fix issue building rocksdb on gcc 15
Example error: `error: ‘uint64_t’ has not been declared uint64_t blob_file_number, uint64_t total_blob_count,`

See https://github.com/facebook/rocksdb/issues/13365
2026-07-15 19:20:17 +05:00
Andrej730 816eba5145 build-all: ensure all patches are present
Also changed type to just `list[str]` to keep it simple.
2026-07-15 19:20:17 +05:00
Andrej730 4963bddd06 build-all: fix issue on gcc 15
Error was:
```
configure: error: could not find a working compiler, see config.log for details
```

config.log:
```
conftest.c: In function 'f':
conftest.c:12:48: error: too many arguments to function 'g'; expected 0, have 6
   12 | for(i=0;i<1;i++){if(e(got,got,9,d[i].n)==0)h();g(i,d[i].src,d[i].n,got,d[i].want,9);if(d[i].n)h();}}
      |                                                ^ ~
```
2026-07-15 19:20:17 +05:00
Andrej730 8718db63da pyproject: flip ty rules to error-by-default, review all new rules added since version bump 2026-07-15 19:20:17 +05:00
Andrej730 c013b9aca7 build-all: drop unused opencollada pr622 patch
Last reference to this file was dropped in 7ae685dbf, though the ref was
pointing to `/patches/opencollada/pr622.patch`, so IIUC
`patches/pr622.patch` was never used.
2026-07-15 19:20:17 +05:00
Andrej730 24e454ce0c build-all: drop unused occt patch
Introduced in e21277e80, reference removed
in 683cadeb7 when occt was bumped to 7.3.0 and switched to git-tag based
download.
2026-07-15 19:20:17 +05:00
Andrej730 cb497b37f7 pyproject: add nix script to ty check 2026-07-15 19:20:17 +05:00
Andrej730 71c6950a59 ifcclash: fix use of undefined clash["position"]
It's an artifact from the old hppfcl clasher dropped in 18c38b312
2026-07-15 19:20:17 +05:00
Andrej730 5a8aa0a659 bsdd: raise informative HTTPError
Previously we were just passing `.json()` which allowed too many request error slip in to later occur as missing attributes on the dictionaries.
2026-07-15 19:20:17 +05:00
Andrej730 f4526d152f bsdd: warn about include_class_properties deprecation
See https://github.com/buildingSMART/bSDD/issues/149
2026-07-15 19:20:17 +05:00
Andrej730 a7a7edfd27 bsdd: fix test_get_class_relations
`classRelations` doesn't exist on `ClassPropertiesContractV1`, probably was just a typo.
2026-07-15 19:20:17 +05:00
Andrej730 6ca8c8ac94 pyproject: add more packages to dev-setup 2026-07-15 19:20:17 +05:00
Andrej730 5273569b08 build-all: fix note about the schemas built by default 2026-07-15 19:20:17 +05:00
Andrej730 78712ead98 misc: more readable poll error for import_quick_favorites 2026-07-14 19:57:31 +05:00
Andrej730 6efb8a4373 misc: add Blender 5.2 offset for Quick Favorites user_menus 2026-07-14 19:57:31 +05:00
Andrej730 9e25c12b16 surveyor: drop never used dead code
Surveyor test was failing because `get_z_rotation` and `set_z_rotation` were not implemented.
The code was added in 230cbe1fd8, but it was never used.
2026-07-14 19:57:31 +05:00
Andrej730 0968d06780 Deduplicate code by reusing tool.document 2026-07-14 19:57:31 +05:00
Andrej730 3a8619726b core.drawing: deduplicate code, fix test
Core test was trying to access actual ifc data (`ifc.get().by_type("IfcGroup")` and was failing.
2026-07-14 19:57:31 +05:00
Andrej730 1b1da821f1 file.get_inverse: document with_attribute_indices overload 2026-07-14 19:29:34 +05:00
Andrej730 d772b24bd6 geometry.add_boolean: fix typo in the class name
🫣🫣
2026-07-14 19:29:34 +05:00
Andrej730 549f81a76e ios pyproject: add networkx stubs as dev dependency 2026-07-14 19:29:34 +05:00
Andrej730 e5c7206a37 express: fix use of non-existent ifcexpressparser
`express.bnf` arg wasn't handled since d506ad77b
`ifcexpressparser` waa moved inside `ifcopenshell-python` awhile ago too
2026-07-14 19:29:34 +05:00
Andrej730 5b968d5c75 pyparsing: fix using deprecated aliases
Deprecated since pyparsing 3.0 and produce runtime warnings. New function work exactly the same, except their name is pep8 compatible.
2026-07-14 19:29:34 +05:00
Andrej730 6075187720 express_parser: fix non-idempotent results 2026-07-14 18:42:12 +05:00
Andrej730 92528d84cd express: clean up trailing spaces 2026-07-14 18:42:12 +05:00
Andrej730 3955718145 express: update transpiled express rules using latest Python's AST
AST parser has changed a bit and there are some minor differences in the .py output. Updating files just to avoid seeing these diffs when rerunning rule compiler.
2026-07-14 18:42:12 +05:00
Andrej730 501246cd0b rule_compiler: fix error running on Python 3.14
Example error:
```
    ast.Str(s=node.attr),
    ^^^^^^^
AttributeError: module 'ast' has no attribute 'Str'
```

`ast.Str` was deprecated since 3.8 and was removed in 3.14, see https://docs.python.org/3/whatsnew/3.14.html#id9
2026-07-14 18:42:12 +05:00
Andrej730 1b863ff8be pyproject: add dev-setup poe task to setup environment for ide 2026-07-14 18:42:11 +05:00
Andrej730 dcd88b6cdd pyproject: Move tool deps from to requirements-tools.txt
Because uv was always trying to install when starting a venv in `ifcopenshell` folder, though they might be already available globally. And also they were listed twice - in pyproject and in the ci-lint.yml, now there's a single source of truth.
2026-07-14 18:42:05 +05:00
Andrej730 fb8b2ee878 bonsai pyproject: move pytest deps to requirements-dev.txt 2026-07-14 18:42:05 +05:00
Andrej730 8a00ce84cc geom/main.py: fix ty complaint 2026-07-14 14:56:06 +05:00
Andrej730 4032bbbd17 entity_instance.py: fix oveloads signatures (f93d79dc)
Without `/` overload implies that it also accepts kw args, while the implementation signature doesn't support them.
2026-07-14 14:56:06 +05:00
Andrej730 d728f09d86 Bump ty
Dropping `conflicting-argument-forms` rule as it was removed in ty 0.0.49.
2026-07-14 14:56:06 +05:00
Andrej730 3e9ef82448 maintenance.rst: move pyver matrix to bundled Python version section 2026-07-14 14:56:06 +05:00
Andrej730 a5c77fd096 ci-bonsai-daily: Use Blender 5.2 for tests 2026-07-14 14:56:06 +05:00
Andrej730 56ed79792e bonsai tests: fix test_failed_to_load_returns_only_base_keys (fdb2947) 2026-07-14 14:56:06 +05:00
Andrej730 9c91727402 express: drop Python 2 fallbacks 2026-07-14 14:56:06 +05:00
Andrej730 d183961280 pyproject: support formatting with ruff
Since it's black-compatible drop-in replacement and they can be used
almost interchangeably.
2026-07-14 14:56:06 +05:00
Andrej730 d30c25010c dev_environment.py: detect Python 3.13 on any Blender 5.1+ 2026-07-14 12:18:00 +05:00
Andrej730 97d1a6e488 dev_environment.py: add shebang and make executable 2026-07-14 12:12:54 +05:00
sboddy 6dec340161 Merge pull request #8576 from IfcOpenShell/fix/docker-macos-arm64-compat
docker: more robust in getting a GID, and editing the .env file.
2026-07-13 13:09:29 +01:00
Petru Conduraru 780739719f Bonsai docs: fix version switcher scheme mismatch (http vs https)
versionURLs in brand.html used http:// while the docs sites are
served over https://, so currentURL.includes(url) never matched and
the <select> never reflected/switched to Unstable. Fixes #8023.

Generated with the assistance of an AI coding tool.
2026-07-13 20:44:19 +10:00
Petru Conduraru 4a717ca7ff Fix ci-bonsai-daily: reconnect Cost/IfcGit tool interfaces (TestImplementsTool)
Two TestImplementsTool failures on v0.8.0:

- test_cost.py: Cost could not be instantiated because
  core.tool.Cost declared abstract get_direct_cost_item_products, which
  tool.cost.Cost never implements. The method is dead (zero call sites;
  get_cost_item_products(is_deep=False) already covers the 'direct'
  case), so remove the abstract declaration.
- test_ifcgit.py: tool.ifcgit.IfcGit was not declared as a subclass of
  its core.tool.IfcGit interface (unlike every sibling tool class), so
  the isinstance check failed. Add the base class (and the
  bonsai.core.tool import it needs). All 50 interface methods are
  already implemented on the concrete class.

No behaviour change. Verified in headless Blender: isinstance(Cost(), core.tool.Cost) and isinstance(IfcGit(), core.tool.IfcGit) both True (were TypeError / False); repo abstract-vs-impl diff confirms all IfcGit abstracts are implemented.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:43:25 +10:00
Petru Conduraru 5a831e3d21 Fix ci-lint: black-format selector.py
black (the version CI's psf/black@stable resolves to) flags three spots
in util/selector.py: the chained .replace() in FormatTransformer.number,
the suppress_zero_inches kwarg in format_length, and the long
`elif key in (...) and hasattr(...)` placement-key tuple in
set_element_value. Reformat all three to black's multi-line style.
Formatting only, no behavioural change (all keys preserved).

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:42:25 +10:00
Petru Conduraru d30286225c Bonsai: deterministic annotation order in generated drawing SVGs (#6608)
generate_annotation built the annotation list from a set union and sorted it by
ZIndex and TEXT-ness only. Annotations that tied on that key kept set iteration
order, which follows entity hash (step id plus the process memory address), so
the order of tied annotations (for example a label and its background fill)
shuffled between Blender restarts and flipped their draw order.

Add the stable IFC step id as a final tiebreaker so the order is total and
session independent. Behavior preserving, no z-layer semantics changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 20:41:41 +10:00
Bruno Postle 65695fb878 ifcedit: fix Optional[entity_instance] coercion crash on native JSON values
coerce_value assumed value_str was always a CLI string, but ifcmcp
passes JSON-decoded native types (int, None) straight through. Guard
the Union/Optional "none" check so it only calls .lower() on strings,
and handle native None explicitly.
2026-07-13 08:55:43 +01:00
Bruno Postle ab15750747 ifcedit: include IfcSpace in default QTO element scope
IfcSpace is not a subtype of IfcElement, so quantify.run_quantify()'s
default selector silently skipped all spaces, reporting
elements_quantified: 0 with no error or warning.

Generated with the assistance of an AI coding tool.
2026-07-13 09:45:25 +01:00
Petru Conduraru 694a44e638 ifc4d: tolerate activities without a CalendarObjectId in P6 import (#5617)
Importing a Primavera P6 XML crashed with
`AttributeError: 'NoneType' object has no attribute 'text'` in
P62Ifc.parse_activity_xml, which read
activity.find("pr:CalendarObjectId").text unconditionally. CalendarObjectId
is optional on a P6 Activity; when omitted, the activity inherits the
project's ActivityDefaultCalendarObjectId.

Capture the project default in parse_xml and fall back to it when an
activity has no CalendarObjectId (`calendar_id or self.default_calendar_id`).

Verified on the reporter's attached file (20241021 Cronograma.xml): 3 of 14
activities lack a CalendarObjectId and reproduced the exact crash on
v0.8.0; after the fix parse_xml completes and those activities resolve to
the project default calendar "2" (a valid calendar in the file). An
activity with an explicit CalendarObjectId keeps its own value.

Fixes the P6 re-import crash reported in #5617 (that issue tracks several
Gantt items; this addresses the import AttributeError).

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 17:15:36 +10:00
Petru Conduraru a3950ac191 util.element: read property sets inside an IfcPropertySetDefinitionSet (#6330)
get_pset and get_psets assumed RelatingPropertyDefinition is a single property
definition and read definition.Name directly. When it is an
IfcPropertySetDefinitionSet (a defined type wrapping a list of property set
definitions) that attribute access raised AttributeError, so an element whose
psets are grouped in a set returned none of them.

Unpack IfcPropertySetDefinitionSet into its members in both loops and process
each one. Single property definitions and the psets_only and qtos_only filters
are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 17:06:18 +10:00
Petru Conduraru 6f90badda8 Fix ci-bonsai-daily: renumber stale STEP ids in BDD feature fixtures
Several BDD scenarios hardcode absolute representation-item object names
whose trailing number is the IFC STEP line id
(f"Item/{item.is_a()}/{item.id()}"). Those ids drift when file-creation
order changes; a recent shift moved all of them by a uniform -4, so the
scenarios failed with "Item/.../NN does not exist".

The failing step (the_object_name_exists in test_feature.py) dumps the
full bpy.data.objects listing on failure, so the correct current ids are
recoverable directly from the CI log (run 29208793599, tested commit
36e21e882f, an ancestor of HEAD with only a .gitignore commit between).
Renumber to match:
  IfcExtrudedAreaSolid/77->73, IfcPolygonalFaceSet/76->72,
  IfcVertexPoint/69->65, IfcEdge/72->68, IfcFace/74->70.

Verified against the CI failure dump (a local build produces different
ids, so this is validated by CI's own object listing rather than a local
run). boolean.feature also hardcodes IfcHalfSpaceSolid/90 and panel text
[91] downstream of the failing assertion, which CI never reached and so
never dumped; left as-is to avoid guessing - they will print a fresh dump
next run for a follow-up if still stale.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:49:33 +10:00
Petru Conduraru 8b05510d6c docker: fix GID collision and macOS sed portability
Two host-environment bugs in the build-env scripts that break on
macOS/Apple Silicon hosts, independent of target architecture:

- Dockerfile: groupadd fails outright when USER_GID collides with an
  existing system group in the rockylinux9 base image (e.g. macOS
  default user GID 20 "staff" collides with RHEL's GID 20 "games").
  Guard with getent so useradd attaches to the existing group instead.
- ifcos_env: `sed -si` is GNU-only syntax and errors under BSD/macOS
  sed. Do the UNIQUE_ID substitution via a portable temp-file + mv.

Per sboddy's review on the original PR: dropped the linux/amd64
platform-pin additions from this change. The stack already targets
Rocky9/x64 build outputs by design, and Docker Desktop on macOS has
no native container runtime regardless (it's a Linux VM either way),
so forcing the image to run under emulation doesn't produce anything
that's actually loadable into a native macOS Blender/Bonsai install.
That's a separate, harder problem worth solving via a native build
path instead (mirroring build_osx.yml), not by fighting emulation
here. These two fixes stand on their own merits on any host.

This change was made with the assistance of an AI tool.
2026-07-13 09:43:57 +03:00
Stephen Boddy b1470223d3 Share ccache volume across checkouts, cap at 2G
The ccache named volume had no explicit name, so Docker Compose
namespaced it under the per-checkout project name (derived from
UNIQUE_ID), giving each checkout its own cache even though
docker/README.md already documented them as shared. Give the volume
a fixed name so all checkouts attach the same one.

Measured cache size after a full build (IfcParse+IfcGeom+IfcConvert+
wrapper, one Python version) is ~300MB, only ~5% of the previous 5G
cap. Shrink CCACHE_MAXSIZE to 2G, which comfortably covers the shared
baseline plus per-branch deltas from several diverging checkouts.

Generated with the assistance of an AI coding tool.
2026-07-13 06:42:05 +01:00
Petru Conduraru f25b072fa0 docker: make the build env work on macOS / Apple Silicon hosts
Three host-portability fixes to the docker/ toolchain from #8564 so it
runs on macOS as well as Linux. All three are no-ops on native amd64
Linux.

1. Dockerfile: only groupadd when the target GID is free. macOS's default
   primary group `staff` is GID 20, which already exists as `games` in
   rockylinux:9, so `groupadd -g 20` aborted the image build. Guard with
   `getent group "${USER_GID}" || groupadd ...`; useradd -g accepts the
   existing GID.

2. ifcos_env unique(): replace GNU-only `sed -si` (BSD/macOS sed errors
   "illegal option -- s") with a portable `sed > tmp && mv` rewrite of the
   UNIQUE_ID line. Verified against macOS BSD sed.

3. create() + compose.yaml: build with an explicit `--platform linux/amd64`
   so the locally built image's platform matches the `platform:
   linux/amd64` pin in compose.yaml. Without it, on arm64 the local image
   is tagged linux/arm64, compose treats the platform-mismatched image as
   absent and tries to pull `ifcopenshell-build-env:updated` from Docker
   Hub (which does not exist -> access denied). Also add `pull_policy:
   never` as a safety net so a future mismatch surfaces as a clear "image
   not found" rather than a registry auth error.

Note: on Apple Silicon the amd64 build runs under emulation and a cold
full build is slow; ccache makes incremental rebuilds tolerable. A native
Linux/Intel host or CI remains the better choice for routine use, but these
fixes turn "hard broken" into "works with a caveat" on macOS.

This change was made with the assistance of an AI tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 06:44:55 +03:00
sboddy ffb867f254 Add .gitignore entries for docker build env (#8569) 2026-07-12 22:23:42 +01:00
sboddy 36e21e882f Merge pull request #8568 from sboddy/fix-autosave-recovery-segfault
Fix segfault on autosave recovery dialog Cancel
2026-07-12 22:00:06 +01:00
Stephen Boddy d0eca6fa90 Fix segfault closing autosave recovery dialog
Reported: Blender segfaults when clicking Cancel on the "newer
autosave found" recovery popup shown by LoadProject at startup.

Root cause: LoadProject.execute()/invoke() triggered the recovery
popup via bpy.ops.bim.load_autosaved_recovery_popup("INVOKE_DEFAULT",
...) and returned that call's result ({'RUNNING_MODAL'}) as their own
return value, without LoadProject itself ever calling
modal_handler_add(). Blender's window manager takes a RUNNING_MODAL
return as a promise the operator registered its own modal handler;
since it hadn't, the WM's operator bookkeeping was left corrupted -
silently, since this is heap/state corruption rather than an
immediate crash. It only surfaced later, when the real modal operator
(the popup) closed and the WM reconciled its modal stack, which lines
up with the crash occurring specifically on dialog close regardless
of which button was pressed. check_autosave_recovery() now returns a
plain bool and fires the popup fire-and-forget; LoadProject reports
its own honest {"FINISHED"}.

Also hardened, as defense in depth: LoadAutosavedRecoveryPopup's
execute()/cancel() call back into bim.load_project(...), which (with
should_start_fresh_session) calls wm.read_homefile() and tears down
the window manager/screens. Doing that synchronously from inside this
popup's own execute()/cancel() - itself invoked from deep inside
Blender's modal handling for the popup's button click - risks the
same class of use-after-free as the timer bug fixed in the previous
commit. The reload is now deferred by one timer tick so it runs after
the popup's modal handling has fully unwound, and the deferred
callback closes over plain values rather than `self`, since the
operator instance may not survive past cancel()/execute() returning.

This defer-only change was tried and tested first, on the (incorrect)
assumption it was the root cause: it produced a byte-for-byte
identical crash backtrace on retest, which is what pointed at the
RUNNING_MODAL bug above as the actual cause - the defer change alone
was insufficient because the corruption happens when the popup is
first shown, not when it's closed.

Generated with the assistance of an AI coding tool.
2026-07-12 21:52:09 +01:00
Stephen Boddy 6306ce0f80 Fix autosave timer self-unregister crash risk
The periodic autosave timer called reset_timer() at the end of its
own callback, which unregistered the timer that was still executing
(itself). Blender frees the timer's internal registry entry on that
manual unregister, then frees it again when the callback returns
None - a double free that corrupts the heap and can crash Blender
later, once the corrupted memory is reused.

Reschedule by returning the next interval from the callback instead,
which is the safe, documented way to repeat a bpy.app.timers
callback. External reset_timer() calls (from SaveProject,
LoadProject, AutosavePrompt) are unaffected since they run from a
separate call stack (UI events), not from inside the timer.

Found while investigating a segfault reported when cancelling the
autosave recovery popup; not itself the cause of that crash (see the
following commit), but the same reentrant-unregister pattern and a
real, independent latent bug in the periodic reminder path.

Generated with the assistance of an AI coding tool.
2026-07-12 21:51:56 +01:00
sboddy 53187ddae9 Merge pull request #8564 from sboddy/docker-build-env-tooling
Add a local docker build environment for IfcOpenShell (docker/)

See PR #8564 for full explanation.
2026-07-12 20:50:53 +01:00
Stephen Boddy 92c50ed3b4 Harden docker build tooling: non-root, clean lifecycle, try()
Dockerfile (renamed from Dockerfile_init, Dockerfile_update removed):
- Run as a non-root `builder` user matching the host UID/GID (passed as
  --build-arg by create() from id -u/id -g), so build output under the
  bind mount stays owned by the host user instead of root.
- Fix CCACHE_MAXSIZE: `ccache -M 5G` wrote its limit to a config file
  under /ccache at image-build time, but /ccache is a volume mount
  point, so that file gets shadowed by the (empty) volume the moment
  the container actually runs - the cap never took effect. Set
  CCACHE_MAXSIZE=5G as an image ENV instead.
- Dedupe ccache/libffi-devel, add --setopt=install_weak_deps=False
  --setopt=tsflags=nodocs, add `git lfs install --system`, combine the
  dnf update+install into one layer.
- Drop Dockerfile_update: it built FROM its own previous output, so
  every `update` call made the image strictly larger forever (Docker
  layers are append-only, `dnf clean` in a later layer can't shrink an
  earlier one). `update` now just calls create(), which already runs
  `dnf update -y` FROM a clean rockylinux:9 every time.

compose.yaml: pin platform: linux/amd64 so this doesn't silently run
under emulation on an ARM host.

ifcos_env:
- Split the previously-conflated stop/down into six distinct,
  Compose-native lifecycle commands: up (create-or-start), down
  (remove), stop, start, restart (stop+start, same container),
  recreate (down+up, fresh container). Previously `stop` was aliased
  to `down`, which silently removed the container instead of pausing
  it.
- Implement try(): copies the built wrapper into a real Blender/Bonsai
  install for manual testing, reading the target from a new
  BLENDER_USER_RESOURCE .env variable and auto-detecting the built
  Python version (disambiguating via PY_TGT for multi-version builds).
  Deliberately kept human-only - it mutates a live Blender install, so
  it shouldn't run unattended as part of an automated/AI workflow,
  which should instead copy the wrapper into the repo's own
  src/ifcopenshell-python/ifcopenshell/ (documented in SKILL.md).
- Fix unique(): the "has .env already got a UNIQUE_ID line" check
  referenced an unset $FILE instead of $ENV_FILE, so it always
  evaluated true and appended a fresh "UNIQUE_ID=dummy" line to .env
  on every single `up`.
- Minor: differentiate remove()'s log message from down()'s (no longer
  identical now that they're distinct operations), tidy help text
  alignment and a stray double-space typo in clean().

SKILL.md: rewritten as current-state documentation (no more "fixed in
this copy" changelog framing) covering the above, plus a migration
note for anyone hitting root-owned leftovers from an older image.

Verified by actually building the image and driving every new
lifecycle command (stop/start/restart keep the same container ID;
down+up and recreate produce a new one) and try() (including the
quoted-tilde BLENDER_USER_RESOURCE edge case) against the real container.

Generated with the assistance of an AI coding tool.
2026-07-12 20:35:01 +01:00
Stephen Boddy fa98aad469 First docker build environment
First functional version, but it needs some improvements and fixes
identified as I've used it personally on one thing, and when an AI
(Claude) used it to work through the CI test errors.

I had the AI make a SKILL.md file. If the AI indicates it needs to
build the ifcopenshell binary, use this and let it rip.
2026-07-12 18:43:27 +01:00
Petru Conduraru 980988f208 Bonsai: fix KeyError in format_distance for kilometre and mile units #8255
The project-unit to Blender-unit mapping in format_distance only knew
FOOT/INCH/METRE/DECIMETRE/CENTIMETRE/MILLIMETRE, so creating a project
with Kilometers or Miles in the New Project Wizard crashed with
KeyError: 'KILOMETRE' (or 'MILE') as soon as the spatial tree formatted
an elevation. Add the missing Blender-supported units (kilometre, mile,
micrometre) and fall through gracefully for anything else (for example
HECTOMETRE) so unknown units use the adaptive formatting branch instead
of raising.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:51:37 +10:00
Petru Conduraru d4805387ef Selector: add rotation_x/y/z value keys #6262
Expose the Euler rotation of an element's placement in degrees through
get_element_value, alongside the existing x/y/z and easting/northing/
elevation keys. This makes element rotation exportable through ifccsv,
e.g. for placing oriented symbols in GIS.

Adopts the approach agreed in the review of the stale PR #6272 by
@TZwielehner: reuse util.shape_builder.np_matrix_to_euler and do the
degree conversion inside get_element_value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:49:43 +10:00
Petru Conduraru 69a4be68e8 Bonsai: fall back to adaptive units for unsupported SI prefixes #8074
Project loading set scene length_unit to f"{Prefix}METERS", but Blender's
enum only defines KILOMETERS, CENTIMETERS, MILLIMETERS and MICROMETERS.
A model with a DECIMETRE (or HECTO/DECA/etc.) length unit therefore raised
on the enum assignment and the file failed to open. Guard with the set of
supported values and fall back to ADAPTIVE display for the rest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:38:10 +10:00
Petru Conduraru 06da416b8f docs: remove TODO placeholder sections from the create-model quickstart #8208
The quickstart ended with three empty sections whose bodies were only
"TODO" (placing occurrences, changing locations, modeling a building),
which read as a dead end on docs.bonsaibim.org. The page now ends on the
completed save-and-view flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:37:34 +10:00
Petru Conduraru 21ae78fbc2 resource.assign_resource: fix typo in duplicate guard #8203
The guard that avoids re-assigning the same object to the same resource
tested is_a("IfclRelAssignsToResource") (stray "l"), so it never matched.
A repeat assignment therefore fell through and appended the related object
to RelatedObjects a second time. Corrected to "IfcRelAssignsToResource".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:36:26 +10:00
Petru Conduraru 0a8ae14789 fix(ifcdiff): check attributes by default so PredefinedType changes are caught (#8214)
IfcDiff defaulted to relationships=["geometry"], so a plain diff only ever
compared geometry. Attribute-only edits on an element that kept its GlobalId
(a modified or removed PredefinedType, a renamed element, etc.) were silently
missed. The CLI made this worse: --relationships did not list "attributes" or
"geometry" as valid values, so there was no documented way to enable it.

The default is now ["attributes", "geometry"], so a plain `ifcdiff old new`
reports attribute changes alongside geometry changes. The CLI help and the
IfcDiff docstring now document all valid relationship values.

Added a regression test covering a PredefinedType change detected with the
default configuration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-12 22:29:31 +10:00
Petru Conduraru 2eea7728d2 fix(selector): round() should not crash on non-numeric values (#6776)
FormatTransformer.round() called Decimal() directly on the input value,
which raises decimal.InvalidOperation when the value is a non-numeric
string (a text property, or a value carrying a unit suffix like "12.5 m").
In a spreadsheet export this crashed the entire operation as soon as one
element carried such a value.

Now round() catches InvalidOperation and returns the value unchanged, the
same graceful-fallback convention used by add(). Numeric rounding is
unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-12 22:28:38 +10:00
Petru Conduraru 6b3cc54afc ifcfm: convert COBie Coordinate space points to project units (#5926)
In the cobie24 Coordinate sheet, Floor rows use get_local_placement, whose values
are in the project length unit, but Space rows come from ifcopenshell.geom
create_shape, whose vertices are in SI metres, and the space branch never scaled
them back. So on a non metre model (for example millimetres) the Coordinate sheet
mixed units a thousandfold apart and disagreed with the Facility sheet's declared
LinearUnits.

Scale the space bounding box by the project unit scale so the whole Coordinate
sheet is consistent. A metre model is unchanged since the scale is 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:27:07 +10:00
Petru Conduraru 5c11946470 Support block comments in selector filter syntax (#5023)
The filter_elements selector grammar had no way to comment out part of a
query, so users had to delete and retype text to temporarily toggle a
facet. Add a /* ... */ block comment terminal that is ignored by the
lexer, and tolerate a trailing "+" so that commenting out the final
operand (e.g. "IfcWall + /* IfcSlab */") parses cleanly. Comments may
span multiple lines; a /* sequence inside a quoted string is not treated
as a comment. Only the filter grammar is affected, not get_element or
format which use "/" for regex and division.

Adds a regression test and documents the syntax.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:23:30 +10:00
Ryan Schultz 0b7e25a3ef Docs: clarify immediate vs. any-depth spatial selectors
The location and parent filters both match at any depth in the spatial
hierarchy, which surprises users who want only the elements immediately
under a given container. Document that the parent query key resolves the
direct parent only (e.g. query:"parent.Name"="My Site"), add a matching
filter example, and note the immediacy on the parent value key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 16:44:22 -05:00
Ryan Schultz d16c283aef Add bulk-load of selected drawings' annotations (#8525)
SHIFT+CTRL+CLICK on Activate Drawing now imports the
annotations of all selected drawings without switching
the active view or camera, then selects their cameras with
the first as active. SHIFT+CTRL+ALT+CLICK also selects the
loaded annotation objects. The drawing camera is imported
when missing so annotations land in the correct collection.
Loading is idempotent.

Generated with the assistance of an AI coding tool.
2026-07-11 15:54:20 -05:00
Petru Conduraru a0f493b471 IfcConvert: report an error when the output file cannot be opened (#438)
Converting to a path whose directory does not exist (or is not writable)
failed silently: the serializer's ready() check correctly returned false,
but IfcConvert deleted the temp file and returned EXIT_FAILURE without any
message, so the user saw no reason for the failure.

Log a SYS error naming the output file before returning, matching the
existing "Unable to open output file" reporting used elsewhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:28:51 +02:00
Petru Conduraru e389939092 serializers: expand IfcPropertySetDefinitionSet in XML output (#6330)
Property sets contained in an IfcPropertySetDefinitionSet were exported as
an empty element in XML. The XmlSerializer already had a block to expand
such a set into its member property sets, but it was gated behind
#ifdef SCHEMAS_HAS_IfcPropertySetDefinitionSet while the schema generator
emits SCHEMA_HAS_IfcPropertySetDefinitionSet (singular). The plural spelling
is defined nowhere, so the block was dead code and a RelatingPropertyDefinition
holding a set produced nothing.

Correct the macro name so the set is expanded and its property sets are
serialized. The parse layer already reads these nested sets (they are
reachable from util.element), so this only completes the XML path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:26:49 +02:00
Petru Conduraru 380675e214 ifcparse: strip XML-illegal control characters in escape_xml (#2043, #3074)
escape_xml escaped the five XML metacharacters but passed control
characters (0x00 to 0x1F other than tab, newline and carriage return)
through unchanged. Those bytes are illegal in XML 1.0 and cannot be
represented even as numeric character references, so any IFC string
containing them produced non-well-formed XML and SVG output.

Strip those illegal control characters before escaping. Bytes belonging to
a valid UTF-8 multibyte sequence are always >= 0x80, so filtering on the low
control range leaves real text intact. This is the shared helper used by the
SVG serializer text and attribute sites (audited: all route through it) and
by the XML/Collada paths, so both reports are resolved at one place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:20:47 +02:00
Petru Conduraru 3e55c5126c ifcgeom: honour PnIndex in triangulated and polygonal face sets (#3434)
IfcTriangulatedFaceSet and IfcPolygonalFaceSet used CoordIndex values to
index Coordinates.CoordList directly, ignoring the optional PnIndex
attribute. When PnIndex is present it remaps point references, so a
CoordIndex value i must resolve as CoordList[PnIndex[i-1]-1] (both 1-based).
Without the indirection any model carrying a PnIndex was built from the wrong
points.

Add a resolve() helper in both mappings that applies the PnIndex indirection
when present and is a plain bounds-checked lookup otherwise, with bounds
checks at both index levels. When PnIndex is absent the behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:19:06 +02:00
Petru Conduraru 7e3d2f936d build: do not request the header-only Boost.System component (build against Boost 1.70+)
Boost.System has been header-only since Boost 1.69 and its compiled stub
library was removed in newer Boost, so listing system in the requested
find_package components makes configuration fail on Boost 1.70 and up (for
example Boost 1.90 errors with "Could not find boost_system"). Boost.System
is still pulled in transitively by thread / iostreams where it is needed, so
drop it from the explicit component list.

Verified: with this change IfcOpenShell configures and builds IfcConvert
cleanly against Homebrew Boost 1.90 and OpenCASCADE 7.9.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:16:35 +02:00
Petru Conduraru 0d70812641 Make CGAL circle-segments 0-default deflection-driven (rework #8368)
Address maintainer request on #8368: instead of a deflection floor on top
of a fixed CircleSegments count, use one mode or the other. When
CircleSegments == 0 (the new default) the CGAL kernel derives the conic
segment count from MesherLinearDeflection, matching the deflection based
meshing OpenCascade already does and fixing #8051. When CircleSegments is
non zero it is used directly as a fixed, radius independent count.

CircleSegments is only read by the CGAL kernel; OpenCascade meshes by
deflection and never reads it, so the new default has no effect there.

Update the setting description and the ifcconvert / geometry-settings docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:12:53 +02:00
Petru Conduraru dd9fa65629 Fix cgal kernel under-tessellating large-radius arcs (#8051)
The CGAL kernels (cgal and cgal-simple) allocate arc segments as a
fraction of the full circle via CircleSegments, ignoring the radius.
A large-radius arc that spans a small angle therefore collapsed to a
single chord, turning curved curtain-wall mullions straight while the
OpenCascade kernel (which meshes by deflection) kept them curved.

evaluate_conic now also enforces a deflection-based floor on the number
of segments, keeping the chord deviation within mesher-linear-deflection,
matching OpenCascade. Small circles are unchanged (CircleSegments floor
still dominates); only large-radius curves get denser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:12:53 +02:00
Petru Conduraru eb7324e7fc IfcConvert: add --fail-on-error to exit non-zero when conversion logs errors (#1118)
IfcConvert returned a success exit code even when geometry conversion logged
errors and silently dropped elements (for example a failed TopoDS::Shell build
under layerset slicing produced valid looking output with most objects
missing), so CI and scripts could not detect a partial conversion.

Add an opt-in --fail-on-error flag that makes IfcConvert exit non-zero when any
error was logged during processing, reusing the existing MaxSeverity based
failure check already used for --validate. The default exit behaviour is
unchanged, so pipelines that tolerate individual element failures are
unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:10:03 +02:00
Petru Conduraru 061bb90d50 Warn when a face inner boundary intersects another boundary (#527)
A face whose inner boundary crosses the outer boundary (or another inner
boundary) is invalid per the schema. Open Cascade silently heals or drops
such a face, so the intended hole is lost or the face is corrupted with no
diagnostic at all (the 2018 report saw a dropped face; on the current line
the face survives as wrong geometry, still silently).

After the wires are collected, if a face has inner boundaries, measure the
BRepExtrema distance between each inner wire and every earlier wire. Two
non intersecting loops have strictly positive distance, so a distance at
or below the modelling precision means the boundaries touch or cross; emit
a warning (GEO 402) naming the offending face. This is diagnostic only, no
geometry change.

The message is emitted via the kernel logger() rather than Logger::Root():
IfcConvert configures a local Logger and worker logs merge into it, while
Logger::Root() is a separate unconfigured singleton whose messages are
discarded (a latent issue affecting some existing GEO messages too).

Verified on OCC 7.9.2 with synthesized IFC4 faces: an inner triangle
crossing the outer edge, and one straddling the bottom edge, each emit one
GEO 402; a valid 4x4 hole emits none and triangulates identically (area
84.0), in both sequential and multithreaded runs. Pure inner self
intersection and full containment are distinct classes and intentionally
left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 14:39:03 +02:00
Petru Conduraru a8d0ef3437 Add AI-generated marker to IfcAsymmetricIShapeProfileDef.cpp
Comply with AGENTS.md: new AI-generated files must carry a top-of-file
comment indicating AI assistance.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 14:24:10 +02:00
Petru Conduraru 438c0955f2 Map IfcAsymmetricIShapeProfileDef standalone in IFC4+ (#1367)
In IFC2X3 IfcAsymmetricIShapeProfileDef is a subtype of
IfcIShapeProfileDef, so the IfcIShapeProfileDef mapping dispatched it by
inheritance. From IFC4 onwards it is a standalone subtype of
IfcParameterizedProfileDef, so nothing mapped it and the extruded solid
came out empty (GEO326, 0 verts).

Add a dedicated map_impl that builds the twelve-point asymmetric section
(independent bottom/top flange widths, thicknesses, fillet/edge radii and
flange slopes), plus a guarded BIND. Both are wrapped in
SCHEMA_IfcAsymmetricIShapeProfileDef_HAS_BottomFlangeWidth, which is only
defined where the type is standalone, so IFC2X3 keeps its existing
subtype route unchanged.

Verified on OCC 7.9.2: an IFC4 asymmetric extrusion goes from 0 verts to
a correct 72-vert solid (bottom flange wider than top); IFC2X3 output is
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 14:24:10 +02:00
Stephen Boddy b9deb9c63d Git ignores CLAUDE.local.md file
This allows a file that will be automatically picked up by Claude. It can either
be a copy of a CLAUDE.md, or a one line file pointing to a shared common file. i.e.

@~/.claude/conventions-ifcopenshell.md
2026-07-11 13:10:14 +01:00
sboddy e14b3ec8a0 Merge pull request #8243 from sboddy/feature-5753-autosave
Feature #5753 - Autosave for ifc files

Merging because it could be a life saver. It is hidden behind an option and is off by default.

- Provides the option have an autosave file created periodically (duration in prefs).
- Can be set to save immediately or a dialog prompt to save, but can be dismissed.
- Removes the autosave when Blender quits cleanly.
- If the autosave file exists at startup, it will prompt which file to load.

_Every_ AI had a hand in this, but I have reviewed, understood and tested it. AI Credits go to:
Cursor, Grok, Copilot, and Claude.
2026-07-11 11:10:59 +01:00
Stephen Boddy c0d2c2ea24 Fix upstream ci-lint failures on this branch
- autosave.py: black formatting (blank line) and ruff's
  collections.abc.Callable import fix.
- project/__init__.py, tool/__init__.py: ruff import-sort fixes. The
  autosave import in tool/__init__.py is deliberately kept last (must
  come after tool.drawing, per its existing comment) via `# isort: skip`
  rather than letting ruff move it, which would reintroduce that bug.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 10:50:00 +01:00
Stephen Boddy 0ce6e94352 Make autosave recovery prompt properly modal
The recovery popup used invoke_popup, which is dismissed the instant
the mouse leaves its bounds - closing the prompt without loading
either file, and with no visible feedback that anything happened.

Switches to invoke_props_dialog, which blocks the rest of the UI and
is only dismissed by an explicit action. Since Blender always renders
both a fixed "Cancel" button and one labelled by confirm_text on that
dialog type, the prompt is reframed as a direct Yes/Cancel question
("Do you want to load the autosaved version instead?") instead of
adding separate Load Original/Load Autosave buttons on top of those.

Folds the load logic directly into the popup's execute()/cancel(), so
the now-redundant LoadAutosavedRecovery operator is removed.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 10:50:00 +01:00
Stephen Boddy be55400ec6 Remove stale autosave file on clean Blender quit
Previously the autosaved copy was only ever overwritten, never removed,
so a deliberate quit (whether the user saved or chose "don't save")
still nagged with a recovery prompt on next startup.

Registers an atexit cleanup that removes the active IFC's autosave
file(s) on a graceful interpreter shutdown. atexit never runs on an
actual crash, so a genuine crash still leaves the recovery file in
place as before.

The cleanup reads a cached plain-string path kept up to date by
reset_timer(), rather than looking it up live via bpy.context - by
the time atexit fires, Blender's C++ side is torn down far enough
that even a read-only bpy.context.scene access aborts the process
(std::bad_optional_access) instead of raising a catchable exception.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 10:50:00 +01:00
Stephen Boddy 6f1737bb58 Feature #5753 - Autosave for ifc files
Implemented as described in #5753, with two options:
- A nag dialog with save or cancel options.
- An autosaved file.

Settings are in preference to activate the feature (default: off), the period before prompting/saving,
and choosing between the two methods.

Prevent the autosave file being added to the recent files list when the user opens the original, but selects to open the autosaved version.

black/ruff

This commit was created using AI assistance. Cursor for the initial code, then Grok and I fixing all the errors
that Cursor made. Finally Copilot did a code review.

I have reviewed and tested the code, and I understand it, and it works and does not introduce any obvious bugs.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Grok
Co-authored-by: Cursor
2026-07-11 10:39:48 +01:00
154 changed files with 3072 additions and 1247 deletions
+2 -3
View File
@@ -109,7 +109,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender.
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.1/blender-5.1.0-linux-x64.tar.xz
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.2/blender-5.2.0-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
@@ -179,8 +179,7 @@ jobs:
blender --online-mode --command extension install --enable --sync sun_position
cd IfcOpenShell/src/bonsai
pip install pytest-blender
pip install pytest-bdd
pip install -r requirements-dev.txt
blender --background --python scripts/setup_pytest.py
blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background
make test
+17 -11
View File
@@ -27,10 +27,7 @@ jobs:
- name: Install dependencies
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install ruff
uv tool install black
uv tool install poethepoet
uv tool install ty==0.0.34
cat requirements-tools.txt | xargs -L1 uv tool install
# black doesn't catch all syntax errors, so we check them explicitly.
- name: Check syntax errors
@@ -58,11 +55,17 @@ jobs:
black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py
continue-on-error: true
- name: ty check
id: ty
run: |
poe ty-venv
poe ty
- name: ty check (venv setup)
run: poe ty-venv
- name: ty check (bonsai)
id: ty-bonsai
run: poe ty-bonsai
continue-on-error: true
- name: ty check (ios)
id: ty-ios
run: poe ty-ios
continue-on-error: true
- name: Ruff check
@@ -112,7 +115,10 @@ jobs:
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
echo "::error::Ruff check failed, see Summary or 'ruff' step for the details." && ERROR=1
fi
if [ "${{ steps.ty.outcome }}" != "success" ]; then
echo "::error::ty check failed, see 'ty check' step for the details." && ERROR=1
if [ "${{ steps.ty-bonsai.outcome }}" != "success" ]; then
echo "::error::ty check (bonsai) failed, see 'ty check (bonsai)' step for the details." && ERROR=1
fi
if [ "${{ steps.ty-ios.outcome }}" != "success" ]; then
echo "::error::ty check (ios) failed, see 'ty check (ios)' step for the details." && ERROR=1
fi
exit $ERROR
+4
View File
@@ -4,6 +4,8 @@
/_deps-vs*-x*-installed/
/_installed-vs*-x*/
/build/
/build.log
/output/
/src/examples/build/
# ifctester docs output
/src/ifctester/test/build/
@@ -22,6 +24,7 @@
__pycache__
*.py.bak
venv
uv.lock
# Visual Studio Code files
.vscode
@@ -127,6 +130,7 @@ src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
# temp files from AI coding tools
*.claude
CLAUDE.local.md
*.py.tmp*
*.json.tmp*
+8 -4
View File
@@ -314,8 +314,12 @@ if(WASM_BUILD)
else()
# @todo review this, shouldn't this be all possible header-only now?
# ... or rewritten using C++17 features?
# Boost.System has been header-only since 1.69 and its compiled stub library
# was dropped in newer Boost, so requesting it as a component makes
# find_package fail on Boost 1.70 and up (for example Boost 1.90). It is
# still pulled in transitively by thread / iostreams where needed, so do not
# request it explicitly.
set(BOOST_COMPONENTS
system
program_options
regex
thread
@@ -559,8 +563,8 @@ if(COMPILE_SCHEMA)
# Bootstrap the parser
message(STATUS "Compiling schema, this will take a while...")
execute_process(
COMMAND ${PYTHON_EXECUTABLE} bootstrap.py express.bnf
WORKING_DIRECTORY ../src/ifcexpressparser
COMMAND ${PYTHON_EXECUTABLE} bootstrap.py
WORKING_DIRECTORY ../src/ifcopenshell-python/ifcopenshell/express
OUTPUT_FILE express_parser.py
RESULT_VARIABLE SUCCESS
)
@@ -571,7 +575,7 @@ if(COMPILE_SCHEMA)
# Generate code
execute_process(
COMMAND ${PYTHON_EXECUTABLE} ../ifcexpressparser/express_parser.py ../../${COMPILE_SCHEMA}
COMMAND ${PYTHON_EXECUTABLE} ../ifcopenshell-python/ifcopenshell/express/express_parser.py ../../${COMPILE_SCHEMA}
WORKING_DIRECTORY ../src/ifcparse
OUTPUT_VARIABLE COMPILED_SCHEMA_NAME
)
+3
View File
@@ -0,0 +1,3 @@
.env
*.pyc
__pycache__
+3
View File
@@ -0,0 +1,3 @@
.env
*.pyc
__pycache__
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# .ifcos_env
# register autocompletes. just source the file in your shell, i.e.
# source .ifcos_env
.ifcos_env() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="create update up down restart build attach logs ps config remove help"
# Basic static completion
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
# Register the completion for the command "ifcos_env"
complete -F .ifcos_env ./ifcos_env
+67
View File
@@ -0,0 +1,67 @@
FROM rockylinux:9
# Update system, enable CRB (needed by some EPEL packages) and install EPEL,
# then install required packages + some common tools for a bit of command
# line comfort. Combined into one layer so a later `create` always installs
# against packages from the same dnf update, rather than layering fresh
# installs on top of a stale cached "update" layer.
RUN dnf update -y && \
dnf install -y epel-release && \
dnf config-manager --set-enabled crb && \
dnf install -y --allowerasing --setopt=install_weak_deps=False --setopt=tsflags=nodocs \
bash-completion vim git curl wget which tree htop sudo \
gcc gcc-c++ autoconf automake bison make zip cmake \
python3 python3-pip \
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libuuid-devel git-lfs \
findutils xz byacc ccache && \
git lfs install --system && \
dnf clean all && \
rm -rf /var/cache/dnf
# Trust bind-mounted repos regardless of which user (root or builder) or host
# UID owns them, rather than a per-user config that only one of them sees.
RUN git config --system --add safe.directory '*'
# Configure ccache. CCACHE_MAXSIZE (not `ccache -M`) because /ccache is a
# volume mount point at runtime - anything `ccache -M` writes to a config
# file under it during this build gets shadowed once the real volume is
# mounted, so the size cap only actually takes effect via the env var.
# 2G is generous: a full build (IfcParse+IfcGeom+IfcConvert+wrapper, one
# Python version) measures ~300MB, and the volume is now shared across all
# checkouts (see compose.yaml), so this covers several diverging branches.
ENV CCACHE_DIR=/ccache
ENV CCACHE_MAXSIZE=2G
ENV PATH="/usr/lib/ccache:$PATH"
# Non-root user matching the host UID/GID that bind-mounts the repo (default
# 1000:1000, the common single-user-Linux-box case), so files the build
# creates under the mount keep sane, non-root ownership on the host side.
# Override with --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g)
# if your host user has a different UID/GID.
ARG USER_UID=1000
ARG USER_GID=1000
# groupadd fails outright if USER_GID is already taken by an existing
# system group - which happens whenever a host's primary GID collides with
# one baked into the rockylinux9 base image. The main real-world case is
# macOS, where the default user's primary group is "staff" at GID 20, and
# GID 20 is "games" on RHEL-family images. Only create the "builder" group
# when that GID is actually free; otherwise useradd just attaches to
# whichever group already owns it. Either way the builder user ends up
# with the right GID for bind-mount ownership, which is all that matters.
RUN (getent group "${USER_GID}" >/dev/null || groupadd -g "${USER_GID}" builder) \
&& useradd -m -u "${USER_UID}" -g "${USER_GID}" -s /bin/bash builder \
&& echo "builder ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/builder
# Copied while still root: /bin is not writable by the builder user.
COPY --from=ghcr.io/astral-sh/uv:0.11.27 /uv /uvx /bin/
USER builder
WORKDIR /__w/IfcOpenShell/IfcOpenShell
# Installed as builder so managed Python interpreters land under builder's
# $HOME, matching the user that actually runs the build.
RUN uv python install
CMD ["sleep", "infinity"]
+78
View File
@@ -0,0 +1,78 @@
Docker build environment
========================
This is a small utility to make it easy to compile a perfect `_ifcopenshell_wrapper.cpython-*-x86_64-linux-gnu.so`
files.
The reason for this tool is that I was trying to follow the web page directions, and my build was behaving differently
to the release builds. Eventually I concluded that the differences between toolchains on the RHEL based rocky9 image
and Ubuntu were just too great. Getting the build setup was already a lot of trial and error, so I thought I'd spend
more time trying to reuse the github actions that perform the build, using a utility called `act`. I learnt a lot, in
particular how much time, energy, and bandwidth Github waste. I also realised I was most of the way to a regular docker
setup anyway, so I might as well just do that. So I've deconstructed all the github action steps, and turned it into
a local docker build environment that uses the exact same base, tools, libraries, and build command/flags etc.
Right now a Github action will:
- launch the rocky9 base
- upgrade all the packages
- install a bunch of extra tools
- do a recursive checkout of your repo
- checkout the build repository
- unpack dependencies
- run the build script, making all python versions (5? right now I think)
- create the .zip release files
And it does _all_ of that _every_ time. This is not a fault of the action writers - it's just how Github seems to work.
These dockers tools do the following differently, and it's actually a bit more powerful too:
- build the base image once.
- update the packages once.
- install the extra tools once.
- the repository is the one on your host, that gets bind mounted in the container as the working directory.
- by adding an environment variable to .env, restricts to compiling for just a single python version.
- when the build is finished the created files are right there under your local repositry (but not added to git) for
ease of access
- each repository can have it's own build environment container.
- the image is shared between those environments.
- the containers share the ccache, so additional envs should get a helping hand.
- it has a simple set of user friendly commands to drive it all.
For example:
``` bash
# To see the commands (a superset of docker compose commands)
./ifcos_env
# Enable autocomplete of commands
source .ifcos_env
# First time commands
./ifcos_env create
./ifcos_env up
./ifcos_env build
# install and test library
# find an issue
# edit code
./ifcos_env build
# and so on. When done stop and optionally delete the container
./ifcos_env stop
./ifcos_env remove
```
To limit the build to one python version just add
``` bash
PY_TGT=py-311
```
or whichever version your Blender requires.
You might see UNIQUE_ID in the .env file too. This keeps containers for separate folders, separate.
System requirements
1. Linux-x64 only at this time.
2. Docker and docker-compose need to be installed.
3. Have a good amount of disk space. (image is in /var (typically the root partition) and will be about 1.7 GB)
4. The build action will create about 10GB in your repository folder. Make sure this partition is spacious
particularly if you intent on having multiple clones building.
5. ... I think that covers most of it.
+186
View File
@@ -0,0 +1,186 @@
---
name: ifcopenshell-docker-build
description: >-
Build a real ifcopenshell_wrapper (.so + .py) and IfcConvert locally via
the docker/ifcos_env toolchain, then wire them into a checkout for
running C++-dependent parts of the test suite (geometry, the SWIG
wrapper stub, the C++ parser). Use whenever a task needs to compile
IfcOpenShell's C++ core rather than just read/patch source - e.g.
reproducing or fixing a bug in src/ifcgeom, src/ifcparse, src/ifcwrap,
or validating util/scripts/validate_stub.py against the actual
generated wrapper.
---
# Building IfcOpenShell locally with docker/ifcos_env
`docker/` mirrors the project's GitHub Actions build environment locally,
in a persistent, non-root container with ccache so repeat builds are fast.
See `docker/README.md` for the design rationale. Pure-Python changes don't
need any of this - only reach for it when you need a real compiled
`_ifcopenshell_wrapper*.so` or `IfcConvert` binary.
## Placement
This `docker/` folder must live as a direct child of the repo root you want
to build (sibling of `src/`, `cmake/`, etc.) - `compose.yaml` and
`ifcos_env` resolve the repo via `../` relative to wherever `docker/`
itself sits, and bind-mount it into the container. If you're setting this
up in a fresh clone, copy the whole `docker/` directory there first.
## Setup
```bash
cd docker
./ifcos_env create # build the image (shared by name across all your clones/checkouts, so usually instant after the first time anywhere)
./ifcos_env up # create + start the container, clone/unpack the third-party dependency cache (~10GB, one-time per container)
./ifcos_env build # full build: all deps + IfcParse + IfcGeom + IfcConvert + the Python wrapper, for one Python version
```
`PY_TGT` and `UNIQUE_ID` live in `docker/.env` - `PY_TGT` (e.g. `py-311`)
restricts the build to one Python version instead of building five;
`UNIQUE_ID` is a hash of the folder path, recalculated on every `up`, so
each checkout gets its own container/volumes automatically.
A full first build takes ~1.5 hours (mostly compiling IfcOpenShell's own
C++, not the cached third-party deps). After that, ccache makes incremental
rebuilds of a couple of touched `.cpp` files **under a minute**.
## Container lifecycle
The container is long-lived (`sleep infinity`) so exec'd commands and
ccache state persist between builds. Commands map directly onto Docker
Compose's own container-vs-image distinction:
```bash
./ifcos_env up # create the container if it doesn't exist, then start it (runs ready_repo too)
./ifcos_env stop # stop the container, keep it around
./ifcos_env start # start it back up (same container, same filesystem layer)
./ifcos_env restart # stop, then start
./ifcos_env down # remove the container (and its network) entirely
./ifcos_env recreate # down, then up - a fresh container
```
Named volumes (`ccache`) and the bind-mounted repo/`build/` are unaffected
by `down`/`recreate` - only the container itself goes away, and `up`
recreates it from the image.
## Fast iteration
Pass a target to `build` to skip the parts you don't need:
```bash
./ifcos_env build IfcConvert # only the executables (IfcConvert, IfcGeomServer) - skips the Python wrapper entirely
./ifcos_env build IfcOpenShell-Python # only the SWIG Python wrapper - skips executables entirely
./ifcos_env build # no target = everything (needed the first time, or after touching shared headers)
```
Use this to keep the edit -> rebuild -> test loop fast when debugging: if
you're only touching `src/ifcgeom/`, build `IfcConvert`; if you're only
exercising the Python API, build `IfcOpenShell-Python`.
## Where the artifacts land
Build output goes to `<repo_root>/build/Linux/x86_64/install/` on the host
(bind-mounted, not just inside the container), owned by you (see
"Container user" below):
- `ifcopenshell/bin/IfcConvert` - the CLI binary
- `python-<version>/lib/python<X.Y>/site-packages/ifcopenshell/_ifcopenshell_wrapper*.so`
and `ifcopenshell_wrapper.py` - the compiled wrapper + its generated
Python glue
## Testing against a checkout (automated / AI-driven)
`_ifcopenshell_wrapper*.so` and `ifcopenshell_wrapper.py` are already
gitignored under `src/ifcopenshell-python/ifcopenshell/`, which is exactly
where a normal in-tree build would put them - copy the two files there:
```bash
SRC=build/Linux/x86_64/install/python-3.11.8/lib/python3.11/site-packages/ifcopenshell
cp "$SRC/_ifcopenshell_wrapper.cpython-311-x86_64-linux-gnu.so" src/ifcopenshell-python/ifcopenshell/
cp "$SRC/ifcopenshell_wrapper.py" src/ifcopenshell-python/ifcopenshell/
```
Then, to run the test suite against it:
```bash
export PATH="$PWD/build/Linux/x86_64/install/ifcopenshell/bin:$PATH" # for IfcConvert-dependent tests
cd src/ifcopenshell-python/test
PYTHONPATH="$PWD/.." python3.11 -m pytest -p no:pytest-blender .
```
(`-p no:pytest-blender` avoids the pytest-blender plugin trying to find a
`blender` executable and failing collection entirely, even for non-Blender
tests.) You'll need the matching Python version's `pip install`s too
(numpy, shapely, isodate, lark, tabulate, pytest, ... - whatever the
modules under test import) since this is a bare interpreter, not the
project's pixi env.
**This is the pattern to use for automated or AI-driven verification.**
Don't use `try` (below) for that - it overwrites files in a real, live
Blender installation, which isn't something an automated/AI workflow
should ever do without the human explicitly asking for it in the moment.
## Testing in Blender itself (human only)
`try` copies the built wrapper straight into your actual Blender/Bonsai
extension install, for manual in-Blender testing:
```bash
./ifcos_env try
```
It reads `BLENDER_USER_RESOURCE` from `.env` - set this to wherever
Blender's user resource folder for the Bonsai extension actually lives on
your system, which depends on your own Blender setup:
```bash
# in docker/.env
BLENDER_USER_RESOURCE=~/.config/blender/bonsai/
```
`try` figures out the built Python version from `build/.../install/`
(disambiguating with `PY_TGT` if more than one version was built) and
copies the wrapper to
`$BLENDER_USER_RESOURCE/extensions/.local/lib/python<X.Y>/site-packages/ifcopenshell/`.
## Container user
The image runs as a non-root `builder` user, UID/GID matching your host
account (passed as `--build-arg` by `create` from `id -u`/`id -g`, so it
adjusts automatically - no manual flag needed even if you're not 1000:1000).
Files the build creates under the bind mount come out owned by you, not
root. Passwordless `sudo` is available inside the container (e.g. via
`attach`) for the rare case you need root for something ad hoc.
If you're picking up an existing checkout that was previously built with
an older, root-based image, you may hit `Permission denied` the first time
you run `up`/`build` under the new image - `build/`, `.git/modules/`, the
`ccache` volume, `output/`, and `build.log` can all be left root-owned from
before. Fix it once via the container's own root (no host `sudo` needed):
```bash
docker exec -u root -w /__w/IfcOpenShell/IfcOpenShell <container-name> \
chown -R "$(id -u)":"$(id -g)" .git/modules build output build.log /ccache
```
(`<container-name>` is `ifcopenshell-<UNIQUE_ID>` - see `docker ps -a`.)
## Other things worth knowing
- **Linux x64 only.** `compose.yaml` pins `platform: linux/amd64`; on an
ARM host (e.g. Apple Silicon) this build isn't available.
- **The final "Package .zip archives" step of `build()` has a pre-existing
bash syntax error**, unrelated to compilation - the actual build already
succeeded by that point (look for `Built IfcOpenShell...` in the output),
so this is safe to ignore if you only need the raw artifacts under
`build/.../install/`, not packaged release zips.
- **`test_mmaped_stream` and similar `USE_MMAP`-dependent tests will fail**
against this build - `nix/build-all.py` is invoked with `USE_MMAP=OFF`
here. Not a bug in your code if you see it fail.
- Only the bind-mounted `<repo>/build` lives on the host filesystem your
repo is checked out on. Anything the container writes *outside* that
mount lives in the container's own writable layer under Docker's data
root (commonly `/var/lib/docker`, i.e. usually your root partition) -
keep an eye on `df -h /` if you're running several of these containers
at once.
+15
View File
@@ -0,0 +1,15 @@
name: ifcopenshell-${UNIQUE_ID}
services:
ifcopenshell:
container_name: ifcopenshell-${UNIQUE_ID}
image: ifcopenshell-build-env:updated
platform: linux/amd64
volumes:
- type: bind
source: ../
target: /__w/IfcOpenShell/IfcOpenShell
- ccache:/ccache
volumes:
ccache:
name: ifcopenshell-ccache-shared
+339
View File
@@ -0,0 +1,339 @@
#!/bin/bash
# ================== CONFIG ==================
SCRIPT_NAME=$(basename "$0")
ENV_FILE=".env"
WORKDIR="/__w/IfcOpenShell/IfcOpenShell"
NAMEPREFIX=ifcopenshell
function set_env() {
# Load .env file if it exists
if [[ -f "$ENV_FILE" ]]; then
set -a
source "$ENV_FILE"
set +a
echo "✅ Loaded environment variables from $ENV_FILE"
else
echo "⚠️ No $ENV_FILE found, proceeding without it."
fi
}
set_env
# ================ FUNCTIONS =================
function create() {
echo "⭐ Creating image: ifcopenshell-build-env"
docker build -f Dockerfile \
--build-arg USER_UID="$(id -u)" --build-arg USER_GID="$(id -g)" \
-t ifcopenshell-build-env:updated .
}
function update() {
# The Dockerfile always builds FROM a clean rockylinux:9 and does
# `dnf update -y` as its first step, so re-running create() is enough
# to get fresh packages.
echo "⚡ Updating image: ifcopenshell-build-env"
create
}
function up() {
# Creates the container if it doesn't exist yet (and starts it either
# way) - this is the one that needs ready_repo, since a freshly created
# container has no submodules/dependency cache in place yet.
echo "🚀 Creating/starting stack: ifcopenshell-${UNIQUE_ID}"
unique # Update UNIQUE_ID first
docker compose up -d "$@" # Container must exist before ready_repo can exec into it.
ready_repo # Ensure repo is recursive, and the build repo is in place.
}
function down() {
# Removes the container (and its network) entirely. Named volumes
# (ccache) and the bind-mounted repo/build/ survive; up() will recreate
# the container from scratch next time.
echo "🔥 Removing stack: ifcopenshell-${UNIQUE_ID}"
docker compose down "$@"
}
function stop() {
# Stops the existing container without removing it - the container,
# its filesystem layer, and its exec history all remain intact.
echo "🛑 Stopping stack: ifcopenshell-${UNIQUE_ID}"
docker compose stop "$@"
}
function start() {
# Starts a previously-stopped container back up. Does nothing (and
# won't create anything) if the container doesn't exist - use up() for
# that.
echo "▶️ Starting stack: ifcopenshell-${UNIQUE_ID}"
docker compose start "$@"
}
function restart() {
echo "🔄 Restarting stack (stop, then start)..."
stop
start
}
function recreate() {
echo "♻️ Recreating stack (down, then up)..."
down
up
}
function logs() {
echo "📜 Showing logs..."
docker compose logs -f "$@"
}
function ps() {
docker compose ps
}
function config() {
echo "🔍 Validated compose configuration:"
docker compose config
}
function remove() {
# Lower-level than down(): removes already-stopped containers without
# touching the compose network. Mostly useful after a plain stop().
echo "🗑️ Removing stopped containers: ifcopenshell-${UNIQUE_ID}"
docker compose rm "$@"
}
function unique() {
echo "🔧 Making stack name folder specific..."
REGEX="^UNIQUE_ID="
if [[ ! -f "$ENV_FILE" ]] || ! grep -qE "$REGEX" "$ENV_FILE"; then
echo -e "\nUNIQUE_ID=dummy\n" >> "$ENV_FILE"
fi
export UNIQUE_ID="$(pwd | sha256sum | cut -c -8)"
# `sed -i` takes incompatible syntax between GNU sed (Linux) and BSD sed
# (macOS) - `-si` is GNU-only and errors as "illegal option -- s" under
# BSD/macOS sed. Avoid -i altogether and do the in-place edit via a temp
# file + mv instead, which behaves identically with either sed.
local tmp_file
tmp_file="$(mktemp "${ENV_FILE}.XXXXXX")"
sed "s/^UNIQUE_ID=.*$/UNIQUE_ID=${UNIQUE_ID}/" "$ENV_FILE" > "$tmp_file"
mv "$tmp_file" "$ENV_FILE"
set_env
}
function ready_repo() {
echo "👍 Getting the repo ready to build..."
docker exec -i -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
set -euo pipefail # Recommended for robustness
git submodule update --init --recursive
if [[ ! -d "build" ]]; then
git clone -b rockylinux9-x64 https://github.com/IfcOpenShell/build-outputs.git build
else
cd build
git pull
cd ..
fi
if [[ ! -d "build/Linux/x86_64/install/boost-1.86.0/" ]]; then
cd build
uv run ../nix/cache_dependencies.py unpack
cd ..
fi
'
}
function build() {
echo "☕ Execute the build, go make yourself a cuppa... I'll be a while"
local BUILD_TARGET="$1"
docker exec -i -w "${WORKDIR}" -e PY_TGT="${PY_TGT}" -e BUILD_TARGET="${BUILD_TARGET}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
set -o pipefail
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release uv run ./nix/build-all.py -v ${PY_TGT:+-$PY_TGT} --diskcleanup ${BUILD_TARGET} 2>&1 | tee build.log
'
echo "🎒 Pack Dependencies"
docker exec -i -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
cd build
uv run ../nix/cache_dependencies.py pack
'
echo "🎁 Package .zip archives"
docker exec -i -w "${WORKDIR}" -e GITHUB_SHA="$(git rev-parse HEAD)" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
OUTPUT_DIR=${PWD}/output
VERSION=v`cat VERSION`
mkdir -p ${OUTPUT_DIR}
cd ./build/`uname`/*/install/ifcopenshell
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
numbers=`echo $py_version | grep -oE "[0-9]+\.[0-9]+" | tr -d "."`
py_version_major=python-${numbers}$postfix
pushd . > /dev/null
cd $py_version
if [ ! -d ifcopenshell ]; then
mkdir ../ifcopenshell_
mv * ../ifcopenshell_
mv ../ifcopenshell_ ifcopenshell
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell/*
mv *.zip ${OUTPUT_DIR}/
popd > /dev/null
done
cd bin
if compgen -G "./*.zip" > /dev/null; then
rm *.zip 2>&1 >/dev/null || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip $exe
done
mv *.zip ${OUTPUT_DIR}/
cd ..
'
}
function attach() {
echo "🔦 Connect to interactive shell"
docker exec -it -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" /bin/bash
}
function try() {
# Copies the freshly built wrapper into your actual Blender/Bonsai
# installation for manual, in-Blender testing. This is a human-only
# convenience: it overwrites files in your live Blender setup, so it's
# not something that should run unattended as part of an automated or
# AI-driven build/test loop (which should instead copy the wrapper into
# the repo's own src/ifcopenshell-python/ifcopenshell/ - see SKILL.md).
echo "🚴 Copying build artifacts into your Blender resource folder for testing"
if [[ -z "${BLENDER_USER_RESOURCE:-}" ]]; then
echo "❌ BLENDER_USER_RESOURCE is not set in .env."
echo " Add a line pointing at wherever Blender's user resource folder for"
echo " the Bonsai extension actually is on your system, e.g.:"
echo " BLENDER_USER_RESOURCE=~/.config/blender/bonsai/"
return 1
fi
# Normalise: expand a leading ~ (in case it was quoted in .env and so
# never went through shell tilde-expansion when set_env sourced it),
# then resolve to an absolute, symlink-free path.
local resource="${BLENDER_USER_RESOURCE/#\~/$HOME}"
resource="$(realpath -m "$resource")"
local install_dir="../build/Linux/x86_64/install"
local py_dirs=("$install_dir"/python-*)
if [[ ${#py_dirs[@]} -gt 1 && -n "${PY_TGT:-}" ]]; then
# PY_TGT is compact (py-311); the install dirs are dotted
# (python-3.11.8) - reinsert the dot (assumes a single-digit major
# version, true for the Python 3.x line) before matching.
local py_tgt_digits="${PY_TGT#py-}"
local py_tgt_dotted="${py_tgt_digits:0:1}.${py_tgt_digits:1}"
local filtered=() d
for d in "${py_dirs[@]}"; do
[[ "$(basename "$d")" == "python-${py_tgt_dotted}."* ]] && filtered+=("$d")
done
[[ ${#filtered[@]} -gt 0 ]] && py_dirs=("${filtered[@]}")
fi
if [[ ${#py_dirs[@]} -ne 1 || ! -d "${py_dirs[0]}" ]]; then
echo "❌ Expected exactly one built python-* dir under $install_dir, found ${#py_dirs[@]}."
echo " Run 'build' first, or set PY_TGT in .env to disambiguate a multi-version build."
return 1
fi
local py_minor
py_minor="$(basename "${py_dirs[0]}" | grep -oE '[0-9]+\.[0-9]+')"
local wrapper_dir="${py_dirs[0]}/lib/python${py_minor}/site-packages/ifcopenshell"
if [[ ! -f "$wrapper_dir/ifcopenshell_wrapper.py" ]]; then
echo "❌ Built wrapper not found at $wrapper_dir - run 'build' first."
return 1
fi
local target="$resource/extensions/.local/lib/python${py_minor}/site-packages/ifcopenshell"
mkdir -p "$target"
cp "$wrapper_dir"/_ifcopenshell_wrapper*.so "$target/"
cp "$wrapper_dir"/ifcopenshell_wrapper.py "$target/"
echo "✅ Copied wrapper into $target"
}
function clean() {
# Host-side only - doesn't touch the container, image, or ccache volume.
echo "💎 Clean the build and output folder up"
if [[ -d "../build" ]]; then
rm -rf ../build
fi
if [[ -d "../output" ]]; then
rm -rf ../output
fi
}
function help() {
cat <<EOF
Usage: ./$SCRIPT_NAME <command>
Available commands:
create Build the rocky9-based image
update Rebuild the image fresh, picking up OS package updates
up Create the container if it doesn't exist yet, and start it
down Remove the container entirely (docker compose down)
stop Stop the container without removing it
start Start a previously-stopped container
restart stop, then start (same container, no recreation)
recreate down, then up (fresh container)
build Execute the IfcOpenShell build
attach Connect to an interactive shell in the container
try Copy the built wrapper into your Blender resource folder
(human-only - see BLENDER_USER_RESOURCE below, and SKILL.md
for the AI/automated-testing equivalent)
clean Remove the build and output folders
logs Follow container logs
ps Show running containers
config Validate and show compose config
remove Remove stopped containers (docker compose rm)
help Show this help
Environment variables from .env are automatically loaded, including:
PY_TGT Restrict the build to one Python version, e.g. py-311
UNIQUE_ID Recalculated automatically on every 'up', don't set by hand
BLENDER_USER_RESOURCE Where 'try' copies the wrapper for manual testing, e.g.
~/.config/blender/bonsai/
EOF
}
# ================= MAIN =================
case "$1" in
create) create ;;
update) update ;;
up) up "${@:2}" ;;
down) down "${@:2}" ;;
stop) stop "${@:2}" ;;
start) start "${@:2}" ;;
restart) restart ;;
recreate) recreate ;;
build) build "${@:2}" ;;
attach) attach ;;
try) try ;;
clean) clean ;;
logs) logs "${@:2}" ;;
ps) ps ;;
config) config ;;
remove) remove ;;
help|-h|--help) help ;;
"")
echo "❌ No command provided."
help
;;
*)
echo "❌ Unknown command: $1"
echo "Type './$SCRIPT_NAME help' for available commands."
exit 1
;;
esac
+15 -5
View File
@@ -50,7 +50,7 @@ Used environment variables:
- ``NO_CLEAN`` - do not clean `ifcopenshell` build directories but continue working on current build
(installed dependencies are never cleared).
By default option is disabled, to enable pass any value from `1`, `on`, `true`.
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (IFC2X3; IFC4; IFC4X3_ADD2) - to be supplied as `2x3;4`
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (8 schemas), to be supplied as `2x3;4;4x3_add2`
- ``USE_OCCT`` - whether to use official Open CASCADE instead of Community Edition
(`true` by default, any other value is considered `false`)
- ``WASM_PYTHON_PATH`` - path to WASM Python installation,
@@ -155,7 +155,7 @@ MPFR_VERSION = "3.1.6" # latest is 4.1.0
CGAL_VERSION = "v5.6.3"
USD_VERSION = "23.05"
TBB_VERSION = "2021.9.0"
ROCKSDB_VERSION = "9.11.2"
ROCKSDB_VERSION = "10.4.2"
ZSTD_VERSION = "1.5.7"
# binaries
cp = "cp"
@@ -627,9 +627,10 @@ def build_dependency(
build_tool_args: "list[str]",
download_url: str,
download_name: str,
*,
download_tool: Literal["py", "git"] = download_tool_default,
revision: "Union[str, None]" = None,
patch: "Union[str, list[str], None]" = None,
patch: list[str] | None = None,
shell=None,
pre_compile_subs: "Sequence[tuple[str, str, str]]" = (),
additional_files: "Union[dict[str, str], None]" = None,
@@ -714,8 +715,6 @@ def build_dependency(
urlretrieve(url, os.path.join(extract_dir, path))
if patch is not None:
if isinstance(patch, str):
patch = [patch]
for p in patch:
patch_abs = (SCRIPT_PATH / p).absolute().__str__()
if os.path.exists(patch_abs):
@@ -724,6 +723,8 @@ def build_dependency(
except Exception as e:
# Assert that the patch has already been applied
run(["patch", "-p1", "--batch", "--reverse", "--dry-run", "-i", patch_abs], cwd=extract_dir)
else:
raise FileNotFoundError(patch_abs)
if shell is not None:
sp.run(shell, shell=True, check=True, cwd=extract_dir)
@@ -1171,6 +1172,14 @@ if "cgal" in targets:
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
gmp_args.extend(MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS)
# Fixes configure failing to find a working compiler under GCC 15's default -std=gnu23.
# Issue presumably will be resolved in any next gmp version, but currently the last one is 6.3.0.
# Patch is just applying fix from upstream meantion below:
# https://gmplib.org/list-archives/gmp-bugs/2025-February/005561.html
gmp_patches = ["./patches/gmp/001-fix-std23.patch"]
if GMP_VERSION != "6.3.0":
raise Exception(f"GMP_VERSION changed to {GMP_VERSION}, check whether {gmp_patches} is still needed.")
build_dependency(
name=f"gmp-{GMP_VERSION}",
mode="autoconf",
@@ -1178,6 +1187,7 @@ if "cgal" in targets:
pre_compile_subs=(
[("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if "wasm" in flags else []
),
patch=gmp_patches,
# Sometimes ftp.gnu.org is very slow, use ftpmirror.gnu.org as a workaround.
download_url="https://ftpmirror.gnu.org/gnu/gmp/",
download_name=f"gmp-{GMP_VERSION}.tar.bz2",
+27
View File
@@ -0,0 +1,27 @@
Fixes configure failing to find a working compiler under GCC 15's default
-std=gnu23 (upstream fix: https://gmplib.org/repo/gmp/rev/8e7bb4ae7a18).
Upstream fix is patching `acinclude.m4`, but since in the release tarball
all macros are already expanded to `configure` script, so we're patching
all occurrences of that macro.
--- a/configure
+++ b/configure
@@ -6568,7 +6568,7 @@
#if defined (__GNUC__) && ! defined (__cplusplus)
typedef unsigned long long t1;typedef t1*t2;
-void g(){}
+void g(int,t1 const*,t1,t2,t1 const*,int){}
void h(){}
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
@@ -8187,7 +8187,7 @@
#if defined (__GNUC__) && ! defined (__cplusplus)
typedef unsigned long long t1;typedef t1*t2;
-void g(){}
+void g(int,t1 const*,t1,t2,t1 const*,int){}
void h(){}
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
-32
View File
@@ -1,32 +0,0 @@
http://git.dev.opencascade.org/gitweb/?p=occt.git;a=commitdiff;h=0ab4e621833f4eae945a3762c9a29ee12e2eec53#patch1
diff --git a/src/HLRBRep/HLRBRep_InternalAlgo.cxx b/src/HLRBRep/HLRBRep_InternalAlgo.cxx
index ca885ca..c13cb06 100644 (file)
--- a/src/HLRBRep/HLRBRep_InternalAlgo.cxx
+++ b/src/HLRBRep/HLRBRep_InternalAlgo.cxx
@@ -165,7 +165,7 @@ void HLRBRep_InternalAlgo::Update ()
SB.Bounds(v1,v2,e1,e2,f1,f2);
for (Standard_Integer e = e1; e <= e2; e++) {
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
HLRAlgo::DecodeMinMax(ed.MinMax(), TheMin, TheMax);
if (FirstTime) {
FirstTime = Standard_False;
@@ -307,7 +307,7 @@ void HLRBRep_InternalAlgo::InitEdgeStatus ()
Standard_Integer nf = myDS->NbFaces();
for (Standard_Integer e = 1; e <= ne; e++) {
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
if (ed.Selected()) ed.Status().ShowAll();
}
// for (Standard_Integer f = 1; f <= nf; f++) {
@@ -368,7 +368,7 @@ void HLRBRep_InternalAlgo::Select ()
Standard_Integer nf = myDS->NbFaces();
for (Standard_Integer e = 1; e <= ne; e++) {
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
ed.Selected(Standard_True);
}
-22
View File
@@ -1,22 +0,0 @@
From a0deb4ce8b43cf3c8b8c0a4225c6be5296446dbd Mon Sep 17 00:00:00 2001
From: Adam Eri <adam.eri@blackmirror.media>
Date: Tue, 3 Sep 2019 23:30:20 +0200
Subject: [PATCH] Resolves compile error on macOS
Resolves "no member named 'isnan' in namespace 'std'" on macOS
---
GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp | 1 +
1 file changed, 1 insertion(+)
diff --git a/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp b/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
index 1f9a3eef..dd6f5c59 100644
--- a/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
+++ b/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
@@ -10,6 +10,7 @@
#include "GeneratedSaxParserUtils.h"
#include <math.h>
+#include <cmath>
#include <memory>
#include <string.h>
#include <limits>
+42 -86
View File
@@ -1,13 +1,8 @@
[project]
name = "IfcOpenShell"
version = "0.0.0"
dependencies = [
"black==26.3.1",
"ruff==0.15.12",
"poethepoet",
"ty==0.0.32",
"gersemi==0.26.1",
]
# Don't provide requires-python explicitly
# allowing pyprojects to set their own (e.g. bonsai and general ifcopenshell version differ).
[tool.black]
line-length = 120
@@ -43,6 +38,7 @@ exclude = [
# then they will be inherited by projects' .toml files.
# This allows using assuming different Python version for different projects.
[tool.ruff]
line-length = 120
exclude = [
# Submodules.
"src/ifcopenshell-python/ifcopenshell/express",
@@ -83,92 +79,39 @@ ignore = [
]
[tool.ty.rules]
all = "ignore"
all = "error"
# Structural rules (no deep type inference needed, easier to adapt).
abstract-method-in-final-class = "error"
ambiguous-protocol-member = "error"
conflicting-declarations = "error"
conflicting-metaclass = "error"
cyclic-class-definition = "error"
cyclic-type-alias-definition = "error"
dataclass-field-order = "error"
duplicate-base = "error"
duplicate-kw-only = "error"
empty-body = "error"
escape-character-in-forward-annotation = "error"
final-on-non-method = "error"
final-without-value = "error"
ignore-comment-unknown-rule = "error"
implicit-concatenated-string-type-annotation = "error"
inconsistent-mro = "error"
ineffective-final = "error"
instance-layout-conflict = "error"
invalid-dataclass = "error"
invalid-dataclass-override = "error"
invalid-enum-member-annotation = "error"
invalid-explicit-override = "error"
invalid-frozen-dataclass-subclass = "error"
invalid-generic-class = "error"
invalid-generic-enum = "error"
invalid-ignore-comment = "error"
invalid-legacy-positional-parameter = "error"
invalid-legacy-type-variable = "error"
invalid-named-tuple = "error"
invalid-newtype = "error"
invalid-overload = "error"
invalid-paramspec = "error"
invalid-protocol = "error"
invalid-syntax-in-forward-annotation = "error"
invalid-total-ordering = "error"
invalid-type-alias-type = "error"
invalid-type-checking-constant = "error"
invalid-type-guard-definition = "error"
invalid-type-variable-bound = "error"
invalid-type-variable-constraints = "error"
invalid-typed-dict-header = "error"
invalid-typed-dict-statement = "error"
override-of-final-method = "error"
override-of-final-variable = "error"
possibly-missing-import = "error"
possibly-missing-submodule = "error"
# Has false positives due to ty walrus operator bug.
# possibly-unresolved-reference = "error"
raw-string-type-annotation = "error"
redundant-final-classvar = "error"
shadowed-type-variable = "error"
subclass-of-final-class = "error"
super-call-in-named-tuple-method = "error"
unavailable-implicit-super-arguments = "error"
unbound-type-variable = "error"
undefined-reveal = "error"
unresolved-global = "error"
unresolved-import = "error"
unresolved-reference = "error"
unused-ignore-comment = "error"
unused-type-ignore-comment = "error"
useless-overload-body = "error"
possibly-unresolved-reference = "ignore"
# Maybe later, requires to specify element types for all generics.
missing-type-argument = "ignore"
# Conflicts with `bpy` props defined using annotations.
invalid-type-form = "ignore"
# Non-structural rules:
deprecated = "error"
zero-stepsize-in-slice = "error"
possibly-missing-implicit-call = "error"
unused-awaitable = "error"
# Function argument rules:
# Conflicts with `ifcopenshell.api.geometry.add_representation` type of callables we have, confusing them with a module.
# call-non-callable = "error"
conflicting-argument-forms = "error"
call-non-callable = "ignore"
# bpy is missing some context manager implementations.
invalid-context-manager = "ignore"
# Doesn't go well with `bpy.ops.xxx.yyy`.
unresolved-attribute = "ignore"
# Too many false positives.
# invalid-argument-type = "error"
missing-argument = "error"
parameter-already-assigned = "error"
positional-only-parameter-as-kwarg = "error"
too-many-positional-arguments = "error"
unknown-argument = "error"
# Has a lot of warnings due to current ty walrus operator issues.
# index-out-of-bounds = "error"
# unresolved-attribute = "error"
invalid-argument-type = "ignore"
invalid-method-override = "ignore"
invalid-assignment = "ignore"
invalid-parameter-default = "ignore"
missing-override-decorator = "ignore"
invalid-yield = "ignore"
invalid-return-type = "ignore"
non-callable-init-subclass = "ignore"
not-iterable = "ignore"
possibly-missing-attribute = "ignore"
no-matching-overload = "ignore"
not-subscriptable = "ignore"
unsupported-dynamic-base = "ignore"
unsupported-operator = "ignore"
type-assertion-failure = "ignore"
[tool.ty.environment]
extra-paths = [
@@ -215,6 +158,18 @@ exclude = [
[tool.poe.tasks]
dev-setup.sequence = [
{cmd = "uv sync"},
{cmd = "uv pip install -e ./src/bsdd/"},
{cmd = "uv pip install -e ./src/ifcopenshell-python/[advanced,dev]"},
{cmd = "uv pip install -e ./src/ifcedit/"},
{cmd = "uv pip install -e ./src/ifcpatch/"},
{cmd = "uv pip install -e ./src/ifcquery/"},
{cmd = "uv pip install -e './src/ifcmcp/[mcp]'"},
{cmd = "uv pip install -r src/bonsai/requirements-dev.txt"},
]
dev-setup.help = "Install repo packages in editable mode"
ruff = "ruff check"
black = "black ."
@@ -243,6 +198,7 @@ cmake-format = "gersemi . --in-place"
# --ignore unresolved-reference: walrus operator false positives in ty.
cmd = """
ty check
nix/
src/bcf
src/bsdd
src/ifc2ca
+5
View File
@@ -0,0 +1,5 @@
black==26.3.1
ruff==0.15.12
poethepoet
ty==0.0.59
gersemi==0.26.1
+2
View File
@@ -320,9 +320,11 @@ def loadIfcStore(scene: bpy.types.Scene) -> None:
IfcStore.purge()
refresh_ui_data()
if not tool.Ifc.get():
tool.Autosave.cancel_timer()
return
tool.Ifc.schema()
IfcStore.relink_all_objects()
tool.Autosave.reset_timer()
@persistent
+1 -1
View File
@@ -46,7 +46,7 @@ IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
class OperationData(TypedDict):
id: int
guid: NotRequired[str]
obj: str
obj: NotRequired[str]
class EditObjectOperationData(TypedDict):
+6 -1
View File
@@ -980,8 +980,13 @@ class IfcImporter:
if unit.Name == "METRE":
if not unit.Prefix:
bpy.context.scene.unit_settings.length_unit = "METERS"
else:
elif f"{unit.Prefix}METERS" in ("KILOMETERS", "CENTIMETERS", "MILLIMETERS", "MICROMETERS"):
bpy.context.scene.unit_settings.length_unit = f"{unit.Prefix}METERS"
else:
# Blender's length_unit enum has no entry for other
# SI prefixes (e.g. DECIMETERS), so fall back to
# adaptive display instead of failing to open.
bpy.context.scene.unit_settings.length_unit = "ADAPTIVE"
else:
bpy.context.scene.unit_settings.system = "IMPERIAL"
name = unit.Name.lower()
@@ -843,7 +843,6 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, opening)
mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
mat.translation = (0, 0, 0)
opening_bm = bmesh.new()
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
for vert in verts:
+3 -6
View File
@@ -156,16 +156,13 @@ class BrickschemaReferencesData:
for rel in getattr(tool.Ifc.get_entity(bpy.context.active_object), "HasAssociations", []):
if rel.is_a("IfcRelAssociatesLibrary"):
reference = rel.RelatingLibrary
if tool.Ifc.get_schema() == "IFC2X3" and "#" not in reference.ItemReference:
continue
if tool.Ifc.get_schema() != "IFC2X3" and "#" not in reference.Identification:
identification = tool.Document.get_external_reference_id(reference)
if not identification or "#" not in identification:
continue
results.append(
{
"id": reference.id(),
"identification": (
reference.ItemReference if tool.Ifc.get_schema() == "IFC2X3" else reference.Identification
),
"identification": identification,
"name": reference.Name or "Unnamed",
}
)
+11 -22
View File
@@ -127,36 +127,25 @@ class ObjectDocumentData:
identification = None
if is_information:
if tool.Ifc.get_schema() == "IFC2X3":
identification = relating_document.DocumentId
else:
identification = relating_document.Identification
identification = tool.Document.get_document_information_id(relating_document)
location = getattr(relating_document, "Location", None)
description = getattr(relating_document, "Description", "No description")
else:
description = relating_document.Description
if tool.Ifc.get_schema() == "IFC2X3":
reference_to_document = relating_document.ReferenceToDocument
if not name and reference_to_document:
name = reference_to_document[0].Name
referenced_document = tool.Document.get_reference_document(relating_document)
identification = relating_document.ItemReference
if not identification and reference_to_document:
identification = reference_to_document[0].DocumentId
location = relating_document.Location
else:
referenced_document = relating_document.ReferencedDocument
if not name and referenced_document:
name = referenced_document.Name
if not name and referenced_document:
name = referenced_document.Name
identification = relating_document.Identification
if not identification and referenced_document:
identification = referenced_document.Identification
identification = tool.Document.get_external_reference_id(relating_document)
if not identification and referenced_document:
identification = tool.Document.get_document_information_id(referenced_document)
location = relating_document.Location
if location is None and referenced_document:
location = referenced_document.Location
location = relating_document.Location
# IFC2X3 IfcDocumentInformation has no Location to fall back to.
if location is None and referenced_document and tool.Ifc.get_schema() != "IFC2X3":
location = referenced_document.Location
location = cls.convert_to_file_uri(location) if location else None
@@ -189,14 +189,20 @@ def format_distance(
if hasattr(length_unit, "Prefix") and length_unit.Prefix:
unit_length = length_unit.Prefix + length_unit.Name
unit_length_mapping = {
"MILE": "MILES",
"FOOT": "FEET",
"INCH": "INCHES",
"KILOMETRE": "KILOMETERS",
"METRE": "METERS",
"DECIMETRE": "DECIMETERS",
"CENTIMETRE": "CENTIMETERS",
"MILLIMETRE": "MILLIMETERS",
"MICROMETRE": "MICROMETERS",
}
unit_length = unit_length_mapping[unit_length]
# Fall through for units without a dedicated formatter (e.g.
# HECTOMETRE) so they use the adaptive branch instead of a
# KeyError (#8255).
unit_length = unit_length_mapping.get(unit_length, unit_length)
# For now we only format area in IFC Units
if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"):
area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit)
@@ -1706,6 +1706,12 @@ class CreateDrawing(bpy.types.Operator):
key=lambda a: (
tool.Drawing.get_annotation_z_index(a),
1 if ifcopenshell.util.element.get_predefined_type(a) == "TEXT" else 0,
# Deterministic tiebreaker so equal-priority annotations keep a
# stable order across sessions. Without it the order comes from
# the set union above, which depends on entity hashes (and thus
# the file pointer), shuffling annotations between Blender
# restarts. See #6608.
a.id(),
),
)
@@ -2343,7 +2349,9 @@ class ActivateDrawingBase(tool.Ifc.Operator):
"Activates the selected drawing view.\n\n"
+ "ALT+CLICK to keep the viewport position.\n\n"
+ "SHIFT+CLICK to load a quick preview of the drawing view.\n\n"
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views"
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views, "
+ "then select their cameras (the first selected drawing's camera becomes active).\n\n"
+ "SHIFT+CTRL+ALT+CLICK to do the same but also select the annotations, not just the cameras"
)
drawing: bpy.props.IntProperty()
@@ -2365,16 +2373,25 @@ class ActivateDrawingBase(tool.Ifc.Operator):
default=False,
options={"SKIP_SAVE"},
)
include_annotations_in_selection: bpy.props.BoolProperty(
name="Include Annotations In Selection",
description="Also select the loaded annotation objects, not just the drawing cameras.",
default=False,
options={"SKIP_SAVE"},
)
if TYPE_CHECKING:
drawing: int
should_view_from_camera: bool
use_quick_preview: bool
load_selected_annotations: bool
include_annotations_in_selection: bool
def invoke(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
if event.type == "LEFTMOUSE" and event.shift and event.ctrl:
self.load_selected_annotations = True
if event.alt:
self.include_annotations_in_selection = True
return self.execute(context)
if event.type == "LEFTMOUSE" and event.alt:
self.should_view_from_camera = False
@@ -2389,15 +2406,34 @@ class ActivateDrawingBase(tool.Ifc.Operator):
bpy.ops.bim.load_drawings()
if self.load_selected_annotations:
objs_to_select = []
active_camera = None
for d in props.drawings:
if not (d.is_drawing and d.is_selected):
continue
selected_drawing = tool.Ifc.get().by_id(d.ifc_definition_id)
# Importing the camera (if missing) ensures the drawing's
# collection exists so the annotations get collected into it.
if not tool.Ifc.get_object(selected_drawing):
tool.Drawing.import_drawing(selected_drawing)
tool.Drawing.import_annotations_in_group(tool.Drawing.get_drawing_group(selected_drawing))
if not (camera := tool.Ifc.get_object(selected_drawing)):
camera = tool.Drawing.import_drawing(selected_drawing)
group = tool.Drawing.get_drawing_group(selected_drawing)
tool.Drawing.import_annotations_in_group(group)
if active_camera is None:
active_camera = camera
objs_to_select.append(camera)
if self.include_annotations_in_selection:
for element in tool.Drawing.get_group_elements(group) or []:
if element.is_a("IfcAnnotation") and element.ObjectType != "DRAWING":
if annotation_obj := tool.Ifc.get_object(element):
objs_to_select.append(annotation_obj)
# Select the checked drawings' objects, with the first drawing's camera as active.
bpy.ops.object.select_all(action="DESELECT")
for obj in objs_to_select:
obj.select_set(True)
if active_camera is not None:
context.view_layer.objects.active = active_camera
return {"FINISHED"}
drawing = tool.Ifc.get().by_id(self.drawing)
@@ -2486,7 +2522,9 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
"Activates the selected drawing view.\n\n"
+ "ALT+CLICK to keep the viewport position.\n\n"
+ "SHIFT+CLICK to load a quick preview of the drawing view.\n\n"
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views"
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views, "
+ "then select their cameras (the first selected drawing's camera becomes active).\n\n"
+ "SHIFT+CTRL+ALT+CLICK to do the same but also select the annotations, not just the cameras"
)
@@ -3548,7 +3586,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
if sheet.is_a("IfcDocumentInformation"):
self.document_type = "SHEET"
self.name = sheet.Name
self.identification = sheet.DocumentId if tool.Ifc.get_schema() == "IFC2X3" else sheet.Identification
self.identification = tool.Document.get_document_information_id(sheet)
elif sheet.is_a("IfcDocumentReference") and tool.Drawing.get_reference_description(sheet) == "TITLEBLOCK":
self.document_type = "TITLEBLOCK"
else:
@@ -903,12 +903,8 @@ class SvgWriter:
continue
sheet = tool.Drawing.get_reference_document(sheet_reference)
if sheet:
if tool.Ifc.get_schema() == "IFC2X3":
reference_id = sheet_reference.ItemReference or "-"
sheet_id = sheet.DocumentId or "-"
else:
reference_id = sheet_reference.Identification or "-"
sheet_id = sheet.Identification or "-"
reference_id = tool.Document.get_external_reference_id(sheet_reference) or "-"
sheet_id = tool.Document.get_document_information_id(sheet) or "-"
return (reference_id, sheet_id)
break
return ("-", "-")
@@ -581,7 +581,11 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
if has_openings and not self.apply_openings:
# Meshlike things with openings can only be updated without openings applied.
if self.from_ui:
self.report({"ERROR"}, f"Object '{obj.name}' has openings - representation cannot be updated.")
self.report(
{"ERROR"},
f"Object '{obj.name}' has openings. "
"ALT+click the button to bake the openings into the new representation.",
)
return
if not product.is_a("IfcGridAxis"):
+1 -3
View File
@@ -103,9 +103,7 @@ class LibraryReferencesData:
results.append(
{
"id": library.id(),
"identification": (
library.ItemReference if tool.Ifc.get_schema() == "IFC2X3" else library.Identification
),
"identification": tool.Document.get_external_reference_id(library),
"name": library.Name or "Unnamed",
}
)
@@ -418,6 +418,13 @@ class ImportQuickFavorites(bpy.types.Operator):
bl_description = "Import operators from Blender's Quick Favorites menu, including their configured properties"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if bpy.app.version[:2] not in tool.Misc.QuickFavorites.OFFSET_USER_MENUS:
cls.poll_message_set(f"Blender version {bpy.app.version_string} is not supported.")
return False
return True
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
props = tool.Misc.get_misc_props()
props.quick_favorites.clear()
@@ -18,7 +18,7 @@
import json
from pathlib import Path
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING
import bpy
import ifcopenshell
@@ -122,8 +122,8 @@ class ExecuteIfcPatch(bpy.types.Operator):
if props.should_load_from_memory and tool.Ifc.get():
args["file"] = tool.Ifc.get()
else:
args["input"] = cast(str, props.ifc_patch_input)
args["file"] = cast(ifcopenshell.file, ifcopenshell.open(props.ifc_patch_input))
args["input"] = props.ifc_patch_input
args["file"] = ifcopenshell.open(props.ifc_patch_input)
# Store this in case the patch recipe resets the Blender session, such as by loading a new project.
ifc_patch_output = props.ifc_patch_output or props.ifc_patch_input
@@ -18,6 +18,8 @@
import bpy
import bonsai.tool as tool
from . import decorator, gizmo, operator, prop, ui, workspace
classes = (
@@ -58,6 +60,8 @@ classes = (
operator.LinkIfc,
operator.LoadBlendMetadataAndIFC,
operator.LoadLink,
operator.AutosavePrompt,
operator.LoadAutosavedRecoveryPopup,
operator.LoadLinkedProject,
operator.LoadProject,
operator.LoadProjectElements,
@@ -136,6 +140,7 @@ def register():
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.ExploreTool)
tool.Autosave.cancel_timer()
del bpy.types.Scene.BIMProjectProperties
del bpy.types.Scene.MeasureToolSettings
bpy.app.handlers.load_post.remove(decorator.toggle_decorations_on_load)
@@ -985,8 +985,10 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
),
default=False,
)
skip_autosave_recovery: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"})
filename_ext = ".ifc"
skip_recent: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
if TYPE_CHECKING:
filepath: str
@@ -995,6 +997,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
use_relative_path: bool
should_start_fresh_session: bool
import_without_ifc_data: bool
skip_autosave_recovery: bool
use_detailed_tooltip: bool
@classmethod
@@ -1041,7 +1044,33 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
return tooltip
def check_autosave_recovery(self, context: bpy.types.Context) -> bool:
if self.skip_autosave_recovery:
return False
autosaved_filepath = tool.Autosave.get_newer_autosaved_path(self.get_filepath_abs())
if not autosaved_filepath:
return False
# Fire-and-forget: don't propagate this popup's own RUNNING_MODAL
# return value up as if *this* operator were running modally too -
# we never call modal_handler_add() on ourselves, so the window
# manager would be left tracking a modal operator with no handler,
# corrupting its operator bookkeeping until it crashes later when
# the (real) popup modal handler is closed.
bpy.ops.bim.load_autosaved_recovery_popup(
"INVOKE_DEFAULT",
original_filepath=str(self.get_filepath_abs()),
autosaved_filepath=autosaved_filepath,
is_advanced=self.is_advanced,
use_relative_path=self.use_relative_path,
should_start_fresh_session=self.should_start_fresh_session,
import_without_ifc_data=self.import_without_ifc_data,
)
return True
def execute(self, context):
if self.check_autosave_recovery(context):
return {"FINISHED"}
if (
tool.Blender.get_addon_preferences().save_metadata_blend_file
and self.should_start_fresh_session
@@ -1136,7 +1165,8 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
props.should_save_metadata_for_this_file = metadata_doc is not None
tool.Blender.register_toolbar()
tool.Project.add_recent_ifc_project(self.get_filepath_abs())
if not self.skip_recent:
tool.Project.add_recent_ifc_project(self.get_filepath_abs())
if self.is_advanced:
pass
@@ -1149,10 +1179,13 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
except:
bonsai.last_error = traceback.format_exc()
raise
tool.Autosave.reset_timer()
return {"FINISHED"}
def invoke(self, context, event):
if self.filepath:
if self.check_autosave_recovery(context):
return {"FINISHED"}
return self.execute(context)
return ImportHelper.invoke(self, context, event)
@@ -1947,6 +1980,7 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False)
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
skip_recent: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
if TYPE_CHECKING:
filter_glob: str
@@ -2007,6 +2041,18 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
return {"FINISHED"}
def _execute(self, context):
project_props = tool.Project.get_project_props()
project_props.use_relative_project_path = self.use_relative_path
# Fallback if filepath is not set
if not getattr(self, "filepath", None) or self.filepath.strip() in ("", ".ifc"):
props = tool.Blender.get_bim_props()
if props.ifc_file:
self.filepath = str(tool.Blender.ensure_blender_path_is_abs(Path(props.ifc_file)))
else:
self.report({"ERROR"}, "No filepath available for saving.")
return {"CANCELLED"}
committed, failed_commits = tool.Parametric.commit_pending_edits()
# Previews are session-transient — discard rather than commit. Sibling
# gizmo polls gate on each preview's is_active flag, and a stuck flag
@@ -2069,7 +2115,8 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
settings.logger.info("Export finished in {:.2f} seconds".format(time.time() - start))
print("Export finished in {:.2f} seconds".format(time.time() - start))
# New project created in Bonsai should be in recent projects too.
tool.Project.add_recent_ifc_project(Path(output_file))
if not self.skip_recent:
tool.Project.add_recent_ifc_project(Path(output_file))
props = tool.Project.get_project_props()
if props.use_relative_project_path and bpy.data.is_saved:
output_file = os.path.relpath(output_file, bpy.path.abspath("//"))
@@ -2103,6 +2150,7 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
)
bonsai.bim.handler.refresh_ui_data()
tool.Autosave.reset_timer()
@classmethod
def description(cls, context, properties):
@@ -2111,6 +2159,123 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
return "Save the IFC file. Will save both .IFC/.BLEND files if synced together"
class LoadAutosavedRecoveryPopup(bpy.types.Operator):
bl_idname = "bim.load_autosaved_recovery_popup"
bl_label = "Recover Autosaved File"
bl_options = {"REGISTER", "UNDO"}
original_filepath: bpy.props.StringProperty(options={"SKIP_SAVE"})
autosaved_filepath: bpy.props.StringProperty(options={"SKIP_SAVE"})
is_advanced: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
use_relative_path: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
should_start_fresh_session: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
import_without_ifc_data: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def draw(self, context):
layout = self.layout
layout.label(text="A newer autosaved copy was found:", icon="INFO")
layout.label(text=os.path.basename(self.autosaved_filepath))
layout.separator()
layout.label(text="Do you want to load the autosaved version instead?")
layout.label(text="(Cancel will load the original)")
def invoke(self, context, event):
# invoke_props_dialog is modal - unlike invoke_popup/popup_menu, it
# isn't dismissed by the mouse simply leaving its bounds. It always
# renders both a fixed "Cancel" button and this confirm_text one, so
# the question is framed as Yes/Cancel rather than adding separate
# Load buttons on top.
return context.window_manager.invoke_props_dialog(
self, width=420, title="Recover Autosaved File", confirm_text="Yes"
)
def _load_kwargs(self, filepath: str, skip_recent: bool) -> dict:
return dict(
filepath=filepath,
skip_autosave_recovery=True, # Prevent infinite loop
is_advanced=self.is_advanced,
use_relative_path=self.use_relative_path,
should_start_fresh_session=self.should_start_fresh_session,
import_without_ifc_data=self.import_without_ifc_data,
skip_recent=skip_recent,
)
@staticmethod
def _defer(callback) -> None:
def on_timer() -> None:
callback()
return None
# bim.load_project (with should_start_fresh_session, our default)
# calls wm.read_homefile(), which tears down the window
# manager/screens/regions. Calling that synchronously from this
# dialog's execute()/cancel() - themselves invoked from deep inside
# Blender's modal handling for this popup's button click - frees
# data that the still-on-stack caller dereferences once we return,
# segfaulting Blender. Deferring by one timer tick runs the reload
# after the popup's own modal handling has fully unwound. The
# callback only closes over plain values (not `self`), since the
# operator instance itself may no longer be valid by the time the
# timer fires.
bpy.app.timers.register(on_timer, first_interval=0.0)
def execute(self, context):
kwargs = self._load_kwargs(self.autosaved_filepath, skip_recent=True)
original_filepath = self.original_filepath
def load_and_repoint() -> None:
bpy.ops.bim.load_project(**kwargs)
# Re-point tracking at the original path so future saves write
# back to it, not "_autosaved.ifc".
tool.Ifc.set_path(original_filepath)
self._defer(load_and_repoint)
return {"FINISHED"}
def cancel(self, context):
# Also reached via Escape or a click outside the dialog, not just Cancel.
kwargs = self._load_kwargs(self.original_filepath, skip_recent=False)
self._defer(lambda: bpy.ops.bim.load_project(**kwargs))
class AutosavePrompt(bpy.types.Operator):
bl_idname = "bim.autosave_prompt"
bl_label = "Autosave Reminder"
bl_options = set()
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(
self, width=400, confirm_text="Save", title="Autosave Reminder"
)
def draw(self, context):
layout = self.layout
layout.label(text="The autosave timer has expired.", icon="INFO")
layout.label(text="Would you like to save your IFC project now?")
def execute(self, context):
# Get current IFC path
props = tool.Blender.get_bim_props()
current_ifc_path = props.ifc_file
if not current_ifc_path:
self.report({"WARNING"}, "No IFC file path set. Please save manually.")
tool.Autosave.reset_timer()
return {"CANCELLED"}
# Call save_project with explicit filepath using EXEC_DEFAULT
result = bpy.ops.bim.save_project(
"EXEC_DEFAULT", filepath=current_ifc_path, should_save_as=False, skip_recent=True
)
tool.Autosave.reset_timer()
return result
def cancel(self, context):
tool.Autosave.reset_timer()
return {"CANCELLED"}
class LoadLinkedProject(bpy.types.Operator, ImportHelper):
bl_idname = "bim.load_linked_project"
bl_label = "Load Project For Viewing Only"
+19 -6
View File
@@ -27,6 +27,7 @@ import ifcopenshell.api.material
import ifcopenshell.api.pset
import ifcopenshell.api.root
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.schema
import ifcopenshell.util.shape_builder
import ifcopenshell.util.type
@@ -129,13 +130,25 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator):
same_ifc_product = element.is_a(ifc_product)
if not same_ifc_product:
if not (element.is_a("IfcElement") and ifc_product == "IfcElementType") and not (
element.is_a("IfcElementType") and ifc_product == "IfcElement"
):
self.report(
{"ERROR"}, f"Not supported class reassignment for object '{obj.name}' -> {ifc_product}."
# A spatial element (e.g. IfcSite) anchors the containment
# hierarchy, so only allow reassigning it to another family when
# it actually carries geometry - i.e. it's a real modelled thing
# (a bench dropped onto IfcSite -> IfcFurniture) rather than an
# empty spatial container we'd be turning into a loose element.
# IfcSpatialStructureElement covers IFC2X3, which has no
# IfcSpatialElement supertype.
is_spatial = element.is_a("IfcSpatialElement") or element.is_a("IfcSpatialStructureElement")
if is_spatial:
has_geometry = (
next(ifcopenshell.util.representation.get_representations_iter(element), None) is not None
)
return {"CANCELLED"}
if not has_geometry:
self.report(
{"ERROR"},
f"Cannot reassign '{obj.name}' ({element.is_a()}) to {ifc_product}: "
"a spatial element can only be reassigned to another class when it has geometry.",
)
return {"CANCELLED"}
props = tool.Blender.get_object_bim_props(obj)
props.is_reassigning_class = False
@@ -71,7 +71,11 @@ class LoadByDirection(TypedDict):
ProcessedLoad = TypedDict(
"ProcessedLoad",
{"linear loads": LoadByDirection, "max linear load": float, "discrete loads": list[list[DiscreteConfigItem]]},
{
"linear loads": dict[str, LoadByDirection] | None,
"max linear load": float,
"discrete loads": list[list[DiscreteConfigItem]],
},
)
@@ -845,13 +849,16 @@ class ShaderInfo:
v = l1[1] + fac * (pos - l1[0])
return v
def interpolate(self, pos: float, loadinfo: list[LoadConfigItem], start: int, end: int, key: str) -> np.ndarray:
def interpolate(self, pos: float, loadinfo: list[LoadConfigItem], start: int, end: int) -> np.ndarray:
"""interpolate the result vectors between load poits"""
result = np.zeros(6)
for i in range(6):
value1 = [loadinfo[start]["pos"], loadinfo[start][key][i]] # [position, force_component]
value2 = [loadinfo[end]["pos"], loadinfo[end][key][i]] # [position, force_component]
result[i] = self.interp1d(value1, value2, pos) # interpolated [position, force_component]
# [position, force_component]
value1 = [loadinfo[start]["pos"], loadinfo[start]["load values"][i]]
# [position, force_component]
value2 = [loadinfo[end]["pos"], loadinfo[end]["load values"][i]]
# interpolated [position, force_component]
result[i] = self.interp1d(value1, value2, pos)
return result
def get_before_and_after(self, pos: float, load_config_list: list[list[LoadConfigItem]]) -> dict[str, list[float]]:
@@ -895,8 +902,8 @@ class ShaderInfo:
load_before += config[end]["load values"]
elif end - start == 1:
load_before += self.interpolate(pos, config, start, end, "load values")
load_after += self.interpolate(pos, config, start, end, "load values")
load_before += self.interpolate(pos, config, start, end)
load_after += self.interpolate(pos, config, start, end)
start += 1
end -= 1
return_value = {"before": load_before.tolist(), "after": load_after.tolist()}
+46
View File
@@ -577,6 +577,43 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
should_disable_undo_on_save: BoolProperty(
name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False
)
def update_autosave_settings(self, context: bpy.types.Context) -> None:
if self.autosave_enabled:
tool.Autosave.reset_timer()
else:
tool.Autosave.cancel_timer()
autosave_enabled: BoolProperty(
name="Enable IFC Autosave Timer",
description="Periodically remind you to save or automatically create a backup copy of the IFC file",
default=False,
update=update_autosave_settings,
)
autosave_interval_minutes: bpy.props.IntProperty(
name="Autosave Interval (Minutes)",
description="Time between autosave reminders or backups. The timer resets whenever you open or save a project",
default=10,
min=1,
max=1440,
update=update_autosave_settings,
)
autosave_mode: bpy.props.EnumProperty(
name="Autosave Mode",
items=[
(
"PROMPT",
"Prompt to Save",
"Show a dialog offering to save the IFC project when the timer expires",
),
(
"BACKUP",
"Automatic Backup",
"Save a backup copy as filename_autosaved.ifc when the timer expires",
),
],
default="PROMPT",
)
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
should_always_cache: BoolProperty(
name="Always Cache Geometry",
@@ -689,6 +726,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
bsdd_load_test_dictionaries: bool
bsdd_baseurl: str
should_disable_undo_on_save: bool
autosave_enabled: bool
autosave_interval_minutes: int
autosave_mode: Literal["PROMPT", "BACKUP"]
should_stream: bool
should_always_cache: bool
occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"]
@@ -837,6 +877,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
def draw_other_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "opening_focus_opacity")
layout.prop(self, "should_disable_undo_on_save")
layout.separator()
layout.label(text="Autosave:")
layout.prop(self, "autosave_enabled")
if self.autosave_enabled:
layout.prop(self, "autosave_interval_minutes")
layout.prop(self, "autosave_mode")
layout.prop(self, "should_stream")
layout.prop(self, "should_always_cache")
layout.label(text="bSDD:")
+4 -48
View File
@@ -303,17 +303,7 @@ def add_drawing(
ifc_representation_class=None,
)
drawings_parent_group = None
for group in ifc.get().by_type("IfcGroup"):
if group.Name == "DRAWINGS" and group.ObjectType == "DRAWINGS":
drawings_parent_group = group
break
if not drawings_parent_group:
drawings_parent_group = ifc.run("group.add_group")
ifc.run(
"group.edit_group", group=drawings_parent_group, attributes={"Name": "DRAWINGS", "ObjectType": "DRAWINGS"}
)
drawings_parent_group = drawing.ensure_drawings_parent_group()
group = ifc.run("group.add_group")
ifc.run("group.edit_group", group=group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"})
@@ -352,19 +342,7 @@ def add_drawing(
)
drawing.setup_shading_styles_path(shading_styles_path)
drawings_parent_document = None
for document in ifc.get().by_type("IfcDocumentInformation"):
if document.Name == "DRAWINGS" and document.Scope == "DRAWINGS":
drawings_parent_document = document
break
if not drawings_parent_document:
drawings_parent_document = ifc.run("document.add_information")
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
else:
attributes = {"Identification": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
ifc.run("document.edit_information", information=drawings_parent_document, attributes=attributes)
drawings_parent_document = drawing.ensure_drawings_parent_document()
information = ifc.run("document.add_information", parent=drawings_parent_document)
uri = drawing.get_default_drawing_path(drawing_name)
@@ -395,17 +373,7 @@ def duplicate_drawing(
group = drawing_tool.get_drawing_group(new_drawing)
ifc.run("group.unassign_group", group=group, products=[new_drawing])
drawings_parent_group = None
for parent_group in ifc.get().by_type("IfcGroup"):
if parent_group.Name == "DRAWINGS" and parent_group.ObjectType == "DRAWINGS":
drawings_parent_group = parent_group
break
if not drawings_parent_group:
drawings_parent_group = ifc.run("group.add_group")
ifc.run(
"group.edit_group", group=drawings_parent_group, attributes={"Name": "DRAWINGS", "ObjectType": "DRAWINGS"}
)
drawings_parent_group = drawing_tool.ensure_drawings_parent_group()
new_group = ifc.run("group.add_group")
ifc.run("group.edit_group", group=new_group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"})
@@ -426,19 +394,7 @@ def duplicate_drawing(
old_reference = drawing_tool.get_drawing_document(new_drawing)
ifc.run("document.unassign_document", products=[new_drawing], document=old_reference)
drawings_parent_document = None
for document in ifc.get().by_type("IfcDocumentInformation"):
if document.Name == "DRAWINGS" and document.Scope == "DRAWINGS":
drawings_parent_document = document
break
if not drawings_parent_document:
drawings_parent_document = ifc.run("document.add_information")
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
else:
attributes = {"Identification": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
ifc.run("document.edit_information", information=drawings_parent_document, attributes=attributes)
drawings_parent_document = drawing_tool.ensure_drawings_parent_document()
information = ifc.run("document.add_information", parent=drawings_parent_document)
uri = drawing_tool.get_default_drawing_path(drawing_name)
-65
View File
@@ -1,65 +0,0 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2026
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
#
# This file was generated with the assistance of an AI coding tool.
from __future__ import annotations
import math
from collections.abc import Iterable
from typing import TYPE_CHECKING
import bonsai.core.geometry
if TYPE_CHECKING:
import bpy
import bonsai.tool as tool
Z_ROTATION_ALIGNMENT_TOLERANCE = 1e-9
def _z_rotation_diff(target_z: float, source_z: float) -> float:
"""Signed Z-Euler difference wrapped to [-π, π]."""
return (target_z - source_z + math.pi) % (2 * math.pi) - math.pi
def copy_z_rotation_to_selected(
ifc: type[tool.Ifc],
geometry: type[tool.Geometry],
surveyor: type[tool.Surveyor],
*,
active: bpy.types.Object,
targets: Iterable[bpy.types.Object],
flip: bool = False,
) -> int:
"""Apply ``active``'s Z-Euler rotation to each target."""
source_z = surveyor.get_z_rotation(active) # ty: ignore[missing-argument]
if flip:
source_z += math.pi
rotated = 0
for obj in targets:
target_z = surveyor.get_z_rotation(obj) # ty: ignore[missing-argument]
if abs(_z_rotation_diff(target_z, source_z)) < Z_ROTATION_ALIGNMENT_TOLERANCE:
continue
surveyor.set_z_rotation(obj, source_z) # ty: ignore[missing-argument]
rotated += 1
if ifc.get_entity(obj) is not None:
bonsai.core.geometry.edit_object_placement(ifc, geometry, surveyor, obj=obj)
return rotated
+2 -3
View File
@@ -254,7 +254,6 @@ class Cost:
def get_cost_schedule(cls, cost_schedule): pass
def get_cost_value_attributes(cls): pass
def get_cost_value_unit_component(cls): pass
def get_direct_cost_item_products(cls): pass
def get_highlighted_cost_item(cls): pass
def get_products(cls, related_object_type): pass
def get_schedule_cost_items(cls, cost_schedule): pass
@@ -355,6 +354,8 @@ class Drawing:
def enable_editing_schedules(cls): pass
def enable_editing_sheets(cls): pass
def enable_editing_text(cls, obj): pass
def ensure_drawings_parent_document(cls): pass
def ensure_drawings_parent_group(cls): pass
def ensure_unique_drawing_name(cls, name): pass
def ensure_unique_identification(cls, identification): pass
def export_font_size(cls, obj): pass
@@ -1170,8 +1171,6 @@ class Style:
@interface
class Surveyor:
def get_absolute_matrix(cls, obj): pass
def get_z_rotation(cls, obj): pass
def set_z_rotation(cls, obj, z): pass
@interface
+3
View File
@@ -80,3 +80,6 @@ from bonsai.tool.type import Type
from bonsai.tool.unit import Unit
from bonsai.tool.wall import Wall
from bonsai.tool.web import Web
# Have to move after import of tool.drawing
from bonsai.tool.autosave import Autosave # isort: skip
+194
View File
@@ -0,0 +1,194 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2026
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
#
# This file was generated with the assistance of an AI coding tool.
from __future__ import annotations
import atexit
import logging
import os
from collections.abc import Callable
from pathlib import Path
from typing import Union
import bpy
import bonsai.tool as tool
from bonsai.bim import export_ifc
from bonsai.bim.module.model import preview_base
AUTOSAVING_SUFFIX = "_autosaving.ifc"
AUTOSAVED_SUFFIX = "_autosaved.ifc"
_timer_callback: Union[Callable[[], None], None] = None
# See cleanup_stale_autosave() for why this is a cached plain string rather
# than looked up live.
_active_ifc_path_cache: Union[str, None] = None
class Autosave:
@classmethod
def get_paths(cls, ifc_path: Union[str, Path]) -> tuple[Path, Path, Path]:
path = Path(ifc_path)
stem = path.stem if path.suffix.lower() == ".ifc" else path.name
parent = path.parent
main_path = path if path.suffix.lower() == ".ifc" else parent / f"{stem}.ifc"
autosaving_path = parent / f"{stem}{AUTOSAVING_SUFFIX}"
autosaved_path = parent / f"{stem}{AUTOSAVED_SUFFIX}"
return main_path, autosaving_path, autosaved_path
@classmethod
def get_active_ifc_path(cls) -> Union[Path, None]:
props = tool.Blender.get_bim_props()
if not props.ifc_file:
return None
path = tool.Blender.ensure_blender_path_is_abs(Path(props.ifc_file))
if path.suffix.lower() != ".ifc":
return None
return path
@classmethod
def _update_active_ifc_path_cache(cls) -> None:
global _active_ifc_path_cache
ifc_path = cls.get_active_ifc_path()
_active_ifc_path_cache = ifc_path.as_posix() if ifc_path is not None else None
@classmethod
def is_enabled(cls) -> bool:
return bool(tool.Blender.get_addon_preferences().autosave_enabled)
@classmethod
def get_interval_seconds(cls) -> float:
minutes = tool.Blender.get_addon_preferences().autosave_interval_minutes
return max(1.0, float(minutes) * 60.0)
@classmethod
def is_eligible(cls) -> bool:
return cls.is_enabled() and tool.Ifc.get() is not None and cls.get_active_ifc_path() is not None
@classmethod
def cancel_timer(cls) -> None:
global _timer_callback
if _timer_callback is not None and bpy.app.timers.is_registered(_timer_callback):
bpy.app.timers.unregister(_timer_callback)
_timer_callback = None
@classmethod
def reset_timer(cls) -> None:
cls.cancel_timer()
cls._update_active_ifc_path_cache()
if not cls.is_eligible():
return
def on_timer() -> Union[float, None]:
cls._on_timer_expired()
# Reschedule by returning the next interval rather than calling
# reset_timer(), which would unregister this timer from within
# its own callback. Blender frees the timer's internal registry
# entry on that manual unregister, then frees it again when the
# callback returns - a double free that corrupts the heap and
# crashes Blender shortly after (e.g. when the prompt dialog
# spawned below is next interacted with).
return cls.get_interval_seconds() if cls.is_eligible() else None
global _timer_callback
_timer_callback = on_timer
bpy.app.timers.register(on_timer, first_interval=cls.get_interval_seconds())
@classmethod
def _on_timer_expired(cls) -> None:
if not cls.is_eligible():
return
prefs = tool.Blender.get_addon_preferences()
bim_props = tool.Blender.get_bim_props()
if bim_props.is_dirty:
if prefs.autosave_mode == "PROMPT":
bpy.ops.bim.autosave_prompt("INVOKE_DEFAULT")
elif prefs.autosave_mode == "BACKUP":
try:
cls.perform_backup(bpy.context)
except Exception as error:
print(f"Bonsai: autosave backup failed: {error}")
@classmethod
def perform_backup(cls, context: bpy.types.Context) -> None:
ifc_path = cls.get_active_ifc_path()
if ifc_path is None:
return
_, autosaving_path, autosaved_path = cls.get_paths(ifc_path)
autosaving_path.parent.mkdir(parents=True, exist_ok=True)
tool.Parametric.commit_pending_edits()
preview_base.discard_pending_previews(context.scene)
logger = logging.getLogger("ExportIFC")
output_file = autosaving_path.as_posix().replace("\\", "/")
settings = export_ifc.IfcExportSettings.factory(context, output_file, logger)
export_ifc.IfcExporter(settings).export()
try:
os.replace(autosaving_path, autosaved_path)
except OSError:
if autosaving_path.is_file():
autosaving_path.unlink(missing_ok=True)
raise
@classmethod
def get_newer_autosaved_path(cls, ifc_path: Union[str, Path]) -> Union[str, None]:
path = Path(ifc_path)
if path.suffix.lower() != ".ifc" or not path.is_file():
return None
_, _, autosaved_path = cls.get_paths(path)
if not autosaved_path.is_file():
return None
if autosaved_path.stat().st_mtime > path.stat().st_mtime:
return autosaved_path.as_posix().replace("\\", "/")
return None
@classmethod
def cleanup_stale_autosave(cls) -> None:
"""Remove the active IFC's autosave file(s) on a graceful shutdown.
Registered via `atexit`, which only runs on a normal interpreter
shutdown - never on an actual crash. So a deliberate quit (whether
the user saved or chose "don't save") clears the recovery file and
won't prompt on next startup, while a genuine crash leaves it in
place for recovery, since no atexit callbacks fire then.
Deliberately reads only `_active_ifc_path_cache` - a plain string
kept up to date by `reset_timer()` - rather than touching `bpy` here.
By the time `atexit` fires, Blender's own C++ side is torn down far
enough that even reading `bpy.context.scene` aborts the process
(std::bad_optional_access) instead of raising a catchable exception.
"""
if _active_ifc_path_cache is None:
return
try:
_, autosaving_path, autosaved_path = cls.get_paths(_active_ifc_path_cache)
autosaving_path.unlink(missing_ok=True)
autosaved_path.unlink(missing_ok=True)
except Exception:
pass
atexit.register(Autosave.cleanup_stale_autosave)
+3 -4
View File
@@ -192,10 +192,9 @@ class Brick(bonsai.core.tool.Brick):
def get_brick(cls, element: ifcopenshell.entity_instance) -> Union[str, None]:
for rel in element.HasAssociations:
if rel.is_a("IfcRelAssociatesLibrary"):
if tool.Ifc.get_schema() == "IFC2X3" and "#" in rel.RelatingLibrary.ItemReference:
return rel.RelatingLibrary.ItemReference
if tool.Ifc.get_schema() != "IFC2X3" and "#" in rel.RelatingLibrary.Identification:
return rel.RelatingLibrary.Identification
identification = tool.Document.get_external_reference_id(rel.RelatingLibrary)
if identification and "#" in identification:
return identification
@classmethod
def get_brick_class(cls, element: ifcopenshell.entity_instance) -> Union[str, None]:
+2 -1
View File
@@ -261,7 +261,8 @@ class Document(bonsai.core.tool.Document):
def get_reference_document(cls, reference: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None:
# TODO: migrate to util.document and replace all instances
if reference.file.schema == "IFC2X3":
return (reference.ReferenceToDocument or (None))[0]
reference_to_document = reference.ReferenceToDocument
return reference_to_document[0] if reference_to_document else None
return reference.ReferencedDocument
@classmethod
+31 -11
View File
@@ -38,6 +38,7 @@ import ifcopenshell.api.context
import ifcopenshell.api.document
import ifcopenshell.api.drawing
import ifcopenshell.api.geometry
import ifcopenshell.api.group
import ifcopenshell.api.pset
import ifcopenshell.api.root
import ifcopenshell.geom
@@ -773,6 +774,32 @@ class Drawing(bonsai.core.tool.Drawing):
def get_drawing_target_view(cls, drawing: ifcopenshell.entity_instance) -> str:
return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("TargetView", "MODEL_VIEW")
@classmethod
def ensure_drawings_parent_document(cls) -> ifcopenshell.entity_instance:
ifc_file = tool.Ifc.get()
for document in ifc_file.by_type("IfcDocumentInformation"):
if document.Name == "DRAWINGS" and document.Scope == "DRAWINGS":
return document
document = ifcopenshell.api.document.add_information(ifc_file)
if ifc_file.schema == "IFC2X3":
attributes = {"DocumentId": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
else:
attributes = {"Identification": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
ifcopenshell.api.document.edit_information(ifc_file, information=document, attributes=attributes)
return document
@classmethod
def ensure_drawings_parent_group(cls) -> ifcopenshell.entity_instance:
ifc_file = tool.Ifc.get()
for group in ifc_file.by_type("IfcGroup"):
if group.Name == "DRAWINGS" and group.ObjectType == "DRAWINGS":
return group
group = ifcopenshell.api.group.add_group(ifc_file)
ifcopenshell.api.group.edit_group(
ifc_file, group=group, attributes={"Name": "DRAWINGS", "ObjectType": "DRAWINGS"}
)
return group
@classmethod
def get_group_elements(cls, group: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
for rel in group.IsGroupedBy or []:
@@ -1140,10 +1167,7 @@ class Drawing(bonsai.core.tool.Drawing):
new = documents_collection.add()
new.ifc_definition_id = schedule.id()
new.name = schedule.Name or "Unnamed"
if tool.Ifc.get_schema() == "IFC2X3":
new.identification = schedule.DocumentId
else:
new.identification = schedule.Identification
new.identification = tool.Document.get_document_information_id(schedule) or ""
@classmethod
def get_sheet_identification(cls, sheet: ifcopenshell.entity_instance) -> str:
@@ -1184,10 +1208,7 @@ class Drawing(bonsai.core.tool.Drawing):
new.ifc_definition_id = reference.id()
new.is_sheet = False
if tool.Ifc.get_schema() == "IFC2X3":
new.identification = reference.ItemReference or ""
else:
new.identification = reference.Identification or ""
new.identification = tool.Document.get_external_reference_id(reference) or ""
new.name = os.path.basename(reference.Location)
new.reference_type = reference_description
@@ -2423,9 +2444,8 @@ class Drawing(bonsai.core.tool.Drawing):
def get_reference_document(
cls, reference: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
if tool.Ifc.get_schema() == "IFC2X3":
return reference.ReferenceToDocument[0]
return reference.ReferencedDocument
# TODO: migrate to document.get_reference_document.
return tool.Document.get_reference_document(reference)
@classmethod
def select_assigned_product(cls, context: bpy.types.Context) -> None:
+1 -2
View File
@@ -33,7 +33,6 @@ from typing import (
Optional,
TypeGuard,
Union,
cast,
get_args,
)
@@ -2187,7 +2186,7 @@ class Geometry(bonsai.core.tool.Geometry):
setattr(item, attribute.name, attribute.get_value())
if item.is_a("IfcSweptAreaSolid"):
item_profile = cast(str, props.item_profile)
item_profile = props.item_profile
profile = item.SweptArea
profile_name: Union[str, None] = profile.ProfileName
if item_profile == "-":
+2 -1
View File
@@ -29,6 +29,7 @@ from typing import TYPE_CHECKING, Any, Union
import bpy
import bonsai.core.tool
import bonsai.tool as tool
from bonsai.bim import import_ifc
from bonsai.bim.ifc import IfcStore
@@ -50,7 +51,7 @@ if TYPE_CHECKING:
from bonsai.bim.module.ifcgit.prop import IfcGitProperties
class IfcGit:
class IfcGit(bonsai.core.tool.IfcGit):
STEP_IDS = dict[str, set[int]]
@classmethod
-1
View File
@@ -197,7 +197,6 @@ class Loader(bonsai.core.tool.Loader):
cls, blender_material: bpy.types.Material, surface_style: ifcopenshell.entity_instance
) -> None:
surface_style = cls.surface_style_to_dict(surface_style)
surface_style: dict[str, Any]
cls.create_surface_style_shading(blender_material, surface_style)
+4
View File
@@ -112,10 +112,14 @@ class Misc(bonsai.core.tool.Misc):
reading data and never writing, to avoid the possibility of corrupting user preferences.
"""
# Byte offset of UserDef.user_menus within the UserDef C struct, per (major, minor)
# Blender version. Shifts whenever UserDef's fields change, so must be re-derived
# per version (e.g. from that Blender build's SDNA).
OFFSET_USER_MENUS: dict[tuple[int, int], int] = {
(4, 5): 10032,
(5, 0): 10032,
(5, 1): 10032,
(5, 2): 10800,
}
@classmethod
+1 -1
View File
@@ -232,7 +232,7 @@ class Raycast(bonsai.core.tool.Raycast):
return final_2d, v2
@classmethod
def intersect_mouse_2d_bounding_box(cls, mouse_pos: tuple[int, int], bbox: list[float, float, float, float]):
def intersect_mouse_2d_bounding_box(cls, mouse_pos: tuple[int, int], bbox: list[float]):
x, y = mouse_pos
xmin, xmax, ymin, ymax = bbox
+2 -2
View File
@@ -38,8 +38,8 @@ Hope your day's going well. :)
<script>
// Define the mapping of versions to URLs
const versionURLs = {
stable: 'http://docs.bonsaibim.org/',
unstable: 'http://docs-unstable.bonsaibim.org/',
stable: 'https://docs.bonsaibim.org/',
unstable: 'https://docs-unstable.bonsaibim.org/',
// Add more versions here as needed
};
@@ -44,8 +44,6 @@ When a new Blender version is released and supported:
* - File
- What to update
* - ``.github/workflows/ci-bonsai.yml``
- ``pyver`` matrix
* - ``.github/workflows/ci-bonsai-daily.yml``
- Blender download URL
@@ -61,6 +59,10 @@ When Blender ships with a new Python version:
- What to update
* - ``.github/workflows/ci-lint.yaml``
- ``MIN_BLENDER_PY_VERSION``
* - ``.github/workflows/ci-bonsai.yml``
- ``pyver`` matrix
* - ``.github/workflows/ci-bonsai-daily.yml``
- ``pyver`` matrix
* - ``.github/scripts/publish-bonsai-releases.py``
- ``CURRENT_PYTHON_VERSION``
* - ``src/bonsai/Makefile``
@@ -77,18 +77,3 @@ the image below. Three simple open source online viewers you can test with are
<https://3dviewer.net/>`__.
.. image:: images/ifc-pipeline.png
Placing occurrences of an element type
--------------------------------------
TODO
Changing the locations of elements
----------------------------------
TODO
Modeling a simple building
--------------------------
TODO
-6
View File
@@ -18,12 +18,6 @@ dependencies = [
"ifcopenshell",
]
[project.optional-dependencies]
dev = [
"pytest-blender",
"pytest-bdd",
]
[project.urls]
Homepage = "http://bonsaibim.org"
Documentation = "https://docs.bonsaibim.org"
+3
View File
@@ -0,0 +1,3 @@
pytest
pytest-blender
pytest-bdd
+3 -1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
"""Setup Bonsai Development Environment.
Script links existing Bonsai installation to the provided IfcOpenShell repository.
@@ -78,7 +79,8 @@ BONSAI_PATH = find_bonsai_path()
# ---------------------------
# Never changed by user.
PYTHON_VERSION = "3.13" if BLENDER_VERSION == "5.1" else "3.11"
BLENDER_VERSION_INT = tuple(map(int, BLENDER_VERSION.split(".")))
PYTHON_VERSION = "3.13" if BLENDER_VERSION_INT >= (5, 1) else "3.11"
PACKAGE_PATH = BLENDER_PATH / rf"extensions/.local/lib/python{PYTHON_VERSION}/site-packages"
+2 -2
View File
@@ -12,7 +12,7 @@ Scenario: Ensure added booleans are marked as manual
And I click "OK"
And the object "IfcFurniture/Unnamed" exists
And I toggle edit mode
And the object "Item/IfcExtrudedAreaSolid/77" exists
And the object "Item/IfcExtrudedAreaSolid/73" exists
And I open the "Add Item" menu
When I click "Half Space Solid"
And the object "Item/IfcHalfSpaceSolid/90" exists
@@ -33,7 +33,7 @@ Scenario: Ensure removed booleans are unmarked as manual
And I click "OK"
And the object "IfcFurniture/Unnamed" exists
And I toggle edit mode
And the object "Item/IfcExtrudedAreaSolid/77" exists
And the object "Item/IfcExtrudedAreaSolid/73" exists
And I open the "Add Item" menu
And I click "Half Space Solid"
And I deselect all objects
+5 -5
View File
@@ -24,7 +24,7 @@ Scenario: Add element - an element with no geometry
When I click "OK"
And I select the object "IfcFurniture/Unnamed"
And I toggle edit mode
Then the object "Item/IfcExtrudedAreaSolid/77" exists
Then the object "Item/IfcExtrudedAreaSolid/73" exists
Scenario: Add element - an element with extrusion geometry
Given an empty IFC project
@@ -36,7 +36,7 @@ Scenario: Add element - an element with extrusion geometry
When I click "OK"
And I select the object "IfcFurniture/Unnamed"
And I toggle edit mode
Then the object "Item/IfcExtrudedAreaSolid/77" exists
Then the object "Item/IfcExtrudedAreaSolid/73" exists
Scenario: Add element - an element with custom tessellation geometry
Given an empty IFC project
@@ -48,7 +48,7 @@ Scenario: Add element - an element with custom tessellation geometry
When I click "OK"
And I select the object "IfcFurniture/Unnamed"
And I toggle edit mode
Then the object "Item/IfcPolygonalFaceSet/76" exists
Then the object "Item/IfcPolygonalFaceSet/72" exists
Scenario: Add element - an element with tessellation geometry from an object
Given an empty IFC project
@@ -62,8 +62,8 @@ Scenario: Add element - an element with tessellation geometry from an object
When I click "OK"
And I select the object "IfcFurniture/Unnamed"
And I toggle edit mode
Then the object "Item/IfcPolygonalFaceSet/76" exists
And the object "Item/IfcPolygonalFaceSet/76" dimensions are "2,2,2"
Then the object "Item/IfcPolygonalFaceSet/72" exists
And the object "Item/IfcPolygonalFaceSet/72" dimensions are "2,2,2"
Scenario: Reassign class
Given an empty IFC project
@@ -12,7 +12,7 @@ Scenario: Add element - a structural point connection
And I make the collection "IfcStructuralItem" visible
And I select the object "IfcStructuralPointConnection/Foo"
And I toggle edit mode
Then the object "Item/IfcVertexPoint/69" exists
Then the object "Item/IfcVertexPoint/65" exists
Scenario: Add element - a structural curve member
Given an empty IFC project
@@ -25,7 +25,7 @@ Scenario: Add element - a structural curve member
And I make the collection "IfcStructuralItem" visible
And I select the object "IfcStructuralCurveMember/Foo"
And I toggle edit mode
Then the object "Item/IfcEdge/72" exists
Then the object "Item/IfcEdge/68" exists
Scenario: Add element - a structural surface member
Given an empty IFC project
@@ -38,7 +38,7 @@ Scenario: Add element - a structural surface member
And I make the collection "IfcStructuralItem" visible
And I select the object "IfcStructuralSurfaceMember/Foo"
And I toggle edit mode
Then the object "Item/IfcFace/74" exists
Then the object "Item/IfcFace/70" exists
Scenario: Load structural analysis models
Given an empty IFC project
@@ -0,0 +1,68 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2026
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
#
# This file was generated with the assistance of an AI coding tool.
import os
import time
from pathlib import Path
import pytest
from bonsai.tool.autosave import AUTOSAVED_SUFFIX, AUTOSAVING_SUFFIX, Autosave
pytestmark = pytest.mark.project
class TestAutosavePaths:
def test_get_paths_for_ifc_file(self):
main_path, autosaving_path, autosaved_path = Autosave.get_paths("/tmp/myfile.ifc")
assert main_path == Path("/tmp/myfile.ifc")
assert autosaving_path == Path(f"/tmp/myfile{AUTOSAVING_SUFFIX}")
assert autosaved_path == Path(f"/tmp/myfile{AUTOSAVED_SUFFIX}")
def test_get_newer_autosaved_path_when_missing(self, tmp_path):
ifc_path = tmp_path / "myfile.ifc"
ifc_path.write_text("ifc")
assert Autosave.get_newer_autosaved_path(ifc_path) is None
def test_get_newer_autosaved_path_when_older(self, tmp_path):
ifc_path = tmp_path / "myfile.ifc"
autosaved_path = tmp_path / f"myfile{AUTOSAVED_SUFFIX}"
ifc_path.write_text("ifc")
autosaved_path.write_text("autosaved")
past = time.time() - 10
os.utime(ifc_path, (past, past))
os.utime(autosaved_path, (time.time(), time.time()))
assert Autosave.get_newer_autosaved_path(ifc_path) == autosaved_path.as_posix()
def test_get_newer_autosaved_path_when_not_newer(self, tmp_path):
ifc_path = tmp_path / "myfile.ifc"
autosaved_path = tmp_path / f"myfile{AUTOSAVED_SUFFIX}"
ifc_path.write_text("ifc")
autosaved_path.write_text("autosaved")
now = time.time()
os.utime(ifc_path, (now, now))
past = now - 10
os.utime(autosaved_path, (past, past))
assert Autosave.get_newer_autosaved_path(ifc_path) is None
def test_get_newer_autosaved_path_ignores_non_ifc(self, tmp_path):
path = tmp_path / "myfile.ifczip"
path.write_text("zip")
assert Autosave.get_newer_autosaved_path(path) is None
+2 -2
View File
@@ -30,7 +30,7 @@ from collections.abc import Generator
from inspect import signature
from math import radians
from pathlib import Path
from typing import Any, Union
from typing import Any, Union, cast
import bpy
import ifcopenshell
@@ -140,7 +140,7 @@ class PanelSpy:
else:
props = kwargs.get("data")
name = kwargs.get("property")
props: bpy.types.bpy_struct
props = cast(bpy.types.bpy_struct, props)
text = kwargs.get("text", props.bl_rna.properties[name].name)
icon = kwargs.get("icon", None)
prop_type = props.bl_rna.properties[name].type
+12 -2
View File
@@ -347,11 +347,13 @@ class TestAddDrawing:
context="context",
ifc_representation_class=None,
).should_be_called().will_return("element")
drawing.ensure_drawings_parent_group().should_be_called().will_return("drawings_parent_group")
ifc.run("group.add_group").should_be_called().will_return("group")
ifc.run(
"group.edit_group", group="group", attributes={"Name": "name", "ObjectType": "DRAWING"}
).should_be_called()
ifc.run("group.assign_group", group="group", products=["element"]).should_be_called()
ifc.run("group.assign_group", group="drawings_parent_group", products=["group"]).should_be_called()
collector.assign("obj").should_be_called()
ifc.run("pset.add_pset", product="element", name="EPset_Drawing").should_be_called().will_return("pset")
drawing.get_default_drawing_resource_path("Stylesheet").should_be_called().will_return("stylesheet.css")
@@ -381,8 +383,11 @@ class TestAddDrawing:
"CurrentShadingStyle": "Blender Default",
},
).should_be_called()
drawing.ensure_drawings_parent_document().should_be_called().will_return("drawings_parent_document")
drawing.get_default_drawing_path("name").should_be_called().will_return("uri")
ifc.run("document.add_information").should_be_called().will_return("information")
ifc.run("document.add_information", parent="drawings_parent_document").should_be_called().will_return(
"information"
)
ifc.run("document.add_reference", information="information").should_be_called().will_return("reference")
ifc.get_schema().should_be_called().will_return("IFC4")
ifc.run(
@@ -406,11 +411,13 @@ class TestDuplicateDrawing:
drawing.set_name("new_drawing", "unique_name").should_be_called()
drawing.get_drawing_group("new_drawing").should_be_called().will_return("group")
ifc.run("group.unassign_group", group="group", products=["new_drawing"]).should_be_called()
drawing.ensure_drawings_parent_group().should_be_called().will_return("drawings_parent_group")
ifc.run("group.add_group").should_be_called().will_return("new_group")
ifc.run(
"group.edit_group", group="new_group", attributes={"Name": "unique_name", "ObjectType": "DRAWING"}
).should_be_called()
ifc.run("group.assign_group", group="new_group", products=["new_drawing"]).should_be_called()
ifc.run("group.assign_group", group="drawings_parent_group", products=["new_group"]).should_be_called()
drawing.get_group_elements("group").should_be_called().will_return(["drawing", "annotation"])
ifc.get_object("annotation").should_be_called().will_return("annotation_obj")
geometry.duplicate_ifc_objects(["annotation_obj"]).should_be_called().will_return(
@@ -425,7 +432,10 @@ class TestDuplicateDrawing:
drawing.get_drawing_document("new_drawing").should_be_called().will_return("old_reference")
ifc.run("document.unassign_document", products=["new_drawing"], document="old_reference").should_be_called()
ifc.run("document.add_information").should_be_called().will_return("information")
drawing.ensure_drawings_parent_document().should_be_called().will_return("drawings_parent_document")
ifc.run("document.add_information", parent="drawings_parent_document").should_be_called().will_return(
"information"
)
ifc.run("document.add_reference", information="information").should_be_called().will_return("reference")
ifc.get_schema().should_be_called().will_return("IFC4")
drawing.get_default_drawing_path("unique_name").should_be_called().will_return("drawing_path")
+1
View File
@@ -203,6 +203,7 @@ class TestGetDebugInfo(NewFile):
"bonsai_version",
"bonsai_commit_hash",
"bonsai_commit_date",
"bonsai_git_branch",
"last_actions",
"last_error",
}
+22 -1
View File
@@ -21,6 +21,7 @@ import http.server
import time
import urllib.parse
import uuid
import warnings
import webbrowser
from typing import TYPE_CHECKING, Any, Literal, Optional, TypedDict
@@ -521,7 +522,17 @@ class Client:
headers = {"User-Agent": "IfcOpenShell.bSDD.py/0.8.0"}
if is_auth_required:
headers["Authorization"] = "Bearer " + self.get_access_token()
return requests.get(f"{self.baseurl}{endpoint}", timeout=10, headers=headers, params=params or None).json()
response = requests.get(f"{self.baseurl}{endpoint}", timeout=10, headers=headers, params=params or None)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
try:
data = response.json()
message = data.get("message", data.get("error", str(e)))
except requests.exceptions.JSONDecodeError:
message = response.text or str(e)
raise requests.exceptions.HTTPError(f"{e}: {message}", response=response) from e
return response.json()
def _get_deprecated(self, endpoint, params=None, is_auth_required=False):
headers = {"User-Agent": "IfcOpenShell.bSDD.py/0.8.0"}
@@ -769,6 +780,16 @@ class Client:
Get Class details
this API replaces Classification
"""
# Not very well documented on bsdd side,
# the deprecation note only occurs when you run into rate limit.
# See https://github.com/buildingSMART/bSDD/issues/149
if include_class_properties:
warnings.warn(
"include_class_properties=True is deprecated and heavily rate-limited by the bSDD API. "
"Use get_class_properties() instead.",
DeprecationWarning,
stacklevel=2,
)
endpoint = f"Class/v{version}"
params = {
"Uri": class_uri,
+2 -1
View File
@@ -31,6 +31,7 @@ def test_get_nbs_classes():
def test_get_class():
uri_light_fixture = next(l for l in get_ifc_classes()["classes"] if "IfcLightFixture" == l["code"])["uri"]
# TODO: fix deprecation warning.
ifc4x3_light_fixture = client.get_class(uri_light_fixture)
assert "Maintenance Factor" and "Light Fixture Mounting Type" in [
l["name"] for l in ifc4x3_light_fixture["classProperties"]
@@ -39,7 +40,7 @@ def test_get_class():
def test_get_class_relations():
uri_light_fixture = next(l for l in get_ifc_classes()["classes"] if "IfcLightFixture" == l["code"])["uri"]
ifc4x3_light_fixture_relations = client.get_class_properties(uri_light_fixture, True)
ifc4x3_light_fixture_relations = client.get_class_relations(uri_light_fixture, True)
assert "Electrical unit for light-line system" and "Tubelight system" in [
r["className"] for r in ifc4x3_light_fixture_relations["classRelations"]
]
+6 -1
View File
@@ -28,6 +28,7 @@ class P62Ifc:
self.file = None
self.work_plan = None
self.project = {}
self.default_calendar_id = None
self.calendars = {}
self.wbs = {}
self.root_activites = []
@@ -89,6 +90,7 @@ class P62Ifc:
self.ns = {"pr": root.tag[1:].partition("}")[0]}
project = root.find("pr:Project", self.ns)
self.project["Name"] = project.findtext("pr:Name") or "Unnamed"
self.default_calendar_id = project.findtext("pr:ActivityDefaultCalendarObjectId", namespaces=self.ns)
self.parse_calendar_xml(root)
self.parse_calendar_xml(project)
self.parse_wbs_xml(project)
@@ -174,6 +176,9 @@ class P62Ifc:
self.wbs[wbs_id]["activities"].append(activity_id)
else:
self.root_activites.append(activity_id)
# CalendarObjectId is optional in the P6 schema: an activity without one
# inherits the project's ActivityDefaultCalendarObjectId.
calendar_id = activity.findtext("pr:CalendarObjectId", namespaces=self.ns)
self.activities[activity_id] = {
"Name": activity.find("pr:Name", self.ns).text,
"Identification": activity.find("pr:Id", self.ns).text,
@@ -181,7 +186,7 @@ class P62Ifc:
"FinishDate": datetime.datetime.fromisoformat(activity.find("pr:FinishDate", self.ns).text),
"PlannedDuration": activity.find("pr:PlannedDuration", self.ns).text,
"Status": activity.find("pr:Status", self.ns).text,
"CalendarObjectId": activity.find("pr:CalendarObjectId", self.ns).text,
"CalendarObjectId": calendar_id or self.default_calendar_id,
"ifc": None,
}
+7 -1
View File
@@ -82,6 +82,11 @@ MAIN_CSV_HEADER_COLUMNS.extend(
)
class CostRate(TypedDict):
Schedule: str | None
RateID: str | None
class CostItem(TypedDict):
children: list[CostItem]
ifc: NotRequired[ifcopenshell.entity_instance]
@@ -97,6 +102,7 @@ class CostItem(TypedDict):
Property: Union[str, None]
Query: Union[str, None]
CostRate: CostRate | None
Formula: Union[str, None]
# QuantityClass: Union[str, None]
@@ -239,7 +245,7 @@ class Csv2Ifc:
cost_values = float(cost_values) if cost_values else None
if self.has_rates:
cost_rate = {
cost_rate: CostRate = {
"Schedule": row[(self.headers["RateSchedule"])] if "RateSchedule" in self.headers else None,
"RateID": row[(self.headers["RateID"])] if "RateID" in self.headers else None,
}
+3 -2
View File
@@ -26,7 +26,8 @@ import logging
import os
import time
from collections import Counter
from typing import Optional, TypedDict, Union
from typing import Optional, Union
from typing_extensions import TypedDict
import ifcopenshell
import ifcopenshell.util.cost
@@ -35,7 +36,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
class CostItem(TypedDict):
class CostItem(TypedDict, extra_items=float):
# Exported columns.
Index: int
Hierarchy: str
+1
View File
@@ -17,6 +17,7 @@ classifiers = [
]
dependencies = [
"ifcopenshell",
"typing_extensions",
]
[project.optional-dependencies]
+4 -1
View File
@@ -53,6 +53,8 @@ class ClashResult(TypedDict):
p1: list[float]
p2: list[float]
distance: float
# Added by `Clasher.smart_group_clashes`.
smart_group: NotRequired[int]
class ClashSet(TypedDict):
@@ -286,7 +288,8 @@ class Clasher:
positions = []
for clash in clashes.values():
positions.append(clash["position"])
# Midpoint of p1/p2 as an approximation of the clash location for clustering purposes.
positions.append([(a + b) / 2 for a, b in zip(clash["p1"], clash["p2"])])
data = np.array(positions)
+21 -10
View File
@@ -252,6 +252,10 @@ int main(int argc, char** argv) {
("stderr-progress", "output progress to stderr stream")
("yes,y", "answer 'yes' automatically to possible confirmation queries (e.g. overwriting an existing output file)")
("no-progress", "suppress possible progress bar type of prints that use carriage return")
("fail-on-error", "return a non-zero exit code when one or more errors were logged during "
"geometry conversion (e.g. an element failed to convert). By default IfcConvert exits "
"successfully as long as an output file could be written, even if some elements were "
"silently dropped. Enable this flag so scripts and CI can detect partial conversions.")
("log-format", po::value<std::string>(&log_format), "log format: plain or json")
("log-file", new po::typed_value<path_t, char_t>(&log_file), "redirect log output to file");
@@ -449,6 +453,7 @@ int main(int argc, char** argv) {
const bool mmap = vmap.count("mmap") != 0;
const bool no_progress = vmap.count("no-progress") != 0;
const bool fail_on_error = vmap.count("fail-on-error") != 0;
const bool quiet = vmap.count("quiet") != 0;
const bool stderr_progress = vmap.count("stderr-progress") != 0;
@@ -670,7 +675,7 @@ int main(int argc, char** argv) {
time_t start, end;
time(&start);
if (output_extension == XML) {
XmlSerializer s(ifc_file, IfcUtil::path::to_utf8(output_temp_filename), logger);
XmlSerializer s(ifc_file, IfcUtil::path::to_utf8(output_temp_filename), &logger);
logger.Status("Writing XML output...");
s.finalize();
} else {
@@ -829,14 +834,14 @@ int main(int argc, char** argv) {
if (output_extension == OBJ) {
// Do not use temp file for MTL as it's such a small file.
const path_t mtl_filename = change_extension(output_filename, MTL);
serializer = boost::make_shared<WaveFrontOBJSerializer>(IfcUtil::path::to_utf8(output_temp_filename), IfcUtil::path::to_utf8(mtl_filename), geometry_settings, serializer_settings, logger);
serializer = boost::make_shared<WaveFrontOBJSerializer>(IfcUtil::path::to_utf8(output_temp_filename), IfcUtil::path::to_utf8(mtl_filename), geometry_settings, serializer_settings, &logger);
#ifdef WITH_OPENCOLLADA
} else if (output_extension == DAE) {
serializer = boost::make_shared<ColladaSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, logger);
serializer = boost::make_shared<ColladaSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, &logger);
#endif
#ifdef WITH_GLTF
} else if (output_extension == GLB) {
serializer = boost::make_shared<GltfSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, logger);
serializer = boost::make_shared<GltfSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, &logger);
#endif
#ifdef WITH_USD
} else if (output_extension == USD || output_extension == USDA || output_extension == USDC) {
@@ -853,15 +858,15 @@ int main(int argc, char** argv) {
serializer = boost::make_shared<IgesSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, logger);
} else if (output_extension == SVG) {
geometry_settings.get<ifcopenshell::geometry::settings::IteratorOutput>().value = ifcopenshell::geometry::settings::NATIVE;
serializer = boost::make_shared<SvgSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, logger);
serializer = boost::make_shared<SvgSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, &logger);
#ifdef WITH_HDF5
} else if (output_extension == HDF) {
geometry_settings.get<ifcopenshell::geometry::settings::IteratorOutput>().value = ifcopenshell::geometry::settings::NATIVE;
serializer = boost::make_shared<HdfSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, false, logger);
serializer = boost::make_shared<HdfSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, false, &logger);
#endif
#endif
#endif
} else if (output_extension == TTL) {
serializer = boost::make_shared<TtlWktSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, logger);
serializer = boost::make_shared<TtlWktSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, &logger);
} else {
cerr_ << "[Error] Unknown output filename extension '" << output_extension << "'\n";
write_log(!quiet);
@@ -885,6 +890,7 @@ int main(int argc, char** argv) {
}
if (!serializer->ready()) {
logger.Error("SYS", 25, "Unable to open output file '" + IfcUtil::path::to_utf8(output_filename) + "' for writing; check that the directory exists and is writable");
IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename));
write_log(!quiet);
return EXIT_FAILURE;
@@ -1005,7 +1011,7 @@ int main(int argc, char** argv) {
if (!vmap.count("cache-file")) {
cache_file = input_filename + CACHE + HDF;
}
cache.reset(new HdfSerializer(IfcUtil::path::to_utf8(cache_file), geometry_settings, serializer_settings, false, logger));
cache.reset(new HdfSerializer(IfcUtil::path::to_utf8(cache_file), geometry_settings, serializer_settings, false, &logger));
context_iterator->set_cache(cache.get());
}
#endif
@@ -1220,6 +1226,11 @@ int main(int argc, char** argv) {
successful = false;
}
if (fail_on_error && logger.MaxSeverity() >= Logger::LOG_ERROR) {
logger.Error("SYS", 26, "Errors encountered during processing, failing due to --fail-on-error.");
successful = false;
}
if (logger.Verbosity() == Logger::LOG_PERF) {
logger.PrintPerformanceStats();
}
@@ -1279,7 +1290,7 @@ bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file,
#ifdef WITH_IFCXML
if (boost::ends_with(boost::to_lower_copy(filename), ".ifcxml")) {
ifc_file = IfcParse::parse_ifcxml(filename, logger);
ifc_file = IfcParse::parse_ifcxml(filename, &logger);
} else
#endif
{
+10 -4
View File
@@ -51,8 +51,10 @@ class IfcDiff:
:param old: IFC file object for the old model
:param new: IFC file object for the new model
:param relationships: List of relationships to check. None means that only
geometry is compared. See RELATIONSHIP_TYPE for available relationships.
:param relationships: List of relationships to check. None means that
attributes and geometry are compared, so changes such as a modified or
removed PredefinedType are reported. See RELATIONSHIP_TYPE for available
relationships.
:param is_shallow: True if you want only the first difference to be listed.
False if you want all differences to be checked. Choosing False means
that comparisons will take longer.
@@ -86,7 +88,7 @@ class IfcDiff:
self.new = new
self.change_register = {}
self.representation_ids = {}
self.relationships = relationships or ["geometry"]
self.relationships = relationships or ["attributes", "geometry"]
self.precision = 1e-4
self.is_shallow = is_shallow
self.filter_elements = filter_elements
@@ -435,7 +437,11 @@ if __name__ == "__main__":
"-r",
"--relationships",
type=str,
help='A list of space-separated relationships, chosen from "type", "property", "container", "aggregate", "classification"',
help=(
'A list of space-separated relationships, chosen from "attributes", "geometry", '
'"type", "property", "container", "aggregate", "classification". '
'Defaults to "attributes geometry" when omitted.'
),
default="",
)
args = parser.parse_args()
+17
View File
@@ -77,6 +77,23 @@ class TestIfcDiff:
assert ifc_diff.deleted_elements == set()
assert ifc_diff.change_register == {wall.GlobalId: {"attributes_changed": True}}
def test_changed_predefined_type_is_caught_by_default(self):
# Regression test for #8214: a plain diff (no relationships specified)
# must report a modified or removed PredefinedType. Previously the
# default only compared geometry, so attribute-only edits were missed.
ifc_file = setup_project()
wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall", name="Foo")
wall.PredefinedType = "SOLIDWALL"
new_file = ifc_file.from_string(ifc_file.to_string())
new_file.by_id(wall.id()).PredefinedType = "NOTDEFINED"
ifc_diff = ifcdiff.IfcDiff(ifc_file, new_file)
ifc_diff.diff()
assert ifc_diff.added_elements == set()
assert ifc_diff.deleted_elements == set()
assert ifc_diff.change_register == {wall.GlobalId: {"attributes_changed": True}}
def test_changed_geometry(self):
ifc_file = setup_project()
wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall", name="Foo")
+1 -1
View File
@@ -258,7 +258,7 @@ ifcedit quantify run model.ifc IFC4QtoBaseQuantities -o model_qto.ifc
Options:
- `--selector <query>` -- ifcopenshell selector to restrict elements (default: all `IfcElement`)
- `--selector <query>` -- ifcopenshell selector to restrict elements (default: all `IfcElement` and `IfcSpace`)
- `-o, --output <path>` -- write to a different file instead of overwriting the input
Note: `quantify run` writes geometry-based measurements and requires the
+3 -1
View File
@@ -244,7 +244,9 @@ def main():
qrun_parser = quantify_sub.add_parser("run", help="Run QTO on an IFC file")
qrun_parser.add_argument("ifc_file", help="Path to the IFC file")
qrun_parser.add_argument("rule_name", help="QTO rule name (e.g. IFC4QtoBaseQuantities)")
qrun_parser.add_argument("--selector", help="ifcopenshell selector to restrict elements (default: all IfcElement)")
qrun_parser.add_argument(
"--selector", help="ifcopenshell selector to restrict elements (default: all IfcElement and IfcSpace)"
)
qrun_parser.add_argument("-o", "--output", help="Output file path (default: overwrite input)")
args, extra = parser.parse_known_args()
+3 -1
View File
@@ -60,9 +60,11 @@ def coerce_value(
# Union / Optional
if origin is typing.Union:
non_none_types = [a for a in args if a is not type(None)]
if value_str.lower() == "none":
if isinstance(value_str, str) and value_str.lower() == "none":
if type(None) in args:
return None
if value_str is None and type(None) in args:
return None
# Try each non-None type in order
for t in non_none_types:
try:
+1 -1
View File
@@ -30,7 +30,7 @@ def run_quantify(model: ifcopenshell.file, rule: str, selector: str | None = Non
if selector:
elements = set(ifcopenshell.util.selector.filter_elements(model, selector))
else:
elements = set(model.by_type("IfcElement"))
elements = set(model.by_type("IfcElement")) | set(model.by_type("IfcSpace"))
results = quantify(model, elements, rule_sets[rule])
edit_qtos(model, results)
+10
View File
@@ -57,6 +57,16 @@ class TestOptionalCoercion:
def test_optional_int(self):
assert coerce_value("42", Optional[int]) == 42
def test_optional_entity_native_int(self, model):
# MCP callers pass JSON-decoded native types (int), not CLI strings.
wall = model.by_type("IfcWall")[0]
result = coerce_value(wall.id(), Optional[ifcopenshell.entity_instance], model)
assert result == wall
def test_optional_entity_native_none(self, model):
# JSON null decodes to Python None, not the string "none".
assert coerce_value(None, Optional[ifcopenshell.entity_instance], model) is None
class TestUnionCoercion:
def test_union_str_int(self):
+17
View File
@@ -85,3 +85,20 @@ class TestRunQuantify:
def test_empty_selector_runs_on_all(self, quantify_model):
result = run_quantify(quantify_model, "IFC4QtoBaseQuantities", selector=None)
assert result["ok"] is True
def test_default_selector_includes_spaces(self, quantify_model, monkeypatch):
"""IfcSpace is not a subtype of IfcElement, so the default scope must add it explicitly."""
import ifc5d.qto
seen_elements = {}
def fake_quantify(ifc_file, elements, rules):
seen_elements["elements"] = elements
return {}
monkeypatch.setattr(ifc5d.qto, "quantify", fake_quantify)
space = ifcopenshell.api.root.create_entity(quantify_model, ifc_class="IfcSpace", name="TestSpace")
run_quantify(quantify_model, "IFC4QtoBaseQuantities")
assert space in seen_elements["elements"]
+6
View File
@@ -28,6 +28,7 @@ import ifcopenshell.util.fm
import ifcopenshell.util.placement
import ifcopenshell.util.shape
import ifcopenshell.util.system
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import np_matrix_to_euler
# The original BIMServer plugin has a function called ifcToCOBie:
@@ -920,6 +921,11 @@ def get_coordinate_data_(element: ifcopenshell.entity_instance) -> Generator[dic
verts = ifcopenshell.util.shape.get_shape_vertices(shape, shape.geometry)
categories = ("box-lowerleft", "box-upperright")
bbox = ifcopenshell.util.shape.get_bbox(verts)
# Geometry vertices are in SI metres, but Floor rows use the raw placement in
# project length units. Convert space points to project units so the whole
# Coordinate sheet is consistent with the Facility LinearUnits.
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(element.file)
bbox = [point / unit_scale for point in bbox]
base_data = base_data | {
"Category": "point",
"SheetName": "Space",
+2 -2
View File
@@ -361,8 +361,8 @@ namespace ifcopenshell {
struct CircleSegments : public SettingBase<CircleSegments, int> {
static constexpr const char* const name = "circle-segments";
static constexpr const char* const description = "Number of segments to approximate full circles in CGAL kernel.";
static constexpr int defaultvalue = 16;
static constexpr const char* const description = "Number of segments to approximate full circles in the CGAL kernel. When 0 (the default) the segment count is derived from mesher-linear-deflection instead, so curves stay within the deflection tolerance regardless of radius.";
static constexpr int defaultvalue = 0;
};
struct CgalSmoothAngleDegrees : public SettingBase<CgalSmoothAngleDegrees, double> {
+35 -1
View File
@@ -391,6 +391,11 @@ namespace {
}
};
// Representative radius used to size the polygonal approximation of a conic.
// For an ellipse the larger semi-axis is the conservative choice.
inline double conic_radius(const taxonomy::circle::ptr& c) { return c->radius; }
inline double conic_radius(const taxonomy::ellipse::ptr& e) { return e->radius > e->radius2 ? e->radius : e->radius2; }
struct cgal_curve_creation_visitor {
Settings& settings_;
parameter_range param;
@@ -425,7 +430,36 @@ namespace {
if (b <= a) {
b += 2 * M_PI;
}
int num_segments = (int)std::ceil(std::fabs(a - b) / (2 * M_PI) * settings_.get<settings::CircleSegments>().get());
const double span = std::fabs(a - b);
// CircleSegments controls how conics (circles, ellipses, arcs) are approximated
// in the CGAL kernel. Two modes, one or the other:
// - CircleSegments == 0 (the default): the segment count is derived from
// MesherLinearDeflection, so the chord deviation stays within the mesher's
// linear deflection regardless of radius. This matches the deflection based
// meshing the OpenCascade kernel already does and fixes issue #8051, where
// large radius arcs (curved curtain wall mullions) collapsed to straight chords
// because a fixed segment count is radius agnostic.
// - CircleSegments > 0: it is used directly as the number of segments for a full
// circle, giving deterministic, radius independent output.
int num_segments;
const int circle_segments = settings_.get<settings::CircleSegments>().get();
if (circle_segments > 0) {
num_segments = (int)std::ceil(span / (2 * M_PI) * circle_segments);
} else {
const double radius = conic_radius(t);
const double deflection = settings_.get<settings::MesherLinearDeflection>().get();
if (deflection > 0. && radius > deflection) {
const double max_segment_angle = 2.0 * std::acos(1.0 - deflection / radius);
num_segments = (int)std::ceil(span / max_segment_angle);
} else {
// Radius within the deflection tolerance (or no deflection set): a chord per
// quarter turn already keeps the deviation within tolerance.
num_segments = (int)std::ceil(span / (M_PI / 2.));
}
}
if (num_segments < 1) {
num_segments = 1;
}
double du = (b - a) / num_segments;
taxonomy::point3 P;
// @nb for loop is not inclusive of the both end points
+22
View File
@@ -31,6 +31,7 @@
#include <ShapeFix_Shape.hxx>
#include <ShapeFix_ShapeTolerance.hxx>
#include <BRep_Tool.hxx>
#include <BRepExtrema_DistShapeShape.hxx>
#include <Standard_Macro.hxx>
#include <TopoDS_Shape.hxx>
@@ -356,6 +357,27 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
return false;
}
// #527: A face whose inner boundary intersects the outer boundary (or
// another inner boundary) is invalid per the schema. Open Cascade heals or
// drops such a face silently, so the intended hole is lost with no
// diagnostic. The distance between two non-intersecting loops is strictly
// positive; a distance at (or below) the modelling precision means the
// boundaries touch or cross. Emit a clear warning so the invalid input is
// not silently lost. wires() is ordered outer-first, inner-bounds after.
if (fd.wires().size() > 1) {
const auto& fwires = fd.wires();
bool reported = false;
for (size_t i = 1; i < fwires.size() && !reported; ++i) {
for (size_t j = 0; j < i && !reported; ++j) {
BRepExtrema_DistShapeShape dss(fwires[i], fwires[j]);
if (dss.IsDone() && dss.Value() < precision_) {
logger().Warning("GEO", 402, "Face inner boundary intersects another face boundary", face->instance);
reported = true;
}
}
}
}
if (fd.surface().IsNull()) {
// Use the first wire to find a plane manually for polygonal wires
const TopoDS_Wire& wire = fd.wires().front();
@@ -0,0 +1,93 @@
// This file was generated with the assistance of an AI coding tool.
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell 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 *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "mapping.h"
#define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry;
#include "../profile_helper.h"
// In IFC2X3 IfcAsymmetricIShapeProfileDef is a subtype of IfcIShapeProfileDef and is
// therefore dispatched (and handled) by the IfcIShapeProfileDef mapping. From IFC4
// onwards it is a standalone subtype of IfcParameterizedProfileDef with its own
// Bottom*/Top* attributes, so nothing mapped it and the extrusion came out empty.
// The presence of the standalone BottomFlangeWidth attribute is the discriminator:
// it is only defined in the schemas where the type is standalone (IFC4 / IFC4X3).
#ifdef SCHEMA_IfcAsymmetricIShapeProfileDef_HAS_BottomFlangeWidth
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAsymmetricIShapeProfileDef* inst) {
// Bottom flange (half width), overall depth (half), web (half thickness).
const double xb = inst->BottomFlangeWidth() / 2.0 * length_unit_;
const double xt = inst->TopFlangeWidth() / 2.0 * length_unit_;
const double y = inst->OverallDepth() / 2.0 * length_unit_;
const double d1 = inst->WebThickness() / 2.0 * length_unit_;
// Bottom flange thickness; top flange thickness defaults to the bottom one.
const double ftb = inst->BottomFlangeThickness() * length_unit_;
const double ftt = inst->TopFlangeThickness().get_value_or(inst->BottomFlangeThickness()) * length_unit_;
// Optional fillet radii (web/flange transition) and flange edge radii.
const double fb = inst->BottomFlangeFilletRadius().get_value_or(0.) * length_unit_;
const double ft_top = inst->TopFlangeFilletRadius().get_value_or(0.) * length_unit_;
const double feb = inst->BottomFlangeEdgeRadius().get_value_or(0.) * length_unit_;
const double fet = inst->TopFlangeEdgeRadius().get_value_or(0.) * length_unit_;
// Optional flange slopes: the inner edge of the flange rises towards the web.
const double bottomSlope = inst->BottomFlangeSlope().get_value_or(0.) * angle_unit_;
const double topSlope = inst->TopFlangeSlope().get_value_or(0.) * angle_unit_;
const double dyb = (xb - d1) * tan(bottomSlope);
const double dyt = (xt - d1) * tan(topSlope);
const double tol = settings_.get<settings::Precision>().get();
if (xb < tol || xt < tol || y < tol || d1 < tol || ftb < tol || ftt < tol) {
logger_.Message(Logger::LOG_NOTICE, "GEO", 264, "Skipping zero sized profile:", inst);
return nullptr;
}
taxonomy::matrix4::ptr m4;
bool has_position = true;
#ifdef SCHEMA_IfcParameterizedProfileDef_Position_IS_OPTIONAL
has_position = !!inst->Position();
#endif
if (has_position) {
m4 = taxonomy::cast<taxonomy::matrix4>(map(inst->Position()));
}
// Twelve corner points, running counter-clockwise from the bottom-left, with the
// bottom flange (xb) possibly wider than the top flange (xt). Fillet/edge radii are
// attached to the corner they round, matching the symmetric IfcIShapeProfileDef.
return profile_helper(m4, {
{{-xb,-y}},
{{xb,-y}},
{{xb,-y + ftb}, {feb}},
{{d1,-y + ftb + dyb},{fb} },
{{d1,y - ftt - dyt},{ft_top} },
{{xt,y - ftt}, {fet}},
{{xt,y}},
{{-xt,y}},
{{-xt,y - ftt}, {fet}},
{{-d1,y - ftt - dyt},{ft_top} },
{{-d1,-y + ftb + dyb},{fb} },
{{-xb,-y + ftb}, {feb}}
});
}
#endif
+22 -11
View File
@@ -39,8 +39,25 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPolygonalFaceSet* inst) {
int max_index = (int)points.size();
// When the optional PnIndex is present, CoordIndex values do not index into
// CoordList directly but into PnIndex, which in turn remaps to CoordList.
// Both index levels are 1-based per the IFC specification.
auto pn_index = inst->PnIndex();
auto resolve = [&](int idx) -> const taxonomy::point3::ptr& {
if (pn_index) {
if (idx < 1 || idx > (int)pn_index->size()) {
throw IfcParse::IfcException("IfcPolygonalFaceSet PnIndex out of bounds for index " + boost::lexical_cast<std::string>(idx));
}
idx = (*pn_index)[idx - 1];
}
if (idx < 1 || idx > max_index) {
throw IfcParse::IfcException("IfcPolygonalFaceSet index out of bounds for index " + boost::lexical_cast<std::string>(idx));
}
return points[idx - 1];
};
auto shell = taxonomy::make<taxonomy::shell>();
for (auto& f : *polygonal_faces) {
auto fa = taxonomy::make<taxonomy::face>();
shell->children.push_back(fa);
@@ -52,17 +69,14 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPolygonalFaceSet* inst) {
auto indices = f->CoordIndex();
taxonomy::point3::ptr previous;
for (std::vector<int>::const_iterator jt = indices.begin(); jt != indices.end(); ++jt) {
if (*jt < 1 || *jt > max_index) {
throw IfcParse::IfcException("IfcPolygonalFaceSet index out of bounds for index " + boost::lexical_cast<std::string>(*jt));
}
auto current = points[(*jt) - 1];
auto current = resolve(*jt);
if (jt != indices.begin()) {
loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current));
}
previous = current;
}
if (!indices.empty()) {
auto current = points[indices.front() - 1];
auto current = resolve(indices.front());
loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current));
}
}
@@ -77,17 +91,14 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPolygonalFaceSet* inst) {
loop->external = false;
for (std::vector<int>::const_iterator jt = li.begin(); jt != li.end(); ++jt) {
if (*jt < 1 || *jt > max_index) {
throw IfcParse::IfcException("IfcPolygonalFaceSet index out of bounds for index " + boost::lexical_cast<std::string>(*jt));
}
auto current = points[(*jt) - 1];
auto current = resolve(*jt);
if (jt != li.begin()) {
loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current));
}
previous = current;
}
if (!li.empty()) {
auto current = points[li.front() - 1];
auto current = resolve(li.front());
loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current));
}
}
+18 -4
View File
@@ -39,6 +39,23 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcTriangulatedFaceSet* inst) {
int max_index = (int)points.size();
// When the optional PnIndex is present, CoordIndex values do not index into
// CoordList directly but into PnIndex, which in turn remaps to CoordList.
// Both index levels are 1-based per the IFC specification.
auto pn_index = inst->PnIndex();
auto resolve = [&](int idx) -> const taxonomy::point3::ptr& {
if (pn_index) {
if (idx < 1 || idx > (int)pn_index->size()) {
throw IfcParse::IfcException("IfcTriangulatedFaceSet PnIndex out of bounds for index " + boost::lexical_cast<std::string>(idx));
}
idx = (*pn_index)[idx - 1];
}
if (idx < 1 || idx > max_index) {
throw IfcParse::IfcException("IfcTriangulatedFaceSet index out of bounds for index " + boost::lexical_cast<std::string>(idx));
}
return points[idx - 1];
};
auto shell = taxonomy::make<taxonomy::shell>();
for (auto& indices : indices_list) {
@@ -51,10 +68,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcTriangulatedFaceSet* inst) {
loop->external = true;
taxonomy::point3::ptr first, previous;
for (std::vector<int>::const_iterator jt = indices.begin(); jt != indices.end(); ++jt) {
if (*jt < 1 || *jt > max_index) {
throw IfcParse::IfcException("IfcTriangulatedFaceSet index out of bounds for index " + boost::lexical_cast<std::string>(*jt));
}
const taxonomy::point3::ptr& current = points[(*jt) - 1];
const taxonomy::point3::ptr& current = resolve(*jt);
if (jt == indices.begin()) {
first = current;
} else {
+5 -1
View File
@@ -89,7 +89,11 @@ BIND(IfcRectangleHollowProfileDef);
BIND(IfcRectangleProfileDef);
BIND(IfcTrapeziumProfileDef);
BIND(IfcCShapeProfileDef);
// IfcAsymmetricIShapeProfileDef included
// In IFC2X3 IfcAsymmetricIShapeProfileDef is a subtype of IfcIShapeProfileDef and is
// mapped by it; from IFC4 onwards it is a standalone type and needs its own binding.
#ifdef SCHEMA_IfcAsymmetricIShapeProfileDef_HAS_BottomFlangeWidth
BIND(IfcAsymmetricIShapeProfileDef);
#endif
BIND(IfcIShapeProfileDef);
BIND(IfcLShapeProfileDef);
BIND(IfcTShapeProfileDef);
+1 -1
View File
@@ -255,7 +255,7 @@ ifc_quantify(rule="IFC4QtoBaseQuantities", selector="IfcWall")
Available rules: `IFC4QtoBaseQuantities`, `IFC4X3QtoBaseQuantities`.
`selector` is an optional ifcopenshell selector to restrict which elements
are quantified (default: all `IfcElement`).
are quantified (default: all `IfcElement` and `IfcSpace`).
Returns `{"ok": true, "rule": "...", "elements_quantified": 42}`.
+1 -1
View File
@@ -703,7 +703,7 @@ class IfcSession:
"rule": {"type": "string", "description": "QTO rule name, e.g. IFC4QtoBaseQuantities"},
"selector": {
"type": "string",
"description": "ifcopenshell selector to restrict elements (default: all IfcElement)",
"description": "ifcopenshell selector to restrict elements (default: all IfcElement and IfcSpace)",
},
},
"required": ["rule"],
+1 -1
View File
@@ -55,7 +55,7 @@ PLATFORMTAG:=win_amd64
endif
BINARY_VERSION:=0.8.6
BUILD_COMMIT:=3e7b739
BUILD_COMMIT:=821cf7b
IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v$(BINARY_VERSION)-$(BUILD_COMMIT)-$(PLATFORM).zip
IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v$(BINARY_VERSION)-$(BUILD_COMMIT)-$(PLATFORM).zip
@@ -311,8 +311,12 @@ CLI Manual
output.
--force-space-transparency arg Overrides transparency of spaces in
geometry output.
--circle-segments arg (= 16) Number of segments to approximate full
circles in CGAL kernel.
--circle-segments arg (= 0) Number of segments to approximate full
circles in the CGAL kernel. When 0 (the
default) the segment count is derived from
mesher-linear-deflection instead, so curves
stay within the deflection tolerance
regardless of radius.
--cgal-smooth-angle-degrees arg (= -1)
Angle in degrees under which adjacent
facets will have averaged vertex
@@ -72,6 +72,8 @@ Filtering is typically used to select any IFC element or type.
"``IfcPump, location=""Level 3""``", "Locations bubble up the hierarchy. So if a pump is in a space and that space is on Level 3, then you can say ""all pumps on level 3"" which will include that pump in the space."
"``IfcElement, query:""parent.Name""=""My Site""``", "Only elements *immediately* under ""My Site"" in the spatial hierarchy. Unlike the ``location`` and ``parent`` filters, which both match at any depth, the ``parent`` query key resolves the direct parent only, so nested storeys (and their contents) are excluded."
The filter elements syntax works by specifying one or more groups of filters
separated by a ``+`` character. Each filter group will return a set of filtered
elements, and these are unioned together.
@@ -87,6 +89,11 @@ The filters are chained and apply from left to right.
filter[, filter]*
Any part of a query may be commented out using a ``/* ... */`` block comment.
This lets you temporarily disable part of a query without deleting the text, for
example ``IfcWall + /* IfcSlab, material=concrete */`` selects only walls while
keeping the slab criteria on hand. Block comments may span multiple lines.
Below is the table of filters to choose from. Most of these filters will filter
previously added elements in your filter group based on their criteria.
@@ -111,6 +118,15 @@ will search through all IfcTypeProducts and IfcProducts in the IFC project.
"Parent", "Filter", "``parent{{=}}{{value}}``", "``parent=Foo`` specifies the criteria that elements must be a direct or indirect child in the spatial hierarchy to an element with a ``Name`` attribute with a value of ``Foo``."
"Query", "Filter", "``query:{{keys}}{{=}}{{value}}``", "``query:types.count=0`` specifies the criteria that elements must have zero type occurrences. The query keys corresponds to the syntax used in the `Getting element values`_ section"
.. note::
The ``location`` and ``parent`` filters both match at **any depth** in the
spatial hierarchy. To match only elements *immediately* contained in (or
aggregated under) a spatial element, use the ``parent`` query key, which
resolves the direct parent only. For example,
``query:"parent.Name"="My Site"`` selects elements directly under ``My
Site`` but excludes anything nested inside its sub-storeys or spaces.
When you specify a filter with a ``{{=}}`` check, you can choose from one of
the following comparison checks:
@@ -191,7 +207,7 @@ Valid keys are:
"``storey``", "Gets the first IfcBuildingStorey spatial element that an element is contained in."
"``building``", "Gets the first IfcBuilding spatial element that an element is contained in."
"``site``", "Gets the first IfcSite spatial element that an element is contained in."
"``parent``", "Gets the parent element in the spatial hierarchy."
"``parent``", "Gets the **immediate** parent element in the spatial hierarchy (the direct spatial container, or the direct aggregate/nest/fill/void parent). Combine with ``.Name`` in a query filter to match only immediate children, e.g. ``query:""parent.Name""=""My Site""``."
"``classification``", "Gets the element's classification reference(s)"
"``group``", "Gets the element's group(s)"
"``system``", "Gets the element's system(s). This is a subset of group(s)."
@@ -206,6 +222,9 @@ Valid keys are:
"``easting``", "Gets the map easting of the element's placement"
"``northing``", "Gets the map northing of the element's placement"
"``elevation``", "Gets the map elevation of the element's placement"
"``rotation_x``", "Gets the X Euler rotation of the element's placement in degrees"
"``rotation_y``", "Gets the Y Euler rotation of the element's placement in degrees"
"``rotation_z``", "Gets the Z Euler rotation of the element's placement in degrees (e.g. plan rotation of a symbol)"
"``count``", "If the previous key returns multiple things, count that list. Otherwise, return 1."
"``{{number}}``", "If the previous key returns multiple things, fetch the ``{{number}}`` index (e.g. 0, 1, 2, 3, etc) item in that list."
@@ -228,10 +228,10 @@ circle-segments
+------+-----------------------+---------+
| Type | IfcConvert Option | Default |
+======+=======================+=========+
| INT | ``--circle-segments`` | 16 |
| INT | ``--circle-segments`` | 0 |
+------+-----------------------+---------+
Number of segments to approximate full circles in CGAL kernel.
Number of segments to approximate full circles in the CGAL kernel. When 0 (the default) the segment count is derived from mesher-linear-deflection instead, so curves stay within the deflection tolerance regardless of radius.
context-identifiers
^^^^^^^^^^^^^^^^^^^
@@ -97,7 +97,19 @@ from .file import file as _file
from .sql import sqlite, sqlite_entity
get_log = ifcopenshell_wrapper.get_log
logger = getattr(ifcopenshell_wrapper, "logger", None)
logger = ifcopenshell_wrapper.logger if hasattr(ifcopenshell_wrapper, "logger") else None
if hasattr(ifcopenshell_wrapper, "logger_or_root"):
logger_or_root = ifcopenshell_wrapper.logger_or_root
else:
def logger_or_root(_logger: ifcopenshell_wrapper.logger | None) -> None:
return None
# TODO: drop this function and all callsites after we migrate to the new build.
def optional_logger_args(logger: ifcopenshell_wrapper.logger | None) -> tuple[logger] | tuple[()]:
return (logger,) if logger is not None else ()
# explicitly specify available imported symbols
# (it's a requirement for a typed library)
@@ -194,10 +206,9 @@ def open(
raise FileNotFoundError(f"Path does not exist: '{path}'.")
if format is None:
format = guess_format(path)
if logger is None and (logger_type := getattr(ifcopenshell_wrapper, "logger", None)):
logger = logger_type.Root()
logger = logger_or_root(logger)
if format == ".ifcXML":
f = ifcopenshell_wrapper.parse_ifcxml(str(path.absolute()), *((logger,) if logger is not None else ()))
f = ifcopenshell_wrapper.parse_ifcxml(str(path.absolute()), *optional_logger_args(logger))
if f:
return file(f)
raise OSError(f"Failed to parse .ifcXML file from {path}")
@@ -214,11 +225,9 @@ def open(
if should_stream:
return stream(path)
if readonly: # Temporary conditional see #7131. Remove once newer builds don't segfault on Linux.
f = ifcopenshell_wrapper.open(str(path.absolute()), readonly, *((logger,) if logger is not None else ()))
f = ifcopenshell_wrapper.open(str(path.absolute()), readonly, *optional_logger_args(logger))
elif bypass_types:
f = ifcopenshell_wrapper.file(
ifcopenshell_wrapper.uninitialized_tag(), *((logger,) if logger is not None else ())
)
f = ifcopenshell_wrapper.file(ifcopenshell_wrapper.uninitialized_tag(), *optional_logger_args(logger))
for ty in bypass_types:
f.bypass_type(ty)
if mmap:
@@ -233,7 +242,7 @@ def open(
kwargs["logger"] = logger
f = ifcopenshell_wrapper.open(str(path.absolute()), **kwargs)
else:
f = ifcopenshell_wrapper.open(str(path.absolute()), False, *((logger,) if logger is not None else ()))
f = ifcopenshell_wrapper.open(str(path.absolute()), False, *optional_logger_args(logger))
return file(f)
@@ -305,12 +314,13 @@ def schema_by_name(
you are testing non-ISO IFC releases.
:return: Schema definition object.
"""
import ifcopenshell.util.schema
assert schema_version or schema, "Either schema or schema_version must be specified."
if schema_version:
prefixes = ("IFC", "X", "_ADD", "_TC")
schema = "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, schema_version))
schema = ifcopenshell.util.schema.get_schema_name_from_version(schema_version)
else:
schema = {"IFC4X3": "IFC4X3_ADD2"}.get(schema, schema)
schema = ifcopenshell.util.schema.get_schema_identifier(schema)
return ifcopenshell_wrapper.schema_by_name(schema)
@@ -90,9 +90,9 @@ def add_boolean(
booleans = []
for second_item in second_items:
if first.is_a("IfcTesselatedFaceSet"):
if first.is_a("IfcTessellatedFaceSet"):
first.Closed = True # For now, trust the user to do the right thing.
if second_item.is_a("IfcTesselatedFaceSet"):
if second_item.is_a("IfcTessellatedFaceSet"):
second_item.Closed = True # For now, trust the user to do the right thing.
if (
operator == "DIFFERENCE"
@@ -81,7 +81,7 @@ def assign_resource(
"""
if related_object.HasAssignments:
for assignment in related_object.HasAssignments:
if assignment.is_a("IfclRelAssignsToResource") and assignment.RelatingResource == relating_resource:
if assignment.is_a("IfcRelAssignsToResource") and assignment.RelatingResource == relating_resource:
return assignment
resource_of = None
+1 -2
View File
@@ -107,8 +107,7 @@ def main(
progress_function: Callable = DO_NOTHING,
logger=None,
):
if logger is None and ifcopenshell.logger is not None:
logger = ifcopenshell.logger.Root()
logger = ifcopenshell.logger_or_root(logger)
def by_guid(g):
for f in files:
@@ -409,9 +409,9 @@ class entity_instance:
@overload
def is_a(self) -> str: ...
@overload
def is_a(self, ifc_class: str) -> bool: ...
def is_a(self, ifc_class: str, /) -> bool: ...
@overload
def is_a(self, with_schema: bool) -> str: ...
def is_a(self, with_schema: bool, /) -> str: ...
def is_a(self, *args: Union[str, bool]) -> Union[str, bool]:
"""Return the IFC class name of an instance, or checks if an instance belongs to a class.
@@ -23,10 +23,7 @@ import itertools
from pyparsing import *
try:
from functools import reduce
except:
pass
from functools import reduce
class Expression:
@@ -97,15 +94,15 @@ PERIOD = Suppress(".")
HASH = Suppress("#")
identifier = Word(alphanums + "_")
keyword = Word(alphanums + "_").setParseAction(Keyword)
keyword = Word(alphanums + "_").set_parse_action(Keyword)
expression = Forward()
optional = Group(LBRACK + expression + RBRACK).setParseAction(Optional)
repeated = Group(LBRACE + expression + RBRACE).setParseAction(Repeated)
terminal = quotedString.setParseAction(Terminal)
term = (keyword | terminal | optional | repeated | (LPAREN + expression + RPAREN)).setParseAction(Term)
concat = Group(term + OneOrMore(term)).setParseAction(Concat)
optional = Group(LBRACK + expression + RBRACK).set_parse_action(Optional)
repeated = Group(LBRACE + expression + RBRACE).set_parse_action(Repeated)
terminal = quotedString.set_parse_action(Terminal)
term = (keyword | terminal | optional | repeated | (LPAREN + expression + RPAREN)).set_parse_action(Term)
concat = Group(term + OneOrMore(term)).set_parse_action(Concat)
factor = concat | term
union = Group(factor + OneOrMore(VBAR + factor)).setParseAction(Union)
union = Group(factor + OneOrMore(VBAR + factor)).set_parse_action(Union)
rule = identifier + EQUALS + expression + PERIOD
expression << (union | factor)
@@ -113,7 +110,7 @@ expression << (union | factor)
grammar = OneOrMore(Group(rule))
grammar.ignore(HASH + restOfLine)
express = grammar.parseFile(os.path.join(os.path.dirname(__file__), "express.bnf"))
express = grammar.parse_file(os.path.join(os.path.dirname(__file__), "express.bnf"))
def find_bytype(expr, ty, li=None):
@@ -161,6 +158,8 @@ statements = []
terminals = reduce(lambda x, y: x | y, (find_bytype(e, Terminal) for id, e in express))
keywords = list(filter(operator.attrgetter("is_keyword"), terminals))
# terminals is identity-ordered (no __eq__/__hash__), so sort for determinism
keywords.sort(key=lambda x: repr(x))
negated_keywords = map(lambda s: "~%s" % s, keywords)
no_action = {
"letter",
@@ -185,36 +184,36 @@ while True:
emitted.add(id)
stmt = "(%s)" % expr
if id in to_combine:
stmt = " + ".join(itertools.chain(negated_keywords, ("originalTextFor(Combine%s)" % stmt,)))
stmt = " + ".join(itertools.chain(negated_keywords, ("original_text_for(Combine%s)" % stmt,)))
elif id in to_original_text:
# We use lower() because it better matches the previous default of the CaselessLiterals for individual lexemes and express dictates case-insensitive comparisons anyway
stmt = "(originalTextFor%s).addParseAction(tokenMap(str.lower))" % stmt
stmt = "(original_text_for%s).add_parse_action(token_map(str.lower))" % stmt
if id not in no_action and not isinstance(expr.contents, Keyword) and not id in to_combine:
node_type = "ListNode" if "ZeroOrMore" in stmt else "Node"
action = actions.get(id, 'lambda s, loc, t: %s(s, loc, t, rule="%s")' % (node_type, id))
stmt = "%s.setParseAction(%s)" % (stmt, action)
stmt = "%s.set_parse_action(%s)" % (stmt, action)
statements.append('%s = %s("%s")' % (id, stmt, id))
to_emit -= emitted_in_loop
if not emitted_in_loop:
break
for id in to_emit:
for id in sorted(to_emit):
statements.append('%s = Forward()("%s")' % (id, id))
for id in to_emit:
for id in sorted(to_emit):
expr = [e for k, e in express if k == id][0]
stmt = "(%s)" % expr
if id in to_combine:
stmt = "Suppress%s" % stmt
elif id in to_original_text:
stmt = "(originalTextFor%s).addParseAction(tokenMap(str.lower))" % stmt
stmt = "(original_text_for%s).add_parse_action(token_map(str.lower))" % stmt
if id not in no_action and not isinstance(expr.contents, Keyword):
children = list(
map(operator.attrgetter("contents"), reduce(lambda x, y: x | y, (find_bytype(e, Keyword) for e in [expr])))
)
has_duplicates = len(children) > len(set(children))
node_type = "ListNode" if ("ZeroOrMore" in stmt or has_duplicates) else "Node"
action = ".setParseAction(%s)" % (
action = ".set_parse_action(%s)" % (
actions[id] if id in actions else 'lambda s, loc, t: %s(s, loc, t, rule="%s")' % (node_type, id)
)
stmt = "(%s)%s" % (stmt, action)
@@ -222,7 +221,7 @@ for id in to_emit:
if __name__ == "__main__":
print(r"""
# This file is generated by IfcOpenShell ifcexpressparser bootstrap.py
# This file is generated by ifcopenshell.express.bootstrap
from __future__ import annotations
import os
@@ -240,19 +239,19 @@ def parse(fn: str) -> mapping.Mapping:
if os.path.exists(cache_file) and os.path.getmtime(cache_file) >= os.path.getmtime(fn):
with open(cache_file, "rb") as f:
m = pickle.load(f)
else:
else:
%s
syntax.ignore("--" + restOfLine)
syntax.ignore(Regex(r"\((?:\*(?:[^*]*\*+)+?\))"))
ast = syntax.parseFile(fn)
ast = syntax.parse_file(fn)
s = schema.Schema(ast)
m = mapping.Mapping(s)
with open(cache_file, "wb") as f:
pickle.dump(m, f, protocol=0)
return m
if __name__ == "__main__":
m = parse(sys.argv[1])
import importlib
@@ -18,6 +18,7 @@
import itertools
import functools
from pathlib import Path
def indent(n, s):
@@ -36,15 +37,4 @@ class Base:
"""
def emit(self):
import platform
if tuple(map(int, platform.python_version_tuple())) < (2, 8):
from io import open as unicode_open
unicode_type = unicode
else:
unicode_open = open
unicode_type = lambda x, *args, **kwargs: x
f = unicode_open(self.file_name, "w", encoding="utf-8")
f.write(unicode_type(repr(self), encoding="utf-8", errors="ignore"))
f.close()
Path(self.file_name).write_text(repr(self), encoding="utf-8")
@@ -1,5 +1,5 @@
# This file is generated by IfcOpenShell ifcexpressparser bootstrap.py
# This file is generated by ifcopenshell.express.bootstrap
from __future__ import annotations
import os
@@ -17,184 +17,184 @@ def parse(fn: str) -> mapping.Mapping:
if os.path.exists(cache_file) and os.path.getmtime(cache_file) >= os.path.getmtime(fn):
with open(cache_file, "rb") as f:
m = pickle.load(f)
else:
ABS = (CaselessKeyword("abs")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ABS"))("ABS")
ABSTRACT = (CaselessKeyword("abstract")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ABSTRACT"))("ABSTRACT")
ACOS = (CaselessKeyword("acos")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ACOS"))("ACOS")
AGGREGATE = (CaselessKeyword("aggregate")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="AGGREGATE"))("AGGREGATE")
ALIAS = (CaselessKeyword("alias")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ALIAS"))("ALIAS")
AND = (CaselessKeyword("and")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="AND"))("AND")
ANDOR = (CaselessKeyword("andor")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ANDOR"))("ANDOR")
ARRAY = (CaselessKeyword("array")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ARRAY"))("ARRAY")
AS = (CaselessKeyword("as")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="AS"))("AS")
ASIN = (CaselessKeyword("asin")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ASIN"))("ASIN")
ATAN = (CaselessKeyword("atan")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ATAN"))("ATAN")
BAG = (CaselessKeyword("bag")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="BAG"))("BAG")
BASED_ON = (CaselessKeyword("based_on")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="BASED_ON"))("BASED_ON")
BEGIN = (CaselessKeyword("begin")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="BEGIN"))("BEGIN")
BINARY = (CaselessKeyword("binary")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="BINARY"))("BINARY")
BLENGTH = (CaselessKeyword("blength")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="BLENGTH"))("BLENGTH")
BOOLEAN = (CaselessKeyword("boolean")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="BOOLEAN"))("BOOLEAN")
BY = (CaselessKeyword("by")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="BY"))("BY")
CASE = (CaselessKeyword("case")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="CASE"))("CASE")
CONSTANT = (CaselessKeyword("constant")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="CONSTANT"))("CONSTANT")
CONST_E = (CaselessKeyword("const_e")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="CONST_E"))("CONST_E")
COS = (CaselessKeyword("cos")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="COS"))("COS")
DERIVE = (CaselessKeyword("derive")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="DERIVE"))("DERIVE")
DIV = (CaselessKeyword("div")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="DIV"))("DIV")
ELSE = (CaselessKeyword("else")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ELSE"))("ELSE")
END = (CaselessKeyword("end")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END"))("END")
END_ALIAS = (CaselessKeyword("end_alias")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_ALIAS"))("END_ALIAS")
END_CASE = (CaselessKeyword("end_case")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_CASE"))("END_CASE")
END_CONSTANT = (CaselessKeyword("end_constant")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_CONSTANT"))("END_CONSTANT")
END_ENTITY = (CaselessKeyword("end_entity")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_ENTITY"))("END_ENTITY")
END_FUNCTION = (CaselessKeyword("end_function")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_FUNCTION"))("END_FUNCTION")
END_IF = (CaselessKeyword("end_if")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_IF"))("END_IF")
END_LOCAL = (CaselessKeyword("end_local")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_LOCAL"))("END_LOCAL")
END_PROCEDURE = (CaselessKeyword("end_procedure")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_PROCEDURE"))("END_PROCEDURE")
END_REPEAT = (CaselessKeyword("end_repeat")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_REPEAT"))("END_REPEAT")
END_RULE = (CaselessKeyword("end_rule")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_RULE"))("END_RULE")
END_SCHEMA = (CaselessKeyword("end_schema")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_SCHEMA"))("END_SCHEMA")
END_SUBTYPE_CONSTRAINT = (CaselessKeyword("end_subtype_constraint")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_SUBTYPE_CONSTRAINT"))("END_SUBTYPE_CONSTRAINT")
END_TYPE = (CaselessKeyword("end_type")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="END_TYPE"))("END_TYPE")
ENTITY = (CaselessKeyword("entity")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ENTITY"))("ENTITY")
ENUMERATION = (CaselessKeyword("enumeration")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ENUMERATION"))("ENUMERATION")
ESCAPE = (CaselessKeyword("escape")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ESCAPE"))("ESCAPE")
EXISTS = (CaselessKeyword("exists")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="EXISTS"))("EXISTS")
EXTENSIBLE = (CaselessKeyword("extensible")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="EXTENSIBLE"))("EXTENSIBLE")
EXP = (CaselessKeyword("exp")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="EXP"))("EXP")
FALSE = (CaselessKeyword("false")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="FALSE"))("FALSE")
FIXED = (CaselessKeyword("fixed")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="FIXED"))("FIXED")
FOR = (CaselessKeyword("for")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="FOR"))("FOR")
FORMAT = (CaselessKeyword("format")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="FORMAT"))("FORMAT")
FROM = (CaselessKeyword("from")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="FROM"))("FROM")
FUNCTION = (CaselessKeyword("function")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="FUNCTION"))("FUNCTION")
GENERIC = (CaselessKeyword("generic")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="GENERIC"))("GENERIC")
GENERIC_ENTITY = (CaselessKeyword("generic_entity")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="GENERIC_ENTITY"))("GENERIC_ENTITY")
HIBOUND = (CaselessKeyword("hibound")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="HIBOUND"))("HIBOUND")
HIINDEX = (CaselessKeyword("hiindex")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="HIINDEX"))("HIINDEX")
IF = (CaselessKeyword("if")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="IF"))("IF")
IN = (CaselessKeyword("in")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="IN"))("IN")
INSERT = (CaselessKeyword("insert")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="INSERT"))("INSERT")
INTEGER = (CaselessKeyword("integer")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="INTEGER"))("INTEGER")
INVERSE = (CaselessKeyword("inverse")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="INVERSE"))("INVERSE")
LENGTH = (CaselessKeyword("length")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="LENGTH"))("LENGTH")
LIKE = (CaselessKeyword("like")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="LIKE"))("LIKE")
LIST = (CaselessKeyword("list")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="LIST"))("LIST")
LOBOUND = (CaselessKeyword("lobound")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="LOBOUND"))("LOBOUND")
LOCAL = (CaselessKeyword("local")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="LOCAL"))("LOCAL")
LOG = (CaselessKeyword("log")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="LOG"))("LOG")
LOG10 = (CaselessKeyword("log10")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="LOG10"))("LOG10")
LOG2 = (CaselessKeyword("log2")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="LOG2"))("LOG2")
LOGICAL = (CaselessKeyword("logical")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="LOGICAL"))("LOGICAL")
LOINDEX = (CaselessKeyword("loindex")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="LOINDEX"))("LOINDEX")
MOD = (CaselessKeyword("mod")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="MOD"))("MOD")
NOT = (CaselessKeyword("not")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="NOT"))("NOT")
NUMBER = (CaselessKeyword("number")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="NUMBER"))("NUMBER")
NVL = (CaselessKeyword("nvl")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="NVL"))("NVL")
ODD = (CaselessKeyword("odd")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ODD"))("ODD")
OF = (CaselessKeyword("of")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="OF"))("OF")
ONEOF = (CaselessKeyword("oneof")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ONEOF"))("ONEOF")
OPTIONAL = (CaselessKeyword("optional")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="OPTIONAL"))("OPTIONAL")
OR = (CaselessKeyword("or")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="OR"))("OR")
OTHERWISE = (CaselessKeyword("otherwise")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="OTHERWISE"))("OTHERWISE")
PI = (CaselessKeyword("pi")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="PI"))("PI")
PROCEDURE = (CaselessKeyword("procedure")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="PROCEDURE"))("PROCEDURE")
QUERY = (CaselessKeyword("query")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="QUERY"))("QUERY")
REAL = (CaselessKeyword("real")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="REAL"))("REAL")
REFERENCE = (CaselessKeyword("reference")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="REFERENCE"))("REFERENCE")
REMOVE = (CaselessKeyword("remove")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="REMOVE"))("REMOVE")
RENAMED = (CaselessKeyword("renamed")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="RENAMED"))("RENAMED")
REPEAT = (CaselessKeyword("repeat")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="REPEAT"))("REPEAT")
RETURN = (CaselessKeyword("return")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="RETURN"))("RETURN")
ROLESOF = (CaselessKeyword("rolesof")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="ROLESOF"))("ROLESOF")
RULE = (CaselessKeyword("rule")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="RULE"))("RULE")
SCHEMA = (CaselessKeyword("schema")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="SCHEMA"))("SCHEMA")
SELECT = (CaselessKeyword("select")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="SELECT"))("SELECT")
SELF = (CaselessKeyword("self")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="SELF"))("SELF")
SET = (CaselessKeyword("set")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="SET"))("SET")
SIN = (CaselessKeyword("sin")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="SIN"))("SIN")
SIZEOF = (CaselessKeyword("sizeof")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="SIZEOF"))("SIZEOF")
SKIP = (CaselessKeyword("skip")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="SKIP"))("SKIP")
SQRT = (CaselessKeyword("sqrt")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="SQRT"))("SQRT")
STRING = (CaselessKeyword("string")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="STRING"))("STRING")
SUBTYPE = (CaselessKeyword("subtype")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="SUBTYPE"))("SUBTYPE")
SUBTYPE_CONSTRAINT = (CaselessKeyword("subtype_constraint")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="SUBTYPE_CONSTRAINT"))("SUBTYPE_CONSTRAINT")
SUPERTYPE = (CaselessKeyword("supertype")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="SUPERTYPE"))("SUPERTYPE")
TAN = (CaselessKeyword("tan")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="TAN"))("TAN")
THEN = (CaselessKeyword("then")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="THEN"))("THEN")
TO = (CaselessKeyword("to")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="TO"))("TO")
TOTAL_OVER = (CaselessKeyword("total_over")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="TOTAL_OVER"))("TOTAL_OVER")
TRUE = (CaselessKeyword("true")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="TRUE"))("TRUE")
TYPE = (CaselessKeyword("type")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="TYPE"))("TYPE")
TYPEOF = (CaselessKeyword("typeof")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="TYPEOF"))("TYPEOF")
UNIQUE = (CaselessKeyword("unique")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="UNIQUE"))("UNIQUE")
UNKNOWN = (CaselessKeyword("unknown")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="UNKNOWN"))("UNKNOWN")
UNTIL = (CaselessKeyword("until")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="UNTIL"))("UNTIL")
USE = (CaselessKeyword("use")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="USE"))("USE")
USEDIN = (CaselessKeyword("usedin")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="USEDIN"))("USEDIN")
VALUE = (CaselessKeyword("value")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="VALUE"))("VALUE")
VALUE_IN = (CaselessKeyword("value_in")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="VALUE_IN"))("VALUE_IN")
VALUE_UNIQUE = (CaselessKeyword("value_unique")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="VALUE_UNIQUE"))("VALUE_UNIQUE")
VAR = (CaselessKeyword("var")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="VAR"))("VAR")
WHERE = (CaselessKeyword("where")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="WHERE"))("WHERE")
WHILE = (CaselessKeyword("while")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="WHILE"))("WHILE")
WITH = (CaselessKeyword("with")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="WITH"))("WITH")
XOR = (CaselessKeyword("xor")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="XOR"))("XOR")
bit = ((CaselessLiteral("0") | CaselessLiteral("1"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="bit"))("bit")
else:
ABS = (CaselessKeyword("abs")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ABS"))("ABS")
ABSTRACT = (CaselessKeyword("abstract")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ABSTRACT"))("ABSTRACT")
ACOS = (CaselessKeyword("acos")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ACOS"))("ACOS")
AGGREGATE = (CaselessKeyword("aggregate")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="AGGREGATE"))("AGGREGATE")
ALIAS = (CaselessKeyword("alias")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ALIAS"))("ALIAS")
AND = (CaselessKeyword("and")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="AND"))("AND")
ANDOR = (CaselessKeyword("andor")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ANDOR"))("ANDOR")
ARRAY = (CaselessKeyword("array")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ARRAY"))("ARRAY")
AS = (CaselessKeyword("as")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="AS"))("AS")
ASIN = (CaselessKeyword("asin")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ASIN"))("ASIN")
ATAN = (CaselessKeyword("atan")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ATAN"))("ATAN")
BAG = (CaselessKeyword("bag")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="BAG"))("BAG")
BASED_ON = (CaselessKeyword("based_on")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="BASED_ON"))("BASED_ON")
BEGIN = (CaselessKeyword("begin")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="BEGIN"))("BEGIN")
BINARY = (CaselessKeyword("binary")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="BINARY"))("BINARY")
BLENGTH = (CaselessKeyword("blength")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="BLENGTH"))("BLENGTH")
BOOLEAN = (CaselessKeyword("boolean")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="BOOLEAN"))("BOOLEAN")
BY = (CaselessKeyword("by")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="BY"))("BY")
CASE = (CaselessKeyword("case")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="CASE"))("CASE")
CONSTANT = (CaselessKeyword("constant")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="CONSTANT"))("CONSTANT")
CONST_E = (CaselessKeyword("const_e")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="CONST_E"))("CONST_E")
COS = (CaselessKeyword("cos")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="COS"))("COS")
DERIVE = (CaselessKeyword("derive")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="DERIVE"))("DERIVE")
DIV = (CaselessKeyword("div")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="DIV"))("DIV")
ELSE = (CaselessKeyword("else")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ELSE"))("ELSE")
END = (CaselessKeyword("end")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END"))("END")
END_ALIAS = (CaselessKeyword("end_alias")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_ALIAS"))("END_ALIAS")
END_CASE = (CaselessKeyword("end_case")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_CASE"))("END_CASE")
END_CONSTANT = (CaselessKeyword("end_constant")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_CONSTANT"))("END_CONSTANT")
END_ENTITY = (CaselessKeyword("end_entity")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_ENTITY"))("END_ENTITY")
END_FUNCTION = (CaselessKeyword("end_function")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_FUNCTION"))("END_FUNCTION")
END_IF = (CaselessKeyword("end_if")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_IF"))("END_IF")
END_LOCAL = (CaselessKeyword("end_local")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_LOCAL"))("END_LOCAL")
END_PROCEDURE = (CaselessKeyword("end_procedure")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_PROCEDURE"))("END_PROCEDURE")
END_REPEAT = (CaselessKeyword("end_repeat")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_REPEAT"))("END_REPEAT")
END_RULE = (CaselessKeyword("end_rule")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_RULE"))("END_RULE")
END_SCHEMA = (CaselessKeyword("end_schema")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_SCHEMA"))("END_SCHEMA")
END_SUBTYPE_CONSTRAINT = (CaselessKeyword("end_subtype_constraint")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_SUBTYPE_CONSTRAINT"))("END_SUBTYPE_CONSTRAINT")
END_TYPE = (CaselessKeyword("end_type")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="END_TYPE"))("END_TYPE")
ENTITY = (CaselessKeyword("entity")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ENTITY"))("ENTITY")
ENUMERATION = (CaselessKeyword("enumeration")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ENUMERATION"))("ENUMERATION")
ESCAPE = (CaselessKeyword("escape")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ESCAPE"))("ESCAPE")
EXISTS = (CaselessKeyword("exists")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="EXISTS"))("EXISTS")
EXTENSIBLE = (CaselessKeyword("extensible")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="EXTENSIBLE"))("EXTENSIBLE")
EXP = (CaselessKeyword("exp")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="EXP"))("EXP")
FALSE = (CaselessKeyword("false")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="FALSE"))("FALSE")
FIXED = (CaselessKeyword("fixed")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="FIXED"))("FIXED")
FOR = (CaselessKeyword("for")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="FOR"))("FOR")
FORMAT = (CaselessKeyword("format")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="FORMAT"))("FORMAT")
FROM = (CaselessKeyword("from")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="FROM"))("FROM")
FUNCTION = (CaselessKeyword("function")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="FUNCTION"))("FUNCTION")
GENERIC = (CaselessKeyword("generic")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="GENERIC"))("GENERIC")
GENERIC_ENTITY = (CaselessKeyword("generic_entity")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="GENERIC_ENTITY"))("GENERIC_ENTITY")
HIBOUND = (CaselessKeyword("hibound")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="HIBOUND"))("HIBOUND")
HIINDEX = (CaselessKeyword("hiindex")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="HIINDEX"))("HIINDEX")
IF = (CaselessKeyword("if")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="IF"))("IF")
IN = (CaselessKeyword("in")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="IN"))("IN")
INSERT = (CaselessKeyword("insert")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="INSERT"))("INSERT")
INTEGER = (CaselessKeyword("integer")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="INTEGER"))("INTEGER")
INVERSE = (CaselessKeyword("inverse")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="INVERSE"))("INVERSE")
LENGTH = (CaselessKeyword("length")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="LENGTH"))("LENGTH")
LIKE = (CaselessKeyword("like")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="LIKE"))("LIKE")
LIST = (CaselessKeyword("list")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="LIST"))("LIST")
LOBOUND = (CaselessKeyword("lobound")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="LOBOUND"))("LOBOUND")
LOCAL = (CaselessKeyword("local")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="LOCAL"))("LOCAL")
LOG = (CaselessKeyword("log")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="LOG"))("LOG")
LOG10 = (CaselessKeyword("log10")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="LOG10"))("LOG10")
LOG2 = (CaselessKeyword("log2")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="LOG2"))("LOG2")
LOGICAL = (CaselessKeyword("logical")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="LOGICAL"))("LOGICAL")
LOINDEX = (CaselessKeyword("loindex")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="LOINDEX"))("LOINDEX")
MOD = (CaselessKeyword("mod")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="MOD"))("MOD")
NOT = (CaselessKeyword("not")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="NOT"))("NOT")
NUMBER = (CaselessKeyword("number")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="NUMBER"))("NUMBER")
NVL = (CaselessKeyword("nvl")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="NVL"))("NVL")
ODD = (CaselessKeyword("odd")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ODD"))("ODD")
OF = (CaselessKeyword("of")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="OF"))("OF")
ONEOF = (CaselessKeyword("oneof")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ONEOF"))("ONEOF")
OPTIONAL = (CaselessKeyword("optional")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="OPTIONAL"))("OPTIONAL")
OR = (CaselessKeyword("or")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="OR"))("OR")
OTHERWISE = (CaselessKeyword("otherwise")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="OTHERWISE"))("OTHERWISE")
PI = (CaselessKeyword("pi")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="PI"))("PI")
PROCEDURE = (CaselessKeyword("procedure")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="PROCEDURE"))("PROCEDURE")
QUERY = (CaselessKeyword("query")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="QUERY"))("QUERY")
REAL = (CaselessKeyword("real")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="REAL"))("REAL")
REFERENCE = (CaselessKeyword("reference")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="REFERENCE"))("REFERENCE")
REMOVE = (CaselessKeyword("remove")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="REMOVE"))("REMOVE")
RENAMED = (CaselessKeyword("renamed")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="RENAMED"))("RENAMED")
REPEAT = (CaselessKeyword("repeat")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="REPEAT"))("REPEAT")
RETURN = (CaselessKeyword("return")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="RETURN"))("RETURN")
ROLESOF = (CaselessKeyword("rolesof")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="ROLESOF"))("ROLESOF")
RULE = (CaselessKeyword("rule")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="RULE"))("RULE")
SCHEMA = (CaselessKeyword("schema")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="SCHEMA"))("SCHEMA")
SELECT = (CaselessKeyword("select")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="SELECT"))("SELECT")
SELF = (CaselessKeyword("self")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="SELF"))("SELF")
SET = (CaselessKeyword("set")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="SET"))("SET")
SIN = (CaselessKeyword("sin")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="SIN"))("SIN")
SIZEOF = (CaselessKeyword("sizeof")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="SIZEOF"))("SIZEOF")
SKIP = (CaselessKeyword("skip")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="SKIP"))("SKIP")
SQRT = (CaselessKeyword("sqrt")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="SQRT"))("SQRT")
STRING = (CaselessKeyword("string")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="STRING"))("STRING")
SUBTYPE = (CaselessKeyword("subtype")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="SUBTYPE"))("SUBTYPE")
SUBTYPE_CONSTRAINT = (CaselessKeyword("subtype_constraint")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="SUBTYPE_CONSTRAINT"))("SUBTYPE_CONSTRAINT")
SUPERTYPE = (CaselessKeyword("supertype")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="SUPERTYPE"))("SUPERTYPE")
TAN = (CaselessKeyword("tan")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="TAN"))("TAN")
THEN = (CaselessKeyword("then")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="THEN"))("THEN")
TO = (CaselessKeyword("to")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="TO"))("TO")
TOTAL_OVER = (CaselessKeyword("total_over")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="TOTAL_OVER"))("TOTAL_OVER")
TRUE = (CaselessKeyword("true")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="TRUE"))("TRUE")
TYPE = (CaselessKeyword("type")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="TYPE"))("TYPE")
TYPEOF = (CaselessKeyword("typeof")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="TYPEOF"))("TYPEOF")
UNIQUE = (CaselessKeyword("unique")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="UNIQUE"))("UNIQUE")
UNKNOWN = (CaselessKeyword("unknown")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="UNKNOWN"))("UNKNOWN")
UNTIL = (CaselessKeyword("until")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="UNTIL"))("UNTIL")
USE = (CaselessKeyword("use")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="USE"))("USE")
USEDIN = (CaselessKeyword("usedin")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="USEDIN"))("USEDIN")
VALUE = (CaselessKeyword("value")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="VALUE"))("VALUE")
VALUE_IN = (CaselessKeyword("value_in")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="VALUE_IN"))("VALUE_IN")
VALUE_UNIQUE = (CaselessKeyword("value_unique")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="VALUE_UNIQUE"))("VALUE_UNIQUE")
VAR = (CaselessKeyword("var")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="VAR"))("VAR")
WHERE = (CaselessKeyword("where")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="WHERE"))("WHERE")
WHILE = (CaselessKeyword("while")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="WHILE"))("WHILE")
WITH = (CaselessKeyword("with")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="WITH"))("WITH")
XOR = (CaselessKeyword("xor")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="XOR"))("XOR")
bit = ((CaselessLiteral("0") | CaselessLiteral("1"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="bit"))("bit")
digit = ((CaselessLiteral("0") | CaselessLiteral("1") | CaselessLiteral("2") | CaselessLiteral("3") | CaselessLiteral("4") | CaselessLiteral("5") | CaselessLiteral("6") | CaselessLiteral("7") | CaselessLiteral("8") | CaselessLiteral("9")))("digit")
digits = ((digit + ZeroOrMore(digit)))("digits")
hex_digit = ((digit | CaselessLiteral("a") | CaselessLiteral("b") | CaselessLiteral("c") | CaselessLiteral("d") | CaselessLiteral("e") | CaselessLiteral("f"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="hex_digit"))("hex_digit")
hex_digit = ((digit | CaselessLiteral("a") | CaselessLiteral("b") | CaselessLiteral("c") | CaselessLiteral("d") | CaselessLiteral("e") | CaselessLiteral("f"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="hex_digit"))("hex_digit")
letter = ((CaselessLiteral("a") | CaselessLiteral("b") | CaselessLiteral("c") | CaselessLiteral("d") | CaselessLiteral("e") | CaselessLiteral("f") | CaselessLiteral("g") | CaselessLiteral("h") | CaselessLiteral("i") | CaselessLiteral("j") | CaselessLiteral("k") | CaselessLiteral("l") | CaselessLiteral("m") | CaselessLiteral("n") | CaselessLiteral("o") | CaselessLiteral("p") | CaselessLiteral("q") | CaselessLiteral("r") | CaselessLiteral("s") | CaselessLiteral("t") | CaselessLiteral("u") | CaselessLiteral("v") | CaselessLiteral("w") | CaselessLiteral("x") | CaselessLiteral("y") | CaselessLiteral("z")))("letter")
not_paren_star_quote_special = ((CaselessLiteral("!") | CaselessLiteral("#") | CaselessLiteral("$") | CaselessLiteral("%") | CaselessLiteral("&") | CaselessLiteral("+") | CaselessLiteral(",") | CaselessLiteral("-") | CaselessLiteral(".") | CaselessLiteral("/") | CaselessLiteral(":") | CaselessLiteral(";") | CaselessLiteral("<") | CaselessLiteral("=") | CaselessLiteral(">") | CaselessLiteral("?") | CaselessLiteral("@") | CaselessLiteral("[") | CaselessLiteral("\\") | CaselessLiteral("]") | CaselessLiteral("^") | CaselessLiteral("_") | CaselessLiteral("{") | CaselessLiteral("|") | CaselessLiteral("}") | CaselessLiteral("~")))("not_paren_star_quote_special")
not_paren_star_special = ((not_paren_star_quote_special | CaselessLiteral("\"\""))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="not_paren_star_special"))("not_paren_star_special")
not_paren_star_special = ((not_paren_star_quote_special | CaselessLiteral("\"\""))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="not_paren_star_special"))("not_paren_star_special")
not_quote = ((not_paren_star_quote_special | letter | digit | CaselessLiteral("(") | CaselessLiteral(")") | CaselessLiteral("*")))("not_quote")
octet = ((hex_digit + hex_digit)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="octet"))("octet")
special = ((not_paren_star_quote_special | CaselessLiteral("(") | CaselessLiteral(")") | CaselessLiteral("*") | CaselessLiteral("\"\""))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="special"))("special")
binary_literal = ((CaselessLiteral("%") + bit + ZeroOrMore(bit))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="binary_literal"))("binary_literal")
octet = ((hex_digit + hex_digit)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="octet"))("octet")
special = ((not_paren_star_quote_special | CaselessLiteral("(") | CaselessLiteral(")") | CaselessLiteral("*") | CaselessLiteral("\"\""))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="special"))("special")
binary_literal = ((CaselessLiteral("%") + bit + ZeroOrMore(bit))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="binary_literal"))("binary_literal")
integer_literal = (digits)("integer_literal")
simple_id = ~CaselessKeyword("format") + ~CaselessKeyword("loindex") + ~CaselessKeyword("in") + ~CaselessKeyword("where") + ~CaselessKeyword("by") + ~CaselessKeyword("subtype") + ~CaselessKeyword("local") + ~CaselessKeyword("sin") + ~CaselessKeyword("case") + ~CaselessKeyword("false") + ~CaselessKeyword("entity") + ~CaselessKeyword("exp") + ~CaselessKeyword("hiindex") + ~CaselessKeyword("generic_entity") + ~CaselessKeyword("number") + ~CaselessKeyword("end_rule") + ~CaselessKeyword("schema") + ~CaselessKeyword("from") + ~CaselessKeyword("length") + ~CaselessKeyword("insert") + ~CaselessKeyword("real") + ~CaselessKeyword("like") + ~CaselessKeyword("hibound") + ~CaselessKeyword("if") + ~CaselessKeyword("end_schema") + ~CaselessKeyword("generic") + ~CaselessKeyword("extensible") + ~CaselessKeyword("pi") + ~CaselessKeyword("of") + ~CaselessKeyword("logical") + ~CaselessKeyword("rolesof") + ~CaselessKeyword("log") + ~CaselessKeyword("integer") + ~CaselessKeyword("or") + ~CaselessKeyword("odd") + ~CaselessKeyword("list") + ~CaselessKeyword("procedure") + ~CaselessKeyword("renamed") + ~CaselessKeyword("optional") + ~CaselessKeyword("log10") + ~CaselessKeyword("end_function") + ~CaselessKeyword("value_unique") + ~CaselessKeyword("fixed") + ~CaselessKeyword("repeat") + ~CaselessKeyword("rule") + ~CaselessKeyword("mod") + ~CaselessKeyword("exists") + ~CaselessKeyword("with") + ~CaselessKeyword("nvl") + ~CaselessKeyword("end_repeat") + ~CaselessKeyword("not") + ~CaselessKeyword("type") + ~CaselessKeyword("otherwise") + ~CaselessKeyword("lobound") + ~CaselessKeyword("query") + ~CaselessKeyword("function") + ~CaselessKeyword("reference") + ~CaselessKeyword("enumeration") + ~CaselessKeyword("oneof") + ~CaselessKeyword("bag") + ~CaselessKeyword("then") + ~CaselessKeyword("end_if") + ~CaselessKeyword("sizeof") + ~CaselessKeyword("end_procedure") + ~CaselessKeyword("end_type") + ~CaselessKeyword("string") + ~CaselessKeyword("end_case") + ~CaselessKeyword("return") + ~CaselessKeyword("end_entity") + ~CaselessKeyword("log2") + ~CaselessKeyword("end_alias") + ~CaselessKeyword("inverse") + ~CaselessKeyword("derive") + ~CaselessKeyword("select") + ~CaselessKeyword("for") + ~CaselessKeyword("set") + ~CaselessKeyword("aggregate") + ~CaselessKeyword("self") + ~CaselessKeyword("array") + ~CaselessKeyword("abs") + ~CaselessKeyword("tan") + ~CaselessKeyword("subtype_constraint") + ~CaselessKeyword("remove") + ~CaselessKeyword("to") + ~CaselessKeyword("acos") + ~CaselessKeyword("skip") + ~CaselessKeyword("end_subtype_constraint") + ~CaselessKeyword("end_local") + ~CaselessKeyword("use") + ~CaselessKeyword("abstract") + ~CaselessKeyword("sqrt") + ~CaselessKeyword("var") + ~CaselessKeyword("until") + ~CaselessKeyword("while") + ~CaselessKeyword("end") + ~CaselessKeyword("typeof") + ~CaselessKeyword("supertype") + ~CaselessKeyword("based_on") + ~CaselessKeyword("true") + ~CaselessKeyword("alias") + ~CaselessKeyword("total_over") + ~CaselessKeyword("andor") + ~CaselessKeyword("cos") + ~CaselessKeyword("div") + ~CaselessKeyword("and") + ~CaselessKeyword("const_e") + ~CaselessKeyword("unique") + ~CaselessKeyword("as") + ~CaselessKeyword("boolean") + ~CaselessKeyword("constant") + ~CaselessKeyword("escape") + ~CaselessKeyword("atan") + ~CaselessKeyword("unknown") + ~CaselessKeyword("asin") + ~CaselessKeyword("usedin") + ~CaselessKeyword("xor") + ~CaselessKeyword("else") + ~CaselessKeyword("blength") + ~CaselessKeyword("value_in") + ~CaselessKeyword("value") + ~CaselessKeyword("begin") + ~CaselessKeyword("binary") + ~CaselessKeyword("end_constant") + originalTextFor(Combine((letter + ZeroOrMore((letter | digit | CaselessLiteral("_"))))))("simple_id")
simple_string_literal = (originalTextFor((CaselessLiteral("'") + ZeroOrMore(((CaselessLiteral("'") + CaselessLiteral("'")) | not_quote)) + CaselessLiteral("'")))).addParseAction(tokenMap(str.lower))("simple_string_literal")
simple_id = ~CaselessKeyword("abs") + ~CaselessKeyword("abstract") + ~CaselessKeyword("acos") + ~CaselessKeyword("aggregate") + ~CaselessKeyword("alias") + ~CaselessKeyword("and") + ~CaselessKeyword("andor") + ~CaselessKeyword("array") + ~CaselessKeyword("as") + ~CaselessKeyword("asin") + ~CaselessKeyword("atan") + ~CaselessKeyword("bag") + ~CaselessKeyword("based_on") + ~CaselessKeyword("begin") + ~CaselessKeyword("binary") + ~CaselessKeyword("blength") + ~CaselessKeyword("boolean") + ~CaselessKeyword("by") + ~CaselessKeyword("case") + ~CaselessKeyword("const_e") + ~CaselessKeyword("constant") + ~CaselessKeyword("cos") + ~CaselessKeyword("derive") + ~CaselessKeyword("div") + ~CaselessKeyword("else") + ~CaselessKeyword("end") + ~CaselessKeyword("end_alias") + ~CaselessKeyword("end_case") + ~CaselessKeyword("end_constant") + ~CaselessKeyword("end_entity") + ~CaselessKeyword("end_function") + ~CaselessKeyword("end_if") + ~CaselessKeyword("end_local") + ~CaselessKeyword("end_procedure") + ~CaselessKeyword("end_repeat") + ~CaselessKeyword("end_rule") + ~CaselessKeyword("end_schema") + ~CaselessKeyword("end_subtype_constraint") + ~CaselessKeyword("end_type") + ~CaselessKeyword("entity") + ~CaselessKeyword("enumeration") + ~CaselessKeyword("escape") + ~CaselessKeyword("exists") + ~CaselessKeyword("exp") + ~CaselessKeyword("extensible") + ~CaselessKeyword("false") + ~CaselessKeyword("fixed") + ~CaselessKeyword("for") + ~CaselessKeyword("format") + ~CaselessKeyword("from") + ~CaselessKeyword("function") + ~CaselessKeyword("generic") + ~CaselessKeyword("generic_entity") + ~CaselessKeyword("hibound") + ~CaselessKeyword("hiindex") + ~CaselessKeyword("if") + ~CaselessKeyword("in") + ~CaselessKeyword("insert") + ~CaselessKeyword("integer") + ~CaselessKeyword("inverse") + ~CaselessKeyword("length") + ~CaselessKeyword("like") + ~CaselessKeyword("list") + ~CaselessKeyword("lobound") + ~CaselessKeyword("local") + ~CaselessKeyword("log") + ~CaselessKeyword("log10") + ~CaselessKeyword("log2") + ~CaselessKeyword("logical") + ~CaselessKeyword("loindex") + ~CaselessKeyword("mod") + ~CaselessKeyword("not") + ~CaselessKeyword("number") + ~CaselessKeyword("nvl") + ~CaselessKeyword("odd") + ~CaselessKeyword("of") + ~CaselessKeyword("oneof") + ~CaselessKeyword("optional") + ~CaselessKeyword("or") + ~CaselessKeyword("otherwise") + ~CaselessKeyword("pi") + ~CaselessKeyword("procedure") + ~CaselessKeyword("query") + ~CaselessKeyword("real") + ~CaselessKeyword("reference") + ~CaselessKeyword("remove") + ~CaselessKeyword("renamed") + ~CaselessKeyword("repeat") + ~CaselessKeyword("return") + ~CaselessKeyword("rolesof") + ~CaselessKeyword("rule") + ~CaselessKeyword("schema") + ~CaselessKeyword("select") + ~CaselessKeyword("self") + ~CaselessKeyword("set") + ~CaselessKeyword("sin") + ~CaselessKeyword("sizeof") + ~CaselessKeyword("skip") + ~CaselessKeyword("sqrt") + ~CaselessKeyword("string") + ~CaselessKeyword("subtype") + ~CaselessKeyword("subtype_constraint") + ~CaselessKeyword("supertype") + ~CaselessKeyword("tan") + ~CaselessKeyword("then") + ~CaselessKeyword("to") + ~CaselessKeyword("total_over") + ~CaselessKeyword("true") + ~CaselessKeyword("type") + ~CaselessKeyword("typeof") + ~CaselessKeyword("unique") + ~CaselessKeyword("unknown") + ~CaselessKeyword("until") + ~CaselessKeyword("use") + ~CaselessKeyword("usedin") + ~CaselessKeyword("value") + ~CaselessKeyword("value_in") + ~CaselessKeyword("value_unique") + ~CaselessKeyword("var") + ~CaselessKeyword("where") + ~CaselessKeyword("while") + ~CaselessKeyword("with") + ~CaselessKeyword("xor") + original_text_for(Combine((letter + ZeroOrMore((letter | digit | CaselessLiteral("_"))))))("simple_id")
simple_string_literal = (original_text_for((CaselessLiteral("'") + ZeroOrMore(((CaselessLiteral("'") + CaselessLiteral("'")) | not_quote)) + CaselessLiteral("'")))).add_parse_action(token_map(str.lower))("simple_string_literal")
abstract_entity_declaration = (ABSTRACT)("abstract_entity_declaration")
abstract_supertype = ((ABSTRACT + SUPERTYPE + CaselessLiteral(";"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="abstract_supertype"))("abstract_supertype")
add_like_op = ((CaselessLiteral("+") | CaselessLiteral("-") | OR | XOR)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="add_like_op"))("add_like_op")
abstract_supertype = ((ABSTRACT + SUPERTYPE + CaselessLiteral(";"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="abstract_supertype"))("abstract_supertype")
add_like_op = ((CaselessLiteral("+") | CaselessLiteral("-") | OR | XOR)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="add_like_op"))("add_like_op")
attribute_id = (simple_id)("attribute_id")
boolean_type = (BOOLEAN)("boolean_type")
built_in_constant = ((CONST_E | PI | SELF | CaselessLiteral("?"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="built_in_constant"))("built_in_constant")
built_in_function = ((ABS | ACOS | ASIN | ATAN | BLENGTH | COS | EXISTS | EXP | FORMAT | HIBOUND | HIINDEX | LENGTH | LOBOUND | LOINDEX | LOG | LOG2 | LOG10 | NVL | ODD | ROLESOF | SIN | SIZEOF | SQRT | TAN | TYPEOF | USEDIN | VALUE | VALUE_IN | VALUE_UNIQUE)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="built_in_function"))("built_in_function")
built_in_procedure = ((INSERT | REMOVE)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="built_in_procedure"))("built_in_procedure")
built_in_constant = ((CONST_E | PI | SELF | CaselessLiteral("?"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="built_in_constant"))("built_in_constant")
built_in_function = ((ABS | ACOS | ASIN | ATAN | BLENGTH | COS | EXISTS | EXP | FORMAT | HIBOUND | HIINDEX | LENGTH | LOBOUND | LOINDEX | LOG | LOG2 | LOG10 | NVL | ODD | ROLESOF | SIN | SIZEOF | SQRT | TAN | TYPEOF | USEDIN | VALUE | VALUE_IN | VALUE_UNIQUE)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="built_in_function"))("built_in_function")
built_in_procedure = ((INSERT | REMOVE)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="built_in_procedure"))("built_in_procedure")
constant_id = (simple_id)("constant_id")
entity_id = (simple_id)("entity_id")
enumeration_id = (simple_id)("enumeration_id")
enumeration_items = ((CaselessLiteral("(") + enumeration_id + ZeroOrMore((CaselessLiteral(",") + enumeration_id)) + CaselessLiteral(")"))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="enumeration_items"))("enumeration_items")
escape_stmt = ((ESCAPE + CaselessLiteral(";"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="escape_stmt"))("escape_stmt")
enumeration_items = ((CaselessLiteral("(") + enumeration_id + ZeroOrMore((CaselessLiteral(",") + enumeration_id)) + CaselessLiteral(")"))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="enumeration_items"))("enumeration_items")
escape_stmt = ((ESCAPE + CaselessLiteral(";"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="escape_stmt"))("escape_stmt")
function_id = (simple_id)("function_id")
integer_type = (INTEGER)("integer_type")
interval_op = ((CaselessLiteral("<=") | CaselessLiteral("<"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="interval_op"))("interval_op")
logical_literal = ((FALSE | TRUE | UNKNOWN)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="logical_literal"))("logical_literal")
interval_op = ((CaselessLiteral("<=") | CaselessLiteral("<"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="interval_op"))("interval_op")
logical_literal = ((FALSE | TRUE | UNKNOWN)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="logical_literal"))("logical_literal")
logical_type = (LOGICAL)("logical_type")
multiplication_like_op = ((CaselessLiteral("*") | CaselessLiteral("/") | DIV | MOD | AND | CaselessLiteral("||"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="multiplication_like_op"))("multiplication_like_op")
null_stmt = (CaselessLiteral(";")).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="null_stmt"))("null_stmt")
multiplication_like_op = ((CaselessLiteral("*") | CaselessLiteral("/") | DIV | MOD | AND | CaselessLiteral("||"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="multiplication_like_op"))("multiplication_like_op")
null_stmt = (CaselessLiteral(";")).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="null_stmt"))("null_stmt")
number_type = (NUMBER)("number_type")
parameter_id = (simple_id)("parameter_id")
procedure_id = (simple_id)("procedure_id")
rel_op = ((CaselessLiteral("<=") | CaselessLiteral(">=") | CaselessLiteral("<>") | CaselessLiteral("=") | CaselessLiteral(":<>:") | CaselessLiteral(":=:") | CaselessLiteral("<") | CaselessLiteral(">"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="rel_op"))("rel_op")
rel_op_extended = ((rel_op | IN | LIKE)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="rel_op_extended"))("rel_op_extended")
rel_op = ((CaselessLiteral("<=") | CaselessLiteral(">=") | CaselessLiteral("<>") | CaselessLiteral("=") | CaselessLiteral(":<>:") | CaselessLiteral(":=:") | CaselessLiteral("<") | CaselessLiteral(">"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="rel_op"))("rel_op")
rel_op_extended = ((rel_op | IN | LIKE)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="rel_op_extended"))("rel_op_extended")
rule_id = (simple_id)("rule_id")
rule_label_id = (simple_id)("rule_label_id")
schema_id = (simple_id)("schema_id")
sign = ((CaselessLiteral("+") | CaselessLiteral("-"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="sign"))("sign")
skip_stmt = ((SKIP + CaselessLiteral(";"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="skip_stmt"))("skip_stmt")
sign = ((CaselessLiteral("+") | CaselessLiteral("-"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="sign"))("sign")
skip_stmt = ((SKIP + CaselessLiteral(";"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="skip_stmt"))("skip_stmt")
subtype_constraint_id = (simple_id)("subtype_constraint_id")
type_id = (simple_id)("type_id")
type_label_id = (simple_id)("type_label_id")
unary_op = ((CaselessLiteral("+") | CaselessLiteral("-") | NOT)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="unary_op"))("unary_op")
unary_op = ((CaselessLiteral("+") | CaselessLiteral("-") | NOT)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="unary_op"))("unary_op")
variable_id = (simple_id)("variable_id")
encoded_character = ((octet + octet + octet + octet)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="encoded_character"))("encoded_character")
not_paren_star = ((letter | digit | not_paren_star_special)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="not_paren_star"))("not_paren_star")
not_rparen_star = ((not_paren_star | CaselessLiteral("("))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="not_rparen_star"))("not_rparen_star")
not_rparen_star_then_rparen = ((not_rparen_star + ZeroOrMore(not_rparen_star) + CaselessLiteral(")") + ZeroOrMore(CaselessLiteral(")")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="not_rparen_star_then_rparen"))("not_rparen_star_then_rparen")
encoded_string_literal = ((CaselessLiteral("\"") + encoded_character + ZeroOrMore(encoded_character) + CaselessLiteral("\""))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="encoded_string_literal"))("encoded_string_literal")
encoded_character = ((octet + octet + octet + octet)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="encoded_character"))("encoded_character")
not_paren_star = ((letter | digit | not_paren_star_special)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="not_paren_star"))("not_paren_star")
not_rparen_star = ((not_paren_star | CaselessLiteral("("))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="not_rparen_star"))("not_rparen_star")
not_rparen_star_then_rparen = ((not_rparen_star + ZeroOrMore(not_rparen_star) + CaselessLiteral(")") + ZeroOrMore(CaselessLiteral(")")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="not_rparen_star_then_rparen"))("not_rparen_star_then_rparen")
encoded_string_literal = ((CaselessLiteral("\"") + encoded_character + ZeroOrMore(encoded_character) + CaselessLiteral("\""))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="encoded_string_literal"))("encoded_string_literal")
real_literal = (((digits + CaselessLiteral(".") + Optional(digits) + Optional((CaselessLiteral("e") + Optional(sign) + digits))) | integer_literal))("real_literal")
attribute_ref = (attribute_id)("attribute_ref")
constant_ref = (constant_id)("constant_ref")
@@ -210,276 +210,276 @@ def parse(fn: str) -> mapping.Mapping:
type_label_ref = (type_label_id)("type_label_ref")
type_ref = (type_id)("type_ref")
variable_ref = (variable_id)("variable_ref")
attribute_qualifier = ((CaselessLiteral(".") + attribute_ref)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="attribute_qualifier"))("attribute_qualifier")
constant_factor = ((built_in_constant | constant_ref)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="constant_factor"))("constant_factor")
enumeration_extension = ((BASED_ON + type_ref + Optional((WITH + enumeration_items)))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="enumeration_extension"))("enumeration_extension")
enumeration_reference = ((Optional((type_ref + CaselessLiteral("."))) + enumeration_ref)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="enumeration_reference"))("enumeration_reference")
enumeration_type = ((Optional(EXTENSIBLE) + ENUMERATION + Optional(((OF + enumeration_items) | enumeration_extension)))).setParseAction(EnumerationType)("enumeration_type")
general_ref = ((parameter_ref | variable_ref)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="general_ref"))("general_ref")
group_qualifier = ((CaselessLiteral("\\") + entity_ref)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="group_qualifier"))("group_qualifier")
named_types = ((entity_ref | type_ref)).setParseAction(NamedType)("named_types")
named_type_or_rename = ((named_types + Optional((AS + (entity_id | type_id))))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="named_type_or_rename"))("named_type_or_rename")
attribute_qualifier = ((CaselessLiteral(".") + attribute_ref)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="attribute_qualifier"))("attribute_qualifier")
constant_factor = ((built_in_constant | constant_ref)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="constant_factor"))("constant_factor")
enumeration_extension = ((BASED_ON + type_ref + Optional((WITH + enumeration_items)))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="enumeration_extension"))("enumeration_extension")
enumeration_reference = ((Optional((type_ref + CaselessLiteral("."))) + enumeration_ref)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="enumeration_reference"))("enumeration_reference")
enumeration_type = ((Optional(EXTENSIBLE) + ENUMERATION + Optional(((OF + enumeration_items) | enumeration_extension)))).set_parse_action(EnumerationType)("enumeration_type")
general_ref = ((parameter_ref | variable_ref)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="general_ref"))("general_ref")
group_qualifier = ((CaselessLiteral("\\") + entity_ref)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="group_qualifier"))("group_qualifier")
named_types = ((entity_ref | type_ref)).set_parse_action(NamedType)("named_types")
named_type_or_rename = ((named_types + Optional((AS + (entity_id | type_id))))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="named_type_or_rename"))("named_type_or_rename")
population = (entity_ref)("population")
qualified_attribute = ((SELF + group_qualifier + attribute_qualifier)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="qualified_attribute"))("qualified_attribute")
redeclared_attribute = ((qualified_attribute + Optional((RENAMED + attribute_id)))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="redeclared_attribute"))("redeclared_attribute")
referenced_attribute = ((attribute_ref | qualified_attribute)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="referenced_attribute"))("referenced_attribute")
rename_id = ((constant_id | entity_id | function_id | procedure_id | type_id)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="rename_id"))("rename_id")
resource_ref = ((constant_ref | entity_ref | function_ref | procedure_ref | type_ref)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="resource_ref"))("resource_ref")
rule_head = ((RULE + rule_id + FOR + CaselessLiteral("(") + entity_ref + ZeroOrMore((CaselessLiteral(",") + entity_ref)) + CaselessLiteral(")") + CaselessLiteral(";"))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="rule_head"))("rule_head")
select_list = ((CaselessLiteral("(") + named_types + ZeroOrMore((CaselessLiteral(",") + named_types)) + CaselessLiteral(")"))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="select_list"))("select_list")
qualified_attribute = ((SELF + group_qualifier + attribute_qualifier)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="qualified_attribute"))("qualified_attribute")
redeclared_attribute = ((qualified_attribute + Optional((RENAMED + attribute_id)))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="redeclared_attribute"))("redeclared_attribute")
referenced_attribute = ((attribute_ref | qualified_attribute)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="referenced_attribute"))("referenced_attribute")
rename_id = ((constant_id | entity_id | function_id | procedure_id | type_id)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="rename_id"))("rename_id")
resource_ref = ((constant_ref | entity_ref | function_ref | procedure_ref | type_ref)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="resource_ref"))("resource_ref")
rule_head = ((RULE + rule_id + FOR + CaselessLiteral("(") + entity_ref + ZeroOrMore((CaselessLiteral(",") + entity_ref)) + CaselessLiteral(")") + CaselessLiteral(";"))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="rule_head"))("rule_head")
select_list = ((CaselessLiteral("(") + named_types + ZeroOrMore((CaselessLiteral(",") + named_types)) + CaselessLiteral(")"))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="select_list"))("select_list")
string_literal = ((simple_string_literal | encoded_string_literal))("string_literal")
subtype_constraint_head = ((SUBTYPE_CONSTRAINT + subtype_constraint_id + FOR + entity_ref + CaselessLiteral(";"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="subtype_constraint_head"))("subtype_constraint_head")
subtype_declaration = ((SUBTYPE + OF + CaselessLiteral("(") + entity_ref + ZeroOrMore((CaselessLiteral(",") + entity_ref)) + CaselessLiteral(")"))).setParseAction(SubTypeExpression)("subtype_declaration")
total_over = ((TOTAL_OVER + CaselessLiteral("(") + entity_ref + ZeroOrMore((CaselessLiteral(",") + entity_ref)) + CaselessLiteral(")") + CaselessLiteral(";"))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="total_over"))("total_over")
type_label = ((type_label_id | type_label_ref)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="type_label"))("type_label")
unique_rule = ((Optional((rule_label_id + CaselessLiteral(":"))) + referenced_attribute + ZeroOrMore((CaselessLiteral(",") + referenced_attribute)))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="unique_rule"))("unique_rule")
use_clause = ((USE + FROM + schema_ref + Optional((CaselessLiteral("(") + named_type_or_rename + ZeroOrMore((CaselessLiteral(",") + named_type_or_rename)) + CaselessLiteral(")"))) + CaselessLiteral(";"))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="use_clause"))("use_clause")
not_lparen_star = ((not_paren_star | CaselessLiteral(")"))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="not_lparen_star"))("not_lparen_star")
remark_ref = ((attribute_ref | constant_ref | entity_ref | enumeration_ref | function_ref | parameter_ref | procedure_ref | rule_label_ref | rule_ref | schema_ref | subtype_constraint_ref | type_label_ref | type_ref | variable_ref)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="remark_ref"))("remark_ref")
attribute_decl = ((redeclared_attribute | attribute_id)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="attribute_decl"))("attribute_decl")
generic_entity_type = ((GENERIC_ENTITY + Optional((CaselessLiteral(":") + type_label)))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="generic_entity_type"))("generic_entity_type")
generic_type = ((GENERIC + Optional((CaselessLiteral(":") + type_label)))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="generic_type"))("generic_type")
literal = ((binary_literal | logical_literal | real_literal | string_literal)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="literal"))("literal")
resource_or_rename = ((resource_ref + Optional((AS + rename_id)))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="resource_or_rename"))("resource_or_rename")
subtype_constraint_head = ((SUBTYPE_CONSTRAINT + subtype_constraint_id + FOR + entity_ref + CaselessLiteral(";"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="subtype_constraint_head"))("subtype_constraint_head")
subtype_declaration = ((SUBTYPE + OF + CaselessLiteral("(") + entity_ref + ZeroOrMore((CaselessLiteral(",") + entity_ref)) + CaselessLiteral(")"))).set_parse_action(SubTypeExpression)("subtype_declaration")
total_over = ((TOTAL_OVER + CaselessLiteral("(") + entity_ref + ZeroOrMore((CaselessLiteral(",") + entity_ref)) + CaselessLiteral(")") + CaselessLiteral(";"))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="total_over"))("total_over")
type_label = ((type_label_id | type_label_ref)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="type_label"))("type_label")
unique_rule = ((Optional((rule_label_id + CaselessLiteral(":"))) + referenced_attribute + ZeroOrMore((CaselessLiteral(",") + referenced_attribute)))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="unique_rule"))("unique_rule")
use_clause = ((USE + FROM + schema_ref + Optional((CaselessLiteral("(") + named_type_or_rename + ZeroOrMore((CaselessLiteral(",") + named_type_or_rename)) + CaselessLiteral(")"))) + CaselessLiteral(";"))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="use_clause"))("use_clause")
not_lparen_star = ((not_paren_star | CaselessLiteral(")"))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="not_lparen_star"))("not_lparen_star")
remark_ref = ((attribute_ref | constant_ref | entity_ref | enumeration_ref | function_ref | parameter_ref | procedure_ref | rule_label_ref | rule_ref | schema_ref | subtype_constraint_ref | type_label_ref | type_ref | variable_ref)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="remark_ref"))("remark_ref")
attribute_decl = ((redeclared_attribute | attribute_id)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="attribute_decl"))("attribute_decl")
generic_entity_type = ((GENERIC_ENTITY + Optional((CaselessLiteral(":") + type_label)))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="generic_entity_type"))("generic_entity_type")
generic_type = ((GENERIC + Optional((CaselessLiteral(":") + type_label)))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="generic_type"))("generic_type")
literal = ((binary_literal | logical_literal | real_literal | string_literal)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="literal"))("literal")
resource_or_rename = ((resource_ref + Optional((AS + rename_id)))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="resource_or_rename"))("resource_or_rename")
schema_version_id = (string_literal)("schema_version_id")
select_extension = ((BASED_ON + type_ref + Optional((WITH + select_list)))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="select_extension"))("select_extension")
select_type = ((Optional((EXTENSIBLE + Optional(GENERIC_ENTITY))) + SELECT + Optional((select_list | select_extension)))).setParseAction(SelectType)("select_type")
unique_clause = ((UNIQUE + unique_rule + CaselessLiteral(";") + ZeroOrMore((unique_rule + CaselessLiteral(";"))))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="unique_clause"))("unique_clause")
lparen_then_not_lparen_star = ((CaselessLiteral("(") + ZeroOrMore(CaselessLiteral("(")) + not_lparen_star + ZeroOrMore(not_lparen_star))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="lparen_then_not_lparen_star"))("lparen_then_not_lparen_star")
remark_tag = ((CaselessLiteral("\"") + remark_ref + ZeroOrMore((CaselessLiteral(".") + remark_ref)) + CaselessLiteral("\""))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="remark_tag"))("remark_tag")
tail_remark = ((CaselessLiteral("--") + Optional(remark_tag))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="tail_remark"))("tail_remark")
constructed_types = ((enumeration_type | select_type)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="constructed_types"))("constructed_types")
reference_clause = ((REFERENCE + FROM + schema_ref + Optional((CaselessLiteral("(") + resource_or_rename + ZeroOrMore((CaselessLiteral(",") + resource_or_rename)) + CaselessLiteral(")"))) + CaselessLiteral(";"))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="reference_clause"))("reference_clause")
interface_specification = ((reference_clause | use_clause)).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="interface_specification"))("interface_specification")
rule_decl = Forward()("rule_decl")
supertype_term = Forward()("supertype_term")
alias_stmt = Forward()("alias_stmt")
subtype_constraint_decl = Forward()("subtype_constraint_decl")
real_type = Forward()("real_type")
until_control = Forward()("until_control")
remark = Forward()("remark")
syntax = Forward()("syntax")
derived_attr = Forward()("derived_attr")
subtype_constraint = Forward()("subtype_constraint")
aggregation_types = Forward()("aggregation_types")
width = Forward()("width")
simple_expression = Forward()("simple_expression")
explicit_attr = Forward()("explicit_attr")
precision_spec = Forward()("precision_spec")
general_list_type = Forward()("general_list_type")
concrete_types = Forward()("concrete_types")
while_control = Forward()("while_control")
aggregate_type = Forward()("aggregate_type")
increment_control = Forward()("increment_control")
index_qualifier = Forward()("index_qualifier")
supertype_rule = Forward()("supertype_rule")
subsuper = Forward()("subsuper")
interval_low = Forward()("interval_low")
bound_2 = Forward()("bound_2")
index_1 = Forward()("index_1")
return_stmt = Forward()("return_stmt")
type_decl = Forward()("type_decl")
increment = Forward()("increment")
factor = Forward()("factor")
underlying_type = Forward()("underlying_type")
declaration = Forward()("declaration")
function_decl = Forward()("function_decl")
entity_constructor = Forward()("entity_constructor")
derive_clause = Forward()("derive_clause")
interval_item = Forward()("interval_item")
stmt = Forward()("stmt")
repeat_control = Forward()("repeat_control")
select_extension = ((BASED_ON + type_ref + Optional((WITH + select_list)))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="select_extension"))("select_extension")
select_type = ((Optional((EXTENSIBLE + Optional(GENERIC_ENTITY))) + SELECT + Optional((select_list | select_extension)))).set_parse_action(SelectType)("select_type")
unique_clause = ((UNIQUE + unique_rule + CaselessLiteral(";") + ZeroOrMore((unique_rule + CaselessLiteral(";"))))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="unique_clause"))("unique_clause")
lparen_then_not_lparen_star = ((CaselessLiteral("(") + ZeroOrMore(CaselessLiteral("(")) + not_lparen_star + ZeroOrMore(not_lparen_star))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="lparen_then_not_lparen_star"))("lparen_then_not_lparen_star")
remark_tag = ((CaselessLiteral("\"") + remark_ref + ZeroOrMore((CaselessLiteral(".") + remark_ref)) + CaselessLiteral("\""))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="remark_tag"))("remark_tag")
tail_remark = ((CaselessLiteral("--") + Optional(remark_tag))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="tail_remark"))("tail_remark")
constructed_types = ((enumeration_type | select_type)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="constructed_types"))("constructed_types")
reference_clause = ((REFERENCE + FROM + schema_ref + Optional((CaselessLiteral("(") + resource_or_rename + ZeroOrMore((CaselessLiteral(",") + resource_or_rename)) + CaselessLiteral(")"))) + CaselessLiteral(";"))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="reference_clause"))("reference_clause")
interface_specification = ((reference_clause | use_clause)).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="interface_specification"))("interface_specification")
abstract_supertype_declaration = Forward()("abstract_supertype_declaration")
bound_spec = Forward()("bound_spec")
entity_decl = Forward()("entity_decl")
actual_parameter_list = Forward()("actual_parameter_list")
aggregate_initializer = Forward()("aggregate_initializer")
aggregate_source = Forward()("aggregate_source")
aggregate_type = Forward()("aggregate_type")
aggregation_types = Forward()("aggregation_types")
algorithm_head = Forward()("algorithm_head")
alias_stmt = Forward()("alias_stmt")
array_type = Forward()("array_type")
assignment_stmt = Forward()("assignment_stmt")
bag_type = Forward()("bag_type")
binary_type = Forward()("binary_type")
bound_1 = Forward()("bound_1")
bound_2 = Forward()("bound_2")
bound_spec = Forward()("bound_spec")
case_action = Forward()("case_action")
case_label = Forward()("case_label")
case_stmt = Forward()("case_stmt")
compound_stmt = Forward()("compound_stmt")
concrete_types = Forward()("concrete_types")
constant_body = Forward()("constant_body")
constant_decl = Forward()("constant_decl")
declaration = Forward()("declaration")
derive_clause = Forward()("derive_clause")
derived_attr = Forward()("derived_attr")
domain_rule = Forward()("domain_rule")
element = Forward()("element")
embedded_remark = Forward()("embedded_remark")
entity_body = Forward()("entity_body")
entity_constructor = Forward()("entity_constructor")
entity_decl = Forward()("entity_decl")
entity_head = Forward()("entity_head")
explicit_attr = Forward()("explicit_attr")
expression = Forward()("expression")
factor = Forward()("factor")
formal_parameter = Forward()("formal_parameter")
function_call = Forward()("function_call")
function_decl = Forward()("function_decl")
function_head = Forward()("function_head")
general_aggregation_types = Forward()("general_aggregation_types")
general_array_type = Forward()("general_array_type")
general_bag_type = Forward()("general_bag_type")
general_list_type = Forward()("general_list_type")
general_set_type = Forward()("general_set_type")
generalized_types = Forward()("generalized_types")
if_stmt = Forward()("if_stmt")
increment = Forward()("increment")
increment_control = Forward()("increment_control")
index = Forward()("index")
index_1 = Forward()("index_1")
index_2 = Forward()("index_2")
index_qualifier = Forward()("index_qualifier")
instantiable_type = Forward()("instantiable_type")
interval = Forward()("interval")
interval_high = Forward()("interval_high")
interval_item = Forward()("interval_item")
interval_low = Forward()("interval_low")
inverse_attr = Forward()("inverse_attr")
inverse_clause = Forward()("inverse_clause")
list_type = Forward()("list_type")
local_decl = Forward()("local_decl")
local_variable = Forward()("local_variable")
logical_expression = Forward()("logical_expression")
numeric_expression = Forward()("numeric_expression")
string_type = Forward()("string_type")
formal_parameter = Forward()("formal_parameter")
generalized_types = Forward()("generalized_types")
function_head = Forward()("function_head")
constant_decl = Forward()("constant_decl")
actual_parameter_list = Forward()("actual_parameter_list")
subtype_constraint_body = Forward()("subtype_constraint_body")
procedure_decl = Forward()("procedure_decl")
case_action = Forward()("case_action")
term = Forward()("term")
general_aggregation_types = Forward()("general_aggregation_types")
function_call = Forward()("function_call")
where_clause = Forward()("where_clause")
local_variable = Forward()("local_variable")
aggregate_initializer = Forward()("aggregate_initializer")
binary_type = Forward()("binary_type")
index = Forward()("index")
case_stmt = Forward()("case_stmt")
general_set_type = Forward()("general_set_type")
procedure_call_stmt = Forward()("procedure_call_stmt")
instantiable_type = Forward()("instantiable_type")
general_array_type = Forward()("general_array_type")
set_type = Forward()("set_type")
supertype_factor = Forward()("supertype_factor")
index_2 = Forward()("index_2")
qualifiable_factor = Forward()("qualifiable_factor")
algorithm_head = Forward()("algorithm_head")
parameter_type = Forward()("parameter_type")
one_of = Forward()("one_of")
compound_stmt = Forward()("compound_stmt")
primary = Forward()("primary")
schema_decl = Forward()("schema_decl")
embedded_remark = Forward()("embedded_remark")
width_spec = Forward()("width_spec")
assignment_stmt = Forward()("assignment_stmt")
element = Forward()("element")
schema_body = Forward()("schema_body")
procedure_head = Forward()("procedure_head")
general_bag_type = Forward()("general_bag_type")
entity_head = Forward()("entity_head")
entity_body = Forward()("entity_body")
list_type = Forward()("list_type")
expression = Forward()("expression")
parameter = Forward()("parameter")
simple_types = Forward()("simple_types")
bag_type = Forward()("bag_type")
repetition = Forward()("repetition")
constant_body = Forward()("constant_body")
if_stmt = Forward()("if_stmt")
inverse_attr = Forward()("inverse_attr")
interval = Forward()("interval")
query_expression = Forward()("query_expression")
array_type = Forward()("array_type")
simple_factor = Forward()("simple_factor")
case_label = Forward()("case_label")
domain_rule = Forward()("domain_rule")
repeat_stmt = Forward()("repeat_stmt")
supertype_expression = Forward()("supertype_expression")
supertype_constraint = Forward()("supertype_constraint")
interval_high = Forward()("interval_high")
local_decl = Forward()("local_decl")
selector = Forward()("selector")
aggregate_source = Forward()("aggregate_source")
parameter_type = Forward()("parameter_type")
precision_spec = Forward()("precision_spec")
primary = Forward()("primary")
procedure_call_stmt = Forward()("procedure_call_stmt")
procedure_decl = Forward()("procedure_decl")
procedure_head = Forward()("procedure_head")
qualifiable_factor = Forward()("qualifiable_factor")
qualifier = Forward()("qualifier")
rule_decl << (((rule_head + algorithm_head + ZeroOrMore(stmt) + where_clause + END_RULE + CaselessLiteral(";")))).setParseAction(RuleDeclaration)
supertype_term << (((one_of | (CaselessLiteral("(") + supertype_expression + CaselessLiteral(")")) | entity_ref))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="supertype_term"))
alias_stmt << (((ALIAS + variable_id + FOR + general_ref + ZeroOrMore(qualifier) + CaselessLiteral(";") + stmt + ZeroOrMore(stmt) + END_ALIAS + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="alias_stmt"))
subtype_constraint_decl << (((subtype_constraint_head + subtype_constraint_body + END_SUBTYPE_CONSTRAINT + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="subtype_constraint_decl"))
real_type << (((REAL + Optional((CaselessLiteral("(") + precision_spec + CaselessLiteral(")")))))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="real_type"))
until_control << (((UNTIL + logical_expression))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="until_control"))
remark << (((embedded_remark | tail_remark))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="remark"))
syntax << (((schema_decl + ZeroOrMore(schema_decl)))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="syntax"))
derived_attr << (((attribute_decl + CaselessLiteral(":") + parameter_type + CaselessLiteral(":=") + expression + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="derived_attr"))
subtype_constraint << (((OF + CaselessLiteral("(") + supertype_expression + CaselessLiteral(")")))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="subtype_constraint"))
aggregation_types << (((array_type | bag_type | list_type | set_type))).setParseAction(AggregationType)
width << (numeric_expression)
simple_expression << (((term + ZeroOrMore((add_like_op + term))))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="simple_expression"))
explicit_attr << (((attribute_decl + ZeroOrMore((CaselessLiteral(",") + attribute_decl)) + CaselessLiteral(":") + Optional(OPTIONAL) + parameter_type + CaselessLiteral(";")))).setParseAction(ExplicitAttribute)
precision_spec << (numeric_expression)
general_list_type << (((LIST + Optional(bound_spec) + OF + Optional(UNIQUE) + parameter_type))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="general_list_type"))
concrete_types << (((aggregation_types | simple_types | type_ref))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="concrete_types"))
while_control << (((WHILE + logical_expression))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="while_control"))
aggregate_type << (((AGGREGATE + Optional((CaselessLiteral(":") + type_label)) + OF + parameter_type))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="aggregate_type"))
increment_control << (((variable_id + CaselessLiteral(":=") + bound_1 + TO + bound_2 + Optional((BY + increment))))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="increment_control"))
index_qualifier << (((CaselessLiteral("[") + index_1 + Optional((CaselessLiteral(":") + index_2)) + CaselessLiteral("]")))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="index_qualifier"))
supertype_rule << (((SUPERTYPE + subtype_constraint))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="supertype_rule"))
subsuper << (((Optional(supertype_constraint) + Optional(subtype_declaration)))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="subsuper"))
interval_low << (simple_expression)
bound_2 << (numeric_expression)
index_1 << (index)
return_stmt << (((RETURN + Optional((CaselessLiteral("(") + expression + CaselessLiteral(")"))) + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="return_stmt"))
type_decl << (((TYPE + type_id + CaselessLiteral("=") + underlying_type + CaselessLiteral(";") + Optional(where_clause) + END_TYPE + CaselessLiteral(";")))).setParseAction(TypeDeclaration)
increment << (numeric_expression)
factor << (((simple_factor + Optional((CaselessLiteral("**") + simple_factor))))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="factor"))
underlying_type << (((constructed_types | concrete_types))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="underlying_type"))
declaration << (((entity_decl | function_decl | procedure_decl | subtype_constraint_decl | type_decl))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="declaration"))
function_decl << (((function_head + algorithm_head + stmt + ZeroOrMore(stmt) + END_FUNCTION + CaselessLiteral(";")))).setParseAction(FunctionDeclaration)
entity_constructor << (((entity_ref + CaselessLiteral("(") + Optional((expression + ZeroOrMore((CaselessLiteral(",") + expression)))) + CaselessLiteral(")")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="entity_constructor"))
derive_clause << (((DERIVE + derived_attr + ZeroOrMore(derived_attr)))).setParseAction(AttributeList)
interval_item << (simple_expression)
stmt << (((alias_stmt | assignment_stmt | case_stmt | compound_stmt | escape_stmt | if_stmt | null_stmt | procedure_call_stmt | repeat_stmt | return_stmt | skip_stmt))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="stmt"))
repeat_control << (((Optional(increment_control) + Optional(while_control) + Optional(until_control)))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="repeat_control"))
abstract_supertype_declaration << (((ABSTRACT + SUPERTYPE + Optional(subtype_constraint)))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="abstract_supertype_declaration"))
bound_spec << (((CaselessLiteral("[") + bound_1 + CaselessLiteral(":") + bound_2 + CaselessLiteral("]")))).setParseAction(BoundSpecification)
entity_decl << (((entity_head + entity_body + END_ENTITY + CaselessLiteral(";")))).setParseAction(EntityDeclaration)
query_expression = Forward()("query_expression")
real_type = Forward()("real_type")
remark = Forward()("remark")
repeat_control = Forward()("repeat_control")
repeat_stmt = Forward()("repeat_stmt")
repetition = Forward()("repetition")
return_stmt = Forward()("return_stmt")
rule_decl = Forward()("rule_decl")
schema_body = Forward()("schema_body")
schema_decl = Forward()("schema_decl")
selector = Forward()("selector")
set_type = Forward()("set_type")
simple_expression = Forward()("simple_expression")
simple_factor = Forward()("simple_factor")
simple_types = Forward()("simple_types")
stmt = Forward()("stmt")
string_type = Forward()("string_type")
subsuper = Forward()("subsuper")
subtype_constraint = Forward()("subtype_constraint")
subtype_constraint_body = Forward()("subtype_constraint_body")
subtype_constraint_decl = Forward()("subtype_constraint_decl")
supertype_constraint = Forward()("supertype_constraint")
supertype_expression = Forward()("supertype_expression")
supertype_factor = Forward()("supertype_factor")
supertype_rule = Forward()("supertype_rule")
supertype_term = Forward()("supertype_term")
syntax = Forward()("syntax")
term = Forward()("term")
type_decl = Forward()("type_decl")
underlying_type = Forward()("underlying_type")
until_control = Forward()("until_control")
where_clause = Forward()("where_clause")
while_control = Forward()("while_control")
width = Forward()("width")
width_spec = Forward()("width_spec")
abstract_supertype_declaration << (((ABSTRACT + SUPERTYPE + Optional(subtype_constraint)))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="abstract_supertype_declaration"))
actual_parameter_list << (((CaselessLiteral("(") + Optional(parameter) + ZeroOrMore((CaselessLiteral(",") + parameter)) + CaselessLiteral(")")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="actual_parameter_list"))
aggregate_initializer << (((CaselessLiteral("[") + Optional((element + ZeroOrMore((CaselessLiteral(",") + element)))) + CaselessLiteral("]")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="aggregate_initializer"))
aggregate_source << (simple_expression)
aggregate_type << (((AGGREGATE + Optional((CaselessLiteral(":") + type_label)) + OF + parameter_type))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="aggregate_type"))
aggregation_types << (((array_type | bag_type | list_type | set_type))).set_parse_action(AggregationType)
algorithm_head << (((ZeroOrMore(declaration) + Optional(constant_decl) + Optional(local_decl)))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="algorithm_head"))
alias_stmt << (((ALIAS + variable_id + FOR + general_ref + ZeroOrMore(qualifier) + CaselessLiteral(";") + stmt + ZeroOrMore(stmt) + END_ALIAS + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="alias_stmt"))
array_type << (((ARRAY + bound_spec + OF + Optional(OPTIONAL) + Optional(UNIQUE) + instantiable_type))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="array_type"))
assignment_stmt << (((general_ref + ZeroOrMore(qualifier) + CaselessLiteral(":=") + expression + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="assignment_stmt"))
bag_type << (((BAG + Optional(bound_spec) + OF + instantiable_type))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="bag_type"))
binary_type << (((BINARY + Optional(width_spec)))).set_parse_action(BinaryType)
bound_1 << (numeric_expression)
inverse_clause << (((INVERSE + inverse_attr + ZeroOrMore(inverse_attr)))).setParseAction(AttributeList)
bound_2 << (numeric_expression)
bound_spec << (((CaselessLiteral("[") + bound_1 + CaselessLiteral(":") + bound_2 + CaselessLiteral("]")))).set_parse_action(BoundSpecification)
case_action << (((case_label + ZeroOrMore((CaselessLiteral(",") + case_label)) + CaselessLiteral(":") + stmt))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="case_action"))
case_label << (expression)
case_stmt << (((CASE + selector + OF + ZeroOrMore(case_action) + Optional((OTHERWISE + CaselessLiteral(":") + stmt)) + END_CASE + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="case_stmt"))
compound_stmt << (((BEGIN + stmt + ZeroOrMore(stmt) + END + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="compound_stmt"))
concrete_types << (((aggregation_types | simple_types | type_ref))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="concrete_types"))
constant_body << (((constant_id + CaselessLiteral(":") + instantiable_type + CaselessLiteral(":=") + expression + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="constant_body"))
constant_decl << (((CONSTANT + constant_body + ZeroOrMore(constant_body) + END_CONSTANT + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="constant_decl"))
declaration << (((entity_decl | function_decl | procedure_decl | subtype_constraint_decl | type_decl))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="declaration"))
derive_clause << (((DERIVE + derived_attr + ZeroOrMore(derived_attr)))).set_parse_action(AttributeList)
derived_attr << (((attribute_decl + CaselessLiteral(":") + parameter_type + CaselessLiteral(":=") + expression + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="derived_attr"))
domain_rule << (((Optional((rule_label_id + CaselessLiteral(":"))) + expression))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="domain_rule"))
element << (((expression + Optional((CaselessLiteral(":") + repetition))))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="element"))
embedded_remark << (((CaselessLiteral("(*") + Optional(remark_tag) + ZeroOrMore(((not_paren_star + ZeroOrMore(not_paren_star)) | lparen_then_not_lparen_star | (CaselessLiteral("*") + ZeroOrMore(CaselessLiteral("*"))) | not_rparen_star_then_rparen | embedded_remark)) + CaselessLiteral("*)")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="embedded_remark"))
entity_body << (((ZeroOrMore(explicit_attr) + Optional(derive_clause) + Optional(inverse_clause) + Optional(unique_clause) + Optional(where_clause)))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="entity_body"))
entity_constructor << (((entity_ref + CaselessLiteral("(") + Optional((expression + ZeroOrMore((CaselessLiteral(",") + expression)))) + CaselessLiteral(")")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="entity_constructor"))
entity_decl << (((entity_head + entity_body + END_ENTITY + CaselessLiteral(";")))).set_parse_action(EntityDeclaration)
entity_head << (((ENTITY + entity_id + subsuper + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="entity_head"))
explicit_attr << (((attribute_decl + ZeroOrMore((CaselessLiteral(",") + attribute_decl)) + CaselessLiteral(":") + Optional(OPTIONAL) + parameter_type + CaselessLiteral(";")))).set_parse_action(ExplicitAttribute)
expression << (((simple_expression + Optional((rel_op_extended + simple_expression))))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="expression"))
factor << (((simple_factor + Optional((CaselessLiteral("**") + simple_factor))))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="factor"))
formal_parameter << (((parameter_id + ZeroOrMore((CaselessLiteral(",") + parameter_id)) + CaselessLiteral(":") + parameter_type))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="formal_parameter"))
function_call << ((((built_in_function | function_ref) + actual_parameter_list))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="function_call"))
function_decl << (((function_head + algorithm_head + stmt + ZeroOrMore(stmt) + END_FUNCTION + CaselessLiteral(";")))).set_parse_action(FunctionDeclaration)
function_head << (((FUNCTION + function_id + Optional((CaselessLiteral("(") + formal_parameter + ZeroOrMore((CaselessLiteral(";") + formal_parameter)) + CaselessLiteral(")"))) + CaselessLiteral(":") + parameter_type + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="function_head"))
general_aggregation_types << (((general_array_type | general_bag_type | general_list_type | general_set_type))).set_parse_action(AggregationType)
general_array_type << (((ARRAY + Optional(bound_spec) + OF + Optional(OPTIONAL) + Optional(UNIQUE) + parameter_type))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="general_array_type"))
general_bag_type << (((BAG + Optional(bound_spec) + OF + parameter_type))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="general_bag_type"))
general_list_type << (((LIST + Optional(bound_spec) + OF + Optional(UNIQUE) + parameter_type))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="general_list_type"))
general_set_type << (((SET + Optional(bound_spec) + OF + parameter_type))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="general_set_type"))
generalized_types << (((aggregate_type | general_aggregation_types | generic_entity_type | generic_type))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="generalized_types"))
if_stmt << (((IF + logical_expression + THEN + stmt + ZeroOrMore(stmt) + Optional((ELSE + stmt + ZeroOrMore(stmt))) + END_IF + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="if_stmt"))
increment << (numeric_expression)
increment_control << (((variable_id + CaselessLiteral(":=") + bound_1 + TO + bound_2 + Optional((BY + increment))))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="increment_control"))
index << (numeric_expression)
index_1 << (index)
index_2 << (index)
index_qualifier << (((CaselessLiteral("[") + index_1 + Optional((CaselessLiteral(":") + index_2)) + CaselessLiteral("]")))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="index_qualifier"))
instantiable_type << (((concrete_types | entity_ref))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="instantiable_type"))
interval << (((CaselessLiteral("{") + interval_low + interval_op + interval_item + interval_op + interval_high + CaselessLiteral("}")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="interval"))
interval_high << (simple_expression)
interval_item << (simple_expression)
interval_low << (simple_expression)
inverse_attr << (((attribute_decl + CaselessLiteral(":") + Optional(((SET | BAG) + Optional(bound_spec) + OF)) + entity_ref + FOR + Optional((entity_ref + CaselessLiteral("."))) + attribute_ref + CaselessLiteral(";")))).set_parse_action(InverseAttribute)
inverse_clause << (((INVERSE + inverse_attr + ZeroOrMore(inverse_attr)))).set_parse_action(AttributeList)
list_type << (((LIST + Optional(bound_spec) + OF + Optional(UNIQUE) + instantiable_type))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="list_type"))
local_decl << (((LOCAL + local_variable + ZeroOrMore(local_variable) + END_LOCAL + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="local_decl"))
local_variable << (((variable_id + ZeroOrMore((CaselessLiteral(",") + variable_id)) + CaselessLiteral(":") + parameter_type + Optional((CaselessLiteral(":=") + expression)) + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="local_variable"))
logical_expression << (expression)
numeric_expression << (simple_expression)
string_type << (((STRING + Optional(width_spec)))).setParseAction(StringType)
formal_parameter << (((parameter_id + ZeroOrMore((CaselessLiteral(",") + parameter_id)) + CaselessLiteral(":") + parameter_type))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="formal_parameter"))
generalized_types << (((aggregate_type | general_aggregation_types | generic_entity_type | generic_type))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="generalized_types"))
function_head << (((FUNCTION + function_id + Optional((CaselessLiteral("(") + formal_parameter + ZeroOrMore((CaselessLiteral(";") + formal_parameter)) + CaselessLiteral(")"))) + CaselessLiteral(":") + parameter_type + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="function_head"))
constant_decl << (((CONSTANT + constant_body + ZeroOrMore(constant_body) + END_CONSTANT + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="constant_decl"))
actual_parameter_list << (((CaselessLiteral("(") + Optional(parameter) + ZeroOrMore((CaselessLiteral(",") + parameter)) + CaselessLiteral(")")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="actual_parameter_list"))
subtype_constraint_body << (((Optional(abstract_supertype) + Optional(total_over) + Optional((supertype_expression + CaselessLiteral(";")))))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="subtype_constraint_body"))
procedure_decl << (((procedure_head + algorithm_head + ZeroOrMore(stmt) + END_PROCEDURE + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="procedure_decl"))
case_action << (((case_label + ZeroOrMore((CaselessLiteral(",") + case_label)) + CaselessLiteral(":") + stmt))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="case_action"))
term << (((factor + ZeroOrMore((multiplication_like_op + factor))))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="term"))
general_aggregation_types << (((general_array_type | general_bag_type | general_list_type | general_set_type))).setParseAction(AggregationType)
function_call << ((((built_in_function | function_ref) + actual_parameter_list))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="function_call"))
where_clause << (((WHERE + domain_rule + CaselessLiteral(";") + ZeroOrMore((domain_rule + CaselessLiteral(";")))))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="where_clause"))
local_variable << (((variable_id + ZeroOrMore((CaselessLiteral(",") + variable_id)) + CaselessLiteral(":") + parameter_type + Optional((CaselessLiteral(":=") + expression)) + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="local_variable"))
aggregate_initializer << (((CaselessLiteral("[") + Optional((element + ZeroOrMore((CaselessLiteral(",") + element)))) + CaselessLiteral("]")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="aggregate_initializer"))
binary_type << (((BINARY + Optional(width_spec)))).setParseAction(BinaryType)
index << (numeric_expression)
case_stmt << (((CASE + selector + OF + ZeroOrMore(case_action) + Optional((OTHERWISE + CaselessLiteral(":") + stmt)) + END_CASE + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="case_stmt"))
general_set_type << (((SET + Optional(bound_spec) + OF + parameter_type))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="general_set_type"))
procedure_call_stmt << ((((built_in_procedure | procedure_ref) + actual_parameter_list + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="procedure_call_stmt"))
instantiable_type << (((concrete_types | entity_ref))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="instantiable_type"))
general_array_type << (((ARRAY + Optional(bound_spec) + OF + Optional(OPTIONAL) + Optional(UNIQUE) + parameter_type))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="general_array_type"))
set_type << (((SET + Optional(bound_spec) + OF + instantiable_type))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="set_type"))
supertype_factor << (((supertype_term + ZeroOrMore((AND + supertype_term))))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="supertype_factor"))
index_2 << (index)
qualifiable_factor << (((function_call | attribute_ref | constant_factor | general_ref | population))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="qualifiable_factor"))
algorithm_head << (((ZeroOrMore(declaration) + Optional(constant_decl) + Optional(local_decl)))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="algorithm_head"))
parameter_type << (((generalized_types | simple_types | named_types))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="parameter_type"))
one_of << (((ONEOF + CaselessLiteral("(") + supertype_expression + ZeroOrMore((CaselessLiteral(",") + supertype_expression)) + CaselessLiteral(")")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="one_of"))
compound_stmt << (((BEGIN + stmt + ZeroOrMore(stmt) + END + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="compound_stmt"))
primary << (((literal | (qualifiable_factor + ZeroOrMore(qualifier))))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="primary"))
schema_decl << (((SCHEMA + schema_id + Optional(schema_version_id) + CaselessLiteral(";") + schema_body + END_SCHEMA + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="schema_decl"))
embedded_remark << (((CaselessLiteral("(*") + Optional(remark_tag) + ZeroOrMore(((not_paren_star + ZeroOrMore(not_paren_star)) | lparen_then_not_lparen_star | (CaselessLiteral("*") + ZeroOrMore(CaselessLiteral("*"))) | not_rparen_star_then_rparen | embedded_remark)) + CaselessLiteral("*)")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="embedded_remark"))
width_spec << (((CaselessLiteral("(") + width + CaselessLiteral(")") + Optional(FIXED)))).setParseAction(WidthSpec)
assignment_stmt << (((general_ref + ZeroOrMore(qualifier) + CaselessLiteral(":=") + expression + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="assignment_stmt"))
element << (((expression + Optional((CaselessLiteral(":") + repetition))))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="element"))
schema_body << (((ZeroOrMore(interface_specification) + Optional(constant_decl) + ZeroOrMore((declaration | rule_decl))))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="schema_body"))
procedure_head << (((PROCEDURE + procedure_id + Optional((CaselessLiteral("(") + Optional(VAR) + formal_parameter + ZeroOrMore((CaselessLiteral(";") + Optional(VAR) + formal_parameter)) + CaselessLiteral(")"))) + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="procedure_head"))
general_bag_type << (((BAG + Optional(bound_spec) + OF + parameter_type))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="general_bag_type"))
entity_head << (((ENTITY + entity_id + subsuper + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="entity_head"))
entity_body << (((ZeroOrMore(explicit_attr) + Optional(derive_clause) + Optional(inverse_clause) + Optional(unique_clause) + Optional(where_clause)))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="entity_body"))
list_type << (((LIST + Optional(bound_spec) + OF + Optional(UNIQUE) + instantiable_type))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="list_type"))
expression << (((simple_expression + Optional((rel_op_extended + simple_expression))))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="expression"))
one_of << (((ONEOF + CaselessLiteral("(") + supertype_expression + ZeroOrMore((CaselessLiteral(",") + supertype_expression)) + CaselessLiteral(")")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="one_of"))
parameter << (expression)
simple_types << (((binary_type | boolean_type | integer_type | logical_type | number_type | real_type | string_type))).setParseAction(SimpleType)
bag_type << (((BAG + Optional(bound_spec) + OF + instantiable_type))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="bag_type"))
parameter_type << (((generalized_types | simple_types | named_types))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="parameter_type"))
precision_spec << (numeric_expression)
primary << (((literal | (qualifiable_factor + ZeroOrMore(qualifier))))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="primary"))
procedure_call_stmt << ((((built_in_procedure | procedure_ref) + actual_parameter_list + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="procedure_call_stmt"))
procedure_decl << (((procedure_head + algorithm_head + ZeroOrMore(stmt) + END_PROCEDURE + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="procedure_decl"))
procedure_head << (((PROCEDURE + procedure_id + Optional((CaselessLiteral("(") + Optional(VAR) + formal_parameter + ZeroOrMore((CaselessLiteral(";") + Optional(VAR) + formal_parameter)) + CaselessLiteral(")"))) + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="procedure_head"))
qualifiable_factor << (((function_call | attribute_ref | constant_factor | general_ref | population))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="qualifiable_factor"))
qualifier << (((attribute_qualifier | group_qualifier | index_qualifier))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="qualifier"))
query_expression << (((QUERY + CaselessLiteral("(") + variable_id + CaselessLiteral("<*") + aggregate_source + CaselessLiteral("|") + logical_expression + CaselessLiteral(")")))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="query_expression"))
real_type << (((REAL + Optional((CaselessLiteral("(") + precision_spec + CaselessLiteral(")")))))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="real_type"))
remark << (((embedded_remark | tail_remark))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="remark"))
repeat_control << (((Optional(increment_control) + Optional(while_control) + Optional(until_control)))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="repeat_control"))
repeat_stmt << (((REPEAT + repeat_control + CaselessLiteral(";") + stmt + ZeroOrMore(stmt) + END_REPEAT + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="repeat_stmt"))
repetition << (numeric_expression)
constant_body << (((constant_id + CaselessLiteral(":") + instantiable_type + CaselessLiteral(":=") + expression + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="constant_body"))
if_stmt << (((IF + logical_expression + THEN + stmt + ZeroOrMore(stmt) + Optional((ELSE + stmt + ZeroOrMore(stmt))) + END_IF + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="if_stmt"))
inverse_attr << (((attribute_decl + CaselessLiteral(":") + Optional(((SET | BAG) + Optional(bound_spec) + OF)) + entity_ref + FOR + Optional((entity_ref + CaselessLiteral("."))) + attribute_ref + CaselessLiteral(";")))).setParseAction(InverseAttribute)
interval << (((CaselessLiteral("{") + interval_low + interval_op + interval_item + interval_op + interval_high + CaselessLiteral("}")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="interval"))
query_expression << (((QUERY + CaselessLiteral("(") + variable_id + CaselessLiteral("<*") + aggregate_source + CaselessLiteral("|") + logical_expression + CaselessLiteral(")")))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="query_expression"))
array_type << (((ARRAY + bound_spec + OF + Optional(OPTIONAL) + Optional(UNIQUE) + instantiable_type))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="array_type"))
simple_factor << (((aggregate_initializer | interval | query_expression | (Optional(unary_op) + ((CaselessLiteral("(") + expression + CaselessLiteral(")")) | primary)) | entity_constructor | enumeration_reference))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="simple_factor"))
case_label << (expression)
domain_rule << (((Optional((rule_label_id + CaselessLiteral(":"))) + expression))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="domain_rule"))
repeat_stmt << (((REPEAT + repeat_control + CaselessLiteral(";") + stmt + ZeroOrMore(stmt) + END_REPEAT + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="repeat_stmt"))
supertype_expression << (((supertype_factor + ZeroOrMore((ANDOR + supertype_factor))))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="supertype_expression"))
supertype_constraint << (((abstract_supertype_declaration | abstract_entity_declaration | supertype_rule))).setParseAction(SuperTypeExpression)
interval_high << (simple_expression)
local_decl << (((LOCAL + local_variable + ZeroOrMore(local_variable) + END_LOCAL + CaselessLiteral(";")))).setParseAction(lambda s, loc, t: ListNode(s, loc, t, rule="local_decl"))
return_stmt << (((RETURN + Optional((CaselessLiteral("(") + expression + CaselessLiteral(")"))) + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="return_stmt"))
rule_decl << (((rule_head + algorithm_head + ZeroOrMore(stmt) + where_clause + END_RULE + CaselessLiteral(";")))).set_parse_action(RuleDeclaration)
schema_body << (((ZeroOrMore(interface_specification) + Optional(constant_decl) + ZeroOrMore((declaration | rule_decl))))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="schema_body"))
schema_decl << (((SCHEMA + schema_id + Optional(schema_version_id) + CaselessLiteral(";") + schema_body + END_SCHEMA + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="schema_decl"))
selector << (expression)
aggregate_source << (simple_expression)
qualifier << (((attribute_qualifier | group_qualifier | index_qualifier))).setParseAction(lambda s, loc, t: Node(s, loc, t, rule="qualifier"))
set_type << (((SET + Optional(bound_spec) + OF + instantiable_type))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="set_type"))
simple_expression << (((term + ZeroOrMore((add_like_op + term))))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="simple_expression"))
simple_factor << (((aggregate_initializer | interval | query_expression | (Optional(unary_op) + ((CaselessLiteral("(") + expression + CaselessLiteral(")")) | primary)) | entity_constructor | enumeration_reference))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="simple_factor"))
simple_types << (((binary_type | boolean_type | integer_type | logical_type | number_type | real_type | string_type))).set_parse_action(SimpleType)
stmt << (((alias_stmt | assignment_stmt | case_stmt | compound_stmt | escape_stmt | if_stmt | null_stmt | procedure_call_stmt | repeat_stmt | return_stmt | skip_stmt))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="stmt"))
string_type << (((STRING + Optional(width_spec)))).set_parse_action(StringType)
subsuper << (((Optional(supertype_constraint) + Optional(subtype_declaration)))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="subsuper"))
subtype_constraint << (((OF + CaselessLiteral("(") + supertype_expression + CaselessLiteral(")")))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="subtype_constraint"))
subtype_constraint_body << (((Optional(abstract_supertype) + Optional(total_over) + Optional((supertype_expression + CaselessLiteral(";")))))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="subtype_constraint_body"))
subtype_constraint_decl << (((subtype_constraint_head + subtype_constraint_body + END_SUBTYPE_CONSTRAINT + CaselessLiteral(";")))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="subtype_constraint_decl"))
supertype_constraint << (((abstract_supertype_declaration | abstract_entity_declaration | supertype_rule))).set_parse_action(SuperTypeExpression)
supertype_expression << (((supertype_factor + ZeroOrMore((ANDOR + supertype_factor))))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="supertype_expression"))
supertype_factor << (((supertype_term + ZeroOrMore((AND + supertype_term))))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="supertype_factor"))
supertype_rule << (((SUPERTYPE + subtype_constraint))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="supertype_rule"))
supertype_term << (((one_of | (CaselessLiteral("(") + supertype_expression + CaselessLiteral(")")) | entity_ref))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="supertype_term"))
syntax << (((schema_decl + ZeroOrMore(schema_decl)))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="syntax"))
term << (((factor + ZeroOrMore((multiplication_like_op + factor))))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="term"))
type_decl << (((TYPE + type_id + CaselessLiteral("=") + underlying_type + CaselessLiteral(";") + Optional(where_clause) + END_TYPE + CaselessLiteral(";")))).set_parse_action(TypeDeclaration)
underlying_type << (((constructed_types | concrete_types))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="underlying_type"))
until_control << (((UNTIL + logical_expression))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="until_control"))
where_clause << (((WHERE + domain_rule + CaselessLiteral(";") + ZeroOrMore((domain_rule + CaselessLiteral(";")))))).set_parse_action(lambda s, loc, t: ListNode(s, loc, t, rule="where_clause"))
while_control << (((WHILE + logical_expression))).set_parse_action(lambda s, loc, t: Node(s, loc, t, rule="while_control"))
width << (numeric_expression)
width_spec << (((CaselessLiteral("(") + width + CaselessLiteral(")") + Optional(FIXED)))).set_parse_action(WidthSpec)
syntax.ignore("--" + restOfLine)
syntax.ignore(Regex(r"\((?:\*(?:[^*]*\*+)+?\))"))
ast = syntax.parseFile(fn)
ast = syntax.parse_file(fn)
s = schema.Schema(ast)
m = mapping.Mapping(s)
with open(cache_file, "wb") as f:
pickle.dump(m, f, protocol=0)
return m
if __name__ == "__main__":
m = parse(sys.argv[1])
import importlib

Some files were not shown because too many files have changed in this diff Show More