mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
Add files via upload
This commit is contained in:
288
exploded-diagrams/css/main.css
Normal file
288
exploded-diagrams/css/main.css
Normal file
@@ -0,0 +1,288 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg-dark: #0f0f1a;
|
||||
--bg-panel: rgba(255,255,255,0.03);
|
||||
--border: rgba(255,255,255,0.06);
|
||||
--accent: #00d4ff;
|
||||
--accent-alt: #00ff88;
|
||||
--text: #fff;
|
||||
--text-muted: #888;
|
||||
--text-dim: #666;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, var(--bg-dark) 0%, #1a1a2e 50%, #0f1a2e 100%);
|
||||
min-height: 100vh;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 15px 0 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: 3px;
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent-alt), #7b2cbf);
|
||||
background-size: 200% auto;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
animation: gradientShift 3s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0%, 100% { background-position: 0% center; }
|
||||
50% { background-position: 100% center; }
|
||||
}
|
||||
|
||||
.subtitle { color: var(--text-dim); font-size: 0.85rem; letter-spacing: 1px; }
|
||||
|
||||
/* Breadcrumb */
|
||||
#breadcrumb {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
background: var(--bg-panel);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 0.85rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#breadcrumb a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
#breadcrumb a:hover { opacity: 0.7; }
|
||||
#breadcrumb span { color: var(--text-dim); }
|
||||
#breadcrumb .current { color: var(--text); font-weight: 500; }
|
||||
|
||||
/* Controls */
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
padding: 18px 25px;
|
||||
background: var(--bg-panel);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
width: 140px;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
background: linear-gradient(90deg, #333, #444);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-alt));
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
select, button {
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
select {
|
||||
background: rgba(40, 40, 60, 0.8);
|
||||
color: var(--text);
|
||||
min-width: 170px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
select:hover { border-color: rgba(0, 212, 255, 0.3); }
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, rgba(0,212,255,0.2), rgba(123,44,191,0.2));
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
border: 1px solid rgba(0,212,255,0.3);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: linear-gradient(135deg, rgba(0,212,255,0.4), rgba(123,44,191,0.4));
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 5px 20px rgba(0, 212, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Main layout */
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 280px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
main { grid-template-columns: 1fr; }
|
||||
#sidebar { order: -1; }
|
||||
}
|
||||
|
||||
.diagram-container {
|
||||
background: #fafafa;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
#diagram { width: 100%; height: auto; display: block; }
|
||||
|
||||
/* Sidebar */
|
||||
#sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--bg-panel);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.panel h3 {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
color: var(--accent);
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stat-value { font-size: 1.4rem; font-weight: 600; color: var(--accent); }
|
||||
.stat-label { font-size: 0.65rem; text-transform: uppercase; color: var(--text-dim); letter-spacing: 1px; }
|
||||
|
||||
.parts-list { max-height: 300px; overflow-y: auto; }
|
||||
|
||||
.part-item {
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.part-item:hover { background: rgba(0, 212, 255, 0.1); }
|
||||
.part-item.highlighted { background: rgba(0, 212, 255, 0.2); border: 1px solid rgba(0, 212, 255, 0.3); }
|
||||
.part-item .name { font-size: 0.8rem; color: #ddd; }
|
||||
.part-item .pn { font-size: 0.7rem; color: var(--text-dim); font-family: monospace; }
|
||||
|
||||
.legend-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
||||
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 0.7rem; color: var(--text-muted); }
|
||||
.legend-color { width: 12px; height: 12px; border-radius: 2px; flex-shrink: 0; }
|
||||
|
||||
/* Tooltip */
|
||||
.tooltip {
|
||||
position: fixed;
|
||||
background: rgba(10, 10, 20, 0.95);
|
||||
padding: 12px 18px;
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.tooltip.visible { display: block; }
|
||||
.tooltip h4 { font-size: 0.95rem; color: var(--accent); margin-bottom: 4px; }
|
||||
.tooltip .details { font-size: 0.75rem; color: var(--text-muted); line-height: 1.5; }
|
||||
.tooltip .material {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 4px;
|
||||
margin-top: 6px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
.parts-list::-webkit-scrollbar { width: 4px; }
|
||||
.parts-list::-webkit-scrollbar-track { background: rgba(255,255,255,0.05); border-radius: 2px; }
|
||||
.parts-list::-webkit-scrollbar-thumb { background: rgba(0, 212, 255, 0.3); border-radius: 2px; }
|
||||
|
||||
/* Package browser */
|
||||
.package-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.package-card {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.package-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
|
||||
.package-card h4 { color: var(--text); margin-bottom: 5px; }
|
||||
.package-card p { font-size: 0.8rem; color: var(--text-muted); }
|
||||
.package-card .meta { font-size: 0.7rem; color: var(--text-dim); margin-top: 10px; }
|
||||
32
exploded-diagrams/index.html
Normal file
32
exploded-diagrams/index.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Exploded Diagram Viewer</title>
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<header>
|
||||
<h1>EXPLODED DIAGRAMS</h1>
|
||||
<p class="subtitle">Modular Technical Illustrations</p>
|
||||
</header>
|
||||
|
||||
<nav id="breadcrumb"></nav>
|
||||
|
||||
<div class="controls" id="controls"></div>
|
||||
|
||||
<main>
|
||||
<div class="diagram-container">
|
||||
<svg id="diagram" viewBox="0 0 900 750"></svg>
|
||||
</div>
|
||||
<aside id="sidebar"></aside>
|
||||
</main>
|
||||
|
||||
<div id="tooltip" class="tooltip"></div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
334
exploded-diagrams/js/DiagramRenderer.js
Normal file
334
exploded-diagrams/js/DiagramRenderer.js
Normal file
@@ -0,0 +1,334 @@
|
||||
export class DiagramRenderer {
|
||||
constructor(svgElement, assemblyData, materials) {
|
||||
this.svg = svgElement;
|
||||
this.assembly = assemblyData;
|
||||
this.materials = materials;
|
||||
this.explosion = 50;
|
||||
this.rotation = 0;
|
||||
this.highlightedPart = null;
|
||||
this.animating = false;
|
||||
this.gradientIds = {};
|
||||
}
|
||||
|
||||
createDefs() {
|
||||
let defs = '<defs>';
|
||||
|
||||
// Create gradients from materials
|
||||
Object.entries(this.materials).forEach(([id, mat]) => {
|
||||
const gradId = `grad-${id}`;
|
||||
this.gradientIds[id] = gradId;
|
||||
const g = mat.gradient;
|
||||
const angle = g.angle || 0;
|
||||
const rad = angle * Math.PI / 180;
|
||||
const x2 = Math.round(50 + Math.cos(rad) * 50);
|
||||
const y2 = Math.round(50 + Math.sin(rad) * 50);
|
||||
|
||||
defs += `<linearGradient id="${gradId}" x1="0%" y1="0%" x2="${x2}%" y2="${y2}%">`;
|
||||
g.stops.forEach(s => {
|
||||
defs += `<stop offset="${s.offset}%" stop-color="${s.color}"/>`;
|
||||
});
|
||||
defs += '</linearGradient>';
|
||||
});
|
||||
|
||||
// Filters
|
||||
defs += `
|
||||
<filter id="dropShadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="2" dy="3" stdDeviation="3" flood-opacity="0.2"/>
|
||||
</filter>
|
||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="4" result="blur"/>
|
||||
<feFlood flood-color="#00d4ff" flood-opacity="0.6"/>
|
||||
<feComposite in2="blur" operator="in"/>
|
||||
<feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
`;
|
||||
|
||||
defs += '</defs>';
|
||||
return defs;
|
||||
}
|
||||
|
||||
getFill(geo, partMaterial) {
|
||||
if (geo.fill) return geo.fill;
|
||||
const mat = geo.material || partMaterial;
|
||||
return mat ? `url(#grad-${mat})` : '#888';
|
||||
}
|
||||
|
||||
renderGeometry(geo, cx, cy, partMaterial) {
|
||||
const ox = geo.offsetX || 0;
|
||||
const oy = geo.offsetY || 0;
|
||||
const x = cx + ox;
|
||||
const y = cy + oy;
|
||||
const fill = this.getFill(geo, partMaterial);
|
||||
const opacity = geo.opacity !== undefined ? `opacity="${geo.opacity}"` : '';
|
||||
|
||||
switch (geo.type) {
|
||||
case 'circle':
|
||||
return `<circle cx="${x}" cy="${y}" r="${geo.r}" fill="${fill}" ${opacity}/>`;
|
||||
|
||||
case 'ellipse':
|
||||
return `<ellipse cx="${x}" cy="${y}" rx="${geo.rx}" ry="${geo.ry}" fill="${fill}" ${opacity}/>`;
|
||||
|
||||
case 'rect': {
|
||||
const rx = geo.rx || 0;
|
||||
return `<rect x="${x - geo.width/2}" y="${y - geo.height/2}" width="${geo.width}" height="${geo.height}" rx="${rx}" fill="${fill}" ${opacity}/>`;
|
||||
}
|
||||
|
||||
case 'line':
|
||||
return `<line x1="${x + geo.x1}" y1="${y + geo.y1}" x2="${x + geo.x2}" y2="${y + geo.y2}" stroke="${geo.stroke}" stroke-width="${geo.strokeWidth}"/>`;
|
||||
|
||||
case 'polygon': {
|
||||
const pts = [];
|
||||
for (let i = 0; i < geo.points.length; i += 2) {
|
||||
pts.push(`${x + geo.points[i]},${y + geo.points[i+1]}`);
|
||||
}
|
||||
const strokeAttr = geo.stroke ? `stroke="${geo.stroke}" stroke-width="${geo.strokeWidth || 1}"` : '';
|
||||
const fillAttr = geo.fill === 'none' ? 'fill="none"' : `fill="${fill}"`;
|
||||
return `<polygon points="${pts.join(' ')}" ${fillAttr} ${strokeAttr}/>`;
|
||||
}
|
||||
|
||||
case 'cylinder': {
|
||||
const { rx, ry, height } = geo;
|
||||
const topY = y - height/2;
|
||||
const botY = y + height/2;
|
||||
return `
|
||||
<ellipse cx="${x}" cy="${topY}" rx="${rx}" ry="${ry}" fill="${fill}"/>
|
||||
<rect x="${x - rx}" y="${topY}" width="${rx * 2}" height="${height}" fill="${fill}"/>
|
||||
<ellipse cx="${x}" cy="${botY}" rx="${rx}" ry="${ry}" fill="${fill}"/>
|
||||
`;
|
||||
}
|
||||
|
||||
case 'cone': {
|
||||
const { topRx, topRy, bottomRx, bottomRy, height } = geo;
|
||||
const topY = y - height/2;
|
||||
const botY = y + height/2;
|
||||
return `
|
||||
<ellipse cx="${x}" cy="${topY}" rx="${topRx}" ry="${topRy}" fill="${fill}"/>
|
||||
<path d="M${x - topRx},${topY} L${x - bottomRx},${botY} L${x + bottomRx},${botY} L${x + topRx},${topY} Z" fill="${fill}"/>
|
||||
<ellipse cx="${x}" cy="${botY}" rx="${bottomRx}" ry="${bottomRy}" fill="${fill}"/>
|
||||
`;
|
||||
}
|
||||
|
||||
case 'coilSpring': {
|
||||
const { coils, rx, ry, pitch, strokeWidth } = geo;
|
||||
let svg = '';
|
||||
for (let i = 0; i < coils; i++) {
|
||||
const cy2 = y + i * pitch;
|
||||
svg += `<ellipse cx="${x}" cy="${cy2}" rx="${rx}" ry="${ry}" fill="none" stroke="${fill}" stroke-width="${strokeWidth}"/>`;
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
case 'gearRing': {
|
||||
const { teeth, outerRadius, toothHeight } = geo;
|
||||
let svg = '';
|
||||
for (let i = 0; i < teeth; i++) {
|
||||
const angle = (i / teeth) * Math.PI * 2;
|
||||
const tx = x + Math.cos(angle) * outerRadius;
|
||||
const ty = y + Math.sin(angle) * (outerRadius * 0.35);
|
||||
const rot = (i / teeth) * 360;
|
||||
svg += `<rect x="${tx - 3}" y="${ty - toothHeight/2}" width="6" height="${toothHeight}" fill="${fill}" transform="rotate(${rot}, ${tx}, ${ty})"/>`;
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
case 'radialRects': {
|
||||
const { count, radius, width, height, offsetY: oy2 = 0 } = geo;
|
||||
const rectFill = geo.material ? `url(#grad-${geo.material})` : (geo.fill || fill);
|
||||
let svg = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = (i / count) * Math.PI * 2;
|
||||
const rx2 = x + Math.cos(angle) * radius;
|
||||
svg += `<rect x="${rx2 - width/2}" y="${y + oy2}" width="${width}" height="${height}" fill="${rectFill}" rx="1"/>`;
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
case 'radialBlades': {
|
||||
const { count, radius, width, height, curve = 0, offsetY: oy2 = 0 } = geo;
|
||||
let svg = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = (i / count) * Math.PI * 2;
|
||||
const bx = x + Math.cos(angle) * radius;
|
||||
const rot = (i / count) * 360 + curve;
|
||||
svg += `<rect x="${bx - width/2}" y="${y + oy2}" width="${width}" height="${height}" fill="${fill}" rx="2" transform="rotate(${rot}, ${bx}, ${y + oy2 + height/2})"/>`;
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
case 'text':
|
||||
return `<text x="${x}" y="${y}" text-anchor="middle" fill="${geo.fill || '#333'}" font-size="${geo.fontSize || 10}" font-family="${geo.fontFamily || 'monospace'}">${geo.content}</text>`;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown geometry type: ${geo.type}`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { assembly } = this;
|
||||
const centerX = 350;
|
||||
const baseOffset = 90;
|
||||
const maxExplosion = 70;
|
||||
const explosionFactor = this.explosion / 100;
|
||||
|
||||
let svg = this.createDefs();
|
||||
|
||||
// Background
|
||||
svg += `<rect width="900" height="750" fill="#fafafa"/>`;
|
||||
|
||||
// Title
|
||||
svg += `
|
||||
<text x="350" y="35" text-anchor="middle" font-family="Arial Black" font-size="18" fill="#1a1a1a">
|
||||
${assembly.name.toUpperCase()}
|
||||
</text>
|
||||
<text x="350" y="52" text-anchor="middle" font-family="Arial" font-size="10" fill="#888">
|
||||
${assembly.description || ''}
|
||||
</text>
|
||||
<line x1="150" y1="65" x2="550" y2="65" stroke="#e0e0e0" stroke-width="1.5"/>
|
||||
`;
|
||||
|
||||
// Axis line
|
||||
svg += `<line x1="${centerX}" y1="80" x2="${centerX}" y2="720" stroke="#ddd" stroke-width="1" stroke-dasharray="6,3"/>`;
|
||||
|
||||
// Render parts
|
||||
assembly.parts.forEach((part, index) => {
|
||||
const explosionOffset = index * maxExplosion * explosionFactor;
|
||||
const baseY = baseOffset + part.baseY * 0.7 + explosionOffset;
|
||||
|
||||
const isHighlighted = this.highlightedPart === part.id;
|
||||
const filter = isHighlighted ? 'url(#glow)' : 'url(#dropShadow)';
|
||||
const transform = this.rotation !== 0 ? `transform="rotate(${this.rotation}, ${centerX}, ${baseY})"` : '';
|
||||
|
||||
svg += `<g class="part" data-part="${part.id}" filter="${filter}" ${transform} style="cursor:pointer">`;
|
||||
|
||||
part.geometry.forEach(geo => {
|
||||
svg += this.renderGeometry(geo, centerX, baseY, part.material);
|
||||
});
|
||||
|
||||
svg += '</g>';
|
||||
|
||||
// Leader line and label
|
||||
const side = index % 2 === 0 ? 1 : -1;
|
||||
const labelX = centerX + side * 160;
|
||||
const lineStartX = centerX + side * 50;
|
||||
|
||||
svg += `
|
||||
<line x1="${lineStartX}" y1="${baseY}" x2="${labelX - side * 8}" y2="${baseY}" stroke="#888" stroke-width="0.6"/>
|
||||
<circle cx="${labelX - side * 8}" cy="${baseY}" r="2.5" fill="#888"/>
|
||||
<text x="${labelX}" y="${baseY - 3}" text-anchor="${side === 1 ? 'start' : 'end'}" font-family="Arial" font-size="10" font-weight="bold" fill="#333">${part.name}</text>
|
||||
<text x="${labelX}" y="${baseY + 9}" text-anchor="${side === 1 ? 'start' : 'end'}" font-family="monospace" font-size="8" fill="#777">${part.partNumber}</text>
|
||||
`;
|
||||
});
|
||||
|
||||
// Footer
|
||||
const totalWeight = assembly.parts.reduce((sum, p) => sum + (p.weight * p.quantity), 0);
|
||||
svg += `
|
||||
<text x="20" y="735" font-family="Arial" font-size="8" fill="#aaa">
|
||||
${assembly.parts.length} unique parts • ${totalWeight.toFixed(1)}g total weight
|
||||
</text>
|
||||
`;
|
||||
|
||||
this.svg.innerHTML = svg;
|
||||
this.attachListeners();
|
||||
}
|
||||
|
||||
attachListeners() {
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
|
||||
this.svg.querySelectorAll('.part').forEach(el => {
|
||||
const partId = el.dataset.part;
|
||||
const part = this.assembly.parts.find(p => p.id === partId);
|
||||
|
||||
el.addEventListener('mouseenter', (e) => {
|
||||
this.highlightPart(partId);
|
||||
|
||||
if (part) {
|
||||
const mat = this.materials[part.material];
|
||||
tooltip.innerHTML = `
|
||||
<h4>${part.name}</h4>
|
||||
<div class="details">
|
||||
<div>${part.partNumber} × ${part.quantity}</div>
|
||||
<div>${part.weight}g each</div>
|
||||
<span class="material">${mat?.name || part.material}</span>
|
||||
</div>
|
||||
`;
|
||||
tooltip.classList.add('visible');
|
||||
}
|
||||
});
|
||||
|
||||
el.addEventListener('mousemove', (e) => {
|
||||
tooltip.style.left = (e.clientX + 15) + 'px';
|
||||
tooltip.style.top = (e.clientY - 10) + 'px';
|
||||
});
|
||||
|
||||
el.addEventListener('mouseleave', () => {
|
||||
this.highlightPart(null);
|
||||
tooltip.classList.remove('visible');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
highlightPart(partId) {
|
||||
this.highlightedPart = partId;
|
||||
this.render();
|
||||
|
||||
// Also highlight in parts list
|
||||
document.querySelectorAll('.part-item').forEach(el => {
|
||||
el.classList.toggle('highlighted', el.dataset.part === partId);
|
||||
});
|
||||
}
|
||||
|
||||
setExplosion(val) {
|
||||
this.explosion = val;
|
||||
this.render();
|
||||
}
|
||||
|
||||
setRotation(val) {
|
||||
this.rotation = val;
|
||||
this.render();
|
||||
}
|
||||
|
||||
animate() {
|
||||
if (this.animating) return;
|
||||
this.animating = true;
|
||||
|
||||
const start = this.explosion;
|
||||
const target = start < 50 ? 100 : 0;
|
||||
const duration = 1200;
|
||||
const startTime = performance.now();
|
||||
|
||||
const tick = (now) => {
|
||||
const elapsed = now - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
|
||||
this.explosion = start + (target - start) * eased;
|
||||
|
||||
// Update slider
|
||||
const slider = document.getElementById('explosionSlider');
|
||||
const label = document.getElementById('explosionValue');
|
||||
if (slider) slider.value = this.explosion;
|
||||
if (label) label.textContent = Math.round(this.explosion);
|
||||
|
||||
this.render();
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(tick);
|
||||
} else {
|
||||
this.animating = false;
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
exportSVG(filename = 'diagram') {
|
||||
const blob = new Blob([this.svg.outerHTML], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${filename}-exploded.svg`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
72
exploded-diagrams/js/PackageLoader.js
Normal file
72
exploded-diagrams/js/PackageLoader.js
Normal file
@@ -0,0 +1,72 @@
|
||||
export class PackageLoader {
|
||||
constructor(basePath = 'packages') {
|
||||
this.basePath = basePath;
|
||||
this.cache = new Map();
|
||||
this.materials = null;
|
||||
}
|
||||
|
||||
async fetchJSON(path) {
|
||||
if (this.cache.has(path)) {
|
||||
return this.cache.get(path);
|
||||
}
|
||||
|
||||
const response = await fetch(path);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load ${path}: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.cache.set(path, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
async getCategories() {
|
||||
const index = await this.fetchJSON(`${this.basePath}/index.json`);
|
||||
return index.categories;
|
||||
}
|
||||
|
||||
async getManufacturers(category) {
|
||||
const index = await this.fetchJSON(`${this.basePath}/${category}/index.json`);
|
||||
return index.manufacturers;
|
||||
}
|
||||
|
||||
async getProducts(category, manufacturer) {
|
||||
const index = await this.fetchJSON(`${this.basePath}/${category}/${manufacturer}/index.json`);
|
||||
return index.products;
|
||||
}
|
||||
|
||||
async loadProductPackage(category, manufacturer, product) {
|
||||
const pkgPath = `${this.basePath}/${category}/${manufacturer}/${product}`;
|
||||
return await this.fetchJSON(`${pkgPath}/package.json`);
|
||||
}
|
||||
|
||||
async loadAssembly(category, manufacturer, product, assembly) {
|
||||
const basePath = `${this.basePath}/${category}/${manufacturer}/${product}/${assembly}`;
|
||||
|
||||
// Load assembly manifest
|
||||
const manifest = await this.fetchJSON(`${basePath}/assembly.json`);
|
||||
|
||||
// Load shared materials if not already loaded
|
||||
if (!this.materials) {
|
||||
this.materials = await this.fetchJSON(`${this.basePath}/materials.json`);
|
||||
}
|
||||
|
||||
// Load each part
|
||||
const parts = await Promise.all(
|
||||
manifest.parts.map(async (partRef) => {
|
||||
const partData = await this.fetchJSON(`${basePath}/parts/${partRef}.json`);
|
||||
return partData;
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...manifest,
|
||||
parts
|
||||
};
|
||||
}
|
||||
|
||||
async loadPart(category, manufacturer, product, assembly, partId) {
|
||||
const path = `${this.basePath}/${category}/${manufacturer}/${product}/${assembly}/parts/${partId}.json`;
|
||||
return await this.fetchJSON(path);
|
||||
}
|
||||
}
|
||||
244
exploded-diagrams/js/UIController.js
Normal file
244
exploded-diagrams/js/UIController.js
Normal file
@@ -0,0 +1,244 @@
|
||||
export class UIController {
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
this.controlsEl = document.getElementById('controls');
|
||||
this.sidebarEl = document.getElementById('sidebar');
|
||||
this.breadcrumbEl = document.getElementById('breadcrumb');
|
||||
this.diagramContainer = document.querySelector('.diagram-container');
|
||||
|
||||
// Callbacks
|
||||
this.onExplosionChange = null;
|
||||
this.onRotationChange = null;
|
||||
this.onAnimate = null;
|
||||
this.onExport = null;
|
||||
this.onPartHover = null;
|
||||
}
|
||||
|
||||
updateBreadcrumb(path) {
|
||||
const labels = {
|
||||
'rc': 'RC Vehicles',
|
||||
'traxxas': 'Traxxas',
|
||||
'slash-4x4': 'Slash 4x4',
|
||||
'front-shock': 'Front Shock',
|
||||
'rear-differential': 'Rear Differential',
|
||||
'steering-servo': 'Steering Servo'
|
||||
};
|
||||
|
||||
let html = `<a href="#" data-path="">Home</a>`;
|
||||
|
||||
path.forEach((segment, i) => {
|
||||
const subPath = path.slice(0, i + 1).join('/');
|
||||
const label = labels[segment] || segment.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
|
||||
html += `<span>›</span>`;
|
||||
|
||||
if (i === path.length - 1) {
|
||||
html += `<span class="current">${label}</span>`;
|
||||
} else {
|
||||
html += `<a href="#${subPath}" data-path="${subPath}">${label}</a>`;
|
||||
}
|
||||
});
|
||||
|
||||
this.breadcrumbEl.innerHTML = html;
|
||||
|
||||
// Attach click handlers
|
||||
this.breadcrumbEl.querySelectorAll('a').forEach(a => {
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const newPath = a.dataset.path ? a.dataset.path.split('/') : [];
|
||||
this.app.navigate(newPath);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
showControls() {
|
||||
this.controlsEl.innerHTML = `
|
||||
<div class="control-group">
|
||||
<label>Explosion: <span id="explosionValue">50</span>%</label>
|
||||
<input type="range" id="explosionSlider" min="0" max="100" value="50">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>Rotation: <span id="rotationValue">0</span>°</label>
|
||||
<input type="range" id="rotationSlider" min="-25" max="25" value="0">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label> </label>
|
||||
<button id="animateBtn">▶ Explode</button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label> </label>
|
||||
<button id="exportBtn">⬇ Export SVG</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Wire up events
|
||||
document.getElementById('explosionSlider').addEventListener('input', (e) => {
|
||||
document.getElementById('explosionValue').textContent = e.target.value;
|
||||
if (this.onExplosionChange) this.onExplosionChange(parseInt(e.target.value));
|
||||
});
|
||||
|
||||
document.getElementById('rotationSlider').addEventListener('input', (e) => {
|
||||
document.getElementById('rotationValue').textContent = e.target.value;
|
||||
if (this.onRotationChange) this.onRotationChange(parseInt(e.target.value));
|
||||
});
|
||||
|
||||
document.getElementById('animateBtn').addEventListener('click', () => {
|
||||
if (this.onAnimate) this.onAnimate();
|
||||
});
|
||||
|
||||
document.getElementById('exportBtn').addEventListener('click', () => {
|
||||
if (this.onExport) this.onExport();
|
||||
});
|
||||
|
||||
this.controlsEl.style.display = 'flex';
|
||||
this.diagramContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
hideControls() {
|
||||
this.controlsEl.style.display = 'none';
|
||||
this.diagramContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
showSidebar(assembly) {
|
||||
const totalParts = assembly.parts.reduce((sum, p) => sum + p.quantity, 0);
|
||||
const totalWeight = assembly.parts.reduce((sum, p) => sum + (p.weight * p.quantity), 0);
|
||||
|
||||
// Get unique materials
|
||||
const materials = [...new Set(assembly.parts.map(p => p.material))];
|
||||
|
||||
this.sidebarEl.innerHTML = `
|
||||
<div class="panel">
|
||||
<h3>Assembly Stats</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat">
|
||||
<div class="stat-value">${assembly.parts.length}</div>
|
||||
<div class="stat-label">Unique Parts</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">${totalWeight < 1000 ? totalWeight.toFixed(0) + 'g' : (totalWeight/1000).toFixed(2) + 'kg'}</div>
|
||||
<div class="stat-label">Weight</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">${assembly.category || '-'}</div>
|
||||
<div class="stat-label">Category</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">${totalParts}</div>
|
||||
<div class="stat-label">Total Pieces</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>Parts List</h3>
|
||||
<div class="parts-list">
|
||||
${assembly.parts.map(p => `
|
||||
<div class="part-item" data-part="${p.id}">
|
||||
<span class="name">${p.name}</span>
|
||||
<span class="pn">${p.partNumber}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>Materials</h3>
|
||||
<div class="legend-grid" id="legend">
|
||||
${materials.map(m => `
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: ${this.getMaterialColor(m)}"></div>
|
||||
<span>${this.getMaterialName(m)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Attach hover events to parts list
|
||||
this.sidebarEl.querySelectorAll('.part-item').forEach(el => {
|
||||
el.addEventListener('mouseenter', () => {
|
||||
if (this.onPartHover) this.onPartHover(el.dataset.part);
|
||||
});
|
||||
el.addEventListener('mouseleave', () => {
|
||||
if (this.onPartHover) this.onPartHover(null);
|
||||
});
|
||||
});
|
||||
|
||||
this.sidebarEl.style.display = 'flex';
|
||||
}
|
||||
|
||||
getMaterialColor(materialId) {
|
||||
const colors = {
|
||||
aluminum: '#d0d0d0',
|
||||
steel: '#6a6a6a',
|
||||
blueAnodized: '#4a90d9',
|
||||
redAnodized: '#d94a4a',
|
||||
blackPlastic: '#2a2a2a',
|
||||
copper: '#b87333',
|
||||
titanium: '#8a8a8a',
|
||||
brass: '#d4a84a',
|
||||
spring: '#d94a4a',
|
||||
greenSpring: '#4ad94a'
|
||||
};
|
||||
return colors[materialId] || '#888';
|
||||
}
|
||||
|
||||
getMaterialName(materialId) {
|
||||
const names = {
|
||||
aluminum: 'Aluminum',
|
||||
steel: 'Steel',
|
||||
blueAnodized: 'Anodized (Blue)',
|
||||
redAnodized: 'Anodized (Red)',
|
||||
blackPlastic: 'Composite',
|
||||
copper: 'Copper',
|
||||
titanium: 'Titanium',
|
||||
brass: 'Brass',
|
||||
spring: 'Spring Steel',
|
||||
greenSpring: 'Spring (Soft)'
|
||||
};
|
||||
return names[materialId] || materialId;
|
||||
}
|
||||
|
||||
showPackageGrid(items, title, onClick) {
|
||||
this.sidebarEl.innerHTML = '';
|
||||
this.sidebarEl.style.display = 'none';
|
||||
|
||||
// Replace diagram with package browser
|
||||
this.diagramContainer.innerHTML = `
|
||||
<div style="padding: 30px;">
|
||||
<h2 style="color: #333; margin-bottom: 20px; font-weight: 400;">${title}</h2>
|
||||
<div class="package-grid">
|
||||
${items.map(item => `
|
||||
<div class="package-card" data-id="${item.id}">
|
||||
<h4>${item.name}</h4>
|
||||
<p>${item.description || ''}</p>
|
||||
${item.meta ? `<div class="meta">${item.meta}</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.diagramContainer.style.display = 'block';
|
||||
this.diagramContainer.style.background = '#fafafa';
|
||||
|
||||
// Attach click handlers
|
||||
this.diagramContainer.querySelectorAll('.package-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const item = items.find(i => i.id === card.dataset.id);
|
||||
if (item && onClick) onClick(item);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.diagramContainer.innerHTML = `
|
||||
<div style="padding: 50px; text-align: center;">
|
||||
<h3 style="color: #d94a4a; margin-bottom: 10px;">Error</h3>
|
||||
<p style="color: #666;">${message}</p>
|
||||
<button onclick="location.hash=''" style="margin-top: 20px;">Go Home</button>
|
||||
</div>
|
||||
`;
|
||||
this.diagramContainer.style.display = 'block';
|
||||
}
|
||||
}
|
||||
135
exploded-diagrams/js/main.js
Normal file
135
exploded-diagrams/js/main.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { PackageLoader } from './PackageLoader.js';
|
||||
import { DiagramRenderer } from './DiagramRenderer.js';
|
||||
import { UIController } from './UIController.js';
|
||||
|
||||
class App {
|
||||
constructor() {
|
||||
this.loader = new PackageLoader();
|
||||
this.renderer = null;
|
||||
this.ui = null;
|
||||
this.currentPath = [];
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.ui = new UIController(this);
|
||||
|
||||
// Parse URL hash for deep linking
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash) {
|
||||
this.currentPath = hash.split('/').filter(Boolean);
|
||||
}
|
||||
|
||||
await this.navigate(this.currentPath);
|
||||
|
||||
// Handle browser back/forward
|
||||
window.addEventListener('hashchange', () => {
|
||||
const newPath = window.location.hash.slice(1).split('/').filter(Boolean);
|
||||
this.navigate(newPath);
|
||||
});
|
||||
}
|
||||
|
||||
async navigate(path) {
|
||||
this.currentPath = path;
|
||||
window.location.hash = path.join('/');
|
||||
|
||||
if (path.length === 0) {
|
||||
// Show root - list categories
|
||||
await this.showCategories();
|
||||
} else if (path.length === 1) {
|
||||
// Show manufacturers in category
|
||||
await this.showManufacturers(path[0]);
|
||||
} else if (path.length === 2) {
|
||||
// Show products from manufacturer
|
||||
await this.showProducts(path[0], path[1]);
|
||||
} else if (path.length === 3) {
|
||||
// Show assemblies in product
|
||||
await this.showAssemblies(path[0], path[1], path[2]);
|
||||
} else if (path.length >= 4) {
|
||||
// Load and render assembly
|
||||
await this.loadAssembly(path[0], path[1], path[2], path[3]);
|
||||
}
|
||||
|
||||
this.ui.updateBreadcrumb(path);
|
||||
}
|
||||
|
||||
async showCategories() {
|
||||
const categories = await this.loader.getCategories();
|
||||
this.ui.showPackageGrid(categories, 'Categories', (cat) => {
|
||||
this.navigate([cat.id]);
|
||||
});
|
||||
this.ui.hideControls();
|
||||
}
|
||||
|
||||
async showManufacturers(category) {
|
||||
const manufacturers = await this.loader.getManufacturers(category);
|
||||
this.ui.showPackageGrid(manufacturers, 'Manufacturers', (mfr) => {
|
||||
this.navigate([category, mfr.id]);
|
||||
});
|
||||
this.ui.hideControls();
|
||||
}
|
||||
|
||||
async showProducts(category, manufacturer) {
|
||||
const products = await this.loader.getProducts(category, manufacturer);
|
||||
this.ui.showPackageGrid(products, 'Products', (prod) => {
|
||||
this.navigate([category, manufacturer, prod.id]);
|
||||
});
|
||||
this.ui.hideControls();
|
||||
}
|
||||
|
||||
async showAssemblies(category, manufacturer, product) {
|
||||
const pkg = await this.loader.loadProductPackage(category, manufacturer, product);
|
||||
this.ui.showPackageGrid(pkg.assemblies.map(a => ({
|
||||
id: a,
|
||||
name: a.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
||||
description: 'Assembly'
|
||||
})), 'Assemblies', (asm) => {
|
||||
this.navigate([category, manufacturer, product, asm.id]);
|
||||
});
|
||||
this.ui.hideControls();
|
||||
}
|
||||
|
||||
async loadAssembly(category, manufacturer, product, assembly) {
|
||||
try {
|
||||
const data = await this.loader.loadAssembly(category, manufacturer, product, assembly);
|
||||
|
||||
this.renderer = new DiagramRenderer(
|
||||
document.getElementById('diagram'),
|
||||
data,
|
||||
this.loader.materials
|
||||
);
|
||||
|
||||
this.ui.showControls();
|
||||
this.ui.showSidebar(data);
|
||||
this.renderer.render();
|
||||
|
||||
// Wire up UI events
|
||||
this.ui.onExplosionChange = (val) => {
|
||||
this.renderer.setExplosion(val);
|
||||
};
|
||||
|
||||
this.ui.onRotationChange = (val) => {
|
||||
this.renderer.setRotation(val);
|
||||
};
|
||||
|
||||
this.ui.onAnimate = () => {
|
||||
this.renderer.animate();
|
||||
};
|
||||
|
||||
this.ui.onExport = () => {
|
||||
this.renderer.exportSVG(assembly);
|
||||
};
|
||||
|
||||
this.ui.onPartHover = (partId) => {
|
||||
this.renderer.highlightPart(partId);
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to load assembly:', err);
|
||||
this.ui.showError(`Failed to load assembly: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Boot
|
||||
const app = new App();
|
||||
app.init().catch(console.error);
|
||||
11
exploded-diagrams/packages/index.json
Normal file
11
exploded-diagrams/packages/index.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"categories": [
|
||||
{
|
||||
"id": "rc",
|
||||
"name": "RC Vehicles",
|
||||
"description": "Radio controlled cars, trucks, and buggies",
|
||||
"meta": "3 manufacturers"
|
||||
}
|
||||
]
|
||||
}
|
||||
97
exploded-diagrams/packages/materials.json
Normal file
97
exploded-diagrams/packages/materials.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"aluminum": {
|
||||
"name": "Aluminum",
|
||||
"gradient": {
|
||||
"angle": 135,
|
||||
"stops": [
|
||||
{ "offset": 0, "color": "#e8e8e8" },
|
||||
{ "offset": 25, "color": "#d0d0d0" },
|
||||
{ "offset": 50, "color": "#f5f5f5" },
|
||||
{ "offset": 75, "color": "#c8c8c8" },
|
||||
{ "offset": 100, "color": "#a8a8a8" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"steel": {
|
||||
"name": "Hardened Steel",
|
||||
"gradient": {
|
||||
"angle": 135,
|
||||
"stops": [
|
||||
{ "offset": 0, "color": "#6a6a6a" },
|
||||
{ "offset": 30, "color": "#9a9a9a" },
|
||||
{ "offset": 50, "color": "#5a5a5a" },
|
||||
{ "offset": 100, "color": "#4a4a4a" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"blueAnodized": {
|
||||
"name": "Anodized Aluminum (Blue)",
|
||||
"gradient": {
|
||||
"angle": 135,
|
||||
"stops": [
|
||||
{ "offset": 0, "color": "#4a90d9" },
|
||||
{ "offset": 30, "color": "#6ab0f9" },
|
||||
{ "offset": 60, "color": "#3a70b9" },
|
||||
{ "offset": 100, "color": "#2a5090" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"redAnodized": {
|
||||
"name": "Anodized Aluminum (Red)",
|
||||
"gradient": {
|
||||
"angle": 135,
|
||||
"stops": [
|
||||
{ "offset": 0, "color": "#d94a4a" },
|
||||
{ "offset": 30, "color": "#f96a6a" },
|
||||
{ "offset": 60, "color": "#b93a3a" },
|
||||
{ "offset": 100, "color": "#902a2a" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"blackPlastic": {
|
||||
"name": "Composite/Nylon",
|
||||
"gradient": {
|
||||
"angle": 135,
|
||||
"stops": [
|
||||
{ "offset": 0, "color": "#3a3a3a" },
|
||||
{ "offset": 40, "color": "#2a2a2a" },
|
||||
{ "offset": 100, "color": "#1a1a1a" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"copper": {
|
||||
"name": "Copper",
|
||||
"gradient": {
|
||||
"angle": 135,
|
||||
"stops": [
|
||||
{ "offset": 0, "color": "#b87333" },
|
||||
{ "offset": 30, "color": "#da9a62" },
|
||||
{ "offset": 60, "color": "#a05a23" },
|
||||
{ "offset": 100, "color": "#804a13" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"brass": {
|
||||
"name": "Brass",
|
||||
"gradient": {
|
||||
"angle": 135,
|
||||
"stops": [
|
||||
{ "offset": 0, "color": "#d4a84a" },
|
||||
{ "offset": 30, "color": "#e8c878" },
|
||||
{ "offset": 60, "color": "#b8923a" },
|
||||
{ "offset": 100, "color": "#987020" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"spring": {
|
||||
"name": "Spring Steel (Red)",
|
||||
"gradient": {
|
||||
"angle": 0,
|
||||
"stops": [
|
||||
{ "offset": 0, "color": "#d94a4a" },
|
||||
{ "offset": 50, "color": "#ff6a6a" },
|
||||
{ "offset": 100, "color": "#b93a3a" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
11
exploded-diagrams/packages/rc/index.json
Normal file
11
exploded-diagrams/packages/rc/index.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"category": "rc",
|
||||
"manufacturers": [
|
||||
{
|
||||
"id": "traxxas",
|
||||
"name": "Traxxas",
|
||||
"description": "The Fastest Name in Radio Control",
|
||||
"meta": "USA • Founded 1986"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
exploded-diagrams/packages/rc/traxxas/index.json
Normal file
11
exploded-diagrams/packages/rc/traxxas/index.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"manufacturer": "traxxas",
|
||||
"products": [
|
||||
{
|
||||
"id": "slash-4x4",
|
||||
"name": "Slash 4x4",
|
||||
"description": "1/10 Scale 4WD Short Course Truck",
|
||||
"meta": "Model #68086-4"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "front-shock",
|
||||
"name": "Front Shock Assembly",
|
||||
"description": "GTR Long Shock - Oil-filled coilover with adjustable preload",
|
||||
"category": "Suspension",
|
||||
"version": "1.0.0",
|
||||
"parts": [
|
||||
"shock-cap",
|
||||
"shaft",
|
||||
"piston",
|
||||
"spring",
|
||||
"retainer",
|
||||
"body",
|
||||
"bushing",
|
||||
"mount"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"id": "body",
|
||||
"name": "Shock Body",
|
||||
"partNumber": "TRA-5461",
|
||||
"material": "aluminum",
|
||||
"quantity": 1,
|
||||
"weight": 45.0,
|
||||
"weightUnit": "g",
|
||||
"baseY": 465,
|
||||
"description": "6061-T6 aluminum shock body with reservoir",
|
||||
"geometry": [
|
||||
{
|
||||
"type": "cylinder",
|
||||
"rx": 30,
|
||||
"ry": 10,
|
||||
"height": 100
|
||||
},
|
||||
{
|
||||
"type": "cylinder",
|
||||
"rx": 8,
|
||||
"ry": 3,
|
||||
"height": 50,
|
||||
"offsetX": 36,
|
||||
"offsetY": -35
|
||||
},
|
||||
{
|
||||
"type": "line",
|
||||
"x1": -28,
|
||||
"y1": -25,
|
||||
"x2": 26,
|
||||
"y2": -25,
|
||||
"stroke": "#bbb",
|
||||
"strokeWidth": 0.5
|
||||
},
|
||||
{
|
||||
"type": "line",
|
||||
"x1": -28,
|
||||
"y1": 0,
|
||||
"x2": 26,
|
||||
"y2": 0,
|
||||
"stroke": "#bbb",
|
||||
"strokeWidth": 0.5
|
||||
},
|
||||
{
|
||||
"type": "line",
|
||||
"x1": -28,
|
||||
"y1": 25,
|
||||
"x2": 26,
|
||||
"y2": 25,
|
||||
"stroke": "#bbb",
|
||||
"strokeWidth": 0.5
|
||||
},
|
||||
{
|
||||
"type": "circle",
|
||||
"r": 4,
|
||||
"offsetX": 36,
|
||||
"offsetY": -15,
|
||||
"fill": "#888"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "bushing",
|
||||
"name": "Lower Bushing",
|
||||
"partNumber": "TRA-5468",
|
||||
"material": "blackPlastic",
|
||||
"quantity": 2,
|
||||
"weight": 1.8,
|
||||
"weightUnit": "g",
|
||||
"baseY": 600,
|
||||
"description": "Delrin shock bushing",
|
||||
"geometry": [
|
||||
{
|
||||
"type": "cylinder",
|
||||
"rx": 20,
|
||||
"ry": 7,
|
||||
"height": 20
|
||||
},
|
||||
{
|
||||
"type": "ellipse",
|
||||
"rx": 8,
|
||||
"ry": 3,
|
||||
"fill": "#0a0a0a"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"id": "mount",
|
||||
"name": "Shock Mount",
|
||||
"partNumber": "TRA-5460",
|
||||
"material": "aluminum",
|
||||
"quantity": 1,
|
||||
"weight": 15.2,
|
||||
"weightUnit": "g",
|
||||
"baseY": 660,
|
||||
"description": "Aluminum lower shock mount bracket",
|
||||
"geometry": [
|
||||
{
|
||||
"type": "polygon",
|
||||
"points": [-35, 20, -20, -10, 20, -10, 35, 20]
|
||||
},
|
||||
{
|
||||
"type": "circle",
|
||||
"r": 8,
|
||||
"offsetY": 2,
|
||||
"fill": "#888"
|
||||
},
|
||||
{
|
||||
"type": "circle",
|
||||
"r": 5,
|
||||
"offsetY": 2,
|
||||
"fill": "#555"
|
||||
},
|
||||
{
|
||||
"type": "circle",
|
||||
"r": 4,
|
||||
"offsetX": -22,
|
||||
"offsetY": 15,
|
||||
"fill": "#666"
|
||||
},
|
||||
{
|
||||
"type": "circle",
|
||||
"r": 4,
|
||||
"offsetX": 22,
|
||||
"offsetY": 15,
|
||||
"fill": "#666"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"id": "piston",
|
||||
"name": "Piston",
|
||||
"partNumber": "TRA-5467",
|
||||
"material": "blackPlastic",
|
||||
"quantity": 1,
|
||||
"weight": 3.1,
|
||||
"weightUnit": "g",
|
||||
"baseY": 195,
|
||||
"description": "PTFE composite piston with 4-hole pattern",
|
||||
"geometry": [
|
||||
{
|
||||
"type": "cylinder",
|
||||
"rx": 22,
|
||||
"ry": 7,
|
||||
"height": 14
|
||||
},
|
||||
{
|
||||
"type": "circle",
|
||||
"r": 3,
|
||||
"offsetX": -10,
|
||||
"fill": "#0a0a0a"
|
||||
},
|
||||
{
|
||||
"type": "circle",
|
||||
"r": 3,
|
||||
"offsetX": 10,
|
||||
"fill": "#0a0a0a"
|
||||
},
|
||||
{
|
||||
"type": "circle",
|
||||
"r": 3,
|
||||
"offsetY": -4,
|
||||
"fill": "#0a0a0a"
|
||||
},
|
||||
{
|
||||
"type": "circle",
|
||||
"r": 3,
|
||||
"offsetY": 4,
|
||||
"fill": "#0a0a0a"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "retainer",
|
||||
"name": "Spring Retainer",
|
||||
"partNumber": "TRA-5465",
|
||||
"material": "blueAnodized",
|
||||
"quantity": 1,
|
||||
"weight": 5.4,
|
||||
"weightUnit": "g",
|
||||
"baseY": 410,
|
||||
"description": "Aluminum spring retainer / preload adjuster",
|
||||
"geometry": [
|
||||
{
|
||||
"type": "cylinder",
|
||||
"rx": 32,
|
||||
"ry": 10,
|
||||
"height": 14
|
||||
},
|
||||
{
|
||||
"type": "ellipse",
|
||||
"rx": 12,
|
||||
"ry": 4,
|
||||
"fill": "#1a3a5a"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "shaft",
|
||||
"name": "Shock Shaft",
|
||||
"partNumber": "TRA-5464",
|
||||
"material": "steel",
|
||||
"quantity": 1,
|
||||
"weight": 12.5,
|
||||
"weightUnit": "g",
|
||||
"baseY": 55,
|
||||
"description": "Hardened steel shock shaft - TiN coated",
|
||||
"geometry": [
|
||||
{
|
||||
"type": "rect",
|
||||
"width": 10,
|
||||
"height": 120,
|
||||
"rx": 2
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"width": 3,
|
||||
"height": 120,
|
||||
"offsetX": -3,
|
||||
"fill": "#aaa",
|
||||
"opacity": 0.3
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "shock-cap",
|
||||
"name": "Shock Cap",
|
||||
"partNumber": "TRA-5466",
|
||||
"material": "blueAnodized",
|
||||
"quantity": 1,
|
||||
"weight": 8.2,
|
||||
"weightUnit": "g",
|
||||
"baseY": 0,
|
||||
"description": "Aluminum shock cap with hex adjustment and bleed hole",
|
||||
"geometry": [
|
||||
{
|
||||
"type": "cylinder",
|
||||
"rx": 28,
|
||||
"ry": 10,
|
||||
"height": 30
|
||||
},
|
||||
{
|
||||
"type": "polygon",
|
||||
"points": [0, -12, 12, -6, 12, 6, 0, 12, -12, 6, -12, -6],
|
||||
"offsetY": 8,
|
||||
"stroke": "#2a5090",
|
||||
"strokeWidth": 1.5,
|
||||
"fill": "none"
|
||||
},
|
||||
{
|
||||
"type": "circle",
|
||||
"r": 5,
|
||||
"fill": "#1a3a5a"
|
||||
},
|
||||
{
|
||||
"type": "ellipse",
|
||||
"rx": 6,
|
||||
"ry": 3,
|
||||
"offsetX": 8,
|
||||
"offsetY": -10,
|
||||
"fill": "#7ac0f9",
|
||||
"opacity": 0.4
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "spring",
|
||||
"name": "Shock Spring",
|
||||
"partNumber": "TRA-5442R",
|
||||
"material": "spring",
|
||||
"quantity": 1,
|
||||
"weight": 22.0,
|
||||
"weightUnit": "g",
|
||||
"baseY": 250,
|
||||
"description": "Progressive rate spring - Red (Firm)",
|
||||
"geometry": [
|
||||
{
|
||||
"type": "coilSpring",
|
||||
"coils": 8,
|
||||
"rx": 26,
|
||||
"ry": 8,
|
||||
"pitch": 18,
|
||||
"strokeWidth": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
14
exploded-diagrams/packages/rc/traxxas/slash-4x4/package.json
Normal file
14
exploded-diagrams/packages/rc/traxxas/slash-4x4/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"product": "slash-4x4",
|
||||
"name": "Traxxas Slash 4x4",
|
||||
"description": "1/10 Scale 4WD Electric Short Course Truck",
|
||||
"model": "68086-4",
|
||||
"manufacturer": "Traxxas",
|
||||
"category": "Short Course Truck",
|
||||
"scale": "1:10",
|
||||
"assemblies": [
|
||||
"front-shock",
|
||||
"rear-differential",
|
||||
"steering-servo"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "rear-differential",
|
||||
"name": "Rear Differential Assembly",
|
||||
"description": "Sealed gear differential with metal internals",
|
||||
"category": "Drivetrain",
|
||||
"version": "1.0.0",
|
||||
"parts": [
|
||||
"case-top",
|
||||
"ring-gear",
|
||||
"spider-gears",
|
||||
"outdrives",
|
||||
"bearings",
|
||||
"case-bottom"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "bearings",
|
||||
"name": "Diff Bearings",
|
||||
"partNumber": "TRA-5118",
|
||||
"material": "steel",
|
||||
"quantity": 2,
|
||||
"weight": 4.5,
|
||||
"weightUnit": "g",
|
||||
"baseY": 350,
|
||||
"description": "10x15x4mm rubber sealed ball bearings",
|
||||
"geometry": [
|
||||
{ "type": "cylinder", "rx": 15, "ry": 5, "height": 10, "offsetX": -45 },
|
||||
{ "type": "ellipse", "rx": 8, "ry": 3, "offsetX": -45, "fill": "#4a4a4a" },
|
||||
{ "type": "cylinder", "rx": 15, "ry": 5, "height": 10, "offsetX": 45 },
|
||||
{ "type": "ellipse", "rx": 8, "ry": 3, "offsetX": 45, "fill": "#4a4a4a" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "case-bottom",
|
||||
"name": "Diff Case (Bottom)",
|
||||
"partNumber": "TRA-5381B",
|
||||
"material": "blackPlastic",
|
||||
"quantity": 1,
|
||||
"weight": 32.0,
|
||||
"weightUnit": "g",
|
||||
"baseY": 430,
|
||||
"description": "Glass-filled nylon differential case half with output shaft bore",
|
||||
"geometry": [
|
||||
{ "type": "cylinder", "rx": 55, "ry": 20, "height": 25 },
|
||||
{ "type": "ellipse", "rx": 25, "ry": 9, "fill": "#1a1a1a" },
|
||||
{ "type": "circle", "r": 5, "offsetX": -35, "fill": "#2a2a2a" },
|
||||
{ "type": "circle", "r": 5, "offsetX": 35, "fill": "#2a2a2a" },
|
||||
{ "type": "circle", "r": 8, "offsetY": 22, "fill": "#2a2a2a" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "case-top",
|
||||
"name": "Diff Case (Top)",
|
||||
"partNumber": "TRA-5381",
|
||||
"material": "blackPlastic",
|
||||
"quantity": 1,
|
||||
"weight": 28.5,
|
||||
"weightUnit": "g",
|
||||
"baseY": 0,
|
||||
"description": "Glass-filled nylon differential case half",
|
||||
"geometry": [
|
||||
{ "type": "cylinder", "rx": 55, "ry": 20, "height": 15 },
|
||||
{ "type": "ellipse", "rx": 25, "ry": 9, "fill": "#1a1a1a" },
|
||||
{ "type": "circle", "r": 5, "offsetX": -35, "fill": "#2a2a2a" },
|
||||
{ "type": "circle", "r": 5, "offsetX": 35, "fill": "#2a2a2a" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "outdrives",
|
||||
"name": "Outdrives",
|
||||
"partNumber": "TRA-5380",
|
||||
"material": "steel",
|
||||
"quantity": 2,
|
||||
"weight": 18.0,
|
||||
"weightUnit": "g",
|
||||
"baseY": 270,
|
||||
"description": "Hardened steel differential outdrives",
|
||||
"geometry": [
|
||||
{ "type": "rect", "width": 35, "height": 12, "offsetX": -32 },
|
||||
{ "type": "ellipse", "rx": 6, "ry": 11, "offsetX": -15 },
|
||||
{ "type": "rect", "width": 15, "height": 8, "offsetX": -57 },
|
||||
{ "type": "rect", "width": 35, "height": 12, "offsetX": 32 },
|
||||
{ "type": "ellipse", "rx": 6, "ry": 11, "offsetX": 15 },
|
||||
{ "type": "rect", "width": 15, "height": 8, "offsetX": 57 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "ring-gear",
|
||||
"name": "Ring Gear",
|
||||
"partNumber": "TRA-5379",
|
||||
"material": "steel",
|
||||
"quantity": 1,
|
||||
"weight": 52.0,
|
||||
"weightUnit": "g",
|
||||
"baseY": 80,
|
||||
"description": "Hardened steel ring gear - 43T",
|
||||
"geometry": [
|
||||
{ "type": "gearRing", "teeth": 24, "outerRadius": 48, "toothHeight": 8 },
|
||||
{ "type": "cylinder", "rx": 45, "ry": 16, "height": 30 },
|
||||
{ "type": "ellipse", "rx": 25, "ry": 9, "fill": "#4a4a4a" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "spider-gears",
|
||||
"name": "Spider Gears",
|
||||
"partNumber": "TRA-5382",
|
||||
"material": "steel",
|
||||
"quantity": 4,
|
||||
"weight": 8.0,
|
||||
"weightUnit": "g",
|
||||
"baseY": 175,
|
||||
"description": "Hardened steel spider gear set with cross pin",
|
||||
"geometry": [
|
||||
{ "type": "cylinder", "rx": 18, "ry": 6, "height": 15, "offsetX": -30 },
|
||||
{ "type": "circle", "r": 5, "offsetX": -30, "fill": "#3a3a3a" },
|
||||
{ "type": "cylinder", "rx": 18, "ry": 6, "height": 15, "offsetX": 30 },
|
||||
{ "type": "circle", "r": 5, "offsetX": 30, "fill": "#3a3a3a" },
|
||||
{ "type": "rect", "width": 16, "height": 25, "rx": 2, "offsetY": -5 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "steering-servo",
|
||||
"name": "Steering Servo Assembly",
|
||||
"description": "High-torque digital servo with metal gear train",
|
||||
"category": "Electronics",
|
||||
"version": "1.0.0",
|
||||
"parts": [
|
||||
"servo-horn",
|
||||
"horn-screw",
|
||||
"output-gear",
|
||||
"servo-body",
|
||||
"servo-mount"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "horn-screw",
|
||||
"name": "Horn Screw",
|
||||
"partNumber": "TRA-2548",
|
||||
"material": "steel",
|
||||
"quantity": 1,
|
||||
"weight": 0.8,
|
||||
"weightUnit": "g",
|
||||
"baseY": 50,
|
||||
"description": "M3 servo horn retaining screw",
|
||||
"geometry": [
|
||||
{ "type": "circle", "r": 8, "offsetY": -8 },
|
||||
{ "type": "rect", "width": 6, "height": 25 },
|
||||
{ "type": "line", "x1": -4, "y1": -8, "x2": 4, "y2": -8, "stroke": "#444", "strokeWidth": 1.5 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "output-gear",
|
||||
"name": "Output Gear",
|
||||
"partNumber": "TRA-2074",
|
||||
"material": "brass",
|
||||
"quantity": 1,
|
||||
"weight": 6.2,
|
||||
"weightUnit": "g",
|
||||
"baseY": 105,
|
||||
"description": "Brass output gear with 25T spline",
|
||||
"geometry": [
|
||||
{ "type": "gearRing", "teeth": 20, "outerRadius": 18, "toothHeight": 4 },
|
||||
{ "type": "cylinder", "rx": 16, "ry": 5, "height": 15 },
|
||||
{ "type": "ellipse", "rx": 6, "ry": 2, "fill": "#806020" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "servo-body",
|
||||
"name": "Servo Body",
|
||||
"partNumber": "TRA-2075X",
|
||||
"material": "blackPlastic",
|
||||
"quantity": 1,
|
||||
"weight": 55.0,
|
||||
"weightUnit": "g",
|
||||
"baseY": 165,
|
||||
"description": "Digital high-torque servo - 255oz-in",
|
||||
"geometry": [
|
||||
{ "type": "rect", "width": 60, "height": 50, "rx": 3 },
|
||||
{ "type": "rect", "width": 80, "height": 8, "offsetY": -10 },
|
||||
{ "type": "circle", "r": 3, "offsetX": -35, "offsetY": -10, "fill": "#1a1a1a" },
|
||||
{ "type": "circle", "r": 3, "offsetX": 35, "offsetY": -10, "fill": "#1a1a1a" },
|
||||
{ "type": "rect", "width": 40, "height": 12, "offsetY": -18, "fill": "#2a2a2a" },
|
||||
{ "type": "text", "content": "2075X", "offsetY": -15, "fill": "#00d4ff", "fontSize": 7 },
|
||||
{ "type": "circle", "r": 8, "offsetY": -30, "material": "steel" },
|
||||
{ "type": "rect", "width": 12, "height": 20, "offsetX": 21, "offsetY": 10, "fill": "#333" },
|
||||
{ "type": "rect", "width": 3, "height": 14, "offsetX": 17, "offsetY": 10, "fill": "#d94a4a" },
|
||||
{ "type": "rect", "width": 3, "height": 14, "offsetX": 25, "offsetY": 10, "fill": "#d4a84a" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "servo-horn",
|
||||
"name": "Servo Horn",
|
||||
"partNumber": "TRA-2072",
|
||||
"material": "blueAnodized",
|
||||
"quantity": 1,
|
||||
"weight": 4.5,
|
||||
"weightUnit": "g",
|
||||
"baseY": 0,
|
||||
"description": "25T spline aluminum servo horn",
|
||||
"geometry": [
|
||||
{ "type": "ellipse", "rx": 12, "ry": 8 },
|
||||
{ "type": "rect", "width": 100, "height": 10, "rx": 5, "offsetY": -4 },
|
||||
{ "type": "circle", "r": 4, "offsetX": -40, "fill": "#1a3a5a" },
|
||||
{ "type": "circle", "r": 4, "offsetX": 40, "fill": "#1a3a5a" },
|
||||
{ "type": "circle", "r": 6, "fill": "#1a3a5a" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "servo-mount",
|
||||
"name": "Servo Mount",
|
||||
"partNumber": "TRA-2073",
|
||||
"material": "aluminum",
|
||||
"quantity": 1,
|
||||
"weight": 18.0,
|
||||
"weightUnit": "g",
|
||||
"baseY": 280,
|
||||
"description": "Aluminum servo mounting bracket",
|
||||
"geometry": [
|
||||
{ "type": "rect", "width": 100, "height": 25, "rx": 3 },
|
||||
{ "type": "rect", "width": 70, "height": 55, "rx": 2, "offsetY": 15 },
|
||||
{ "type": "rect", "width": 56, "height": 35, "offsetY": 20, "fill": "#aaa" },
|
||||
{ "type": "circle", "r": 4, "offsetX": -42, "fill": "#888" },
|
||||
{ "type": "circle", "r": 4, "offsetX": 42, "fill": "#888" },
|
||||
{ "type": "circle", "r": 4, "offsetX": -42, "offsetY": 38, "fill": "#888" },
|
||||
{ "type": "circle", "r": 4, "offsetX": 42, "offsetY": 38, "fill": "#888" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user