mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 22:50:21 +00:00
Bonsai, drawing sheets show git tag revision history in titleblocks
Titleblock SVGs are given a revisions table populated from git tags on the IFC file's repository. Tags are sorted oldest-first so they stack upward from a fixed anchor point. Each row shows the tag name, the tagged commit date, the first line of the tag message, and the author's initials. A1/A2/A3 titleblock templates are updated accordingly. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -323,5 +323,14 @@
|
|||||||
x="160.29811"
|
x="160.29811"
|
||||||
y="250.54721"
|
y="250.54721"
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
|
||||||
|
<g transform="translate(0,285.54)">
|
||||||
|
{{#revisions}}
|
||||||
|
<text x="11" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{rev}}</text>
|
||||||
|
<text x="31" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{description}}</text>
|
||||||
|
<text x="111" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{author}}</text>
|
||||||
|
<text x="131" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{issued}}</text>
|
||||||
|
<text x="151" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{date}}</text>
|
||||||
|
{{/revisions}}
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
@@ -318,5 +318,14 @@
|
|||||||
x="160.29811"
|
x="160.29811"
|
||||||
y="250.54721"
|
y="250.54721"
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
|
||||||
|
<g transform="translate(0,285.54)">
|
||||||
|
{{#revisions}}
|
||||||
|
<text x="11" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{rev}}</text>
|
||||||
|
<text x="31" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{description}}</text>
|
||||||
|
<text x="111" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{author}}</text>
|
||||||
|
<text x="131" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{issued}}</text>
|
||||||
|
<text x="151" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{date}}</text>
|
||||||
|
{{/revisions}}
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@@ -195,6 +195,15 @@
|
|||||||
x="160.29811"
|
x="160.29811"
|
||||||
y="250.54721"
|
y="250.54721"
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.24897px;line-height:100%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.25;stroke-miterlimit:4;stroke-dasharray:none">DATE</tspan></text>
|
||||||
|
<g transform="translate(0,285.54)">
|
||||||
|
{{#revisions}}
|
||||||
|
<text x="11" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{rev}}</text>
|
||||||
|
<text x="31" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{description}}</text>
|
||||||
|
<text x="111" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{author}}</text>
|
||||||
|
<text x="131" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{issued}}</text>
|
||||||
|
<text x="151" y="{{y}}" style="font-size:3.175px;font-family:sans-serif;fill:#000000">{{date}}</text>
|
||||||
|
{{/revisions}}
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<g
|
<g
|
||||||
id="g38171"
|
id="g38171"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
@@ -23,6 +23,7 @@ import shutil
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import uuid
|
import uuid
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
@@ -32,6 +33,8 @@ from mathutils import Vector
|
|||||||
|
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
|
|
||||||
|
os.environ.setdefault("GIT_PYTHON_REFRESH", "quiet")
|
||||||
|
|
||||||
VIEW_TITLE_OFFSET_Y = 5
|
VIEW_TITLE_OFFSET_Y = 5
|
||||||
DRAWING_PADDING = 10
|
DRAWING_PADDING = 10
|
||||||
DEFAULT_POSITION = Vector((30, 30))
|
DEFAULT_POSITION = Vector((30, 30))
|
||||||
@@ -316,10 +319,65 @@ class SheetBuilder:
|
|||||||
|
|
||||||
return {"SHEET": sheet_path}
|
return {"SHEET": sheet_path}
|
||||||
|
|
||||||
|
def get_titleblock_data(self, sheet: ifcopenshell.entity_instance) -> dict:
|
||||||
|
data = sheet.get_info()
|
||||||
|
revisions = self._get_git_revisions()
|
||||||
|
data["revisions"] = revisions
|
||||||
|
data["has_revisions"] = bool(revisions)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _get_git_revisions(self) -> list[dict]:
|
||||||
|
try:
|
||||||
|
import git
|
||||||
|
except ImportError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
ifc_path = tool.Ifc.get_path()
|
||||||
|
if not ifc_path:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
repo = git.Repo(ifc_path, search_parent_directories=True)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Oldest-first so the SVG template can anchor at the bottom: oldest
|
||||||
|
# tag sits at y=0 (the anchor point) and newer tags stack upward.
|
||||||
|
# Always sort and date by the tagged commit, not when the tag was applied.
|
||||||
|
tags = sorted(repo.tags, key=lambda t: t.commit.committed_date)
|
||||||
|
|
||||||
|
def initials(actor) -> str:
|
||||||
|
if not actor or not actor.name:
|
||||||
|
return ""
|
||||||
|
return "".join(w[0].upper() for w in actor.name.split() if w)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for i, tag_ref in enumerate(tags):
|
||||||
|
date = datetime.fromtimestamp(tag_ref.commit.committed_date).date().isoformat()
|
||||||
|
if tag_ref.tag:
|
||||||
|
description = (tag_ref.tag.message or "").strip().splitlines()[0]
|
||||||
|
author = initials(tag_ref.tag.tagger)
|
||||||
|
else:
|
||||||
|
description = ""
|
||||||
|
author = initials(tag_ref.commit.author)
|
||||||
|
# y is a negative offset from the group anchor (5 mm row height to
|
||||||
|
# match the A3 titleblock grid). Oldest tag sits at y=0 (the anchor);
|
||||||
|
# newer tags stack upward so the oldest stays at a fixed position.
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"rev": tag_ref.name,
|
||||||
|
"date": date,
|
||||||
|
"description": description,
|
||||||
|
"author": author,
|
||||||
|
"issued": "",
|
||||||
|
"y": -i * 5,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
|
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
|
||||||
titleblock = root.findall(f'{SVG}g[@data-type="titleblock"]')[0]
|
titleblock = root.findall(f'{SVG}g[@data-type="titleblock"]')[0]
|
||||||
image = titleblock.findall(f"{SVG}image")[0]
|
image = titleblock.findall(f"{SVG}image")[0]
|
||||||
g = self.parse_embedded_svg(image, sheet.get_info())
|
g = self.parse_embedded_svg(image, self.get_titleblock_data(sheet))
|
||||||
grid_north = ifcopenshell.util.geolocation.get_grid_north(tool.Ifc.get()) * -1
|
grid_north = ifcopenshell.util.geolocation.get_grid_north(tool.Ifc.get()) * -1
|
||||||
true_north = ifcopenshell.util.geolocation.get_true_north(tool.Ifc.get()) * -1
|
true_north = ifcopenshell.util.geolocation.get_true_north(tool.Ifc.get()) * -1
|
||||||
for north in g.iterfind(f'.//{SVG}g[@data-type="grid-north"]'):
|
for north in g.iterfind(f'.//{SVG}g[@data-type="grid-north"]'):
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
# Bonsai - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
import ifcopenshell
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
try:
|
||||||
|
import git
|
||||||
|
import git.exc
|
||||||
|
|
||||||
|
HAS_GIT = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_GIT = False
|
||||||
|
|
||||||
|
requires_git = pytest.mark.skipif(not HAS_GIT, reason="GitPython not available")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _make_repo(tmpdir: str) -> "git.Repo":
|
||||||
|
repo = git.Repo.init(tmpdir)
|
||||||
|
with repo.config_writer() as cfg:
|
||||||
|
cfg.set_value("user", "name", "Test User")
|
||||||
|
cfg.set_value("user", "email", "test@example.com")
|
||||||
|
return repo
|
||||||
|
|
||||||
|
|
||||||
|
def _commit(repo: "git.Repo", tmpdir: str, content: str = "data") -> "git.Commit":
|
||||||
|
path = os.path.join(tmpdir, "model.ifc")
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
repo.index.add([os.path.normpath(path)])
|
||||||
|
return repo.index.commit(content)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def builder():
|
||||||
|
"""Return a SheetBuilder with bonsai.bim loaded lazily."""
|
||||||
|
from bonsai.bim.module.drawing import sheeter
|
||||||
|
|
||||||
|
return sheeter.SheetBuilder()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def builder_at(monkeypatch):
|
||||||
|
"""Factory: builder_at(ifc_path) patches tool.Ifc.get_path and returns a SheetBuilder."""
|
||||||
|
import bonsai.tool as tool
|
||||||
|
from bonsai.bim.module.drawing import sheeter
|
||||||
|
|
||||||
|
def factory(ifc_path):
|
||||||
|
monkeypatch.setattr(tool.Ifc, "get_path", classmethod(lambda cls: ifc_path))
|
||||||
|
return sheeter.SheetBuilder()
|
||||||
|
|
||||||
|
return factory
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Unit conversions (pure, no add-on registration required)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertToMm:
|
||||||
|
def test_mm(self, builder):
|
||||||
|
assert builder.convert_to_mm("297mm") == pytest.approx(297.0)
|
||||||
|
|
||||||
|
def test_cm(self, builder):
|
||||||
|
assert builder.convert_to_mm("29.7cm") == pytest.approx(297.0)
|
||||||
|
|
||||||
|
def test_in(self, builder):
|
||||||
|
assert builder.convert_to_mm("1in") == pytest.approx(25.4)
|
||||||
|
|
||||||
|
def test_px(self, builder):
|
||||||
|
# 96 dpi: 96 px = 25.4 mm
|
||||||
|
assert builder.convert_to_mm("96px") == pytest.approx(25.4, rel=1e-3)
|
||||||
|
|
||||||
|
def test_pt(self, builder):
|
||||||
|
# 72 pt = 25.4 mm
|
||||||
|
assert builder.convert_to_mm("72pt") == pytest.approx(25.4, rel=1e-3)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMmToPx:
|
||||||
|
def test_round_trip(self, builder):
|
||||||
|
assert builder.mm_to_px(builder.convert_to_mm("96px")) == pytest.approx(96.0, rel=1e-3)
|
||||||
|
|
||||||
|
def test_zero(self, builder):
|
||||||
|
assert builder.mm_to_px(0.0) == pytest.approx(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# parse_embedded_svg — Pystache template rendering
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseEmbeddedSvg:
|
||||||
|
def _image_element(self, filename="titleblock.svg"):
|
||||||
|
image = ET.Element("image")
|
||||||
|
image.attrib["x"] = "0mm"
|
||||||
|
image.attrib["y"] = "0mm"
|
||||||
|
image.attrib["width"] = "420mm"
|
||||||
|
image.attrib["height"] = "297mm"
|
||||||
|
image.attrib["{http://www.w3.org/1999/xlink}href"] = filename
|
||||||
|
return image
|
||||||
|
|
||||||
|
def _make_builder(self, tmp_path):
|
||||||
|
from bonsai.bim.module.drawing import sheeter
|
||||||
|
|
||||||
|
b = sheeter.SheetBuilder()
|
||||||
|
b.layout_dir = str(tmp_path)
|
||||||
|
b.sheets_dir = str(tmp_path)
|
||||||
|
b.defs = ET.Element("defs")
|
||||||
|
return b
|
||||||
|
|
||||||
|
def test_variable_substitution(self, tmp_path):
|
||||||
|
svg = '<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg"><text>{{Name}}</text></svg>'
|
||||||
|
(tmp_path / "titleblock.svg").write_text(svg)
|
||||||
|
|
||||||
|
group = self._make_builder(tmp_path).parse_embedded_svg(self._image_element(), {"Name": "My Sheet"})
|
||||||
|
texts = group.findall(".//{http://www.w3.org/2000/svg}text")
|
||||||
|
assert any(t.text == "My Sheet" for t in texts)
|
||||||
|
|
||||||
|
def test_revision_rows_rendered(self, tmp_path):
|
||||||
|
svg = (
|
||||||
|
'<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg"><g>'
|
||||||
|
'{{#revisions}}<text x="11" y="{{y}}">{{rev}}</text>{{/revisions}}'
|
||||||
|
"</g></svg>"
|
||||||
|
)
|
||||||
|
(tmp_path / "titleblock.svg").write_text(svg)
|
||||||
|
|
||||||
|
revisions = [
|
||||||
|
{"rev": "v1.0", "date": "2024-01-01", "description": "First", "author": "TU", "issued": "", "y": 0},
|
||||||
|
{"rev": "v2.0", "date": "2025-01-01", "description": "Second", "author": "TU", "issued": "", "y": -5},
|
||||||
|
]
|
||||||
|
group = self._make_builder(tmp_path).parse_embedded_svg(self._image_element(), {"revisions": revisions})
|
||||||
|
labels = [t.text for t in group.findall(".//{http://www.w3.org/2000/svg}text")]
|
||||||
|
assert "v1.0" in labels
|
||||||
|
assert "v2.0" in labels
|
||||||
|
|
||||||
|
def test_has_revisions_section_hidden_when_false(self, tmp_path):
|
||||||
|
svg = (
|
||||||
|
'<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg">'
|
||||||
|
"{{#has_revisions}}<text>HEADERS</text>{{/has_revisions}}"
|
||||||
|
"</svg>"
|
||||||
|
)
|
||||||
|
(tmp_path / "titleblock.svg").write_text(svg)
|
||||||
|
|
||||||
|
group = self._make_builder(tmp_path).parse_embedded_svg(
|
||||||
|
self._image_element(), {"has_revisions": False, "revisions": []}
|
||||||
|
)
|
||||||
|
texts = group.findall(".//{http://www.w3.org/2000/svg}text")
|
||||||
|
assert not any(t.text == "HEADERS" for t in texts)
|
||||||
|
|
||||||
|
def test_has_revisions_section_shown_when_true(self, tmp_path):
|
||||||
|
svg = (
|
||||||
|
'<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg">'
|
||||||
|
"{{#has_revisions}}<text>HEADERS</text>{{/has_revisions}}"
|
||||||
|
"</svg>"
|
||||||
|
)
|
||||||
|
(tmp_path / "titleblock.svg").write_text(svg)
|
||||||
|
|
||||||
|
group = self._make_builder(tmp_path).parse_embedded_svg(
|
||||||
|
self._image_element(), {"has_revisions": True, "revisions": []}
|
||||||
|
)
|
||||||
|
texts = group.findall(".//{http://www.w3.org/2000/svg}text")
|
||||||
|
assert any(t.text == "HEADERS" for t in texts)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# _get_git_revisions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetGitRevisions:
|
||||||
|
def test_no_ifc_path_returns_empty(self, builder_at):
|
||||||
|
assert builder_at(None)._get_git_revisions() == []
|
||||||
|
|
||||||
|
@requires_git
|
||||||
|
def test_not_a_git_repo_returns_empty(self, builder_at, tmp_path):
|
||||||
|
assert builder_at(str(tmp_path / "model.ifc"))._get_git_revisions() == []
|
||||||
|
|
||||||
|
@requires_git
|
||||||
|
def test_no_tags_returns_empty(self, builder_at, tmp_path):
|
||||||
|
repo = _make_repo(str(tmp_path))
|
||||||
|
_commit(repo, str(tmp_path))
|
||||||
|
assert builder_at(str(tmp_path / "model.ifc"))._get_git_revisions() == []
|
||||||
|
|
||||||
|
@requires_git
|
||||||
|
def test_lightweight_tag_fields(self, builder_at, tmp_path):
|
||||||
|
repo = _make_repo(str(tmp_path))
|
||||||
|
_commit(repo, str(tmp_path))
|
||||||
|
repo.create_tag("v1.0")
|
||||||
|
|
||||||
|
rows = builder_at(str(tmp_path / "model.ifc"))._get_git_revisions()
|
||||||
|
|
||||||
|
assert len(rows) == 1
|
||||||
|
assert rows[0]["rev"] == "v1.0"
|
||||||
|
assert rows[0]["description"] == ""
|
||||||
|
assert rows[0]["author"] == "TU" # initials of "Test User"
|
||||||
|
assert rows[0]["issued"] == ""
|
||||||
|
assert rows[0]["y"] == 0
|
||||||
|
|
||||||
|
@requires_git
|
||||||
|
def test_annotated_tag_uses_message_first_line(self, builder_at, tmp_path):
|
||||||
|
repo = _make_repo(str(tmp_path))
|
||||||
|
_commit(repo, str(tmp_path))
|
||||||
|
repo.create_tag("v1.0", message="First release\nExtra detail ignored")
|
||||||
|
|
||||||
|
rows = builder_at(str(tmp_path / "model.ifc"))._get_git_revisions()
|
||||||
|
|
||||||
|
assert rows[0]["description"] == "First release"
|
||||||
|
|
||||||
|
@requires_git
|
||||||
|
def test_annotated_tag_author_is_tagger_initials(self, builder_at, tmp_path):
|
||||||
|
repo = _make_repo(str(tmp_path))
|
||||||
|
_commit(repo, str(tmp_path))
|
||||||
|
repo.create_tag("v1.0", message="Release")
|
||||||
|
|
||||||
|
rows = builder_at(str(tmp_path / "model.ifc"))._get_git_revisions()
|
||||||
|
|
||||||
|
assert rows[0]["author"] == "TU"
|
||||||
|
|
||||||
|
@requires_git
|
||||||
|
def test_date_is_iso_format(self, builder_at, tmp_path):
|
||||||
|
repo = _make_repo(str(tmp_path))
|
||||||
|
_commit(repo, str(tmp_path))
|
||||||
|
repo.create_tag("v1.0", message="Release")
|
||||||
|
|
||||||
|
rows = builder_at(str(tmp_path / "model.ifc"))._get_git_revisions()
|
||||||
|
|
||||||
|
assert re.match(r"\d{4}-\d{2}-\d{2}$", rows[0]["date"])
|
||||||
|
|
||||||
|
@requires_git
|
||||||
|
def test_multiple_tags_sorted_oldest_first(self, builder_at, tmp_path):
|
||||||
|
repo = _make_repo(str(tmp_path))
|
||||||
|
_commit(repo, str(tmp_path), "first")
|
||||||
|
repo.create_tag("v1.0", message="First")
|
||||||
|
time.sleep(1.1) # ensure distinct second-level timestamps
|
||||||
|
_commit(repo, str(tmp_path), "second")
|
||||||
|
repo.create_tag("v2.0", message="Second")
|
||||||
|
|
||||||
|
rows = builder_at(str(tmp_path / "model.ifc"))._get_git_revisions()
|
||||||
|
|
||||||
|
assert len(rows) == 2
|
||||||
|
assert rows[0]["rev"] == "v1.0" # oldest at index 0 (bottom of table)
|
||||||
|
assert rows[1]["rev"] == "v2.0" # newest at index 1 (stacks upward)
|
||||||
|
|
||||||
|
@requires_git
|
||||||
|
def test_y_offsets_are_negative_multiples_of_5(self, builder_at, tmp_path):
|
||||||
|
repo = _make_repo(str(tmp_path))
|
||||||
|
for i in range(3):
|
||||||
|
_commit(repo, str(tmp_path), f"rev{i}")
|
||||||
|
repo.create_tag(f"v{i}.0", message=f"Release {i}")
|
||||||
|
time.sleep(1.1)
|
||||||
|
|
||||||
|
rows = builder_at(str(tmp_path / "model.ifc"))._get_git_revisions()
|
||||||
|
|
||||||
|
assert rows[0]["y"] == 0
|
||||||
|
assert rows[1]["y"] == -5
|
||||||
|
assert rows[2]["y"] == -10
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# get_titleblock_data
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetTitleblockData:
|
||||||
|
@requires_git
|
||||||
|
def test_has_revisions_false_when_no_tags(self, builder_at, tmp_path):
|
||||||
|
repo = _make_repo(str(tmp_path))
|
||||||
|
_commit(repo, str(tmp_path))
|
||||||
|
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
sheet = ifc.createIfcDocumentInformation(Identification="DR-01", Name="Site Plan")
|
||||||
|
data = builder_at(str(tmp_path / "model.ifc")).get_titleblock_data(sheet)
|
||||||
|
|
||||||
|
assert data["has_revisions"] is False
|
||||||
|
assert data["revisions"] == []
|
||||||
|
|
||||||
|
@requires_git
|
||||||
|
def test_has_revisions_true_when_tags_present(self, builder_at, tmp_path):
|
||||||
|
repo = _make_repo(str(tmp_path))
|
||||||
|
_commit(repo, str(tmp_path))
|
||||||
|
repo.create_tag("v1.0", message="Issued for review")
|
||||||
|
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
sheet = ifc.createIfcDocumentInformation(Identification="DR-01", Name="Site Plan")
|
||||||
|
data = builder_at(str(tmp_path / "model.ifc")).get_titleblock_data(sheet)
|
||||||
|
|
||||||
|
assert data["has_revisions"] is True
|
||||||
|
assert len(data["revisions"]) == 1
|
||||||
|
|
||||||
|
@requires_git
|
||||||
|
def test_sheet_ifc_attributes_are_included(self, builder_at, tmp_path):
|
||||||
|
repo = _make_repo(str(tmp_path))
|
||||||
|
_commit(repo, str(tmp_path))
|
||||||
|
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
sheet = ifc.createIfcDocumentInformation(Identification="DR-01", Name="Site Plan")
|
||||||
|
data = builder_at(str(tmp_path / "model.ifc")).get_titleblock_data(sheet)
|
||||||
|
|
||||||
|
assert data["Identification"] == "DR-01"
|
||||||
|
assert data["Name"] == "Site Plan"
|
||||||
Reference in New Issue
Block a user