diff --git a/exploded-diagrams/css/main.css b/exploded-diagrams/css/main.css
new file mode 100644
index 000000000..970072f26
--- /dev/null
+++ b/exploded-diagrams/css/main.css
@@ -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; }
diff --git a/exploded-diagrams/index.html b/exploded-diagrams/index.html
new file mode 100644
index 000000000..a1035fbc8
--- /dev/null
+++ b/exploded-diagrams/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+ Exploded Diagram Viewer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/exploded-diagrams/js/DiagramRenderer.js b/exploded-diagrams/js/DiagramRenderer.js
new file mode 100644
index 000000000..67e9ba40b
--- /dev/null
+++ b/exploded-diagrams/js/DiagramRenderer.js
@@ -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 = '';
+
+ // 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 += ``;
+ g.stops.forEach(s => {
+ defs += ``;
+ });
+ defs += '';
+ });
+
+ // Filters
+ 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 ``;
+
+ case 'ellipse':
+ return ``;
+
+ case 'rect': {
+ const rx = geo.rx || 0;
+ return ``;
+ }
+
+ case 'line':
+ return ``;
+
+ 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 ``;
+ }
+
+ case 'cylinder': {
+ const { rx, ry, height } = geo;
+ const topY = y - height/2;
+ const botY = y + height/2;
+ return `
+
+
+
+ `;
+ }
+
+ case 'cone': {
+ const { topRx, topRy, bottomRx, bottomRy, height } = geo;
+ const topY = y - height/2;
+ const botY = y + height/2;
+ return `
+
+
+
+ `;
+ }
+
+ case 'coilSpring': {
+ const { coils, rx, ry, pitch, strokeWidth } = geo;
+ let svg = '';
+ for (let i = 0; i < coils; i++) {
+ const cy2 = y + i * pitch;
+ svg += ``;
+ }
+ 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 += ``;
+ }
+ 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 += ``;
+ }
+ 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 += ``;
+ }
+ return svg;
+ }
+
+ case 'text':
+ return `${geo.content}`;
+
+ 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 += ``;
+
+ // Title
+ svg += `
+
+ ${assembly.name.toUpperCase()}
+
+
+ ${assembly.description || ''}
+
+
+ `;
+
+ // Axis line
+ svg += ``;
+
+ // 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 += ``;
+
+ part.geometry.forEach(geo => {
+ svg += this.renderGeometry(geo, centerX, baseY, part.material);
+ });
+
+ svg += '';
+
+ // Leader line and label
+ const side = index % 2 === 0 ? 1 : -1;
+ const labelX = centerX + side * 160;
+ const lineStartX = centerX + side * 50;
+
+ svg += `
+
+
+ ${part.name}
+ ${part.partNumber}
+ `;
+ });
+
+ // Footer
+ const totalWeight = assembly.parts.reduce((sum, p) => sum + (p.weight * p.quantity), 0);
+ svg += `
+
+ ${assembly.parts.length} unique parts • ${totalWeight.toFixed(1)}g total weight
+
+ `;
+
+ 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 = `
+ ${part.name}
+
+
${part.partNumber} × ${part.quantity}
+
${part.weight}g each
+
${mat?.name || part.material}
+
+ `;
+ 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);
+ }
+}
diff --git a/exploded-diagrams/js/PackageLoader.js b/exploded-diagrams/js/PackageLoader.js
new file mode 100644
index 000000000..f0fe8e6be
--- /dev/null
+++ b/exploded-diagrams/js/PackageLoader.js
@@ -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);
+ }
+}
diff --git a/exploded-diagrams/js/UIController.js b/exploded-diagrams/js/UIController.js
new file mode 100644
index 000000000..82f560dd0
--- /dev/null
+++ b/exploded-diagrams/js/UIController.js
@@ -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 = `Home`;
+
+ 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 += `›`;
+
+ if (i === path.length - 1) {
+ html += `${label}`;
+ } else {
+ html += `${label}`;
+ }
+ });
+
+ 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 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // 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 = `
+
+
Assembly Stats
+
+
+
${assembly.parts.length}
+
Unique Parts
+
+
+
${totalWeight < 1000 ? totalWeight.toFixed(0) + 'g' : (totalWeight/1000).toFixed(2) + 'kg'}
+
Weight
+
+
+
${assembly.category || '-'}
+
Category
+
+
+
${totalParts}
+
Total Pieces
+
+
+
+
+
+
Parts List
+
+ ${assembly.parts.map(p => `
+
+ ${p.name}
+ ${p.partNumber}
+
+ `).join('')}
+
+
+
+
+
Materials
+
+ ${materials.map(m => `
+
+
+
${this.getMaterialName(m)}
+
+ `).join('')}
+
+
+ `;
+
+ // 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 = `
+
+
${title}
+
+ ${items.map(item => `
+
+
${item.name}
+
${item.description || ''}
+ ${item.meta ? `
${item.meta}
` : ''}
+
+ `).join('')}
+
+
+ `;
+
+ 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 = `
+
+
Error
+
${message}
+
+
+ `;
+ this.diagramContainer.style.display = 'block';
+ }
+}
diff --git a/exploded-diagrams/js/main.js b/exploded-diagrams/js/main.js
new file mode 100644
index 000000000..f62be9c29
--- /dev/null
+++ b/exploded-diagrams/js/main.js
@@ -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);
diff --git a/exploded-diagrams/packages/index.json b/exploded-diagrams/packages/index.json
new file mode 100644
index 000000000..739e95ab1
--- /dev/null
+++ b/exploded-diagrams/packages/index.json
@@ -0,0 +1,11 @@
+{
+ "version": "1.0.0",
+ "categories": [
+ {
+ "id": "rc",
+ "name": "RC Vehicles",
+ "description": "Radio controlled cars, trucks, and buggies",
+ "meta": "3 manufacturers"
+ }
+ ]
+}
diff --git a/exploded-diagrams/packages/materials.json b/exploded-diagrams/packages/materials.json
new file mode 100644
index 000000000..155b00bbd
--- /dev/null
+++ b/exploded-diagrams/packages/materials.json
@@ -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" }
+ ]
+ }
+ }
+}
diff --git a/exploded-diagrams/packages/rc/index.json b/exploded-diagrams/packages/rc/index.json
new file mode 100644
index 000000000..59593eaec
--- /dev/null
+++ b/exploded-diagrams/packages/rc/index.json
@@ -0,0 +1,11 @@
+{
+ "category": "rc",
+ "manufacturers": [
+ {
+ "id": "traxxas",
+ "name": "Traxxas",
+ "description": "The Fastest Name in Radio Control",
+ "meta": "USA • Founded 1986"
+ }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/index.json b/exploded-diagrams/packages/rc/traxxas/index.json
new file mode 100644
index 000000000..d1fc2f44d
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/index.json
@@ -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"
+ }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/assembly.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/assembly.json
new file mode 100644
index 000000000..102b934ed
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/assembly.json
@@ -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"
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/body.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/body.json
new file mode 100644
index 000000000..6250b1214
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/body.json
@@ -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"
+ }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/bushing.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/bushing.json
new file mode 100644
index 000000000..1885e9f98
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/bushing.json
@@ -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"
+ }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/mount.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/mount.json
new file mode 100644
index 000000000..493374ac6
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/mount.json
@@ -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"
+ }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/piston.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/piston.json
new file mode 100644
index 000000000..b38abf202
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/piston.json
@@ -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"
+ }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/retainer.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/retainer.json
new file mode 100644
index 000000000..f3305ae38
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/retainer.json
@@ -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"
+ }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/shaft.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/shaft.json
new file mode 100644
index 000000000..54f1ee5d2
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/shaft.json
@@ -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
+ }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/shock-cap.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/shock-cap.json
new file mode 100644
index 000000000..6c8505628
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/shock-cap.json
@@ -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
+ }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/spring.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/spring.json
new file mode 100644
index 000000000..0b4f34df3
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/front-shock/parts/spring.json
@@ -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
+ }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/package.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/package.json
new file mode 100644
index 000000000..60c933152
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/package.json
@@ -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"
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/assembly.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/assembly.json
new file mode 100644
index 000000000..a64422d92
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/assembly.json
@@ -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"
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/bearings.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/bearings.json
new file mode 100644
index 000000000..2a77b3b24
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/bearings.json
@@ -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" }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/case-bottom.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/case-bottom.json
new file mode 100644
index 000000000..008da36ee
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/case-bottom.json
@@ -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" }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/case-top.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/case-top.json
new file mode 100644
index 000000000..86c908f66
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/case-top.json
@@ -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" }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/outdrives.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/outdrives.json
new file mode 100644
index 000000000..845cfd778
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/outdrives.json
@@ -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 }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/ring-gear.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/ring-gear.json
new file mode 100644
index 000000000..96198b84b
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/ring-gear.json
@@ -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" }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/spider-gears.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/spider-gears.json
new file mode 100644
index 000000000..f9675b3c3
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/rear-differential/parts/spider-gears.json
@@ -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 }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/assembly.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/assembly.json
new file mode 100644
index 000000000..e63068a41
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/assembly.json
@@ -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"
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/horn-screw.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/horn-screw.json
new file mode 100644
index 000000000..2a9b8807b
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/horn-screw.json
@@ -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 }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/output-gear.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/output-gear.json
new file mode 100644
index 000000000..8f6d1a0ec
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/output-gear.json
@@ -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" }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/servo-body.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/servo-body.json
new file mode 100644
index 000000000..a05947857
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/servo-body.json
@@ -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" }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/servo-horn.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/servo-horn.json
new file mode 100644
index 000000000..4d6911a53
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/servo-horn.json
@@ -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" }
+ ]
+}
diff --git a/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/servo-mount.json b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/servo-mount.json
new file mode 100644
index 000000000..8a8eb6945
--- /dev/null
+++ b/exploded-diagrams/packages/rc/traxxas/slash-4x4/steering-servo/parts/servo-mount.json
@@ -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" }
+ ]
+}