thibaud frere
commited on
Commit
·
36a0415
1
Parent(s):
1f9a800
update multi images
Browse files
app/scripts/latex-to-mdx/mdx-converter.mjs
CHANGED
|
@@ -123,6 +123,104 @@ function addComponentImports(content) {
|
|
| 123 |
}
|
| 124 |
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
/**
|
| 127 |
* Transform images to ResponsiveImage components
|
| 128 |
* @param {string} content - MDX content
|
|
@@ -724,6 +822,7 @@ function processMdxContent(content, latexContent = '') {
|
|
| 724 |
processedContent = formatDisplayMathBlocks(processedContent);
|
| 725 |
processedContent = removeHtmlComments(processedContent);
|
| 726 |
processedContent = cleanMdxSyntax(processedContent);
|
|
|
|
| 727 |
processedContent = transformImages(processedContent);
|
| 728 |
processedContent = transformStyledSpans(processedContent);
|
| 729 |
processedContent = transformReferenceLinks(processedContent);
|
|
|
|
| 123 |
}
|
| 124 |
|
| 125 |
|
| 126 |
+
/**
|
| 127 |
+
* Convert grouped figures (subfigures) to MultiImage components
|
| 128 |
+
* @param {string} content - MDX content
|
| 129 |
+
* @returns {string} - Content with MultiImage components for grouped figures
|
| 130 |
+
*/
|
| 131 |
+
function convertSubfiguresToMultiImage(content) {
|
| 132 |
+
console.log(' 🖼️✨ Converting subfigures to MultiImage components...');
|
| 133 |
+
|
| 134 |
+
let convertedCount = 0;
|
| 135 |
+
|
| 136 |
+
// Pattern to match: <figure> containing multiple <figure> elements with a global caption
|
| 137 |
+
// This matches the LaTeX subfigure pattern that gets converted by Pandoc
|
| 138 |
+
const subfigureGroupPattern = /<figure>\s*((?:<figure>[\s\S]*?<\/figure>\s*){2,})<figcaption>([\s\S]*?)<\/figcaption>\s*<\/figure>/g;
|
| 139 |
+
|
| 140 |
+
const convertedContent = content.replace(subfigureGroupPattern, (match, figuresMatch, globalCaption) => {
|
| 141 |
+
convertedCount++;
|
| 142 |
+
|
| 143 |
+
// Extract individual figures within the group
|
| 144 |
+
// This pattern is more flexible to handle variations in HTML structure
|
| 145 |
+
const individualFigurePattern = /<figure>\s*<img src="([^"]*)"[^>]*\/>\s*<p><span id="([^"]*)"[^&]*><\/span><\/p>\s*<figcaption>([\s\S]*?)<\/figcaption>\s*<\/figure>/g;
|
| 146 |
+
|
| 147 |
+
const images = [];
|
| 148 |
+
let figureMatch;
|
| 149 |
+
|
| 150 |
+
while ((figureMatch = individualFigurePattern.exec(figuresMatch)) !== null) {
|
| 151 |
+
const [, src, id, caption] = figureMatch;
|
| 152 |
+
|
| 153 |
+
// Clean the source path (similar to existing transformImages function)
|
| 154 |
+
const cleanSrc = src.replace(/.*\/output\/assets\//, './assets/')
|
| 155 |
+
.replace(/\/Users\/[^\/]+\/[^\/]+\/[^\/]+\/[^\/]+\/[^\/]+\/app\/scripts\/latex-to-markdown\/output\/assets\//, './assets/');
|
| 156 |
+
|
| 157 |
+
// Clean caption text (remove HTML, normalize whitespace)
|
| 158 |
+
const cleanCaption = caption
|
| 159 |
+
.replace(/<[^>]*>/g, '')
|
| 160 |
+
.replace(/\n/g, ' ')
|
| 161 |
+
.replace(/\s+/g, ' ')
|
| 162 |
+
.replace(/'/g, "\\'")
|
| 163 |
+
.trim();
|
| 164 |
+
|
| 165 |
+
// Generate alt text from caption
|
| 166 |
+
const altText = cleanCaption.length > 100
|
| 167 |
+
? cleanCaption.substring(0, 100) + '...'
|
| 168 |
+
: cleanCaption;
|
| 169 |
+
|
| 170 |
+
// Generate variable name for import
|
| 171 |
+
const varName = generateImageVarName(cleanSrc);
|
| 172 |
+
imageImports.set(cleanSrc, varName);
|
| 173 |
+
|
| 174 |
+
images.push({
|
| 175 |
+
src: varName,
|
| 176 |
+
alt: altText,
|
| 177 |
+
caption: cleanCaption,
|
| 178 |
+
id: id
|
| 179 |
+
});
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// Clean global caption
|
| 183 |
+
const cleanGlobalCaption = globalCaption
|
| 184 |
+
.replace(/<[^>]*>/g, '')
|
| 185 |
+
.replace(/\n/g, ' ')
|
| 186 |
+
.replace(/\s+/g, ' ')
|
| 187 |
+
.replace(/'/g, "\\'")
|
| 188 |
+
.trim();
|
| 189 |
+
|
| 190 |
+
// Mark MultiImage component as used
|
| 191 |
+
usedComponents.add('MultiImage');
|
| 192 |
+
|
| 193 |
+
// Determine layout based on number of images
|
| 194 |
+
let layout = 'auto';
|
| 195 |
+
if (images.length === 2) layout = '2-column';
|
| 196 |
+
else if (images.length === 3) layout = '3-column';
|
| 197 |
+
else if (images.length === 4) layout = '4-column';
|
| 198 |
+
|
| 199 |
+
// Generate MultiImage component
|
| 200 |
+
const imagesJson = images.map(img =>
|
| 201 |
+
` {\n src: ${img.src},\n alt: "${img.alt}",\n caption: "${img.caption}",\n id: "${img.id}"\n }`
|
| 202 |
+
).join(',\n');
|
| 203 |
+
|
| 204 |
+
return `<MultiImage
|
| 205 |
+
images={[
|
| 206 |
+
${imagesJson}
|
| 207 |
+
]}
|
| 208 |
+
layout="${layout}"
|
| 209 |
+
zoomable
|
| 210 |
+
downloadable
|
| 211 |
+
caption="${cleanGlobalCaption}"
|
| 212 |
+
/>`;
|
| 213 |
+
});
|
| 214 |
+
|
| 215 |
+
if (convertedCount > 0) {
|
| 216 |
+
console.log(` ✅ Converted ${convertedCount} subfigure group(s) to MultiImage component(s)`);
|
| 217 |
+
} else {
|
| 218 |
+
console.log(' ℹ️ No subfigure groups found');
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
return convertedContent;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
/**
|
| 225 |
* Transform images to ResponsiveImage components
|
| 226 |
* @param {string} content - MDX content
|
|
|
|
| 822 |
processedContent = formatDisplayMathBlocks(processedContent);
|
| 823 |
processedContent = removeHtmlComments(processedContent);
|
| 824 |
processedContent = cleanMdxSyntax(processedContent);
|
| 825 |
+
processedContent = convertSubfiguresToMultiImage(processedContent);
|
| 826 |
processedContent = transformImages(processedContent);
|
| 827 |
processedContent = transformStyledSpans(processedContent);
|
| 828 |
processedContent = transformReferenceLinks(processedContent);
|
app/scripts/latex-to-mdx/output/main.mdx
CHANGED
|
@@ -18,15 +18,16 @@ published: "Sep 18, 2025"
|
|
| 18 |
tableOfContentsAutoCollapse: true
|
| 19 |
---
|
| 20 |
|
|
|
|
| 21 |
import ResponsiveImage from '../components/ResponsiveImage.astro';
|
|
|
|
|
|
|
|
|
|
| 22 |
import ch1_lerobot_figure1 from './assets/image/figures/ch1/ch1-lerobot-figure1.png';
|
| 23 |
import ch2_approaches from './assets/image/figures/ch2/ch2-approaches.png';
|
| 24 |
import ch2_platforms from './assets/image/figures/ch2/ch2-platforms.png';
|
| 25 |
import ch2_cost_accessibility from './assets/image/figures/ch2/ch2-cost-accessibility.png';
|
| 26 |
import ch2_so100_to_planar_manipulator from './assets/image/figures/ch2/ch2-so100-to-planar-manipulator.png';
|
| 27 |
-
import ch2_planar_manipulator_free from './assets/image/figures/ch2/ch2-planar-manipulator-free.png';
|
| 28 |
-
import ch2_planar_manipulator_floor from './assets/image/figures/ch2/ch2-planar-manipulator-floor.png';
|
| 29 |
-
import ch2_planar_manipulator_floor_shelf from './assets/image/figures/ch2/ch2-planar-manipulator-floor-shelf.png';
|
| 30 |
import ch2_planar_manipulator_floor_box from './assets/image/figures/ch2/ch2-planar-manipulator-floor-box.png';
|
| 31 |
import ch2_classical_limitations from './assets/image/figures/ch2/ch2-classical-limitations.png';
|
| 32 |
import ch3_learning_benefits from './assets/image/figures/ch3/ch3-learning-benefits.png';
|
|
@@ -304,42 +305,32 @@ Further, let us make the simplifying assumption that actuators can produce rotat
|
|
| 304 |
|
| 305 |
All these simplifying assumptions leave us with the planar manipulator of Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, free of moving its end-effector by controlling the angles $\theta_1$ and $\theta_2$, jointly referred to as the robot’s *configuration*, and indicated with $q = [\theta_1, \theta_2 ] \in [-\pi, +\pi]^2$. The axis attached to the joints indicate the associated reference frame, whereas circular arrows indicate the maximal feasible rotation allowed at each joint. In this tutorial, we do not cover topics related to spatial algebra, and we instead refer the reader to and for excellent explanations of the mechanics and theoretical foundations of producing motion on rigid bodies.
|
| 306 |
|
| 307 |
-
<
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
zoomable
|
| 312 |
downloadable
|
| 313 |
-
|
| 314 |
-
alt="Figure"
|
| 315 |
/>
|
| 316 |
-
<span id="planar-manipulation-simple" style="position: absolute;"></span>
|
| 317 |
-
<figcaption>Free to move</figcaption>
|
| 318 |
-
</figure>
|
| 319 |
-
<figure>
|
| 320 |
-
<ResponsiveImage
|
| 321 |
-
src={ch2_planar_manipulator_floor}
|
| 322 |
-
zoomable
|
| 323 |
-
downloadable
|
| 324 |
-
layout="fixed"
|
| 325 |
-
alt="Figure"
|
| 326 |
-
/>
|
| 327 |
-
<span id="planar-manipulator-floor" style="position: absolute;"></span>
|
| 328 |
-
<figcaption>Constrained by the surface</figcaption>
|
| 329 |
-
</figure>
|
| 330 |
-
<figure>
|
| 331 |
-
<ResponsiveImage
|
| 332 |
-
src={ch2_planar_manipulator_floor_shelf}
|
| 333 |
-
zoomable
|
| 334 |
-
downloadable
|
| 335 |
-
layout="fixed"
|
| 336 |
-
alt="Figure"
|
| 337 |
-
/>
|
| 338 |
-
<span id="planar-manipulator-floor-shelf" style="position: absolute;"></span>
|
| 339 |
-
<figcaption>Constrained by surface and (fixed) obstacle</figcaption>
|
| 340 |
-
</figure>
|
| 341 |
-
<figcaption>Planar, 2-dof schematic representation of the SO-100 manipulator under diverse deployment settings. From left to right: completely free of moving; constrained by the presence of the surface; constrained by the surface and presence of obstacles. Circular arrows around each joint indicate the maximal rotation feasible at that joint.</figcaption>
|
| 342 |
-
</figure>
|
| 343 |
|
| 344 |
Considering the (toy) example presented in Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, then we can analytically write the end-effector’s position $p \in \mathbb R^2$ as a function of the robot’s configuration, $p = p(q), p: \mathcal Q \mapsto \mathbb R^2$. In particular, we have: $p(q) = \begin{pmatrix} p_x(\theta_1, \theta_2) \\ p_y(\theta_1, \theta_2) \end{pmatrix} = \begin{pmatrix} l \cos(\theta_1) + l \cos(\theta_1 + \theta_2) \\ l \sin(\theta_1) + l \sin(\theta_1 + \theta_2) \end{pmatrix} \in S^{n=2}_{l_1+l_2} = \{ p(q) \in \mathbb R^2: \Vert p(q) \Vert_2^2 \leq (2l)^2, \ \forall q \in \mathcal Q \}$
|
| 345 |
|
|
|
|
| 18 |
tableOfContentsAutoCollapse: true
|
| 19 |
---
|
| 20 |
|
| 21 |
+
import MultiImage from '../components/MultiImage.astro';
|
| 22 |
import ResponsiveImage from '../components/ResponsiveImage.astro';
|
| 23 |
+
import ch2_planar_manipulator_free from './assets/image/figures/ch2/ch2-planar-manipulator-free.png';
|
| 24 |
+
import ch2_planar_manipulator_floor from './assets/image/figures/ch2/ch2-planar-manipulator-floor.png';
|
| 25 |
+
import ch2_planar_manipulator_floor_shelf from './assets/image/figures/ch2/ch2-planar-manipulator-floor-shelf.png';
|
| 26 |
import ch1_lerobot_figure1 from './assets/image/figures/ch1/ch1-lerobot-figure1.png';
|
| 27 |
import ch2_approaches from './assets/image/figures/ch2/ch2-approaches.png';
|
| 28 |
import ch2_platforms from './assets/image/figures/ch2/ch2-platforms.png';
|
| 29 |
import ch2_cost_accessibility from './assets/image/figures/ch2/ch2-cost-accessibility.png';
|
| 30 |
import ch2_so100_to_planar_manipulator from './assets/image/figures/ch2/ch2-so100-to-planar-manipulator.png';
|
|
|
|
|
|
|
|
|
|
| 31 |
import ch2_planar_manipulator_floor_box from './assets/image/figures/ch2/ch2-planar-manipulator-floor-box.png';
|
| 32 |
import ch2_classical_limitations from './assets/image/figures/ch2/ch2-classical-limitations.png';
|
| 33 |
import ch3_learning_benefits from './assets/image/figures/ch3/ch3-learning-benefits.png';
|
|
|
|
| 305 |
|
| 306 |
All these simplifying assumptions leave us with the planar manipulator of Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, free of moving its end-effector by controlling the angles $\theta_1$ and $\theta_2$, jointly referred to as the robot’s *configuration*, and indicated with $q = [\theta_1, \theta_2 ] \in [-\pi, +\pi]^2$. The axis attached to the joints indicate the associated reference frame, whereas circular arrows indicate the maximal feasible rotation allowed at each joint. In this tutorial, we do not cover topics related to spatial algebra, and we instead refer the reader to and for excellent explanations of the mechanics and theoretical foundations of producing motion on rigid bodies.
|
| 307 |
|
| 308 |
+
<MultiImage
|
| 309 |
+
images={[
|
| 310 |
+
{
|
| 311 |
+
src: ch2_planar_manipulator_free,
|
| 312 |
+
alt: "Free to move",
|
| 313 |
+
caption: "Free to move",
|
| 314 |
+
id: "planar-manipulation-simple"
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
src: ch2_planar_manipulator_floor,
|
| 318 |
+
alt: "Constrained by the surface",
|
| 319 |
+
caption: "Constrained by the surface",
|
| 320 |
+
id: "planar-manipulator-floor"
|
| 321 |
+
},
|
| 322 |
+
{
|
| 323 |
+
src: ch2_planar_manipulator_floor_shelf,
|
| 324 |
+
alt: "Constrained by surface and (fixed) obstacle",
|
| 325 |
+
caption: "Constrained by surface and (fixed) obstacle",
|
| 326 |
+
id: "planar-manipulator-floor-shelf"
|
| 327 |
+
}
|
| 328 |
+
]}
|
| 329 |
+
layout="3-column"
|
| 330 |
zoomable
|
| 331 |
downloadable
|
| 332 |
+
caption="Planar, 2-dof schematic representation of the SO-100 manipulator under diverse deployment settings. From left to right: completely free of moving; constrained by the presence of the surface; constrained by the surface and presence of obstacles. Circular arrows around each joint indicate the maximal rotation feasible at that joint."
|
|
|
|
| 333 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
|
| 335 |
Considering the (toy) example presented in Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, then we can analytically write the end-effector’s position $p \in \mathbb R^2$ as a function of the robot’s configuration, $p = p(q), p: \mathcal Q \mapsto \mathbb R^2$. In particular, we have: $p(q) = \begin{pmatrix} p_x(\theta_1, \theta_2) \\ p_y(\theta_1, \theta_2) \end{pmatrix} = \begin{pmatrix} l \cos(\theta_1) + l \cos(\theta_1 + \theta_2) \\ l \sin(\theta_1) + l \sin(\theta_1 + \theta_2) \end{pmatrix} \in S^{n=2}_{l_1+l_2} = \{ p(q) \in \mathbb R^2: \Vert p(q) \Vert_2^2 \leq (2l)^2, \ \forall q \in \mathcal Q \}$
|
| 336 |
|
app/src/components/MultiImage.astro
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
// @ts-ignore - types provided by Astro at runtime
|
| 3 |
+
import ResponsiveImage from "./ResponsiveImage.astro";
|
| 4 |
+
|
| 5 |
+
interface ImageItem {
|
| 6 |
+
/** Source image imported via astro:assets */
|
| 7 |
+
src: any;
|
| 8 |
+
/** Alt text for accessibility */
|
| 9 |
+
alt: string;
|
| 10 |
+
/** Individual caption for this image */
|
| 11 |
+
caption?: string;
|
| 12 |
+
/** Optional individual image ID for referencing */
|
| 13 |
+
id?: string;
|
| 14 |
+
/** Enable zoom on this specific image (defaults to parent zoomable setting) */
|
| 15 |
+
zoomable?: boolean;
|
| 16 |
+
/** Enable download on this specific image (defaults to parent downloadable setting) */
|
| 17 |
+
downloadable?: boolean;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
interface Props {
|
| 21 |
+
/** Array of images to display */
|
| 22 |
+
images: ImageItem[];
|
| 23 |
+
/** Global caption for the entire figure */
|
| 24 |
+
caption?: string;
|
| 25 |
+
/** Layout mode: number of columns or 'auto' for responsive */
|
| 26 |
+
layout?: "2-column" | "3-column" | "4-column" | "auto";
|
| 27 |
+
/** Enable medium-zoom behavior on all images (can be overridden per image) */
|
| 28 |
+
zoomable?: boolean;
|
| 29 |
+
/** Show download buttons on all images (can be overridden per image) */
|
| 30 |
+
downloadable?: boolean;
|
| 31 |
+
/** Optional class to apply on the wrapper */
|
| 32 |
+
class?: string;
|
| 33 |
+
/** Optional global ID for the multi-image figure */
|
| 34 |
+
id?: string;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
const {
|
| 38 |
+
images,
|
| 39 |
+
caption,
|
| 40 |
+
layout = "3-column",
|
| 41 |
+
zoomable = false,
|
| 42 |
+
downloadable = false,
|
| 43 |
+
class: className,
|
| 44 |
+
id,
|
| 45 |
+
} = Astro.props as Props;
|
| 46 |
+
|
| 47 |
+
const hasCaptionSlot = Astro.slots.has("caption");
|
| 48 |
+
const hasCaption =
|
| 49 |
+
hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
|
| 50 |
+
const uid = `mi_${Math.random().toString(36).slice(2)}`;
|
| 51 |
+
|
| 52 |
+
// Generate CSS grid columns based on layout
|
| 53 |
+
const getGridColumns = () => {
|
| 54 |
+
switch (layout) {
|
| 55 |
+
case "2-column":
|
| 56 |
+
return "repeat(2, 1fr)";
|
| 57 |
+
case "3-column":
|
| 58 |
+
return "repeat(3, 1fr)";
|
| 59 |
+
case "4-column":
|
| 60 |
+
return "repeat(4, 1fr)";
|
| 61 |
+
case "auto":
|
| 62 |
+
return "repeat(auto-fit, minmax(200px, 1fr))";
|
| 63 |
+
default:
|
| 64 |
+
return "repeat(3, 1fr)";
|
| 65 |
+
}
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
const gridColumns = getGridColumns();
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
<div
|
| 72 |
+
class={`multi-image ${className || ""}`}
|
| 73 |
+
data-mi-root={uid}
|
| 74 |
+
data-layout={layout}
|
| 75 |
+
{id}
|
| 76 |
+
>
|
| 77 |
+
{
|
| 78 |
+
hasCaption ? (
|
| 79 |
+
<figure class="multi-image-figure">
|
| 80 |
+
<div
|
| 81 |
+
class="multi-image-grid"
|
| 82 |
+
style={`grid-template-columns: ${gridColumns}`}
|
| 83 |
+
>
|
| 84 |
+
{images.map((image, index) => (
|
| 85 |
+
<div class="multi-image-item">
|
| 86 |
+
<ResponsiveImage
|
| 87 |
+
src={image.src}
|
| 88 |
+
alt={image.alt}
|
| 89 |
+
zoomable={image.zoomable ?? zoomable}
|
| 90 |
+
downloadable={
|
| 91 |
+
image.downloadable ?? downloadable
|
| 92 |
+
}
|
| 93 |
+
class="multi-image-img"
|
| 94 |
+
/>
|
| 95 |
+
{image.caption && (
|
| 96 |
+
<div class="multi-image-subcaption">
|
| 97 |
+
{image.caption}
|
| 98 |
+
</div>
|
| 99 |
+
)}
|
| 100 |
+
{image.id && (
|
| 101 |
+
<span
|
| 102 |
+
id={image.id}
|
| 103 |
+
style="position: absolute;"
|
| 104 |
+
/>
|
| 105 |
+
)}
|
| 106 |
+
</div>
|
| 107 |
+
))}
|
| 108 |
+
</div>
|
| 109 |
+
<figcaption class="multi-image-caption">
|
| 110 |
+
{hasCaptionSlot ? (
|
| 111 |
+
<slot name="caption" />
|
| 112 |
+
) : (
|
| 113 |
+
caption && <span set:html={caption} />
|
| 114 |
+
)}
|
| 115 |
+
</figcaption>
|
| 116 |
+
</figure>
|
| 117 |
+
) : (
|
| 118 |
+
<div
|
| 119 |
+
class="multi-image-grid"
|
| 120 |
+
style={`grid-template-columns: ${gridColumns}`}
|
| 121 |
+
>
|
| 122 |
+
{images.map((image, index) => (
|
| 123 |
+
<div class="multi-image-item">
|
| 124 |
+
<ResponsiveImage
|
| 125 |
+
src={image.src}
|
| 126 |
+
alt={image.alt}
|
| 127 |
+
zoomable={image.zoomable ?? zoomable}
|
| 128 |
+
downloadable={image.downloadable ?? downloadable}
|
| 129 |
+
class="multi-image-img"
|
| 130 |
+
/>
|
| 131 |
+
{image.caption && (
|
| 132 |
+
<div class="multi-image-subcaption">
|
| 133 |
+
{image.caption}
|
| 134 |
+
</div>
|
| 135 |
+
)}
|
| 136 |
+
{image.id && (
|
| 137 |
+
<span id={image.id} style="position: absolute;" />
|
| 138 |
+
)}
|
| 139 |
+
</div>
|
| 140 |
+
))}
|
| 141 |
+
</div>
|
| 142 |
+
)
|
| 143 |
+
}
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<style>
|
| 147 |
+
.multi-image {
|
| 148 |
+
margin: var(--block-spacing-y) 0;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.multi-image-figure {
|
| 152 |
+
margin: 0;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.multi-image-grid {
|
| 156 |
+
display: grid;
|
| 157 |
+
gap: 1rem;
|
| 158 |
+
align-items: start;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.multi-image-item {
|
| 162 |
+
display: flex;
|
| 163 |
+
flex-direction: column;
|
| 164 |
+
text-align: center;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.multi-image-item :global(.ri-root) {
|
| 168 |
+
margin: 0;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.multi-image-item :global(figure) {
|
| 172 |
+
margin: 0;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.multi-image-img {
|
| 176 |
+
width: 100%;
|
| 177 |
+
height: auto;
|
| 178 |
+
object-fit: contain;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.multi-image-subcaption {
|
| 182 |
+
font-size: 0.85rem;
|
| 183 |
+
color: var(--muted-color);
|
| 184 |
+
margin-top: 0.5rem;
|
| 185 |
+
line-height: 1.4;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.multi-image-caption {
|
| 189 |
+
text-align: left;
|
| 190 |
+
font-size: 0.9rem;
|
| 191 |
+
color: var(--muted-color);
|
| 192 |
+
margin-top: 1rem;
|
| 193 |
+
line-height: 1.4;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/* Responsive behavior */
|
| 197 |
+
@media (max-width: 768px) {
|
| 198 |
+
.multi-image-grid[style*="repeat(3, 1fr)"],
|
| 199 |
+
.multi-image-grid[style*="repeat(4, 1fr)"] {
|
| 200 |
+
grid-template-columns: 1fr !important;
|
| 201 |
+
gap: 1.5rem;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.multi-image-grid[style*="repeat(2, 1fr)"] {
|
| 205 |
+
grid-template-columns: 1fr !important;
|
| 206 |
+
gap: 1.5rem;
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
@media (min-width: 769px) and (max-width: 1024px) {
|
| 211 |
+
.multi-image-grid[style*="repeat(4, 1fr)"] {
|
| 212 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/* Equal height images when desired */
|
| 217 |
+
.multi-image[data-layout*="column"] .multi-image-item :global(img) {
|
| 218 |
+
height: 200px;
|
| 219 |
+
object-fit: contain;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* Auto layout gets flexible heights */
|
| 223 |
+
.multi-image[data-layout="auto"] .multi-image-item :global(img) {
|
| 224 |
+
height: auto;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
/* Ensure images maintain aspect ratio */
|
| 228 |
+
.multi-image-item :global(img) {
|
| 229 |
+
max-width: 100%;
|
| 230 |
+
display: block;
|
| 231 |
+
margin: 0 auto;
|
| 232 |
+
}
|
| 233 |
+
</style>
|
app/src/content/article.mdx
CHANGED
|
@@ -18,15 +18,16 @@ published: "Sep 18, 2025"
|
|
| 18 |
tableOfContentsAutoCollapse: true
|
| 19 |
---
|
| 20 |
|
|
|
|
| 21 |
import ResponsiveImage from '../components/ResponsiveImage.astro';
|
|
|
|
|
|
|
|
|
|
| 22 |
import ch1_lerobot_figure1 from './assets/image/figures/ch1/ch1-lerobot-figure1.png';
|
| 23 |
import ch2_approaches from './assets/image/figures/ch2/ch2-approaches.png';
|
| 24 |
import ch2_platforms from './assets/image/figures/ch2/ch2-platforms.png';
|
| 25 |
import ch2_cost_accessibility from './assets/image/figures/ch2/ch2-cost-accessibility.png';
|
| 26 |
import ch2_so100_to_planar_manipulator from './assets/image/figures/ch2/ch2-so100-to-planar-manipulator.png';
|
| 27 |
-
import ch2_planar_manipulator_free from './assets/image/figures/ch2/ch2-planar-manipulator-free.png';
|
| 28 |
-
import ch2_planar_manipulator_floor from './assets/image/figures/ch2/ch2-planar-manipulator-floor.png';
|
| 29 |
-
import ch2_planar_manipulator_floor_shelf from './assets/image/figures/ch2/ch2-planar-manipulator-floor-shelf.png';
|
| 30 |
import ch2_planar_manipulator_floor_box from './assets/image/figures/ch2/ch2-planar-manipulator-floor-box.png';
|
| 31 |
import ch2_classical_limitations from './assets/image/figures/ch2/ch2-classical-limitations.png';
|
| 32 |
import ch3_learning_benefits from './assets/image/figures/ch3/ch3-learning-benefits.png';
|
|
@@ -304,42 +305,32 @@ Further, let us make the simplifying assumption that actuators can produce rotat
|
|
| 304 |
|
| 305 |
All these simplifying assumptions leave us with the planar manipulator of Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, free of moving its end-effector by controlling the angles $\theta_1$ and $\theta_2$, jointly referred to as the robot’s *configuration*, and indicated with $q = [\theta_1, \theta_2 ] \in [-\pi, +\pi]^2$. The axis attached to the joints indicate the associated reference frame, whereas circular arrows indicate the maximal feasible rotation allowed at each joint. In this tutorial, we do not cover topics related to spatial algebra, and we instead refer the reader to and for excellent explanations of the mechanics and theoretical foundations of producing motion on rigid bodies.
|
| 306 |
|
| 307 |
-
<
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
zoomable
|
| 312 |
downloadable
|
| 313 |
-
|
| 314 |
-
alt="Figure"
|
| 315 |
/>
|
| 316 |
-
<span id="planar-manipulation-simple" style="position: absolute;"></span>
|
| 317 |
-
<figcaption>Free to move</figcaption>
|
| 318 |
-
</figure>
|
| 319 |
-
<figure>
|
| 320 |
-
<ResponsiveImage
|
| 321 |
-
src={ch2_planar_manipulator_floor}
|
| 322 |
-
zoomable
|
| 323 |
-
downloadable
|
| 324 |
-
layout="fixed"
|
| 325 |
-
alt="Figure"
|
| 326 |
-
/>
|
| 327 |
-
<span id="planar-manipulator-floor" style="position: absolute;"></span>
|
| 328 |
-
<figcaption>Constrained by the surface</figcaption>
|
| 329 |
-
</figure>
|
| 330 |
-
<figure>
|
| 331 |
-
<ResponsiveImage
|
| 332 |
-
src={ch2_planar_manipulator_floor_shelf}
|
| 333 |
-
zoomable
|
| 334 |
-
downloadable
|
| 335 |
-
layout="fixed"
|
| 336 |
-
alt="Figure"
|
| 337 |
-
/>
|
| 338 |
-
<span id="planar-manipulator-floor-shelf" style="position: absolute;"></span>
|
| 339 |
-
<figcaption>Constrained by surface and (fixed) obstacle</figcaption>
|
| 340 |
-
</figure>
|
| 341 |
-
<figcaption>Planar, 2-dof schematic representation of the SO-100 manipulator under diverse deployment settings. From left to right: completely free of moving; constrained by the presence of the surface; constrained by the surface and presence of obstacles. Circular arrows around each joint indicate the maximal rotation feasible at that joint.</figcaption>
|
| 342 |
-
</figure>
|
| 343 |
|
| 344 |
Considering the (toy) example presented in Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, then we can analytically write the end-effector’s position $p \in \mathbb R^2$ as a function of the robot’s configuration, $p = p(q), p: \mathcal Q \mapsto \mathbb R^2$. In particular, we have: $p(q) = \begin{pmatrix} p_x(\theta_1, \theta_2) \\ p_y(\theta_1, \theta_2) \end{pmatrix} = \begin{pmatrix} l \cos(\theta_1) + l \cos(\theta_1 + \theta_2) \\ l \sin(\theta_1) + l \sin(\theta_1 + \theta_2) \end{pmatrix} \in S^{n=2}_{l_1+l_2} = \{ p(q) \in \mathbb R^2: \Vert p(q) \Vert_2^2 \leq (2l)^2, \ \forall q \in \mathcal Q \}$
|
| 345 |
|
|
|
|
| 18 |
tableOfContentsAutoCollapse: true
|
| 19 |
---
|
| 20 |
|
| 21 |
+
import MultiImage from '../components/MultiImage.astro';
|
| 22 |
import ResponsiveImage from '../components/ResponsiveImage.astro';
|
| 23 |
+
import ch2_planar_manipulator_free from './assets/image/figures/ch2/ch2-planar-manipulator-free.png';
|
| 24 |
+
import ch2_planar_manipulator_floor from './assets/image/figures/ch2/ch2-planar-manipulator-floor.png';
|
| 25 |
+
import ch2_planar_manipulator_floor_shelf from './assets/image/figures/ch2/ch2-planar-manipulator-floor-shelf.png';
|
| 26 |
import ch1_lerobot_figure1 from './assets/image/figures/ch1/ch1-lerobot-figure1.png';
|
| 27 |
import ch2_approaches from './assets/image/figures/ch2/ch2-approaches.png';
|
| 28 |
import ch2_platforms from './assets/image/figures/ch2/ch2-platforms.png';
|
| 29 |
import ch2_cost_accessibility from './assets/image/figures/ch2/ch2-cost-accessibility.png';
|
| 30 |
import ch2_so100_to_planar_manipulator from './assets/image/figures/ch2/ch2-so100-to-planar-manipulator.png';
|
|
|
|
|
|
|
|
|
|
| 31 |
import ch2_planar_manipulator_floor_box from './assets/image/figures/ch2/ch2-planar-manipulator-floor-box.png';
|
| 32 |
import ch2_classical_limitations from './assets/image/figures/ch2/ch2-classical-limitations.png';
|
| 33 |
import ch3_learning_benefits from './assets/image/figures/ch3/ch3-learning-benefits.png';
|
|
|
|
| 305 |
|
| 306 |
All these simplifying assumptions leave us with the planar manipulator of Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, free of moving its end-effector by controlling the angles $\theta_1$ and $\theta_2$, jointly referred to as the robot’s *configuration*, and indicated with $q = [\theta_1, \theta_2 ] \in [-\pi, +\pi]^2$. The axis attached to the joints indicate the associated reference frame, whereas circular arrows indicate the maximal feasible rotation allowed at each joint. In this tutorial, we do not cover topics related to spatial algebra, and we instead refer the reader to and for excellent explanations of the mechanics and theoretical foundations of producing motion on rigid bodies.
|
| 307 |
|
| 308 |
+
<MultiImage
|
| 309 |
+
images={[
|
| 310 |
+
{
|
| 311 |
+
src: ch2_planar_manipulator_free,
|
| 312 |
+
alt: "Free to move",
|
| 313 |
+
caption: "Free to move",
|
| 314 |
+
id: "planar-manipulation-simple"
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
src: ch2_planar_manipulator_floor,
|
| 318 |
+
alt: "Constrained by the surface",
|
| 319 |
+
caption: "Constrained by the surface",
|
| 320 |
+
id: "planar-manipulator-floor"
|
| 321 |
+
},
|
| 322 |
+
{
|
| 323 |
+
src: ch2_planar_manipulator_floor_shelf,
|
| 324 |
+
alt: "Constrained by surface and (fixed) obstacle",
|
| 325 |
+
caption: "Constrained by surface and (fixed) obstacle",
|
| 326 |
+
id: "planar-manipulator-floor-shelf"
|
| 327 |
+
}
|
| 328 |
+
]}
|
| 329 |
+
layout="3-column"
|
| 330 |
zoomable
|
| 331 |
downloadable
|
| 332 |
+
caption="Planar, 2-dof schematic representation of the SO-100 manipulator under diverse deployment settings. From left to right: completely free of moving; constrained by the presence of the surface; constrained by the surface and presence of obstacles. Circular arrows around each joint indicate the maximal rotation feasible at that joint."
|
|
|
|
| 333 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
|
| 335 |
Considering the (toy) example presented in Figure <a href="#planar-manipulation-simple" data-reference-type="ref" data-reference="planar-manipulation-simple">[planar-manipulation-simple]</a>, then we can analytically write the end-effector’s position $p \in \mathbb R^2$ as a function of the robot’s configuration, $p = p(q), p: \mathcal Q \mapsto \mathbb R^2$. In particular, we have: $p(q) = \begin{pmatrix} p_x(\theta_1, \theta_2) \\ p_y(\theta_1, \theta_2) \end{pmatrix} = \begin{pmatrix} l \cos(\theta_1) + l \cos(\theta_1 + \theta_2) \\ l \sin(\theta_1) + l \sin(\theta_1 + \theta_2) \end{pmatrix} \in S^{n=2}_{l_1+l_2} = \{ p(q) \in \mathbb R^2: \Vert p(q) \Vert_2^2 \leq (2l)^2, \ \forall q \in \mathcal Q \}$
|
| 336 |
|