- 덧그림 범위를 서버가 화면과 같은 TS 로 셈 — Node 진입점 B07_DesignDetail_Cross_Extent_Node(최소 DOM 위에서 오프스크린 SVG 그리기 그대로) · 번들 build:b07-extent - 화면 그리기·수확 부분을 B07_DesignDetail_Cross_Overlay 로 떼어 화면과 Node 가 한 벌로 씀 - 장 배치·블록 테두리가 측점별 덧그림 범위를 덮음(목록·장 열기·확정·수정 네 길 모두 같은 계획) - 글자는 CAD 그리기대로 세로 가운데 · 폭은 글꼴 어림(한글 1.0 · 나머지 0.6 배) - 번들 낡음 판정에 B07 맨 위 TS 포함 - 검증_sub4: 66개 박스 ↔ 그림(덧그림·글자 실폭 포함) 모두 1.0 mm · 잘림 0 · 겹침 0 · 20장 → 21장 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
476 lines
18 KiB
TypeScript
476 lines
18 KiB
TypeScript
/* =============================================================================
|
||
* B07_DesignDetail_Cross_Overlay.ts
|
||
* 횡단도 **덧그림**(B06 설계선·구조물) → CAD 엔티티. 화면(`_UI_Cad_Structures`)과
|
||
* 서버 Node(`_Cross_Extent_Node`)가 같이 쓴다 — 서버는 이 범위로 블록 박스를 잡는다(68-3).
|
||
*
|
||
* 산식은 새로 만들지 않는다. B06 화면이 쓰는 기하·그리기 함수를 **그대로** 불러
|
||
* 화면에 붙이지 않은 오프스크린 SVG에 그린 뒤, 그 도형을 CAD 엔티티로 옮긴다
|
||
* (2026-08-30 사용자 확정: 프론트에서 B06 산식 재사용 — 파이썬 포팅 금지).
|
||
* ========================================================================== */
|
||
|
||
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
|
||
import {
|
||
computeStoredLayouts as computeLayouts,
|
||
type StoredLayouts,
|
||
} from "../B06_Section/B06_Section_Structure_Layouts";
|
||
import { appendBoxOverlay } from "../B06_Section/B06_Section_UI_Cross_Box";
|
||
import { appendCulvertOverlay } from "../B06_Section/B06_Section_UI_Cross_Culvert";
|
||
import { appendFordOverlay } from "../B06_Section/B06_Section_UI_Cross_Ford";
|
||
import { appendFordPavementOverlay } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement";
|
||
import {
|
||
appendCrossDesignOverlay,
|
||
appendPavementOverlay,
|
||
} from "../B06_Section/B06_Section_UI_Cross_Design";
|
||
import { appendRevetmentOverlay } from "../B06_Section/B06_Section_UI_Cross_Revetment";
|
||
|
||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||
|
||
/** 구조물 엔티티가 들어갈 레이어·색 — 서버 도면이 이미 선언해 둔 그 레이어다. */
|
||
const STRUCTURE_LAYER_ID = "b08-structure";
|
||
const STRUCTURE_COLOR = "#f6d55c";
|
||
/** 설계선 도면층·색 — 서버 도면(`B07_DesignDetail_Engine_Cad`)이 쓰는 그 값이다. */
|
||
export const DESIGN_LAYER_ID = "b08-design";
|
||
const DESIGN_COLOR = "#b794f6";
|
||
/** 글자 크기(종이 mm) — SVG는 CSS로 크기를 잡아 오프스크린에서는 읽을 수 없다. */
|
||
const LABEL_FONT_MM = 2.0;
|
||
|
||
/** 정의부(해칭 패턴·클립)는 도형이 아니다 — 클립 경계 자체는 그림이 아니라 잘라 낼 자다. */
|
||
const SKIP_SELECTOR = "defs, clipPath, pattern";
|
||
/** 원을 폴리선으로 바꿀 때 쓰는 분할 수 — 클립 안에서만 쓴다(밖은 Circle 그대로). */
|
||
const CIRCLE_SEGMENTS = 36;
|
||
|
||
export type Entity = Record<string, unknown>;
|
||
type XY = [number, number];
|
||
/** 수확한 도형을 어느 도면층·색으로 넣을지. */
|
||
interface Style {
|
||
layerId: string;
|
||
color: string;
|
||
}
|
||
|
||
const STRUCTURE_STYLE: Style = { layerId: STRUCTURE_LAYER_ID, color: STRUCTURE_COLOR };
|
||
const DESIGN_STYLE: Style = { layerId: DESIGN_LAYER_ID, color: DESIGN_COLOR };
|
||
|
||
function baseEntity(style: Style, type: string, shapeData: unknown): Entity {
|
||
return {
|
||
id: crypto.randomUUID(),
|
||
type,
|
||
lineColor: style.color,
|
||
lineWidth: 1,
|
||
layerId: style.layerId,
|
||
shapeData,
|
||
};
|
||
}
|
||
|
||
function lineEntity(style: Style, start: XY, end: XY): Entity {
|
||
return baseEntity(style, "Line", {
|
||
startPoint: { x: start[0], y: start[1] },
|
||
endPoint: { x: end[0], y: end[1] },
|
||
});
|
||
}
|
||
|
||
/** 점열 → PolyLine(자식 Line 묶음). 서버 도면의 폴리라인과 같은 직렬화다. */
|
||
function polyEntity(style: Style, points: XY[]): Entity | null {
|
||
if (points.length < 2) return null;
|
||
const children: Entity[] = [];
|
||
for (let index = 0; index < points.length - 1; index += 1) {
|
||
children.push(lineEntity(style, points[index], points[index + 1]));
|
||
}
|
||
const poly = baseEntity(style, "PolyLine", null);
|
||
poly.children = children;
|
||
return poly;
|
||
}
|
||
|
||
function textEntity(style: Style, label: string, at: XY, align: string): Entity {
|
||
return baseEntity(style, "Text", {
|
||
label,
|
||
basePoint: { x: at[0], y: at[1] },
|
||
options: {
|
||
textDirection: { x: 1, y: 0 },
|
||
textAlign: align,
|
||
textColor: style.color,
|
||
fontSize: LABEL_FONT_MM,
|
||
fontFamily: "sans-serif",
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 공선점 제거(Ramer–Douglas–Peucker) — 설계선은 지반 샘플마다 점을 갖고 있어(측점 하나에
|
||
* 83점) 곧은 사면이 수십 조각으로 쪼개진다. CAD에서 한 변은 한 선이어야 잡고 고칠 수 있다.
|
||
* 허용 오차는 **종이 mm**라, 1/100에서 0.05mm = 실거리 5mm — 도면상 같은 직선이다.
|
||
*/
|
||
function simplify(points: XY[], tolerance = 0.05): XY[] {
|
||
if (points.length < 3) return points;
|
||
const first = points[0];
|
||
const last = points[points.length - 1];
|
||
const [dx, dy] = [last[0] - first[0], last[1] - first[1]];
|
||
const span = Math.hypot(dx, dy);
|
||
let worst = 0;
|
||
let index = 0;
|
||
for (let i = 1; i < points.length - 1; i += 1) {
|
||
const [px, py] = points[i];
|
||
const distance =
|
||
span > 1e-9
|
||
? Math.abs(dy * px - dx * py + last[0] * first[1] - last[1] * first[0]) / span
|
||
: Math.hypot(px - first[0], py - first[1]);
|
||
if (distance > worst) {
|
||
worst = distance;
|
||
index = i;
|
||
}
|
||
}
|
||
if (worst <= tolerance) return [first, last];
|
||
return [
|
||
...simplify(points.slice(0, index + 1), tolerance).slice(0, -1),
|
||
...simplify(points.slice(index), tolerance),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 끝점이 맞물리는 선분들을 하나의 점열로 잇는다.
|
||
*
|
||
* B06 설계선 그리기는 **점 사이마다 `<line>` 하나**를 만든다(_Cross_Design 603행) —
|
||
* 그대로 옮기면 곧은 사면 한 변이 CAD에서 선 열댓 개가 된다. 이어 붙인 뒤 공선점을
|
||
* 지우면 한 변이 한 선이 된다(2026-08-30 사용자 지적).
|
||
*/
|
||
function chain(segments: XY[][], tolerance = 1e-3): XY[][] {
|
||
const used = new Array(segments.length).fill(false);
|
||
const near = (a: XY, b: XY): boolean =>
|
||
Math.abs(a[0] - b[0]) <= tolerance && Math.abs(a[1] - b[1]) <= tolerance;
|
||
const chains: XY[][] = [];
|
||
for (let seed = 0; seed < segments.length; seed += 1) {
|
||
if (used[seed]) continue;
|
||
used[seed] = true;
|
||
const points: XY[] = [segments[seed][0], segments[seed][1]];
|
||
let grew = true;
|
||
while (grew) {
|
||
grew = false;
|
||
for (let i = 0; i < segments.length; i += 1) {
|
||
if (used[i]) continue;
|
||
const [a, b] = segments[i];
|
||
const head = points[0];
|
||
const tail = points[points.length - 1];
|
||
if (near(tail, a)) points.push(b);
|
||
else if (near(tail, b)) points.push(a);
|
||
else if (near(head, b)) points.unshift(a);
|
||
else if (near(head, a)) points.unshift(b);
|
||
else continue;
|
||
used[i] = true;
|
||
grew = true;
|
||
}
|
||
}
|
||
chains.push(points);
|
||
}
|
||
return chains;
|
||
}
|
||
|
||
/** 점열을 종이 x범위로 자른다 — 경계는 선형보간으로 새 점을 만든다(서버 _clip_polyline과 같은 규칙). */
|
||
function clipX(points: XY[], x0: number, x1: number): XY[] {
|
||
const clipped: XY[] = [];
|
||
for (let index = 0; index < points.length; index += 1) {
|
||
const [x, y] = points[index];
|
||
if (index > 0) {
|
||
const [px, py] = points[index - 1];
|
||
for (const edge of [x0, x1]) {
|
||
if ((px < edge && edge < x) || (x < edge && edge < px)) {
|
||
const ratio = (edge - px) / (x - px);
|
||
clipped.push([edge, py + (y - py) * ratio]);
|
||
}
|
||
}
|
||
}
|
||
if (x >= x0 && x <= x1) clipped.push([x, y]);
|
||
}
|
||
return clipped;
|
||
}
|
||
|
||
function attr(element: SVGElement, name: string): number {
|
||
return Number(element.getAttribute(name) ?? 0);
|
||
}
|
||
|
||
/** SVG 좌표(y 아래로 증가) → 도면 좌표(y 위로 증가). */
|
||
function flip(x: number, y: number): XY {
|
||
return [x, -y];
|
||
}
|
||
|
||
function parsePoints(element: SVGElement): XY[] {
|
||
const raw = (element.getAttribute("points") ?? "").trim();
|
||
if (!raw) return [];
|
||
const numbers = raw.split(/[\s,]+/).map(Number);
|
||
const points: XY[] = [];
|
||
for (let index = 0; index + 1 < numbers.length; index += 2) {
|
||
points.push(flip(numbers[index], numbers[index + 1]));
|
||
}
|
||
return points;
|
||
}
|
||
|
||
/**
|
||
* 이 도형에 걸린 clipPath 폴리곤(도면 좌표). 없으면 null.
|
||
*
|
||
* 기슭막이 형태 해칭(`B06_Section_UI_Cross_Wall_Hatch`)은 벽 폴리곤 clip 안에서 그린다.
|
||
* 종전 수확기는 그 그룹을 통째로 건너뛰어 **돌쌓기·콘크리트 해칭이 CAD 도면에 하나도
|
||
* 실리지 않았다**. 클립을 쓰는 대신 경계로 **잘라서** 싣는다(2026-09-03 사용자 결정).
|
||
*/
|
||
function clipPolygonOf(element: SVGElement): XY[] | null {
|
||
const group = element.closest("g[clip-path]");
|
||
const reference = group?.getAttribute("clip-path") ?? "";
|
||
const id = reference.match(/url\(#([^)]+)\)/)?.[1];
|
||
if (!id) return null;
|
||
const shape = element.ownerDocument?.getElementById(id)?.querySelector("polygon");
|
||
if (!shape) return null;
|
||
const points = parsePoints(shape as unknown as SVGElement);
|
||
return points.length >= 3 ? points : null;
|
||
}
|
||
|
||
/** 점이 폴리곤 안인가 — 오목한 벽 단면도 되도록 광선 교차 홀짝으로 본다. */
|
||
function pointInPolygon(point: XY, polygon: XY[]): boolean {
|
||
let inside = false;
|
||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i, i += 1) {
|
||
const [xi, yi] = polygon[i];
|
||
const [xj, yj] = polygon[j];
|
||
const straddles = yi > point[1] !== yj > point[1];
|
||
if (straddles && point[0] < ((xj - xi) * (point[1] - yi)) / (yj - yi) + xi) inside = !inside;
|
||
}
|
||
return inside;
|
||
}
|
||
|
||
/** 선분을 폴리곤 경계에서 잘라 **안쪽 조각들**만 돌려준다. */
|
||
function clipSegment(start: XY, end: XY, polygon: XY[]): XY[][] {
|
||
const [x0, y0] = start;
|
||
const [x1, y1] = end;
|
||
const dx = x1 - x0;
|
||
const dy = y1 - y0;
|
||
const cuts = [0, 1];
|
||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i, i += 1) {
|
||
const [ex, ey] = polygon[j];
|
||
const [fx, fy] = polygon[i];
|
||
const denominator = dx * (fy - ey) - dy * (fx - ex);
|
||
if (Math.abs(denominator) < 1e-12) continue;
|
||
const t = ((ex - x0) * (fy - ey) - (ey - y0) * (fx - ex)) / denominator;
|
||
const u = ((ex - x0) * dy - (ey - y0) * dx) / denominator;
|
||
if (t > 0 && t < 1 && u >= 0 && u <= 1) cuts.push(t);
|
||
}
|
||
cuts.sort((a, b) => a - b);
|
||
const runs: XY[][] = [];
|
||
for (let index = 0; index + 1 < cuts.length; index += 1) {
|
||
const from = cuts[index];
|
||
const to = cuts[index + 1];
|
||
if (to - from < 1e-9) continue;
|
||
const mid = (from + to) / 2;
|
||
if (!pointInPolygon([x0 + dx * mid, y0 + dy * mid], polygon)) continue;
|
||
runs.push([
|
||
[x0 + dx * from, y0 + dy * from],
|
||
[x0 + dx * to, y0 + dy * to],
|
||
]);
|
||
}
|
||
return runs;
|
||
}
|
||
|
||
/** 점열을 폴리곤 안으로 자른다 — 조각마다 한 줄. */
|
||
function clipPointsToPolygon(points: XY[], polygon: XY[]): XY[][] {
|
||
const runs: XY[][] = [];
|
||
for (let index = 0; index + 1 < points.length; index += 1) {
|
||
runs.push(...clipSegment(points[index], points[index + 1], polygon));
|
||
}
|
||
return runs;
|
||
}
|
||
|
||
function circlePoints(center: XY, radius: number): XY[] {
|
||
const points: XY[] = [];
|
||
for (let index = 0; index <= CIRCLE_SEGMENTS; index += 1) {
|
||
const angle = (2 * Math.PI * index) / CIRCLE_SEGMENTS;
|
||
points.push([center[0] + radius * Math.cos(angle), center[1] + radius * Math.sin(angle)]);
|
||
}
|
||
return points;
|
||
}
|
||
|
||
/**
|
||
* `rect` → 닫힌 네 모서리 점열(회전 transform 적용).
|
||
*
|
||
* 돌쌓기 돌·돌망태 칸(`B06_Section_UI_Cross_Wall_Hatch.cell()`)은 `rect` 하나에
|
||
* `rotate(벽기울기 cx cy)` 를 걸어 그린다. 수확기가 `rect` 를 안 보던 때는 형태 해칭
|
||
* 80개 중 **49개(돌·칸 전부)가 CAD 에 안 실렸다**(2026-09-03 실측). 모서리 반경(`rx`)은
|
||
* 무시한다 — 도면에서는 각진 칸으로 충분하다.
|
||
*/
|
||
function rectPoints(element: SVGElement): XY[] {
|
||
const x = attr(element, "x");
|
||
const y = attr(element, "y");
|
||
const width = attr(element, "width");
|
||
const height = attr(element, "height");
|
||
const corners: XY[] = [
|
||
[x, y],
|
||
[x + width, y],
|
||
[x + width, y + height],
|
||
[x, y + height],
|
||
[x, y],
|
||
];
|
||
const rotate = /rotate\(\s*(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+(-?[\d.]+)\s*\)/.exec(
|
||
element.getAttribute("transform") ?? "",
|
||
);
|
||
if (!rotate) return corners.map(([cx, cy]) => flip(cx, cy));
|
||
const angle = (Number(rotate[1]) * Math.PI) / 180;
|
||
const [ox, oy] = [Number(rotate[2]), Number(rotate[3])];
|
||
const cos = Math.cos(angle);
|
||
const sin = Math.sin(angle);
|
||
return corners.map(([cx, cy]) => {
|
||
const dx = cx - ox;
|
||
const dy = cy - oy;
|
||
return flip(ox + dx * cos - dy * sin, oy + dx * sin + dy * cos);
|
||
});
|
||
}
|
||
|
||
/** 오프스크린 SVG에 그려진 도형을 CAD 엔티티로 옮긴다 (블록 테두리 안으로 자른다). */
|
||
function harvest(root: SVGElement, style: Style, frame: number[]): Entity[] {
|
||
const [fx0, , fx1] = frame;
|
||
const entities: Entity[] = [];
|
||
const segments: XY[][] = [];
|
||
const inside = (px: number): boolean => px >= fx0 && px <= fx1;
|
||
const nodes = root.querySelectorAll<SVGElement>("polygon, polyline, line, circle, rect, text");
|
||
for (const element of Array.from(nodes)) {
|
||
if (element.closest(SKIP_SELECTOR)) continue;
|
||
const tag = element.tagName.toLowerCase();
|
||
// 클립 그룹 안의 해칭은 경계로 **잘라서** 싣는다 — CAD 에는 클립이 없다.
|
||
const clip = clipPolygonOf(element);
|
||
if (tag === "polygon" || tag === "polyline" || tag === "rect") {
|
||
const points = tag === "rect" ? rectPoints(element) : parsePoints(element);
|
||
if (tag === "polygon" && points.length > 2) points.push(points[0]);
|
||
const runs = clip ? clipPointsToPolygon(points, clip) : [points];
|
||
for (const run of runs) {
|
||
const poly = polyEntity(style, simplify(clipX(run, fx0, fx1)));
|
||
if (poly) entities.push(poly);
|
||
}
|
||
} else if (tag === "line") {
|
||
const start = flip(attr(element, "x1"), attr(element, "y1"));
|
||
const end = flip(attr(element, "x2"), attr(element, "y2"));
|
||
for (const run of clip ? clipSegment(start, end, clip) : [[start, end]]) {
|
||
const cut = clipX(run, fx0, fx1);
|
||
if (cut.length === 2) segments.push(cut);
|
||
}
|
||
} else if (tag === "circle") {
|
||
const center = flip(attr(element, "cx"), attr(element, "cy"));
|
||
if (!inside(center[0])) continue;
|
||
const radius = attr(element, "r");
|
||
if (!clip) {
|
||
entities.push(
|
||
baseEntity(style, "Circle", {
|
||
center: { x: center[0], y: center[1] },
|
||
radius,
|
||
}),
|
||
);
|
||
continue;
|
||
}
|
||
// 클립 안 원(통나무 마구리 등)은 폴리선으로 바꿔 경계에서 자른다.
|
||
for (const run of clipPointsToPolygon(circlePoints(center, radius), clip)) {
|
||
const poly = polyEntity(style, simplify(clipX(run, fx0, fx1)));
|
||
if (poly) entities.push(poly);
|
||
}
|
||
} else if (tag === "text") {
|
||
const label = (element.textContent ?? "").trim();
|
||
const at = flip(attr(element, "x"), attr(element, "y"));
|
||
if (!label || !inside(at[0])) continue;
|
||
if (clip && !pointInPolygon(at, clip)) continue;
|
||
const anchor = element.getAttribute("text-anchor");
|
||
const align = anchor === "start" ? "left" : anchor === "end" ? "right" : "center";
|
||
entities.push(textEntity(style, label, at, align));
|
||
}
|
||
}
|
||
// 낱개 선분은 이어 붙인 뒤 공선점을 지워 한 변을 한 선으로 만든다.
|
||
for (const points of chain(segments)) {
|
||
const poly = polyEntity(style, simplify(points));
|
||
if (poly) entities.push(poly);
|
||
}
|
||
return entities;
|
||
}
|
||
|
||
/** 엔티티가 차지하는 종이 좌표 사각형 (없으면 null). */
|
||
export function entityBox(entity: Record<string, unknown>): number[] | null {
|
||
const xs: number[] = [];
|
||
const ys: number[] = [];
|
||
const walk = (node: Record<string, unknown>): void => {
|
||
const shape = node.shapeData as Record<string, Record<string, number>> | null;
|
||
for (const key of ["startPoint", "endPoint", "basePoint", "center"]) {
|
||
const point = shape?.[key];
|
||
if (point) {
|
||
xs.push(point.x);
|
||
ys.push(point.y);
|
||
}
|
||
}
|
||
for (const child of (node.children as Record<string, unknown>[]) ?? []) walk(child);
|
||
};
|
||
walk(entity);
|
||
return xs.length ? [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)] : null;
|
||
}
|
||
|
||
type Layouts = StoredLayouts;
|
||
|
||
/**
|
||
* 설계선(+포장층)을 그린다. **구조물이 깎아 낸 설계선**(designTrim)을 B06 카드와 같은
|
||
* 우선순위로 넘긴다 — 정본에는 트림 전 원본만 있어서, 이걸 안 태우면 벽이 서 있어도
|
||
* 설계선이 원래대로 지나간다(2026-08-30 사용자 지적).
|
||
*/
|
||
function drawDesign(
|
||
svg: SVGElement,
|
||
section: CrossSection,
|
||
layouts: Layouts,
|
||
x: (offset: number) => number,
|
||
y: (elevation: number) => number,
|
||
): void {
|
||
const { design, culvert, box, ford, own } = layouts;
|
||
const paved = appendFordPavementOverlay(svg, section.ford_pavement, design, x, y);
|
||
if (!paved) appendPavementOverlay(svg, design, x, y);
|
||
appendCrossDesignOverlay(
|
||
svg,
|
||
design,
|
||
x,
|
||
y,
|
||
section.samples,
|
||
culvert?.designTrim ?? ford?.designTrim ?? box?.designTrim ?? own?.designTrim,
|
||
);
|
||
}
|
||
|
||
/** 구조물을 그린다. 그리는 순서는 B06 카드와 같다. */
|
||
function drawStructures(
|
||
svg: SVGElement,
|
||
section: CrossSection,
|
||
layouts: Layouts,
|
||
x: (offset: number) => number,
|
||
y: (elevation: number) => number,
|
||
): void {
|
||
const { link, culvert, box, ford, own } = layouts;
|
||
if (own) appendRevetmentOverlay(svg, own, x, y);
|
||
if (box) appendBoxOverlay(svg, box, x, y);
|
||
if (ford) appendFordOverlay(svg, ford, x, y);
|
||
if (culvert) {
|
||
const linked = !section.culvert && !!link;
|
||
appendCulvertOverlay(
|
||
svg,
|
||
culvert,
|
||
x,
|
||
y,
|
||
undefined,
|
||
linked || culvert.culvert.hidden_pipe === true,
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 한 측점의 덧그림 엔티티 — 설계선(구조물 트림 반영) + 구조물. 가로는 `frame`
|
||
* [x0, y0, x1, y1] 안으로 자른다. 기하가 없으면(layouts 없음) null.
|
||
*/
|
||
export function overlayEntities(
|
||
section: CrossSection,
|
||
sections: CrossSection[],
|
||
x: (offset: number) => number,
|
||
y: (elevation: number) => number,
|
||
frame: number[],
|
||
): Entity[] | null {
|
||
const layouts = computeLayouts(section, sections);
|
||
if (!layouts) return null;
|
||
const designSvg = document.createElementNS(SVG_NS, "svg");
|
||
drawDesign(designSvg, section, layouts, x, y);
|
||
const structureSvg = document.createElementNS(SVG_NS, "svg");
|
||
drawStructures(structureSvg, section, layouts, x, y);
|
||
return [
|
||
...harvest(designSvg, DESIGN_STYLE, frame),
|
||
...harvest(structureSvg, STRUCTURE_STYLE, frame),
|
||
];
|
||
}
|