@genart-dev/plugin-construction 0.1.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/README.md +263 -0
- package/dist/index.cjs +2900 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +211 -0
- package/dist/index.d.ts +211 -0
- package/dist/index.js +2842 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2842 @@
|
|
|
1
|
+
// src/shared.ts
|
|
2
|
+
var COMMON_GUIDE_PROPERTIES = [
|
|
3
|
+
{
|
|
4
|
+
key: "guideColor",
|
|
5
|
+
label: "Guide Color",
|
|
6
|
+
type: "color",
|
|
7
|
+
default: "rgba(0,200,255,0.5)",
|
|
8
|
+
group: "style"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
key: "lineWidth",
|
|
12
|
+
label: "Line Width",
|
|
13
|
+
type: "number",
|
|
14
|
+
default: 1,
|
|
15
|
+
min: 0.5,
|
|
16
|
+
max: 5,
|
|
17
|
+
step: 0.5,
|
|
18
|
+
group: "style"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: "dashPattern",
|
|
22
|
+
label: "Dash Pattern",
|
|
23
|
+
type: "string",
|
|
24
|
+
default: "6,4",
|
|
25
|
+
group: "style"
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
function setupGuideStyle(ctx, color, lineWidth, dashPattern) {
|
|
29
|
+
ctx.strokeStyle = color;
|
|
30
|
+
ctx.lineWidth = lineWidth;
|
|
31
|
+
const dashes = dashPattern.split(",").map(Number).filter((n) => !isNaN(n) && n > 0);
|
|
32
|
+
ctx.setLineDash(dashes.length > 0 ? dashes : [6, 4]);
|
|
33
|
+
}
|
|
34
|
+
function drawLine(ctx, x1, y1, x2, y2) {
|
|
35
|
+
ctx.beginPath();
|
|
36
|
+
ctx.moveTo(x1, y1);
|
|
37
|
+
ctx.lineTo(x2, y2);
|
|
38
|
+
ctx.stroke();
|
|
39
|
+
}
|
|
40
|
+
function drawDashedLine(ctx, x1, y1, x2, y2, dash) {
|
|
41
|
+
const saved = ctx.getLineDash();
|
|
42
|
+
ctx.setLineDash(dash);
|
|
43
|
+
drawLine(ctx, x1, y1, x2, y2);
|
|
44
|
+
ctx.setLineDash(saved);
|
|
45
|
+
}
|
|
46
|
+
function drawPolyline(ctx, points, close = false) {
|
|
47
|
+
if (points.length < 2) return;
|
|
48
|
+
ctx.beginPath();
|
|
49
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
50
|
+
for (let i = 1; i < points.length; i++) {
|
|
51
|
+
ctx.lineTo(points[i].x, points[i].y);
|
|
52
|
+
}
|
|
53
|
+
if (close) ctx.closePath();
|
|
54
|
+
ctx.stroke();
|
|
55
|
+
}
|
|
56
|
+
function fillPolyline(ctx, points, fillStyle) {
|
|
57
|
+
if (points.length < 3) return;
|
|
58
|
+
ctx.beginPath();
|
|
59
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
60
|
+
for (let i = 1; i < points.length; i++) {
|
|
61
|
+
ctx.lineTo(points[i].x, points[i].y);
|
|
62
|
+
}
|
|
63
|
+
ctx.closePath();
|
|
64
|
+
ctx.fillStyle = fillStyle;
|
|
65
|
+
ctx.fill();
|
|
66
|
+
}
|
|
67
|
+
function drawLabel(ctx, text, x, y, color, fontSize = 10) {
|
|
68
|
+
ctx.fillStyle = color;
|
|
69
|
+
ctx.font = `${fontSize}px sans-serif`;
|
|
70
|
+
ctx.textAlign = "center";
|
|
71
|
+
ctx.textBaseline = "middle";
|
|
72
|
+
ctx.fillText(text, x, y);
|
|
73
|
+
}
|
|
74
|
+
function toPixel(norm, bounds) {
|
|
75
|
+
return {
|
|
76
|
+
x: bounds.x + norm.x * bounds.width,
|
|
77
|
+
y: bounds.y + norm.y * bounds.height
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function parseJSON(json, fallback) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(json);
|
|
83
|
+
} catch {
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function parseCSVColors(csv, count) {
|
|
88
|
+
const parts = csv.split(",").map((s) => s.trim());
|
|
89
|
+
const result = [];
|
|
90
|
+
const defaults = ["red", "green", "blue"];
|
|
91
|
+
for (let i = 0; i < count; i++) {
|
|
92
|
+
result.push(parts[i] || defaults[i % defaults.length]);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
function applyHiddenEdgeStyle(ctx, style, alpha) {
|
|
97
|
+
switch (style) {
|
|
98
|
+
case "dashed":
|
|
99
|
+
ctx.setLineDash([6, 4]);
|
|
100
|
+
ctx.globalAlpha = alpha;
|
|
101
|
+
break;
|
|
102
|
+
case "dotted":
|
|
103
|
+
ctx.setLineDash([2, 3]);
|
|
104
|
+
ctx.globalAlpha = alpha;
|
|
105
|
+
break;
|
|
106
|
+
case "faint":
|
|
107
|
+
ctx.setLineDash([]);
|
|
108
|
+
ctx.globalAlpha = alpha * 0.5;
|
|
109
|
+
break;
|
|
110
|
+
case "hidden":
|
|
111
|
+
ctx.globalAlpha = 0;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function resetEdgeStyle(ctx, savedAlpha) {
|
|
116
|
+
ctx.setLineDash([]);
|
|
117
|
+
ctx.globalAlpha = savedAlpha;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/math/rotation.ts
|
|
121
|
+
var DEG2RAD = Math.PI / 180;
|
|
122
|
+
function clamp(v, min, max) {
|
|
123
|
+
return v < min ? min : v > max ? max : v;
|
|
124
|
+
}
|
|
125
|
+
function rotationMatrix(rxDeg, ryDeg, rzDeg) {
|
|
126
|
+
const rx = clamp(rxDeg, -90, 90) * DEG2RAD;
|
|
127
|
+
const ry = ryDeg * DEG2RAD;
|
|
128
|
+
const rz = rzDeg * DEG2RAD;
|
|
129
|
+
const cx = Math.cos(rx), sx = Math.sin(rx);
|
|
130
|
+
const cy = Math.cos(ry), sy = Math.sin(ry);
|
|
131
|
+
const cz = Math.cos(rz), sz = Math.sin(rz);
|
|
132
|
+
return [
|
|
133
|
+
cz * cy,
|
|
134
|
+
cz * sy * sx - sz * cx,
|
|
135
|
+
cz * sy * cx + sz * sx,
|
|
136
|
+
sz * cy,
|
|
137
|
+
sz * sy * sx + cz * cx,
|
|
138
|
+
sz * sy * cx - cz * sx,
|
|
139
|
+
-sy,
|
|
140
|
+
cy * sx,
|
|
141
|
+
cy * cx
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
function rotate3D(point, m) {
|
|
145
|
+
return {
|
|
146
|
+
x: m[0] * point.x + m[1] * point.y + m[2] * point.z,
|
|
147
|
+
y: m[3] * point.x + m[4] * point.y + m[5] * point.z,
|
|
148
|
+
z: m[6] * point.x + m[7] * point.y + m[8] * point.z
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function project(point, projection = "orthographic", focalLength = 5) {
|
|
152
|
+
if (projection === "weak-perspective") {
|
|
153
|
+
const scale = focalLength / (focalLength + point.z);
|
|
154
|
+
return { x: point.x * scale, y: point.y * scale };
|
|
155
|
+
}
|
|
156
|
+
return { x: point.x, y: point.y };
|
|
157
|
+
}
|
|
158
|
+
function transformPoint(point, rxDeg, ryDeg, rzDeg, projection = "orthographic", focalLength = 5) {
|
|
159
|
+
const m = rotationMatrix(rxDeg, ryDeg, rzDeg);
|
|
160
|
+
const rotated = rotate3D(point, m);
|
|
161
|
+
return project(rotated, projection, focalLength);
|
|
162
|
+
}
|
|
163
|
+
function transformedNormalZ(normal, m) {
|
|
164
|
+
return m[6] * normal.x + m[7] * normal.y + m[8] * normal.z;
|
|
165
|
+
}
|
|
166
|
+
function identityMatrix() {
|
|
167
|
+
return [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
168
|
+
}
|
|
169
|
+
function multiplyMat3(a, b) {
|
|
170
|
+
return [
|
|
171
|
+
a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
|
|
172
|
+
a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
|
|
173
|
+
a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
|
|
174
|
+
a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
|
|
175
|
+
a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
|
|
176
|
+
a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
|
|
177
|
+
a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
|
|
178
|
+
a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
|
|
179
|
+
a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
function normalize3(v) {
|
|
183
|
+
const len = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
|
|
184
|
+
if (len < 1e-10) return { x: 0, y: 0, z: 0 };
|
|
185
|
+
return { x: v.x / len, y: v.y / len, z: v.z / len };
|
|
186
|
+
}
|
|
187
|
+
function dot3(a, b) {
|
|
188
|
+
return a.x * b.x + a.y * b.y + a.z * b.z;
|
|
189
|
+
}
|
|
190
|
+
function cross3(a, b) {
|
|
191
|
+
return {
|
|
192
|
+
x: a.y * b.z - a.z * b.y,
|
|
193
|
+
y: a.z * b.x - a.x * b.z,
|
|
194
|
+
z: a.x * b.y - a.y * b.x
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function dist2(a, b) {
|
|
198
|
+
const dx = b.x - a.x;
|
|
199
|
+
const dy = b.y - a.y;
|
|
200
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
201
|
+
}
|
|
202
|
+
function lerp2(a, b, t) {
|
|
203
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/math/ellipse.ts
|
|
207
|
+
var DEG2RAD2 = Math.PI / 180;
|
|
208
|
+
function projectedEllipse(center, radius, normalTilt, axisRotation) {
|
|
209
|
+
const tiltRad = normalTilt * DEG2RAD2;
|
|
210
|
+
const minor = radius * Math.abs(Math.cos(tiltRad));
|
|
211
|
+
return {
|
|
212
|
+
cx: center.x,
|
|
213
|
+
cy: center.y,
|
|
214
|
+
rx: radius,
|
|
215
|
+
ry: minor,
|
|
216
|
+
rotation: axisRotation * DEG2RAD2
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function drawEllipse(ctx, cx, cy, rx, ry, rotation, startAngle = 0, endAngle = Math.PI * 2, counterclockwise = false) {
|
|
220
|
+
ctx.beginPath();
|
|
221
|
+
ctx.ellipse(cx, cy, Math.abs(rx), Math.abs(ry), rotation, startAngle, endAngle, counterclockwise);
|
|
222
|
+
ctx.stroke();
|
|
223
|
+
}
|
|
224
|
+
function drawEllipseWithHidden(ctx, params, frontHalf, hiddenAlpha, hiddenDash) {
|
|
225
|
+
const { cx, cy, rx, ry, rotation } = params;
|
|
226
|
+
if (rx < 0.5 || ry < 0.5) return;
|
|
227
|
+
const savedAlpha = ctx.globalAlpha;
|
|
228
|
+
const savedDash = ctx.getLineDash();
|
|
229
|
+
ctx.beginPath();
|
|
230
|
+
ctx.ellipse(cx, cy, rx, ry, rotation, frontHalf[0], frontHalf[1], false);
|
|
231
|
+
ctx.stroke();
|
|
232
|
+
ctx.globalAlpha = hiddenAlpha;
|
|
233
|
+
ctx.setLineDash(hiddenDash);
|
|
234
|
+
ctx.beginPath();
|
|
235
|
+
ctx.ellipse(cx, cy, rx, ry, rotation, frontHalf[1], frontHalf[0] + Math.PI * 2, false);
|
|
236
|
+
ctx.stroke();
|
|
237
|
+
ctx.globalAlpha = savedAlpha;
|
|
238
|
+
ctx.setLineDash(savedDash);
|
|
239
|
+
}
|
|
240
|
+
function ellipsePoints(params, segments, startAngle = 0, endAngle = Math.PI * 2) {
|
|
241
|
+
const { cx, cy, rx, ry, rotation } = params;
|
|
242
|
+
const cos = Math.cos(rotation);
|
|
243
|
+
const sin = Math.sin(rotation);
|
|
244
|
+
const points = [];
|
|
245
|
+
for (let i = 0; i <= segments; i++) {
|
|
246
|
+
const t = i / segments;
|
|
247
|
+
const angle = startAngle + (endAngle - startAngle) * t;
|
|
248
|
+
const px = rx * Math.cos(angle);
|
|
249
|
+
const py = ry * Math.sin(angle);
|
|
250
|
+
points.push({
|
|
251
|
+
x: cx + px * cos - py * sin,
|
|
252
|
+
y: cy + px * sin + py * cos
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return points;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/forms/box.ts
|
|
259
|
+
var BOX_VERTICES = [
|
|
260
|
+
{ x: -0.5, y: -0.5, z: -0.5 },
|
|
261
|
+
// 0: left-bottom-back
|
|
262
|
+
{ x: 0.5, y: -0.5, z: -0.5 },
|
|
263
|
+
// 1: right-bottom-back
|
|
264
|
+
{ x: 0.5, y: 0.5, z: -0.5 },
|
|
265
|
+
// 2: right-top-back
|
|
266
|
+
{ x: -0.5, y: 0.5, z: -0.5 },
|
|
267
|
+
// 3: left-top-back
|
|
268
|
+
{ x: -0.5, y: -0.5, z: 0.5 },
|
|
269
|
+
// 4: left-bottom-front
|
|
270
|
+
{ x: 0.5, y: -0.5, z: 0.5 },
|
|
271
|
+
// 5: right-bottom-front
|
|
272
|
+
{ x: 0.5, y: 0.5, z: 0.5 },
|
|
273
|
+
// 6: right-top-front
|
|
274
|
+
{ x: -0.5, y: 0.5, z: 0.5 }
|
|
275
|
+
// 7: left-top-front
|
|
276
|
+
];
|
|
277
|
+
var FACE_NORMALS = [
|
|
278
|
+
{ x: 0, y: 0, z: 1 },
|
|
279
|
+
// 0: front
|
|
280
|
+
{ x: 0, y: 0, z: -1 },
|
|
281
|
+
// 1: back
|
|
282
|
+
{ x: 1, y: 0, z: 0 },
|
|
283
|
+
// 2: right
|
|
284
|
+
{ x: -1, y: 0, z: 0 },
|
|
285
|
+
// 3: left
|
|
286
|
+
{ x: 0, y: 1, z: 0 },
|
|
287
|
+
// 4: top
|
|
288
|
+
{ x: 0, y: -1, z: 0 }
|
|
289
|
+
// 5: bottom
|
|
290
|
+
];
|
|
291
|
+
var BOX_EDGES = [
|
|
292
|
+
// Front face edges
|
|
293
|
+
{ a: 4, b: 5, faces: [0, 5] },
|
|
294
|
+
// front-bottom
|
|
295
|
+
{ a: 5, b: 6, faces: [0, 2] },
|
|
296
|
+
// front-right
|
|
297
|
+
{ a: 6, b: 7, faces: [0, 4] },
|
|
298
|
+
// front-top
|
|
299
|
+
{ a: 7, b: 4, faces: [0, 3] },
|
|
300
|
+
// front-left
|
|
301
|
+
// Back face edges
|
|
302
|
+
{ a: 0, b: 1, faces: [1, 5] },
|
|
303
|
+
// back-bottom
|
|
304
|
+
{ a: 1, b: 2, faces: [1, 2] },
|
|
305
|
+
// back-right
|
|
306
|
+
{ a: 2, b: 3, faces: [1, 4] },
|
|
307
|
+
// back-top
|
|
308
|
+
{ a: 3, b: 0, faces: [1, 3] },
|
|
309
|
+
// back-left
|
|
310
|
+
// Connecting edges (front-to-back)
|
|
311
|
+
{ a: 4, b: 0, faces: [3, 5] },
|
|
312
|
+
// left-bottom
|
|
313
|
+
{ a: 5, b: 1, faces: [2, 5] },
|
|
314
|
+
// right-bottom
|
|
315
|
+
{ a: 6, b: 2, faces: [2, 4] },
|
|
316
|
+
// right-top
|
|
317
|
+
{ a: 7, b: 3, faces: [3, 4] }
|
|
318
|
+
// left-top
|
|
319
|
+
];
|
|
320
|
+
function isEdgeFrontFacing(edge, faceVisibility) {
|
|
321
|
+
return faceVisibility[edge.faces[0]] || faceVisibility[edge.faces[1]];
|
|
322
|
+
}
|
|
323
|
+
function renderBox(ctx, opts) {
|
|
324
|
+
const { center, scale, sizeX, sizeY, sizeZ, matrix, projection, focalLength, showHidden, hiddenStyle, hiddenAlpha, edgeColor } = opts;
|
|
325
|
+
const projected = BOX_VERTICES.map((v) => {
|
|
326
|
+
const scaled = { x: v.x * sizeX, y: v.y * sizeY, z: v.z * sizeZ };
|
|
327
|
+
const rotated = rotate3D(scaled, matrix);
|
|
328
|
+
const p = project(rotated, projection, focalLength);
|
|
329
|
+
return { x: center.x + p.x * scale, y: center.y - p.y * scale };
|
|
330
|
+
});
|
|
331
|
+
const faceVisibility = FACE_NORMALS.map((n) => transformedNormalZ(n, matrix) > 0);
|
|
332
|
+
ctx.strokeStyle = edgeColor;
|
|
333
|
+
const savedAlpha = ctx.globalAlpha;
|
|
334
|
+
if (showHidden) {
|
|
335
|
+
applyHiddenEdgeStyle(ctx, hiddenStyle, hiddenAlpha);
|
|
336
|
+
for (const edge of BOX_EDGES) {
|
|
337
|
+
if (!isEdgeFrontFacing(edge, faceVisibility)) {
|
|
338
|
+
const pa = projected[edge.a];
|
|
339
|
+
const pb = projected[edge.b];
|
|
340
|
+
drawLine(ctx, pa.x, pa.y, pb.x, pb.y);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
resetEdgeStyle(ctx, savedAlpha);
|
|
344
|
+
}
|
|
345
|
+
ctx.setLineDash([]);
|
|
346
|
+
ctx.globalAlpha = savedAlpha;
|
|
347
|
+
for (const edge of BOX_EDGES) {
|
|
348
|
+
if (isEdgeFrontFacing(edge, faceVisibility)) {
|
|
349
|
+
const pa = projected[edge.a];
|
|
350
|
+
const pb = projected[edge.b];
|
|
351
|
+
drawLine(ctx, pa.x, pa.y, pb.x, pb.y);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return projected;
|
|
355
|
+
}
|
|
356
|
+
function boxCrossContours(opts, count) {
|
|
357
|
+
const { center, scale, sizeX, sizeY, sizeZ, matrix, projection, focalLength } = opts;
|
|
358
|
+
const faceVisibility = FACE_NORMALS.map((n) => transformedNormalZ(n, matrix) > 0);
|
|
359
|
+
const front = [];
|
|
360
|
+
const hidden = [];
|
|
361
|
+
const faceContours = [
|
|
362
|
+
// Front/back faces: horizontal lines
|
|
363
|
+
{ faceIdx: 0, generate: () => generateFaceContours(sizeX, sizeY, 0.5 * sizeZ, "xy", count) },
|
|
364
|
+
{ faceIdx: 1, generate: () => generateFaceContours(sizeX, sizeY, -0.5 * sizeZ, "xy", count) },
|
|
365
|
+
// Right/left faces: horizontal lines
|
|
366
|
+
{ faceIdx: 2, generate: () => generateFaceContours(sizeZ, sizeY, 0.5 * sizeX, "zy-right", count) },
|
|
367
|
+
{ faceIdx: 3, generate: () => generateFaceContours(sizeZ, sizeY, -0.5 * sizeX, "zy-left", count) },
|
|
368
|
+
// Top/bottom faces: depth lines
|
|
369
|
+
{ faceIdx: 4, generate: () => generateFaceContours(sizeX, sizeZ, 0.5 * sizeY, "xz-top", count) },
|
|
370
|
+
{ faceIdx: 5, generate: () => generateFaceContours(sizeX, sizeZ, -0.5 * sizeY, "xz-bottom", count) }
|
|
371
|
+
];
|
|
372
|
+
for (const fc of faceContours) {
|
|
373
|
+
const lines3D = fc.generate();
|
|
374
|
+
const lines2D = lines3D.map(
|
|
375
|
+
(line) => line.map((p) => {
|
|
376
|
+
const rotated = rotate3D(p, matrix);
|
|
377
|
+
const proj = project(rotated, projection, focalLength);
|
|
378
|
+
return { x: center.x + proj.x * scale, y: center.y - proj.y * scale };
|
|
379
|
+
})
|
|
380
|
+
);
|
|
381
|
+
if (faceVisibility[fc.faceIdx]) {
|
|
382
|
+
front.push(...lines2D);
|
|
383
|
+
} else {
|
|
384
|
+
hidden.push(...lines2D);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return { front, hidden };
|
|
388
|
+
}
|
|
389
|
+
function generateFaceContours(width, height, fixedCoord, plane, count) {
|
|
390
|
+
const lines = [];
|
|
391
|
+
for (let i = 1; i < count; i++) {
|
|
392
|
+
const t = i / count - 0.5;
|
|
393
|
+
const line = [];
|
|
394
|
+
switch (plane) {
|
|
395
|
+
case "xy":
|
|
396
|
+
line.push({ x: -width / 2, y: t * height, z: fixedCoord });
|
|
397
|
+
line.push({ x: width / 2, y: t * height, z: fixedCoord });
|
|
398
|
+
break;
|
|
399
|
+
case "zy-right":
|
|
400
|
+
line.push({ x: fixedCoord, y: t * height, z: -width / 2 });
|
|
401
|
+
line.push({ x: fixedCoord, y: t * height, z: width / 2 });
|
|
402
|
+
break;
|
|
403
|
+
case "zy-left":
|
|
404
|
+
line.push({ x: fixedCoord, y: t * height, z: -width / 2 });
|
|
405
|
+
line.push({ x: fixedCoord, y: t * height, z: width / 2 });
|
|
406
|
+
break;
|
|
407
|
+
case "xz-top":
|
|
408
|
+
line.push({ x: -width / 2, y: fixedCoord, z: t * height });
|
|
409
|
+
line.push({ x: width / 2, y: fixedCoord, z: t * height });
|
|
410
|
+
break;
|
|
411
|
+
case "xz-bottom":
|
|
412
|
+
line.push({ x: -width / 2, y: fixedCoord, z: t * height });
|
|
413
|
+
line.push({ x: width / 2, y: fixedCoord, z: t * height });
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
lines.push(line);
|
|
417
|
+
}
|
|
418
|
+
return lines;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/forms/cylinder.ts
|
|
422
|
+
function projectVertex(v, opts) {
|
|
423
|
+
const rotated = rotate3D(v, opts.matrix);
|
|
424
|
+
const p = project(rotated, opts.projection, opts.focalLength);
|
|
425
|
+
return { x: opts.center.x + p.x * opts.scale, y: opts.center.y - p.y * opts.scale };
|
|
426
|
+
}
|
|
427
|
+
function cylinderEllipseAt(yPos, opts) {
|
|
428
|
+
const { matrix, center, scale, sizeX, sizeZ } = opts;
|
|
429
|
+
const center3D = { x: 0, y: yPos, z: 0 };
|
|
430
|
+
const center2D = projectVertex(center3D, opts);
|
|
431
|
+
const yAxisRotated = rotate3D({ x: 0, y: 1, z: 0 }, matrix);
|
|
432
|
+
const topSideVisible = yAxisRotated.z > 0;
|
|
433
|
+
const px = projectVertex({ x: sizeX * 0.5, y: yPos, z: 0 }, opts);
|
|
434
|
+
const pz = projectVertex({ x: 0, y: yPos, z: sizeZ * 0.5 }, opts);
|
|
435
|
+
const ax = Math.sqrt((px.x - center2D.x) ** 2 + (px.y - center2D.y) ** 2);
|
|
436
|
+
const az = Math.sqrt((pz.x - center2D.x) ** 2 + (pz.y - center2D.y) ** 2);
|
|
437
|
+
const angle = Math.atan2(px.y - center2D.y, px.x - center2D.x);
|
|
438
|
+
const topP = projectVertex({ x: 0, y: opts.sizeY / 2, z: 0 }, opts);
|
|
439
|
+
const botP = projectVertex({ x: 0, y: -opts.sizeY / 2, z: 0 }, opts);
|
|
440
|
+
const axisDir = {
|
|
441
|
+
x: topP.x - botP.x,
|
|
442
|
+
y: topP.y - botP.y
|
|
443
|
+
};
|
|
444
|
+
return {
|
|
445
|
+
params: { cx: center2D.x, cy: center2D.y, rx: Math.max(ax, az), ry: Math.min(ax, az), rotation: angle },
|
|
446
|
+
axisDir,
|
|
447
|
+
topSideVisible
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function renderCylinder(ctx, opts) {
|
|
451
|
+
const { showHidden, hiddenStyle, hiddenAlpha, edgeColor, sizeY } = opts;
|
|
452
|
+
const halfH = sizeY / 2;
|
|
453
|
+
ctx.strokeStyle = edgeColor;
|
|
454
|
+
const savedAlpha = ctx.globalAlpha;
|
|
455
|
+
const topEllipse = cylinderEllipseAt(halfH, opts);
|
|
456
|
+
const botEllipse = cylinderEllipseAt(-halfH, opts);
|
|
457
|
+
const hiddenDash = hiddenStyle === "dotted" ? [2, 3] : [6, 4];
|
|
458
|
+
const topFront = topEllipse.topSideVisible ? [0, Math.PI] : [Math.PI, Math.PI * 2];
|
|
459
|
+
const botFront = !botEllipse.topSideVisible ? [0, Math.PI] : [Math.PI, Math.PI * 2];
|
|
460
|
+
const topLeft = ellipsePointAt(topEllipse.params, Math.PI);
|
|
461
|
+
const topRight = ellipsePointAt(topEllipse.params, 0);
|
|
462
|
+
const botLeft = ellipsePointAt(botEllipse.params, Math.PI);
|
|
463
|
+
const botRight = ellipsePointAt(botEllipse.params, 0);
|
|
464
|
+
ctx.setLineDash([]);
|
|
465
|
+
ctx.globalAlpha = savedAlpha;
|
|
466
|
+
drawLine(ctx, topLeft.x, topLeft.y, botLeft.x, botLeft.y);
|
|
467
|
+
drawLine(ctx, topRight.x, topRight.y, botRight.x, botRight.y);
|
|
468
|
+
if (showHidden) {
|
|
469
|
+
drawEllipseWithHidden(ctx, topEllipse.params, topFront, hiddenAlpha, hiddenDash);
|
|
470
|
+
drawEllipseWithHidden(ctx, botEllipse.params, botFront, hiddenAlpha, hiddenDash);
|
|
471
|
+
} else {
|
|
472
|
+
ctx.beginPath();
|
|
473
|
+
ctx.ellipse(
|
|
474
|
+
topEllipse.params.cx,
|
|
475
|
+
topEllipse.params.cy,
|
|
476
|
+
topEllipse.params.rx,
|
|
477
|
+
topEllipse.params.ry,
|
|
478
|
+
topEllipse.params.rotation,
|
|
479
|
+
topFront[0],
|
|
480
|
+
topFront[1]
|
|
481
|
+
);
|
|
482
|
+
ctx.stroke();
|
|
483
|
+
ctx.beginPath();
|
|
484
|
+
ctx.ellipse(
|
|
485
|
+
botEllipse.params.cx,
|
|
486
|
+
botEllipse.params.cy,
|
|
487
|
+
botEllipse.params.rx,
|
|
488
|
+
botEllipse.params.ry,
|
|
489
|
+
botEllipse.params.rotation,
|
|
490
|
+
botFront[0],
|
|
491
|
+
botFront[1]
|
|
492
|
+
);
|
|
493
|
+
ctx.stroke();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function cylinderCrossContours(opts, count) {
|
|
497
|
+
const halfH = opts.sizeY / 2;
|
|
498
|
+
const contours = [];
|
|
499
|
+
for (let i = 1; i < count; i++) {
|
|
500
|
+
const t = i / count;
|
|
501
|
+
const y = -halfH + t * opts.sizeY;
|
|
502
|
+
const ellipse = cylinderEllipseAt(y, opts);
|
|
503
|
+
const frontHalf = ellipse.topSideVisible ? [0, Math.PI] : [Math.PI, Math.PI * 2];
|
|
504
|
+
contours.push({ params: ellipse.params, frontHalf });
|
|
505
|
+
}
|
|
506
|
+
return contours;
|
|
507
|
+
}
|
|
508
|
+
function ellipsePointAt(params, angle) {
|
|
509
|
+
const cos = Math.cos(params.rotation);
|
|
510
|
+
const sin = Math.sin(params.rotation);
|
|
511
|
+
const px = params.rx * Math.cos(angle);
|
|
512
|
+
const py = params.ry * Math.sin(angle);
|
|
513
|
+
return {
|
|
514
|
+
x: params.cx + px * cos - py * sin,
|
|
515
|
+
y: params.cy + px * sin + py * cos
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/forms/sphere.ts
|
|
520
|
+
function projectVertex2(v, opts) {
|
|
521
|
+
const rotated = rotate3D(v, opts.matrix);
|
|
522
|
+
const p = project(rotated, opts.projection, opts.focalLength);
|
|
523
|
+
return { x: opts.center.x + p.x * opts.scale, y: opts.center.y - p.y * opts.scale };
|
|
524
|
+
}
|
|
525
|
+
function renderSphere(ctx, opts) {
|
|
526
|
+
const { center, scale, radius, edgeColor } = opts;
|
|
527
|
+
const screenRadius = radius * scale;
|
|
528
|
+
ctx.strokeStyle = edgeColor;
|
|
529
|
+
ctx.beginPath();
|
|
530
|
+
ctx.arc(center.x, center.y, screenRadius, 0, Math.PI * 2);
|
|
531
|
+
ctx.stroke();
|
|
532
|
+
}
|
|
533
|
+
function sphereCrossContours(opts, latCount, lonCount) {
|
|
534
|
+
const { center, scale, radius, matrix, projection, focalLength } = opts;
|
|
535
|
+
const contours = [];
|
|
536
|
+
const r = radius;
|
|
537
|
+
for (let i = 1; i < latCount; i++) {
|
|
538
|
+
const t = i / latCount;
|
|
539
|
+
const y = -r + t * 2 * r;
|
|
540
|
+
const circleRadius = Math.sqrt(r * r - y * y);
|
|
541
|
+
const circleCenter = { x: 0, y, z: 0 };
|
|
542
|
+
const cc2D = projectVertex2(circleCenter, opts);
|
|
543
|
+
const px = projectVertex2({ x: circleRadius, y, z: 0 }, opts);
|
|
544
|
+
const pz = projectVertex2({ x: 0, y, z: circleRadius }, opts);
|
|
545
|
+
const ax = Math.sqrt((px.x - cc2D.x) ** 2 + (px.y - cc2D.y) ** 2);
|
|
546
|
+
const az = Math.sqrt((pz.x - cc2D.x) ** 2 + (pz.y - cc2D.y) ** 2);
|
|
547
|
+
const angle = Math.atan2(px.y - cc2D.y, px.x - cc2D.x);
|
|
548
|
+
const normalZ = rotate3D({ x: 0, y: 1, z: 0 }, matrix).z;
|
|
549
|
+
const topVisible = normalZ > 0;
|
|
550
|
+
const isAboveEquator = y > 0;
|
|
551
|
+
const frontHalf = isAboveEquator === topVisible ? [0, Math.PI] : [Math.PI, Math.PI * 2];
|
|
552
|
+
contours.push({
|
|
553
|
+
params: { cx: cc2D.x, cy: cc2D.y, rx: Math.max(ax, az), ry: Math.min(ax, az), rotation: angle },
|
|
554
|
+
frontHalf
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
for (let i = 0; i < lonCount; i++) {
|
|
558
|
+
const angle = i / lonCount * Math.PI;
|
|
559
|
+
const nx = Math.cos(angle);
|
|
560
|
+
const nz = Math.sin(angle);
|
|
561
|
+
const p1 = projectVertex2({ x: nx * r, y: 0, z: nz * r }, opts);
|
|
562
|
+
const p2 = projectVertex2({ x: -nx * r, y: 0, z: -nz * r }, opts);
|
|
563
|
+
const pTop = projectVertex2({ x: 0, y: r, z: 0 }, opts);
|
|
564
|
+
const pBot = projectVertex2({ x: 0, y: -r, z: 0 }, opts);
|
|
565
|
+
const cc2D = { x: center.x, y: center.y };
|
|
566
|
+
const ax1 = Math.sqrt((p1.x - cc2D.x) ** 2 + (p1.y - cc2D.y) ** 2);
|
|
567
|
+
const ax2 = Math.sqrt((pTop.x - cc2D.x) ** 2 + (pTop.y - cc2D.y) ** 2);
|
|
568
|
+
const rot = Math.atan2(p1.y - cc2D.y, p1.x - cc2D.x);
|
|
569
|
+
const sliceNormalZ = rotate3D({ x: -nz, y: 0, z: nx }, matrix).z;
|
|
570
|
+
const frontHalf = sliceNormalZ > 0 ? [0, Math.PI] : [Math.PI, Math.PI * 2];
|
|
571
|
+
contours.push({
|
|
572
|
+
params: { cx: cc2D.x, cy: cc2D.y, rx: Math.max(ax1, ax2), ry: Math.min(ax1, ax2), rotation: rot },
|
|
573
|
+
frontHalf
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
return contours;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/forms/cone.ts
|
|
580
|
+
function projectVertex3(v, opts) {
|
|
581
|
+
const rotated = rotate3D(v, opts.matrix);
|
|
582
|
+
const p = project(rotated, opts.projection, opts.focalLength);
|
|
583
|
+
return { x: opts.center.x + p.x * opts.scale, y: opts.center.y - p.y * opts.scale };
|
|
584
|
+
}
|
|
585
|
+
function renderCone(ctx, opts) {
|
|
586
|
+
const { sizeX, sizeY, sizeZ, edgeColor, showHidden, hiddenStyle, hiddenAlpha, matrix } = opts;
|
|
587
|
+
const halfH = sizeY / 2;
|
|
588
|
+
ctx.strokeStyle = edgeColor;
|
|
589
|
+
const savedAlpha = ctx.globalAlpha;
|
|
590
|
+
const apex = projectVertex3({ x: 0, y: halfH, z: 0 }, opts);
|
|
591
|
+
const baseCenter = projectVertex3({ x: 0, y: -halfH, z: 0 }, opts);
|
|
592
|
+
const basePx = projectVertex3({ x: sizeX * 0.5, y: -halfH, z: 0 }, opts);
|
|
593
|
+
const basePz = projectVertex3({ x: 0, y: -halfH, z: sizeZ * 0.5 }, opts);
|
|
594
|
+
const ax = Math.sqrt((basePx.x - baseCenter.x) ** 2 + (basePx.y - baseCenter.y) ** 2);
|
|
595
|
+
const az = Math.sqrt((basePz.x - baseCenter.x) ** 2 + (basePz.y - baseCenter.y) ** 2);
|
|
596
|
+
const angle = Math.atan2(basePx.y - baseCenter.y, basePx.x - baseCenter.x);
|
|
597
|
+
const baseEllipse = {
|
|
598
|
+
cx: baseCenter.x,
|
|
599
|
+
cy: baseCenter.y,
|
|
600
|
+
rx: Math.max(ax, az),
|
|
601
|
+
ry: Math.min(ax, az),
|
|
602
|
+
rotation: angle
|
|
603
|
+
};
|
|
604
|
+
const bottomNormalZ = rotate3D({ x: 0, y: -1, z: 0 }, matrix).z;
|
|
605
|
+
const baseVisible = bottomNormalZ > 0;
|
|
606
|
+
const baseFront = baseVisible ? [0, Math.PI] : [Math.PI, Math.PI * 2];
|
|
607
|
+
const hiddenDash = hiddenStyle === "dotted" ? [2, 3] : [6, 4];
|
|
608
|
+
const leftPoint = ellipsePointAt2(baseEllipse, Math.PI);
|
|
609
|
+
const rightPoint = ellipsePointAt2(baseEllipse, 0);
|
|
610
|
+
ctx.setLineDash([]);
|
|
611
|
+
ctx.globalAlpha = savedAlpha;
|
|
612
|
+
drawLine(ctx, apex.x, apex.y, leftPoint.x, leftPoint.y);
|
|
613
|
+
drawLine(ctx, apex.x, apex.y, rightPoint.x, rightPoint.y);
|
|
614
|
+
if (showHidden) {
|
|
615
|
+
drawEllipseWithHidden(ctx, baseEllipse, baseFront, hiddenAlpha, hiddenDash);
|
|
616
|
+
} else {
|
|
617
|
+
ctx.beginPath();
|
|
618
|
+
ctx.ellipse(
|
|
619
|
+
baseEllipse.cx,
|
|
620
|
+
baseEllipse.cy,
|
|
621
|
+
baseEllipse.rx,
|
|
622
|
+
baseEllipse.ry,
|
|
623
|
+
baseEllipse.rotation,
|
|
624
|
+
baseFront[0],
|
|
625
|
+
baseFront[1]
|
|
626
|
+
);
|
|
627
|
+
ctx.stroke();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function coneCrossContours(opts, count) {
|
|
631
|
+
const { sizeX, sizeY, sizeZ, matrix } = opts;
|
|
632
|
+
const halfH = sizeY / 2;
|
|
633
|
+
const contours = [];
|
|
634
|
+
for (let i = 1; i < count; i++) {
|
|
635
|
+
const t = i / count;
|
|
636
|
+
const y = -halfH + t * sizeY;
|
|
637
|
+
const taper = 1 - (y + halfH) / sizeY;
|
|
638
|
+
const rx = sizeX * 0.5 * taper;
|
|
639
|
+
const rz = sizeZ * 0.5 * taper;
|
|
640
|
+
if (rx < 1e-3 || rz < 1e-3) continue;
|
|
641
|
+
const cc = projectVertex3({ x: 0, y, z: 0 }, opts);
|
|
642
|
+
const px = projectVertex3({ x: rx, y, z: 0 }, opts);
|
|
643
|
+
const pz = projectVertex3({ x: 0, y, z: rz }, opts);
|
|
644
|
+
const ax = Math.sqrt((px.x - cc.x) ** 2 + (px.y - cc.y) ** 2);
|
|
645
|
+
const az = Math.sqrt((pz.x - cc.x) ** 2 + (pz.y - cc.y) ** 2);
|
|
646
|
+
const angle = Math.atan2(px.y - cc.y, px.x - cc.x);
|
|
647
|
+
const normalZ = rotate3D({ x: 0, y: 1, z: 0 }, matrix).z;
|
|
648
|
+
const frontHalf = normalZ > 0 ? [0, Math.PI] : [Math.PI, Math.PI * 2];
|
|
649
|
+
contours.push({
|
|
650
|
+
params: { cx: cc.x, cy: cc.y, rx: Math.max(ax, az), ry: Math.min(ax, az), rotation: angle },
|
|
651
|
+
frontHalf
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return contours;
|
|
655
|
+
}
|
|
656
|
+
function ellipsePointAt2(params, angle) {
|
|
657
|
+
const cos = Math.cos(params.rotation);
|
|
658
|
+
const sin = Math.sin(params.rotation);
|
|
659
|
+
const px = params.rx * Math.cos(angle);
|
|
660
|
+
const py = params.ry * Math.sin(angle);
|
|
661
|
+
return {
|
|
662
|
+
x: params.cx + px * cos - py * sin,
|
|
663
|
+
y: params.cy + px * sin + py * cos
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// src/forms/wedge.ts
|
|
668
|
+
var WEDGE_VERTICES = [
|
|
669
|
+
{ x: -0.5, y: -0.5, z: 0.5 },
|
|
670
|
+
// 0: front-left-bottom
|
|
671
|
+
{ x: 0.5, y: -0.5, z: 0.5 },
|
|
672
|
+
// 1: front-right-bottom
|
|
673
|
+
{ x: 0, y: 0.5, z: 0.5 },
|
|
674
|
+
// 2: front-top
|
|
675
|
+
{ x: -0.5, y: -0.5, z: -0.5 },
|
|
676
|
+
// 3: back-left-bottom
|
|
677
|
+
{ x: 0.5, y: -0.5, z: -0.5 },
|
|
678
|
+
// 4: back-right-bottom
|
|
679
|
+
{ x: 0, y: 0.5, z: -0.5 }
|
|
680
|
+
// 5: back-top
|
|
681
|
+
];
|
|
682
|
+
var FACE_NORMALS2 = [
|
|
683
|
+
{ x: 0, y: 0, z: 1 },
|
|
684
|
+
// 0: front (triangle 0,1,2)
|
|
685
|
+
{ x: 0, y: 0, z: -1 },
|
|
686
|
+
// 1: back (triangle 3,4,5)
|
|
687
|
+
{ x: 0, y: -1, z: 0 },
|
|
688
|
+
// 2: bottom (quad 0,1,4,3)
|
|
689
|
+
{ x: -0.894, y: 0.447, z: 0 },
|
|
690
|
+
// 3: left slope (quad 0,2,5,3) normalized(-1, 0.5, 0)
|
|
691
|
+
{ x: 0.894, y: 0.447, z: 0 }
|
|
692
|
+
// 4: right slope (quad 1,2,5,4) normalized(1, 0.5, 0)
|
|
693
|
+
];
|
|
694
|
+
var WEDGE_EDGES = [
|
|
695
|
+
// Front triangle
|
|
696
|
+
{ a: 0, b: 1, faces: [0, 2] },
|
|
697
|
+
{ a: 1, b: 2, faces: [0, 4] },
|
|
698
|
+
{ a: 2, b: 0, faces: [0, 3] },
|
|
699
|
+
// Back triangle
|
|
700
|
+
{ a: 3, b: 4, faces: [1, 2] },
|
|
701
|
+
{ a: 4, b: 5, faces: [1, 4] },
|
|
702
|
+
{ a: 5, b: 3, faces: [1, 3] },
|
|
703
|
+
// Connecting edges
|
|
704
|
+
{ a: 0, b: 3, faces: [2, 3] },
|
|
705
|
+
{ a: 1, b: 4, faces: [2, 4] },
|
|
706
|
+
{ a: 2, b: 5, faces: [3, 4] }
|
|
707
|
+
];
|
|
708
|
+
function projectVertex4(v, opts) {
|
|
709
|
+
const scaled = { x: v.x * opts.sizeX, y: v.y * opts.sizeY, z: v.z * opts.sizeZ };
|
|
710
|
+
const rotated = rotate3D(scaled, opts.matrix);
|
|
711
|
+
const p = project(rotated, opts.projection, opts.focalLength);
|
|
712
|
+
return { x: opts.center.x + p.x * opts.scale, y: opts.center.y - p.y * opts.scale };
|
|
713
|
+
}
|
|
714
|
+
function renderWedge(ctx, opts) {
|
|
715
|
+
const { showHidden, hiddenStyle, hiddenAlpha, edgeColor, matrix } = opts;
|
|
716
|
+
const projected = WEDGE_VERTICES.map((v) => projectVertex4(v, opts));
|
|
717
|
+
const faceVisibility = FACE_NORMALS2.map((n) => transformedNormalZ(n, matrix) > 0);
|
|
718
|
+
ctx.strokeStyle = edgeColor;
|
|
719
|
+
const savedAlpha = ctx.globalAlpha;
|
|
720
|
+
if (showHidden) {
|
|
721
|
+
applyHiddenEdgeStyle(ctx, hiddenStyle, hiddenAlpha);
|
|
722
|
+
for (const edge of WEDGE_EDGES) {
|
|
723
|
+
if (!(faceVisibility[edge.faces[0]] || faceVisibility[edge.faces[1]])) {
|
|
724
|
+
const pa = projected[edge.a];
|
|
725
|
+
const pb = projected[edge.b];
|
|
726
|
+
drawLine(ctx, pa.x, pa.y, pb.x, pb.y);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
resetEdgeStyle(ctx, savedAlpha);
|
|
730
|
+
}
|
|
731
|
+
ctx.setLineDash([]);
|
|
732
|
+
ctx.globalAlpha = savedAlpha;
|
|
733
|
+
for (const edge of WEDGE_EDGES) {
|
|
734
|
+
if (faceVisibility[edge.faces[0]] || faceVisibility[edge.faces[1]]) {
|
|
735
|
+
const pa = projected[edge.a];
|
|
736
|
+
const pb = projected[edge.b];
|
|
737
|
+
drawLine(ctx, pa.x, pa.y, pb.x, pb.y);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/forms/egg.ts
|
|
743
|
+
function projectVertex5(v, opts) {
|
|
744
|
+
const rotated = rotate3D(v, opts.matrix);
|
|
745
|
+
const p = project(rotated, opts.projection, opts.focalLength);
|
|
746
|
+
return { x: opts.center.x + p.x * opts.scale, y: opts.center.y - p.y * opts.scale };
|
|
747
|
+
}
|
|
748
|
+
function eggRadius(t) {
|
|
749
|
+
const base = Math.sin(t * Math.PI);
|
|
750
|
+
const asymmetry = 1 - t * 0.3;
|
|
751
|
+
return base * asymmetry;
|
|
752
|
+
}
|
|
753
|
+
function renderEgg(ctx, opts) {
|
|
754
|
+
const { center, scale, sizeX, sizeY, sizeZ, matrix, projection, focalLength, edgeColor } = opts;
|
|
755
|
+
const segments = 48;
|
|
756
|
+
ctx.strokeStyle = edgeColor;
|
|
757
|
+
const outlinePoints = [];
|
|
758
|
+
for (let i = 0; i <= segments; i++) {
|
|
759
|
+
const angle = i / segments * Math.PI * 2;
|
|
760
|
+
const t = (Math.sin(angle) + 1) / 2;
|
|
761
|
+
const y = (t - 0.5) * sizeY;
|
|
762
|
+
const r = eggRadius(t);
|
|
763
|
+
const x = Math.cos(angle) * r * sizeX * 0.5;
|
|
764
|
+
const z = 0;
|
|
765
|
+
const rotated = rotate3D({ x, y, z }, matrix);
|
|
766
|
+
const p = project(rotated, projection, focalLength);
|
|
767
|
+
outlinePoints.push({ x: center.x + p.x * scale, y: center.y - p.y * scale });
|
|
768
|
+
}
|
|
769
|
+
const surfacePoints = [];
|
|
770
|
+
const ySteps = 32;
|
|
771
|
+
const aSteps = 24;
|
|
772
|
+
for (let yi = 0; yi <= ySteps; yi++) {
|
|
773
|
+
const t = yi / ySteps;
|
|
774
|
+
const y = (t - 0.5) * sizeY;
|
|
775
|
+
const r = eggRadius(t);
|
|
776
|
+
let leftmost = Infinity;
|
|
777
|
+
let rightmost = -Infinity;
|
|
778
|
+
let leftP = { x: 0, y: 0 };
|
|
779
|
+
let rightP = { x: 0, y: 0 };
|
|
780
|
+
for (let ai = 0; ai <= aSteps; ai++) {
|
|
781
|
+
const a = ai / aSteps * Math.PI * 2;
|
|
782
|
+
const x3d = Math.cos(a) * r * sizeX * 0.5;
|
|
783
|
+
const z3d = Math.sin(a) * r * sizeZ * 0.5;
|
|
784
|
+
const p3d = { x: x3d, y, z: z3d };
|
|
785
|
+
const rotated = rotate3D(p3d, matrix);
|
|
786
|
+
const p = project(rotated, projection, focalLength);
|
|
787
|
+
const sx = center.x + p.x * scale;
|
|
788
|
+
const sy = center.y - p.y * scale;
|
|
789
|
+
if (sx < leftmost) {
|
|
790
|
+
leftmost = sx;
|
|
791
|
+
leftP = { x: sx, y: sy };
|
|
792
|
+
}
|
|
793
|
+
if (sx > rightmost) {
|
|
794
|
+
rightmost = sx;
|
|
795
|
+
rightP = { x: sx, y: sy };
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
surfacePoints.push(leftP, rightP);
|
|
799
|
+
}
|
|
800
|
+
const leftPath = [];
|
|
801
|
+
const rightPath = [];
|
|
802
|
+
for (let i = 0; i < surfacePoints.length; i += 2) {
|
|
803
|
+
leftPath.push(surfacePoints[i]);
|
|
804
|
+
rightPath.push(surfacePoints[i + 1]);
|
|
805
|
+
}
|
|
806
|
+
ctx.beginPath();
|
|
807
|
+
if (leftPath.length > 1) {
|
|
808
|
+
ctx.moveTo(leftPath[0].x, leftPath[0].y);
|
|
809
|
+
for (let i = 1; i < leftPath.length; i++) {
|
|
810
|
+
ctx.lineTo(leftPath[i].x, leftPath[i].y);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
for (let i = rightPath.length - 1; i >= 0; i--) {
|
|
814
|
+
ctx.lineTo(rightPath[i].x, rightPath[i].y);
|
|
815
|
+
}
|
|
816
|
+
ctx.closePath();
|
|
817
|
+
ctx.stroke();
|
|
818
|
+
}
|
|
819
|
+
function eggCrossContours(opts, count) {
|
|
820
|
+
const { center, scale, sizeX, sizeY, sizeZ, matrix, projection, focalLength } = opts;
|
|
821
|
+
const contours = [];
|
|
822
|
+
for (let i = 1; i < count; i++) {
|
|
823
|
+
const t = i / count;
|
|
824
|
+
const y = (t - 0.5) * sizeY;
|
|
825
|
+
const r = eggRadius(t);
|
|
826
|
+
const rx = r * sizeX * 0.5;
|
|
827
|
+
const rz = r * sizeZ * 0.5;
|
|
828
|
+
if (rx < 1e-3 || rz < 1e-3) continue;
|
|
829
|
+
const cc = projectVertex5({ x: 0, y, z: 0 }, opts);
|
|
830
|
+
const px = projectVertex5({ x: rx, y, z: 0 }, opts);
|
|
831
|
+
const pz = projectVertex5({ x: 0, y, z: rz }, opts);
|
|
832
|
+
const ax = Math.sqrt((px.x - cc.x) ** 2 + (px.y - cc.y) ** 2);
|
|
833
|
+
const az = Math.sqrt((pz.x - cc.x) ** 2 + (pz.y - cc.y) ** 2);
|
|
834
|
+
const angle = Math.atan2(px.y - cc.y, px.x - cc.x);
|
|
835
|
+
const normalZ = rotate3D({ x: 0, y: 1, z: 0 }, matrix).z;
|
|
836
|
+
const topVisible = normalZ > 0;
|
|
837
|
+
const isAboveCenter = t > 0.5;
|
|
838
|
+
const frontHalf = isAboveCenter === topVisible ? [0, Math.PI] : [Math.PI, Math.PI * 2];
|
|
839
|
+
contours.push({
|
|
840
|
+
params: { cx: cc.x, cy: cc.y, rx: Math.max(ax, az), ry: Math.min(ax, az), rotation: angle },
|
|
841
|
+
frontHalf
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return contours;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/form-layer.ts
|
|
848
|
+
var FORM_PROPERTIES = [
|
|
849
|
+
{
|
|
850
|
+
key: "formType",
|
|
851
|
+
label: "Form Type",
|
|
852
|
+
type: "select",
|
|
853
|
+
default: "box",
|
|
854
|
+
options: [
|
|
855
|
+
{ value: "box", label: "Box" },
|
|
856
|
+
{ value: "cylinder", label: "Cylinder" },
|
|
857
|
+
{ value: "sphere", label: "Sphere" },
|
|
858
|
+
{ value: "cone", label: "Cone" },
|
|
859
|
+
{ value: "wedge", label: "Wedge" },
|
|
860
|
+
{ value: "egg", label: "Egg / Ovoid" }
|
|
861
|
+
],
|
|
862
|
+
group: "form"
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
key: "position",
|
|
866
|
+
label: "Position",
|
|
867
|
+
type: "point",
|
|
868
|
+
default: { x: 0.5, y: 0.5 },
|
|
869
|
+
group: "form"
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
key: "formSize",
|
|
873
|
+
label: "Size",
|
|
874
|
+
type: "number",
|
|
875
|
+
default: 0.25,
|
|
876
|
+
min: 0.05,
|
|
877
|
+
max: 0.6,
|
|
878
|
+
step: 0.01,
|
|
879
|
+
group: "form"
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
key: "sizeX",
|
|
883
|
+
label: "Width Scale",
|
|
884
|
+
type: "number",
|
|
885
|
+
default: 1,
|
|
886
|
+
min: 0.2,
|
|
887
|
+
max: 3,
|
|
888
|
+
step: 0.1,
|
|
889
|
+
group: "form"
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
key: "sizeY",
|
|
893
|
+
label: "Height Scale",
|
|
894
|
+
type: "number",
|
|
895
|
+
default: 1,
|
|
896
|
+
min: 0.2,
|
|
897
|
+
max: 3,
|
|
898
|
+
step: 0.1,
|
|
899
|
+
group: "form"
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
key: "sizeZ",
|
|
903
|
+
label: "Depth Scale",
|
|
904
|
+
type: "number",
|
|
905
|
+
default: 1,
|
|
906
|
+
min: 0.2,
|
|
907
|
+
max: 3,
|
|
908
|
+
step: 0.1,
|
|
909
|
+
group: "form"
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
key: "rotationX",
|
|
913
|
+
label: "Rotation X",
|
|
914
|
+
type: "number",
|
|
915
|
+
default: 15,
|
|
916
|
+
min: -90,
|
|
917
|
+
max: 90,
|
|
918
|
+
step: 5,
|
|
919
|
+
group: "rotation"
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
key: "rotationY",
|
|
923
|
+
label: "Rotation Y",
|
|
924
|
+
type: "number",
|
|
925
|
+
default: 30,
|
|
926
|
+
min: -180,
|
|
927
|
+
max: 180,
|
|
928
|
+
step: 5,
|
|
929
|
+
group: "rotation"
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
key: "rotationZ",
|
|
933
|
+
label: "Rotation Z",
|
|
934
|
+
type: "number",
|
|
935
|
+
default: 0,
|
|
936
|
+
min: -180,
|
|
937
|
+
max: 180,
|
|
938
|
+
step: 5,
|
|
939
|
+
group: "rotation"
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
key: "projection",
|
|
943
|
+
label: "Projection",
|
|
944
|
+
type: "select",
|
|
945
|
+
default: "orthographic",
|
|
946
|
+
options: [
|
|
947
|
+
{ value: "orthographic", label: "Orthographic" },
|
|
948
|
+
{ value: "weak-perspective", label: "Weak Perspective" }
|
|
949
|
+
],
|
|
950
|
+
group: "projection"
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
key: "showCrossContours",
|
|
954
|
+
label: "Show Cross-Contours",
|
|
955
|
+
type: "boolean",
|
|
956
|
+
default: true,
|
|
957
|
+
group: "contours"
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
key: "crossContourCount",
|
|
961
|
+
label: "Cross-Contour Count",
|
|
962
|
+
type: "number",
|
|
963
|
+
default: 5,
|
|
964
|
+
min: 1,
|
|
965
|
+
max: 12,
|
|
966
|
+
step: 1,
|
|
967
|
+
group: "contours"
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
key: "showAxes",
|
|
971
|
+
label: "Show Axes",
|
|
972
|
+
type: "boolean",
|
|
973
|
+
default: true,
|
|
974
|
+
group: "display"
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
key: "axisLength",
|
|
978
|
+
label: "Axis Length",
|
|
979
|
+
type: "number",
|
|
980
|
+
default: 1.2,
|
|
981
|
+
min: 0.5,
|
|
982
|
+
max: 2,
|
|
983
|
+
step: 0.1,
|
|
984
|
+
group: "display"
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
key: "showHiddenEdges",
|
|
988
|
+
label: "Show Hidden Edges",
|
|
989
|
+
type: "boolean",
|
|
990
|
+
default: true,
|
|
991
|
+
group: "display"
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
key: "hiddenEdgeStyle",
|
|
995
|
+
label: "Hidden Edge Style",
|
|
996
|
+
type: "select",
|
|
997
|
+
default: "dashed",
|
|
998
|
+
options: [
|
|
999
|
+
{ value: "dashed", label: "Dashed" },
|
|
1000
|
+
{ value: "dotted", label: "Dotted" },
|
|
1001
|
+
{ value: "faint", label: "Faint" },
|
|
1002
|
+
{ value: "hidden", label: "Hidden" }
|
|
1003
|
+
],
|
|
1004
|
+
group: "display"
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
key: "hiddenEdgeAlpha",
|
|
1008
|
+
label: "Hidden Edge Alpha",
|
|
1009
|
+
type: "number",
|
|
1010
|
+
default: 0.3,
|
|
1011
|
+
min: 0,
|
|
1012
|
+
max: 0.8,
|
|
1013
|
+
step: 0.05,
|
|
1014
|
+
group: "display"
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
key: "edgeColor",
|
|
1018
|
+
label: "Edge Color",
|
|
1019
|
+
type: "color",
|
|
1020
|
+
default: "rgba(0,200,255,0.7)",
|
|
1021
|
+
group: "style"
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
key: "contourColor",
|
|
1025
|
+
label: "Contour Color",
|
|
1026
|
+
type: "color",
|
|
1027
|
+
default: "rgba(100,255,100,0.5)",
|
|
1028
|
+
group: "style"
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
key: "axisColors",
|
|
1032
|
+
label: "Axis Colors (X,Y,Z)",
|
|
1033
|
+
type: "string",
|
|
1034
|
+
default: "red,green,blue",
|
|
1035
|
+
group: "style"
|
|
1036
|
+
},
|
|
1037
|
+
...COMMON_GUIDE_PROPERTIES
|
|
1038
|
+
];
|
|
1039
|
+
var formLayerType = {
|
|
1040
|
+
typeId: "construction:form",
|
|
1041
|
+
displayName: "Construction Form",
|
|
1042
|
+
icon: "cube",
|
|
1043
|
+
category: "guide",
|
|
1044
|
+
properties: FORM_PROPERTIES,
|
|
1045
|
+
propertyEditorId: "construction:form-editor",
|
|
1046
|
+
createDefault() {
|
|
1047
|
+
const props = {};
|
|
1048
|
+
for (const schema of FORM_PROPERTIES) {
|
|
1049
|
+
props[schema.key] = schema.default;
|
|
1050
|
+
}
|
|
1051
|
+
return props;
|
|
1052
|
+
},
|
|
1053
|
+
render(properties, ctx, bounds) {
|
|
1054
|
+
const formType = properties.formType ?? "box";
|
|
1055
|
+
const pos = properties.position;
|
|
1056
|
+
const center = toPixel(pos ?? { x: 0.5, y: 0.5 }, bounds);
|
|
1057
|
+
const formSize = properties.formSize ?? 0.25;
|
|
1058
|
+
const sizeX = properties.sizeX ?? 1;
|
|
1059
|
+
const sizeY = properties.sizeY ?? 1;
|
|
1060
|
+
const sizeZ = properties.sizeZ ?? 1;
|
|
1061
|
+
const rxDeg = properties.rotationX ?? 15;
|
|
1062
|
+
const ryDeg = properties.rotationY ?? 30;
|
|
1063
|
+
const rzDeg = properties.rotationZ ?? 0;
|
|
1064
|
+
const proj = properties.projection ?? "orthographic";
|
|
1065
|
+
const showContours = properties.showCrossContours ?? true;
|
|
1066
|
+
const contourCount = properties.crossContourCount ?? 5;
|
|
1067
|
+
const showAxes = properties.showAxes ?? true;
|
|
1068
|
+
const axisLen = properties.axisLength ?? 1.2;
|
|
1069
|
+
const showHidden = properties.showHiddenEdges ?? true;
|
|
1070
|
+
const hiddenStyle = properties.hiddenEdgeStyle ?? "dashed";
|
|
1071
|
+
const hiddenAlpha = properties.hiddenEdgeAlpha ?? 0.3;
|
|
1072
|
+
const edgeColor = properties.edgeColor ?? "rgba(0,200,255,0.7)";
|
|
1073
|
+
const contourColor = properties.contourColor ?? "rgba(100,255,100,0.5)";
|
|
1074
|
+
const axisColorsCSV = properties.axisColors ?? "red,green,blue";
|
|
1075
|
+
const scale = Math.min(bounds.width, bounds.height) * formSize;
|
|
1076
|
+
const matrix = rotationMatrix(rxDeg, ryDeg, rzDeg);
|
|
1077
|
+
const focalLength = 5;
|
|
1078
|
+
ctx.save();
|
|
1079
|
+
const baseOpts = {
|
|
1080
|
+
center,
|
|
1081
|
+
scale,
|
|
1082
|
+
sizeX,
|
|
1083
|
+
sizeY,
|
|
1084
|
+
sizeZ,
|
|
1085
|
+
matrix,
|
|
1086
|
+
projection: proj,
|
|
1087
|
+
focalLength,
|
|
1088
|
+
showHidden,
|
|
1089
|
+
hiddenStyle,
|
|
1090
|
+
hiddenAlpha,
|
|
1091
|
+
edgeColor
|
|
1092
|
+
};
|
|
1093
|
+
switch (formType) {
|
|
1094
|
+
case "box":
|
|
1095
|
+
renderBox(ctx, baseOpts);
|
|
1096
|
+
break;
|
|
1097
|
+
case "cylinder":
|
|
1098
|
+
renderCylinder(ctx, baseOpts);
|
|
1099
|
+
break;
|
|
1100
|
+
case "sphere":
|
|
1101
|
+
renderSphere(ctx, { ...baseOpts, radius: 0.5 });
|
|
1102
|
+
break;
|
|
1103
|
+
case "cone":
|
|
1104
|
+
renderCone(ctx, baseOpts);
|
|
1105
|
+
break;
|
|
1106
|
+
case "wedge":
|
|
1107
|
+
renderWedge(ctx, baseOpts);
|
|
1108
|
+
break;
|
|
1109
|
+
case "egg":
|
|
1110
|
+
renderEgg(ctx, baseOpts);
|
|
1111
|
+
break;
|
|
1112
|
+
}
|
|
1113
|
+
if (showContours) {
|
|
1114
|
+
ctx.strokeStyle = contourColor;
|
|
1115
|
+
ctx.lineWidth = 0.75;
|
|
1116
|
+
const hiddenDash = hiddenStyle === "dotted" ? [2, 3] : [6, 4];
|
|
1117
|
+
switch (formType) {
|
|
1118
|
+
case "box": {
|
|
1119
|
+
const contours = boxCrossContours(baseOpts, contourCount);
|
|
1120
|
+
for (const line of contours.front) {
|
|
1121
|
+
drawPolyline(ctx, line);
|
|
1122
|
+
}
|
|
1123
|
+
if (showHidden) {
|
|
1124
|
+
ctx.globalAlpha = hiddenAlpha;
|
|
1125
|
+
ctx.setLineDash(hiddenDash);
|
|
1126
|
+
for (const line of contours.hidden) {
|
|
1127
|
+
drawPolyline(ctx, line);
|
|
1128
|
+
}
|
|
1129
|
+
ctx.globalAlpha = 1;
|
|
1130
|
+
ctx.setLineDash([]);
|
|
1131
|
+
}
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
case "cylinder": {
|
|
1135
|
+
const contours = cylinderCrossContours(baseOpts, contourCount + 1);
|
|
1136
|
+
for (const c of contours) {
|
|
1137
|
+
drawEllipseWithHidden(ctx, c.params, c.frontHalf, hiddenAlpha, hiddenDash);
|
|
1138
|
+
}
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
case "sphere": {
|
|
1142
|
+
const latCount = Math.ceil(contourCount / 2);
|
|
1143
|
+
const lonCount = Math.floor(contourCount / 2);
|
|
1144
|
+
const contours = sphereCrossContours({ ...baseOpts, radius: 0.5 }, latCount + 1, lonCount);
|
|
1145
|
+
for (const c of contours) {
|
|
1146
|
+
drawEllipseWithHidden(ctx, c.params, c.frontHalf, hiddenAlpha, hiddenDash);
|
|
1147
|
+
}
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
case "cone": {
|
|
1151
|
+
const contours = coneCrossContours(baseOpts, contourCount + 1);
|
|
1152
|
+
for (const c of contours) {
|
|
1153
|
+
drawEllipseWithHidden(ctx, c.params, c.frontHalf, hiddenAlpha, hiddenDash);
|
|
1154
|
+
}
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
case "egg": {
|
|
1158
|
+
const contours = eggCrossContours(baseOpts, contourCount + 1);
|
|
1159
|
+
for (const c of contours) {
|
|
1160
|
+
drawEllipseWithHidden(ctx, c.params, c.frontHalf, hiddenAlpha, hiddenDash);
|
|
1161
|
+
}
|
|
1162
|
+
break;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (showAxes) {
|
|
1167
|
+
const axisColors = parseCSVColors(axisColorsCSV, 3);
|
|
1168
|
+
const axisScale = scale * axisLen;
|
|
1169
|
+
const axes = [
|
|
1170
|
+
{ dir: { x: 1, y: 0, z: 0 }, color: axisColors[0] },
|
|
1171
|
+
{ dir: { x: 0, y: 1, z: 0 }, color: axisColors[1] },
|
|
1172
|
+
{ dir: { x: 0, y: 0, z: 1 }, color: axisColors[2] }
|
|
1173
|
+
];
|
|
1174
|
+
for (const axis of axes) {
|
|
1175
|
+
const rotated = rotate3D(axis.dir, matrix);
|
|
1176
|
+
const p = project(rotated, proj, focalLength);
|
|
1177
|
+
const endX = center.x + p.x * axisScale;
|
|
1178
|
+
const endY = center.y - p.y * axisScale;
|
|
1179
|
+
ctx.strokeStyle = axis.color;
|
|
1180
|
+
ctx.lineWidth = 1.5;
|
|
1181
|
+
ctx.setLineDash([]);
|
|
1182
|
+
drawLine(ctx, center.x, center.y, endX, endY);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
ctx.restore();
|
|
1186
|
+
},
|
|
1187
|
+
validate(properties) {
|
|
1188
|
+
const errors = [];
|
|
1189
|
+
const formType = properties.formType;
|
|
1190
|
+
if (!["box", "cylinder", "sphere", "cone", "wedge", "egg"].includes(formType)) {
|
|
1191
|
+
errors.push({ property: "formType", message: "Invalid form type" });
|
|
1192
|
+
}
|
|
1193
|
+
const rx = properties.rotationX;
|
|
1194
|
+
if (typeof rx === "number" && (rx < -90 || rx > 90)) {
|
|
1195
|
+
errors.push({ property: "rotationX", message: "Must be -90 to 90" });
|
|
1196
|
+
}
|
|
1197
|
+
return errors.length > 0 ? errors : null;
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
// src/cross-contour-layer.ts
|
|
1202
|
+
var CROSS_CONTOUR_PROPERTIES = [
|
|
1203
|
+
{ key: "outline", label: "Outline Points (JSON)", type: "string", default: "[]", group: "shape" },
|
|
1204
|
+
{ key: "axis", label: "Axis Points (JSON)", type: "string", default: "[]", group: "shape" },
|
|
1205
|
+
{ key: "contourCount", label: "Contour Count", type: "number", default: 8, min: 2, max: 20, step: 1, group: "contours" },
|
|
1206
|
+
{ key: "curvature", label: "Curvature", type: "number", default: 0.5, min: 0, max: 1, step: 0.05, group: "contours" },
|
|
1207
|
+
{ key: "curvatureVariation", label: "Curvature Variation (JSON)", type: "string", default: "[]", group: "contours" },
|
|
1208
|
+
{
|
|
1209
|
+
key: "contourStyle",
|
|
1210
|
+
label: "Contour Style",
|
|
1211
|
+
type: "select",
|
|
1212
|
+
default: "elliptical",
|
|
1213
|
+
options: [
|
|
1214
|
+
{ value: "elliptical", label: "Elliptical" },
|
|
1215
|
+
{ value: "angular", label: "Angular" },
|
|
1216
|
+
{ value: "organic", label: "Organic" }
|
|
1217
|
+
],
|
|
1218
|
+
group: "contours"
|
|
1219
|
+
},
|
|
1220
|
+
{ key: "showAxis", label: "Show Axis", type: "boolean", default: true, group: "display" },
|
|
1221
|
+
{ key: "showOutline", label: "Show Outline", type: "boolean", default: true, group: "display" },
|
|
1222
|
+
{
|
|
1223
|
+
key: "wrapDirection",
|
|
1224
|
+
label: "Wrap Direction",
|
|
1225
|
+
type: "select",
|
|
1226
|
+
default: "perpendicular",
|
|
1227
|
+
options: [{ value: "perpendicular", label: "Perpendicular" }, { value: "custom", label: "Custom" }],
|
|
1228
|
+
group: "contours"
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
key: "contourSpacing",
|
|
1232
|
+
label: "Contour Spacing",
|
|
1233
|
+
type: "select",
|
|
1234
|
+
default: "even",
|
|
1235
|
+
options: [{ value: "even", label: "Even" }, { value: "perspective", label: "Perspective" }],
|
|
1236
|
+
group: "contours"
|
|
1237
|
+
},
|
|
1238
|
+
...COMMON_GUIDE_PROPERTIES
|
|
1239
|
+
];
|
|
1240
|
+
var crossContourLayerType = {
|
|
1241
|
+
typeId: "construction:cross-contour",
|
|
1242
|
+
displayName: "Cross-Contour Lines",
|
|
1243
|
+
icon: "waves",
|
|
1244
|
+
category: "guide",
|
|
1245
|
+
properties: CROSS_CONTOUR_PROPERTIES,
|
|
1246
|
+
propertyEditorId: "construction:cross-contour-editor",
|
|
1247
|
+
createDefault() {
|
|
1248
|
+
const props = {};
|
|
1249
|
+
for (const schema of CROSS_CONTOUR_PROPERTIES) {
|
|
1250
|
+
props[schema.key] = schema.default;
|
|
1251
|
+
}
|
|
1252
|
+
return props;
|
|
1253
|
+
},
|
|
1254
|
+
render(properties, ctx, bounds) {
|
|
1255
|
+
const outlineNorm = parseJSON(properties.outline ?? "[]", []);
|
|
1256
|
+
const axisNorm = parseJSON(properties.axis ?? "[]", []);
|
|
1257
|
+
if (axisNorm.length < 2) return;
|
|
1258
|
+
const contourCount = properties.contourCount ?? 8;
|
|
1259
|
+
const curvature = properties.curvature ?? 0.5;
|
|
1260
|
+
const curvatureVar = parseJSON(properties.curvatureVariation ?? "[]", []);
|
|
1261
|
+
const contourStyle = properties.contourStyle ?? "elliptical";
|
|
1262
|
+
const showAxis = properties.showAxis ?? true;
|
|
1263
|
+
const showOutline = properties.showOutline ?? true;
|
|
1264
|
+
const spacing = properties.contourSpacing ?? "even";
|
|
1265
|
+
const color = properties.guideColor ?? "rgba(0,200,255,0.5)";
|
|
1266
|
+
const lineWidth = properties.lineWidth ?? 1;
|
|
1267
|
+
const toPixelPt = (p) => ({
|
|
1268
|
+
x: bounds.x + p.x * bounds.width,
|
|
1269
|
+
y: bounds.y + p.y * bounds.height
|
|
1270
|
+
});
|
|
1271
|
+
const outline = outlineNorm.map(toPixelPt);
|
|
1272
|
+
const axis = axisNorm.map(toPixelPt);
|
|
1273
|
+
ctx.save();
|
|
1274
|
+
if (showOutline && outline.length >= 2) {
|
|
1275
|
+
ctx.strokeStyle = color;
|
|
1276
|
+
ctx.lineWidth = lineWidth * 0.7;
|
|
1277
|
+
ctx.globalAlpha = 0.5;
|
|
1278
|
+
ctx.setLineDash([4, 3]);
|
|
1279
|
+
drawPolyline(ctx, outline, true);
|
|
1280
|
+
ctx.globalAlpha = 1;
|
|
1281
|
+
}
|
|
1282
|
+
if (showAxis && axis.length >= 2) {
|
|
1283
|
+
ctx.strokeStyle = color;
|
|
1284
|
+
ctx.lineWidth = lineWidth * 0.5;
|
|
1285
|
+
ctx.setLineDash([3, 5]);
|
|
1286
|
+
ctx.globalAlpha = 0.4;
|
|
1287
|
+
drawPolyline(ctx, axis);
|
|
1288
|
+
ctx.globalAlpha = 1;
|
|
1289
|
+
}
|
|
1290
|
+
const axisPoints = interpolatePolyline(axis, 200);
|
|
1291
|
+
setupGuideStyle(ctx, color, lineWidth, "");
|
|
1292
|
+
ctx.setLineDash([]);
|
|
1293
|
+
for (let i = 0; i < contourCount; i++) {
|
|
1294
|
+
let t;
|
|
1295
|
+
if (spacing === "perspective") {
|
|
1296
|
+
const raw = (i + 1) / (contourCount + 1);
|
|
1297
|
+
t = 0.5 + (raw - 0.5) * Math.sqrt(Math.abs(raw - 0.5) * 2) * Math.sign(raw - 0.5);
|
|
1298
|
+
} else {
|
|
1299
|
+
t = (i + 1) / (contourCount + 1);
|
|
1300
|
+
}
|
|
1301
|
+
const axisIdx = Math.floor(t * (axisPoints.length - 1));
|
|
1302
|
+
const axisPoint = axisPoints[Math.min(axisIdx, axisPoints.length - 1)];
|
|
1303
|
+
const prevIdx = Math.max(0, axisIdx - 1);
|
|
1304
|
+
const nextIdx = Math.min(axisPoints.length - 1, axisIdx + 1);
|
|
1305
|
+
const tangent = {
|
|
1306
|
+
x: axisPoints[nextIdx].x - axisPoints[prevIdx].x,
|
|
1307
|
+
y: axisPoints[nextIdx].y - axisPoints[prevIdx].y
|
|
1308
|
+
};
|
|
1309
|
+
const tangentLen = Math.sqrt(tangent.x ** 2 + tangent.y ** 2);
|
|
1310
|
+
if (tangentLen < 1e-3) continue;
|
|
1311
|
+
const perpX = -tangent.y / tangentLen;
|
|
1312
|
+
const perpY = tangent.x / tangentLen;
|
|
1313
|
+
const leftEdge = findOutlineIntersection(axisPoint, { x: perpX, y: perpY }, outline);
|
|
1314
|
+
const rightEdge = findOutlineIntersection(axisPoint, { x: -perpX, y: -perpY }, outline);
|
|
1315
|
+
if (!leftEdge || !rightEdge) continue;
|
|
1316
|
+
const curv = curvatureVar[i] ?? curvature;
|
|
1317
|
+
const contourLine = generateContourLine(leftEdge, rightEdge, axisPoint, curv, contourStyle);
|
|
1318
|
+
drawPolyline(ctx, contourLine);
|
|
1319
|
+
}
|
|
1320
|
+
ctx.restore();
|
|
1321
|
+
},
|
|
1322
|
+
validate(properties) {
|
|
1323
|
+
const errors = [];
|
|
1324
|
+
const count = properties.contourCount;
|
|
1325
|
+
if (typeof count === "number" && (count < 2 || count > 20)) {
|
|
1326
|
+
errors.push({ property: "contourCount", message: "Must be 2-20" });
|
|
1327
|
+
}
|
|
1328
|
+
return errors.length > 0 ? errors : null;
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
function polylineLength(points) {
|
|
1332
|
+
let len = 0;
|
|
1333
|
+
for (let i = 1; i < points.length; i++) len += dist2(points[i - 1], points[i]);
|
|
1334
|
+
return len;
|
|
1335
|
+
}
|
|
1336
|
+
function interpolatePolyline(points, targetCount) {
|
|
1337
|
+
if (points.length < 2) return points;
|
|
1338
|
+
const totalLen = polylineLength(points);
|
|
1339
|
+
if (totalLen < 1e-3) return points;
|
|
1340
|
+
const result = [];
|
|
1341
|
+
const step = totalLen / (targetCount - 1);
|
|
1342
|
+
let segIdx = 0;
|
|
1343
|
+
let segStart = 0;
|
|
1344
|
+
for (let i = 0; i < targetCount; i++) {
|
|
1345
|
+
const target = i * step;
|
|
1346
|
+
while (segIdx < points.length - 2) {
|
|
1347
|
+
const segLen2 = dist2(points[segIdx], points[segIdx + 1]);
|
|
1348
|
+
if (segStart + segLen2 >= target) break;
|
|
1349
|
+
segStart += segLen2;
|
|
1350
|
+
segIdx++;
|
|
1351
|
+
}
|
|
1352
|
+
const segLen = dist2(points[segIdx], points[segIdx + 1]);
|
|
1353
|
+
const t = segLen > 0 ? (target - segStart) / segLen : 0;
|
|
1354
|
+
result.push(lerp2(points[segIdx], points[segIdx + 1], Math.min(1, Math.max(0, t))));
|
|
1355
|
+
}
|
|
1356
|
+
return result;
|
|
1357
|
+
}
|
|
1358
|
+
function findOutlineIntersection(origin, direction, outline) {
|
|
1359
|
+
let bestT = Infinity;
|
|
1360
|
+
let bestPoint = null;
|
|
1361
|
+
for (let i = 0; i < outline.length; i++) {
|
|
1362
|
+
const a = outline[i];
|
|
1363
|
+
const b = outline[(i + 1) % outline.length];
|
|
1364
|
+
const dx = b.x - a.x, dy = b.y - a.y;
|
|
1365
|
+
const denom = direction.x * dy - direction.y * dx;
|
|
1366
|
+
if (Math.abs(denom) < 1e-10) continue;
|
|
1367
|
+
const t = ((a.x - origin.x) * dy - (a.y - origin.y) * dx) / denom;
|
|
1368
|
+
const u = ((a.x - origin.x) * direction.y - (a.y - origin.y) * direction.x) / denom;
|
|
1369
|
+
if (t > 0 && u >= 0 && u <= 1 && t < bestT) {
|
|
1370
|
+
bestT = t;
|
|
1371
|
+
bestPoint = { x: origin.x + direction.x * t, y: origin.y + direction.y * t };
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return bestPoint;
|
|
1375
|
+
}
|
|
1376
|
+
function generateContourLine(left, right, axisPoint, curvature, style) {
|
|
1377
|
+
const segments = 20;
|
|
1378
|
+
const points = [];
|
|
1379
|
+
const lrDx = right.x - left.x, lrDy = right.y - left.y;
|
|
1380
|
+
const lrLen = Math.sqrt(lrDx * lrDx + lrDy * lrDy);
|
|
1381
|
+
if (lrLen < 1e-3) return [left, right];
|
|
1382
|
+
const perpX = -lrDy / lrLen, perpY = lrDx / lrLen;
|
|
1383
|
+
const toAxisX = axisPoint.x - (left.x + right.x) / 2;
|
|
1384
|
+
const toAxisY = axisPoint.y - (left.y + right.y) / 2;
|
|
1385
|
+
const sign = perpX * toAxisX + perpY * toAxisY > 0 ? 1 : -1;
|
|
1386
|
+
for (let i = 0; i <= segments; i++) {
|
|
1387
|
+
const t = i / segments;
|
|
1388
|
+
const baseX = left.x + (right.x - left.x) * t;
|
|
1389
|
+
const baseY = left.y + (right.y - left.y) * t;
|
|
1390
|
+
let offset;
|
|
1391
|
+
if (style === "angular") {
|
|
1392
|
+
offset = curvature * (1 - Math.abs(t - 0.5) * 2) * lrLen * 0.3 * sign;
|
|
1393
|
+
} else {
|
|
1394
|
+
offset = curvature * Math.sin(t * Math.PI) * lrLen * 0.3 * sign;
|
|
1395
|
+
}
|
|
1396
|
+
points.push({ x: baseX + perpX * offset, y: baseY + perpY * offset });
|
|
1397
|
+
}
|
|
1398
|
+
return points;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/math/shadow.ts
|
|
1402
|
+
var DEG2RAD3 = Math.PI / 180;
|
|
1403
|
+
function lightDirection(light) {
|
|
1404
|
+
const az = light.azimuth * DEG2RAD3;
|
|
1405
|
+
const el = light.elevation * DEG2RAD3;
|
|
1406
|
+
const cosEl = Math.cos(el);
|
|
1407
|
+
return normalize3({
|
|
1408
|
+
x: -Math.cos(az) * cosEl,
|
|
1409
|
+
y: -Math.sin(el),
|
|
1410
|
+
z: -Math.sin(az) * cosEl
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
function lightDirection2D(light) {
|
|
1414
|
+
const az = light.azimuth * DEG2RAD3;
|
|
1415
|
+
return { x: Math.cos(az), y: Math.sin(az) };
|
|
1416
|
+
}
|
|
1417
|
+
function sphereTerminator(center, radius, light, matrix) {
|
|
1418
|
+
const ld = lightDirection(light);
|
|
1419
|
+
const rotatedLD = rotate3D(ld, matrix);
|
|
1420
|
+
const ldScreen = { x: rotatedLD.x, y: -rotatedLD.y };
|
|
1421
|
+
const perpAngle = Math.atan2(ldScreen.y, ldScreen.x) + Math.PI / 2;
|
|
1422
|
+
const viewAngle = Math.acos(Math.min(1, Math.abs(rotatedLD.z)));
|
|
1423
|
+
const minorRadius = radius * Math.sin(viewAngle);
|
|
1424
|
+
return {
|
|
1425
|
+
terminatorEllipse: {
|
|
1426
|
+
cx: center.x,
|
|
1427
|
+
cy: center.y,
|
|
1428
|
+
rx: radius,
|
|
1429
|
+
ry: minorRadius,
|
|
1430
|
+
rotation: perpAngle
|
|
1431
|
+
},
|
|
1432
|
+
lightSide: { x: -ldScreen.x, y: -ldScreen.y }
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
function castShadow(center, radius, light, groundY) {
|
|
1436
|
+
const ld = lightDirection(light);
|
|
1437
|
+
const ld2d = lightDirection2D(light);
|
|
1438
|
+
const shadowLength = radius * (1 / Math.tan(light.elevation * DEG2RAD3));
|
|
1439
|
+
const shadowDirX = -ld2d.x;
|
|
1440
|
+
const shadowDirY = 0;
|
|
1441
|
+
const points = [];
|
|
1442
|
+
const segments = 24;
|
|
1443
|
+
for (let i = 0; i <= segments; i++) {
|
|
1444
|
+
const angle = i / segments * Math.PI * 2;
|
|
1445
|
+
const bx = Math.cos(angle) * radius;
|
|
1446
|
+
const by = Math.sin(angle) * radius * 0.3;
|
|
1447
|
+
const sx = center.x + bx + shadowDirX * shadowLength * 0.5;
|
|
1448
|
+
const sy = groundY + by;
|
|
1449
|
+
points.push({ x: sx, y: sy });
|
|
1450
|
+
}
|
|
1451
|
+
return points;
|
|
1452
|
+
}
|
|
1453
|
+
function sphereValueZones(center, radius, light, matrix, grouping) {
|
|
1454
|
+
const zones = [];
|
|
1455
|
+
const ld = lightDirection(light);
|
|
1456
|
+
const rotatedLD = rotate3D(ld, matrix);
|
|
1457
|
+
const ldScreen = { x: -rotatedLD.x, y: rotatedLD.y };
|
|
1458
|
+
const terminatorAngle = Math.atan2(ldScreen.y, ldScreen.x);
|
|
1459
|
+
const segments = 32;
|
|
1460
|
+
if (grouping === "two-value") {
|
|
1461
|
+
zones.push({
|
|
1462
|
+
type: "light",
|
|
1463
|
+
path: generateArcPath(center, radius, terminatorAngle - Math.PI / 2, terminatorAngle + Math.PI / 2, segments),
|
|
1464
|
+
value: 0.8,
|
|
1465
|
+
label: "Light"
|
|
1466
|
+
});
|
|
1467
|
+
zones.push({
|
|
1468
|
+
type: "core-shadow",
|
|
1469
|
+
path: generateArcPath(center, radius, terminatorAngle + Math.PI / 2, terminatorAngle + Math.PI * 1.5, segments),
|
|
1470
|
+
value: 0.2,
|
|
1471
|
+
label: "Shadow"
|
|
1472
|
+
});
|
|
1473
|
+
} else if (grouping === "three-value") {
|
|
1474
|
+
zones.push({
|
|
1475
|
+
type: "light",
|
|
1476
|
+
path: generateArcPath(center, radius, terminatorAngle - Math.PI / 2, terminatorAngle, segments),
|
|
1477
|
+
value: 0.85,
|
|
1478
|
+
label: "Light"
|
|
1479
|
+
});
|
|
1480
|
+
zones.push({
|
|
1481
|
+
type: "halftone",
|
|
1482
|
+
path: generateArcPath(center, radius, terminatorAngle, terminatorAngle + Math.PI / 3, segments),
|
|
1483
|
+
value: 0.5,
|
|
1484
|
+
label: "Halftone"
|
|
1485
|
+
});
|
|
1486
|
+
zones.push({
|
|
1487
|
+
type: "core-shadow",
|
|
1488
|
+
path: generateArcPath(center, radius, terminatorAngle + Math.PI / 3, terminatorAngle + Math.PI * 1.5, segments),
|
|
1489
|
+
value: 0.15,
|
|
1490
|
+
label: "Shadow"
|
|
1491
|
+
});
|
|
1492
|
+
} else {
|
|
1493
|
+
const lightEnd = terminatorAngle - Math.PI / 6;
|
|
1494
|
+
const highlightEnd = terminatorAngle - Math.PI / 3;
|
|
1495
|
+
zones.push({
|
|
1496
|
+
type: "highlight",
|
|
1497
|
+
path: generateArcPath(center, radius, terminatorAngle - Math.PI / 2, highlightEnd, segments),
|
|
1498
|
+
value: 0.95,
|
|
1499
|
+
label: "Highlight"
|
|
1500
|
+
});
|
|
1501
|
+
zones.push({
|
|
1502
|
+
type: "light",
|
|
1503
|
+
path: generateArcPath(center, radius, highlightEnd, lightEnd, segments),
|
|
1504
|
+
value: 0.8,
|
|
1505
|
+
label: "Light"
|
|
1506
|
+
});
|
|
1507
|
+
zones.push({
|
|
1508
|
+
type: "halftone",
|
|
1509
|
+
path: generateArcPath(center, radius, lightEnd, terminatorAngle + Math.PI / 6, segments),
|
|
1510
|
+
value: 0.5,
|
|
1511
|
+
label: "Halftone"
|
|
1512
|
+
});
|
|
1513
|
+
zones.push({
|
|
1514
|
+
type: "core-shadow",
|
|
1515
|
+
path: generateArcPath(center, radius, terminatorAngle + Math.PI / 6, terminatorAngle + Math.PI * 0.6, segments),
|
|
1516
|
+
value: 0.1,
|
|
1517
|
+
label: "Core Shadow"
|
|
1518
|
+
});
|
|
1519
|
+
zones.push({
|
|
1520
|
+
type: "reflected-light",
|
|
1521
|
+
path: generateArcPath(center, radius, terminatorAngle + Math.PI * 0.6, terminatorAngle + Math.PI * 1.5, segments),
|
|
1522
|
+
value: 0.3,
|
|
1523
|
+
label: "Reflected Light"
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
return zones;
|
|
1527
|
+
}
|
|
1528
|
+
function generateArcPath(center, radius, startAngle, endAngle, segments) {
|
|
1529
|
+
const points = [{ x: center.x, y: center.y }];
|
|
1530
|
+
for (let i = 0; i <= segments; i++) {
|
|
1531
|
+
const t = i / segments;
|
|
1532
|
+
const angle = startAngle + (endAngle - startAngle) * t;
|
|
1533
|
+
points.push({
|
|
1534
|
+
x: center.x + Math.cos(angle) * radius,
|
|
1535
|
+
y: center.y + Math.sin(angle) * radius
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
return points;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/value-shapes-layer.ts
|
|
1542
|
+
var VALUE_SHAPES_PROPERTIES = [
|
|
1543
|
+
{
|
|
1544
|
+
key: "formData",
|
|
1545
|
+
label: "Forms (JSON)",
|
|
1546
|
+
type: "string",
|
|
1547
|
+
default: "[]",
|
|
1548
|
+
group: "forms"
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
key: "lightAzimuth",
|
|
1552
|
+
label: "Light Azimuth",
|
|
1553
|
+
type: "number",
|
|
1554
|
+
default: 315,
|
|
1555
|
+
min: 0,
|
|
1556
|
+
max: 360,
|
|
1557
|
+
step: 15,
|
|
1558
|
+
group: "light"
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
key: "lightElevation",
|
|
1562
|
+
label: "Light Elevation",
|
|
1563
|
+
type: "number",
|
|
1564
|
+
default: 45,
|
|
1565
|
+
min: 10,
|
|
1566
|
+
max: 80,
|
|
1567
|
+
step: 5,
|
|
1568
|
+
group: "light"
|
|
1569
|
+
},
|
|
1570
|
+
{
|
|
1571
|
+
key: "lightIntensity",
|
|
1572
|
+
label: "Light Intensity",
|
|
1573
|
+
type: "number",
|
|
1574
|
+
default: 0.8,
|
|
1575
|
+
min: 0.1,
|
|
1576
|
+
max: 1,
|
|
1577
|
+
step: 0.05,
|
|
1578
|
+
group: "light"
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
key: "showLightIndicator",
|
|
1582
|
+
label: "Show Light Indicator",
|
|
1583
|
+
type: "boolean",
|
|
1584
|
+
default: true,
|
|
1585
|
+
group: "display"
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
key: "valueGrouping",
|
|
1589
|
+
label: "Value Grouping",
|
|
1590
|
+
type: "select",
|
|
1591
|
+
default: "three-value",
|
|
1592
|
+
options: [
|
|
1593
|
+
{ value: "two-value", label: "2-Value (Light/Shadow)" },
|
|
1594
|
+
{ value: "three-value", label: "3-Value (Light/Half/Shadow)" },
|
|
1595
|
+
{ value: "five-value", label: "5-Value (Full Anatomy)" }
|
|
1596
|
+
],
|
|
1597
|
+
group: "display"
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
key: "shadowColor",
|
|
1601
|
+
label: "Shadow Color",
|
|
1602
|
+
type: "color",
|
|
1603
|
+
default: "rgba(0,0,0,0.3)",
|
|
1604
|
+
group: "style"
|
|
1605
|
+
},
|
|
1606
|
+
{
|
|
1607
|
+
key: "lightColor",
|
|
1608
|
+
label: "Light Color",
|
|
1609
|
+
type: "color",
|
|
1610
|
+
default: "rgba(255,255,200,0.15)",
|
|
1611
|
+
group: "style"
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
key: "halftoneColor",
|
|
1615
|
+
label: "Halftone Color",
|
|
1616
|
+
type: "color",
|
|
1617
|
+
default: "rgba(0,0,0,0.12)",
|
|
1618
|
+
group: "style"
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
key: "highlightColor",
|
|
1622
|
+
label: "Highlight Color",
|
|
1623
|
+
type: "color",
|
|
1624
|
+
default: "rgba(255,255,255,0.25)",
|
|
1625
|
+
group: "style"
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
key: "reflectedLightColor",
|
|
1629
|
+
label: "Reflected Light Color",
|
|
1630
|
+
type: "color",
|
|
1631
|
+
default: "rgba(100,100,120,0.15)",
|
|
1632
|
+
group: "style"
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
key: "showTerminator",
|
|
1636
|
+
label: "Show Terminator",
|
|
1637
|
+
type: "boolean",
|
|
1638
|
+
default: true,
|
|
1639
|
+
group: "display"
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
key: "terminatorWidth",
|
|
1643
|
+
label: "Terminator Width",
|
|
1644
|
+
type: "number",
|
|
1645
|
+
default: 2,
|
|
1646
|
+
min: 1,
|
|
1647
|
+
max: 5,
|
|
1648
|
+
step: 0.5,
|
|
1649
|
+
group: "display"
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
key: "showCastShadow",
|
|
1653
|
+
label: "Show Cast Shadow",
|
|
1654
|
+
type: "boolean",
|
|
1655
|
+
default: true,
|
|
1656
|
+
group: "display"
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
key: "showOcclusionShadow",
|
|
1660
|
+
label: "Show Occlusion Shadow",
|
|
1661
|
+
type: "boolean",
|
|
1662
|
+
default: true,
|
|
1663
|
+
group: "display"
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
key: "showZoneLabels",
|
|
1667
|
+
label: "Show Zone Labels",
|
|
1668
|
+
type: "boolean",
|
|
1669
|
+
default: false,
|
|
1670
|
+
group: "display"
|
|
1671
|
+
},
|
|
1672
|
+
{
|
|
1673
|
+
key: "groundPlaneY",
|
|
1674
|
+
label: "Ground Plane Y",
|
|
1675
|
+
type: "number",
|
|
1676
|
+
default: 0.8,
|
|
1677
|
+
min: 0,
|
|
1678
|
+
max: 1,
|
|
1679
|
+
step: 0.05,
|
|
1680
|
+
group: "light"
|
|
1681
|
+
},
|
|
1682
|
+
...COMMON_GUIDE_PROPERTIES
|
|
1683
|
+
];
|
|
1684
|
+
var valueShapesLayerType = {
|
|
1685
|
+
typeId: "construction:value-shapes",
|
|
1686
|
+
displayName: "Value Shapes Study",
|
|
1687
|
+
icon: "sun",
|
|
1688
|
+
category: "guide",
|
|
1689
|
+
properties: VALUE_SHAPES_PROPERTIES,
|
|
1690
|
+
propertyEditorId: "construction:value-shapes-editor",
|
|
1691
|
+
createDefault() {
|
|
1692
|
+
const props = {};
|
|
1693
|
+
for (const schema of VALUE_SHAPES_PROPERTIES) {
|
|
1694
|
+
props[schema.key] = schema.default;
|
|
1695
|
+
}
|
|
1696
|
+
return props;
|
|
1697
|
+
},
|
|
1698
|
+
render(properties, ctx, bounds) {
|
|
1699
|
+
const formDataJSON = properties.formData ?? "[]";
|
|
1700
|
+
const forms = parseJSON(formDataJSON, []);
|
|
1701
|
+
const lightAzimuth = properties.lightAzimuth ?? 315;
|
|
1702
|
+
const lightElevation = properties.lightElevation ?? 45;
|
|
1703
|
+
const lightIntensity = properties.lightIntensity ?? 0.8;
|
|
1704
|
+
const showIndicator = properties.showLightIndicator ?? true;
|
|
1705
|
+
const grouping = properties.valueGrouping ?? "three-value";
|
|
1706
|
+
const shadowColor = properties.shadowColor ?? "rgba(0,0,0,0.3)";
|
|
1707
|
+
const lightColor = properties.lightColor ?? "rgba(255,255,200,0.15)";
|
|
1708
|
+
const halftoneColor = properties.halftoneColor ?? "rgba(0,0,0,0.12)";
|
|
1709
|
+
const highlightColor = properties.highlightColor ?? "rgba(255,255,255,0.25)";
|
|
1710
|
+
const reflectedLightColor = properties.reflectedLightColor ?? "rgba(100,100,120,0.15)";
|
|
1711
|
+
const showTerminator = properties.showTerminator ?? true;
|
|
1712
|
+
const terminatorWidth = properties.terminatorWidth ?? 2;
|
|
1713
|
+
const showCastShadow = properties.showCastShadow ?? true;
|
|
1714
|
+
const showOcclusion = properties.showOcclusionShadow ?? true;
|
|
1715
|
+
const showLabels = properties.showZoneLabels ?? false;
|
|
1716
|
+
const groundPlaneY = properties.groundPlaneY ?? 0.8;
|
|
1717
|
+
const light = {
|
|
1718
|
+
azimuth: lightAzimuth,
|
|
1719
|
+
elevation: lightElevation,
|
|
1720
|
+
intensity: lightIntensity
|
|
1721
|
+
};
|
|
1722
|
+
ctx.save();
|
|
1723
|
+
const effectiveForms = forms.length > 0 ? forms : [{ type: "sphere", position: { x: 0, y: 0, z: 0 }, size: { x: 1, y: 1, z: 1 }, rotation: { x: 0, y: 0, z: 0 } }];
|
|
1724
|
+
for (const form of effectiveForms) {
|
|
1725
|
+
const centerNorm = {
|
|
1726
|
+
x: 0.5 + form.position.x * 0.3,
|
|
1727
|
+
y: 0.5 - form.position.y * 0.3
|
|
1728
|
+
};
|
|
1729
|
+
const center = toPixel(centerNorm, bounds);
|
|
1730
|
+
const radius = Math.min(bounds.width, bounds.height) * 0.15 * form.size.x;
|
|
1731
|
+
const matrix = rotationMatrix(form.rotation.x, form.rotation.y, form.rotation.z);
|
|
1732
|
+
const groundPx = bounds.y + groundPlaneY * bounds.height;
|
|
1733
|
+
const zones = sphereValueZones(center, radius, light, matrix, grouping);
|
|
1734
|
+
const colorMap = {
|
|
1735
|
+
"highlight": highlightColor,
|
|
1736
|
+
"light": lightColor,
|
|
1737
|
+
"halftone": halftoneColor,
|
|
1738
|
+
"core-shadow": shadowColor,
|
|
1739
|
+
"reflected-light": reflectedLightColor,
|
|
1740
|
+
"cast-shadow": shadowColor,
|
|
1741
|
+
"occlusion-shadow": shadowColor
|
|
1742
|
+
};
|
|
1743
|
+
for (const zone of zones) {
|
|
1744
|
+
fillPolyline(ctx, zone.path, colorMap[zone.type] ?? shadowColor);
|
|
1745
|
+
}
|
|
1746
|
+
if (showTerminator) {
|
|
1747
|
+
const term = sphereTerminator(center, radius, light, matrix);
|
|
1748
|
+
ctx.strokeStyle = "rgba(255,100,0,0.6)";
|
|
1749
|
+
ctx.lineWidth = terminatorWidth;
|
|
1750
|
+
ctx.setLineDash([]);
|
|
1751
|
+
drawEllipse(
|
|
1752
|
+
ctx,
|
|
1753
|
+
term.terminatorEllipse.cx,
|
|
1754
|
+
term.terminatorEllipse.cy,
|
|
1755
|
+
term.terminatorEllipse.rx,
|
|
1756
|
+
term.terminatorEllipse.ry,
|
|
1757
|
+
term.terminatorEllipse.rotation
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
if (showCastShadow) {
|
|
1761
|
+
const shadow = castShadow(center, radius, light, groundPx);
|
|
1762
|
+
fillPolyline(ctx, shadow, "rgba(0,0,0,0.15)");
|
|
1763
|
+
}
|
|
1764
|
+
if (showOcclusion) {
|
|
1765
|
+
const occRadius = radius * 0.15;
|
|
1766
|
+
ctx.beginPath();
|
|
1767
|
+
ctx.ellipse(center.x, center.y + radius, occRadius * 3, occRadius, 0, 0, Math.PI * 2);
|
|
1768
|
+
ctx.fillStyle = "rgba(0,0,0,0.25)";
|
|
1769
|
+
ctx.fill();
|
|
1770
|
+
}
|
|
1771
|
+
if (showLabels) {
|
|
1772
|
+
for (const zone of zones) {
|
|
1773
|
+
if (zone.path.length > 2) {
|
|
1774
|
+
let cx = 0, cy = 0;
|
|
1775
|
+
for (const p of zone.path) {
|
|
1776
|
+
cx += p.x;
|
|
1777
|
+
cy += p.y;
|
|
1778
|
+
}
|
|
1779
|
+
cx /= zone.path.length;
|
|
1780
|
+
cy /= zone.path.length;
|
|
1781
|
+
drawLabel(ctx, zone.label, cx, cy, "rgba(255,255,255,0.8)", 9);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
ctx.strokeStyle = "rgba(0,200,255,0.5)";
|
|
1786
|
+
ctx.lineWidth = 1;
|
|
1787
|
+
ctx.setLineDash([6, 4]);
|
|
1788
|
+
ctx.beginPath();
|
|
1789
|
+
ctx.arc(center.x, center.y, radius, 0, Math.PI * 2);
|
|
1790
|
+
ctx.stroke();
|
|
1791
|
+
}
|
|
1792
|
+
if (showIndicator) {
|
|
1793
|
+
const ld2d = lightDirection2D(light);
|
|
1794
|
+
const indX = bounds.x + bounds.width - 30;
|
|
1795
|
+
const indY = bounds.y + 30;
|
|
1796
|
+
const indLen = 15;
|
|
1797
|
+
ctx.strokeStyle = "rgba(255,220,50,0.8)";
|
|
1798
|
+
ctx.lineWidth = 2;
|
|
1799
|
+
ctx.setLineDash([]);
|
|
1800
|
+
drawLine(ctx, indX, indY, indX + ld2d.x * indLen, indY + ld2d.y * indLen);
|
|
1801
|
+
ctx.beginPath();
|
|
1802
|
+
ctx.arc(indX, indY, 6, 0, Math.PI * 2);
|
|
1803
|
+
ctx.fillStyle = "rgba(255,220,50,0.6)";
|
|
1804
|
+
ctx.fill();
|
|
1805
|
+
ctx.stroke();
|
|
1806
|
+
}
|
|
1807
|
+
ctx.restore();
|
|
1808
|
+
},
|
|
1809
|
+
validate(properties) {
|
|
1810
|
+
const errors = [];
|
|
1811
|
+
const el = properties.lightElevation;
|
|
1812
|
+
if (typeof el === "number" && (el < 10 || el > 80)) {
|
|
1813
|
+
errors.push({ property: "lightElevation", message: "Must be 10-80" });
|
|
1814
|
+
}
|
|
1815
|
+
return errors.length > 0 ? errors : null;
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
// src/math/envelope.ts
|
|
1820
|
+
function convexHull(points) {
|
|
1821
|
+
if (points.length < 3) return [...points];
|
|
1822
|
+
const sorted = [...points].sort((a, b) => a.x - b.x || a.y - b.y);
|
|
1823
|
+
const lower = [];
|
|
1824
|
+
for (const p of sorted) {
|
|
1825
|
+
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
|
|
1826
|
+
lower.pop();
|
|
1827
|
+
}
|
|
1828
|
+
lower.push(p);
|
|
1829
|
+
}
|
|
1830
|
+
const upper = [];
|
|
1831
|
+
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
1832
|
+
const p = sorted[i];
|
|
1833
|
+
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
|
|
1834
|
+
upper.pop();
|
|
1835
|
+
}
|
|
1836
|
+
upper.push(p);
|
|
1837
|
+
}
|
|
1838
|
+
lower.pop();
|
|
1839
|
+
upper.pop();
|
|
1840
|
+
return lower.concat(upper);
|
|
1841
|
+
}
|
|
1842
|
+
function cross(o, a, b) {
|
|
1843
|
+
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
1844
|
+
}
|
|
1845
|
+
function computeEnvelope(points, style) {
|
|
1846
|
+
if (points.length < 3) return [...points];
|
|
1847
|
+
if (style === "fitted") return [...points];
|
|
1848
|
+
const hull = convexHull(points);
|
|
1849
|
+
if (style === "loose") {
|
|
1850
|
+
const cx = hull.reduce((s, p) => s + p.x, 0) / hull.length;
|
|
1851
|
+
const cy = hull.reduce((s, p) => s + p.y, 0) / hull.length;
|
|
1852
|
+
const expand = 0.05;
|
|
1853
|
+
return hull.map((p) => ({
|
|
1854
|
+
x: p.x + (p.x - cx) * expand,
|
|
1855
|
+
y: p.y + (p.y - cy) * expand
|
|
1856
|
+
}));
|
|
1857
|
+
}
|
|
1858
|
+
return hull;
|
|
1859
|
+
}
|
|
1860
|
+
function envelopeAngles(vertices) {
|
|
1861
|
+
const n = vertices.length;
|
|
1862
|
+
if (n < 3) return [];
|
|
1863
|
+
const result = [];
|
|
1864
|
+
for (let i = 0; i < n; i++) {
|
|
1865
|
+
const prev = vertices[(i - 1 + n) % n];
|
|
1866
|
+
const curr = vertices[i];
|
|
1867
|
+
const next = vertices[(i + 1) % n];
|
|
1868
|
+
const dx1 = prev.x - curr.x, dy1 = prev.y - curr.y;
|
|
1869
|
+
const dx2 = next.x - curr.x, dy2 = next.y - curr.y;
|
|
1870
|
+
const dot = dx1 * dx2 + dy1 * dy2;
|
|
1871
|
+
const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
|
|
1872
|
+
const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
|
|
1873
|
+
if (len1 < 1e-10 || len2 < 1e-10) {
|
|
1874
|
+
result.push({ vertex: curr, angle: 180 });
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
const cosAngle = Math.max(-1, Math.min(1, dot / (len1 * len2)));
|
|
1878
|
+
const angle = Math.acos(cosAngle) * (180 / Math.PI);
|
|
1879
|
+
result.push({ vertex: curr, angle });
|
|
1880
|
+
}
|
|
1881
|
+
return result;
|
|
1882
|
+
}
|
|
1883
|
+
function plumbLine(referencePoint, bounds) {
|
|
1884
|
+
return [
|
|
1885
|
+
{ x: referencePoint.x, y: bounds.y },
|
|
1886
|
+
{ x: referencePoint.x, y: bounds.y + bounds.height }
|
|
1887
|
+
];
|
|
1888
|
+
}
|
|
1889
|
+
function levelLine(referencePoint, bounds) {
|
|
1890
|
+
return [
|
|
1891
|
+
{ x: bounds.x, y: referencePoint.y },
|
|
1892
|
+
{ x: bounds.x + bounds.width, y: referencePoint.y }
|
|
1893
|
+
];
|
|
1894
|
+
}
|
|
1895
|
+
function comparativeMeasure(segment1, segment2) {
|
|
1896
|
+
const len1 = dist2(segment1[0], segment1[1]);
|
|
1897
|
+
const len2 = dist2(segment2[0], segment2[1]);
|
|
1898
|
+
if (len2 < 1e-10) return { ratio: Infinity, label: "\u221E" };
|
|
1899
|
+
const ratio = len1 / len2;
|
|
1900
|
+
return { ratio, label: `1 : ${ratio.toFixed(2)}` };
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// src/envelope-layer.ts
|
|
1904
|
+
var ENVELOPE_PROPERTIES = [
|
|
1905
|
+
{ key: "envelopePath", label: "Envelope Points (JSON)", type: "string", default: "[]", group: "envelope" },
|
|
1906
|
+
{
|
|
1907
|
+
key: "envelopeStyle",
|
|
1908
|
+
label: "Envelope Style",
|
|
1909
|
+
type: "select",
|
|
1910
|
+
default: "tight",
|
|
1911
|
+
options: [
|
|
1912
|
+
{ value: "tight", label: "Tight (Convex Hull)" },
|
|
1913
|
+
{ value: "loose", label: "Loose (Expanded)" },
|
|
1914
|
+
{ value: "fitted", label: "Fitted (As Given)" }
|
|
1915
|
+
],
|
|
1916
|
+
group: "envelope"
|
|
1917
|
+
},
|
|
1918
|
+
{ key: "showAngles", label: "Show Angles", type: "boolean", default: true, group: "display" },
|
|
1919
|
+
{ key: "angleThreshold", label: "Angle Threshold", type: "number", default: 10, min: 5, max: 45, step: 5, group: "display" },
|
|
1920
|
+
{ key: "showPlumbLine", label: "Show Plumb Line", type: "boolean", default: true, group: "display" },
|
|
1921
|
+
{ key: "plumbLinePoint", label: "Plumb Line Point", type: "point", default: { x: 0.5, y: 0 }, group: "display" },
|
|
1922
|
+
{ key: "showLevelLines", label: "Show Level Lines", type: "boolean", default: false, group: "display" },
|
|
1923
|
+
{ key: "levelLinePoints", label: "Level Line Y Positions (JSON)", type: "string", default: "[]", group: "display" },
|
|
1924
|
+
{ key: "showMeasurements", label: "Show Measurements", type: "boolean", default: false, group: "display" },
|
|
1925
|
+
{ key: "measurementPairs", label: "Measurement Pairs (JSON)", type: "string", default: "[]", group: "display" },
|
|
1926
|
+
{ key: "showSubdivisions", label: "Show Subdivisions", type: "boolean", default: false, group: "display" },
|
|
1927
|
+
{ key: "subdivisionDepth", label: "Subdivision Depth", type: "number", default: 1, min: 0, max: 3, step: 1, group: "display" },
|
|
1928
|
+
{ key: "envelopeColor", label: "Envelope Color", type: "color", default: "rgba(255,200,0,0.6)", group: "style" },
|
|
1929
|
+
{ key: "plumbColor", label: "Plumb Color", type: "color", default: "rgba(0,255,0,0.4)", group: "style" },
|
|
1930
|
+
{ key: "measureColor", label: "Measure Color", type: "color", default: "rgba(255,100,100,0.5)", group: "style" },
|
|
1931
|
+
...COMMON_GUIDE_PROPERTIES
|
|
1932
|
+
];
|
|
1933
|
+
var envelopeLayerType = {
|
|
1934
|
+
typeId: "construction:envelope",
|
|
1935
|
+
displayName: "Envelope Block-In",
|
|
1936
|
+
icon: "pentagon",
|
|
1937
|
+
category: "guide",
|
|
1938
|
+
properties: ENVELOPE_PROPERTIES,
|
|
1939
|
+
propertyEditorId: "construction:envelope-editor",
|
|
1940
|
+
createDefault() {
|
|
1941
|
+
const props = {};
|
|
1942
|
+
for (const schema of ENVELOPE_PROPERTIES) {
|
|
1943
|
+
props[schema.key] = schema.default;
|
|
1944
|
+
}
|
|
1945
|
+
return props;
|
|
1946
|
+
},
|
|
1947
|
+
render(properties, ctx, bounds) {
|
|
1948
|
+
const pathNorm = parseJSON(properties.envelopePath ?? "[]", []);
|
|
1949
|
+
if (pathNorm.length < 3) return;
|
|
1950
|
+
const style = properties.envelopeStyle ?? "tight";
|
|
1951
|
+
const showAngles = properties.showAngles ?? true;
|
|
1952
|
+
const angleThreshold = properties.angleThreshold ?? 10;
|
|
1953
|
+
const showPlumb = properties.showPlumbLine ?? true;
|
|
1954
|
+
const plumbPt = properties.plumbLinePoint;
|
|
1955
|
+
const showLevel = properties.showLevelLines ?? false;
|
|
1956
|
+
const levelPts = parseJSON(properties.levelLinePoints ?? "[]", []);
|
|
1957
|
+
const showMeasure = properties.showMeasurements ?? false;
|
|
1958
|
+
const measurePairs = parseJSON(properties.measurementPairs ?? "[]", []);
|
|
1959
|
+
const showSub = properties.showSubdivisions ?? false;
|
|
1960
|
+
const subDepth = properties.subdivisionDepth ?? 1;
|
|
1961
|
+
const envColor = properties.envelopeColor ?? "rgba(255,200,0,0.6)";
|
|
1962
|
+
const plumbColor = properties.plumbColor ?? "rgba(0,255,0,0.4)";
|
|
1963
|
+
const measureColor = properties.measureColor ?? "rgba(255,100,100,0.5)";
|
|
1964
|
+
const lineWidth = properties.lineWidth ?? 1;
|
|
1965
|
+
const toPixelPt = (p) => ({
|
|
1966
|
+
x: bounds.x + p.x * bounds.width,
|
|
1967
|
+
y: bounds.y + p.y * bounds.height
|
|
1968
|
+
});
|
|
1969
|
+
const pixelPath = pathNorm.map(toPixelPt);
|
|
1970
|
+
const envelope = computeEnvelope(pixelPath, style);
|
|
1971
|
+
ctx.save();
|
|
1972
|
+
ctx.strokeStyle = envColor;
|
|
1973
|
+
ctx.lineWidth = lineWidth * 1.5;
|
|
1974
|
+
ctx.setLineDash([]);
|
|
1975
|
+
drawPolyline(ctx, envelope, true);
|
|
1976
|
+
if (showAngles) {
|
|
1977
|
+
const angles = envelopeAngles(envelope);
|
|
1978
|
+
for (const { vertex, angle } of angles) {
|
|
1979
|
+
if (Math.abs(angle - 180) > angleThreshold) {
|
|
1980
|
+
drawLabel(ctx, `${Math.round(angle)}\xB0`, vertex.x + 12, vertex.y - 12, envColor, 9);
|
|
1981
|
+
ctx.beginPath();
|
|
1982
|
+
ctx.arc(vertex.x, vertex.y, 8, 0, Math.PI * 2);
|
|
1983
|
+
ctx.strokeStyle = envColor;
|
|
1984
|
+
ctx.lineWidth = 0.5;
|
|
1985
|
+
ctx.stroke();
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (showPlumb) {
|
|
1990
|
+
const plumbPixel = toPixelPt(plumbPt ?? { x: 0.5, y: 0 });
|
|
1991
|
+
const [p1, p2] = plumbLine(plumbPixel, bounds);
|
|
1992
|
+
ctx.strokeStyle = plumbColor;
|
|
1993
|
+
ctx.lineWidth = 0.75;
|
|
1994
|
+
drawDashedLine(ctx, p1.x, p1.y, p2.x, p2.y, [8, 6]);
|
|
1995
|
+
}
|
|
1996
|
+
if (showLevel) {
|
|
1997
|
+
ctx.strokeStyle = plumbColor;
|
|
1998
|
+
ctx.lineWidth = 0.75;
|
|
1999
|
+
for (const lp of levelPts) {
|
|
2000
|
+
const py = bounds.y + lp.y * bounds.height;
|
|
2001
|
+
drawDashedLine(ctx, bounds.x, py, bounds.x + bounds.width, py, [8, 6]);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (showMeasure) {
|
|
2005
|
+
ctx.strokeStyle = measureColor;
|
|
2006
|
+
ctx.lineWidth = 1;
|
|
2007
|
+
for (const pair of measurePairs) {
|
|
2008
|
+
const from = toPixelPt(pair.from);
|
|
2009
|
+
const to = toPixelPt(pair.to);
|
|
2010
|
+
drawLine(ctx, from.x, from.y, to.x, to.y);
|
|
2011
|
+
const len = dist2(from, to);
|
|
2012
|
+
const midX = (from.x + to.x) / 2;
|
|
2013
|
+
const midY = (from.y + to.y) / 2;
|
|
2014
|
+
drawLabel(ctx, `${Math.round(len)}px`, midX, midY - 8, measureColor, 8);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (showSub && subDepth > 0) {
|
|
2018
|
+
ctx.strokeStyle = envColor;
|
|
2019
|
+
ctx.globalAlpha = 0.3;
|
|
2020
|
+
ctx.lineWidth = 0.5;
|
|
2021
|
+
drawSubdivisions(ctx, envelope, subDepth);
|
|
2022
|
+
ctx.globalAlpha = 1;
|
|
2023
|
+
}
|
|
2024
|
+
ctx.restore();
|
|
2025
|
+
},
|
|
2026
|
+
validate(properties) {
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
function drawSubdivisions(ctx, vertices, depth) {
|
|
2031
|
+
if (depth <= 0 || vertices.length < 3) return;
|
|
2032
|
+
const midpoints = [];
|
|
2033
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
2034
|
+
const a = vertices[i];
|
|
2035
|
+
const b = vertices[(i + 1) % vertices.length];
|
|
2036
|
+
midpoints.push({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
|
|
2037
|
+
}
|
|
2038
|
+
ctx.setLineDash([3, 4]);
|
|
2039
|
+
for (let i = 0; i < midpoints.length; i++) {
|
|
2040
|
+
const a = midpoints[i];
|
|
2041
|
+
const b = midpoints[(i + 1) % midpoints.length];
|
|
2042
|
+
drawLine(ctx, a.x, a.y, b.x, b.y);
|
|
2043
|
+
}
|
|
2044
|
+
if (depth > 1) {
|
|
2045
|
+
drawSubdivisions(ctx, midpoints, depth - 1);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// src/math/intersection.ts
|
|
2050
|
+
function approximateIntersection(form1, form2, samples = 24) {
|
|
2051
|
+
const m1 = rotationMatrix(form1.rotation.x, form1.rotation.y, form1.rotation.z);
|
|
2052
|
+
const m2 = rotationMatrix(form2.rotation.x, form2.rotation.y, form2.rotation.z);
|
|
2053
|
+
const surf1 = sampleFormSurface(form1, m1, samples);
|
|
2054
|
+
const surf2 = sampleFormSurface(form2, m2, samples);
|
|
2055
|
+
const intersectionPoints = [];
|
|
2056
|
+
const threshold = Math.max(
|
|
2057
|
+
form1.size.x,
|
|
2058
|
+
form1.size.y,
|
|
2059
|
+
form1.size.z,
|
|
2060
|
+
form2.size.x,
|
|
2061
|
+
form2.size.y,
|
|
2062
|
+
form2.size.z
|
|
2063
|
+
) * 0.15;
|
|
2064
|
+
for (const p1 of surf1) {
|
|
2065
|
+
for (const p2 of surf2) {
|
|
2066
|
+
const dx = p1.x - p2.x;
|
|
2067
|
+
const dy = p1.y - p2.y;
|
|
2068
|
+
const dz = p1.z - p2.z;
|
|
2069
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
2070
|
+
if (dist < threshold) {
|
|
2071
|
+
intersectionPoints.push({
|
|
2072
|
+
x: (p1.x + p2.x) / 2,
|
|
2073
|
+
y: (p1.y + p2.y) / 2,
|
|
2074
|
+
z: (p1.z + p2.z) / 2
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
if (intersectionPoints.length < 2) return [];
|
|
2080
|
+
const projected = intersectionPoints.map((p) => project(p, "orthographic"));
|
|
2081
|
+
return orderPointsIntoCurve(projected);
|
|
2082
|
+
}
|
|
2083
|
+
function sampleFormSurface(form, matrix, samples) {
|
|
2084
|
+
const points = [];
|
|
2085
|
+
const { x: sx, y: sy, z: sz } = form.size;
|
|
2086
|
+
const { x: px, y: py, z: pz } = form.position;
|
|
2087
|
+
switch (form.type) {
|
|
2088
|
+
case "sphere": {
|
|
2089
|
+
for (let i = 0; i < samples; i++) {
|
|
2090
|
+
for (let j = 0; j < samples; j++) {
|
|
2091
|
+
const theta = i / samples * Math.PI;
|
|
2092
|
+
const phi = j / samples * Math.PI * 2;
|
|
2093
|
+
const local = {
|
|
2094
|
+
x: sx * 0.5 * Math.sin(theta) * Math.cos(phi),
|
|
2095
|
+
y: sy * 0.5 * Math.cos(theta),
|
|
2096
|
+
z: sz * 0.5 * Math.sin(theta) * Math.sin(phi)
|
|
2097
|
+
};
|
|
2098
|
+
const rotated = rotate3D(local, matrix);
|
|
2099
|
+
points.push({ x: rotated.x + px, y: rotated.y + py, z: rotated.z + pz });
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
break;
|
|
2103
|
+
}
|
|
2104
|
+
case "box": {
|
|
2105
|
+
const half = { x: sx / 2, y: sy / 2, z: sz / 2 };
|
|
2106
|
+
for (let i = 0; i < samples; i++) {
|
|
2107
|
+
for (let j = 0; j < samples; j++) {
|
|
2108
|
+
const u = i / (samples - 1) * 2 - 1;
|
|
2109
|
+
const v = j / (samples - 1) * 2 - 1;
|
|
2110
|
+
const faces = [
|
|
2111
|
+
{ x: u * half.x, y: v * half.y, z: half.z },
|
|
2112
|
+
{ x: u * half.x, y: v * half.y, z: -half.z },
|
|
2113
|
+
{ x: half.x, y: u * half.y, z: v * half.z },
|
|
2114
|
+
{ x: -half.x, y: u * half.y, z: v * half.z },
|
|
2115
|
+
{ x: u * half.x, y: half.y, z: v * half.z },
|
|
2116
|
+
{ x: u * half.x, y: -half.y, z: v * half.z }
|
|
2117
|
+
];
|
|
2118
|
+
for (const local of faces) {
|
|
2119
|
+
const rotated = rotate3D(local, matrix);
|
|
2120
|
+
points.push({ x: rotated.x + px, y: rotated.y + py, z: rotated.z + pz });
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
break;
|
|
2125
|
+
}
|
|
2126
|
+
case "cylinder": {
|
|
2127
|
+
for (let i = 0; i < samples; i++) {
|
|
2128
|
+
const angle = i / samples * Math.PI * 2;
|
|
2129
|
+
for (let j = 0; j < samples; j++) {
|
|
2130
|
+
const t = j / (samples - 1) * 2 - 1;
|
|
2131
|
+
const local = {
|
|
2132
|
+
x: sx * 0.5 * Math.cos(angle),
|
|
2133
|
+
y: sy * 0.5 * t,
|
|
2134
|
+
z: sz * 0.5 * Math.sin(angle)
|
|
2135
|
+
};
|
|
2136
|
+
const rotated = rotate3D(local, matrix);
|
|
2137
|
+
points.push({ x: rotated.x + px, y: rotated.y + py, z: rotated.z + pz });
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
case "cone": {
|
|
2143
|
+
for (let i = 0; i < samples; i++) {
|
|
2144
|
+
const angle = i / samples * Math.PI * 2;
|
|
2145
|
+
for (let j = 0; j < samples; j++) {
|
|
2146
|
+
const t = j / (samples - 1);
|
|
2147
|
+
const radius = 1 - t;
|
|
2148
|
+
const local = {
|
|
2149
|
+
x: sx * 0.5 * radius * Math.cos(angle),
|
|
2150
|
+
y: sy * (t - 0.5),
|
|
2151
|
+
z: sz * 0.5 * radius * Math.sin(angle)
|
|
2152
|
+
};
|
|
2153
|
+
const rotated = rotate3D(local, matrix);
|
|
2154
|
+
points.push({ x: rotated.x + px, y: rotated.y + py, z: rotated.z + pz });
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
break;
|
|
2158
|
+
}
|
|
2159
|
+
default: {
|
|
2160
|
+
for (let i = 0; i < samples; i++) {
|
|
2161
|
+
const theta = i / samples * Math.PI;
|
|
2162
|
+
for (let j = 0; j < samples; j++) {
|
|
2163
|
+
const phi = j / samples * Math.PI * 2;
|
|
2164
|
+
const local = {
|
|
2165
|
+
x: sx * 0.5 * Math.sin(theta) * Math.cos(phi),
|
|
2166
|
+
y: sy * 0.5 * Math.cos(theta),
|
|
2167
|
+
z: sz * 0.5 * Math.sin(theta) * Math.sin(phi)
|
|
2168
|
+
};
|
|
2169
|
+
const rotated = rotate3D(local, matrix);
|
|
2170
|
+
points.push({ x: rotated.x + px, y: rotated.y + py, z: rotated.z + pz });
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return points;
|
|
2176
|
+
}
|
|
2177
|
+
function orderPointsIntoCurve(points) {
|
|
2178
|
+
if (points.length < 2) return points;
|
|
2179
|
+
const used = /* @__PURE__ */ new Set();
|
|
2180
|
+
const result = [];
|
|
2181
|
+
let currentIdx = 0;
|
|
2182
|
+
for (let i = 1; i < points.length; i++) {
|
|
2183
|
+
if (points[i].x < points[currentIdx].x) currentIdx = i;
|
|
2184
|
+
}
|
|
2185
|
+
result.push(points[currentIdx]);
|
|
2186
|
+
used.add(currentIdx);
|
|
2187
|
+
while (used.size < points.length) {
|
|
2188
|
+
let bestIdx = -1;
|
|
2189
|
+
let bestDist = Infinity;
|
|
2190
|
+
for (let i = 0; i < points.length; i++) {
|
|
2191
|
+
if (used.has(i)) continue;
|
|
2192
|
+
const dx = points[i].x - points[currentIdx].x;
|
|
2193
|
+
const dy = points[i].y - points[currentIdx].y;
|
|
2194
|
+
const d = dx * dx + dy * dy;
|
|
2195
|
+
if (d < bestDist) {
|
|
2196
|
+
bestDist = d;
|
|
2197
|
+
bestIdx = i;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
if (bestIdx === -1) break;
|
|
2201
|
+
result.push(points[bestIdx]);
|
|
2202
|
+
used.add(bestIdx);
|
|
2203
|
+
currentIdx = bestIdx;
|
|
2204
|
+
}
|
|
2205
|
+
return result;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// src/intersection-layer.ts
|
|
2209
|
+
var INTERSECTION_PROPERTIES = [
|
|
2210
|
+
{ key: "forms", label: "Forms (JSON)", type: "string", default: "[]", group: "forms" },
|
|
2211
|
+
{ key: "showForms", label: "Show Forms", type: "boolean", default: true, group: "display" },
|
|
2212
|
+
{ key: "showIntersectionLines", label: "Show Intersection Lines", type: "boolean", default: true, group: "display" },
|
|
2213
|
+
{ key: "intersectionWidth", label: "Intersection Width", type: "number", default: 2.5, min: 1, max: 6, step: 0.5, group: "style" },
|
|
2214
|
+
{ key: "intersectionColor", label: "Intersection Color", type: "color", default: "rgba(255,50,50,0.8)", group: "style" },
|
|
2215
|
+
{
|
|
2216
|
+
key: "intersectionStyle",
|
|
2217
|
+
label: "Intersection Style",
|
|
2218
|
+
type: "select",
|
|
2219
|
+
default: "solid",
|
|
2220
|
+
options: [
|
|
2221
|
+
{ value: "solid", label: "Solid" },
|
|
2222
|
+
{ value: "bold", label: "Bold" },
|
|
2223
|
+
{ value: "emphasized", label: "Emphasized" }
|
|
2224
|
+
],
|
|
2225
|
+
group: "style"
|
|
2226
|
+
},
|
|
2227
|
+
{ key: "showFormLabels", label: "Show Form Labels", type: "boolean", default: false, group: "display" },
|
|
2228
|
+
{
|
|
2229
|
+
key: "transitionType",
|
|
2230
|
+
label: "Transition Type",
|
|
2231
|
+
type: "select",
|
|
2232
|
+
default: "hard",
|
|
2233
|
+
options: [
|
|
2234
|
+
{ value: "hard", label: "Hard" },
|
|
2235
|
+
{ value: "soft", label: "Soft" },
|
|
2236
|
+
{ value: "mixed", label: "Mixed" }
|
|
2237
|
+
],
|
|
2238
|
+
group: "display"
|
|
2239
|
+
},
|
|
2240
|
+
...COMMON_GUIDE_PROPERTIES
|
|
2241
|
+
];
|
|
2242
|
+
var intersectionLayerType = {
|
|
2243
|
+
typeId: "construction:intersection",
|
|
2244
|
+
displayName: "Form Intersection",
|
|
2245
|
+
icon: "intersect",
|
|
2246
|
+
category: "guide",
|
|
2247
|
+
properties: INTERSECTION_PROPERTIES,
|
|
2248
|
+
propertyEditorId: "construction:intersection-editor",
|
|
2249
|
+
createDefault() {
|
|
2250
|
+
const props = {};
|
|
2251
|
+
for (const schema of INTERSECTION_PROPERTIES) {
|
|
2252
|
+
props[schema.key] = schema.default;
|
|
2253
|
+
}
|
|
2254
|
+
return props;
|
|
2255
|
+
},
|
|
2256
|
+
render(properties, ctx, bounds) {
|
|
2257
|
+
const forms = parseJSON(properties.forms ?? "[]", []);
|
|
2258
|
+
if (forms.length < 2) return;
|
|
2259
|
+
const showForms = properties.showForms ?? true;
|
|
2260
|
+
const showIntersections = properties.showIntersectionLines ?? true;
|
|
2261
|
+
const intWidth = properties.intersectionWidth ?? 2.5;
|
|
2262
|
+
const intColor = properties.intersectionColor ?? "rgba(255,50,50,0.8)";
|
|
2263
|
+
const intStyle = properties.intersectionStyle ?? "solid";
|
|
2264
|
+
const showLabels = properties.showFormLabels ?? false;
|
|
2265
|
+
const transition = properties.transitionType ?? "hard";
|
|
2266
|
+
const guideColor = properties.guideColor ?? "rgba(0,200,255,0.5)";
|
|
2267
|
+
const lineWidth = properties.lineWidth ?? 1;
|
|
2268
|
+
const scale = Math.min(bounds.width, bounds.height) * 0.15;
|
|
2269
|
+
ctx.save();
|
|
2270
|
+
if (showForms) {
|
|
2271
|
+
for (let fi = 0; fi < forms.length; fi++) {
|
|
2272
|
+
const form = forms[fi];
|
|
2273
|
+
const centerNorm = {
|
|
2274
|
+
x: 0.5 + form.position.x * 0.3,
|
|
2275
|
+
y: 0.5 - form.position.y * 0.3
|
|
2276
|
+
};
|
|
2277
|
+
const center = toPixel(centerNorm, bounds);
|
|
2278
|
+
const matrix = rotationMatrix(form.rotation.x, form.rotation.y, form.rotation.z);
|
|
2279
|
+
const baseOpts = {
|
|
2280
|
+
center,
|
|
2281
|
+
scale,
|
|
2282
|
+
sizeX: form.size.x,
|
|
2283
|
+
sizeY: form.size.y,
|
|
2284
|
+
sizeZ: form.size.z,
|
|
2285
|
+
matrix,
|
|
2286
|
+
projection: "orthographic",
|
|
2287
|
+
focalLength: 5,
|
|
2288
|
+
showHidden: true,
|
|
2289
|
+
hiddenStyle: "dashed",
|
|
2290
|
+
hiddenAlpha: 0.2,
|
|
2291
|
+
edgeColor: guideColor
|
|
2292
|
+
};
|
|
2293
|
+
switch (form.type) {
|
|
2294
|
+
case "box":
|
|
2295
|
+
renderBox(ctx, baseOpts);
|
|
2296
|
+
break;
|
|
2297
|
+
case "cylinder":
|
|
2298
|
+
renderCylinder(ctx, baseOpts);
|
|
2299
|
+
break;
|
|
2300
|
+
case "sphere":
|
|
2301
|
+
renderSphere(ctx, { ...baseOpts, radius: 0.5 });
|
|
2302
|
+
break;
|
|
2303
|
+
case "cone":
|
|
2304
|
+
renderCone(ctx, baseOpts);
|
|
2305
|
+
break;
|
|
2306
|
+
case "wedge":
|
|
2307
|
+
renderWedge(ctx, baseOpts);
|
|
2308
|
+
break;
|
|
2309
|
+
case "egg":
|
|
2310
|
+
renderEgg(ctx, baseOpts);
|
|
2311
|
+
break;
|
|
2312
|
+
}
|
|
2313
|
+
if (showLabels) {
|
|
2314
|
+
const label = String.fromCharCode(65 + fi);
|
|
2315
|
+
drawLabel(ctx, label, center.x, center.y - scale * 0.6, guideColor, 12);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
if (showIntersections) {
|
|
2320
|
+
ctx.strokeStyle = intColor;
|
|
2321
|
+
ctx.lineWidth = intStyle === "bold" ? intWidth * 1.5 : intWidth;
|
|
2322
|
+
ctx.setLineDash(intStyle === "emphasized" ? [2, 0] : []);
|
|
2323
|
+
for (let i = 0; i < forms.length - 1; i++) {
|
|
2324
|
+
for (let j = i + 1; j < forms.length; j++) {
|
|
2325
|
+
const curve = approximateIntersection(forms[i], forms[j], 16);
|
|
2326
|
+
if (curve.length < 2) continue;
|
|
2327
|
+
const screenCurve = curve.map((p) => ({
|
|
2328
|
+
x: bounds.x + bounds.width / 2 + p.x * scale,
|
|
2329
|
+
y: bounds.y + bounds.height / 2 - p.y * scale
|
|
2330
|
+
}));
|
|
2331
|
+
if (transition === "soft") {
|
|
2332
|
+
ctx.lineWidth = intWidth * 2;
|
|
2333
|
+
ctx.globalAlpha = 0.4;
|
|
2334
|
+
drawPolyline(ctx, screenCurve);
|
|
2335
|
+
ctx.globalAlpha = 1;
|
|
2336
|
+
ctx.lineWidth = intWidth;
|
|
2337
|
+
}
|
|
2338
|
+
drawPolyline(ctx, screenCurve);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
ctx.restore();
|
|
2343
|
+
},
|
|
2344
|
+
validate(properties) {
|
|
2345
|
+
return null;
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
|
|
2349
|
+
// src/construction-tools.ts
|
|
2350
|
+
function textResult(text) {
|
|
2351
|
+
return { content: [{ type: "text", text }] };
|
|
2352
|
+
}
|
|
2353
|
+
function errorResult(text) {
|
|
2354
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
2355
|
+
}
|
|
2356
|
+
function generateLayerId() {
|
|
2357
|
+
return `layer-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2358
|
+
}
|
|
2359
|
+
function fullCanvasTransform(ctx) {
|
|
2360
|
+
return {
|
|
2361
|
+
x: 0,
|
|
2362
|
+
y: 0,
|
|
2363
|
+
width: ctx.canvasWidth,
|
|
2364
|
+
height: ctx.canvasHeight,
|
|
2365
|
+
rotation: 0,
|
|
2366
|
+
scaleX: 1,
|
|
2367
|
+
scaleY: 1,
|
|
2368
|
+
anchorX: 0,
|
|
2369
|
+
anchorY: 0
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
function addLayer(context, layer) {
|
|
2373
|
+
context.layers.add(layer);
|
|
2374
|
+
context.emitChange("layer-added");
|
|
2375
|
+
}
|
|
2376
|
+
var VALID_FORMS = ["box", "cylinder", "sphere", "cone", "wedge", "egg"];
|
|
2377
|
+
var addConstructionFormTool = {
|
|
2378
|
+
name: "add_construction_form",
|
|
2379
|
+
description: "Add a 3D construction form guide layer. Form types: box, cylinder, sphere, cone, wedge, egg.",
|
|
2380
|
+
inputSchema: {
|
|
2381
|
+
type: "object",
|
|
2382
|
+
properties: {
|
|
2383
|
+
formType: {
|
|
2384
|
+
type: "string",
|
|
2385
|
+
enum: VALID_FORMS,
|
|
2386
|
+
description: "The form primitive type."
|
|
2387
|
+
},
|
|
2388
|
+
position: {
|
|
2389
|
+
type: "object",
|
|
2390
|
+
properties: { x: { type: "number" }, y: { type: "number" } },
|
|
2391
|
+
description: "Normalized position (0-1). Default: {x:0.5, y:0.5}."
|
|
2392
|
+
},
|
|
2393
|
+
size: { type: "number", description: "Overall form size (0.05-0.6). Default: 0.25." },
|
|
2394
|
+
sizeX: { type: "number", description: "Width scale (0.2-3.0). Default: 1.0." },
|
|
2395
|
+
sizeY: { type: "number", description: "Height scale (0.2-3.0). Default: 1.0." },
|
|
2396
|
+
sizeZ: { type: "number", description: "Depth scale (0.2-3.0). Default: 1.0." },
|
|
2397
|
+
rotationX: { type: "number", description: "X rotation (-90 to 90). Default: 15." },
|
|
2398
|
+
rotationY: { type: "number", description: "Y rotation (-180 to 180). Default: 30." },
|
|
2399
|
+
rotationZ: { type: "number", description: "Z rotation (-180 to 180). Default: 0." },
|
|
2400
|
+
crossContours: { type: "boolean", description: "Show cross-contour lines. Default: true." },
|
|
2401
|
+
projection: {
|
|
2402
|
+
type: "string",
|
|
2403
|
+
enum: ["orthographic", "weak-perspective"],
|
|
2404
|
+
description: "Projection type. Default: orthographic."
|
|
2405
|
+
}
|
|
2406
|
+
},
|
|
2407
|
+
required: ["formType"]
|
|
2408
|
+
},
|
|
2409
|
+
async handler(input, context) {
|
|
2410
|
+
const formType = input.formType;
|
|
2411
|
+
if (!VALID_FORMS.includes(formType)) {
|
|
2412
|
+
return errorResult(`Invalid form type '${formType}'. Use: ${VALID_FORMS.join(", ")}`);
|
|
2413
|
+
}
|
|
2414
|
+
const defaults = formLayerType.createDefault();
|
|
2415
|
+
const properties = { ...defaults, formType };
|
|
2416
|
+
if (input.position) properties.position = input.position;
|
|
2417
|
+
if (input.size !== void 0) properties.formSize = input.size;
|
|
2418
|
+
if (input.sizeX !== void 0) properties.sizeX = input.sizeX;
|
|
2419
|
+
if (input.sizeY !== void 0) properties.sizeY = input.sizeY;
|
|
2420
|
+
if (input.sizeZ !== void 0) properties.sizeZ = input.sizeZ;
|
|
2421
|
+
if (input.rotationX !== void 0) properties.rotationX = input.rotationX;
|
|
2422
|
+
if (input.rotationY !== void 0) properties.rotationY = input.rotationY;
|
|
2423
|
+
if (input.rotationZ !== void 0) properties.rotationZ = input.rotationZ;
|
|
2424
|
+
if (input.crossContours !== void 0) properties.showCrossContours = input.crossContours;
|
|
2425
|
+
if (input.projection !== void 0) properties.projection = input.projection;
|
|
2426
|
+
const id = generateLayerId();
|
|
2427
|
+
addLayer(context, {
|
|
2428
|
+
id,
|
|
2429
|
+
type: "construction:form",
|
|
2430
|
+
name: `Construction Form (${formType})`,
|
|
2431
|
+
visible: true,
|
|
2432
|
+
locked: true,
|
|
2433
|
+
opacity: 1,
|
|
2434
|
+
blendMode: "normal",
|
|
2435
|
+
transform: fullCanvasTransform(context),
|
|
2436
|
+
properties
|
|
2437
|
+
});
|
|
2438
|
+
return textResult(`Added ${formType} construction form '${id}'.`);
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
var addConstructionSceneTool = {
|
|
2442
|
+
name: "add_construction_scene",
|
|
2443
|
+
description: "Add multiple 3D construction forms arranged as a scene.",
|
|
2444
|
+
inputSchema: {
|
|
2445
|
+
type: "object",
|
|
2446
|
+
properties: {
|
|
2447
|
+
forms: {
|
|
2448
|
+
type: "array",
|
|
2449
|
+
items: {
|
|
2450
|
+
type: "object",
|
|
2451
|
+
properties: {
|
|
2452
|
+
type: { type: "string", enum: VALID_FORMS },
|
|
2453
|
+
position: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
2454
|
+
size: { type: "number" },
|
|
2455
|
+
rotation: { type: "object", properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } } }
|
|
2456
|
+
},
|
|
2457
|
+
required: ["type"]
|
|
2458
|
+
},
|
|
2459
|
+
description: "Array of form definitions."
|
|
2460
|
+
},
|
|
2461
|
+
showAxes: { type: "boolean", description: "Show axes on all forms. Default: true." }
|
|
2462
|
+
},
|
|
2463
|
+
required: ["forms"]
|
|
2464
|
+
},
|
|
2465
|
+
async handler(input, context) {
|
|
2466
|
+
const formsInput = input.forms;
|
|
2467
|
+
if (!Array.isArray(formsInput) || formsInput.length === 0) {
|
|
2468
|
+
return errorResult("'forms' must be a non-empty array.");
|
|
2469
|
+
}
|
|
2470
|
+
const showAxes = input.showAxes ?? true;
|
|
2471
|
+
const ids = [];
|
|
2472
|
+
for (const f of formsInput) {
|
|
2473
|
+
const formType = f.type;
|
|
2474
|
+
if (!VALID_FORMS.includes(formType)) continue;
|
|
2475
|
+
const defaults = formLayerType.createDefault();
|
|
2476
|
+
const properties = { ...defaults, formType, showAxes };
|
|
2477
|
+
const pos = f.position;
|
|
2478
|
+
if (pos) properties.position = pos;
|
|
2479
|
+
if (f.size !== void 0) properties.formSize = f.size;
|
|
2480
|
+
const rot = f.rotation;
|
|
2481
|
+
if (rot) {
|
|
2482
|
+
properties.rotationX = rot.x ?? 15;
|
|
2483
|
+
properties.rotationY = rot.y ?? 30;
|
|
2484
|
+
properties.rotationZ = rot.z ?? 0;
|
|
2485
|
+
}
|
|
2486
|
+
const id = generateLayerId();
|
|
2487
|
+
ids.push(id);
|
|
2488
|
+
addLayer(context, {
|
|
2489
|
+
id,
|
|
2490
|
+
type: "construction:form",
|
|
2491
|
+
name: `Construction Form (${formType})`,
|
|
2492
|
+
visible: true,
|
|
2493
|
+
locked: true,
|
|
2494
|
+
opacity: 1,
|
|
2495
|
+
blendMode: "normal",
|
|
2496
|
+
transform: fullCanvasTransform(context),
|
|
2497
|
+
properties
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
return textResult(`Added ${ids.length} construction form(s): ${ids.join(", ")}.`);
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
var addCrossContoursTool = {
|
|
2504
|
+
name: "add_cross_contours",
|
|
2505
|
+
description: "Add cross-contour lines over an outline + axis path, revealing surface direction on any organic shape.",
|
|
2506
|
+
inputSchema: {
|
|
2507
|
+
type: "object",
|
|
2508
|
+
properties: {
|
|
2509
|
+
outline: { type: "array", items: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } }, description: "Outline points (normalized 0-1)." },
|
|
2510
|
+
axis: { type: "array", items: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } }, description: "Central axis path (normalized 0-1)." },
|
|
2511
|
+
curvature: { type: "number", description: "Curvature 0-1 (0=flat, 0.5=cylindrical, 1=spherical). Default: 0.5." },
|
|
2512
|
+
count: { type: "number", description: "Number of contour lines (2-20). Default: 8." },
|
|
2513
|
+
style: { type: "string", enum: ["elliptical", "angular", "organic"], description: "Contour style. Default: elliptical." },
|
|
2514
|
+
curvatureVariation: { type: "array", items: { type: "number" }, description: "Per-contour curvature overrides." }
|
|
2515
|
+
},
|
|
2516
|
+
required: ["outline", "axis"]
|
|
2517
|
+
},
|
|
2518
|
+
async handler(input, context) {
|
|
2519
|
+
const outline = input.outline;
|
|
2520
|
+
const axis = input.axis;
|
|
2521
|
+
if (!Array.isArray(outline) || !Array.isArray(axis) || axis.length < 2) {
|
|
2522
|
+
return errorResult("'outline' and 'axis' (min 2 points) are required.");
|
|
2523
|
+
}
|
|
2524
|
+
const defaults = crossContourLayerType.createDefault();
|
|
2525
|
+
const properties = {
|
|
2526
|
+
...defaults,
|
|
2527
|
+
outline: JSON.stringify(outline),
|
|
2528
|
+
axis: JSON.stringify(axis)
|
|
2529
|
+
};
|
|
2530
|
+
if (input.curvature !== void 0) properties.curvature = input.curvature;
|
|
2531
|
+
if (input.count !== void 0) properties.contourCount = input.count;
|
|
2532
|
+
if (input.style !== void 0) properties.contourStyle = input.style;
|
|
2533
|
+
if (input.curvatureVariation !== void 0) {
|
|
2534
|
+
properties.curvatureVariation = JSON.stringify(input.curvatureVariation);
|
|
2535
|
+
}
|
|
2536
|
+
const id = generateLayerId();
|
|
2537
|
+
addLayer(context, {
|
|
2538
|
+
id,
|
|
2539
|
+
type: "construction:cross-contour",
|
|
2540
|
+
name: "Cross-Contour Lines",
|
|
2541
|
+
visible: true,
|
|
2542
|
+
locked: true,
|
|
2543
|
+
opacity: 1,
|
|
2544
|
+
blendMode: "normal",
|
|
2545
|
+
transform: fullCanvasTransform(context),
|
|
2546
|
+
properties
|
|
2547
|
+
});
|
|
2548
|
+
return textResult(`Added cross-contour layer '${id}'.`);
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
var addValueStudyTool = {
|
|
2552
|
+
name: "add_value_study",
|
|
2553
|
+
description: "Add a light/shadow value study overlay with terminator, cast shadow, and value zones.",
|
|
2554
|
+
inputSchema: {
|
|
2555
|
+
type: "object",
|
|
2556
|
+
properties: {
|
|
2557
|
+
lightAzimuth: { type: "number", description: "Light azimuth 0-360 (0=right, 90=bottom). Default: 315." },
|
|
2558
|
+
lightElevation: { type: "number", description: "Light elevation 10-80 degrees. Default: 45." },
|
|
2559
|
+
forms: { type: "array", description: "Optional array of FormDefinition objects." },
|
|
2560
|
+
valueGrouping: { type: "string", enum: ["two-value", "three-value", "five-value"], description: "Value grouping. Default: three-value." },
|
|
2561
|
+
showTerminator: { type: "boolean", description: "Show terminator line. Default: true." }
|
|
2562
|
+
}
|
|
2563
|
+
},
|
|
2564
|
+
async handler(input, context) {
|
|
2565
|
+
const defaults = valueShapesLayerType.createDefault();
|
|
2566
|
+
const properties = { ...defaults };
|
|
2567
|
+
if (input.lightAzimuth !== void 0) properties.lightAzimuth = input.lightAzimuth;
|
|
2568
|
+
if (input.lightElevation !== void 0) properties.lightElevation = input.lightElevation;
|
|
2569
|
+
if (input.forms !== void 0) properties.formData = JSON.stringify(input.forms);
|
|
2570
|
+
if (input.valueGrouping !== void 0) properties.valueGrouping = input.valueGrouping;
|
|
2571
|
+
if (input.showTerminator !== void 0) properties.showTerminator = input.showTerminator;
|
|
2572
|
+
const id = generateLayerId();
|
|
2573
|
+
addLayer(context, {
|
|
2574
|
+
id,
|
|
2575
|
+
type: "construction:value-shapes",
|
|
2576
|
+
name: "Value Study",
|
|
2577
|
+
visible: true,
|
|
2578
|
+
locked: true,
|
|
2579
|
+
opacity: 1,
|
|
2580
|
+
blendMode: "normal",
|
|
2581
|
+
transform: fullCanvasTransform(context),
|
|
2582
|
+
properties
|
|
2583
|
+
});
|
|
2584
|
+
return textResult(`Added value study layer '${id}'.`);
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
var addEnvelopeTool = {
|
|
2588
|
+
name: "add_envelope",
|
|
2589
|
+
description: "Add a straight-line envelope block-in with angle and measurement annotations.",
|
|
2590
|
+
inputSchema: {
|
|
2591
|
+
type: "object",
|
|
2592
|
+
properties: {
|
|
2593
|
+
points: { type: "array", items: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } }, description: "Envelope vertex points (normalized 0-1)." },
|
|
2594
|
+
style: { type: "string", enum: ["tight", "loose", "fitted"], description: "Envelope style. Default: tight." },
|
|
2595
|
+
showAngles: { type: "boolean", description: "Show angle annotations. Default: true." },
|
|
2596
|
+
showPlumbLine: { type: "boolean", description: "Show vertical plumb line. Default: true." },
|
|
2597
|
+
showMeasurements: { type: "boolean", description: "Show comparative measurements. Default: false." }
|
|
2598
|
+
},
|
|
2599
|
+
required: ["points"]
|
|
2600
|
+
},
|
|
2601
|
+
async handler(input, context) {
|
|
2602
|
+
const points = input.points;
|
|
2603
|
+
if (!Array.isArray(points) || points.length < 3) {
|
|
2604
|
+
return errorResult("'points' must have at least 3 vertices.");
|
|
2605
|
+
}
|
|
2606
|
+
const defaults = envelopeLayerType.createDefault();
|
|
2607
|
+
const properties = { ...defaults, envelopePath: JSON.stringify(points) };
|
|
2608
|
+
if (input.style !== void 0) properties.envelopeStyle = input.style;
|
|
2609
|
+
if (input.showAngles !== void 0) properties.showAngles = input.showAngles;
|
|
2610
|
+
if (input.showPlumbLine !== void 0) properties.showPlumbLine = input.showPlumbLine;
|
|
2611
|
+
if (input.showMeasurements !== void 0) properties.showMeasurements = input.showMeasurements;
|
|
2612
|
+
const id = generateLayerId();
|
|
2613
|
+
addLayer(context, {
|
|
2614
|
+
id,
|
|
2615
|
+
type: "construction:envelope",
|
|
2616
|
+
name: "Envelope Block-In",
|
|
2617
|
+
visible: true,
|
|
2618
|
+
locked: true,
|
|
2619
|
+
opacity: 1,
|
|
2620
|
+
blendMode: "normal",
|
|
2621
|
+
transform: fullCanvasTransform(context),
|
|
2622
|
+
properties
|
|
2623
|
+
});
|
|
2624
|
+
return textResult(`Added envelope layer '${id}'.`);
|
|
2625
|
+
}
|
|
2626
|
+
};
|
|
2627
|
+
var addFormIntersectionTool = {
|
|
2628
|
+
name: "add_form_intersection",
|
|
2629
|
+
description: "Add intersection lines between two or more overlapping 3D forms.",
|
|
2630
|
+
inputSchema: {
|
|
2631
|
+
type: "object",
|
|
2632
|
+
properties: {
|
|
2633
|
+
forms: {
|
|
2634
|
+
type: "array",
|
|
2635
|
+
minItems: 2,
|
|
2636
|
+
items: {
|
|
2637
|
+
type: "object",
|
|
2638
|
+
properties: {
|
|
2639
|
+
type: { type: "string", enum: VALID_FORMS },
|
|
2640
|
+
position: { type: "object", properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } } },
|
|
2641
|
+
size: { type: "object", properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } } },
|
|
2642
|
+
rotation: { type: "object", properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } } }
|
|
2643
|
+
},
|
|
2644
|
+
required: ["type"]
|
|
2645
|
+
},
|
|
2646
|
+
description: "Array of form definitions (at least 2)."
|
|
2647
|
+
},
|
|
2648
|
+
transitionType: { type: "string", enum: ["hard", "soft", "mixed"], description: "Intersection transition type. Default: hard." },
|
|
2649
|
+
showForms: { type: "boolean", description: "Render the forms alongside intersection lines. Default: true." }
|
|
2650
|
+
},
|
|
2651
|
+
required: ["forms"]
|
|
2652
|
+
},
|
|
2653
|
+
async handler(input, context) {
|
|
2654
|
+
const forms = input.forms;
|
|
2655
|
+
if (!Array.isArray(forms) || forms.length < 2) {
|
|
2656
|
+
return errorResult("'forms' must have at least 2 entries.");
|
|
2657
|
+
}
|
|
2658
|
+
const normalizedForms = forms.map((f) => ({
|
|
2659
|
+
type: f.type || "box",
|
|
2660
|
+
position: f.position ?? { x: 0, y: 0, z: 0 },
|
|
2661
|
+
size: f.size ?? { x: 1, y: 1, z: 1 },
|
|
2662
|
+
rotation: f.rotation ?? { x: 0, y: 0, z: 0 }
|
|
2663
|
+
}));
|
|
2664
|
+
const defaults = intersectionLayerType.createDefault();
|
|
2665
|
+
const properties = { ...defaults, forms: JSON.stringify(normalizedForms) };
|
|
2666
|
+
if (input.transitionType !== void 0) properties.transitionType = input.transitionType;
|
|
2667
|
+
if (input.showForms !== void 0) properties.showForms = input.showForms;
|
|
2668
|
+
const id = generateLayerId();
|
|
2669
|
+
addLayer(context, {
|
|
2670
|
+
id,
|
|
2671
|
+
type: "construction:intersection",
|
|
2672
|
+
name: "Form Intersection",
|
|
2673
|
+
visible: true,
|
|
2674
|
+
locked: true,
|
|
2675
|
+
opacity: 1,
|
|
2676
|
+
blendMode: "normal",
|
|
2677
|
+
transform: fullCanvasTransform(context),
|
|
2678
|
+
properties
|
|
2679
|
+
});
|
|
2680
|
+
return textResult(`Added form intersection layer '${id}'.`);
|
|
2681
|
+
}
|
|
2682
|
+
};
|
|
2683
|
+
var generateExerciseTool = {
|
|
2684
|
+
name: "generate_construction_exercise",
|
|
2685
|
+
description: "Generate a random construction exercise with forms at varying difficulty.",
|
|
2686
|
+
inputSchema: {
|
|
2687
|
+
type: "object",
|
|
2688
|
+
properties: {
|
|
2689
|
+
difficulty: { type: "string", enum: ["beginner", "intermediate", "advanced"], description: "Exercise difficulty. Default: beginner." },
|
|
2690
|
+
formCount: { type: "number", description: "Number of forms (overrides difficulty default)." },
|
|
2691
|
+
seed: { type: "number", description: "Random seed for reproducibility." },
|
|
2692
|
+
includeValues: { type: "boolean", description: "Include a value study overlay. Default: false." },
|
|
2693
|
+
includeIntersections: { type: "boolean", description: "Include intersection lines. Default: false." }
|
|
2694
|
+
}
|
|
2695
|
+
},
|
|
2696
|
+
async handler(input, context) {
|
|
2697
|
+
const difficulty = input.difficulty ?? "beginner";
|
|
2698
|
+
const seed = input.seed ?? Date.now();
|
|
2699
|
+
const includeValues = input.includeValues ?? false;
|
|
2700
|
+
const includeIntersections = input.includeIntersections ?? false;
|
|
2701
|
+
let s = seed;
|
|
2702
|
+
const rand = () => {
|
|
2703
|
+
s = s * 1103515245 + 12345 & 2147483647;
|
|
2704
|
+
return s / 2147483647;
|
|
2705
|
+
};
|
|
2706
|
+
const defaultCounts = { beginner: 1, intermediate: 3, advanced: 5 };
|
|
2707
|
+
const formCount = input.formCount ?? defaultCounts[difficulty] ?? 1;
|
|
2708
|
+
const ids = [];
|
|
2709
|
+
const maxRotation = { beginner: 30, intermediate: 60, advanced: 90 };
|
|
2710
|
+
const rotLimit = maxRotation[difficulty] ?? 30;
|
|
2711
|
+
for (let i = 0; i < formCount; i++) {
|
|
2712
|
+
const typeIdx = Math.floor(rand() * VALID_FORMS.length);
|
|
2713
|
+
const formType = VALID_FORMS[typeIdx];
|
|
2714
|
+
const posSpread = difficulty === "beginner" ? 0 : 0.3;
|
|
2715
|
+
const defaults = formLayerType.createDefault();
|
|
2716
|
+
const properties = {
|
|
2717
|
+
...defaults,
|
|
2718
|
+
formType,
|
|
2719
|
+
position: { x: 0.5 + (rand() - 0.5) * posSpread, y: 0.5 + (rand() - 0.5) * posSpread },
|
|
2720
|
+
formSize: 0.15 + rand() * 0.15,
|
|
2721
|
+
rotationX: Math.round((rand() - 0.5) * 2 * rotLimit / 5) * 5,
|
|
2722
|
+
rotationY: Math.round((rand() - 0.5) * 2 * 180 / 5) * 5,
|
|
2723
|
+
rotationZ: difficulty === "advanced" ? Math.round((rand() - 0.5) * 2 * 45 / 5) * 5 : 0,
|
|
2724
|
+
showCrossContours: true,
|
|
2725
|
+
showAxes: true
|
|
2726
|
+
};
|
|
2727
|
+
const id = generateLayerId();
|
|
2728
|
+
ids.push(id);
|
|
2729
|
+
addLayer(context, {
|
|
2730
|
+
id,
|
|
2731
|
+
type: "construction:form",
|
|
2732
|
+
name: `Exercise Form ${i + 1} (${formType})`,
|
|
2733
|
+
visible: true,
|
|
2734
|
+
locked: true,
|
|
2735
|
+
opacity: 1,
|
|
2736
|
+
blendMode: "normal",
|
|
2737
|
+
transform: fullCanvasTransform(context),
|
|
2738
|
+
properties
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
if (includeValues) {
|
|
2742
|
+
const valDefaults = valueShapesLayerType.createDefault();
|
|
2743
|
+
const valId = generateLayerId();
|
|
2744
|
+
ids.push(valId);
|
|
2745
|
+
addLayer(context, {
|
|
2746
|
+
id: valId,
|
|
2747
|
+
type: "construction:value-shapes",
|
|
2748
|
+
name: "Exercise Value Study",
|
|
2749
|
+
visible: true,
|
|
2750
|
+
locked: true,
|
|
2751
|
+
opacity: 1,
|
|
2752
|
+
blendMode: "normal",
|
|
2753
|
+
transform: fullCanvasTransform(context),
|
|
2754
|
+
properties: { ...valDefaults, lightAzimuth: Math.round(rand() * 360 / 15) * 15 }
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
return textResult(`Generated ${difficulty} exercise with ${formCount} form(s): ${ids.join(", ")}.`);
|
|
2758
|
+
}
|
|
2759
|
+
};
|
|
2760
|
+
var clearConstructionGuidesTool = {
|
|
2761
|
+
name: "clear_construction_guides",
|
|
2762
|
+
description: "Remove all construction:* layers from the layer stack.",
|
|
2763
|
+
inputSchema: { type: "object", properties: {} },
|
|
2764
|
+
async handler(_input, context) {
|
|
2765
|
+
const layers = context.layers.getAll();
|
|
2766
|
+
const ids = layers.filter((l) => l.type.startsWith("construction:")).map((l) => l.id);
|
|
2767
|
+
if (ids.length === 0) return textResult("No construction layers to remove.");
|
|
2768
|
+
for (const id of ids) context.layers.remove(id);
|
|
2769
|
+
context.emitChange("layer-removed");
|
|
2770
|
+
return textResult(`Removed ${ids.length} construction layer(s).`);
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
var constructionMcpTools = [
|
|
2774
|
+
addConstructionFormTool,
|
|
2775
|
+
addConstructionSceneTool,
|
|
2776
|
+
addCrossContoursTool,
|
|
2777
|
+
addValueStudyTool,
|
|
2778
|
+
addEnvelopeTool,
|
|
2779
|
+
addFormIntersectionTool,
|
|
2780
|
+
generateExerciseTool,
|
|
2781
|
+
clearConstructionGuidesTool
|
|
2782
|
+
];
|
|
2783
|
+
|
|
2784
|
+
// src/index.ts
|
|
2785
|
+
var constructionPlugin = {
|
|
2786
|
+
id: "construction",
|
|
2787
|
+
name: "Construction Guides",
|
|
2788
|
+
version: "0.1.0",
|
|
2789
|
+
tier: "free",
|
|
2790
|
+
description: "Drawing construction guides: 3D form primitives, cross-contour lines, value/shadow studies, envelope block-ins, and form intersections.",
|
|
2791
|
+
layerTypes: [
|
|
2792
|
+
formLayerType,
|
|
2793
|
+
crossContourLayerType,
|
|
2794
|
+
valueShapesLayerType,
|
|
2795
|
+
envelopeLayerType,
|
|
2796
|
+
intersectionLayerType
|
|
2797
|
+
],
|
|
2798
|
+
tools: [],
|
|
2799
|
+
exportHandlers: [],
|
|
2800
|
+
mcpTools: constructionMcpTools,
|
|
2801
|
+
async initialize(_context) {
|
|
2802
|
+
},
|
|
2803
|
+
dispose() {
|
|
2804
|
+
}
|
|
2805
|
+
};
|
|
2806
|
+
var index_default = constructionPlugin;
|
|
2807
|
+
export {
|
|
2808
|
+
approximateIntersection,
|
|
2809
|
+
castShadow,
|
|
2810
|
+
clamp,
|
|
2811
|
+
comparativeMeasure,
|
|
2812
|
+
computeEnvelope,
|
|
2813
|
+
constructionMcpTools,
|
|
2814
|
+
cross3,
|
|
2815
|
+
crossContourLayerType,
|
|
2816
|
+
index_default as default,
|
|
2817
|
+
dot3,
|
|
2818
|
+
drawEllipse,
|
|
2819
|
+
drawEllipseWithHidden,
|
|
2820
|
+
ellipsePoints,
|
|
2821
|
+
envelopeAngles,
|
|
2822
|
+
envelopeLayerType,
|
|
2823
|
+
formLayerType,
|
|
2824
|
+
identityMatrix,
|
|
2825
|
+
intersectionLayerType,
|
|
2826
|
+
levelLine,
|
|
2827
|
+
lightDirection,
|
|
2828
|
+
lightDirection2D,
|
|
2829
|
+
multiplyMat3,
|
|
2830
|
+
normalize3,
|
|
2831
|
+
plumbLine,
|
|
2832
|
+
project,
|
|
2833
|
+
projectedEllipse,
|
|
2834
|
+
rotate3D,
|
|
2835
|
+
rotationMatrix,
|
|
2836
|
+
sphereTerminator,
|
|
2837
|
+
sphereValueZones,
|
|
2838
|
+
transformPoint,
|
|
2839
|
+
transformedNormalZ,
|
|
2840
|
+
valueShapesLayerType
|
|
2841
|
+
};
|
|
2842
|
+
//# sourceMappingURL=index.js.map
|