Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_8
This commit is contained in:
@@ -317,6 +317,108 @@ function rectPoints(element: SVGElement): XY[] {
|
||||
});
|
||||
}
|
||||
|
||||
/** pattern 한 칸의 도형(SVG 좌표) — 선(path M/L) · 원/타원. 바탕 rect 는 그림이 아니라 뺌. */
|
||||
export interface PatternTile {
|
||||
width: number;
|
||||
height: number;
|
||||
paths: XY[][];
|
||||
rings: Array<{ cx: number; cy: number; rx: number; ry: number }>;
|
||||
}
|
||||
|
||||
/** 무늬 칸 수 상한 — 넘으면 무늬를 버리고 외곽선만(ponytail: 큰 다각형이면 칸 크기를 키울 것). */
|
||||
const PATTERN_TILE_LIMIT = 20000;
|
||||
|
||||
/**
|
||||
* pattern 채움을 다각형(도면 좌표) 안으로 펴서 자른다(88-4) — CAD 에는 무늬 채움이 없다.
|
||||
* 칸은 SVG 사용자 좌표 원점 기준(`patternUnits="userSpaceOnUse"`). 선은 이어 붙여 한 줄로,
|
||||
* 다각형 안에 온전히 든 원은 원 그대로 · 걸친 원/타원은 잘린 폴리선으로.
|
||||
*/
|
||||
export function patternMarks(
|
||||
polygon: XY[],
|
||||
tile: PatternTile,
|
||||
): { polylines: XY[][]; circles: Array<{ center: XY; radius: number }> } {
|
||||
const polylines: XY[][] = [];
|
||||
const circles: Array<{ center: XY; radius: number }> = [];
|
||||
const { width, height } = tile;
|
||||
if (polygon.length < 3 || !(width > 0) || !(height > 0)) return { polylines, circles };
|
||||
const xs = polygon.map((point) => point[0]);
|
||||
const ys = polygon.map((point) => -point[1]);
|
||||
const [i0, i1] = [Math.floor(Math.min(...xs) / width), Math.floor(Math.max(...xs) / width)];
|
||||
const [j0, j1] = [Math.floor(Math.min(...ys) / height), Math.floor(Math.max(...ys) / height)];
|
||||
if ((i1 - i0 + 1) * (j1 - j0 + 1) > PATTERN_TILE_LIMIT) return { polylines, circles };
|
||||
const lines: XY[][] = [];
|
||||
const arcs: XY[][] = [];
|
||||
for (let i = i0; i <= i1; i += 1) {
|
||||
for (let j = j0; j <= j1; j += 1) {
|
||||
const [dx, dy] = [i * width, j * height];
|
||||
for (const path of tile.paths) {
|
||||
lines.push(
|
||||
...clipPointsToPolygon(
|
||||
path.map(([x, y]) => flip(x + dx, y + dy)),
|
||||
polygon,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const { cx, cy, rx, ry } of tile.rings) {
|
||||
const center = flip(cx + dx, cy + dy);
|
||||
const ring: XY[] = circlePoints([0, 0], 1).map(([x, y]) => [
|
||||
center[0] + x * rx,
|
||||
center[1] + y * ry,
|
||||
]);
|
||||
if (rx === ry && ring.every((point) => pointInPolygon(point, polygon))) {
|
||||
circles.push({ center, radius: rx });
|
||||
} else {
|
||||
arcs.push(...clipPointsToPolygon(ring, polygon));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
polylines.push(...chain(lines).map((points) => simplify(points)), ...chain(arcs));
|
||||
return { polylines, circles };
|
||||
}
|
||||
|
||||
/** 다각형 채움이 가리키는 pattern 을 칸 도형으로 읽는다(채움 = fill 속성 또는 style). 없으면 null. */
|
||||
function patternTileOf(element: SVGElement, root: SVGElement): PatternTile | null {
|
||||
const fill = element.getAttribute("fill") ?? String(element.style?.fill ?? "");
|
||||
const id = /url\(["']?#([^"')]+)/.exec(fill)?.[1];
|
||||
if (!id) return null;
|
||||
const pattern = Array.from(root.querySelectorAll<SVGElement>("pattern")).find(
|
||||
(node) => node.getAttribute("id") === id,
|
||||
);
|
||||
if (!pattern) return null;
|
||||
const tile: PatternTile = {
|
||||
width: attr(pattern, "width"),
|
||||
height: attr(pattern, "height"),
|
||||
paths: [],
|
||||
rings: [],
|
||||
};
|
||||
for (const child of Array.from(pattern.children) as SVGElement[]) {
|
||||
const tag = child.tagName.toLowerCase();
|
||||
if (tag === "path") {
|
||||
// M/L 절대 좌표만(무늬 빗금 꼴) — M 마다 새 줄
|
||||
for (const run of (child.getAttribute("d") ?? "").split(/(?=M)/i)) {
|
||||
const numbers = run.match(/-?[\d.]+/g)?.map(Number) ?? [];
|
||||
const points: XY[] = [];
|
||||
for (let index = 0; index + 1 < numbers.length; index += 2) {
|
||||
points.push([numbers[index], numbers[index + 1]]);
|
||||
}
|
||||
if (points.length > 1) tile.paths.push(points);
|
||||
}
|
||||
} else if (tag === "circle") {
|
||||
const r = attr(child, "r");
|
||||
tile.rings.push({ cx: attr(child, "cx"), cy: attr(child, "cy"), rx: r, ry: r });
|
||||
} else if (tag === "ellipse") {
|
||||
tile.rings.push({
|
||||
cx: attr(child, "cx"),
|
||||
cy: attr(child, "cy"),
|
||||
rx: attr(child, "rx"),
|
||||
ry: attr(child, "ry"),
|
||||
});
|
||||
}
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
/** 오프스크린 SVG에 그려진 도형을 CAD 엔티티로 옮긴다 (블록 테두리 안으로 자른다). */
|
||||
function harvest(root: SVGElement, style: Style, frame: number[]): Entity[] {
|
||||
const [fx0, , fx1] = frame;
|
||||
@@ -337,6 +439,21 @@ function harvest(root: SVGElement, style: Style, frame: number[]): Entity[] {
|
||||
const poly = polyEntity(style, simplify(clipX(run, fx0, fx1)));
|
||||
if (poly) entities.push(poly);
|
||||
}
|
||||
// 무늬 채움(노면공 · 물넘이포장) — 무늬 선 · 원을 다각형 안으로 펴서 같은 층 · 색으로(88-4)
|
||||
const tile = tag === "polygon" && !clip ? patternTileOf(element, root) : null;
|
||||
if (tile) {
|
||||
const marks = patternMarks(points, tile);
|
||||
for (const run of marks.polylines) {
|
||||
const poly = polyEntity(style, clipX(run, fx0, fx1));
|
||||
if (poly) entities.push(poly);
|
||||
}
|
||||
for (const { center, radius } of marks.circles) {
|
||||
if (!inside(center[0])) continue;
|
||||
entities.push(
|
||||
baseEntity(style, "Circle", { center: { x: center[0], y: center[1] }, radius }),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (tag === "line") {
|
||||
const start = flip(attr(element, "x1"), attr(element, "y1"));
|
||||
const end = flip(attr(element, "x2"), attr(element, "y2"));
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// B07 CAD 수확 — pattern 채움(노면공 무늬 셋 · 물넘이 빗금)을 다각형 안 선 · 원으로 펴기(88-4).
|
||||
// 실제 코드(`B07_DesignDetail_Cross_Overlay.patternMarks`)를 vite ssr 로 묶어 돌린다.
|
||||
// 실행: node resources/tester/test_cad_pattern_harvest.mjs
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const ROOT = resolve(import.meta.dirname, "../..");
|
||||
const out = mkdtempSync(join(tmpdir(), "cad-pattern-"));
|
||||
execFileSync(
|
||||
"node",
|
||||
[
|
||||
"./config/node_modules/vite/bin/vite.js",
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts",
|
||||
"--outDir",
|
||||
out,
|
||||
"--logLevel",
|
||||
"error",
|
||||
],
|
||||
{ cwd: ROOT, stdio: "pipe" },
|
||||
);
|
||||
const { patternMarks } = await import(
|
||||
pathToFileURL(join(out, "B07_DesignDetail_Cross_Overlay.js")).href
|
||||
);
|
||||
|
||||
// 도면 좌표(y 위로) 다각형 — SVG y 100~103(두께 3) · x 10~70. 닫힌 점열.
|
||||
const polygon = [
|
||||
[10, -100],
|
||||
[70, -100],
|
||||
[70, -103],
|
||||
[10, -103],
|
||||
[10, -100],
|
||||
];
|
||||
const inPoly = ([x, y]) => x >= 10 - 1e-6 && x <= 70 + 1e-6 && y <= -100 + 1e-6 && y >= -103 - 1e-6;
|
||||
const allPoints = (marks) => [
|
||||
...marks.polylines.flat(),
|
||||
...marks.circles.flatMap(({ center: [cx, cy], radius }) => [
|
||||
[cx - radius, cy],
|
||||
[cx + radius, cy],
|
||||
[cx, cy - radius],
|
||||
[cx, cy + radius],
|
||||
]),
|
||||
];
|
||||
|
||||
// ① 콘크리트 · 물넘이 빗금(M0,6 L6,0 · 칸 6) — 대각선이 칸을 넘어 이어져 한 줄씩 · 모두 다각형 안
|
||||
{
|
||||
const marks = patternMarks(polygon, {
|
||||
width: 6,
|
||||
height: 6,
|
||||
paths: [
|
||||
[
|
||||
[0, 6],
|
||||
[6, 0],
|
||||
],
|
||||
],
|
||||
rings: [],
|
||||
});
|
||||
assert.equal(marks.circles.length, 0);
|
||||
// x+y = 6k 인 대각선 중 띠(y 100~103)를 지나는 것: x+y ∈ (110, 173) → k = 19..28 = 10 줄
|
||||
assert.equal(marks.polylines.length, 10, `빗금 줄 수 ${marks.polylines.length}`);
|
||||
for (const run of marks.polylines) assert.equal(run.length, 2, "한 줄 = 두 점(이어 붙임)");
|
||||
assert.ok(allPoints(marks).every(inPoly), "빗금이 다각형 밖으로 나감");
|
||||
}
|
||||
|
||||
// ② 아스콘 점(원 r 0.7 · 칸 5) — 온전히 든 원은 원 그대로
|
||||
{
|
||||
const marks = patternMarks(polygon, {
|
||||
width: 5,
|
||||
height: 5,
|
||||
paths: [],
|
||||
rings: [{ cx: 1.5, cy: 1.5, rx: 0.7, ry: 0.7 }],
|
||||
});
|
||||
// 중심 y = 101.5 (칸 j=20) · x = 11.5, 16.5 … 66.5 = 12 개 — 모두 띠 안
|
||||
assert.equal(marks.circles.length, 12, `점 수 ${marks.circles.length}`);
|
||||
assert.equal(marks.polylines.length, 0);
|
||||
assert.ok(allPoints(marks).every(inPoly), "점이 다각형 밖으로 나감");
|
||||
}
|
||||
|
||||
// ③ 쇄석 돌(원 r 1.6 + 타원 · 칸 9) — 걸친 것은 잘린 폴리선 · 밖으로 안 나감
|
||||
{
|
||||
const marks = patternMarks(polygon, {
|
||||
width: 9,
|
||||
height: 9,
|
||||
paths: [],
|
||||
rings: [
|
||||
{ cx: 2.5, cy: 2.5, rx: 1.6, ry: 1.6 },
|
||||
{ cx: 6.8, cy: 6.5, rx: 1.8, ry: 1.2 },
|
||||
],
|
||||
});
|
||||
assert.ok(marks.polylines.length + marks.circles.length > 0, "돌 무늬 없음");
|
||||
assert.ok(allPoints(marks).every(inPoly), "돌이 다각형 밖으로 나감");
|
||||
}
|
||||
|
||||
// ④ 빈 칸 · 다각형 없음 = 무늬 없음
|
||||
{
|
||||
assert.deepEqual(patternMarks(polygon, { width: 0, height: 6, paths: [], rings: [] }), {
|
||||
polylines: [],
|
||||
circles: [],
|
||||
});
|
||||
assert.equal(patternMarks([], { width: 6, height: 6, paths: [], rings: [] }).polylines.length, 0);
|
||||
}
|
||||
|
||||
console.log("OK test_cad_pattern_harvest.mjs — 4건");
|
||||
Reference in New Issue
Block a user