cyclecad 0.1.8 → 0.1.9
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/app/agent-demo.html +268 -5
- package/package.json +1 -1
package/app/agent-demo.html
CHANGED
|
@@ -98,14 +98,42 @@
|
|
|
98
98
|
</div>
|
|
99
99
|
</div>
|
|
100
100
|
|
|
101
|
+
<!-- Example chips -->
|
|
102
|
+
<div id="example-chips" style="position:absolute;bottom:118px;left:50%;transform:translateX(-50%);z-index:11;display:flex;gap:6px;flex-wrap:wrap;justify-content:center;max-width:720px;">
|
|
103
|
+
<button class="chip" onclick="fillExample('build a cylinder 50mm diameter 80mm tall in steel')">cylinder</button>
|
|
104
|
+
<button class="chip" onclick="fillExample('gear 24 teeth module 2.5 in brass')">gear</button>
|
|
105
|
+
<button class="chip" onclick="fillExample('bracket 120x60 4 M8 holes 15mm from edge fillet 8')">bracket</button>
|
|
106
|
+
<button class="chip" onclick="fillExample('hex bolt M12 40mm long')">hex bolt</button>
|
|
107
|
+
<button class="chip" onclick="fillExample('flange 100mm diameter inner 40 6 M6 holes')">flange</button>
|
|
108
|
+
<button class="chip" onclick="fillExample('tube outer 50 inner 40 height 80 in aluminum')">tube</button>
|
|
109
|
+
<button class="chip" onclick="fillExample('sphere radius 30 in titanium')">sphere</button>
|
|
110
|
+
<button class="chip" onclick="fillExample('add a hole radius 8')">+ hole</button>
|
|
111
|
+
<button class="chip" onclick="fillExample('fillet 5')">+ fillet</button>
|
|
112
|
+
<button class="chip" onclick="fillExample('shell thickness 2')">+ shell</button>
|
|
113
|
+
<button class="chip" onclick="fillExample('pattern circular 6 copies')">+ pattern</button>
|
|
114
|
+
<button class="chip" onclick="fillExample('change material to steel')">material</button>
|
|
115
|
+
<button class="chip" onclick="fillExample('validate')">validate</button>
|
|
116
|
+
<button class="chip" onclick="fillExample('export stl')">export</button>
|
|
117
|
+
</div>
|
|
118
|
+
<style>
|
|
119
|
+
.chip { padding:5px 12px;border:1px solid rgba(255,255,255,0.12);border-radius:16px;background:rgba(18,34,64,0.8);color:var(--muted);font-size:11px;cursor:pointer;transition:all 0.15s;font-family:inherit; }
|
|
120
|
+
.chip:hover { background:rgba(46,134,222,0.2);color:var(--text);border-color:rgba(46,134,222,0.4); }
|
|
121
|
+
</style>
|
|
122
|
+
|
|
101
123
|
<!-- Voice input bar -->
|
|
102
|
-
<div id="voice-bar" style="position:absolute;bottom:70px;left:50%;transform:translateX(-50%);z-index:11;display:flex;align-items:center;gap:10px;background:rgba(18,34,64,0.95);border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:8px 16px;min-width:500px;max-width:
|
|
124
|
+
<div id="voice-bar" style="position:absolute;bottom:70px;left:50%;transform:translateX(-50%);z-index:11;display:flex;align-items:center;gap:10px;background:rgba(18,34,64,0.95);border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:8px 16px;min-width:500px;max-width:720px;">
|
|
103
125
|
<button id="btn-mic" onclick="toggleVoice()" style="width:40px;height:40px;border-radius:50%;border:2px solid var(--coral);background:transparent;color:var(--coral);font-size:18px;cursor:pointer;transition:all 0.2s;flex-shrink:0;">🎤</button>
|
|
104
|
-
<input id="voice-input" type="text" placeholder="
|
|
105
|
-
<button onclick="executeVoiceCommand()" style="padding:8px 16px;border:none;border-radius:8px;background:linear-gradient(135deg,var(--gold),var(--blue));color:#000;font-size:13px;font-weight:700;cursor:pointer;flex-shrink:0;">
|
|
126
|
+
<input id="voice-input" type="text" placeholder="Describe what to build or modify..." style="flex:1;background:transparent;border:none;color:var(--text);font-size:14px;font-family:inherit;outline:none;" onkeydown="if(event.key==='Enter')executeVoiceCommand()">
|
|
127
|
+
<button onclick="executeVoiceCommand()" style="padding:8px 16px;border:none;border-radius:8px;background:linear-gradient(135deg,var(--gold),var(--blue));color:#000;font-size:13px;font-weight:700;cursor:pointer;flex-shrink:0;">Go</button>
|
|
106
128
|
<div id="voice-status" style="position:absolute;top:-22px;left:16px;font-size:11px;color:var(--dim);"></div>
|
|
107
129
|
</div>
|
|
108
130
|
|
|
131
|
+
<!-- Feature count badge -->
|
|
132
|
+
<div id="feature-badge" style="position:absolute;bottom:70px;right:20px;z-index:11;display:none;background:rgba(18,34,64,0.95);border:1px solid rgba(255,255,255,0.08);border-radius:8px;padding:8px 12px;font-size:11px;color:var(--muted);max-width:180px;">
|
|
133
|
+
<div style="color:var(--gold);font-weight:600;margin-bottom:4px;">FEATURES</div>
|
|
134
|
+
<div id="feature-list" style="line-height:1.6;"></div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
109
137
|
<div class="controls">
|
|
110
138
|
<button class="btn-run" id="btn-run" onclick="runDemo()">▶ Run Agent Demo</button>
|
|
111
139
|
<button class="btn-reset" onclick="resetDemo()">↺ Reset</button>
|
|
@@ -571,6 +599,11 @@
|
|
|
571
599
|
if (/chamfer/.test(t)) return 'chamfer';
|
|
572
600
|
if (/add\s*(a\s*)?(slot|groove|channel)/.test(t)) return 'slot';
|
|
573
601
|
if (/add\s*(a\s*)?(boss|peg|pin|post)/.test(t)) return 'boss';
|
|
602
|
+
if (/shell|hollow\s*out|thin\s*wall/.test(t)) return 'shell';
|
|
603
|
+
if (/pattern|array|copies|repeat/.test(t)) return 'pattern';
|
|
604
|
+
if (/counterbore|counter\s*bore|c'?bore/.test(t)) return 'counterbore';
|
|
605
|
+
if (/thread|tap/.test(t)) return 'thread';
|
|
606
|
+
if (/mirror|symmetr/.test(t)) return 'mirror';
|
|
574
607
|
// If a known shape word is present → create new
|
|
575
608
|
if (detectShape(t)) return 'create';
|
|
576
609
|
// Default: try to create
|
|
@@ -598,6 +631,11 @@
|
|
|
598
631
|
holeOffset: parseNum(t, /(\d+)\s*mm?\s*from\s*(?:the\s*)?edge/) || 10,
|
|
599
632
|
posX: parseNum(t, /(?:at|x)\s*(-?\d+(?:\.\d+)?)/) || 0,
|
|
600
633
|
posZ: parseNum(t, /(?:,\s*|y\s*)(-?\d+(?:\.\d+)?)/) || 0,
|
|
634
|
+
shellThick: parseNum(t, /shell\s*(?:thickness\s*)?(\d+(?:\.\d+)?)/, /thick(?:ness)?\s*(\d+(?:\.\d+)?)/, /(\d+(?:\.\d+)?)\s*mm?\s*(?:shell|wall)/) || 0,
|
|
635
|
+
copies: parseNum(t, /(\d+)\s*cop(?:y|ies)/, /(\d+)\s*times/, /pattern\s*(?:of\s*)?(\d+)/) || 0,
|
|
636
|
+
patternType: /circular|radial|around/.test(t) ? 'circular' : (/linear|row|line/.test(t) ? 'linear' : 'circular'),
|
|
637
|
+
spacing: parseNum(t, /spacing\s*(\d+(?:\.\d+)?)/, /(\d+(?:\.\d+)?)\s*mm?\s*(?:apart|spacing)/) || 0,
|
|
638
|
+
threadPitch: parseNum(t, /pitch\s*(\d+(?:\.\d+)?)/, /(\d+(?:\.\d+)?)\s*mm?\s*pitch/) || 0,
|
|
601
639
|
};
|
|
602
640
|
}
|
|
603
641
|
|
|
@@ -823,6 +861,105 @@
|
|
|
823
861
|
return { steps, clearScene: false };
|
|
824
862
|
}
|
|
825
863
|
|
|
864
|
+
function buildShellSteps(text, t, ex) {
|
|
865
|
+
const steps = [];
|
|
866
|
+
if (!sceneState.shape) {
|
|
867
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
|
|
868
|
+
return { steps, clearScene: false };
|
|
869
|
+
}
|
|
870
|
+
const thick = ex.shellThick || ex.thick || 2;
|
|
871
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
872
|
+
steps.push({ type: 'divider' });
|
|
873
|
+
steps.push({ type: 'agent', text: `🥚 Shell — hollow out with ${thick}mm wall thickness` });
|
|
874
|
+
steps.push({ type: 'cmd', method: 'ops.shell', params: { target: 'main', thickness: thick }, mesh: 'shell' });
|
|
875
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
876
|
+
sceneState.dims.shellThick = thick;
|
|
877
|
+
sceneState.features.push(`Shell t=${thick}`);
|
|
878
|
+
steps.push({ type: 'agent', text: `✅ Shelled. ${sceneState.features.length} features. Keep going!` });
|
|
879
|
+
return { steps, clearScene: false };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function buildPatternSteps(text, t, ex) {
|
|
883
|
+
const steps = [];
|
|
884
|
+
if (!sceneState.shape) {
|
|
885
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
|
|
886
|
+
return { steps, clearScene: false };
|
|
887
|
+
}
|
|
888
|
+
const copies = ex.copies || 6;
|
|
889
|
+
const pType = ex.patternType || 'circular';
|
|
890
|
+
const spacing = ex.spacing || 30;
|
|
891
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
892
|
+
steps.push({ type: 'divider' });
|
|
893
|
+
steps.push({ type: 'agent', text: `🔄 ${pType.charAt(0).toUpperCase() + pType.slice(1)} pattern — ${copies} copies` });
|
|
894
|
+
steps.push({ type: 'cmd', method: 'ops.pattern', params: { target: 'main', type: pType, copies, spacing }, mesh: 'pattern' });
|
|
895
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
896
|
+
sceneState.dims.patternCopies = copies;
|
|
897
|
+
sceneState.dims.patternType = pType;
|
|
898
|
+
sceneState.features.push(`${pType} pattern ×${copies}`);
|
|
899
|
+
steps.push({ type: 'agent', text: `✅ Pattern applied. ${sceneState.features.length} features. Keep going!` });
|
|
900
|
+
return { steps, clearScene: false };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function buildCounterboreSteps(text, t, ex) {
|
|
904
|
+
const steps = [];
|
|
905
|
+
if (!sceneState.shape) {
|
|
906
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
|
|
907
|
+
return { steps, clearScene: false };
|
|
908
|
+
}
|
|
909
|
+
const boreR = ex.diameter ? ex.diameter / 2 : (ex.radius || 6);
|
|
910
|
+
const headR = boreR * 1.6;
|
|
911
|
+
const headDepth = ex.thick || 4;
|
|
912
|
+
const px = ex.posX || 0, pz = ex.posZ || 0;
|
|
913
|
+
const py = (sceneState.dims.h || 10) / 2;
|
|
914
|
+
sceneState.holeIdx++;
|
|
915
|
+
const name = `cbore${sceneState.holeIdx}`;
|
|
916
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
917
|
+
steps.push({ type: 'divider' });
|
|
918
|
+
steps.push({ type: 'agent', text: `🔩 Counterbore: ø${boreR * 2}mm through + ø${headR * 2}mm × ${headDepth}mm pocket` });
|
|
919
|
+
steps.push({ type: 'cmd', method: 'ops.cut', params: { shape: 'cylinder', radius: boreR, height: (sceneState.dims.h || 10) + 2, x: px, z: pz }, mesh: `hole${sceneState.holeIdx}`, pos: [px, py, pz] });
|
|
920
|
+
steps.push({ type: 'cmd', method: 'ops.cut', params: { shape: 'cylinder', radius: headR, height: headDepth, x: px, z: pz, counterbore: true }, mesh: name, pos: [px, (sceneState.dims.h || 10) - headDepth / 2, pz] });
|
|
921
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
922
|
+
sceneState.features.push(`Counterbore ø${boreR * 2} at (${px},${pz})`);
|
|
923
|
+
steps.push({ type: 'agent', text: `✅ Counterbore added. ${sceneState.features.length} features. Keep going!` });
|
|
924
|
+
return { steps, clearScene: false };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function buildThreadSteps(text, t, ex) {
|
|
928
|
+
const steps = [];
|
|
929
|
+
if (!sceneState.shape) {
|
|
930
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
|
|
931
|
+
return { steps, clearScene: false };
|
|
932
|
+
}
|
|
933
|
+
const pitch = ex.threadPitch || 1.5;
|
|
934
|
+
const threadR = ex.diameter ? ex.diameter / 2 : (ex.radius || (sceneState.dims.r || 10));
|
|
935
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
936
|
+
steps.push({ type: 'divider' });
|
|
937
|
+
steps.push({ type: 'agent', text: `🔩 Thread: ø${threadR * 2}mm, pitch ${pitch}mm` });
|
|
938
|
+
steps.push({ type: 'cmd', method: 'ops.thread', params: { target: 'main', radius: threadR, pitch, external: true }, mesh: 'thread' });
|
|
939
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
940
|
+
sceneState.dims.threadPitch = pitch;
|
|
941
|
+
sceneState.features.push(`Thread p=${pitch} ø${threadR * 2}`);
|
|
942
|
+
steps.push({ type: 'agent', text: `✅ Thread applied. ${sceneState.features.length} features. Keep going!` });
|
|
943
|
+
return { steps, clearScene: false };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function buildMirrorSteps(text, t, ex) {
|
|
947
|
+
const steps = [];
|
|
948
|
+
if (!sceneState.shape) {
|
|
949
|
+
steps.push({ type: 'agent', text: '⚠️ No part yet. Create something first.' });
|
|
950
|
+
return { steps, clearScene: false };
|
|
951
|
+
}
|
|
952
|
+
const axis = /[yz]/i.test(t) ? (t.match(/([yz])/i)[1].toUpperCase()) : 'X';
|
|
953
|
+
steps.push({ type: 'agent', text: `🎤 "${text}"` });
|
|
954
|
+
steps.push({ type: 'divider' });
|
|
955
|
+
steps.push({ type: 'agent', text: `🪞 Mirror across ${axis} axis` });
|
|
956
|
+
steps.push({ type: 'cmd', method: 'ops.mirror', params: { target: 'main', axis }, mesh: 'mirror' });
|
|
957
|
+
steps.push({ type: 'delay', ms: 200 });
|
|
958
|
+
sceneState.features.push(`Mirror ${axis}`);
|
|
959
|
+
steps.push({ type: 'agent', text: `✅ Mirrored. ${sceneState.features.length} features. Keep going!` });
|
|
960
|
+
return { steps, clearScene: false };
|
|
961
|
+
}
|
|
962
|
+
|
|
826
963
|
// ======= EXECUTE =======
|
|
827
964
|
async function executeVoiceCommand() {
|
|
828
965
|
const input = document.getElementById('voice-input');
|
|
@@ -848,12 +985,17 @@
|
|
|
848
985
|
if (intent === 'create') result = buildCreateSteps(text, t, ex);
|
|
849
986
|
else if (intent === 'hole') result = buildHoleSteps(text, t, ex);
|
|
850
987
|
else if (intent === 'fillet') result = buildFilletSteps(text, t, ex);
|
|
851
|
-
else if (intent === 'chamfer') result = buildFilletSteps(text, t, ex);
|
|
988
|
+
else if (intent === 'chamfer') result = buildFilletSteps(text, t, ex);
|
|
852
989
|
else if (intent === 'export') result = buildExportSteps(text, t);
|
|
853
990
|
else if (intent === 'validate') result = buildValidateSteps(text);
|
|
854
991
|
else if (intent === 'material') result = buildMaterialSteps(text, t);
|
|
855
992
|
else if (intent === 'boss') result = buildBossSteps(text, t, ex);
|
|
856
|
-
else result =
|
|
993
|
+
else if (intent === 'shell') result = buildShellSteps(text, t, ex);
|
|
994
|
+
else if (intent === 'pattern') result = buildPatternSteps(text, t, ex);
|
|
995
|
+
else if (intent === 'counterbore') result = buildCounterboreSteps(text, t, ex);
|
|
996
|
+
else if (intent === 'thread') result = buildThreadSteps(text, t, ex);
|
|
997
|
+
else if (intent === 'mirror') result = buildMirrorSteps(text, t, ex);
|
|
998
|
+
else result = buildCreateSteps(text, t, ex);
|
|
857
999
|
|
|
858
1000
|
const voiceSteps = result.steps;
|
|
859
1001
|
if (result.clearScene) {
|
|
@@ -900,6 +1042,7 @@
|
|
|
900
1042
|
}
|
|
901
1043
|
}
|
|
902
1044
|
running = false;
|
|
1045
|
+
updateFeatureBadge();
|
|
903
1046
|
}
|
|
904
1047
|
|
|
905
1048
|
function simulateResult(step) {
|
|
@@ -913,6 +1056,10 @@
|
|
|
913
1056
|
if (m === 'ops.fillet') return { ok: true, radius: step.params.radius, applied: true };
|
|
914
1057
|
if (m === 'ops.material') return { ok: true, material: step.params.material };
|
|
915
1058
|
if (m === 'ops.boss') return { ok: true, id: step.mesh };
|
|
1059
|
+
if (m === 'ops.shell') return { ok: true, thickness: step.params.thickness, hollowed: true };
|
|
1060
|
+
if (m === 'ops.pattern') return { ok: true, type: step.params.type, copies: step.params.copies };
|
|
1061
|
+
if (m === 'ops.thread') return { ok: true, pitch: step.params.pitch, external: true };
|
|
1062
|
+
if (m === 'ops.mirror') return { ok: true, axis: step.params.axis, mirrored: true };
|
|
916
1063
|
if (m === 'validate.dimensions') return { ok: true, size: sceneState.dims.sizeLabel };
|
|
917
1064
|
if (m === 'validate.printability') return { ok: true, printable: true, issues: [] };
|
|
918
1065
|
if (m === 'validate.cost') return { ok: true, unitCost: costMap[sceneState.material] || 12.40 };
|
|
@@ -1019,6 +1166,104 @@
|
|
|
1019
1166
|
if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
|
|
1020
1167
|
window.addMeshToScene(mesh);
|
|
1021
1168
|
}
|
|
1169
|
+
else if (step.mesh === 'shell') {
|
|
1170
|
+
// Visual shell: make existing part semi-transparent + add inner void
|
|
1171
|
+
const scene = window._scene;
|
|
1172
|
+
const main = scene.getObjectByName('main');
|
|
1173
|
+
if (main && main.isMesh) {
|
|
1174
|
+
main.material.transparent = true;
|
|
1175
|
+
main.material.opacity = 0.35;
|
|
1176
|
+
main.material.depthWrite = false;
|
|
1177
|
+
// Add inner representation (slightly smaller dark shape)
|
|
1178
|
+
const thick = sc.dims.shellThick || 2;
|
|
1179
|
+
let innerGeo;
|
|
1180
|
+
const s = sc.shape;
|
|
1181
|
+
if (s === 'cylinder' || s === 'disk') {
|
|
1182
|
+
innerGeo = new THREE.CylinderGeometry(d.r - thick, d.r - thick, d.h - thick * 2, 48);
|
|
1183
|
+
} else if (s === 'sphere') {
|
|
1184
|
+
innerGeo = new THREE.SphereGeometry(d.r - thick, 48, 32);
|
|
1185
|
+
} else {
|
|
1186
|
+
innerGeo = new THREE.BoxGeometry((d.w || 80) - thick * 2, (d.h || 5) - thick, (d.d || 40) - thick * 2);
|
|
1187
|
+
}
|
|
1188
|
+
const innerMat = new THREE.MeshStandardMaterial({ color: 0x0A1628, metalness: 0, roughness: 1 });
|
|
1189
|
+
const inner = new THREE.Mesh(innerGeo, innerMat);
|
|
1190
|
+
inner.position.copy(main.position);
|
|
1191
|
+
inner.name = 'shell_inner';
|
|
1192
|
+
window.addMeshToScene(inner);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
else if (step.mesh === 'pattern') {
|
|
1196
|
+
// Clone the main part in a circular or linear pattern
|
|
1197
|
+
const scene = window._scene;
|
|
1198
|
+
const main = scene.getObjectByName('main');
|
|
1199
|
+
if (main) {
|
|
1200
|
+
const copies = sc.dims.patternCopies || 6;
|
|
1201
|
+
const pType = sc.dims.patternType || 'circular';
|
|
1202
|
+
const spacing = step.params?.spacing || 30;
|
|
1203
|
+
for (let i = 1; i < copies; i++) {
|
|
1204
|
+
const clone = main.clone();
|
|
1205
|
+
clone.material = main.material.clone();
|
|
1206
|
+
clone.name = `pattern_${i}`;
|
|
1207
|
+
if (pType === 'circular') {
|
|
1208
|
+
const angle = (i / copies) * Math.PI * 2;
|
|
1209
|
+
const rad = Math.max(d.r || 30, (d.w || 80) / 2) * 2.5;
|
|
1210
|
+
clone.position.x = Math.cos(angle) * rad;
|
|
1211
|
+
clone.position.z = Math.sin(angle) * rad;
|
|
1212
|
+
clone.position.y = main.position.y;
|
|
1213
|
+
} else {
|
|
1214
|
+
clone.position.x = main.position.x + i * spacing;
|
|
1215
|
+
clone.position.y = main.position.y;
|
|
1216
|
+
clone.position.z = main.position.z;
|
|
1217
|
+
}
|
|
1218
|
+
window.addMeshToScene(clone);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
else if (step.mesh === 'thread') {
|
|
1223
|
+
// Visual thread: add a helical line wrapping the part
|
|
1224
|
+
const scene = window._scene;
|
|
1225
|
+
const main = scene.getObjectByName('main');
|
|
1226
|
+
const threadR = d.r || (d.outerR || 15);
|
|
1227
|
+
const threadH = d.h || 40;
|
|
1228
|
+
const pitch = sc.dims.threadPitch || 1.5;
|
|
1229
|
+
const turns = threadH / pitch;
|
|
1230
|
+
const pts = [];
|
|
1231
|
+
for (let i = 0; i <= turns * 36; i++) {
|
|
1232
|
+
const angle = (i / 36) * Math.PI * 2;
|
|
1233
|
+
const y = (i / (turns * 36)) * threadH;
|
|
1234
|
+
pts.push(new THREE.Vector3(Math.cos(angle) * (threadR + 0.5), y, Math.sin(angle) * (threadR + 0.5)));
|
|
1235
|
+
}
|
|
1236
|
+
const geo = new THREE.BufferGeometry().setFromPoints(pts);
|
|
1237
|
+
const mat = new THREE.LineBasicMaterial({ color: 0xD4A843, linewidth: 1 });
|
|
1238
|
+
const line = new THREE.Line(geo, mat);
|
|
1239
|
+
line.name = 'thread_visual';
|
|
1240
|
+
window.addMeshToScene(line);
|
|
1241
|
+
}
|
|
1242
|
+
else if (step.mesh === 'mirror') {
|
|
1243
|
+
// Clone the entire scene content and flip across the axis
|
|
1244
|
+
const scene = window._scene;
|
|
1245
|
+
const toClone = [];
|
|
1246
|
+
scene.traverse(c => { if (c.isMesh && c.name !== 'grid') toClone.push(c); });
|
|
1247
|
+
const axis = step.params?.axis || 'X';
|
|
1248
|
+
toClone.forEach(m => {
|
|
1249
|
+
const clone = m.clone();
|
|
1250
|
+
clone.material = m.material.clone();
|
|
1251
|
+
clone.name = `mirror_${m.name}`;
|
|
1252
|
+
if (axis === 'X') clone.position.x = -clone.position.x;
|
|
1253
|
+
else if (axis === 'Y') clone.position.y = -clone.position.y;
|
|
1254
|
+
else clone.position.z = -clone.position.z;
|
|
1255
|
+
window.addMeshToScene(clone);
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
else if (step.mesh && step.mesh.startsWith('cbore')) {
|
|
1259
|
+
// Counterbore pocket — wider shallow hole
|
|
1260
|
+
const r = step.params?.radius || 10;
|
|
1261
|
+
const h = step.params?.height || 4;
|
|
1262
|
+
const geo = new THREE.CylinderGeometry(r, r, h, 24);
|
|
1263
|
+
const mesh = new THREE.Mesh(geo, darkMat);
|
|
1264
|
+
if (step.pos) mesh.position.set(step.pos[0], step.pos[1], step.pos[2]);
|
|
1265
|
+
window.addMeshToScene(mesh);
|
|
1266
|
+
}
|
|
1022
1267
|
else if (step.mesh === 'recolor') {
|
|
1023
1268
|
// Change material color on existing part
|
|
1024
1269
|
const scene = window._scene;
|
|
@@ -1035,6 +1280,24 @@
|
|
|
1035
1280
|
styleEl.textContent = `@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(224,85,85,0.4); } 50% { box-shadow: 0 0 0 12px rgba(224,85,85,0); } }`;
|
|
1036
1281
|
document.head.appendChild(styleEl);
|
|
1037
1282
|
|
|
1283
|
+
// ======= UTILITY FUNCTIONS =======
|
|
1284
|
+
function fillExample(text) {
|
|
1285
|
+
const input = document.getElementById('voice-input');
|
|
1286
|
+
input.value = text;
|
|
1287
|
+
input.focus();
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function updateFeatureBadge() {
|
|
1291
|
+
const badge = document.getElementById('feature-badge');
|
|
1292
|
+
const list = document.getElementById('feature-list');
|
|
1293
|
+
if (!sceneState.features.length) { badge.style.display = 'none'; return; }
|
|
1294
|
+
badge.style.display = 'block';
|
|
1295
|
+
list.innerHTML = sceneState.features.map((f, i) =>
|
|
1296
|
+
`<div style="color:${i === 0 ? 'var(--teal)' : 'var(--muted)'}">${i + 1}. ${f}</div>`
|
|
1297
|
+
).join('');
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
window.fillExample = fillExample;
|
|
1038
1301
|
window.toggleVoice = toggleVoice;
|
|
1039
1302
|
window.executeVoiceCommand = executeVoiceCommand;
|
|
1040
1303
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cyclecad",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Browser-based parametric 3D CAD modeler with AI-powered tools, native Inventor file parsing, and smart assembly management. No install required.",
|
|
5
5
|
"main": "index.html",
|
|
6
6
|
"scripts": {
|