Rebuild a Street Problem in 3D
Turn safe observations into a cautious 3D evidence brief with Claude Code.
Introduction
30 Second Summary
Street defects can look shallow from one angle and severe from another. A report built around one confident number can hide that uncertainty from the person deciding what happens next.
In this project, you will use Claude Code to turn six safe street photographs into a local Pothole Evidence Builder. The finished page uses cautious dimension ranges to update a rotatable Three.js road model before producing a printable source-linked evidence brief.
What You'll Build
You will watch six photographs, cautious ranges, an interactive road surface, and three labelled vehicle demonstrations come together in one landscape brief that you can print or save as a PDF.
By the end of this project, you'll have:
- A six-photo evidence contact sheet that displays your safe street observations with cautious dimension ranges and a visible confidence rating.
- A responsive 3D model that changes shape when you update the estimated length, width, or depth range.
- Three illustrative 30 km/h demonstrations beside a printable one-page brief with source links and an official reporting URL.
- Secret Mission: A second independent scale reference that stress-tests whether your original ranges still deserve the same confidence.
Are there any prerequisites?
You need a Mac with internet access plus an existing paid Claude plan. You also need a street defect visible from the footpath plus permission to borrow an iPhone and a 30 cm metric ruler.
Before We Start
Before the hands-on work begins, choose a street defect that you can observe safely from the footpath. This commitment keeps your evidence focused on communicating uncertainty instead of manufacturing precision.
Set Up Claude Code and Your Safe Field Kit
Your purpose statement sets the standard for this project. Every result must communicate uncertainty without manufacturing precision.
Before local files or field evidence enter the project, you need a controlled workspace. You will install Claude Desktop before configuring its Claude Code workspace for reviewed file changes.
A minimal Three.js scene proves that your browser-based model can run. A footpath-only capture plan keeps the fieldwork within a firm safety boundary.
In this step, get ready to:
- Install Claude Desktop before opening a local session in Manual permission mode.
- Create pothole-evidence-brief with a rotatable Three.js starter.
- Prepare the borrowed field kit before transferring a harmless test image through AirDrop.
Install Claude Desktop and set Manual permissions
The Code tab gives Claude access to a folder you choose. Manual permission mode pauses proposed file changes until you review them.
- Visit the official Claude Desktop quickstart in your browser.
- Download the macOS installer from the quickstart page.
- Run the downloaded installer on your Mac.
- Press Cmd+Space to open macOS search.
- Type Claude before pressing Enter to launch Claude Desktop.
- Sign in with your existing paid Claude account.
Why Manual permission mode?
Manual permission mode waits for your approval before applying edits or commands. This gives you a review point before Claude changes any local file.
- Click the Code tab.
- Choose Local in the environment selector.
- Set the permission mode to Manual.
You should see the Code tab with a local session ready for a project folder. That setup hurdle is cleared because Claude now waits for your review before changing local files.
Code tab not ready?
- Confirm that Claude Desktop shows the paid account you intended to use.
- Return to the environment selector if the session does not show Local.
- Return to the permission control if the session does not show Manual.
- Ask for help with setting up a local Manual session in Claude Desktop.
Create the Three.js starter
The starter is a small technical check before you collect real evidence. Its flat road patch proves that the renderer loads inside the Browser pane.
- Click the project folder control in the Code tab.
- Use the folder chooser to create pothole-evidence-brief on your Desktop.
- Select the new pothole-evidence-brief folder for the local session.
The Code tab should now identify pothole-evidence-brief as the selected local folder.
What should the starter contain?
- A minimal index.html file inside pothole-evidence-brief.
- An import map for three plus three/addons/ from the same Three.js 0.187.0 release.
- A WebGLRenderer with a PerspectiveCamera.
- An OrbitControls controller attached to the renderer.
- Lighting that makes a flat road patch visible.
- A render loop that keeps camera movement responsive.
- Ask Claude to create index.html inside the selected folder using every requirement above.
- Review the proposed contents of index.html in the Manual permission review.
- Approve the proposed index.html file change.
Before you preview the starter, do you expect dragging to move the road patch itself or move the camera around it?
- Click the index.html path in the Claude Code chat to open it in the Browser pane.
- Drag across the road patch in the Browser pane.
You should see a flat road patch from a changing viewpoint as you drag. This confirms that Three.js and OrbitControls are running in the local HTML page.
Road patch not rotating?
- Confirm that your Mac still has internet access because the import map loads Three.js from a CDN.
- Ask Claude to verify that OrbitControls is attached to the renderer used by the visible canvas.
- Ask Claude to inspect the Browser pane for a loading problem without adding later project sections.
- Get help with debugging a flat Three.js starter that does not rotate in Claude Desktop.
✔️ Awesome, I've got everything!
Great. Confirm that you approved index.html before keeping the rotatable road patch open in the Browser pane.
ⓧ I'd like to double check the full code
Claude may choose different spacing or helper names for this starter. Check that the complete file has every required part below.
- The file is saved at pothole-evidence-brief/index.html.
- The import map resolves three with three/addons/ from Three.js 0.187.0.
- The page creates a WebGLRenderer.
- The scene uses a PerspectiveCamera.
- The camera is controlled through OrbitControls.
- The scene contains lighting plus a visible flat road patch.
- Dragging in the Browser pane rotates the view around the patch.
Prepare the safe field kit and transfer a test photo
Field evidence only helps when gathering it remains safe. Official road guidance keeps people out of the carriageway during photography or measurement.
The ruler provides a possible scale reference without removing measurement uncertainty. Safety decides whether you use it at all.
- Borrow an iPhone for the field capture.
- Borrow a 30 cm metric ruler for safe scale references.
- Choose a harmless test subject away from traffic.
- Commit to this rule: neither you nor the ruler ever enters the carriageway.
- Open the Camera app on the borrowed iPhone.
- Tap the Shutter button to capture the harmless test subject.
AirDrop transfers the test image without creating a cloud resource. Both devices need Wi-Fi plus Bluetooth for the transfer.
Keep the AirDrop devices close
Keep the iPhone and Mac within 30 feet or 10 metres during the transfer. This removes distance as a likely cause if the Mac does not appear as a recipient.
- Turn on Wi-Fi on both devices.
- Turn on Bluetooth on both devices.
- Keep the devices within 30 feet or 10 metres.
- Open the harmless test image on the iPhone.
- Tap Share.
- Choose AirDrop.
- Select your nearby Mac as the recipient.
- Wait for the transferred image to appear in Downloads on the Mac.
The successful transfer proves that your capture path works before you visit the observation point. The image remains a harmless equipment test.
Why export the image?
The transferred image may use a different camera format. Preview can export it as a real JPEG with the exact project filename.
- Press Cmd+Space to open macOS search.
- Type Preview before pressing Enter to launch it.
- Open the transferred image from Downloads in Preview.
Exporting creates the JPEG
Changing a filename alone does not convert its image format. Preview's export process creates the JPEG required by this project.
- Click File in the Preview menu bar.
- Click Export.
- Choose JPEG from the Format menu.
- Enter test-photo as the image name.
- Choose the pothole-evidence-brief folder as the save location.
- Click Save.
Before you check the folder, which two files do you expect to find there?
- Return to the pothole-evidence-brief folder in Finder.
- Confirm that index.html appears beside test-photo.jpg.
Your safe capture pipeline is working. The folder now contains the reviewed Three.js starter plus one harmless test image from the borrowed iPhone.
Test photo missing?
- Confirm that Wi-Fi plus Bluetooth remain switched on for both devices.
- Move the devices within 30 feet or 10 metres before repeating the AirDrop transfer.
- Check Downloads for the original transferred image.
- Repeat the Preview export if the project folder contains the image under a different extension.
- Get help with finding or converting an AirDrop test photo on macOS.
Your controlled workspace is ready for evidence capture. Next up, you will collect six safe views before exposing the weakness of a single precise-looking estimate.
Capture the Pothole and Expose False Precision
Your Three.js starter is ready in the Browser pane. Now you need the street evidence that the model will eventually represent.
A single estimate from photographs can look more decisive than the images deserve. It hides disagreements between viewing angles.
In this step, get ready to:
- Capture six safe views of the street defect from the footpath.
- Privacy-review the photographs before transferring them with AirDrop.
- Create a single-value baseline from the six photographs in Claude Code.
Capture six safe views
Each view gives the iPhone Camera app a different angle on the same defect. Wider views show location context. Closer views show visible shape.
Keep the footpath boundary
This capture happens near traffic. Keep yourself on the footpath. Keep the ruler there too.
Skip any angle that requires entering the carriageway. A safe substitute records the limitation honestly.
- Position yourself on the footpath where the defect is visible with the surrounding street.
- Capture a front contextual view from the footpath.
- Capture a rear contextual view from another safe footpath position.
- Capture a left-side view from the footpath.
- Capture a right-side view from the footpath.
- Capture a close-up by using the iPhone zoom from the footpath.
- Capture a scale-reference view only if the 30 cm ruler can remain entirely off the carriageway.
- Substitute a second safe contextual view when the scale-reference view would be unsafe.
You should now see six safe photographs on the iPhone. Every image should have been captured without you or the ruler entering the carriageway.
Review and attach the photographs
Faces can identify people. Number plates can identify vehicles. A privacy review keeps the evidence focused on the street defect.
- Review each of the six photographs on the iPhone for faces, number plates, or private details.
- Delete any photograph that contains identifying details.
- Replace each deleted photograph with a safe view from the footpath.
- Review every replacement photograph for identifying details.
- Select the six reviewed photographs on the iPhone.
- Tap Share.
- Choose AirDrop.
- Select the nearby Mac as the recipient.
Where AirDrop saves the files
AirDrop may save the received photographs in Downloads on the Mac. The next check moves them into your existing project folder.
- In Finder, open the Downloads folder.
- Move the six received photographs into pothole-evidence-brief.
- Rename the six photographs to match their views using the filename guide below:
Filename guide
- Use context-front.jpg for the front contextual view.
- Use context-back.jpg for the rear contextual view.
- Use left.jpg for the left-side view.
- Use right.jpg for the right-side view.
- Use close-up.jpg for the close-up view.
- Use scale-reference.jpg for the ruler view or its safe contextual substitute.
- Check the pothole-evidence-brief folder for all six filenames.
You should see the six evidence photographs beside index.html and test-photo.jpg.
Attaching the photographs gives Claude access to them as conversation context. Your privacy review keeps identifying details out of that context.
- Switch back to the Code tab from earlier.
- Click the + control in the chat composer.
- Select all six evidence photographs from pothole-evidence-brief.
- Tell Claude which planned view any safe substitute replaced.
- Tell Claude that scale-reference.jpg contains no ruler if a ruler view was unsafe.
You should see all six filenames attached to the Claude Code conversation. The original test-photo.jpg should remain unattached.
Missing an attachment?
- Count the attachment filenames before submitting the baseline request.
- Click the + control again if fewer than six filenames appear.
- Select the missing privacy-reviewed photograph from pothole-evidence-brief.
- Help me work out why one of my local photographs is missing from the Claude Code attachment list.
Run the single-value baseline
A multimodal prompt lets Claude respond using the six attached photographs. This first request establishes one baseline value for each dimension.
Before you ask, do you think one length, one width, and one depth can preserve disagreements between six viewing angles? Hold that prediction.
- Submit this exact request in the Claude Code chat: Using only these six photos, give one best estimate in millimetres for the pothole's length, width, and depth.
Claude's reply should contain one length value, one width value, and one depth value in millimetres. The three numbers may look more certain than the photographs justify.
Why this is false precision
Each point value hides which photograph supports it. It also hides contradictions between viewing angles.
These numbers are unaudited baseline estimates. Their apparent certainty exceeds what the visible evidence can support.
- Save Claude's length estimate as Claude's length estimate in millimetres.
- Save Claude's width estimate as Claude's width estimate in millimetres.
- Save Claude's depth estimate as Claude's depth estimate in millimetres.
- Identify the estimate with the weakest visible support.
- Save your evidence note as the weakest-supported estimate and why.
Your baseline record now contains three point estimates plus the visible gap that makes one estimate especially uncertain. Treat every value as a starting point only.
You now have a privacy-reviewed six-photo set plus a baseline that exposes its own evidential gap. Next, you'll turn that gap into an auditable model.
Replace Guesswork with Ranges and a 3D Model
The three point estimates from the last step look decisive. Their precision hides measurement uncertainty between the six camera angles.
In this step, you will use Claude Code to audit each estimate against the photographs. You will then rebuild the starter as a Three.js evidence interface whose depression follows the range midpoints.
In this step, get ready to:
- Audit each dimension into an evidence-backed range with a confidence label.
- Replace the minimal starter with the evidence-builder foundation.
- Load the six photographs and verify the midpoint-based 3D depression.
Audit the point estimates
A range exposes the spread that one number conceals. Each bound needs visible support from the attached photographs.
- Return to the Code tab chat from the previous step.
- Ask Claude to audit the saved baseline estimates against all six attached photographs.
- Require a low-to-high range in millimetres for each dimension.
- Require Claude to identify the photographs supporting each lower bound.
- Require Claude to identify the photographs supporting each upper bound.
- Require Claude to describe contradictions between the views.
- Require Claude to identify any missing scale information.
Why Use Ranges?
Photographs can support approximate dimensions when scale and perspective are visible. Road inspectors can take direct measurements after a report reaches the responsible authority.
A range keeps your evidence useful while showing where the photographs stop supporting a claim. That boundary makes the final brief more credible.
- Tell Claude not to infer any depth hidden from every camera angle.
- Require one overall confidence rating of Low, Medium, or High.
- Send the audit request.
Claude's response should now separate visible support from uncertainty. You should see six range endpoints plus evidence notes and one confidence rating.
- Record the lower length bound here: audited length low in mm.
- Record the upper length bound here: audited length high in mm.
- Record the lower width bound here: audited width low in mm.
- Record the upper width bound here: audited width high in mm.
- Record the lower depth bound here: audited depth low in mm.
- Record the upper depth bound here: audited depth high in mm.
- Record the overall rating here: Low, Medium, or High.
- Record the supporting evidence and limitations here: evidence and limitation notes.
Build the evidence-builder foundation
The starter already proves that the browser can render a rotatable road patch. The next edit connects that patch to photographs and audited measurement ranges.
What Should the Foundation Contain?
- The existing import map stays pinned to Three.js 0.187.0.
- The interface contains the safety notice plus the case-detail controls.
- The photo control produces a six-image contact sheet in photo-preview.
- The measurement panel uses the six low and high input IDs from the full-code reference.
- The model renders inside model-view.
- The scenario cards plus the cost and report sections remain reserved for later steps.
- Ask Claude to replace the current contents of pothole-evidence-brief/index.html with the evidence-builder foundation described above.
- Tell Claude to use the complete foundation in the full-code reference below as its target.
- Send the build request.
Claude should present a proposed replacement for index.html in the Code tab. The file waits for your decision because the local session still uses Manual permission mode.
This review can feel fiddly because the replacement is much larger than the starter. Manual approval keeps the local file under your control.
- Review the proposed edit for pothole-evidence-brief/index.html.
- Approve the proposed edit from the Manual permission review.
Before you refresh the preview, which new evidence controls do you expect to see around the model?
- Return to the Browser pane from earlier.
- Refresh the existing index.html preview.
You should see the Pothole Evidence Builder header plus a red safety notice. Below them, you should see case details, a six-photo control, six measurement inputs, a confidence selector, evidence notes, and the 3D model.
Still Seeing the Flat Starter?
- Confirm that you approved the edit for pothole-evidence-brief/index.html in the Manual permission review.
- Refresh the existing Browser preview after the approved edit finishes.
- Compare the file with the full-code reference below if any input or model control is missing.
- Help me diagnose why my approved evidence-builder edit is missing from the Claude Desktop Browser pane.
Load the evidence and test the model
The model converts each low-to-high range into a midpoint. Those three midpoint values control the length, width, and depth of the tiled depression.
- Use the file picker beside Six safe photographs.
- Select context-front.jpg, context-back.jpg, left.jpg, right.jpg, close-up.jpg, and scale-reference.jpg.
Good, the six privacy-reviewed views now appear as a contact sheet. The model and its evidence can now be inspected on the same page.
- Enter audited length low in mm in Length low, mm.
- Enter audited length high in mm in Length high, mm.
- Enter audited width low in mm in Width low, mm.
- Enter audited width high in mm in Width high, mm.
- Enter audited depth low in mm in Depth low, mm.
- Enter audited depth high in mm in Depth high, mm.
The depression should change as each value arrives. Once all six fields contain numbers, the model status should list the three midpoint estimates.
- Select Low, Medium, or High in Overall confidence.
- Keep Low selected if scale-reference.jpg is a substitute contextual view without a safe ruler reference.
- Enter evidence and limitation notes in Evidence and limitations.
Before you move the model, do you expect its depression to match one exact photograph or communicate the midpoint proportions of your audited ranges?
- Drag across the model with the left mouse button to rotate the road surface.
- Use the mouse wheel to zoom closer to the depression.
- Compare the midpoint-based shape with the six photographs.
You should see the tiled road surface rotate and zoom while preserving the entered proportions. The page states that the model uses midpoint estimates and does not claim a survey or photogrammetric reconstruction.
Model Not Updating?
- Confirm that all six measurement fields contain numbers.
- Check that every lower and upper value sits in the matching length, width, or depth field.
- Refresh the Browser preview if the model canvas is blank after the approved file edit.
- Help me troubleshoot why the midpoint-based Three.js depression in model-view is not updating from the six measurement inputs.
✔️ Awesome, I've got everything!
- Confirm that Claude applied the approved changes to pothole-evidence-brief/index.html.
- Keep the Browser preview on the working midpoint-based model.
ⓧ I'd like to double check the full code
- Compare pothole-evidence-brief/index.html with this cumulative foundation.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pothole Evidence Builder</title>
<style>
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #eef2f7;
}
* { box-sizing: border-box; }
body { margin: 0; }
button, input, select, textarea { font: inherit; }
a { color: #174ea6; }
.page-header {
padding: 28px clamp(18px, 4vw, 56px);
background: #172033;
color: white;
}
.page-header h1 { margin: 0 0 8px; }
.page-header p { margin: 0; max-width: 76ch; color: #dbe5f5; }
.safety {
margin: 18px auto;
max-width: 1180px;
padding: 14px 16px;
border: 2px solid #b42318;
border-radius: 12px;
background: #fff4f2;
color: #7a271a;
}
main {
max-width: 1180px;
margin: 0 auto;
padding: 0 18px 36px;
display: grid;
gap: 18px;
}
.panel {
background: white;
border: 1px solid #d9e0ea;
border-radius: 14px;
padding: 18px;
box-shadow: 0 8px 24px rgba(23, 32, 51, 0.06);
}
.panel h2, .panel h3 { margin-top: 0; }
.two-column {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
gap: 18px;
}
.fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.field { display: grid; gap: 6px; }
.field.full { grid-column: 1 / -1; }
label { font-weight: 650; font-size: 0.9rem; }
input, select, textarea {
width: 100%;
border: 1px solid #aeb9c9;
border-radius: 9px;
padding: 9px 10px;
background: white;
color: #172033;
}
textarea { min-height: 82px; resize: vertical; }
.hint { margin: 6px 0 0; color: #526075; font-size: 0.86rem; }
#model-view {
min-height: 420px;
border: 1px solid #cbd5e1;
border-radius: 12px;
overflow: hidden;
background: #f5f7fb;
}
#model-view canvas { display: block; width: 100%; height: 100%; }
.model-status { margin: 10px 0 0; font-size: 0.88rem; color: #526075; }
.photo-grid, .report-photo-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin-top: 12px;
}
.photo-grid img, .report-photo-grid img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: 8px;
border: 1px solid #cbd5e1;
}
.scenario-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.scenario-card {
border: 1px solid #cbd5e1;
border-radius: 12px;
padding: 14px;
background: #fbfcfe;
}
.track {
position: relative;
height: 72px;
margin: 10px 0;
border-bottom: 6px solid #555d69;
overflow: hidden;
}
.track::after {
content: "";
position: absolute;
left: 48%;
bottom: -6px;
width: 22%;
height: 18px;
border-radius: 0 0 50% 50%;
background: #eef2f7;
border-bottom: 3px solid #555d69;
}
.traveller {
position: absolute;
left: 0;
bottom: 8px;
z-index: 2;
min-width: 58px;
padding: 5px 7px;
border-radius: 7px;
background: #175cd3;
color: white;
text-align: center;
font-size: 0.72rem;
font-weight: 800;
}
button {
border: 0;
border-radius: 9px;
padding: 10px 14px;
background: #175cd3;
color: white;
font-weight: 750;
cursor: pointer;
}
button.secondary { background: #344054; }
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
.limitations { border-left: 4px solid #f79009; padding-left: 12px; color: #7a2e0e; }
#report {
max-width: 1180px;
margin: 24px auto 40px;
padding: 20px;
background: white;
border: 1px solid #cbd5e1;
border-radius: 14px;
box-shadow: 0 8px 24px rgba(23, 32, 51, 0.08);
}
.report-header {
display: flex;
justify-content: space-between;
gap: 18px;
border-bottom: 3px solid #172033;
padding-bottom: 10px;
margin-bottom: 12px;
}
.report-header h1 { margin: 0 0 4px; font-size: 1.45rem; }
.report-header p { margin: 2px 0; }
.report-grid { display: grid; grid-template-columns: 1.05fr 1fr; gap: 16px; }
.report-section { break-inside: avoid; }
.report-section h2 { margin: 10px 0 6px; font-size: 1rem; }
.report-model { width: 100%; max-height: 205px; object-fit: cover; border-radius: 8px; border: 1px solid #cbd5e1; }
table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
th, td { border: 1px solid #cbd5e1; padding: 6px; text-align: left; vertical-align: top; }
th { background: #eef2f7; }
.report-note { font-size: 0.8rem; color: #475467; }
@media (max-width: 820px) {
.two-column, .report-grid, .scenario-grid { grid-template-columns: 1fr; }
.fields { grid-template-columns: 1fr; }
}
@page { size: A4 landscape; margin: 8mm; }
@media print {
body { background: white; font-size: 8.5pt; }
.editor-only { display: none !important; }
#report {
margin: 0;
padding: 0;
max-width: none;
border: 0;
border-radius: 0;
box-shadow: none;
}
.report-header h1 { font-size: 15pt; }
.report-grid { grid-template-columns: 1.05fr 1fr; gap: 8mm; }
.report-photo-grid { gap: 3px; }
.report-photo-grid img { max-height: 29mm; }
.report-model { max-height: 45mm; }
table { font-size: 7.5pt; }
th, td { padding: 3px; }
.report-section h2 { font-size: 9pt; margin: 5px 0 3px; }
a { color: black; text-decoration: none; }
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.187.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.187.0/examples/jsm/"
}
}
</script>
</head>
<body>
<header class="page-header editor-only">
<h1>Pothole Evidence Builder</h1>
<p>Turn safe street observations into a cautious, source-linked visual brief. The model and scenarios communicate estimates. They are not surveys, engineering calculations, or predictions.</p>
</header>
<div class="safety editor-only">
Safety rule: stay on the footpath. Never enter the carriageway to photograph, place a ruler, or measure a defect. Skip any angle or scale reference that cannot be captured safely.
</div>
<main class="editor-only">
<section class="panel">
<h2>1. Case details and photos</h2>
<div class="fields">
<div class="field">
<label for="case-title">Brief title</label>
<input id="case-title" value="Street defect evidence brief">
</div>
<div class="field">
<label for="location">Exact location and landmark</label>
<input id="location" placeholder="Street, nearest number or landmark">
</div>
<div class="field">
<label for="authority">Responsible authority</label>
<input id="authority" placeholder="Verified authority name">
</div>
<div class="field">
<label for="council-url">Official reporting URL</label>
<input id="council-url" type="url" placeholder="https://official-government-domain.example/report">
</div>
<div class="field full">
<label for="photos">Six safe photographs</label>
<input id="photos" type="file" accept="image/*" multiple>
<p class="hint">Choose up to six images. Avoid faces, number plates, and private information.</p>
<div id="photo-preview" class="photo-grid"></div>
</div>
</div>
</section>
<section class="two-column">
<div class="panel">
<h2>2. Audited measurement ranges</h2>
<div class="fields">
<div class="field"><label for="length-min">Length low, mm</label><input id="length-min" type="number" min="0"></div>
<div class="field"><label for="length-max">Length high, mm</label><input id="length-max" type="number" min="0"></div>
<div class="field"><label for="width-min">Width low, mm</label><input id="width-min" type="number" min="0"></div>
<div class="field"><label for="width-max">Width high, mm</label><input id="width-max" type="number" min="0"></div>
<div class="field"><label for="depth-min">Depth low, mm</label><input id="depth-min" type="number" min="0"></div>
<div class="field"><label for="depth-max">Depth high, mm</label><input id="depth-max" type="number" min="0"></div>
<div class="field full">
<label for="confidence">Overall confidence</label>
<select id="confidence">
<option>Low</option>
<option>Medium</option>
<option>High</option>
</select>
</div>
<div class="field full">
<label for="notes">Evidence and limitations</label>
<textarea id="notes" placeholder="Which photos support the bounds? What could not be seen or measured safely?"></textarea>
</div>
</div>
<p class="hint">The 3D surface uses range midpoints. Blank fields show a demo shape and must not appear in a final submission.</p>
</div>
<div class="panel">
<h2>3. Rotatable 3D model</h2>
<div id="model-view" aria-label="Rotatable Three.js pothole model"></div>
<p id="model-status" class="model-status"></p>
<p class="report-note">The model is proportional to entered midpoint estimates. It is not a survey or photogrammetric reconstruction.</p>
</div>
</section>
</main>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const byId = (id) => document.getElementById(id);
const modelContainer = byId('model-view');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(3.8, 3, 4.4);
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0xf5f7fb, 1);
modelContainer.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, -0.12, 0);
controls.minDistance = 2.4;
controls.maxDistance = 9;
scene.add(new THREE.HemisphereLight(0xffffff, 0x313844, 2.4));
const sun = new THREE.DirectionalLight(0xffffff, 2.2);
sun.position.set(3, 5, 4);
scene.add(sun);
const columns = 25;
const rows = 19;
const patchLength = 3.6;
const patchWidth = 2.8;
const cellLength = patchLength / columns;
const cellWidth = patchWidth / rows;
const tileGeometry = new THREE.BoxGeometry(cellLength * 0.97, 1, cellWidth * 0.97);
const tileMaterial = new THREE.MeshStandardMaterial({ color: 0x59616c, roughness: 0.96, metalness: 0 });
const tiles = [];
for (let row = 0; row < rows; row += 1) {
for (let column = 0; column < columns; column += 1) {
const x = -patchLength / 2 + cellLength / 2 + column * cellLength;
const z = -patchWidth / 2 + cellWidth / 2 + row * cellWidth;
const tile = new THREE.Mesh(tileGeometry, tileMaterial);
tile.userData.x = x;
tile.userData.z = z;
tile.position.set(x, -0.06, z);
tile.scale.y = 0.12;
scene.add(tile);
tiles.push(tile);
}
}
const measurementIds = ['length-min', 'length-max', 'width-min', 'width-max', 'depth-min', 'depth-max'];
const photoData = [];
function numericValue(id) {
const raw = byId(id).value.trim();
return raw === '' ? null : Number(raw);
}
function enteredRange(minId, maxId) {
const first = numericValue(minId);
const second = numericValue(maxId);
if (first === null || second === null || !Number.isFinite(first) || !Number.isFinite(second)) return null;
return { min: Math.min(first, second), max: Math.max(first, second) };
}
function modelRange(minId, maxId, demoMin, demoMax) {
return enteredRange(minId, maxId) || { min: demoMin, max: demoMax };
}
function midpoint(range) {
return (range.min + range.max) / 2;
}
function updateModel() {
const lengthRange = modelRange('length-min', 'length-max', 600, 800);
const widthRange = modelRange('width-min', 'width-max', 350, 500);
const depthRange = modelRange('depth-min', 'depth-max', 30, 55);
const unitMillimetres = 500;
const halfLength = Math.max(0.2, midpoint(lengthRange) / (2 * unitMillimetres));
const halfWidth = Math.max(0.16, midpoint(widthRange) / (2 * unitMillimetres));
const depth = Math.max(0.02, midpoint(depthRange) / unitMillimetres);
const baseThickness = 0.12;
for (const tile of tiles) {
const nx = tile.userData.x / halfLength;
const nz = tile.userData.z / halfWidth;
const radiusSquared = nx * nx + nz * nz;
const depression = radiusSquared < 1 ? depth * Math.pow(1 - radiusSquared, 1.6) : 0;
tile.scale.y = baseThickness;
tile.position.y = -depression - baseThickness / 2;
}
const complete = measurementIds.every((id) => numericValue(id) !== null);
byId('model-status').textContent = complete
? `Model uses midpoint estimates: ${Math.round(midpoint(lengthRange))} mm long, ${Math.round(midpoint(widthRange))} mm wide, ${Math.round(midpoint(depthRange))} mm deep.`
: 'Demo shape shown. Enter all six audited range values before using the brief.';
}
function resizeModel() {
const width = Math.max(modelContainer.clientWidth, 300);
const height = Math.max(modelContainer.clientHeight, 320);
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
renderer.setAnimationLoop(() => {
controls.update();
renderer.render(scene, camera);
});
function renderPhotos(container, data) {
container.replaceChildren();
if (data.length === 0) {
const note = document.createElement('p');
note.className = 'report-note';
note.textContent = 'No photographs added.';
container.appendChild(note);
return;
}
data.forEach((source, index) => {
const image = document.createElement('img');
image.src = source;
image.alt = `Street defect evidence photo ${index + 1}`;
container.appendChild(image);
});
}
byId('photos').addEventListener('change', async (event) => {
const files = Array.from(event.target.files).slice(0, 6);
const loaded = await Promise.all(files.map((file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', () => resolve(reader.result));
reader.addEventListener('error', () => reject(reader.error));
reader.readAsDataURL(file);
})));
photoData.splice(0, photoData.length, ...loaded);
renderPhotos(byId('photo-preview'), photoData);
});
measurementIds.forEach((id) => byId(id).addEventListener('input', () => {
updateModel();
}));
window.addEventListener('resize', resizeModel);
resizeModel();
updateModel();
renderPhotos(byId('photo-preview'), photoData);
</script>
</body>
</html>
That is the hard part done: your photographs now drive a cautious range-based model instead of a precise-looking guess. Next, you will compare how three road users move across separate illustrative scenarios.
Compare Three 30 km/h Vehicle Scenarios
Your Three.js model now turns audited ranges into a rotatable midpoint-based shape. The next goal is to show how three road users can experience different visual disturbances over that same shape.
In this step, Claude Code adds separate car, bicycle, and scooter demonstrations. A single shared visual would hide those differences.
In this step, get ready to:
- Add three separate 30 km/h scenario cards to the evidence builder.
- Run the car, bicycle, and scooter visuals.
- Keep every consequence description cautious and illustrative.
Add three scenario cards
Each scenario uses the midpoint of your entered depth range to calculate an on-screen movement. A separate display multiplier makes each demonstration visually distinct.
What is a display multiplier?
A display multiplier scales only the vertical movement shown by the animation. The three values make the visual comparison easier to read.
- The CAR card uses data-factor="0.75".
- The BICYCLE card uses data-factor="1.25".
- The SCOOTER card uses data-factor="1.5".
These values are communication choices. They are not vehicle-dynamics coefficients.
- Switch back to the Claude Code chat in the Code tab from earlier.
- Ask Claude to add the three scenario cards from the Complete Code to pothole-evidence-brief/index.html while preserving the existing measurement inputs and model.
- Review Claude's proposed scenario changes in the file comparison.
- Approve the file change through the Manual permission review.
- Switch back to the Browser pane from earlier.
You should see a section titled Illustrative 30 km/h scenarios. It contains separate cards for Car at 30 km/h, Bicycle at 30 km/h, and Scooter at 30 km/h.
Scenario cards missing?
- Confirm that Claude edited pothole-evidence-brief/index.html.
- Check that the file change was approved during the Manual permission review.
- Ask Claude to compare the current scenario section with the Complete Code while preserving the existing measurement inputs and Three.js model.
- Help me restore the missing scenario cards.
Run the three visuals
All three animations read the same midpoint depth. Their separate factors create different illustrative movements without calculating real vehicle behaviour.
- Re-enter all six audited range endpoints if the refreshed preview cleared them.
- Choose the six privacy-reviewed photographs again if the refreshed preview cleared them.
Before you start the comparison, which road-user label do you expect to show the largest vertical movement?
- Click Run visual on the car card.
You should see CAR travel across the first track. Its label dips over the illustrated depression.
- Click Run visual on the bicycle card.
You should see BICYCLE travel across the second track. Its factor produces a sharper illustrative disturbance.
- Click Run visual on the scooter card.
You should see SCOOTER travel across the third track. Its factor produces the strongest illustrative disturbance.
- Read the limitation text beneath the scenario cards.
What Does the Limitation Protect?
The page identifies the multipliers as choices for communicating relative disturbance. It states that they are not vehicle-dynamics coefficients.
The page makes no prediction of damage, injury, or crash probability. This boundary keeps the visuals honest.
Visuals not moving correctly?
- Confirm that each card contains a Run visual button.
- Check that all six measurement fields contain numeric values.
- Ask Claude to compare the .run-scenario event listener with the Complete Code.
- Help me debug the vehicle visuals.
Audit the claims and verify the comparison
Consequence wording shapes how a viewer interprets the scenarios. Cautious descriptions communicate possible disturbance without promising a real-world outcome.
- Confirm that the car description includes Possible tyre, wheel, or suspension disturbance.
- Confirm that the bicycle description includes Possible loss-of-control risk context.
- Confirm that the scooter description includes Possible fall-risk context.
- Search the three descriptions for any promise of damage or injury.
- Search the three descriptions for any promise of repair eligibility or crash probability.
- Ask Claude to restore the Complete Code wording if you find an unsupported promise.
Before the final run, do you expect the three labels to show the same movement or separate illustrative disturbances?
- Click Run visual on the car card.
- Click Run visual on the bicycle card.
- Click Run visual on the scooter card.
You should see CAR, BICYCLE, and SCOOTER travel across their separately labelled 30 km/h tracks. The three cards show distinct illustrative vertical disturbances from the same entered depth range.
That comparison is working. Your evidence builder now communicates three possible disturbance patterns without presenting them as engineering predictions.
✔️ Awesome, I've got everything!
Your three scenario cards work. Double-check that pothole-evidence-brief/index.html is saved.
ⓧ I'd like to double check the full code
The photographs, audited ranges, confidence label, limitations, and rotatable model should remain unchanged. Compare the scenario additions in pothole-evidence-brief/index.html with this checklist.
- Confirm that the page contains the heading Illustrative 30 km/h scenarios.
- Confirm that the CAR .scenario-card uses data-factor="0.75".
- Confirm that the BICYCLE .scenario-card uses data-factor="1.25".
- Confirm that the SCOOTER .scenario-card uses data-factor="1.5".
- Confirm that every card contains a .track element.
- Confirm that every card contains a .traveller element plus a .run-scenario button.
The shared animation handler should connect those cards to the audited depth range.
- Confirm that the module attaches click listeners with document.querySelectorAll('.run-scenario').
- Confirm that the handler gets the closest .scenario-card.
- Confirm that the handler reads card.dataset.factor.
- Confirm that the handler derives movement from modelRange('depth-min', 'depth-max', 30, 55).
- Confirm that the movement calculation uses midpoint(depthRange).
- Confirm that existing traveller animations are cancelled before traveller.animate() runs.
The visible limitation should identify the multipliers as communication choices. It should make no prediction of damage, injury, or crash probability.
Your model now compares three road-user scenarios while keeping every consequence claim cautious. Next up, you will turn the photographs, ranges, scenarios, and sources into a printable evidence brief.
Build the Evidence Brief and Find the Reporting Page
Your Three.js model now communicates the defect's estimated shape without presenting it as an exact reconstruction. The model still needs a verified location and sources before a road authority can act on it.
In this step, you will find the responsible authority's official reporting page. You will add source-linked cost context and turn the evidence builder into a printable brief.
In this step, get ready to:
- Verify the responsible road authority and its official pothole-reporting page.
- Collect local property-cost context and official national injury-cost context.
- Complete the evidence builder and save a one-page landscape PDF.
Find the official reporting page
A useful civic report must reach the authority responsible for the road. Claude Desktop can help with the search, but you remain responsible for checking the result before using it.
- Return to the Code chat from earlier.
- Start a request for the authority responsible for pothole reports in your city and country.
- Add the street description your street description.
- Add the nearest landmark your nearest landmark.
- Ask Claude to identify the responsible road authority.
- Ask Claude for the exact pothole-reporting page on an official government or council domain.
- Ask Claude to return the authority name and reporting URL in a small source table.
- Send the completed request.
Claude should return a likely authority and a direct reporting link. The next check establishes whether that result belongs in your evidence brief.
- Open the suggested reporting link in the Browser pane.
- Confirm that the domain belongs to the named government or council.
- Confirm that the page accepts reports for potholes or road defects.
- Ask Claude to correct its source table if either check fails.
- Keep the verified authority name and URL in the source table.
- Leave the reporting form unsubmitted.
Why verify the reporting page yourself?
Search results can surface contractors or general contact pages before the responsible authority's reporting form. Checking the domain and page purpose prevents your brief from directing readers to an unrelated service.
You are collecting a verified route for later submission. You keep control of every claim and attachment.
Research source-linked cost context
Cost figures provide context for why a defect deserves attention. Each figure needs a visible source because the brief does not predict that any specific loss will occur.
- Ask Claude to create a source table for current local car tyre or wheel repair ranges.
- Ask Claude to add a local bicycle property or repair range.
- Ask Claude to add a local scooter property or repair range.
- Require a retailer or repairer URL for every local range.
- Require an access date for every URL.
- Ask Claude to find an official national injury-cost source.
- Require the official figure and URL when an authoritative local source exists.
- Require the exact fallback Not quantified from an authoritative local source when no suitable source exists.
Claude's table gives you a research trail for the brief. Each row should contain a range or figure plus its source URL and access date.
- Open each local property source to confirm that it belongs to a retailer or repairer.
- Compare each recorded range with the values shown on its source page.
- Open the injury-cost source to confirm that it belongs to an official national body.
- Ask Claude to revise any row that fails your source check.
- Keep the verified table in the Code chat for the next substep.
What do these figures communicate?
The local ranges show the scale of property costs found on current source pages. The official injury figure provides national context when a suitable source exists.
Neither type of figure proves that this defect caused a loss. Your final brief keeps that boundary visible.
Finish and print the evidence brief
The final application turns your working model into a compact report. Its helper functions copy the entered values into a printable section and capture the current 3D canvas as an image.
- In the Code chat, ask Claude to update index.html into the complete evidence builder.
- Require the source-linked car, bicycle, scooter, and injury-cost fields from the target application.
- Require the Update brief and Print one-page brief controls.
- Require the printable report section and A4 landscape print rules.
- Require the report photo sheet and the report-model canvas snapshot.
- Require formatRange, moneyRange, validHttpUrl, updateLink, and updateReport from the target application.
- Tell Claude to preserve the existing model and three scenario cards.
- Send the completed request.
- Review Claude's proposed changes in the existing Manual-permission session.
- Confirm that the proposal only changes pothole-evidence-brief/index.html.
- Approve the reviewed change.
- Return to the Browser pane from earlier.
- Refresh the evidence builder.
You should see a new source-linked cost section beneath the scenario cards. A printable evidence brief should also appear below the editor.
How does the report stay synchronized?
- The range helpers turn each entered pair into readable low-to-high text.
- The URL helper accepts web links before placing them in the report.
- The report updater copies the current form values into the printable layout.
- The renderer snapshot stores the current model view inside report-model.
Missing the new report sections?
Confirm that Claude edited pothole-evidence-brief/index.html inside the selected local project folder. Reopen the same HTML path in the Browser pane if the preview still shows an older file.
Compare your file with the complete reference below if a section or helper is missing. Help me compare my evidence builder with the final target file.
✔️ Awesome, I've got everything!
Great. Save pothole-evidence-brief/index.html before entering the verified report details.
ⓧ I'd like to double check the full code
Compare your file with this complete final version. Every existing model and scenario section should remain present.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pothole Evidence Builder</title>
<style>
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #eef2f7;
}
* { box-sizing: border-box; }
body { margin: 0; }
button, input, select, textarea { font: inherit; }
a { color: #174ea6; }
.page-header {
padding: 28px clamp(18px, 4vw, 56px);
background: #172033;
color: white;
}
.page-header h1 { margin: 0 0 8px; }
.page-header p { margin: 0; max-width: 76ch; color: #dbe5f5; }
.safety {
margin: 18px auto;
max-width: 1180px;
padding: 14px 16px;
border: 2px solid #b42318;
border-radius: 12px;
background: #fff4f2;
color: #7a271a;
}
main {
max-width: 1180px;
margin: 0 auto;
padding: 0 18px 36px;
display: grid;
gap: 18px;
}
.panel {
background: white;
border: 1px solid #d9e0ea;
border-radius: 14px;
padding: 18px;
box-shadow: 0 8px 24px rgba(23, 32, 51, 0.06);
}
.panel h2, .panel h3 { margin-top: 0; }
.two-column {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
gap: 18px;
}
.fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.field { display: grid; gap: 6px; }
.field.full { grid-column: 1 / -1; }
label { font-weight: 650; font-size: 0.9rem; }
input, select, textarea {
width: 100%;
border: 1px solid #aeb9c9;
border-radius: 9px;
padding: 9px 10px;
background: white;
color: #172033;
}
textarea { min-height: 82px; resize: vertical; }
.hint { margin: 6px 0 0; color: #526075; font-size: 0.86rem; }
#model-view {
min-height: 420px;
border: 1px solid #cbd5e1;
border-radius: 12px;
overflow: hidden;
background: #f5f7fb;
}
#model-view canvas { display: block; width: 100%; height: 100%; }
.model-status { margin: 10px 0 0; font-size: 0.88rem; color: #526075; }
.photo-grid, .report-photo-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin-top: 12px;
}
.photo-grid img, .report-photo-grid img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: 8px;
border: 1px solid #cbd5e1;
}
.scenario-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.scenario-card {
border: 1px solid #cbd5e1;
border-radius: 12px;
padding: 14px;
background: #fbfcfe;
}
.track {
position: relative;
height: 72px;
margin: 10px 0;
border-bottom: 6px solid #555d69;
overflow: hidden;
}
.track::after {
content: "";
position: absolute;
left: 48%;
bottom: -6px;
width: 22%;
height: 18px;
border-radius: 0 0 50% 50%;
background: #eef2f7;
border-bottom: 3px solid #555d69;
}
.traveller {
position: absolute;
left: 0;
bottom: 8px;
z-index: 2;
min-width: 58px;
padding: 5px 7px;
border-radius: 7px;
background: #175cd3;
color: white;
text-align: center;
font-size: 0.72rem;
font-weight: 800;
}
button {
border: 0;
border-radius: 9px;
padding: 10px 14px;
background: #175cd3;
color: white;
font-weight: 750;
cursor: pointer;
}
button.secondary { background: #344054; }
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
.limitations { border-left: 4px solid #f79009; padding-left: 12px; color: #7a2e0e; }
#report {
max-width: 1180px;
margin: 24px auto 40px;
padding: 20px;
background: white;
border: 1px solid #cbd5e1;
border-radius: 14px;
box-shadow: 0 8px 24px rgba(23, 32, 51, 0.08);
}
.report-header {
display: flex;
justify-content: space-between;
gap: 18px;
border-bottom: 3px solid #172033;
padding-bottom: 10px;
margin-bottom: 12px;
}
.report-header h1 { margin: 0 0 4px; font-size: 1.45rem; }
.report-header p { margin: 2px 0; }
.report-grid { display: grid; grid-template-columns: 1.05fr 1fr; gap: 16px; }
.report-section { break-inside: avoid; }
.report-section h2 { margin: 10px 0 6px; font-size: 1rem; }
.report-model { width: 100%; max-height: 205px; object-fit: cover; border-radius: 8px; border: 1px solid #cbd5e1; }
table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
th, td { border: 1px solid #cbd5e1; padding: 6px; text-align: left; vertical-align: top; }
th { background: #eef2f7; }
.report-note { font-size: 0.8rem; color: #475467; }
@media (max-width: 820px) {
.two-column, .report-grid, .scenario-grid { grid-template-columns: 1fr; }
.fields { grid-template-columns: 1fr; }
}
@page { size: A4 landscape; margin: 8mm; }
@media print {
body { background: white; font-size: 8.5pt; }
.editor-only { display: none !important; }
#report {
margin: 0;
padding: 0;
max-width: none;
border: 0;
border-radius: 0;
box-shadow: none;
}
.report-header h1 { font-size: 15pt; }
.report-grid { grid-template-columns: 1.05fr 1fr; gap: 8mm; }
.report-photo-grid { gap: 3px; }
.report-photo-grid img { max-height: 29mm; }
.report-model { max-height: 45mm; }
table { font-size: 7.5pt; }
th, td { padding: 3px; }
.report-section h2 { font-size: 9pt; margin: 5px 0 3px; }
a { color: black; text-decoration: none; }
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.187.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.187.0/examples/jsm/"
}
}
</script>
</head>
<body>
<header class="page-header editor-only">
<h1>Pothole Evidence Builder</h1>
<p>Turn safe street observations into a cautious, source-linked visual brief. The model and scenarios communicate estimates. They are not surveys, engineering calculations, or predictions.</p>
</header>
<div class="safety editor-only">
Safety rule: stay on the footpath. Never enter the carriageway to photograph, place a ruler, or measure a defect. Skip any angle or scale reference that cannot be captured safely.
</div>
<main class="editor-only">
<section class="panel">
<h2>1. Case details and photos</h2>
<div class="fields">
<div class="field">
<label for="case-title">Brief title</label>
<input id="case-title" value="Street defect evidence brief">
</div>
<div class="field">
<label for="location">Exact location and landmark</label>
<input id="location" placeholder="Street, nearest number or landmark">
</div>
<div class="field">
<label for="authority">Responsible authority</label>
<input id="authority" placeholder="Verified authority name">
</div>
<div class="field">
<label for="council-url">Official reporting URL</label>
<input id="council-url" type="url" placeholder="https://official-government-domain.example/report">
</div>
<div class="field full">
<label for="photos">Six safe photographs</label>
<input id="photos" type="file" accept="image/*" multiple>
<p class="hint">Choose up to six images. Avoid faces, number plates, and private information.</p>
<div id="photo-preview" class="photo-grid"></div>
</div>
</div>
</section>
<section class="two-column">
<div class="panel">
<h2>2. Audited measurement ranges</h2>
<div class="fields">
<div class="field"><label for="length-min">Length low, mm</label><input id="length-min" type="number" min="0"></div>
<div class="field"><label for="length-max">Length high, mm</label><input id="length-max" type="number" min="0"></div>
<div class="field"><label for="width-min">Width low, mm</label><input id="width-min" type="number" min="0"></div>
<div class="field"><label for="width-max">Width high, mm</label><input id="width-max" type="number" min="0"></div>
<div class="field"><label for="depth-min">Depth low, mm</label><input id="depth-min" type="number" min="0"></div>
<div class="field"><label for="depth-max">Depth high, mm</label><input id="depth-max" type="number" min="0"></div>
<div class="field full">
<label for="confidence">Overall confidence</label>
<select id="confidence">
<option>Low</option>
<option>Medium</option>
<option>High</option>
</select>
</div>
<div class="field full">
<label for="notes">Evidence and limitations</label>
<textarea id="notes" placeholder="Which photos support the bounds? What could not be seen or measured safely?"></textarea>
</div>
</div>
<p class="hint">The 3D surface uses range midpoints. Blank fields show a demo shape and must not appear in a final submission.</p>
</div>
<div class="panel">
<h2>3. Rotatable 3D model</h2>
<div id="model-view" aria-label="Rotatable Three.js pothole model"></div>
<p id="model-status" class="model-status"></p>
</div>
</section>
<section class="panel">
<h2>4. Illustrative 30 km/h scenarios</h2>
<div class="scenario-grid">
<article class="scenario-card" data-factor="0.75">
<h3>Car at 30 km/h</h3>
<div class="track"><div class="traveller">CAR</div></div>
<p>Abrupt wheel drop and rise. Possible tyre, wheel, or suspension disturbance.</p>
<button type="button" class="run-scenario">Run visual</button>
</article>
<article class="scenario-card" data-factor="1.25">
<h3>Bicycle at 30 km/h</h3>
<div class="track"><div class="traveller">BICYCLE</div></div>
<p>Sharper steering and balance disturbance. Possible loss-of-control risk context.</p>
<button type="button" class="run-scenario">Run visual</button>
</article>
<article class="scenario-card" data-factor="1.5">
<h3>Scooter at 30 km/h</h3>
<div class="track"><div class="traveller">SCOOTER</div></div>
<p>Pronounced small-wheel disturbance. Possible fall-risk context.</p>
<button type="button" class="run-scenario">Run visual</button>
</article>
</div>
<p class="limitations">These animations use display multipliers chosen to communicate relative disturbance. They are not vehicle-dynamics coefficients and do not predict damage, injury, or crash probability.</p>
</section>
<section class="panel">
<h2>5. Source-linked cost context</h2>
<div class="fields">
<div class="field"><label for="currency">Currency symbol</label><input id="currency" placeholder="£, €, or $"></div>
<div class="field"><label for="car-low">Car property low</label><input id="car-low" type="number" min="0"></div>
<div class="field"><label for="car-high">Car property high</label><input id="car-high" type="number" min="0"></div>
<div class="field"><label for="car-source">Car cost source URL</label><input id="car-source" type="url"></div>
<div class="field"><label for="bike-low">Bicycle property low</label><input id="bike-low" type="number" min="0"></div>
<div class="field"><label for="bike-high">Bicycle property high</label><input id="bike-high" type="number" min="0"></div>
<div class="field"><label for="bike-source">Bicycle cost source URL</label><input id="bike-source" type="url"></div>
<div class="field"><label for="scooter-low">Scooter property low</label><input id="scooter-low" type="number" min="0"></div>
<div class="field"><label for="scooter-high">Scooter property high</label><input id="scooter-high" type="number" min="0"></div>
<div class="field"><label for="scooter-source">Scooter cost source URL</label><input id="scooter-source" type="url"></div>
<div class="field"><label for="injury-context">Official injury-cost context</label><input id="injury-context" placeholder="Amount or Not quantified"></div>
<div class="field"><label for="injury-source">Official injury source URL</label><input id="injury-source" type="url"></div>
</div>
<div class="actions">
<button id="update-brief" type="button">Update brief</button>
<button id="print-brief" type="button" class="secondary">Print one-page brief</button>
</div>
</section>
</main>
<section id="report" aria-label="Printable evidence brief">
<header class="report-header">
<div>
<h1 id="report-title">Street defect evidence brief</h1>
<p><strong>Location:</strong> <span id="report-location">Not entered</span></p>
<p><strong>Authority:</strong> <span id="report-authority">Not entered</span></p>
</div>
<div>
<p><strong>Prepared:</strong> <span id="report-date"></span></p>
<p><a id="report-council">Official reporting page not entered</a></p>
</div>
</header>
<div class="report-grid">
<div>
<section class="report-section">
<h2>Photographic evidence</h2>
<div id="report-photos" class="report-photo-grid"></div>
</section>
<section class="report-section">
<h2>Midpoint-based 3D communication model</h2>
<img id="report-model" class="report-model" alt="Snapshot of the rotatable pothole model">
<p class="report-note">The model is proportional to entered midpoint estimates. It is not a survey or photogrammetric reconstruction.</p>
</section>
</div>
<div>
<section class="report-section">
<h2>Estimated dimensions and confidence</h2>
<table>
<thead><tr><th>Measure</th><th>Range</th></tr></thead>
<tbody>
<tr><td>Length</td><td id="report-length">Not entered</td></tr>
<tr><td>Width</td><td id="report-width">Not entered</td></tr>
<tr><td>Depth</td><td id="report-depth">Not entered</td></tr>
<tr><td>Confidence</td><td id="report-confidence">Low</td></tr>
</tbody>
</table>
<p id="report-notes" class="report-note">Limitations not entered.</p>
</section>
<section class="report-section">
<h2>Illustrative 30 km/h scenarios</h2>
<table>
<tbody>
<tr><td>Car</td><td>Abrupt wheel drop and rise; possible tyre, wheel, or suspension disturbance.</td></tr>
<tr><td>Bicycle</td><td>Sharper steering and balance disturbance; possible loss-of-control risk context.</td></tr>
<tr><td>Scooter</td><td>Pronounced small-wheel disturbance; possible fall-risk context.</td></tr>
</tbody>
</table>
<p class="report-note">Illustrative communication only. No damage, injury, or crash probability is predicted.</p>
</section>
<section class="report-section">
<h2>Source-linked cost context</h2>
<table>
<thead><tr><th>Context</th><th>Range or figure</th><th>Source</th></tr></thead>
<tbody>
<tr><td>Car property</td><td id="report-car-cost">Not researched</td><td><a id="report-car-source">Not supplied</a></td></tr>
<tr><td>Bicycle property</td><td id="report-bike-cost">Not researched</td><td><a id="report-bike-source">Not supplied</a></td></tr>
<tr><td>Scooter property</td><td id="report-scooter-cost">Not researched</td><td><a id="report-scooter-source">Not supplied</a></td></tr>
<tr><td>Official injury context</td><td id="report-injury-cost">Not quantified</td><td><a id="report-injury-source">Not supplied</a></td></tr>
</tbody>
</table>
<p class="report-note">Retail prices and official injury-cost figures are context, not a prediction of loss or compensation.</p>
</section>
</div>
</div>
</section>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const byId = (id) => document.getElementById(id);
const modelContainer = byId('model-view');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(3.8, 3, 4.4);
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0xf5f7fb, 1);
modelContainer.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, -0.12, 0);
controls.minDistance = 2.4;
controls.maxDistance = 9;
scene.add(new THREE.HemisphereLight(0xffffff, 0x313844, 2.4));
const sun = new THREE.DirectionalLight(0xffffff, 2.2);
sun.position.set(3, 5, 4);
scene.add(sun);
const columns = 25;
const rows = 19;
const patchLength = 3.6;
const patchWidth = 2.8;
const cellLength = patchLength / columns;
const cellWidth = patchWidth / rows;
const tileGeometry = new THREE.BoxGeometry(cellLength * 0.97, 1, cellWidth * 0.97);
const tileMaterial = new THREE.MeshStandardMaterial({ color: 0x59616c, roughness: 0.96, metalness: 0 });
const tiles = [];
for (let row = 0; row < rows; row += 1) {
for (let column = 0; column < columns; column += 1) {
const x = -patchLength / 2 + cellLength / 2 + column * cellLength;
const z = -patchWidth / 2 + cellWidth / 2 + row * cellWidth;
const tile = new THREE.Mesh(tileGeometry, tileMaterial);
tile.userData.x = x;
tile.userData.z = z;
tile.position.set(x, -0.06, z);
tile.scale.y = 0.12;
scene.add(tile);
tiles.push(tile);
}
}
const measurementIds = ['length-min', 'length-max', 'width-min', 'width-max', 'depth-min', 'depth-max'];
const photoData = [];
function numericValue(id) {
const raw = byId(id).value.trim();
return raw === '' ? null : Number(raw);
}
function enteredRange(minId, maxId) {
const first = numericValue(minId);
const second = numericValue(maxId);
if (first === null || second === null || !Number.isFinite(first) || !Number.isFinite(second)) return null;
return { min: Math.min(first, second), max: Math.max(first, second) };
}
function modelRange(minId, maxId, demoMin, demoMax) {
return enteredRange(minId, maxId) || { min: demoMin, max: demoMax };
}
function midpoint(range) {
return (range.min + range.max) / 2;
}
function updateModel() {
const lengthRange = modelRange('length-min', 'length-max', 600, 800);
const widthRange = modelRange('width-min', 'width-max', 350, 500);
const depthRange = modelRange('depth-min', 'depth-max', 30, 55);
const unitMillimetres = 500;
const halfLength = Math.max(0.2, midpoint(lengthRange) / (2 * unitMillimetres));
const halfWidth = Math.max(0.16, midpoint(widthRange) / (2 * unitMillimetres));
const depth = Math.max(0.02, midpoint(depthRange) / unitMillimetres);
const baseThickness = 0.12;
for (const tile of tiles) {
const nx = tile.userData.x / halfLength;
const nz = tile.userData.z / halfWidth;
const radiusSquared = nx * nx + nz * nz;
const depression = radiusSquared < 1 ? depth * Math.pow(1 - radiusSquared, 1.6) : 0;
tile.scale.y = baseThickness;
tile.position.y = -depression - baseThickness / 2;
}
const complete = measurementIds.every((id) => numericValue(id) !== null);
byId('model-status').textContent = complete
? `Model uses midpoint estimates: ${Math.round(midpoint(lengthRange))} mm long, ${Math.round(midpoint(widthRange))} mm wide, ${Math.round(midpoint(depthRange))} mm deep.`
: 'Demo shape shown. Enter all six audited range values before using the brief.';
}
function resizeModel() {
const width = Math.max(modelContainer.clientWidth, 300);
const height = Math.max(modelContainer.clientHeight, 320);
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
renderer.setAnimationLoop(() => {
controls.update();
renderer.render(scene, camera);
});
function renderPhotos(container, data) {
container.replaceChildren();
if (data.length === 0) {
const note = document.createElement('p');
note.className = 'report-note';
note.textContent = 'No photographs added.';
container.appendChild(note);
return;
}
data.forEach((source, index) => {
const image = document.createElement('img');
image.src = source;
image.alt = `Street defect evidence photo ${index + 1}`;
container.appendChild(image);
});
}
byId('photos').addEventListener('change', async (event) => {
const files = Array.from(event.target.files).slice(0, 6);
const loaded = await Promise.all(files.map((file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', () => resolve(reader.result));
reader.addEventListener('error', () => reject(reader.error));
reader.readAsDataURL(file);
})));
photoData.splice(0, photoData.length, ...loaded);
renderPhotos(byId('photo-preview'), photoData);
renderPhotos(byId('report-photos'), photoData);
updateReport();
});
function formatRange(minId, maxId) {
const range = enteredRange(minId, maxId);
return range ? `${Math.round(range.min)} to ${Math.round(range.max)} mm` : 'Not entered';
}
function moneyRange(lowId, highId) {
const low = numericValue(lowId);
const high = numericValue(highId);
if (low === null || high === null) return 'Not researched';
const symbol = byId('currency').value.trim();
return `${symbol}${Math.min(low, high).toLocaleString()} to ${symbol}${Math.max(low, high).toLocaleString()}`;
}
function validHttpUrl(value) {
try {
const url = new URL(value);
return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : null;
} catch {
return null;
}
}
function updateLink(id, rawUrl, emptyText) {
const link = byId(id);
const safeUrl = validHttpUrl(rawUrl.trim());
if (safeUrl) {
link.href = safeUrl;
link.target = '_blank';
link.rel = 'noreferrer';
link.textContent = 'Open source';
} else {
link.removeAttribute('href');
link.removeAttribute('target');
link.removeAttribute('rel');
link.textContent = emptyText;
}
}
function updateReport() {
byId('report-title').textContent = byId('case-title').value.trim() || 'Street defect evidence brief';
byId('report-location').textContent = byId('location').value.trim() || 'Not entered';
byId('report-authority').textContent = byId('authority').value.trim() || 'Not entered';
byId('report-date').textContent = new Date().toLocaleDateString();
byId('report-length').textContent = formatRange('length-min', 'length-max');
byId('report-width').textContent = formatRange('width-min', 'width-max');
byId('report-depth').textContent = formatRange('depth-min', 'depth-max');
byId('report-confidence').textContent = byId('confidence').value;
byId('report-notes').textContent = byId('notes').value.trim() || 'Limitations not entered.';
byId('report-car-cost').textContent = moneyRange('car-low', 'car-high');
byId('report-bike-cost').textContent = moneyRange('bike-low', 'bike-high');
byId('report-scooter-cost').textContent = moneyRange('scooter-low', 'scooter-high');
byId('report-injury-cost').textContent = byId('injury-context').value.trim() || 'Not quantified';
const council = byId('report-council');
const councilUrl = validHttpUrl(byId('council-url').value.trim());
if (councilUrl) {
council.href = councilUrl;
council.target = '_blank';
council.rel = 'noreferrer';
council.textContent = 'Open official reporting page';
} else {
council.removeAttribute('href');
council.textContent = 'Official reporting page not entered';
}
updateLink('report-car-source', byId('car-source').value, 'Not supplied');
updateLink('report-bike-source', byId('bike-source').value, 'Not supplied');
updateLink('report-scooter-source', byId('scooter-source').value, 'Not supplied');
updateLink('report-injury-source', byId('injury-source').value, 'Not supplied');
renderPhotos(byId('report-photos'), photoData);
renderer.render(scene, camera);
byId('report-model').src = renderer.domElement.toDataURL('image/png');
}
document.querySelectorAll('.run-scenario').forEach((button) => {
button.addEventListener('click', () => {
const card = button.closest('.scenario-card');
const traveller = card.querySelector('.traveller');
const factor = Number(card.dataset.factor);
const depthRange = modelRange('depth-min', 'depth-max', 30, 55);
const drop = Math.max(8, Math.min(44, midpoint(depthRange) * factor * 0.5));
traveller.getAnimations().forEach((animation) => animation.cancel());
traveller.animate([
{ left: '0%', transform: 'translateY(0)' },
{ left: '40%', transform: 'translateY(0)' },
{ left: '52%', transform: `translateY(${drop}px)` },
{ left: '64%', transform: 'translateY(-8px)' },
{ left: '82%', transform: 'translateY(0)' }
], { duration: 2200, easing: 'ease-in-out' });
});
});
measurementIds.forEach((id) => byId(id).addEventListener('input', () => {
updateModel();
updateReport();
}));
document.querySelectorAll('input:not(#photos), select, textarea').forEach((control) => {
control.addEventListener('input', updateReport);
});
byId('update-brief').addEventListener('click', () => {
updateModel();
updateReport();
byId('report').scrollIntoView({ behavior: 'smooth' });
});
byId('print-brief').addEventListener('click', () => {
updateModel();
updateReport();
setTimeout(() => window.print(), 100);
});
window.addEventListener('resize', resizeModel);
resizeModel();
updateModel();
renderPhotos(byId('photo-preview'), photoData);
renderPhotos(byId('report-photos'), photoData);
updateReport();
</script>
</body>
</html>
What should match?
Use this full-file reference to compare structure and identifiers. Keep every existing photo, measurement, model, and scenario feature while checking the final report additions.
Refreshing the page can clear browser-selected files. Re-enter the report values so the final snapshot reflects your verified evidence.
- Enter the verified street description and nearest landmark in the Exact location and landmark field.
- Enter the verified authority name in the Responsible authority field.
- Enter the verified government or council URL in the Official reporting URL field.
- Select context-front.jpg, context-back.jpg, left.jpg, right.jpg, close-up.jpg, and scale-reference.jpg in the Six safe photographs field.
- Click Update brief.
You should see the verified location and authority in the report header. You should also see six photographs in the contact sheet.
- Enter the local currency symbol in the Currency symbol field.
- Enter the verified car property range and its source URL.
- Enter the verified bicycle property range and its source URL.
- Enter the verified scooter property range and its source URL.
- Enter the official national figure in the Official injury-cost context field when a suitable source exists.
- Enter Not quantified from an authoritative local source when no suitable injury-cost source exists.
- Append each source access date to the Evidence and limitations field.
- Click Update brief.
You should see the property ranges and source links in the report table. The current 3D viewpoint should appear as a snapshot beside the six photographs.
Before you open print preview, do you expect every evidence section to fit on one landscape page?
- Review every photograph and written claim in the report.
- Confirm that the report still describes all three scenarios as illustrative.
- Confirm that the official reporting link opens the verified government or council page.
- Click Print one-page brief.
The print preview should show one A4 landscape evidence page. You should see six photos, the 3D snapshot, all three ranges, the confidence label, the scenarios, the source context, and the official reporting URL.
- Use the print dialog's PDF-saving option.
- Save the file as evidence-brief.pdf inside the pothole-evidence-brief folder.
- Leave the authority's reporting form unsubmitted until you have checked every claim and attachment.
Does the preview use more than one page?
Confirm that the print orientation is landscape. Use the print dialog's scaling control to fit the report onto one A4 sheet if your browser applies different default margins.
Check that exactly six photos are selected. Help me diagnose why my pothole evidence brief does not fit on one A4 landscape page.
That is the full evidence workflow complete. Your local brief now connects cautious measurements to a verified reporting route without presenting estimates as facts.
Secret mission
Stress-Test the Measurements
Capture a second safe reference. Use it to test every original range. Then record the result in a revised evidence brief without overstating precision.
Clean Up Your Resources
Clean Up Your Resources
Everything you created is stored locally on your Mac. Keeping these files creates no new ongoing project cost.
Resources you used:
- The local evidence builder at pothole-evidence-brief/index.html, which loads Three.js from a CDN.
- The six privacy-reviewed original evidence images stored inside pothole-evidence-brief/.
- The second reference image at pothole-evidence-brief/scale-reference-2.jpg.
- The harmless transfer test at pothole-evidence-brief/test-photo.jpg.
- The superseded first brief at pothole-evidence-brief/evidence-brief.pdf.
- The current revised brief at pothole-evidence-brief/evidence-brief-revised.pdf.
Keep everything running
No action is needed. Choose this option if you are still reviewing the evidence or waiting for a response from the responsible authority.
- Keep the pothole-evidence-brief folder in its current location on your Mac.
- Use pothole-evidence-brief/evidence-brief-revised.pdf as your current evidence brief.
- Retain pothole-evidence-brief/evidence-brief.pdf as a comparison with the revised evidence.
- Continue using the open local session in Claude Desktop while you review the rotatable model.
Pause - I'll come back to this later
This option stops the active local session while preserving every project file for later. Internet access lets the browser model load its CDN dependencies when you return.
- Quit Claude Desktop.
- Leave the pothole-evidence-brief folder in its current location.
- Select pothole-evidence-brief from the Code tab when you return.
- Open pothole-evidence-brief/index.html in the Browser pane to resume reviewing the model.
Delete - I don't want to use this again
Deleting this folder removes every local project photo plus both evidence briefs. Emptying the Bin makes that removal permanent, which protects location-sensitive evidence you no longer need.
- Quit Claude Desktop.
- Open Finder by clicking its icon in the macOS Dock.
- Locate the project folder by entering pothole-evidence-brief in Finder's search field.
- Move the pothole-evidence-brief folder to the Bin.
- Open the Bin from the macOS Dock.
- Confirm that pothole-evidence-brief is listed in the Bin.
- Empty the Bin to permanently remove the project folder.
- Search for pothole-evidence-brief again in Finder to verify the deletion.
You should see no matching project folder. Your existing Claude account remains unchanged.
Nice Work!
Nice Work!
You did it! Your local Pothole Evidence Builder now turns safe street observations into a cautious 3D model plus a source-linked evidence brief.
You've learned how to:
- Built a local browser evidence builder with Claude Code. Its Three.js scene turns six safe photographs plus audited measurement ranges into a rotatable midpoint-based pothole model.
- Replaced a precise-looking baseline estimate with evidence-backed ranges. Added confidence labels that expose contradictions plus missing scale information.
- Produced a printable one-page evidence brief with three illustrative 30 km/h vehicle scenarios. Added source-linked cost context plus an official reporting URL without automatic submission.
- Completed the optional Secret Mission by running a second-reference audit against every measurement bound. The revised brief records any range or confidence change without overstating precision.
Ready to quiz yourself?