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