makeit4me 1.0.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/.claude-plugin/plugin.json +14 -0
- package/.mcp.json +8 -0
- package/index.js +223 -0
- package/package.json +30 -0
- package/skills/sketchup-3d-warehouse-wrangling/SKILL.md +221 -0
- package/skills/sketchup-bridge/SKILL.md +255 -0
- package/skills/sketchup-building-model-geometry/SKILL.md +134 -0
- package/skills/sketchup-model-auditing/SKILL.md +160 -0
- package/skills/sketchup-review-design/SKILL.md +363 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "makeit4me",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server and skills that give AI agents full control over SketchUp Pro",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Lookahead Labs LLC",
|
|
7
|
+
"email": "support@makeit4me.net"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://www.makeit4me.net",
|
|
10
|
+
"license": "proprietary",
|
|
11
|
+
"keywords": ["sketchup", "3d-modeling", "mcp", "cad"],
|
|
12
|
+
"skills": "./skills/",
|
|
13
|
+
"mcpServers": "./.mcp.json"
|
|
14
|
+
}
|
package/.mcp.json
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
|
|
11
|
+
const PORT = 9876;
|
|
12
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
13
|
+
|
|
14
|
+
// ── HTTP helper ──────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function request(method, endpoint, body) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const url = new URL(endpoint, BASE);
|
|
19
|
+
const payload = body ? JSON.stringify(body) : undefined;
|
|
20
|
+
|
|
21
|
+
const req = http.request(
|
|
22
|
+
{
|
|
23
|
+
hostname: url.hostname,
|
|
24
|
+
port: url.port,
|
|
25
|
+
path: url.pathname,
|
|
26
|
+
method,
|
|
27
|
+
headers: {
|
|
28
|
+
...(payload
|
|
29
|
+
? {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
32
|
+
}
|
|
33
|
+
: {}),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
(res) => {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
res.on("data", (c) => chunks.push(c));
|
|
39
|
+
res.on("end", () => {
|
|
40
|
+
const text = Buffer.concat(chunks).toString();
|
|
41
|
+
try {
|
|
42
|
+
resolve(JSON.parse(text));
|
|
43
|
+
} catch {
|
|
44
|
+
resolve({ ok: false, error: `Non-JSON response: ${text.slice(0, 200)}` });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
req.on("error", (err) => {
|
|
51
|
+
resolve({
|
|
52
|
+
ok: false,
|
|
53
|
+
error: `Connection failed: ${err.message}. Is SketchUp running with MakeIt4Me?`,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (payload) req.write(payload);
|
|
58
|
+
req.end();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── MCP Server ───────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const server = new McpServer({
|
|
65
|
+
name: "makeit4me",
|
|
66
|
+
version: "1.0.0",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── Tools ────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
server.registerTool("sketchup_status", {
|
|
72
|
+
title: "SketchUp Status",
|
|
73
|
+
description:
|
|
74
|
+
"Check if SketchUp is running and the MakeIt4Me plugin is active. " +
|
|
75
|
+
"Returns version info and active model details. Always call this first.",
|
|
76
|
+
}, async () => {
|
|
77
|
+
const result = await request("GET", "/status");
|
|
78
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
server.registerTool("sketchup_models", {
|
|
82
|
+
title: "List Open Models",
|
|
83
|
+
description: "List all open SketchUp models with their titles and file paths.",
|
|
84
|
+
}, async () => {
|
|
85
|
+
const result = await request("GET", "/models");
|
|
86
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
server.registerTool("sketchup_inspect", {
|
|
90
|
+
title: "Inspect Model (Read-Only)",
|
|
91
|
+
description:
|
|
92
|
+
"Evaluate Ruby code in SketchUp for read-only queries. " +
|
|
93
|
+
"The variable `model` is pre-bound to Sketchup.active_model. " +
|
|
94
|
+
"Use this for reading model state — entity counts, bounds, materials, etc. " +
|
|
95
|
+
"For geometry changes, use sketchup_operation instead.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
code: z
|
|
98
|
+
.string()
|
|
99
|
+
.describe("Ruby code to evaluate in SketchUp. `model` is pre-bound to Sketchup.active_model."),
|
|
100
|
+
},
|
|
101
|
+
}, async ({ code }) => {
|
|
102
|
+
const result = await request("POST", "/inspect", { code });
|
|
103
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
server.registerTool("sketchup_operation", {
|
|
107
|
+
title: "Execute Operation (Undoable)",
|
|
108
|
+
description:
|
|
109
|
+
"Execute Ruby code wrapped in a named undo operation. " +
|
|
110
|
+
"Use for ALL geometry changes (add, move, delete) so the user can Ctrl+Z. " +
|
|
111
|
+
"The variable `model` is pre-bound to Sketchup.active_model.",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
name: z
|
|
114
|
+
.string()
|
|
115
|
+
.describe("Undo label shown in Edit > Undo (e.g. 'Add front wall')"),
|
|
116
|
+
code: z
|
|
117
|
+
.string()
|
|
118
|
+
.describe("Ruby code to execute. `model` is pre-bound to Sketchup.active_model."),
|
|
119
|
+
},
|
|
120
|
+
}, async ({ name, code }) => {
|
|
121
|
+
const result = await request("POST", "/operation", { name, code });
|
|
122
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
server.registerTool("sketchup_selection", {
|
|
126
|
+
title: "Manage Selection",
|
|
127
|
+
description:
|
|
128
|
+
"Read or manipulate the active selection in SketchUp. " +
|
|
129
|
+
"Actions: 'get' (list selected), 'clear', 'add' (by entity GUIDs), 'remove' (by entity GUIDs).",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
action: z.enum(["get", "clear", "add", "remove"]).describe("Selection action"),
|
|
132
|
+
entities: z
|
|
133
|
+
.array(z.string())
|
|
134
|
+
.optional()
|
|
135
|
+
.describe("Entity GUIDs for add/remove actions"),
|
|
136
|
+
},
|
|
137
|
+
}, async ({ action, entities }) => {
|
|
138
|
+
const body = { action };
|
|
139
|
+
if (entities) body.entities = entities;
|
|
140
|
+
const result = await request("POST", "/selection", body);
|
|
141
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
server.registerTool("sketchup_screenshot", {
|
|
145
|
+
title: "Take Viewport Screenshot",
|
|
146
|
+
description:
|
|
147
|
+
"Capture a screenshot of the current SketchUp viewport. " +
|
|
148
|
+
"Optionally set the camera position first. " +
|
|
149
|
+
"Returns the image so you can visually verify geometry.",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
eye: z
|
|
152
|
+
.array(z.number())
|
|
153
|
+
.length(3)
|
|
154
|
+
.optional()
|
|
155
|
+
.describe("Camera eye position [x, y, z] in inches. If omitted, uses current camera."),
|
|
156
|
+
target: z
|
|
157
|
+
.array(z.number())
|
|
158
|
+
.length(3)
|
|
159
|
+
.optional()
|
|
160
|
+
.describe("Camera target/look-at point [x, y, z] in inches."),
|
|
161
|
+
orthographic: z
|
|
162
|
+
.boolean()
|
|
163
|
+
.optional()
|
|
164
|
+
.describe("Use parallel projection (orthographic) instead of perspective. Good for elevation views."),
|
|
165
|
+
width: z.number().optional().describe("Image width in pixels (default 1920)"),
|
|
166
|
+
height: z.number().optional().describe("Image height in pixels (default 1080)"),
|
|
167
|
+
},
|
|
168
|
+
}, async ({ eye, target, orthographic, width, height }) => {
|
|
169
|
+
const w = width || 1920;
|
|
170
|
+
const h = height || 1080;
|
|
171
|
+
const imgPath = path.join(os.tmpdir(), "sketchup_mcp_screenshot.png");
|
|
172
|
+
|
|
173
|
+
let code = "view = model.active_view\n";
|
|
174
|
+
|
|
175
|
+
if (eye && target) {
|
|
176
|
+
code += `eye = Geom::Point3d.new(${eye.join(", ")})\n`;
|
|
177
|
+
code += `target = Geom::Point3d.new(${target.join(", ")})\n`;
|
|
178
|
+
code += `up = Geom::Vector3d.new(0, 0, 1)\n`;
|
|
179
|
+
code += `cam = Sketchup::Camera.new(eye, target, up)\n`;
|
|
180
|
+
if (orthographic) {
|
|
181
|
+
code += `cam.perspective = false\n`;
|
|
182
|
+
}
|
|
183
|
+
code += `view.camera = cam\n`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
code += `view.write_image("${imgPath}", ${w}, ${h}, true)\n`;
|
|
187
|
+
code += `"${imgPath}"`;
|
|
188
|
+
|
|
189
|
+
const result = await request("POST", "/inspect", { code });
|
|
190
|
+
|
|
191
|
+
if (!result.ok) {
|
|
192
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Read the image and return it
|
|
196
|
+
try {
|
|
197
|
+
const imgBuffer = fs.readFileSync(imgPath);
|
|
198
|
+
const base64 = imgBuffer.toString("base64");
|
|
199
|
+
return {
|
|
200
|
+
content: [
|
|
201
|
+
{
|
|
202
|
+
type: "image",
|
|
203
|
+
data: base64,
|
|
204
|
+
mimeType: "image/png",
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return {
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: "text",
|
|
213
|
+
text: `Screenshot saved to ${imgPath} but failed to read: ${err.message}`,
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ── Start ────────────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
const transport = new StdioServerTransport();
|
|
223
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "makeit4me",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"makeit4me": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
".claude-plugin/",
|
|
12
|
+
".mcp.json",
|
|
13
|
+
"skills/"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node index.js"
|
|
17
|
+
},
|
|
18
|
+
"author": "Lookahead Labs LLC",
|
|
19
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
20
|
+
"description": "MCP server that bridges AI agents to SketchUp via the MakeIt4Me plugin",
|
|
21
|
+
"keywords": ["mcp", "sketchup", "3d-modeling", "cad", "architecture"],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://www.makeit4me.net"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://www.makeit4me.net",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.27.1"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sketchup-3d-warehouse-wrangling
|
|
3
|
+
description: |
|
|
4
|
+
Place, orient, trim, and manage 3D Warehouse components in SketchUp via the
|
|
5
|
+
Ruby API. Use when: (1) a component from the warehouse is too large or wrong
|
|
6
|
+
orientation for the model, (2) roof shingles/tiles need to be cropped to fit
|
|
7
|
+
a roof, (3) doors or windows need to be placed in wall openings, (4) components
|
|
8
|
+
need rotation for correct function (e.g., water flow direction on shingles),
|
|
9
|
+
(5) need to inspect nested component hierarchies. Covers orientation, trimming
|
|
10
|
+
by bounds, nested hierarchy navigation, and placement in openings.
|
|
11
|
+
author: Claude Code
|
|
12
|
+
version: 1.0.0
|
|
13
|
+
date: 2026-03-11
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# SketchUp 3D Warehouse Component Wrangling
|
|
17
|
+
|
|
18
|
+
## Problem
|
|
19
|
+
Components from the 3D Warehouse rarely fit your model as-is. They're often the
|
|
20
|
+
wrong size, orientation, or position. Solid Tools often don't work because warehouse
|
|
21
|
+
components aren't manifold solids. This skill covers practical techniques for making
|
|
22
|
+
them fit.
|
|
23
|
+
|
|
24
|
+
## Inspecting a Component
|
|
25
|
+
|
|
26
|
+
Always start by understanding what you're working with:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
defn = model.definitions["Component Name"]
|
|
30
|
+
bb = defn.bounds
|
|
31
|
+
puts "W: #{bb.width.to_l}, D: #{bb.depth.to_l}, H: #{bb.height.to_l}"
|
|
32
|
+
puts "Min: [#{bb.min.x.to_l}, #{bb.min.y.to_l}, #{bb.min.z.to_l}]"
|
|
33
|
+
puts "Max: [#{bb.max.x.to_l}, #{bb.max.y.to_l}, #{bb.max.z.to_l}]"
|
|
34
|
+
|
|
35
|
+
# Check internal structure
|
|
36
|
+
counts = Hash.new(0)
|
|
37
|
+
defn.entities.each { |e| counts[e.typename] += 1 }
|
|
38
|
+
puts counts # Often reveals nested ComponentInstances or Groups
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Navigating Nested Hierarchies
|
|
42
|
+
|
|
43
|
+
Warehouse components are often deeply nested. A "roof shingles" panel might be:
|
|
44
|
+
```
|
|
45
|
+
ComponentInstance (panel)
|
|
46
|
+
└─ 22x ComponentInstance (rows)
|
|
47
|
+
└─ 26x Group (individual tiles)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
To operate on leaf geometry, you must traverse:
|
|
51
|
+
```ruby
|
|
52
|
+
instance.definition.entities.each do |row|
|
|
53
|
+
next unless row.is_a?(Sketchup::ComponentInstance)
|
|
54
|
+
row.make_unique # IMPORTANT: avoid modifying shared definition
|
|
55
|
+
row.definition.entities.each do |tile|
|
|
56
|
+
next unless tile.is_a?(Sketchup::Group)
|
|
57
|
+
# Now you can check bounds and delete
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Trimming Oversized Components (Non-Solid)
|
|
63
|
+
|
|
64
|
+
When Solid Tools won't work (component isn't manifold), trim by deleting
|
|
65
|
+
sub-components outside your target bounds:
|
|
66
|
+
|
|
67
|
+
### Step 1: Remove entire rows/sections outside bounds
|
|
68
|
+
```ruby
|
|
69
|
+
# Remove rows outside Y bounds (e.g., for a roof 0..144")
|
|
70
|
+
instance.make_unique
|
|
71
|
+
rows = instance.definition.entities.to_a.select { |e| e.is_a?(Sketchup::ComponentInstance) }
|
|
72
|
+
to_del = rows.select { |r| r.bounds.min.y > 148 || r.bounds.max.y < -4 }
|
|
73
|
+
instance.definition.entities.erase_entities(to_del)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Step 2: Remove individual pieces within rows outside other bounds
|
|
77
|
+
```ruby
|
|
78
|
+
rows.each do |row|
|
|
79
|
+
row.make_unique
|
|
80
|
+
tiles = row.definition.entities.to_a.select { |e| e.is_a?(Sketchup::Group) }
|
|
81
|
+
to_del = tiles.select { |t|
|
|
82
|
+
tb = t.bounds
|
|
83
|
+
min_pt = tb.min.transform(row.transformation)
|
|
84
|
+
min_pt.x > max_x || tb.max.transform(row.transformation).x < min_x
|
|
85
|
+
}
|
|
86
|
+
row.definition.entities.erase_entities(to_del) if to_del.any?
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Important: `make_unique` before modifying
|
|
91
|
+
Without `make_unique`, modifying a definition affects ALL instances sharing it.
|
|
92
|
+
Always call `make_unique` before deleting sub-geometry.
|
|
93
|
+
|
|
94
|
+
## Orienting Roof Shingles for Water Flow
|
|
95
|
+
|
|
96
|
+
Shingles must overlap so water runs downhill. The overlap direction depends on
|
|
97
|
+
which way the component's internal rows stack.
|
|
98
|
+
|
|
99
|
+
### Determine current orientation
|
|
100
|
+
```ruby
|
|
101
|
+
ci = model.entities.find { |e| e.is_a?(Sketchup::ComponentInstance) }
|
|
102
|
+
t = ci.transformation
|
|
103
|
+
puts "X axis (row direction): #{t.xaxis.to_a}"
|
|
104
|
+
puts "Y axis (stacking direction): #{t.yaxis.to_a}"
|
|
105
|
+
puts "Z axis (normal): #{t.zaxis.to_a}"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Fix water flow direction
|
|
109
|
+
If rows stack the wrong way (uphill instead of downhill), flip 180° around the
|
|
110
|
+
component's local X axis, centered on the roof:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
roof_center = Geom::Point3d.new(42, 66, 96)
|
|
114
|
+
flip = Geom::Transformation.rotation(roof_center, ci.transformation.xaxis, Math::PI)
|
|
115
|
+
ci.transformation = flip * ci.transformation
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Rotate rows to run across width (not along slope)
|
|
119
|
+
If shingle rows run along the slope instead of across it, rotate 90° around
|
|
120
|
+
the roof surface normal:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
normal = ci.transformation.zaxis
|
|
124
|
+
center = ci.bounds.center
|
|
125
|
+
rot = Geom::Transformation.rotation(center, normal, 90.degrees)
|
|
126
|
+
ci.transformation = rot * ci.transformation
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**After any rotation, recenter on the roof:**
|
|
130
|
+
```ruby
|
|
131
|
+
roof_group = model.entities.find { |e| e.is_a?(Sketchup::Group) && e.bounds.min.z > 80 }
|
|
132
|
+
rb = roof_group.bounds
|
|
133
|
+
sb = ci.bounds
|
|
134
|
+
move = Geom::Transformation.translation([
|
|
135
|
+
rb.center.x - sb.center.x,
|
|
136
|
+
rb.center.y - sb.center.y,
|
|
137
|
+
rb.center.z - sb.center.z
|
|
138
|
+
])
|
|
139
|
+
ci.transformation = move * ci.transformation
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Placing Doors in Wall Openings
|
|
143
|
+
|
|
144
|
+
### Find the opening dimensions
|
|
145
|
+
```ruby
|
|
146
|
+
# Door frame group or wall opening
|
|
147
|
+
frame = model.entities.find { |e|
|
|
148
|
+
e.is_a?(Sketchup::Group) && e.bounds.min.z == 0 && e.bounds.height > 70
|
|
149
|
+
}
|
|
150
|
+
fb = frame.bounds
|
|
151
|
+
center_x = (fb.min.x + fb.max.x) / 2.0
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Position the door component
|
|
155
|
+
```ruby
|
|
156
|
+
door = model.entities.find { |e|
|
|
157
|
+
e.is_a?(Sketchup::ComponentInstance) && e.definition.name == "DoorName"
|
|
158
|
+
}
|
|
159
|
+
defn_bb = door.definition.bounds
|
|
160
|
+
door_w = defn_bb.width
|
|
161
|
+
door_d = defn_bb.depth
|
|
162
|
+
|
|
163
|
+
# Center on opening, straddle the wall plane
|
|
164
|
+
new_x = center_x - (door_w / 2.0)
|
|
165
|
+
new_y = wall_y - (door_d / 2.0) # center depth on wall plane
|
|
166
|
+
new_z = 0.0 # or slab_z
|
|
167
|
+
|
|
168
|
+
door.transformation = Geom::Transformation.new([new_x, new_y, new_z])
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### For doors on non-front walls, rotate first
|
|
172
|
+
```ruby
|
|
173
|
+
# Door on right wall (X=84): rotate -90° around Z
|
|
174
|
+
t_rot = Geom::Transformation.rotation(ORIGIN, Z_AXIS, -90.degrees)
|
|
175
|
+
t_move = Geom::Transformation.new([wall_x, door_y, 0])
|
|
176
|
+
door.transformation = t_move * t_rot
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Double doors (mirrored pair)
|
|
180
|
+
```ruby
|
|
181
|
+
# Left door: hinge at left edge, opens right
|
|
182
|
+
t_rot1 = Geom::Transformation.rotation(ORIGIN, Z_AXIS, -90.degrees)
|
|
183
|
+
t_move1 = Geom::Transformation.new([wall_x, opening_y_min, 0])
|
|
184
|
+
door1.transformation = t_move1 * t_rot1
|
|
185
|
+
|
|
186
|
+
# Right door: hinge at right edge, opens left (flip Y)
|
|
187
|
+
t_rot2 = Geom::Transformation.rotation(ORIGIN, Z_AXIS, 90.degrees)
|
|
188
|
+
t_move2 = Geom::Transformation.new([wall_x, opening_y_max, 0])
|
|
189
|
+
door2.transformation = t_move2 * t_rot2
|
|
190
|
+
|
|
191
|
+
# Shift inward by door width if they open outward
|
|
192
|
+
move_in = Geom::Transformation.translation([0, door_width, 0])
|
|
193
|
+
door1.transformation = move_in * door1.transformation
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Checking for Solid Tools Compatibility
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
group.manifold? # true = can use Solid Tools (intersect, trim, union, etc.)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Most warehouse components return `false`. Use the bounds-based trimming
|
|
203
|
+
approach above instead.
|
|
204
|
+
|
|
205
|
+
## Verification
|
|
206
|
+
|
|
207
|
+
After any component manipulation:
|
|
208
|
+
1. Screenshot from multiple angles
|
|
209
|
+
2. Check for z-fighting (flickering surfaces)
|
|
210
|
+
3. Verify bounds match expected area
|
|
211
|
+
4. Check entity count hasn't exploded (common after bad explodes)
|
|
212
|
+
|
|
213
|
+
## Notes
|
|
214
|
+
- Scaling changes individual element sizes (bad for shingles/tiles) — trim instead
|
|
215
|
+
- `explode` inside a group keeps geometry contained; explode at top level creates a mess
|
|
216
|
+
- Components from the warehouse may use non-standard axes — always check `bounds` not just `origin`
|
|
217
|
+
- Some components have hidden geometry — check `entities.length` vs what you see
|
|
218
|
+
|
|
219
|
+
## See Also
|
|
220
|
+
- `sketchup-bridge`: MCP tools and Ruby API reference
|
|
221
|
+
- `sketchup-building-framing`: Generating realistic lumber framing
|