Files
T
Umut CANandClaude Opus 5.5 d5c5e90406 feat(arch): the plan model and its foundation - layers, materials, EN/TR, two generalisations
Track F starts here; the three group branches are cut from this commit.

- engineering/arch/model.py: Wall, Opening, Stair and Room as frozen
  dataclasses, and every refusal of spec 4 before anything is drawn, by path
  (walls[2].thickness): finite positive numbers, an axis of at least two
  distinct points with no zero-length segment, an arc segment refused by name
  rather than approximated, an opening on an unknown wall, past its wall,
  across an axis vertex or overlapping another (both ids named), an unknown
  material, and a typed room area - areas are measured, never typed. The
  plan travels as ACADMCP_ARCH XDATA; an opening's door/window kind and a
  stair's straight/l/u kind ride beside the payload kind as opening_kind /
  stair_kind, because one key cannot hold both.
- engineering/arch/layers.py: the arch set - thirteen ISO 13567-style layers
  with ISO 128 lineweights only (0.05 mm on the construction scratch layer,
  as every set in this repository) - and ARCH_ROLE_LAYER, the one place a
  role becomes a layer. Registered in engineering/layers.py last, which is
  what keeps the engineering.layers <-> engineering.arch import cycle closed.
- engineering/arch/materials.py: eight wall materials, one pattern each,
  every row citing its source: ANSI31 for brick (ISO 128-50 general hatching,
  acadiso.pat's own "Iron, Brick, Stone masonry"), AAC as ANSI31 at twice the
  spacing (a repository convention, and it says so), AR-CONC / AR-SAND as
  AutoCAD patterns, and the JIS A 0150 RC / stone / wood symbols. Reinforced
  concrete cannot be ANSI31 and AR-CONC in one HATCH, so it takes the one
  symbol that names it. Periods on a 1:50 sheet are recomputed from ezdxf's
  own pattern definitions by the tests. normalise_material is the one reading
  of a material name, shared by the model and wall_hatch, so the model never
  refuses "Reinforced Concrete" that the hatch table resolves.
- engineering/arch/lang.py: EN / TR vocabulary, D/W and K/P tag prefixes, and
  the decimal comma for lang="tr" (24,50 m2).
- draw_prims(role_layer=) and the XDATA codec's app_id= / kinds=: both
  generalised with the mechanical defaults, and the mechanical suite passes
  unchanged against them - that is the regression gate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 22:27:00 +03:00

121 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""English / Turkish vocabulary for labels and schedules, and the number format.
``lang="en"`` is the default; ``lang="tr"`` switches the words *and* the
decimal separator to a comma (``24,50 m²``), the user's decision of
2026-09-23. Any other value is refused with the list, never silently
rendered in English.
The Turkish words are the ones a Turkish drawing office writes on a plan
(``MAHAL LİSTESİ`` for a room schedule, ``KOT`` for a level, ``ÇIKIŞ`` for
the up-arrow of a stair); they are vocabulary, not a standard.
"""
from __future__ import annotations
import math
LANGS: tuple[str, ...] = ("en", "tr")
_VOCAB: dict[str, dict[str, str]] = {
"en": {
"door_schedule": "DOOR SCHEDULE",
"window_schedule": "WINDOW SCHEDULE",
"room_schedule": "ROOM SCHEDULE",
"tag": "TAG",
"width": "WIDTH",
"height": "HEIGHT",
"sill": "SILL",
"swing": "SWING",
"hand": "HAND",
"wall": "WALL",
"number": "NO.",
"name": "NAME",
"area": "AREA",
"total": "TOTAL",
"up": "UP",
"down": "DN",
"north": "N",
"level": "LEVEL",
"in": "IN",
"out": "OUT",
"left": "LEFT",
"right": "RIGHT",
},
"tr": {
"door_schedule": "KAPI LİSTESİ",
"window_schedule": "PENCERE LİSTESİ",
"room_schedule": "MAHAL LİSTESİ",
"tag": "KOD",
"width": "GENİŞLİK",
"height": "YÜKSEKLİK",
"sill": "PARAPET",
"swing": "AÇILIŞ",
"hand": "YÖN",
"wall": "DUVAR",
"number": "NO",
"name": "MAHAL ADI",
"area": "ALAN",
"total": "TOPLAM",
"up": "ÇIKIŞ",
"down": "İNİŞ",
"north": "K",
"level": "KOT",
"in": "İÇE",
"out": "DIŞA",
"left": "SOL",
"right": "SAĞ",
},
}
_TAG_PREFIX: dict[str, dict[str, str]] = {
"en": {"door": "D", "window": "W"},
"tr": {"door": "K", "window": "P"},
}
def _lang(lang: str) -> str:
if not isinstance(lang, str) or lang.strip().lower() not in LANGS:
raise ValueError(f"lang: unknown language {lang!r}; languages are {', '.join(LANGS)}")
return lang.strip().lower()
def vocab(lang: str) -> dict[str, str]:
"""Every word a label or schedule writes, in ``lang``. A copy: callers may not edit it."""
return dict(_VOCAB[_lang(lang)])
def tag_prefix(kind: str, lang: str) -> str:
"""``door`` -> ``D`` / ``K`` (kapı), ``window`` -> ``W`` / ``P`` (pencere)."""
table = _TAG_PREFIX[_lang(lang)]
if kind not in table:
raise ValueError(f"kind: unknown opening kind {kind!r}; kinds are door, window")
return table[kind]
def fmt_number(value: float, lang: str, decimals: int = 2) -> str:
"""``24.5`` -> ``"24.50"`` (en) / ``"24,50"`` (tr). No thousands separator."""
code = _lang(lang)
if isinstance(decimals, bool) or not isinstance(decimals, int) or decimals < 0:
raise ValueError(f"decimals: expected a whole number >= 0, got {decimals!r}")
try:
number = float(value)
except (TypeError, ValueError):
raise ValueError(f"value: expected a number, got {value!r}") from None
if not math.isfinite(number):
raise ValueError(f"value: must be finite, got {value!r}")
text = f"{number:.{decimals}f}"
if text.startswith("-") and float(text) == 0.0:
text = text[1:] # never print "-0.00"
return text.replace(".", ",") if code == "tr" else text
def fmt_area_m2(area_mm2: float, lang: str) -> str:
"""An area measured in mm² written in m² with two decimals: ``"24.50 m²"`` / ``"24,50 m²"``."""
try:
area = float(area_mm2)
except (TypeError, ValueError):
raise ValueError(f"area: expected a number, got {area_mm2!r}") from None
if not math.isfinite(area) or area < 0.0:
raise ValueError(f"area: must be finite and not negative, got {area_mm2!r}")
return f"{fmt_number(area / 1.0e6, lang, 2)} m²"