Skip to content

Commit eea2e5e

Browse files
committed
feat(constructie): voeg doortekenbaar betonbalk-object toe
- hartlijn klik-voor-klik (zoals polylijn) met live balkvoorbeeld; Enter/ dubbelklik/rechtermuisklik sluit af, Escape annuleert laatste punt en daarna de balk; tool blijft actief voor de volgende balk - instelbare breedte in mm (schaalgebied-bewust, vaste 1:100-fallback) en lijnstijl doorgetrokken/gestippeld (NL-conventie: hartlijn streep-punt resp. doorgetrokken dun) - verstek-joins op knikken met miter-limiet naar bevel; haakse eindkappen op vrije uiteinden - automatische T-/hoekaansluiting tussen balken: eigen randen eindigen op de doelrand, geen eindkap op het aansluitvlak; doelbalk blijft in data ongewijzigd (opschonen puur op render- en AP-tijd) - persistentie als /Polygon met /Vertices=omtrek en eigen appearance- stream; OPS_Hartlijn/OPS_BreedteMm/OPS_Lijnstijl maken de balk bij heropenen weer bewerkbaar, incl. verplaatsings-compensatie via OPS_BbRect - eigenschappen-paneel (breedte/lijnstijl), palette-entry NL IFC Bouw, registraties in snap-engine, hit-test, bounds en spatial-index - node-unittest scripts/test-betonbalk.mjs (13 tests, o.a. verstek 90/45, miter-limiet, T-aansluiting, eindkappen, round-trip)
1 parent 3a48467 commit eea2e5e

22 files changed

