@genart-dev/plugin-construction 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,12 @@ Drawing construction guides plugin for [genart.dev](https://genart.dev) — 3D f
4
4
 
5
5
  Part of [genart.dev](https://genart.dev) — a generative art platform with an MCP server, desktop app, and IDE extensions.
6
6
 
7
+ ## Examples
8
+
9
+ ![Construction guides montage](test-renders/construction-guides.png)
10
+
11
+ Source file: [construction-guides.genart](test-renders/construction-guides.genart)
12
+
7
13
  ## Install
8
14
 
9
15
  ```bash
@@ -152,18 +158,6 @@ Intersection lines between two or more overlapping 3D forms, computed by surface
152
158
  | `showFormLabels` | boolean | `false` | Show A/B/C labels |
153
159
  | `transitionType` | select | `"hard"` | hard / soft / mixed |
154
160
 
155
- ## Test Render
156
-
157
- ![Construction guides montage](test-renders/construction-guides.png)
158
-
159
- 16-panel montage showing all layer types with variations:
160
- - **Row 1**: Box, cylinder, sphere, cone — default rotations with cross-contours and axes
161
- - **Row 2**: Wedge, egg/ovoid, box (extreme rotation), cylinder (weak-perspective foreshortening)
162
- - **Row 3**: Cross-contour lines on organic shape, 3-value study, 5-value study (full shadow anatomy), envelope block-in with angles and plumb line
163
- - **Row 4**: Box+sphere intersection, cylinder+cone intersection (soft), multi-form scene, construction exercise
164
-
165
- Regenerate with `node render-test-construction.cjs` (requires `canvas` dev dependency).
166
-
167
161
  ## MCP Tools (8)
168
162
 
169
163
  | Tool | Description |
package/dist/index.cjs CHANGED
@@ -20,10 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ COMPOUND_PRESETS: () => COMPOUND_PRESETS,
23
24
  approximateIntersection: () => approximateIntersection,
24
25
  castShadow: () => castShadow,
25
26
  clamp: () => clamp,
26
27
  comparativeMeasure: () => comparativeMeasure,
28
+ compoundFormLayerType: () => compoundFormLayerType,
27
29
  computeEnvelope: () => computeEnvelope,
28
30
  constructionMcpTools: () => constructionMcpTools,
29
31
  cross3: () => cross3,
@@ -2404,6 +2406,169 @@ var intersectionLayerType = {
2404
2406
  }
2405
2407
  };
2406
2408
 
2409
+ // src/compound-form-layer.ts
2410
+ var COMPOUND_PRESETS = {
2411
+ snowman: [
2412
+ { type: "sphere", offsetX: 0, offsetY: 0.35, offsetZ: 0, scaleX: 1, scaleY: 1, scaleZ: 1 },
2413
+ { type: "sphere", offsetX: 0, offsetY: -0.05, offsetZ: 0, scaleX: 0.75, scaleY: 0.75, scaleZ: 0.75 },
2414
+ { type: "sphere", offsetX: 0, offsetY: -0.35, offsetZ: 0, scaleX: 0.5, scaleY: 0.5, scaleZ: 0.5 }
2415
+ ],
2416
+ bottle: [
2417
+ { type: "cylinder", offsetX: 0, offsetY: 0.15, offsetZ: 0, scaleX: 1, scaleY: 0.7, scaleZ: 1 },
2418
+ { type: "cone", offsetX: 0, offsetY: -0.25, offsetZ: 0, scaleX: 0.6, scaleY: 0.3, scaleZ: 0.6 },
2419
+ { type: "cylinder", offsetX: 0, offsetY: -0.42, offsetZ: 0, scaleX: 0.3, scaleY: 0.15, scaleZ: 0.3 }
2420
+ ],
2421
+ mushroom: [
2422
+ { type: "sphere", offsetX: 0, offsetY: -0.2, offsetZ: 0, scaleX: 1.2, scaleY: 0.5, scaleZ: 1.2 },
2423
+ { type: "cylinder", offsetX: 0, offsetY: 0.2, offsetZ: 0, scaleX: 0.3, scaleY: 0.5, scaleZ: 0.3 }
2424
+ ],
2425
+ lamp: [
2426
+ { type: "cone", offsetX: 0, offsetY: -0.25, offsetZ: 0, scaleX: 1, scaleY: 0.5, scaleZ: 1 },
2427
+ { type: "cylinder", offsetX: 0, offsetY: 0.1, offsetZ: 0, scaleX: 0.15, scaleY: 0.5, scaleZ: 0.15 },
2428
+ { type: "cylinder", offsetX: 0, offsetY: 0.4, offsetZ: 0, scaleX: 0.5, scaleY: 0.05, scaleZ: 0.5 }
2429
+ ],
2430
+ tree: [
2431
+ { type: "cylinder", offsetX: 0, offsetY: 0.25, offsetZ: 0, scaleX: 0.2, scaleY: 0.5, scaleZ: 0.2 },
2432
+ { type: "egg", offsetX: 0, offsetY: -0.2, offsetZ: 0, scaleX: 0.8, scaleY: 0.6, scaleZ: 0.8 }
2433
+ ]
2434
+ };
2435
+ var COMPOUND_PROPERTIES = [
2436
+ {
2437
+ key: "preset",
2438
+ label: "Preset",
2439
+ type: "select",
2440
+ default: "snowman",
2441
+ options: Object.keys(COMPOUND_PRESETS).map((k) => ({ value: k, label: k.charAt(0).toUpperCase() + k.slice(1) })),
2442
+ group: "compound"
2443
+ },
2444
+ {
2445
+ key: "components",
2446
+ label: "Components (JSON)",
2447
+ type: "string",
2448
+ default: "",
2449
+ group: "compound"
2450
+ },
2451
+ {
2452
+ key: "position",
2453
+ label: "Position",
2454
+ type: "point",
2455
+ default: { x: 0.5, y: 0.5 },
2456
+ group: "compound"
2457
+ },
2458
+ {
2459
+ key: "formSize",
2460
+ label: "Size",
2461
+ type: "number",
2462
+ default: 0.25,
2463
+ min: 0.05,
2464
+ max: 0.6,
2465
+ step: 0.01,
2466
+ group: "compound"
2467
+ },
2468
+ {
2469
+ key: "rotX",
2470
+ label: "Rotation X (deg)",
2471
+ type: "number",
2472
+ default: 15,
2473
+ min: -90,
2474
+ max: 90,
2475
+ step: 5,
2476
+ group: "compound"
2477
+ },
2478
+ {
2479
+ key: "rotY",
2480
+ label: "Rotation Y (deg)",
2481
+ type: "number",
2482
+ default: 25,
2483
+ min: -180,
2484
+ max: 180,
2485
+ step: 5,
2486
+ group: "compound"
2487
+ },
2488
+ ...COMMON_GUIDE_PROPERTIES
2489
+ ];
2490
+ var compoundFormLayerType = {
2491
+ typeId: "construction:compound-form",
2492
+ displayName: "Compound Form",
2493
+ icon: "compound",
2494
+ category: "guide",
2495
+ properties: COMPOUND_PROPERTIES,
2496
+ propertyEditorId: "construction:compound-form-editor",
2497
+ createDefault() {
2498
+ const props = {};
2499
+ for (const schema of COMPOUND_PROPERTIES) {
2500
+ props[schema.key] = schema.default;
2501
+ }
2502
+ return props;
2503
+ },
2504
+ render(properties, ctx, bounds, _resources) {
2505
+ const preset = properties.preset ?? "snowman";
2506
+ const componentsJson = properties.components ?? "";
2507
+ const position = properties.position ?? { x: 0.5, y: 0.5 };
2508
+ const formSize = properties.formSize ?? 0.25;
2509
+ const rotX = properties.rotX ?? 15;
2510
+ const rotY = properties.rotY ?? 25;
2511
+ let components;
2512
+ if (componentsJson.trim()) {
2513
+ try {
2514
+ components = JSON.parse(componentsJson);
2515
+ } catch {
2516
+ components = COMPOUND_PRESETS[preset] ?? COMPOUND_PRESETS["snowman"];
2517
+ }
2518
+ } else {
2519
+ components = COMPOUND_PRESETS[preset] ?? COMPOUND_PRESETS["snowman"];
2520
+ }
2521
+ if (components.length === 0) return;
2522
+ const w = bounds.width;
2523
+ const h = bounds.height;
2524
+ const baseSize = Math.min(w, h) * formSize;
2525
+ const cx = bounds.x + w * position.x;
2526
+ const cy = bounds.y + h * position.y;
2527
+ ctx.save();
2528
+ const guideColor = properties.guideColor ?? "rgba(0,200,255,0.5)";
2529
+ const lineWidth = properties.lineWidth ?? 1;
2530
+ const dashPattern = properties.dashPattern ?? "";
2531
+ setupGuideStyle(ctx, guideColor, lineWidth, dashPattern);
2532
+ const mat = rotationMatrix(rotX, rotY, 0);
2533
+ const sorted = components.map((comp, i) => {
2534
+ const p3d = rotate3D({ x: comp.offsetX * baseSize, y: comp.offsetY * baseSize, z: comp.offsetZ * baseSize }, mat);
2535
+ return { comp, z: p3d.z, index: i };
2536
+ }).sort((a, b) => b.z - a.z);
2537
+ for (const { comp } of sorted) {
2538
+ const offset = rotate3D(
2539
+ { x: comp.offsetX * baseSize, y: comp.offsetY * baseSize, z: comp.offsetZ * baseSize },
2540
+ mat
2541
+ );
2542
+ const compCx = cx + offset.x;
2543
+ const compCy = cy + offset.y;
2544
+ const compSize = baseSize * Math.max(comp.scaleX, comp.scaleY);
2545
+ ctx.beginPath();
2546
+ const rx = compSize * comp.scaleX * 0.5;
2547
+ const ry = compSize * comp.scaleY * 0.5;
2548
+ ctx.ellipse(compCx, compCy, Math.max(1, rx), Math.max(1, ry), 0, 0, Math.PI * 2);
2549
+ ctx.stroke();
2550
+ ctx.beginPath();
2551
+ ctx.arc(compCx, compCy, 2, 0, Math.PI * 2);
2552
+ ctx.fill();
2553
+ }
2554
+ ctx.restore();
2555
+ },
2556
+ validate(properties) {
2557
+ const componentsJson = properties.components;
2558
+ if (componentsJson && componentsJson.trim()) {
2559
+ try {
2560
+ const parsed = JSON.parse(componentsJson);
2561
+ if (!Array.isArray(parsed)) {
2562
+ return [{ property: "components", message: "Components must be a JSON array" }];
2563
+ }
2564
+ } catch {
2565
+ return [{ property: "components", message: "Invalid JSON for components" }];
2566
+ }
2567
+ }
2568
+ return null;
2569
+ }
2570
+ };
2571
+
2407
2572
  // src/construction-tools.ts
2408
2573
  function textResult(text) {
2409
2574
  return { content: [{ type: "text", text }] };
@@ -2843,7 +3008,7 @@ var constructionMcpTools = [
2843
3008
  var constructionPlugin = {
2844
3009
  id: "construction",
2845
3010
  name: "Construction Guides",
2846
- version: "0.1.0",
3011
+ version: "0.2.0",
2847
3012
  tier: "free",
2848
3013
  description: "Drawing construction guides: 3D form primitives, cross-contour lines, value/shadow studies, envelope block-ins, and form intersections.",
2849
3014
  layerTypes: [
@@ -2851,7 +3016,8 @@ var constructionPlugin = {
2851
3016
  crossContourLayerType,
2852
3017
  valueShapesLayerType,
2853
3018
  envelopeLayerType,
2854
- intersectionLayerType
3019
+ intersectionLayerType,
3020
+ compoundFormLayerType
2855
3021
  ],
2856
3022
  tools: [],
2857
3023
  exportHandlers: [],
@@ -2864,10 +3030,12 @@ var constructionPlugin = {
2864
3030
  var index_default = constructionPlugin;
2865
3031
  // Annotate the CommonJS export names for ESM import in node:
2866
3032
  0 && (module.exports = {
3033
+ COMPOUND_PRESETS,
2867
3034
  approximateIntersection,
2868
3035
  castShadow,
2869
3036
  clamp,
2870
3037
  comparativeMeasure,
3038
+ compoundFormLayerType,
2871
3039
  computeEnvelope,
2872
3040
  constructionMcpTools,
2873
3041
  cross3,