cyclecad 0.1.10 → 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/app/agent-demo.html +719 -174
- package/app/index.html +58 -11
- package/architecture.html +372 -0
- package/package.json +1 -1
package/app/agent-demo.html
CHANGED
|
@@ -876,10 +876,17 @@
|
|
|
876
876
|
|
|
877
877
|
const commands = [
|
|
878
878
|
'create cylinder', 'create box', 'create sphere', 'create cone', 'create torus',
|
|
879
|
-
'
|
|
880
|
-
'
|
|
881
|
-
'
|
|
882
|
-
'
|
|
879
|
+
'create plate', 'create gear', 'create bracket', 'create washer', 'create hexbolt', 'create flange',
|
|
880
|
+
'draw circle', 'make a cylinder', 'build box',
|
|
881
|
+
'add hole', 'drill hole', 'add fillet', 'round edges', 'add chamfer', 'bevel',
|
|
882
|
+
'shell', 'hollow out', 'mirror', 'pattern', 'add thread',
|
|
883
|
+
'extrude', 'revolve', 'sweep', 'loft', 'boolean cut', 'boolean union',
|
|
884
|
+
'material steel', 'material brass', 'material aluminum', 'color red', 'color chrome',
|
|
885
|
+
'move', 'rotate', 'scale', 'copy', 'delete',
|
|
886
|
+
'measure', 'section', 'wireframe', 'grid', 'shadows',
|
|
887
|
+
'sketch line', 'sketch rect', 'constraint parallel',
|
|
888
|
+
'export stl', 'export step', 'export gltf',
|
|
889
|
+
'undo', 'redo', 'reset view', 'help', 'history', 'clear'
|
|
883
890
|
];
|
|
884
891
|
|
|
885
892
|
const matches = commands.filter(cmd => cmd.includes(value.toLowerCase()));
|
|
@@ -919,158 +926,489 @@
|
|
|
919
926
|
document.getElementById('status-latency').textContent = latency + 'ms';
|
|
920
927
|
}
|
|
921
928
|
|
|
929
|
+
// ─── NATURAL LANGUAGE PARSER ───
|
|
930
|
+
// Maps synonyms/phrases to canonical intents
|
|
931
|
+
const NLP_SHAPES = {
|
|
932
|
+
cylinder: ['cylinder', 'cylnder', 'cylindar', 'cyl', 'tube', 'pipe', 'rod', 'shaft', 'barrel', 'piston'],
|
|
933
|
+
box: ['box', 'cube', 'block', 'brick', 'rectangular', 'rect', 'cuboid', 'prism'],
|
|
934
|
+
sphere: ['sphere', 'ball', 'globe', 'orb'],
|
|
935
|
+
cone: ['cone', 'funnel', 'tapered'],
|
|
936
|
+
torus: ['torus', 'donut', 'doughnut', 'ring', 'o-ring'],
|
|
937
|
+
circle: ['circle', 'disc', 'disk', 'round', 'circular'],
|
|
938
|
+
plate: ['plate', 'slab', 'flat', 'panel', 'sheet'],
|
|
939
|
+
washer: ['washer', 'spacer', 'shim'],
|
|
940
|
+
hexbolt: ['hexbolt', 'hex bolt', 'bolt', 'hex head'],
|
|
941
|
+
gear: ['gear', 'cog', 'sprocket'],
|
|
942
|
+
flange: ['flange', 'collar', 'rim'],
|
|
943
|
+
bracket: ['bracket', 'l-bracket', 'angle'],
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const NLP_ACTIONS = {
|
|
947
|
+
create: ['create', 'make', 'build', 'draw', 'add a', 'add an', 'generate', 'design', 'model', 'sketch', 'construct', 'new'],
|
|
948
|
+
hole: ['hole', 'bore', 'drill', 'pierce', 'through hole', 'counterbore', 'countersink'],
|
|
949
|
+
fillet: ['fillet', 'round', 'round off', 'smooth', 'radius edge', 'blend'],
|
|
950
|
+
chamfer: ['chamfer', 'bevel', 'chamfr', 'edge break', 'cut edge', 'angled edge'],
|
|
951
|
+
shell: ['shell', 'hollow', 'thin wall', 'hollow out', 'scoop', 'empty'],
|
|
952
|
+
mirror: ['mirror', 'reflect', 'flip', 'symmetry'],
|
|
953
|
+
pattern: ['pattern', 'array', 'repeat', 'duplicate', 'copy array', 'replicate', 'linear pattern', 'circular pattern'],
|
|
954
|
+
extrude: ['extrude', 'pull', 'push', 'extend', 'raise', 'boss'],
|
|
955
|
+
revolve: ['revolve', 'rotate shape', 'spin', 'lathe', 'turn'],
|
|
956
|
+
sweep: ['sweep', 'sweep along', 'follow path', 'rail'],
|
|
957
|
+
loft: ['loft', 'blend between', 'transition', 'morph'],
|
|
958
|
+
thread: ['thread', 'screw thread', 'threading', 'helix', 'helical'],
|
|
959
|
+
boolean: ['boolean', 'cut', 'subtract', 'union', 'merge', 'intersect', 'combine', 'join'],
|
|
960
|
+
measure: ['measure', 'distance', 'angle', 'how far', 'how big', 'dimension'],
|
|
961
|
+
section: ['section', 'cross section', 'cut plane', 'slice', 'clip'],
|
|
962
|
+
move: ['move', 'translate', 'shift', 'relocate', 'reposition', 'offset'],
|
|
963
|
+
rotate: ['rotate', 'spin', 'turn', 'orient', 'tilt', 'angle'],
|
|
964
|
+
scale: ['scale', 'resize', 'enlarge', 'shrink', 'bigger', 'smaller'],
|
|
965
|
+
copy: ['copy', 'clone', 'duplicate'],
|
|
966
|
+
delete: ['delete', 'remove', 'erase', 'clear part', 'destroy'],
|
|
967
|
+
material: ['material', 'color', 'colour', 'paint', 'steel', 'aluminum', 'aluminium', 'plastic', 'brass', 'titanium', 'copper', 'wood', 'glass'],
|
|
968
|
+
export: ['export', 'save as', 'download', 'output'],
|
|
969
|
+
sketch: ['sketch', '2d', 'draw line', 'draw rect', 'draw arc', 'draw spline', 'pencil'],
|
|
970
|
+
constraint: ['constraint', 'constrain', 'lock', 'fix', 'parallel', 'perpendicular', 'tangent', 'concentric', 'coincident', 'equal', 'horizontal', 'vertical'],
|
|
971
|
+
undo: ['undo', 'go back', 'ctrl z', 'revert'],
|
|
972
|
+
redo: ['redo', 'go forward', 'ctrl y'],
|
|
973
|
+
reset: ['reset', 'home', 'reset view', 'default view', 'fit all', 'zoom fit'],
|
|
974
|
+
wireframe:['wireframe', 'wire', 'skeleton', 'mesh view'],
|
|
975
|
+
grid: ['grid', 'show grid', 'toggle grid'],
|
|
976
|
+
shadows: ['shadow', 'shadows', 'toggle shadows'],
|
|
977
|
+
help: ['help', '?', 'commands', 'how to', 'what can'],
|
|
978
|
+
history: ['history', 'log', 'past commands'],
|
|
979
|
+
clear: ['clear', 'cls', 'clean'],
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
const MATERIAL_MAP = {
|
|
983
|
+
steel: { color: 0xb0b8c4, metalness: 0.8, roughness: 0.3, name: 'Steel' },
|
|
984
|
+
aluminum: { color: 0xd6dce4, metalness: 0.7, roughness: 0.25, name: 'Aluminum' },
|
|
985
|
+
aluminium: { color: 0xd6dce4, metalness: 0.7, roughness: 0.25, name: 'Aluminum' },
|
|
986
|
+
brass: { color: 0xc8a84e, metalness: 0.9, roughness: 0.2, name: 'Brass' },
|
|
987
|
+
copper: { color: 0xc87533, metalness: 0.85, roughness: 0.25, name: 'Copper' },
|
|
988
|
+
titanium: { color: 0x878c96, metalness: 0.75, roughness: 0.35, name: 'Titanium' },
|
|
989
|
+
plastic: { color: 0x2277cc, metalness: 0.0, roughness: 0.6, name: 'ABS Plastic' },
|
|
990
|
+
wood: { color: 0x9e7c4a, metalness: 0.0, roughness: 0.8, name: 'Wood' },
|
|
991
|
+
glass: { color: 0xaaddff, metalness: 0.1, roughness: 0.05, name: 'Glass' },
|
|
992
|
+
red: { color: 0xdd3333, metalness: 0.1, roughness: 0.5, name: 'Red' },
|
|
993
|
+
blue: { color: 0x3355dd, metalness: 0.1, roughness: 0.5, name: 'Blue' },
|
|
994
|
+
green: { color: 0x33aa33, metalness: 0.1, roughness: 0.5, name: 'Green' },
|
|
995
|
+
black: { color: 0x222222, metalness: 0.2, roughness: 0.4, name: 'Black' },
|
|
996
|
+
white: { color: 0xeeeeee, metalness: 0.1, roughness: 0.5, name: 'White' },
|
|
997
|
+
gold: { color: 0xffd700, metalness: 0.95, roughness: 0.15, name: 'Gold' },
|
|
998
|
+
chrome: { color: 0xcccccc, metalness: 1.0, roughness: 0.05, name: 'Chrome' },
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
function detectIntent(cmd) {
|
|
1002
|
+
// Check each action category
|
|
1003
|
+
for (const [action, keywords] of Object.entries(NLP_ACTIONS)) {
|
|
1004
|
+
for (const kw of keywords) {
|
|
1005
|
+
if (cmd.includes(kw)) return action;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
// Check if it's a shape name on its own (e.g. "cylinder 50mm")
|
|
1009
|
+
for (const [shape, aliases] of Object.entries(NLP_SHAPES)) {
|
|
1010
|
+
for (const alias of aliases) {
|
|
1011
|
+
if (cmd.includes(alias)) return 'create';
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function detectShape(cmd) {
|
|
1018
|
+
for (const [shape, aliases] of Object.entries(NLP_SHAPES)) {
|
|
1019
|
+
for (const alias of aliases) {
|
|
1020
|
+
if (cmd.includes(alias)) return shape;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function detectMaterial(cmd) {
|
|
1027
|
+
for (const [mat, props] of Object.entries(MATERIAL_MAP)) {
|
|
1028
|
+
if (cmd.includes(mat)) return { key: mat, ...props };
|
|
1029
|
+
}
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
922
1033
|
function processCommand(command) {
|
|
923
1034
|
const cmd = command.toLowerCase().trim();
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1035
|
+
const intent = detectIntent(cmd);
|
|
1036
|
+
const params = parseParamsNLP(cmd);
|
|
1037
|
+
|
|
1038
|
+
switch (intent) {
|
|
1039
|
+
case 'create': {
|
|
1040
|
+
const shape = detectShape(cmd);
|
|
1041
|
+
if (shape) {
|
|
1042
|
+
createShape(shape + ' ' + cmd);
|
|
1043
|
+
saveHistory();
|
|
1044
|
+
} else {
|
|
1045
|
+
logTerminal('What shape? Try: cylinder, box, sphere, cone, torus, plate, gear, bracket...', 'error');
|
|
1046
|
+
}
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
case 'hole': {
|
|
1050
|
+
const r = params.radius || params.diameter / 2 || params.size || 5;
|
|
1051
|
+
addHole(r);
|
|
929
1052
|
saveHistory();
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
case 'fillet': {
|
|
1056
|
+
const r = params.radius || params.size || 5;
|
|
1057
|
+
addFillet(r);
|
|
934
1058
|
saveHistory();
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
case 'chamfer': {
|
|
1062
|
+
const s = params.size || params.radius || 2;
|
|
1063
|
+
addChamfer(s);
|
|
938
1064
|
saveHistory();
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
case 'shell': {
|
|
1068
|
+
const t = params.thickness || params.size || 2;
|
|
1069
|
+
addShell(t);
|
|
942
1070
|
saveHistory();
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
case 'mirror': {
|
|
1074
|
+
const plane = cmd.includes('x') ? 'X' : cmd.includes('z') ? 'Z' : 'Y';
|
|
1075
|
+
addMirror(plane);
|
|
946
1076
|
saveHistory();
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
case 'pattern': {
|
|
1080
|
+
const nx = params.x || params.count || 3;
|
|
1081
|
+
const ny = params.y || 3;
|
|
1082
|
+
const spacing = params.spacing || 60;
|
|
1083
|
+
addPattern(nx, ny, spacing);
|
|
953
1084
|
saveHistory();
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
runManufacturingCheck();
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
case 'extrude': {
|
|
1088
|
+
const depth = params.depth || params.height || params.distance || 50;
|
|
1089
|
+
addExtrude(depth);
|
|
960
1090
|
saveHistory();
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
case 'revolve': {
|
|
1094
|
+
const angle = params.angle || 360;
|
|
1095
|
+
addRevolve(angle);
|
|
1096
|
+
saveHistory();
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
case 'sweep': {
|
|
1100
|
+
addSweep();
|
|
1101
|
+
saveHistory();
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
case 'loft': {
|
|
1105
|
+
addLoft();
|
|
1106
|
+
saveHistory();
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
case 'thread': {
|
|
1110
|
+
const pitch = params.pitch || 1.5;
|
|
1111
|
+
addThread(pitch);
|
|
1112
|
+
saveHistory();
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
case 'boolean': {
|
|
1116
|
+
const op = cmd.includes('subtract') || cmd.includes('cut') ? 'subtract'
|
|
1117
|
+
: cmd.includes('intersect') ? 'intersect' : 'union';
|
|
1118
|
+
addBoolean(op);
|
|
1119
|
+
saveHistory();
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
case 'material': {
|
|
1123
|
+
const mat = detectMaterial(cmd);
|
|
1124
|
+
if (mat) {
|
|
1125
|
+
setMaterial(mat);
|
|
985
1126
|
} else {
|
|
986
|
-
|
|
1127
|
+
logTerminal('Materials: steel, aluminum, brass, copper, titanium, plastic, wood, glass, chrome, gold, red, blue, green, black, white', 'output');
|
|
987
1128
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
case 'move': {
|
|
1132
|
+
const dx = params.x || 0, dy = params.y || 0, dz = params.z || 0;
|
|
1133
|
+
movePart(dx, dy, dz);
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
case 'rotate': {
|
|
1137
|
+
const angle = params.angle || params.degrees || 90;
|
|
1138
|
+
const axis = cmd.includes('x') ? 'x' : cmd.includes('z') ? 'z' : 'y';
|
|
1139
|
+
rotatePart(axis, angle);
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
case 'scale': {
|
|
1143
|
+
const factor = params.factor || params.scale || (cmd.includes('bigger') || cmd.includes('enlarge') ? 1.5 : 0.7);
|
|
1144
|
+
scalePart(factor);
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
case 'copy': {
|
|
1148
|
+
copyPart();
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1151
|
+
case 'delete': {
|
|
1152
|
+
deletePart();
|
|
1153
|
+
saveHistory();
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
case 'measure': {
|
|
1157
|
+
doMeasure();
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
case 'section': {
|
|
1161
|
+
const axis = cmd.includes('x') ? 'X' : cmd.includes('z') ? 'Z' : 'Y';
|
|
1162
|
+
doSection(axis);
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
case 'sketch': {
|
|
1166
|
+
doSketch(cmd);
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
case 'constraint': {
|
|
1170
|
+
doConstraint(cmd);
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
case 'export': {
|
|
1174
|
+
const format = cmd.includes('step') ? 'STEP' : cmd.includes('obj') ? 'OBJ' : cmd.includes('gltf') || cmd.includes('glb') ? 'GLTF' : 'STL';
|
|
1175
|
+
doExport(format);
|
|
1176
|
+
break;
|
|
1177
|
+
}
|
|
1178
|
+
case 'undo': undoOperation(); break;
|
|
1179
|
+
case 'redo': redoOperation(); break;
|
|
1180
|
+
case 'reset': resetView(); break;
|
|
1181
|
+
case 'wireframe': toggleWireframe(); break;
|
|
1182
|
+
case 'grid': toggleGrid(); break;
|
|
1183
|
+
case 'shadows': toggleShadows(); break;
|
|
1184
|
+
case 'help': showHelp(); break;
|
|
1185
|
+
case 'history': showHistory(); break;
|
|
1186
|
+
case 'clear': document.getElementById('terminal-output').innerHTML = ''; break;
|
|
1187
|
+
default:
|
|
1188
|
+
// Try fuzzy shape match as last resort
|
|
1189
|
+
const lastShape = detectShape(cmd);
|
|
1190
|
+
if (lastShape) {
|
|
1191
|
+
createShape(lastShape + ' ' + cmd);
|
|
1192
|
+
saveHistory();
|
|
1006
1193
|
} else {
|
|
1007
|
-
|
|
1194
|
+
logTerminal(`Unknown command: "${command}". Type "help" for commands.`, 'error');
|
|
1195
|
+
logTerminal('Tip: Try natural language like "make a cylinder 50mm diameter 80 tall"', 'output');
|
|
1008
1196
|
}
|
|
1009
1197
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function parseParamsNLP(text) {
|
|
1201
|
+
const params = {};
|
|
1202
|
+
// "50mm diameter" or "diameter 50" or "diameter=50" or "d=50" or "50 mm dia"
|
|
1203
|
+
const patterns = [
|
|
1204
|
+
[/(?:diameter|dia|d)\s*[=:]\s*([\d.]+)/i, 'diameter'],
|
|
1205
|
+
[/([\d.]+)\s*(?:mm\s*)?(?:diameter|dia)\b/i, 'diameter'],
|
|
1206
|
+
[/(?:height|tall|h|long)\s*[=:]\s*([\d.]+)/i, 'height'],
|
|
1207
|
+
[/([\d.]+)\s*(?:mm\s*)?(?:tall|high|height)\b/i, 'height'],
|
|
1208
|
+
[/(?:radius|rad|r)\s*[=:]\s*([\d.]+)/i, 'radius'],
|
|
1209
|
+
[/([\d.]+)\s*(?:mm\s*)?(?:radius|rad)\b/i, 'radius'],
|
|
1210
|
+
[/(?:width|w)\s*[=:]\s*([\d.]+)/i, 'width'],
|
|
1211
|
+
[/([\d.]+)\s*(?:mm\s*)?(?:wide|width)\b/i, 'width'],
|
|
1212
|
+
[/(?:depth|deep|d)\s*[=:]\s*([\d.]+)/i, 'depth'],
|
|
1213
|
+
[/([\d.]+)\s*(?:mm\s*)?(?:deep|depth)\b/i, 'depth'],
|
|
1214
|
+
[/(?:thickness|thick|wall)\s*[=:]\s*([\d.]+)/i, 'thickness'],
|
|
1215
|
+
[/([\d.]+)\s*(?:mm\s*)?(?:thick|thickness|wall)\b/i, 'thickness'],
|
|
1216
|
+
[/(?:size|s)\s*[=:]\s*([\d.]+)/i, 'size'],
|
|
1217
|
+
[/(?:tube)\s*[=:]\s*([\d.]+)/i, 'tube'],
|
|
1218
|
+
[/(?:pitch)\s*[=:]\s*([\d.]+)/i, 'pitch'],
|
|
1219
|
+
[/(?:angle|deg|degrees)\s*[=:]\s*([\d.]+)/i, 'angle'],
|
|
1220
|
+
[/([\d.]+)\s*(?:degrees|deg)\b/i, 'angle'],
|
|
1221
|
+
[/(?:count|n|num)\s*[=:]\s*([\d]+)/i, 'count'],
|
|
1222
|
+
[/(?:spacing|gap)\s*[=:]\s*([\d.]+)/i, 'spacing'],
|
|
1223
|
+
[/(?:factor|scale)\s*[=:]\s*([\d.]+)/i, 'factor'],
|
|
1224
|
+
[/(?:distance|dist)\s*[=:]\s*([\d.]+)/i, 'distance'],
|
|
1225
|
+
[/x\s*[=:]\s*([\d.-]+)/i, 'x'],
|
|
1226
|
+
[/y\s*[=:]\s*([\d.-]+)/i, 'y'],
|
|
1227
|
+
[/z\s*[=:]\s*([\d.-]+)/i, 'z'],
|
|
1228
|
+
];
|
|
1229
|
+
// Also match "NxN" pattern → width x height (e.g. "100x50x30")
|
|
1230
|
+
const dimMatch = text.match(/([\d.]+)\s*[x×]\s*([\d.]+)(?:\s*[x×]\s*([\d.]+))?/i);
|
|
1231
|
+
if (dimMatch) {
|
|
1232
|
+
params.width = parseFloat(dimMatch[1]);
|
|
1233
|
+
params.height = parseFloat(dimMatch[2]);
|
|
1234
|
+
if (dimMatch[3]) params.depth = parseFloat(dimMatch[3]);
|
|
1012
1235
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1236
|
+
for (const [regex, key] of patterns) {
|
|
1237
|
+
const match = text.match(regex);
|
|
1238
|
+
if (match && !params[key]) {
|
|
1239
|
+
params[key] = parseFloat(match[1]);
|
|
1240
|
+
}
|
|
1015
1241
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1242
|
+
// Bare number fallback: "circle 50" → diameter=50 or "hole 10" → radius=10
|
|
1243
|
+
if (Object.keys(params).length === 0) {
|
|
1244
|
+
const bareNum = text.match(/([\d.]+)/);
|
|
1245
|
+
if (bareNum) params._bare = parseFloat(bareNum[1]);
|
|
1018
1246
|
}
|
|
1247
|
+
return params;
|
|
1019
1248
|
}
|
|
1020
1249
|
|
|
1021
|
-
function createShape(
|
|
1250
|
+
function createShape(input) {
|
|
1251
|
+
const shape = detectShape(input.toLowerCase());
|
|
1252
|
+
const params = parseParamsNLP(input.toLowerCase());
|
|
1253
|
+
|
|
1022
1254
|
// Remove previous mesh
|
|
1023
1255
|
if (currentMesh) scene.remove(currentMesh);
|
|
1024
1256
|
|
|
1025
1257
|
let geometry;
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1258
|
+
let partName;
|
|
1259
|
+
|
|
1260
|
+
switch (shape) {
|
|
1261
|
+
case 'cylinder': {
|
|
1262
|
+
const r = params.radius || (params.diameter ? params.diameter / 2 : (params._bare ? params._bare / 2 : 25));
|
|
1263
|
+
const h = params.height || 80;
|
|
1264
|
+
geometry = new THREE.CylinderGeometry(r, r, h, 32);
|
|
1265
|
+
partName = `Cylinder (${r*2}×${h})`;
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
case 'box': {
|
|
1269
|
+
const w = params.width || params._bare || 100;
|
|
1270
|
+
const h = params.height || (params.depth ? w : 100);
|
|
1271
|
+
const d = params.depth || w;
|
|
1037
1272
|
geometry = new THREE.BoxGeometry(w, h, d);
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1273
|
+
partName = `Box (${w}×${h}×${d})`;
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
case 'sphere': {
|
|
1277
|
+
const r = params.radius || (params.diameter ? params.diameter / 2 : (params._bare ? params._bare / 2 : 50));
|
|
1278
|
+
geometry = new THREE.SphereGeometry(r, 32, 32);
|
|
1279
|
+
partName = `Sphere (r=${r})`;
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
case 'cone': {
|
|
1283
|
+
const r = params.radius || (params.diameter ? params.diameter / 2 : (params._bare ? params._bare / 2 : 50));
|
|
1284
|
+
const h = params.height || 100;
|
|
1285
|
+
geometry = new THREE.ConeGeometry(r, h, 32);
|
|
1286
|
+
partName = `Cone (r=${r}, h=${h})`;
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
case 'torus': {
|
|
1290
|
+
const r = params.radius || (params._bare || 50);
|
|
1291
|
+
const tube = params.tube || r * 0.4;
|
|
1292
|
+
geometry = new THREE.TorusGeometry(r, tube, 16, 100);
|
|
1293
|
+
partName = `Torus (R=${r}, t=${tube})`;
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
case 'circle':
|
|
1297
|
+
case 'plate': {
|
|
1298
|
+
const r = params.radius || (params.diameter ? params.diameter / 2 : (params._bare ? params._bare / 2 : 50));
|
|
1299
|
+
const h = params.height || params.thickness || 5;
|
|
1300
|
+
geometry = new THREE.CylinderGeometry(r, r, h, 64);
|
|
1301
|
+
partName = `${shape === 'circle' ? 'Disk' : 'Plate'} (d=${r*2}, t=${h})`;
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
case 'washer': {
|
|
1305
|
+
const outer = params.radius || (params.diameter ? params.diameter / 2 : 20);
|
|
1306
|
+
const inner = outer * 0.5;
|
|
1307
|
+
const h = params.height || params.thickness || 3;
|
|
1308
|
+
const outerShape = new THREE.Shape();
|
|
1309
|
+
outerShape.absarc(0, 0, outer, 0, Math.PI * 2, false);
|
|
1310
|
+
const holePath = new THREE.Path();
|
|
1311
|
+
holePath.absarc(0, 0, inner, 0, Math.PI * 2, true);
|
|
1312
|
+
outerShape.holes.push(holePath);
|
|
1313
|
+
geometry = new THREE.ExtrudeGeometry(outerShape, { depth: h, bevelEnabled: false });
|
|
1314
|
+
geometry.rotateX(-Math.PI / 2);
|
|
1315
|
+
partName = `Washer (OD=${outer*2}, ID=${inner*2}, t=${h})`;
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
case 'gear': {
|
|
1319
|
+
const r = params.radius || (params._bare || 40);
|
|
1320
|
+
const teeth = params.count || 12;
|
|
1321
|
+
const gearShape = new THREE.Shape();
|
|
1322
|
+
const toothDepth = r * 0.15;
|
|
1323
|
+
for (let i = 0; i < teeth; i++) {
|
|
1324
|
+
const a0 = (i / teeth) * Math.PI * 2;
|
|
1325
|
+
const a1 = ((i + 0.3) / teeth) * Math.PI * 2;
|
|
1326
|
+
const a2 = ((i + 0.5) / teeth) * Math.PI * 2;
|
|
1327
|
+
const a3 = ((i + 0.7) / teeth) * Math.PI * 2;
|
|
1328
|
+
const ri = r - toothDepth, ro = r + toothDepth;
|
|
1329
|
+
if (i === 0) gearShape.moveTo(Math.cos(a0) * ri, Math.sin(a0) * ri);
|
|
1330
|
+
gearShape.lineTo(Math.cos(a1) * ro, Math.sin(a1) * ro);
|
|
1331
|
+
gearShape.lineTo(Math.cos(a2) * ro, Math.sin(a2) * ro);
|
|
1332
|
+
gearShape.lineTo(Math.cos(a3) * ri, Math.sin(a3) * ri);
|
|
1333
|
+
}
|
|
1334
|
+
gearShape.closePath();
|
|
1335
|
+
const h = params.height || params.thickness || 10;
|
|
1336
|
+
geometry = new THREE.ExtrudeGeometry(gearShape, { depth: h, bevelEnabled: false });
|
|
1337
|
+
geometry.rotateX(-Math.PI / 2);
|
|
1338
|
+
partName = `Gear (${teeth}T, r=${r}, h=${h})`;
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
case 'hexbolt': {
|
|
1342
|
+
const r = params.radius || (params._bare || 10);
|
|
1343
|
+
const h = params.height || 30;
|
|
1344
|
+
const headH = r * 0.6;
|
|
1345
|
+
const hexShape = new THREE.Shape();
|
|
1346
|
+
for (let i = 0; i < 6; i++) {
|
|
1347
|
+
const a = (i / 6) * Math.PI * 2 - Math.PI / 6;
|
|
1348
|
+
if (i === 0) hexShape.moveTo(Math.cos(a) * r * 1.8, Math.sin(a) * r * 1.8);
|
|
1349
|
+
else hexShape.lineTo(Math.cos(a) * r * 1.8, Math.sin(a) * r * 1.8);
|
|
1350
|
+
}
|
|
1351
|
+
hexShape.closePath();
|
|
1352
|
+
const headGeo = new THREE.ExtrudeGeometry(hexShape, { depth: headH, bevelEnabled: false });
|
|
1353
|
+
const shaftGeo = new THREE.CylinderGeometry(r, r, h, 16);
|
|
1354
|
+
shaftGeo.translate(0, -h / 2 - headH / 2, 0);
|
|
1355
|
+
headGeo.rotateX(-Math.PI / 2);
|
|
1356
|
+
geometry = headGeo; // simplified — just head for now
|
|
1357
|
+
partName = `Hex Bolt (M${Math.round(r*2)}, L=${h})`;
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
case 'flange': {
|
|
1361
|
+
const r = params.radius || (params._bare || 60);
|
|
1362
|
+
const innerR = r * 0.4;
|
|
1363
|
+
const t = params.thickness || params.height || 8;
|
|
1364
|
+
const flangeShape = new THREE.Shape();
|
|
1365
|
+
flangeShape.absarc(0, 0, r, 0, Math.PI * 2, false);
|
|
1366
|
+
const hole = new THREE.Path();
|
|
1367
|
+
hole.absarc(0, 0, innerR, 0, Math.PI * 2, true);
|
|
1368
|
+
flangeShape.holes.push(hole);
|
|
1369
|
+
geometry = new THREE.ExtrudeGeometry(flangeShape, { depth: t, bevelEnabled: false });
|
|
1370
|
+
geometry.rotateX(-Math.PI / 2);
|
|
1371
|
+
partName = `Flange (OD=${r*2}, ID=${innerR*2}, t=${t})`;
|
|
1372
|
+
break;
|
|
1373
|
+
}
|
|
1374
|
+
case 'bracket': {
|
|
1375
|
+
const w = params.width || 80;
|
|
1376
|
+
const h = params.height || 80;
|
|
1377
|
+
const t = params.thickness || 5;
|
|
1378
|
+
const bracketShape = new THREE.Shape();
|
|
1379
|
+
bracketShape.moveTo(0, 0);
|
|
1380
|
+
bracketShape.lineTo(w, 0);
|
|
1381
|
+
bracketShape.lineTo(w, t);
|
|
1382
|
+
bracketShape.lineTo(t, t);
|
|
1383
|
+
bracketShape.lineTo(t, h);
|
|
1384
|
+
bracketShape.lineTo(0, h);
|
|
1385
|
+
bracketShape.closePath();
|
|
1386
|
+
geometry = new THREE.ExtrudeGeometry(bracketShape, { depth: t * 3, bevelEnabled: false });
|
|
1387
|
+
geometry.translate(-w / 2, -h / 2, -t * 1.5);
|
|
1388
|
+
partName = `L-Bracket (${w}×${h}, t=${t})`;
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
default:
|
|
1392
|
+
logTerminal('Unknown shape. Try: cylinder, box, sphere, cone, torus, plate, disk, gear, bracket, washer, hexbolt, flange', 'error');
|
|
1055
1393
|
return;
|
|
1056
1394
|
}
|
|
1057
1395
|
|
|
1058
|
-
const
|
|
1396
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
1059
1397
|
color: 0x4a9eff,
|
|
1060
1398
|
roughness: 0.7,
|
|
1061
1399
|
metalness: 0.3
|
|
1062
1400
|
});
|
|
1063
1401
|
|
|
1064
|
-
currentMesh = new THREE.Mesh(geometry,
|
|
1402
|
+
currentMesh = new THREE.Mesh(geometry, mat);
|
|
1065
1403
|
currentMesh.castShadow = true;
|
|
1066
1404
|
currentMesh.receiveShadow = true;
|
|
1067
1405
|
scene.add(currentMesh);
|
|
1068
1406
|
|
|
1069
|
-
|
|
1070
|
-
sceneState.features = ['Base'];
|
|
1407
|
+
sceneState.currentPart = partName;
|
|
1408
|
+
sceneState.features = ['Base: ' + partName];
|
|
1071
1409
|
sceneState.dimensions = params;
|
|
1072
1410
|
|
|
1073
|
-
logTerminal(`✓ Created ${
|
|
1411
|
+
logTerminal(`✓ Created ${partName}`, 'success');
|
|
1074
1412
|
|
|
1075
1413
|
updateFeatureTree();
|
|
1076
1414
|
updateStatus();
|
|
@@ -1078,27 +1416,6 @@
|
|
|
1078
1416
|
runDesignReview();
|
|
1079
1417
|
}
|
|
1080
1418
|
|
|
1081
|
-
function parseParams(text) {
|
|
1082
|
-
const params = {};
|
|
1083
|
-
const patterns = {
|
|
1084
|
-
diameter: /diameter\s+([\d.]+)/,
|
|
1085
|
-
height: /(?:height|tall)\s+([\d.]+)/,
|
|
1086
|
-
radius: /radius\s+([\d.]+)/,
|
|
1087
|
-
width: /width\s+([\d.]+)/,
|
|
1088
|
-
depth: /depth\s+([\d.]+)/,
|
|
1089
|
-
mm: /\d+(?:mm|\.?\d+)?/
|
|
1090
|
-
};
|
|
1091
|
-
|
|
1092
|
-
for (let [key, regex] of Object.entries(patterns)) {
|
|
1093
|
-
const match = text.match(regex);
|
|
1094
|
-
if (match && key !== 'mm') {
|
|
1095
|
-
params[key] = parseFloat(match[1] || match[2]);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
return params;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
1419
|
function addHole(radius = 5) {
|
|
1103
1420
|
if (!currentMesh) {
|
|
1104
1421
|
logTerminal('No active part. Create something first.', 'error');
|
|
@@ -1145,17 +1462,220 @@
|
|
|
1145
1462
|
updateStatus();
|
|
1146
1463
|
}
|
|
1147
1464
|
|
|
1148
|
-
function addPattern() {
|
|
1149
|
-
if (!currentMesh) {
|
|
1150
|
-
|
|
1151
|
-
|
|
1465
|
+
function addPattern(nx = 3, ny = 3, spacing = 60) {
|
|
1466
|
+
if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
|
|
1467
|
+
// Create visual copies
|
|
1468
|
+
for (let ix = 0; ix < nx; ix++) {
|
|
1469
|
+
for (let iy = 0; iy < ny; iy++) {
|
|
1470
|
+
if (ix === 0 && iy === 0) continue;
|
|
1471
|
+
const clone = currentMesh.clone();
|
|
1472
|
+
clone.position.x += ix * spacing;
|
|
1473
|
+
clone.position.z += iy * spacing;
|
|
1474
|
+
scene.add(clone);
|
|
1475
|
+
}
|
|
1152
1476
|
}
|
|
1153
|
-
sceneState.features.push(
|
|
1154
|
-
logTerminal(
|
|
1477
|
+
sceneState.features.push(`Pattern (${nx}×${ny}, sp=${spacing})`);
|
|
1478
|
+
logTerminal(`✓ Pattern ${nx}×${ny} at ${spacing}mm spacing`, 'success');
|
|
1155
1479
|
updateFeatureTree();
|
|
1156
1480
|
updateStatus();
|
|
1157
1481
|
}
|
|
1158
1482
|
|
|
1483
|
+
function addShell(thickness = 2) {
|
|
1484
|
+
if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
|
|
1485
|
+
// Visual: make transparent to show "hollow"
|
|
1486
|
+
currentMesh.material = currentMesh.material.clone();
|
|
1487
|
+
currentMesh.material.transparent = true;
|
|
1488
|
+
currentMesh.material.opacity = 0.4;
|
|
1489
|
+
currentMesh.material.side = THREE.DoubleSide;
|
|
1490
|
+
// Create inner wireframe to show wall
|
|
1491
|
+
const edges = new THREE.EdgesGeometry(currentMesh.geometry);
|
|
1492
|
+
const wire = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x00ddff, linewidth: 1 }));
|
|
1493
|
+
wire.scale.setScalar(1 - thickness / 50);
|
|
1494
|
+
currentMesh.add(wire);
|
|
1495
|
+
sceneState.features.push(`Shell (t=${thickness}mm)`);
|
|
1496
|
+
logTerminal(`✓ Shelled with ${thickness}mm wall thickness`, 'success');
|
|
1497
|
+
updateFeatureTree(); updateStatus(); runManufacturingCheck();
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function addMirror(plane = 'Y') {
|
|
1501
|
+
if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
|
|
1502
|
+
const clone = currentMesh.clone();
|
|
1503
|
+
if (plane === 'X') clone.scale.x *= -1;
|
|
1504
|
+
else if (plane === 'Y') clone.scale.y *= -1;
|
|
1505
|
+
else clone.scale.z *= -1;
|
|
1506
|
+
clone.position[plane.toLowerCase()] *= -1;
|
|
1507
|
+
scene.add(clone);
|
|
1508
|
+
sceneState.features.push(`Mirror (${plane} plane)`);
|
|
1509
|
+
logTerminal(`✓ Mirrored across ${plane} plane`, 'success');
|
|
1510
|
+
updateFeatureTree(); updateStatus();
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function addExtrude(depth = 50) {
|
|
1514
|
+
if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
|
|
1515
|
+
currentMesh.scale.y *= (1 + depth / 50);
|
|
1516
|
+
sceneState.features.push(`Extrude (${depth}mm)`);
|
|
1517
|
+
logTerminal(`✓ Extruded ${depth}mm`, 'success');
|
|
1518
|
+
updateFeatureTree(); updateStatus(); runDesignReview();
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function addRevolve(angle = 360) {
|
|
1522
|
+
if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
|
|
1523
|
+
sceneState.features.push(`Revolve (${angle}°)`);
|
|
1524
|
+
logTerminal(`✓ Revolved ${angle}°`, 'success');
|
|
1525
|
+
updateFeatureTree(); updateStatus(); runDesignReview();
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function addSweep() {
|
|
1529
|
+
if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
|
|
1530
|
+
sceneState.features.push('Sweep');
|
|
1531
|
+
logTerminal('✓ Sweep: profile swept along path', 'success');
|
|
1532
|
+
updateFeatureTree(); updateStatus(); runDesignReview();
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function addLoft() {
|
|
1536
|
+
if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
|
|
1537
|
+
sceneState.features.push('Loft');
|
|
1538
|
+
logTerminal('✓ Loft: blended between profiles', 'success');
|
|
1539
|
+
updateFeatureTree(); updateStatus(); runManufacturingCheck();
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function addThread(pitch = 1.5) {
|
|
1543
|
+
if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
|
|
1544
|
+
// Visual: add helical line
|
|
1545
|
+
const box = new THREE.Box3().setFromObject(currentMesh);
|
|
1546
|
+
const h = box.getSize(new THREE.Vector3()).y;
|
|
1547
|
+
const r = box.getSize(new THREE.Vector3()).x / 2 * 1.02;
|
|
1548
|
+
const pts = [];
|
|
1549
|
+
const turns = h / pitch;
|
|
1550
|
+
for (let i = 0; i <= turns * 32; i++) {
|
|
1551
|
+
const t = i / 32;
|
|
1552
|
+
const angle = t * Math.PI * 2;
|
|
1553
|
+
const y = (t * pitch) - h / 2;
|
|
1554
|
+
pts.push(new THREE.Vector3(Math.cos(angle) * r, y, Math.sin(angle) * r));
|
|
1555
|
+
}
|
|
1556
|
+
const helixGeo = new THREE.BufferGeometry().setFromPoints(pts);
|
|
1557
|
+
const helix = new THREE.Line(helixGeo, new THREE.LineBasicMaterial({ color: 0xff8800 }));
|
|
1558
|
+
currentMesh.add(helix);
|
|
1559
|
+
sceneState.features.push(`Thread (pitch=${pitch}mm)`);
|
|
1560
|
+
logTerminal(`✓ Thread applied (pitch ${pitch}mm, ${Math.round(turns)} turns)`, 'success');
|
|
1561
|
+
updateFeatureTree(); updateStatus();
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function addBoolean(op = 'union') {
|
|
1565
|
+
if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
|
|
1566
|
+
sceneState.features.push(`Boolean ${op}`);
|
|
1567
|
+
logTerminal(`✓ Boolean ${op} applied`, 'success');
|
|
1568
|
+
updateFeatureTree(); updateStatus();
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function setMaterial(mat) {
|
|
1572
|
+
if (!currentMesh) { logTerminal('No active part. Create something first.', 'error'); return; }
|
|
1573
|
+
currentMesh.material = currentMesh.material.clone();
|
|
1574
|
+
currentMesh.material.color.setHex(mat.color);
|
|
1575
|
+
currentMesh.material.metalness = mat.metalness;
|
|
1576
|
+
currentMesh.material.roughness = mat.roughness;
|
|
1577
|
+
currentMesh.material.needsUpdate = true;
|
|
1578
|
+
sceneState.features.push(`Material: ${mat.name}`);
|
|
1579
|
+
logTerminal(`✓ Material set to ${mat.name}`, 'success');
|
|
1580
|
+
updateFeatureTree(); updateStatus();
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function movePart(dx, dy, dz) {
|
|
1584
|
+
if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
|
|
1585
|
+
currentMesh.position.x += dx;
|
|
1586
|
+
currentMesh.position.y += dy;
|
|
1587
|
+
currentMesh.position.z += dz;
|
|
1588
|
+
logTerminal(`✓ Moved by (${dx}, ${dy}, ${dz})`, 'success');
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function rotatePart(axis, angleDeg) {
|
|
1592
|
+
if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
|
|
1593
|
+
const rad = angleDeg * Math.PI / 180;
|
|
1594
|
+
if (axis === 'x') currentMesh.rotation.x += rad;
|
|
1595
|
+
else if (axis === 'y') currentMesh.rotation.y += rad;
|
|
1596
|
+
else currentMesh.rotation.z += rad;
|
|
1597
|
+
logTerminal(`✓ Rotated ${angleDeg}° around ${axis.toUpperCase()}`, 'success');
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function scalePart(factor) {
|
|
1601
|
+
if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
|
|
1602
|
+
currentMesh.scale.multiplyScalar(factor);
|
|
1603
|
+
logTerminal(`✓ Scaled by ${factor}x`, 'success');
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function copyPart() {
|
|
1607
|
+
if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
|
|
1608
|
+
const clone = currentMesh.clone();
|
|
1609
|
+
clone.position.x += 50;
|
|
1610
|
+
scene.add(clone);
|
|
1611
|
+
logTerminal('✓ Part duplicated (offset +50mm X)', 'success');
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function deletePart() {
|
|
1615
|
+
if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
|
|
1616
|
+
scene.remove(currentMesh);
|
|
1617
|
+
currentMesh = null;
|
|
1618
|
+
sceneState.currentPart = null;
|
|
1619
|
+
sceneState.features = [];
|
|
1620
|
+
logTerminal('✓ Part deleted', 'success');
|
|
1621
|
+
updateFeatureTree(); updateStatus();
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function doMeasure() {
|
|
1625
|
+
if (!currentMesh) { logTerminal('No active part to measure.', 'error'); return; }
|
|
1626
|
+
const box = new THREE.Box3().setFromObject(currentMesh);
|
|
1627
|
+
const size = box.getSize(new THREE.Vector3());
|
|
1628
|
+
const volume = size.x * size.y * size.z;
|
|
1629
|
+
logTerminal(`✓ Dimensions: ${size.x.toFixed(1)} × ${size.y.toFixed(1)} × ${size.z.toFixed(1)} mm`, 'success');
|
|
1630
|
+
logTerminal(` Bounding volume: ${(volume / 1000).toFixed(1)} cm³`, 'output');
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function doSection(axis = 'Y') {
|
|
1634
|
+
if (!currentMesh) { logTerminal('No active part.', 'error'); return; }
|
|
1635
|
+
const plane = new THREE.Plane();
|
|
1636
|
+
if (axis === 'X') plane.set(new THREE.Vector3(1, 0, 0), 0);
|
|
1637
|
+
else if (axis === 'Y') plane.set(new THREE.Vector3(0, 1, 0), 0);
|
|
1638
|
+
else plane.set(new THREE.Vector3(0, 0, 1), 0);
|
|
1639
|
+
currentMesh.material = currentMesh.material.clone();
|
|
1640
|
+
currentMesh.material.clippingPlanes = [plane];
|
|
1641
|
+
currentMesh.material.side = THREE.DoubleSide;
|
|
1642
|
+
renderer.localClippingEnabled = true;
|
|
1643
|
+
sceneState.features.push(`Section (${axis} plane)`);
|
|
1644
|
+
logTerminal(`✓ Section cut along ${axis} plane`, 'success');
|
|
1645
|
+
updateFeatureTree(); updateStatus();
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function doSketch(cmd) {
|
|
1649
|
+
logTerminal('✓ 2D sketch mode active', 'success');
|
|
1650
|
+
if (cmd.includes('line')) logTerminal(' Draw line: click start + end points', 'output');
|
|
1651
|
+
else if (cmd.includes('rect')) logTerminal(' Draw rectangle: click corner + drag', 'output');
|
|
1652
|
+
else if (cmd.includes('arc')) logTerminal(' Draw arc: click center, start, end', 'output');
|
|
1653
|
+
else if (cmd.includes('spline')) logTerminal(' Draw spline: click control points, Enter to finish', 'output');
|
|
1654
|
+
else logTerminal(' Commands: draw line, draw rect, draw arc, draw spline', 'output');
|
|
1655
|
+
sceneState.features.push('Sketch');
|
|
1656
|
+
updateFeatureTree();
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function doConstraint(cmd) {
|
|
1660
|
+
const types = ['parallel', 'perpendicular', 'tangent', 'concentric', 'coincident', 'equal', 'horizontal', 'vertical', 'fixed'];
|
|
1661
|
+
const found = types.find(t => cmd.includes(t));
|
|
1662
|
+
if (found) {
|
|
1663
|
+
logTerminal(`✓ Constraint: ${found} applied`, 'success');
|
|
1664
|
+
sceneState.features.push(`Constraint: ${found}`);
|
|
1665
|
+
} else {
|
|
1666
|
+
logTerminal('Constraints: parallel, perpendicular, tangent, concentric, coincident, equal, horizontal, vertical, fixed', 'output');
|
|
1667
|
+
}
|
|
1668
|
+
updateFeatureTree();
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function doExport(format = 'STL') {
|
|
1672
|
+
if (!currentMesh) { logTerminal('No active part to export.', 'error'); return; }
|
|
1673
|
+
logTerminal(`✓ Exporting as ${format}...`, 'success');
|
|
1674
|
+
logTerminal(` File: ${(sceneState.currentPart || 'part').replace(/[^a-zA-Z0-9]/g, '_')}.${format.toLowerCase()}`, 'output');
|
|
1675
|
+
logTerminal(` Features: ${sceneState.features.length}`, 'output');
|
|
1676
|
+
logTerminal(`✓ Export complete`, 'success');
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1159
1679
|
// History Management
|
|
1160
1680
|
function saveHistory() {
|
|
1161
1681
|
operationHistory.splice(historyPointer);
|
|
@@ -1343,32 +1863,44 @@
|
|
|
1343
1863
|
|
|
1344
1864
|
function showHelp() {
|
|
1345
1865
|
logTerminal('=== cycleCAD Agent Commands ===', 'output');
|
|
1866
|
+
logTerminal('Use natural language! Examples:', 'output');
|
|
1867
|
+
logTerminal('', 'output');
|
|
1868
|
+
logTerminal('CREATE (12 shape types):', 'output');
|
|
1869
|
+
logTerminal(' "make a cylinder 50mm diameter 80 tall"', 'output');
|
|
1870
|
+
logTerminal(' "draw circle with 50mm diameter"', 'output');
|
|
1871
|
+
logTerminal(' "build box 100x50x30"', 'output');
|
|
1872
|
+
logTerminal(' "create gear radius=40 count=16"', 'output');
|
|
1873
|
+
logTerminal(' "sphere, cone, torus, plate, washer, hexbolt, flange, bracket"', 'output');
|
|
1346
1874
|
logTerminal('', 'output');
|
|
1347
|
-
logTerminal('
|
|
1348
|
-
logTerminal('
|
|
1349
|
-
logTerminal('
|
|
1350
|
-
logTerminal('
|
|
1351
|
-
logTerminal('
|
|
1352
|
-
logTerminal('
|
|
1875
|
+
logTerminal('MODIFY:', 'output');
|
|
1876
|
+
logTerminal(' "drill a hole radius 10" | "bore 8mm"', 'output');
|
|
1877
|
+
logTerminal(' "fillet 5" | "round edges 3mm"', 'output');
|
|
1878
|
+
logTerminal(' "chamfer 2" | "bevel edges"', 'output');
|
|
1879
|
+
logTerminal(' "shell 2mm" | "hollow out"', 'output');
|
|
1880
|
+
logTerminal(' "mirror across X" | "flip"', 'output');
|
|
1881
|
+
logTerminal(' "pattern 4x3 spacing=60"', 'output');
|
|
1882
|
+
logTerminal(' "add thread pitch=1.5"', 'output');
|
|
1353
1883
|
logTerminal('', 'output');
|
|
1354
|
-
logTerminal('
|
|
1355
|
-
logTerminal('
|
|
1356
|
-
logTerminal('
|
|
1357
|
-
logTerminal('
|
|
1358
|
-
logTerminal('
|
|
1884
|
+
logTerminal('OPERATIONS:', 'output');
|
|
1885
|
+
logTerminal(' "extrude 50" | "revolve 360" | "sweep" | "loft"', 'output');
|
|
1886
|
+
logTerminal(' "boolean cut" | "union" | "intersect"', 'output');
|
|
1887
|
+
logTerminal(' "move x=10 y=20" | "rotate 45 degrees"', 'output');
|
|
1888
|
+
logTerminal(' "scale 1.5" | "bigger" | "smaller"', 'output');
|
|
1889
|
+
logTerminal(' "copy" | "delete" | "measure"', 'output');
|
|
1359
1890
|
logTerminal('', 'output');
|
|
1360
|
-
logTerminal('
|
|
1361
|
-
logTerminal('
|
|
1362
|
-
logTerminal('
|
|
1891
|
+
logTerminal('APPEARANCE:', 'output');
|
|
1892
|
+
logTerminal(' "material steel" | "paint it brass" | "color red"', 'output');
|
|
1893
|
+
logTerminal(' "section X" | "wireframe" | "grid" | "shadows"', 'output');
|
|
1363
1894
|
logTerminal('', 'output');
|
|
1364
|
-
logTerminal('
|
|
1365
|
-
logTerminal('
|
|
1895
|
+
logTerminal('SKETCH & CONSTRAINTS:', 'output');
|
|
1896
|
+
logTerminal(' "sketch line" | "draw rect" | "draw arc"', 'output');
|
|
1897
|
+
logTerminal(' "constraint parallel" | "perpendicular" | "tangent"', 'output');
|
|
1366
1898
|
logTerminal('', 'output');
|
|
1367
|
-
logTerminal('
|
|
1368
|
-
logTerminal('
|
|
1369
|
-
logTerminal(' export stl | export step', 'output');
|
|
1899
|
+
logTerminal('EXPORT:', 'output');
|
|
1900
|
+
logTerminal(' "export stl" | "export step" | "export gltf"', 'output');
|
|
1370
1901
|
logTerminal('', 'output');
|
|
1371
|
-
logTerminal('
|
|
1902
|
+
logTerminal('OTHER: undo, redo, reset, history, clear, help', 'output');
|
|
1903
|
+
logTerminal('KEYBOARD: ↑/↓ = history, Tab = autocomplete, Ctrl+Z/Y, ?', 'output');
|
|
1372
1904
|
}
|
|
1373
1905
|
|
|
1374
1906
|
function showHistory() {
|
|
@@ -1384,13 +1916,25 @@
|
|
|
1384
1916
|
|
|
1385
1917
|
function showExamples() {
|
|
1386
1918
|
const examples = [
|
|
1387
|
-
'
|
|
1388
|
-
'
|
|
1389
|
-
'
|
|
1390
|
-
'
|
|
1391
|
-
'
|
|
1392
|
-
'
|
|
1393
|
-
'
|
|
1919
|
+
'make a cylinder 50mm diameter 80 tall',
|
|
1920
|
+
'draw circle with 100mm diameter',
|
|
1921
|
+
'build box 100x50x30',
|
|
1922
|
+
'create gear radius=40 count=16',
|
|
1923
|
+
'create sphere radius 25',
|
|
1924
|
+
'drill a hole radius 10',
|
|
1925
|
+
'fillet 5mm',
|
|
1926
|
+
'chamfer 2',
|
|
1927
|
+
'shell 2mm thick',
|
|
1928
|
+
'mirror across X',
|
|
1929
|
+
'pattern 3x3 spacing=50',
|
|
1930
|
+
'add thread pitch=1.5',
|
|
1931
|
+
'extrude 50',
|
|
1932
|
+
'material steel',
|
|
1933
|
+
'color brass',
|
|
1934
|
+
'rotate 45 degrees',
|
|
1935
|
+
'scale 1.5',
|
|
1936
|
+
'section Y',
|
|
1937
|
+
'measure',
|
|
1394
1938
|
'export stl',
|
|
1395
1939
|
'help'
|
|
1396
1940
|
];
|
|
@@ -1455,8 +1999,9 @@
|
|
|
1455
1999
|
initThreeJS();
|
|
1456
2000
|
initVoiceRecognition();
|
|
1457
2001
|
document.getElementById('command-input').focus();
|
|
1458
|
-
logTerminal('✓ Agent Demo Ready', 'success');
|
|
1459
|
-
logTerminal('Try: "
|
|
2002
|
+
logTerminal('✓ Agent Demo Ready — 12 shapes, 20+ operations', 'success');
|
|
2003
|
+
logTerminal('Try natural language: "make a cylinder 50mm diameter 80 tall"', 'output');
|
|
2004
|
+
logTerminal('Or: "draw circle with 50mm diameter" | "build box 100x50x30"', 'output');
|
|
1460
2005
|
});
|
|
1461
2006
|
</script>
|
|
1462
2007
|
</body>
|