Lines changed: 1276 additions & 2 deletions
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Betonbalk — doortekenbaar balkobject in plattegrond (ontwerp)
2+
3+
**Doel:** een constructieve betonbalk voor plattegronden die je als hartlijn
4+
doortekent (klik-klik-klik, zoals de polylijn), met een instelbare breedte.
5+
De twee randlijnen worden op elke knik in verstek gejoined, en aansluitingen
6+
tussen áparte balken worden automatisch opgeschoond (T- en hoekaansluiting).
7+
8+
## Interactie
9+
10+
1. Gereedschap `betonbalk` activeren vanuit het toolpalette (NL IFC Bouw).
11+
2. Elke klik voegt een hartlijnpunt toe; het segment-in-wording toont een
12+
live voorbeeld met randen op de huidige breedte. Snap-engine doet mee
13+
(eindpunten, haaks, polair) zoals bij de bestaande tekengereedschappen.
14+
3. Enter of dubbelklik sluit de balk af; Escape annuleert het laatste punt,
15+
nogmaals Escape annuleert de balk.
16+
4. Grippunten op de hartlijnpunten; verslepen van een punt hertekent de
17+
verstekken. Verplaatsen van de hele balk zoals elk object.
18+
19+
## Datamodel
20+
21+
Eigen annotatietype `betonbalk` (precedent: stavenreeks — eigen type, geen
22+
sjabloon, want sjablonen kennen geen klik-voor-klik-invoer):
23+
24+
| Veld | Betekenis | Default |
25+
|---|---|---|
26+
| `points` | hartlijnpunten (app-ruimte) ||
27+
| `breedteMm` | balkbreedte in mm, schaalgebied-bewust | 300 |
28+
| `lijnstijl` | `'doorgetrokken'` \| `'gestippeld'` | doorgetrokken |
29+
| `strokeColor`, `lineWidth`, `opacity` | zoals elders | |
30+
31+
`realSizeMm`-gedrag: bij plaatsing in een schaalgebied wordt `breedteMm`
32+
omgerekend naar papierpunten via de plaatselijke schaal; buiten een
33+
schaalgebied geldt een vaste omrekening (1:100) zoals bij de staalprofielen.
34+
De hartlijn zelf volgt de klikpunten (die zijn al in papierruimte).
35+
36+
## Geometrie (kern, testbaar in Node)
37+
38+
Nieuw module `js/annotations/betonbalk.js` — één bron voor canvas én PDF-AP:
39+
40+
- `beamOutline(points, halfWidthPt)` → de twee randpolylijnen, met
41+
**verstek-join** per knik: randsegmenten van aangrenzende segmenten
42+
worden gesneden; bij bijna-parallelle of zeer scherpe hoeken (miter-limiet
43+
~4× halve breedte) terugvallen op afgeschuinde (bevel) join zodat geen
44+
extreme uitschieters ontstaan.
45+
- `trimAgainstBeams(outline, andereBalken)`**inter-balk-join**: eindigt of
46+
begint een hartlijn op (het lijf of een uiteinde van) een andere betonbalk
47+
op dezelfde pagina (tolerantie: halve breedte van de dunste balk), dan:
48+
- worden de eigen randlijnen doorgetrokken/afgekort tot de randlijn van de
49+
doelbalk (T-aansluiting), en
50+
- wordt op het aansluitvlak géén eindkap getekend, zodat de balken visueel
51+
één geheel vormen.
52+
De doelbalk zelf blijft ongewijzigd in data; het opschonen is puur
53+
render-tijd (en AP-tijd), zodat verplaatsen/verwijderen altijd
54+
omkeerbaar blijft en er geen verborgen mutaties in andere annotaties
55+
ontstaan.
56+
- Eindkappen: haaks dichtgezet op vrije uiteinden.
57+
58+
## Weergave
59+
60+
- Twee randlijnen (kleur/lijndikte uit het object), hartlijn optioneel als
61+
dunne streep-punt-lijn (stijl `'gestippeld'`: randen onderbroken — balk
62+
boven het aanzichtvlak, NL-conventie; hartlijn dan doorgetrokken dun).
63+
- Geen vulling/arcering in v1.
64+
- Selectie: bestaande selectiehandvatten; hit-test op het balklijf
65+
(polygon-contains op de outline).
66+
67+
## Persistentie
68+
69+
Opslaan als `/Polygon`-annotatie met `/Vertices` = outline (zodat externe
70+
programma's iets redelijks tonen en het object verplaatsbaar is), eigen
71+
appearance-stream met de exacte lijnvoering (incl. verstek en inter-balk-trim
72+
op het moment van opslaan), en privésleutels `OPS_Subtype: 'betonbalk'`,
73+
`OPS_Hartlijn` (punten), `OPS_BreedteMm`, `OPS_Lijnstijl` zodat de app hem
74+
bij heropenen weer als bewerkbare betonbalk laadt. Canonieke conventie:
75+
Matrix identiteit, BBox = Rect-maat, geen top-level rotatie.
76+
77+
## Verificatie
78+
79+
1. Node-unittest `scripts/test-betonbalk.mjs`: verstek op 90°- en 45°-knik
80+
(exacte snijpunten), miter-limiet → bevel bij scherpe hoek, T-aansluiting
81+
(rand eindigt op doelrand, geen eindkap), vrije einden wel kap,
82+
round-trip hartlijn → outline → zelfde bij herberekening.
83+
2. Rig: balk met 3 knikken tekenen, breedte wijzigen in het paneel,
84+
lijnstijl wisselen, tweede balk T-vormig laten aansluiten — screenshots.
85+
3. Opslaan naar kopie → heropenen: nog steeds bewerkbaar; pypdfium2-render
86+
van de kopie toont dezelfde lijnvoering.
87+
4. Rooktest `a-drawing` groen; `npm run build` groen.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Schaalgebied-bewuste brug voor de betonbalk.
2+
//
3+
// De pure geometrie-module (betonbalk.js) mag GEEN app-state importeren; de
4+
// balkbreedte (breedteMm) moet toch de plaatselijke tekeningschaal volgen.
5+
// Deze dunne helper haalt de px-per-mm op de positie van de balk op en levert
6+
// de opts waarmee buildBetonbalk() de balk op werkelijke breedte tekent.
7+
//
8+
// De schaal wordt via de LICHTE bronnen opgevraagd (scale-region.js +
9+
// scale-bar.js, die alleen state/factory importeren) — bewust NIET via
10+
// symbols/real-size.js → annotations/measurement.js, want die keten trekt
11+
// rendering.js mee en vormt dan een import-cyclus met de modules die deze
12+
// helper aanroepen (rendering, geometry, spatial-index, selection-helpers).
13+
// Zelfde patroon als stavenreeks-scale.js.
14+
//
15+
// FALLBACK: buiten elk schaalgebied (en zonder kalibratie) geldt de vaste
16+
// 1:100-omrekening uit betonbalk.js (PX_PER_MM_1_100), zodat een balk op een
17+
// ongekalibreerde tekening een verdedigbare papierbreedte houdt.
18+
import { getScaleFromRegion } from './scale-region.js';
19+
import { getScaleForPoint } from './scale-bar.js';
20+
import { resolveBetonbalkParams, halfWidthFromMm } from './betonbalk.js';
21+
22+
const UNIT_TO_MM = { mm: 1, cm: 10, m: 1000, in: 25.4, ft: 304.8 };
23+
24+
// Referentiepunt van een balk: het midden van de bounding box van de
25+
// hartlijnpunten — valt altijd binnen (of het dichtst bij) een schaalgebied
26+
// dat de balk overlapt en is stabiel bij het bijtekenen van punten.
27+
function _anchor(ann) {
28+
const pts = Array.isArray(ann?.points) ? ann.points : [];
29+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
30+
for (const p of pts) {
31+
const x = Number(p?.x), y = Number(p?.y);
32+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
33+
if (x < minX) minX = x;
34+
if (x > maxX) maxX = x;
35+
if (y < minY) minY = y;
36+
if (y > maxY) maxY = y;
37+
}
38+
if (!Number.isFinite(minX)) return null;
39+
return { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
40+
}
41+
42+
/**
43+
* Pagina-pixels per werkelijke millimeter op de positie van de balk.
44+
* Prioriteit: schaalgebied → viewport/schaalbalk/doc-schaal. 0 = onbekend
45+
* (dan valt halfWidthFromMm terug op de vaste 1:100-omrekening).
46+
*/
47+
export function betonbalkPxPerMm(ann) {
48+
if (!ann || ann.page == null) return 0;
49+
const a = _anchor(ann);
50+
if (!a) return 0;
51+
const ms = getScaleFromRegion(ann.page, a.x, a.y)
52+
|| getScaleForPoint(ann.page, a.x, a.y) || null;
53+
if (!ms || !(ms.pixelsPerUnit > 0)) return 0;
54+
const mmPerUnit = UNIT_TO_MM[ms.unit || 'mm'] || 1;
55+
const k = ms.pixelsPerUnit / mmPerUnit;
56+
return Number.isFinite(k) && k > 0 ? k : 0;
57+
}
58+
59+
/** Halve balkbreedte in app-px, schaalgebied-bewust (fallback 1:100). */
60+
export function betonbalkHalfWidthPx(ann) {
61+
const { breedteMm } = resolveBetonbalkParams(ann);
62+
return halfWidthFromMm(breedteMm, betonbalkPxPerMm(ann));
63+
}
64+
65+
/**
66+
* opts voor buildBetonbalk(): eigen halve breedte + de andere betonbalken op
67+
* dezelfde pagina (voor de inter-balk-join), elk met hun eigen schaalbewuste
68+
* halve breedte. De doelbalken worden hier alleen GELEZEN — nooit gemuteerd.
69+
*/
70+
export function betonbalkBuildOpts(ann, annotations) {
71+
const others = [];
72+
for (const o of annotations || []) {
73+
if (!o || o.type !== 'betonbalk' || o === ann) continue;
74+
if (o.id != null && ann?.id != null && o.id === ann.id) continue;
75+
if (o.page !== ann?.page) continue;
76+
if (!Array.isArray(o.points) || o.points.length < 2) continue;
77+
others.push({ points: o.points, halfWidth: betonbalkHalfWidthPx(o) });
78+
}
79+
return { halfWidth: betonbalkHalfWidthPx(ann), others };
80+
}

0 commit comments

Comments
 (0)