dsh-cad 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +190 -0
- package/README.zh-CN.md +173 -0
- package/cordis.patch.yml +3 -0
- package/lib/b64.js +12 -0
- package/lib/cad_connector/builtin-executor.js +55 -0
- package/lib/cad_connector/builtin.js +8 -0
- package/lib/cad_connector/executor.js +76 -0
- package/lib/cad_connector/freecad-executor.js +398 -0
- package/lib/cad_connector/freecad.js +8 -0
- package/lib/cad_connector/fusion360-executor.js +398 -0
- package/lib/cad_connector/fusion360.js +8 -0
- package/lib/cad_connector/gstarcad3d.js +8 -0
- package/lib/cad_connector/index.js +20 -0
- package/lib/cad_connector/onshape.js +8 -0
- package/lib/cad_connector/solidworks.js +8 -0
- package/lib/cad_connector/types.js +6 -0
- package/lib/cad_connector/zw3d.js +8 -0
- package/lib/client.js +4117 -0
- package/lib/client.js.map +7 -0
- package/lib/convert/dcprt.js +38 -0
- package/lib/convert/detect.js +22 -0
- package/lib/convert/dxf.js +124 -0
- package/lib/convert/index.js +70 -0
- package/lib/convert/obj.js +106 -0
- package/lib/convert/png.js +114 -0
- package/lib/convert/step-worker.mjs +61 -0
- package/lib/convert/step.js +66 -0
- package/lib/convert/stl.js +110 -0
- package/lib/convert/svg.js +101 -0
- package/lib/demo-bracket.brep +668 -0
- package/lib/demo-flange.brep +709 -0
- package/lib/demo-shaft.brep +516 -0
- package/lib/feature_script/dc_asm.js +17 -0
- package/lib/feature_script/dc_engineering.js +19 -0
- package/lib/feature_script/dc_prt.js +21 -0
- package/lib/feature_script/index.js +7 -0
- package/lib/image/profile.js +218 -0
- package/lib/index.js +77 -0
- package/lib/modeling/assembly.js +63 -0
- package/lib/modeling/bin-format.js +80 -0
- package/lib/modeling/bin-store.js +67 -0
- package/lib/modeling/client.js +117 -0
- package/lib/modeling/document.js +55 -0
- package/lib/modeling/drawing.js +324 -0
- package/lib/modeling/hlr.cjs +328 -0
- package/lib/modeling/modeling-worker.cjs +235 -0
- package/lib/modeling/occt-adapter.cjs +271 -0
- package/lib/routes.js +142 -0
- package/lib/store.js +68 -0
- package/lib/tools/cad-freecad.js +160 -0
- package/lib/tools/cad-fusion.js +138 -0
- package/lib/tools/cad-image.js +121 -0
- package/lib/tools/cad-info.js +92 -0
- package/lib/tools/cad-model.js +816 -0
- package/lib/tools/cad-view.js +112 -0
- package/lib/tools/util.js +25 -0
- package/lib/types/b64.d.ts +8 -0
- package/lib/types/cad/builtin.d.ts +3 -0
- package/lib/types/cad/freecad.d.ts +3 -0
- package/lib/types/cad/fusion360.d.ts +3 -0
- package/lib/types/cad/gstarcad3d.d.ts +3 -0
- package/lib/types/cad/index.d.ts +8 -0
- package/lib/types/cad/onshape.d.ts +3 -0
- package/lib/types/cad/solidworks.d.ts +3 -0
- package/lib/types/cad/types.d.ts +21 -0
- package/lib/types/cad/zw3d.d.ts +3 -0
- package/lib/types/cad_connector/builtin-executor.d.ts +9 -0
- package/lib/types/cad_connector/builtin.d.ts +3 -0
- package/lib/types/cad_connector/executor.d.ts +69 -0
- package/lib/types/cad_connector/freecad-executor.d.ts +55 -0
- package/lib/types/cad_connector/freecad.d.ts +3 -0
- package/lib/types/cad_connector/fusion360-executor.d.ts +11 -0
- package/lib/types/cad_connector/fusion360.d.ts +3 -0
- package/lib/types/cad_connector/gstarcad3d.d.ts +3 -0
- package/lib/types/cad_connector/index.d.ts +8 -0
- package/lib/types/cad_connector/onshape.d.ts +3 -0
- package/lib/types/cad_connector/solidworks.d.ts +3 -0
- package/lib/types/cad_connector/types.d.ts +21 -0
- package/lib/types/cad_connector/zw3d.d.ts +3 -0
- package/lib/types/convert/dcprt.d.ts +14 -0
- package/lib/types/convert/detect.d.ts +7 -0
- package/lib/types/convert/dxf.d.ts +3 -0
- package/lib/types/convert/index.d.ts +9 -0
- package/lib/types/convert/obj.d.ts +7 -0
- package/lib/types/convert/png.d.ts +8 -0
- package/lib/types/convert/step.d.ts +4 -0
- package/lib/types/convert/stl.d.ts +7 -0
- package/lib/types/convert/svg.d.ts +8 -0
- package/lib/types/feature_script/dc_asm.d.ts +40 -0
- package/lib/types/feature_script/dc_engineering.d.ts +55 -0
- package/lib/types/feature_script/dc_prt.d.ts +37 -0
- package/lib/types/feature_script/index.d.ts +7 -0
- package/lib/types/image/profile.d.ts +28 -0
- package/lib/types/index.d.ts +18 -0
- package/lib/types/modeling/assembly.d.ts +17 -0
- package/lib/types/modeling/bin-format.d.ts +42 -0
- package/lib/types/modeling/bin-store.d.ts +20 -0
- package/lib/types/modeling/client.d.ts +125 -0
- package/lib/types/modeling/document.d.ts +26 -0
- package/lib/types/modeling/drawing.d.ts +38 -0
- package/lib/types/routes.d.ts +27 -0
- package/lib/types/store.d.ts +18 -0
- package/lib/types/tools/cad-freecad.d.ts +10 -0
- package/lib/types/tools/cad-fusion.d.ts +8 -0
- package/lib/types/tools/cad-image.d.ts +8 -0
- package/lib/types/tools/cad-info.d.ts +6 -0
- package/lib/types/tools/cad-model.d.ts +13 -0
- package/lib/types/tools/cad-view.d.ts +10 -0
- package/lib/types/tools/util.d.ts +4 -0
- package/lib/types/types.d.ts +142 -0
- package/lib/types.js +25 -0
- package/package.json +91 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/** Landscape paper in mm. */
|
|
2
|
+
const PAPER = {
|
|
3
|
+
A4: { width: 297, height: 210 },
|
|
4
|
+
A3: { width: 420, height: 297 },
|
|
5
|
+
};
|
|
6
|
+
const FRAME = 10; // inner frame inset from the paper edge
|
|
7
|
+
const GAP = 22; // breathing room between views
|
|
8
|
+
const TITLE_BLOCK = { width: 130, height: 28 };
|
|
9
|
+
/** GB-preferred scale series (values < 1 reduce, > 1 enlarge). */
|
|
10
|
+
const SCALE_SERIES = [5, 2, 1, 0.5, 0.4, 0.25, 0.2, 0.1, 0.05, 0.02, 0.01];
|
|
11
|
+
const COLORS = {
|
|
12
|
+
visible: 0x1a1d21,
|
|
13
|
+
hidden: 0x98a0ab,
|
|
14
|
+
dim: 0x2456a8,
|
|
15
|
+
paper: 0xb6bdc7,
|
|
16
|
+
text: 0x3d434c,
|
|
17
|
+
};
|
|
18
|
+
/** (x, y) bounds of a view's projected polylines; null when empty. */
|
|
19
|
+
function viewBounds(view) {
|
|
20
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
21
|
+
for (const polyline of [...view.visible, ...view.hidden]) {
|
|
22
|
+
for (let i = 0; i + 1 < polyline.length; i += 2) {
|
|
23
|
+
if (polyline[i] < minX)
|
|
24
|
+
minX = polyline[i];
|
|
25
|
+
if (polyline[i] > maxX)
|
|
26
|
+
maxX = polyline[i];
|
|
27
|
+
if (polyline[i + 1] < minY)
|
|
28
|
+
minY = polyline[i + 1];
|
|
29
|
+
if (polyline[i + 1] > maxY)
|
|
30
|
+
maxY = polyline[i + 1];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Number.isFinite(minX) ? { minX, minY, maxX, maxY } : null;
|
|
34
|
+
}
|
|
35
|
+
function sizeOf(bounds) {
|
|
36
|
+
return bounds === null ? { w: 0, h: 0 } : { w: bounds.maxX - bounds.minX, h: bounds.maxY - bounds.minY };
|
|
37
|
+
}
|
|
38
|
+
/** Largest GB scale that keeps `size` inside `cell`. */
|
|
39
|
+
function fitScale(viewW, viewH, cellW, cellH) {
|
|
40
|
+
if (viewW <= 1e-9 || viewH <= 1e-9)
|
|
41
|
+
return 1;
|
|
42
|
+
const raw = Math.min(cellW / viewW, cellH / viewH);
|
|
43
|
+
for (const scale of SCALE_SERIES) {
|
|
44
|
+
if (scale <= raw)
|
|
45
|
+
return scale;
|
|
46
|
+
}
|
|
47
|
+
return SCALE_SERIES[SCALE_SERIES.length - 1] ?? 0.01;
|
|
48
|
+
}
|
|
49
|
+
/** Scale a view's projected polylines (layout works in paper mm). */
|
|
50
|
+
function scaleView(view, s) {
|
|
51
|
+
if (s === 1)
|
|
52
|
+
return view;
|
|
53
|
+
const scalePoly = (polyline) => polyline.map((v) => v * s);
|
|
54
|
+
return { name: view.name, visible: view.visible.map(scalePoly), hidden: view.hidden.map(scalePoly) };
|
|
55
|
+
}
|
|
56
|
+
/** Translation that centers a (scaled) view on (ox, oy). */
|
|
57
|
+
function centerTransform(view, ox, oy) {
|
|
58
|
+
const bounds = viewBounds(view);
|
|
59
|
+
if (bounds === null)
|
|
60
|
+
return { tx: ox, ty: oy };
|
|
61
|
+
return { tx: ox - (bounds.minX + bounds.maxX) / 2, ty: oy - (bounds.minY + bounds.maxY) / 2 };
|
|
62
|
+
}
|
|
63
|
+
function trim(value) {
|
|
64
|
+
return Number(value.toFixed(1)).toString();
|
|
65
|
+
}
|
|
66
|
+
function scaleText(scale) {
|
|
67
|
+
return scale >= 1 ? `${trim(scale)}:1` : `1:${trim(1 / scale)}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A linear dimension: extension lines from the measure points, a dimension
|
|
71
|
+
* line offset perpendicular by `offset`, arrowhead strokes, and the measured
|
|
72
|
+
* text centered above the dimension line. `normal` is the side the dimension
|
|
73
|
+
* line sits on (unit vector).
|
|
74
|
+
*/
|
|
75
|
+
function linearDim(entities, p1, p2, normal, offset, text) {
|
|
76
|
+
const dx = p2[0] - p1[0];
|
|
77
|
+
const dy = p2[1] - p1[1];
|
|
78
|
+
const len = Math.hypot(dx, dy);
|
|
79
|
+
if (len < 1e-9)
|
|
80
|
+
return;
|
|
81
|
+
const ux = dx / len;
|
|
82
|
+
const uy = dy / len;
|
|
83
|
+
const [nx, ny] = normal;
|
|
84
|
+
const a1 = [p1[0] + nx * offset, p1[1] + ny * offset];
|
|
85
|
+
const a2 = [p2[0] + nx * offset, p2[1] + ny * offset];
|
|
86
|
+
const gap = 1.2;
|
|
87
|
+
const over = 2.5;
|
|
88
|
+
const extension = (p) => {
|
|
89
|
+
entities.push({
|
|
90
|
+
type: 'line',
|
|
91
|
+
x1: p[0] + nx * gap, y1: p[1] + ny * gap,
|
|
92
|
+
x2: p[0] + nx * (offset + over), y2: p[1] + ny * (offset + over),
|
|
93
|
+
layer: 'dim', color: COLORS.dim,
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
extension(p1);
|
|
97
|
+
extension(p2);
|
|
98
|
+
entities.push({ type: 'line', x1: a1[0], y1: a1[1], x2: a2[0], y2: a2[1], layer: 'dim', color: COLORS.dim });
|
|
99
|
+
// arrowheads: two strokes each, ~30° half angle, 3mm
|
|
100
|
+
for (const [at, sign] of [[a1, 1], [a2, -1]]) {
|
|
101
|
+
for (const side of [1, -1]) {
|
|
102
|
+
entities.push({
|
|
103
|
+
type: 'line',
|
|
104
|
+
x1: at[0], y1: at[1],
|
|
105
|
+
x2: at[0] + (ux * 0.87 + -uy * side * 0.5) * 3 * sign,
|
|
106
|
+
y2: at[1] + (uy * 0.87 + ux * side * 0.5) * 3 * sign,
|
|
107
|
+
layer: 'dim', color: COLORS.dim,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const mid = [(a1[0] + a2[0]) / 2 + nx * 1.6, (a1[1] + a2[1]) / 2 + ny * 1.6];
|
|
112
|
+
let rotation = (Math.atan2(uy, ux) * 180) / Math.PI;
|
|
113
|
+
if (rotation > 90 || rotation < -90)
|
|
114
|
+
rotation += 180;
|
|
115
|
+
entities.push({ type: 'text', x: mid[0], y: mid[1], text, height: 3.2, rotation, layer: 'dim', color: COLORS.dim });
|
|
116
|
+
}
|
|
117
|
+
/** Build the full drawing sheet (frame, title block, views, dimensions). */
|
|
118
|
+
export function buildDrawingSheet(input) {
|
|
119
|
+
const paper = PAPER[input.paper ?? 'A4'];
|
|
120
|
+
const { width, height } = paper;
|
|
121
|
+
const byName = new Map(input.views.map((view) => [view.name, view]));
|
|
122
|
+
const front = byName.get('front');
|
|
123
|
+
const top = byName.get('top');
|
|
124
|
+
const left = byName.get('left');
|
|
125
|
+
const iso = byName.get('iso');
|
|
126
|
+
// ── placements (dimensions collected into a side list, appended later) ────
|
|
127
|
+
const placed = [];
|
|
128
|
+
const dimEntities = [];
|
|
129
|
+
let scale = 1;
|
|
130
|
+
if (front !== undefined) {
|
|
131
|
+
const regionW = (width - FRAME * 2) * 0.64;
|
|
132
|
+
const regionH = height - FRAME * 2;
|
|
133
|
+
const cellW = (regionW - GAP) / 2;
|
|
134
|
+
const cellH = (regionH - GAP) / 2;
|
|
135
|
+
const pad = 20; // room for the dimension annotations inside each cell
|
|
136
|
+
const frontSize = sizeOf(viewBounds(front));
|
|
137
|
+
const leftSize = left !== undefined ? sizeOf(viewBounds(left)) : { w: 0, h: 0 };
|
|
138
|
+
const topSize = top !== undefined ? sizeOf(viewBounds(top)) : { w: 0, h: 0 };
|
|
139
|
+
const candidates = [fitScale(frontSize.w, frontSize.h, cellW - pad, cellH - pad)];
|
|
140
|
+
if (left !== undefined)
|
|
141
|
+
candidates.push(fitScale(leftSize.w, leftSize.h, cellW - pad, cellH - pad));
|
|
142
|
+
if (top !== undefined)
|
|
143
|
+
candidates.push(fitScale(topSize.w, topSize.h, cellW - pad, cellH - pad));
|
|
144
|
+
scale = Math.min(...candidates);
|
|
145
|
+
// GB first angle: 主视图 upper-left, 左视图 right of it, 俯视图 below it.
|
|
146
|
+
const rowY = FRAME + regionH - cellH / 2;
|
|
147
|
+
const frontCX = FRAME + cellW / 2 + 4;
|
|
148
|
+
const place = (raw, ox, oy) => {
|
|
149
|
+
const scaled = scaleView(raw, scale);
|
|
150
|
+
const { tx, ty } = centerTransform(scaled, ox, oy);
|
|
151
|
+
placed.push({ view: scaled, tx, ty });
|
|
152
|
+
};
|
|
153
|
+
place(front, frontCX, rowY);
|
|
154
|
+
if (left !== undefined)
|
|
155
|
+
place(left, FRAME + cellW * 1.5 + GAP, rowY);
|
|
156
|
+
if (top !== undefined)
|
|
157
|
+
place(top, frontCX, FRAME + cellH / 2);
|
|
158
|
+
// Overall dimensions off the scaled front/top geometry (mm on paper).
|
|
159
|
+
const fw = frontSize.w * scale;
|
|
160
|
+
const fh = frontSize.h * scale;
|
|
161
|
+
if (fw > 1e-9) {
|
|
162
|
+
linearDim(dimEntities, [frontCX - fw / 2, rowY - fh / 2], [frontCX + fw / 2, rowY - fh / 2], [0, -1], 9, trim(frontSize.w));
|
|
163
|
+
}
|
|
164
|
+
if (fh > 1e-9) {
|
|
165
|
+
linearDim(dimEntities, [frontCX - fw / 2, rowY - fh / 2], [frontCX - fw / 2, rowY + fh / 2], [-1, 0], 9, trim(frontSize.h));
|
|
166
|
+
}
|
|
167
|
+
if (top !== undefined && topSize.h > 1e-9) {
|
|
168
|
+
const topCY = FRAME + cellH / 2;
|
|
169
|
+
const tw = topSize.w * scale;
|
|
170
|
+
const td = topSize.h * scale;
|
|
171
|
+
linearDim(dimEntities, [frontCX + tw / 2, topCY - td / 2], [frontCX + tw / 2, topCY + td / 2], [1, 0], 9, trim(topSize.h));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Iso: fit the right free column above the title block, independent scale.
|
|
175
|
+
if (iso !== undefined) {
|
|
176
|
+
const isoLeft = FRAME + (width - FRAME * 2) * 0.64 + GAP / 2;
|
|
177
|
+
const isoBottom = FRAME + TITLE_BLOCK.height + GAP / 2;
|
|
178
|
+
const cellW = Math.max(width - FRAME - isoLeft, 40);
|
|
179
|
+
const cellH = Math.max(height - FRAME - isoBottom, 40);
|
|
180
|
+
const isoSize = sizeOf(viewBounds(iso));
|
|
181
|
+
const s = fitScale(isoSize.w, isoSize.h, cellW - 10, cellH - 10);
|
|
182
|
+
const scaled = scaleView(iso, s);
|
|
183
|
+
const { tx, ty } = centerTransform(scaled, isoLeft + cellW / 2, isoBottom + cellH / 2);
|
|
184
|
+
placed.push({ view: scaled, tx, ty });
|
|
185
|
+
}
|
|
186
|
+
// ── entities ───────────────────────────────────────────────────────────────
|
|
187
|
+
const entities = [];
|
|
188
|
+
const frameRect = (x, y, w, h) => {
|
|
189
|
+
entities.push({ type: 'polyline', points: [x, y, x + w, y, x + w, y + h, x, y + h], closed: true, layer: 'paper', color: COLORS.paper });
|
|
190
|
+
};
|
|
191
|
+
frameRect(2, 2, width - 4, height - 4);
|
|
192
|
+
frameRect(FRAME, FRAME, width - FRAME * 2, height - FRAME * 2);
|
|
193
|
+
const tbX = width - FRAME - TITLE_BLOCK.width;
|
|
194
|
+
const tbY = FRAME;
|
|
195
|
+
frameRect(tbX, tbY, TITLE_BLOCK.width, TITLE_BLOCK.height);
|
|
196
|
+
entities.push({ type: 'line', x1: tbX, y1: tbY + TITLE_BLOCK.height / 2, x2: tbX + TITLE_BLOCK.width, y2: tbY + TITLE_BLOCK.height / 2, layer: 'paper', color: COLORS.paper });
|
|
197
|
+
entities.push({ type: 'line', x1: tbX + 78, y1: tbY, x2: tbX + 78, y2: tbY + TITLE_BLOCK.height, layer: 'paper', color: COLORS.paper });
|
|
198
|
+
const titleText = (x, y, text, size = 3.2) => {
|
|
199
|
+
entities.push({ type: 'text', x, y, text, height: size, layer: 'paper', color: COLORS.text });
|
|
200
|
+
};
|
|
201
|
+
titleText(tbX + 4, tbY + TITLE_BLOCK.height - 8, input.partName, 4.2);
|
|
202
|
+
titleText(tbX + 4, tbY + 6, `图号 ${input.drawingNo ?? '—'}`);
|
|
203
|
+
titleText(tbX + 44, tbY + 6, `比例 ${scaleText(scale)}`);
|
|
204
|
+
titleText(tbX + 82, tbY + 6, '单位 mm');
|
|
205
|
+
titleText(tbX + 82, tbY + TITLE_BLOCK.height / 2 - 4, `日期 ${input.date ?? ''}`);
|
|
206
|
+
entities.push(...dimEntities);
|
|
207
|
+
for (const item of placed) {
|
|
208
|
+
for (const polyline of item.view.visible) {
|
|
209
|
+
entities.push({
|
|
210
|
+
type: 'polyline',
|
|
211
|
+
points: polyline.map((v, i) => v + (i % 2 === 0 ? item.tx : item.ty)),
|
|
212
|
+
closed: false, layer: 'visible', color: COLORS.visible,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
for (const polyline of item.view.hidden) {
|
|
216
|
+
entities.push({
|
|
217
|
+
type: 'polyline',
|
|
218
|
+
points: polyline.map((v, i) => v + (i % 2 === 0 ? item.tx : item.ty)),
|
|
219
|
+
closed: false, layer: 'hidden', color: COLORS.hidden,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
entities,
|
|
225
|
+
bounds: { min: { x: 0, y: 0 }, max: { x: width, y: height } },
|
|
226
|
+
layers: ['visible', 'hidden', 'dim', 'paper'],
|
|
227
|
+
width,
|
|
228
|
+
height,
|
|
229
|
+
scale,
|
|
230
|
+
entityCount: entities.length,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// ── export serializations ─────────────────────────────────────────────────────
|
|
234
|
+
const esc = (text) => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
235
|
+
/** Serialize the sheet to a standalone SVG (paper mm = user units, Y flipped). */
|
|
236
|
+
export function drawingToSvg(sheet) {
|
|
237
|
+
const { width, height, entities } = sheet;
|
|
238
|
+
const parts = [];
|
|
239
|
+
parts.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}mm" height="${height}mm">`);
|
|
240
|
+
parts.push(`<rect x="0" y="0" width="${width}" height="${height}" fill="#ffffff"/>`);
|
|
241
|
+
parts.push(`<g transform="translate(0,${height}) scale(1,-1)">`);
|
|
242
|
+
for (const entity of entities) {
|
|
243
|
+
switch (entity.type) {
|
|
244
|
+
case 'line':
|
|
245
|
+
parts.push(`<line x1="${entity.x1}" y1="${entity.y1}" x2="${entity.x2}" y2="${entity.y2}" stroke="${cssColor(entity)}" ${strokeStyle(entity)}/>`);
|
|
246
|
+
break;
|
|
247
|
+
case 'polyline': {
|
|
248
|
+
const points = [];
|
|
249
|
+
for (let i = 0; i + 1 < entity.points.length; i += 2)
|
|
250
|
+
points.push(`${entity.points[i]},${entity.points[i + 1]}`);
|
|
251
|
+
parts.push(`<polyline points="${points.join(' ')}" fill="none" stroke="${cssColor(entity)}" ${strokeStyle(entity)}/>`);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case 'text':
|
|
255
|
+
parts.push(`<text x="${entity.x}" y="${entity.y}" font-size="${entity.height}" fill="${cssColor(entity)}" ` +
|
|
256
|
+
`transform="translate(0,${2 * entity.y}) scale(1,-1)" font-family="sans-serif">${esc(entity.text)}</text>`);
|
|
257
|
+
break;
|
|
258
|
+
default:
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
parts.push('</g></svg>');
|
|
263
|
+
return parts.join('\n');
|
|
264
|
+
}
|
|
265
|
+
function cssColor(entity) {
|
|
266
|
+
const color = entity.color ?? 0x1a1d21;
|
|
267
|
+
return `#${color.toString(16).padStart(6, '0')}`;
|
|
268
|
+
}
|
|
269
|
+
function strokeStyle(entity) {
|
|
270
|
+
const width = entity.layer === 'hidden' || entity.layer === 'dim' ? 0.25 : 0.45;
|
|
271
|
+
const dash = entity.layer === 'hidden' ? ' stroke-dasharray="2,1.2"' : '';
|
|
272
|
+
return `stroke-width="${width}"${dash}`;
|
|
273
|
+
}
|
|
274
|
+
/** Minimal R12 ASCII DXF (LINE + TEXT entities; polylines expand to lines). */
|
|
275
|
+
export function drawingToDxf(sheet) {
|
|
276
|
+
const out = [];
|
|
277
|
+
const push = (code, value) => {
|
|
278
|
+
out.push(String(code), String(value));
|
|
279
|
+
};
|
|
280
|
+
push(0, 'SECTION');
|
|
281
|
+
push(2, 'ENTITIES');
|
|
282
|
+
for (const entity of sheet.entities) {
|
|
283
|
+
switch (entity.type) {
|
|
284
|
+
case 'line':
|
|
285
|
+
push(0, 'LINE');
|
|
286
|
+
push(8, entity.layer ?? '0');
|
|
287
|
+
push(10, entity.x1.toFixed(4));
|
|
288
|
+
push(20, entity.y1.toFixed(4));
|
|
289
|
+
push(30, '0');
|
|
290
|
+
push(11, entity.x2.toFixed(4));
|
|
291
|
+
push(21, entity.y2.toFixed(4));
|
|
292
|
+
push(31, '0');
|
|
293
|
+
break;
|
|
294
|
+
case 'polyline':
|
|
295
|
+
for (let i = 0; i + 3 < entity.points.length; i += 2) {
|
|
296
|
+
push(0, 'LINE');
|
|
297
|
+
push(8, entity.layer ?? '0');
|
|
298
|
+
push(10, entity.points[i].toFixed(4));
|
|
299
|
+
push(20, entity.points[i + 1].toFixed(4));
|
|
300
|
+
push(30, '0');
|
|
301
|
+
push(11, entity.points[i + 2].toFixed(4));
|
|
302
|
+
push(21, entity.points[i + 3].toFixed(4));
|
|
303
|
+
push(30, '0');
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
case 'text':
|
|
307
|
+
push(0, 'TEXT');
|
|
308
|
+
push(8, entity.layer ?? '0');
|
|
309
|
+
push(10, entity.x.toFixed(4));
|
|
310
|
+
push(20, entity.y.toFixed(4));
|
|
311
|
+
push(30, '0');
|
|
312
|
+
push(40, entity.height.toFixed(3));
|
|
313
|
+
push(1, entity.text);
|
|
314
|
+
if (entity.rotation !== undefined)
|
|
315
|
+
push(50, entity.rotation.toFixed(2));
|
|
316
|
+
break;
|
|
317
|
+
default:
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
push(0, 'ENDSEC');
|
|
322
|
+
push(0, 'EOF');
|
|
323
|
+
return out.join('\n');
|
|
324
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh-based hidden-line-removal projection (plain CJS — runs inside the
|
|
3
|
+
* modeling worker). The opencascade.js build exposes no HLRBRep bindings, so
|
|
4
|
+
* engineering-drawing views are computed from the tessellation instead:
|
|
5
|
+
*
|
|
6
|
+
* 1. candidate edges = sharp feature edges (dihedral > threshold, view
|
|
7
|
+
* independent) ∪ silhouette edges (one adjacent face front-facing, one
|
|
8
|
+
* back-facing — per view), which is what traces curved outlines;
|
|
9
|
+
* 2. each candidate is occlusion-tested against the projected triangle soup
|
|
10
|
+
* via a uniform 2D grid (depth-interpolated barycentric coverage);
|
|
11
|
+
* 3. surviving edges are chained into polylines and simplified (RDP), so
|
|
12
|
+
* the caller gets compact flat [x,y, x,y, …] paths per visibility class.
|
|
13
|
+
*
|
|
14
|
+
* Projected coordinates stay in model units (mm): u along the view's xDir,
|
|
15
|
+
* v = w × u (w = view direction toward the observer), so screen y is "up".
|
|
16
|
+
*/
|
|
17
|
+
'use strict'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the edge table keyed by quantized endpoint COORDINATES (not vertex
|
|
21
|
+
* indices): OCCT triangulations are per-face, so the same topological edge
|
|
22
|
+
* appears once per adjacent face under different indices — only coordinates
|
|
23
|
+
* merge them. Values accumulate adjacent face normals for the dihedral test.
|
|
24
|
+
*/
|
|
25
|
+
function edgeTable(positions, indices) {
|
|
26
|
+
const q = (v) => Math.round(v * 1e4)
|
|
27
|
+
const edges = new Map()
|
|
28
|
+
const triCount = indices.length / 3
|
|
29
|
+
for (let t = 0; t < triCount; t++) {
|
|
30
|
+
const a = indices[t * 3]
|
|
31
|
+
const b = indices[t * 3 + 1]
|
|
32
|
+
const c = indices[t * 3 + 2]
|
|
33
|
+
const ax = positions[a * 3], ay = positions[a * 3 + 1], az = positions[a * 3 + 2]
|
|
34
|
+
const bx = positions[b * 3], by = positions[b * 3 + 1], bz = positions[b * 3 + 2]
|
|
35
|
+
const cx = positions[c * 3], cy = positions[c * 3 + 1], cz = positions[c * 3 + 2]
|
|
36
|
+
// Face normal (normalized; the winding is the orientation source of truth).
|
|
37
|
+
const ux = bx - ax, uy = by - ay, uz = bz - az
|
|
38
|
+
const vx = cx - ax, vy = cy - ay, vz = cz - az
|
|
39
|
+
let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx
|
|
40
|
+
const len = Math.hypot(nx, ny, nz) || 1
|
|
41
|
+
nx /= len; ny /= len; nz /= len
|
|
42
|
+
for (const [i, j] of [[a, b], [b, c], [c, a]]) {
|
|
43
|
+
const ka = `${q(positions[i * 3])},${q(positions[i * 3 + 1])},${q(positions[i * 3 + 2])}`
|
|
44
|
+
const kb = `${q(positions[j * 3])},${q(positions[j * 3 + 1])},${q(positions[j * 3 + 2])}`
|
|
45
|
+
const key = ka < kb ? `${ka};${kb}` : `${kb};${ka}`
|
|
46
|
+
let entry = edges.get(key)
|
|
47
|
+
if (entry === undefined) {
|
|
48
|
+
entry = { normals: [], x1: positions[i * 3], y1: positions[i * 3 + 1], z1: positions[i * 3 + 2], x2: positions[j * 3], y2: positions[j * 3 + 1], z2: positions[j * 3 + 2] }
|
|
49
|
+
edges.set(key, entry)
|
|
50
|
+
}
|
|
51
|
+
entry.normals.push([nx, ny, nz])
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return edges
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cosThreshold = (angleDeg) => Math.cos((angleDeg * Math.PI) / 180)
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sharp + boundary feature edges, view independent. Returns flat segments
|
|
61
|
+
* [x1,y1,z1, x2,y2,z2].
|
|
62
|
+
*/
|
|
63
|
+
function featureEdges(positions, indices, angleDeg = 25) {
|
|
64
|
+
return featureEdgesFrom(edgeTable(positions, indices), angleDeg)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function featureEdgesFrom(table, angleDeg = 25) {
|
|
68
|
+
const cosMin = cosThreshold(angleDeg)
|
|
69
|
+
const segments = []
|
|
70
|
+
for (const entry of table.values()) {
|
|
71
|
+
if (entry.normals.length === 1) {
|
|
72
|
+
// Boundary edge (open mesh) — always a candidate.
|
|
73
|
+
} else if (entry.normals.length === 2) {
|
|
74
|
+
const [n1, n2] = entry.normals
|
|
75
|
+
const dot = n1[0] * n2[0] + n1[1] * n2[1] + n1[2] * n2[2]
|
|
76
|
+
if (dot > cosMin) continue // smooth — not a feature edge
|
|
77
|
+
} else {
|
|
78
|
+
continue // non-manifold star: skip
|
|
79
|
+
}
|
|
80
|
+
segments.push(entry.x1, entry.y1, entry.z1, entry.x2, entry.y2, entry.z2)
|
|
81
|
+
}
|
|
82
|
+
return segments
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Silhouette edges for one view: adjacent faces straddle the view plane. */
|
|
86
|
+
function silhouetteEdges(positions, indices, dir) {
|
|
87
|
+
return silhouetteEdgesFrom(edgeTable(positions, indices), dir)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function silhouetteEdgesFrom(table, dir) {
|
|
91
|
+
const segments = []
|
|
92
|
+
for (const entry of table.values()) {
|
|
93
|
+
if (entry.normals.length !== 2) continue // boundary handled by the sharp pass
|
|
94
|
+
const [n1, n2] = entry.normals
|
|
95
|
+
const d1 = n1[0] * dir[0] + n1[1] * dir[1] + n1[2] * dir[2]
|
|
96
|
+
const d2 = n2[0] * dir[0] + n2[1] * dir[1] + n2[2] * dir[2]
|
|
97
|
+
if (d1 * d2 < 0) segments.push(entry.x1, entry.y1, entry.z1, entry.x2, entry.y2, entry.z2)
|
|
98
|
+
}
|
|
99
|
+
return segments
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Occlusion tester over the projected triangle soup (uniform grid). */
|
|
103
|
+
function createOccluderGrid(px, py, depth, indices, cellCount) {
|
|
104
|
+
const triCount = indices.length / 3
|
|
105
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
|
|
106
|
+
for (let i = 0; i < px.length; i++) {
|
|
107
|
+
if (px[i] < minX) minX = px[i]
|
|
108
|
+
if (px[i] > maxX) maxX = px[i]
|
|
109
|
+
if (py[i] < minY) minY = py[i]
|
|
110
|
+
if (py[i] > maxY) maxY = py[i]
|
|
111
|
+
}
|
|
112
|
+
const extent = Math.max(maxX - minX, maxY - minY, 1e-9)
|
|
113
|
+
const cell = extent / cellCount
|
|
114
|
+
const grid = new Map()
|
|
115
|
+
const gridKey = (gx, gy) => gx * 4096 + gy
|
|
116
|
+
for (let t = 0; t < triCount; t++) {
|
|
117
|
+
const a = indices[t * 3] * 1, b = indices[t * 3 + 1], c = indices[t * 3 + 2]
|
|
118
|
+
const x0 = px[a], y0 = py[a], x1 = px[b], y1 = py[b], x2 = px[c], y2 = py[c]
|
|
119
|
+
const loX = Math.floor((Math.min(x0, x1, x2) - minX) / cell)
|
|
120
|
+
const hiX = Math.floor((Math.max(x0, x1, x2) - minX) / cell)
|
|
121
|
+
const loY = Math.floor((Math.min(y0, y1, y2) - minY) / cell)
|
|
122
|
+
const hiY = Math.floor((Math.max(y0, y1, y2) - minY) / cell)
|
|
123
|
+
for (let gy = loY; gy <= hiY; gy++) {
|
|
124
|
+
for (let gx = loX; gx <= hiX; gx++) {
|
|
125
|
+
const key = gridKey(gx, gy)
|
|
126
|
+
const bucket = grid.get(key)
|
|
127
|
+
if (bucket === undefined) grid.set(key, [t])
|
|
128
|
+
else bucket.push(t)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const EPS = extent * 1e-9
|
|
133
|
+
// Strict-interior margin: coverage only counts when the sample is a real
|
|
134
|
+
// distance inside the projected triangle. Orthographic projections make
|
|
135
|
+
// back faces share their boundary with the silhouette outline exactly —
|
|
136
|
+
// boundary-touching coverage must not hide the outline itself.
|
|
137
|
+
const MARGIN = 5e-3
|
|
138
|
+
/**
|
|
139
|
+
* Is (x, y) at depth `d` hidden? A triangle covers it when the projected
|
|
140
|
+
* point lies strictly inside and the interpolated depth is nearer by more
|
|
141
|
+
* than `bias` (bias ≈ half the tessellation chord sag to keep edges lying
|
|
142
|
+
* ON their own faces visible).
|
|
143
|
+
*/
|
|
144
|
+
return (x, y, d, bias) => {
|
|
145
|
+
const gx = Math.floor((x - minX) / cell)
|
|
146
|
+
const gy = Math.floor((y - minY) / cell)
|
|
147
|
+
const bucket = grid.get(gridKey(gx, gy))
|
|
148
|
+
if (bucket === undefined) return false
|
|
149
|
+
for (const t of bucket) {
|
|
150
|
+
const a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]
|
|
151
|
+
const x0 = px[a], y0 = py[a], x1 = px[b], y1 = py[b], x2 = px[c], y2 = py[c]
|
|
152
|
+
const area = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)
|
|
153
|
+
if (Math.abs(area) < EPS) continue
|
|
154
|
+
const w0 = ((x1 - x) * (y2 - y) - (x2 - x) * (y1 - y)) / area
|
|
155
|
+
const w1 = ((x2 - x) * (y0 - y) - (x0 - x) * (y2 - y)) / area
|
|
156
|
+
const w2 = 1 - w0 - w1
|
|
157
|
+
if (w0 < MARGIN || w1 < MARGIN || w2 < MARGIN) continue
|
|
158
|
+
const frontDepth = w0 * depth[a] + w1 * depth[b] + w2 * depth[c]
|
|
159
|
+
if (frontDepth > d + bias) return true
|
|
160
|
+
}
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Ramer–Douglas–Peucker on a flat [x,y,…] polyline. */
|
|
166
|
+
function simplify(points, epsilon) {
|
|
167
|
+
const n = points.length / 2
|
|
168
|
+
if (n < 3) return points
|
|
169
|
+
const keep = new Uint8Array(n)
|
|
170
|
+
keep[0] = 1
|
|
171
|
+
keep[n - 1] = 1
|
|
172
|
+
const stack = [[0, n - 1]]
|
|
173
|
+
while (stack.length > 0) {
|
|
174
|
+
const [first, last] = stack.pop()
|
|
175
|
+
const x1 = points[first * 2], y1 = points[first * 2 + 1]
|
|
176
|
+
const x2 = points[last * 2], y2 = points[last * 2 + 1]
|
|
177
|
+
const dx = x2 - x1, dy = y2 - y1
|
|
178
|
+
const len = Math.hypot(dx, dy)
|
|
179
|
+
let maxDist = 0
|
|
180
|
+
let index = -1
|
|
181
|
+
for (let i = first + 1; i < last; i++) {
|
|
182
|
+
const px = points[i * 2], py = points[i * 2 + 1]
|
|
183
|
+
const dist = len === 0 ? Math.hypot(px - x1, py - y1) : Math.abs(dy * px - dx * py + x2 * y1 - y2 * x1) / len
|
|
184
|
+
if (dist > maxDist) { maxDist = dist; index = i }
|
|
185
|
+
}
|
|
186
|
+
if (maxDist > epsilon && index > 0) {
|
|
187
|
+
keep[index] = 1
|
|
188
|
+
stack.push([first, index], [index, last])
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const out = []
|
|
192
|
+
for (let i = 0; i < n; i++) {
|
|
193
|
+
if (keep[i]) { out.push(points[i * 2], points[i * 2 + 1]) }
|
|
194
|
+
}
|
|
195
|
+
return out
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Chain collinear-adjacent segments ([x1,y1,x2,y2] tuples) into polylines. */
|
|
199
|
+
function chainSegments(segments) {
|
|
200
|
+
const key = (x, y) => `${Math.round(x * 1000)}|${Math.round(y * 1000)}`
|
|
201
|
+
const map = new Map()
|
|
202
|
+
segments.forEach((seg, s) => {
|
|
203
|
+
const [x1, y1, x2, y2] = seg
|
|
204
|
+
for (const [k, end] of [[key(x1, y1), 0], [key(x2, y2), 1]]) {
|
|
205
|
+
let list = map.get(k)
|
|
206
|
+
if (list === undefined) { list = []; map.set(k, list) }
|
|
207
|
+
list.push({ s, end })
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
const used = new Uint8Array(segments.length)
|
|
211
|
+
const chains = []
|
|
212
|
+
for (let s = 0; s < segments.length; s++) {
|
|
213
|
+
if (used[s]) continue
|
|
214
|
+
used[s] = 1
|
|
215
|
+
const chain = [segments[s][0], segments[s][1], segments[s][2], segments[s][3]]
|
|
216
|
+
// Extend forward from the tail, then backward from the head.
|
|
217
|
+
for (const direction of [1, -1]) {
|
|
218
|
+
for (;;) {
|
|
219
|
+
const tailX = direction === 1 ? chain[chain.length - 2] : chain[0]
|
|
220
|
+
const tailY = direction === 1 ? chain[chain.length - 1] : chain[1]
|
|
221
|
+
const candidates = map.get(key(tailX, tailY)) ?? []
|
|
222
|
+
let found = null
|
|
223
|
+
for (const candidate of candidates) {
|
|
224
|
+
if (used[candidate.s]) continue
|
|
225
|
+
found = candidate
|
|
226
|
+
break
|
|
227
|
+
}
|
|
228
|
+
if (found === null) break
|
|
229
|
+
used[found.s] = 1
|
|
230
|
+
const [ex1, ey1, ex2, ey2] = segments[found.s]
|
|
231
|
+
// Append the far end oriented away from the join point.
|
|
232
|
+
const joinIsStart = key(ex1, ey1) === key(tailX, tailY)
|
|
233
|
+
const px = joinIsStart ? ex2 : ex1
|
|
234
|
+
const py = joinIsStart ? ey2 : ey1
|
|
235
|
+
if (direction === 1) chain.push(px, py)
|
|
236
|
+
else chain.unshift(px, py)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
chains.push(chain)
|
|
240
|
+
}
|
|
241
|
+
return chains
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Project one tessellated body into drawing views.
|
|
246
|
+
* views: [{ name, dir:[x,y,z] toward observer, xDir:[x,y,z] screen-right }].
|
|
247
|
+
* Returns { views: [{ name, visible: number[][] (flat polylines), hidden: number[][] }] }.
|
|
248
|
+
*/
|
|
249
|
+
function projectViews({ positions, indices }, views, options = {}) {
|
|
250
|
+
const sharpAngle = options.sharpAngle ?? 25
|
|
251
|
+
const table = edgeTable(positions, indices)
|
|
252
|
+
const sharp = featureEdgesFrom(table, sharpAngle)
|
|
253
|
+
const triCount = indices.length / 3
|
|
254
|
+
const modelScale = (() => {
|
|
255
|
+
let min = Infinity, max = -Infinity
|
|
256
|
+
for (let i = 0; i < positions.length; i++) {
|
|
257
|
+
if (positions[i] < min) min = positions[i]
|
|
258
|
+
if (positions[i] > max) max = positions[i]
|
|
259
|
+
}
|
|
260
|
+
return Math.max(max - min, 1e-9)
|
|
261
|
+
})()
|
|
262
|
+
const bias = modelScale * (options.biasRatio ?? 0.002)
|
|
263
|
+
const simplifyEps = modelScale * (options.simplifyRatio ?? 0.0012)
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
views: views.map((view) => {
|
|
267
|
+
const w = normalize(view.dir)
|
|
268
|
+
let u = normalize(view.xDir)
|
|
269
|
+
if (Math.abs(dot(u, w)) > 0.999) u = [1, 0, 0] // degenerate xDir fallback
|
|
270
|
+
const v = normalize(cross(w, u))
|
|
271
|
+
const count = positions.length / 3
|
|
272
|
+
const px = new Float64Array(count)
|
|
273
|
+
const py = new Float64Array(count)
|
|
274
|
+
const depth = new Float64Array(count)
|
|
275
|
+
for (let i = 0; i < count; i++) {
|
|
276
|
+
const x = positions[i * 3], y = positions[i * 3 + 1], z = positions[i * 3 + 2]
|
|
277
|
+
px[i] = x * u[0] + y * u[1] + z * u[2]
|
|
278
|
+
py[i] = x * v[0] + y * v[1] + z * v[2]
|
|
279
|
+
depth[i] = x * w[0] + y * w[1] + z * w[2]
|
|
280
|
+
}
|
|
281
|
+
const occluded = createOccluderGrid(px, py, depth, indices, 48)
|
|
282
|
+
const candidates = [...sharp, ...silhouetteEdgesFrom(table, w)]
|
|
283
|
+
const visibleSegments = []
|
|
284
|
+
const hiddenSegments = []
|
|
285
|
+
for (let s = 0; s < candidates.length / 6; s++) {
|
|
286
|
+
const x1 = candidates[s * 6], y1 = candidates[s * 6 + 1], z1 = candidates[s * 6 + 2]
|
|
287
|
+
const x2 = candidates[s * 6 + 3], y2 = candidates[s * 6 + 4], z2 = candidates[s * 6 + 5]
|
|
288
|
+
const p1x = x1 * u[0] + y1 * u[1] + z1 * u[2]
|
|
289
|
+
const p1y = x1 * v[0] + y1 * v[1] + z1 * v[2]
|
|
290
|
+
const p2x = x2 * u[0] + y2 * u[1] + z2 * u[2]
|
|
291
|
+
const p2y = x2 * v[0] + y2 * v[1] + z2 * v[2]
|
|
292
|
+
if (Math.abs(p1x - p2x) < 1e-9 && Math.abs(p1y - p2y) < 1e-9) continue
|
|
293
|
+
const d1 = x1 * w[0] + y1 * w[1] + z1 * w[2]
|
|
294
|
+
const d2 = x2 * w[0] + y2 * w[1] + z2 * w[2]
|
|
295
|
+
// Three interior samples, majority vote.
|
|
296
|
+
let hits = 0
|
|
297
|
+
for (const t of [0.25, 0.5, 0.75]) {
|
|
298
|
+
const sx = p1x + (p2x - p1x) * t
|
|
299
|
+
const sy = p1y + (p2y - p1y) * t
|
|
300
|
+
const sd = d1 + (d2 - d1) * t
|
|
301
|
+
if (occluded(sx, sy, sd, bias)) hits++
|
|
302
|
+
}
|
|
303
|
+
const target = hits >= 2 ? hiddenSegments : visibleSegments
|
|
304
|
+
target.push([p1x, p1y, p2x, p2y])
|
|
305
|
+
}
|
|
306
|
+
const unwrap = (segments) =>
|
|
307
|
+
chainSegments(segments)
|
|
308
|
+
.map((chain) => simplify(chain, simplifyEps))
|
|
309
|
+
.filter((chain) => chain.length >= 4)
|
|
310
|
+
return { name: view.name, visible: unwrap(visibleSegments), hidden: unwrap(hiddenSegments) }
|
|
311
|
+
}),
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function normalize(v) {
|
|
316
|
+
const len = Math.hypot(v[0], v[1], v[2]) || 1
|
|
317
|
+
return [v[0] / len, v[1] / len, v[2] / len]
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function cross(a, b) {
|
|
321
|
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function dot(a, b) {
|
|
325
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = { featureEdges, silhouetteEdges, projectViews }
|