Metadata-Version: 2.4
Name: detailer
Version: 0.1.0
Summary: Annotation and sheet composition for parametric engineering drawings, on polyhedral and ezdxf
Author-email: Wuttiwong Banjongwattana <banjongwattana.w@gmail.com>
License-Expression: MIT
Keywords: cad,drawing,dimensioning,annotation,dxf,parametric,steel,concrete
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Manufacturing
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Classifier: Typing :: Typed
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: polyhedral>=0.5.0
Requires-Dist: numpy>=1.26
Requires-Dist: shapely>=2.0.7
Requires-Dist: ezdxf>=1.4.4
Requires-Dist: Pillow
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: cairosvg; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# detailer

Annotation and sheet composition for parametric engineering drawings,
on [polyhedral](https://pypi.org/project/polyhedral/) (geometry) and
[ezdxf](https://pypi.org/project/ezdxf/) (the file).

You say *what* to annotate, as anchors into the model and intents; the
drawing decides *where*, by drafting rules, sizes its own paper and
reports what it could not place cleanly. Change a parameter and every
dimension, leader and title moves with the model.

```
pip install detailer
```

## A figure from settled numbers

A calculation settles the numbers and the script draws the section it
just checked. Every anchor names the shape the view draws -- a face, the
closest pair, a bar picked out of the order it appears in -- so no
coordinate is written down.

```python
from polyhedral import Section, Sheet, View, Viewport, make
from detailer import Drawing, dim, face, label, nearest, pick
from detailer.concrete import Cover, Host, Layout, Links, Longitudinal, Member

b, h, cover, link, bar, pitch = 300, 600, 25, 9, 20, 200        # settled

conc = make.box((3000, b, h), center=(1500, 0, 0), kind="concrete", pid="B1")
host = Host(conc, axis="x", cover=Cover(cover, ends=40), link=link)
m = Member("B1", host, runs=(
    Longitudinal(host, "bottom", n=3, size=bar),
    Longitudinal(host, "top", n=2, size=bar),
    Links(host, size=link, layout=Layout.zones(f"4@100, rest@{pitch}, 4@100"),
          around=bar)))
bottom, top, links = (m.sets_of(r)[0] for r in m.runs)

cut = Section((1, 0, 0), 1500, depth=200)
vp = Viewport(View.from_direction((-1, 0, 0), up=(0, 0, 1), name="SEC A", cut=cut),
              at=(0, 0))
d = Drawing(Sheet([conc, *(s for st in m.sets for s in st.solids)], [vp]), [
    dim(face(conc, (0, -1, 0)), face(conc, (0, 1, 0))),              # width
    dim(face(conc, (0, 0, -1)), face(conc, (0, 0, 1)), along="y"),   # depth, up the sheet
    dim(nearest(face(conc, (0, 0, -1)), links)),                     # cover
    label(pick(bottom.solids), f"3-DB{bar}"),
    label(pick(top.solids), f"2-DB{bar}"),
], scale=10)
assert d.check() == []
d.save("B1-SEC.dxf")
figure = d.to_svg(view="SEC A")
```

The cover dimension measures what the drawing shows: `nearest` finds the
closest drawn point of the whole target, a lap hook standing proud of a
link included. `concrete.cover_dim(host, face, bar)` is what states the
nominal cover instead.

`check()` reports what the placer could not do, what the caller pinned
and what the paper cannot hold; `save`, `to_dxf_doc` and `to_svg` refuse
a drawing that has any of it, and `accept=` names the codes or intents
drawn on the sheet as a list instead. No paper was asked for, so
`d.paper` is the size the arranged drawing needs.

## The whole sheet

`concrete.drawing` is the default view over a model of your own: the
elevation, the sections, the covers, a leader per run, the schedule and
the sheet items, arranged.

```python
from detailer import title_block
from detailer.concrete import drawing

sheet = drawing(m, sections={"A": 0.5}, section_scale=10, iso=False)
sheet = sheet.replace_intent("title-block",
                             title_block({"PROJECT": "Block A", "DWG No.": "S-12"}))
assert sheet.check() == []
sheet.save("B1.dxf")
```

`Drawing.names` is one name per intent -- its own, or one derived from
its type and the parts it names -- and `without` and `replace_intent`
take those names.

## What you can say

Anchors resolve through the viewport at placement time, against the line
work that viewport draws for a part -- a solid, a `Flat` region or a
`Wire` path, so a bar diagram with load arrows is annotated like
anything else:

`face(part, direction)`, `nearest(a, b)`, `pick(parts)[i]` with `.row`
and `.col`, `segment(part, i)`, `hole(part, i)` or `hole(part, row=,
col=)`, `on(part).left`, `extent(*parts).top`, `edge(part, na, nb)`,
`mid(a, b)`, `offset(a, dx, dy)`, `at(p)`.

Intents say what to show: `dim`, `chain`, `baseline`, `label`, `mark`,
`note`, `notes`, `weld`, `centre`, `section_marker`, `lines`, `heading`,
`table`, `title_block`.

`dim(along=)` and `chain(along=)` name a **sheet** axis -- `"x"` across
the paper, `"y"` up it, `"aligned"` along the two ends -- never a model
axis, since a plan turns one.

Where the automatic choice is not wanted, `side="+"`/`"-"` pins the side
and `tier="outer"` puts a dimension past every automatic tier; `near=`
asks for a text's place and settles for the nearest clear one, and
`text_at=`/`at=` fix the text's centre outright. A pin takes an anchor,
never a coordinate, and is reported as `pinned`.

## One view at a time

```python
small = d.to_svg(view="SEC A", fit=(80, 60))   # re-placed at the scale that fits
```

`to_svg(view=)` and `to_dxf_doc(view=)` draw one viewport and its
annotation, nothing else. A viewport can be any polyhedral view, a
shaded isometric included. A part that runs past a viewport's crop is
ended with a break line; for a pictorial view, `detailer.model.broken`
cuts the member itself short.

## Rules

Every rule lives in `Style`, in paper millimetres, and is converted once
by the drawing's scale:

- dimensions sit on tiers outside the view, shortest first so the
  overall dimension lies outermost, and two whose spans do not overlap
  share a tier; extension lines start at the end of the edge nearest the
  dimension line;
- a number sits above its line where it fits between the arrowheads;
  otherwise it is scored over candidate places out along the line, and
  two that would clash are staggered;
- an extension line ends a gap short of what its dimension measures, and
  a line already drawn along it serves as it;
- leaders end in a shoulder and put their text in a column past the view
  and its dimensions; the rows are solved first, in target order, so the
  column does not cross itself, and no two land within an arrowhead of
  each other;
- section markers stand off the outline of their parent view, and go on
  before the leaders;
- notes, then view titles, go under everything placed in the view;
- anything outside the paper window, or a sheet item over a view, is an
  issue.

`Style.font` is a font *file* name, which is what a DXF text style
carries and what a renderer looks up. The default `"Garuda.ttf"` (TLWG)
draws Thai as well as Latin, since a caller's words are not always the
library's.

## Scope

Model space is millimetres; `units=` is the unit the drawing reads in,
its geometry and every number it writes alike -- a bar bending schedule
excepted, which stays in millimetres. `scale=None`
picks the finest scale of `Style.scale_series` at which the drawing fits
its paper. Hatching follows polyhedral's solid kinds through `materials`
(bolt, concrete, generic, grout, rebar, soil, steel, timber).

Documentation: `docs/SPEC.md` (behaviour), `docs/INTERNALS.md`
(maintainer notes), `CHANGELOG.md`.

## License

MIT.
