Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 181 additions & 44 deletions app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,98 +11,235 @@ interface BracketParams {
ribbingThickness: number;
holeDiameter: number;
earWidth: number;
hasBottom: boolean;
bottomType: 'none' | 'solid' | 'lip';
lipSize?: number;
color: string;
}

// Initialize the preview
const canvas = document.getElementById("preview") as HTMLCanvasElement;
const updateBracket = setupPreview(canvas);

const controls = document.querySelector<HTMLFormElement>("#controls");
if (!controls) throw new Error("Could not find controls form element");

// Get all range inputs
const inputs = Array.from(controls?.querySelectorAll<HTMLInputElement>("input") ?? []).filter(input => !input.classList.contains('value-display'));
// todo - I have a tip somewhere on an easy way to split this into two arrays
const displayInputs = Array.from(controls?.querySelectorAll<HTMLInputElement>("input") ?? []).filter(input => input.classList.contains('value-display'));
const inputs = Array.from(controls.querySelectorAll<HTMLInputElement>("input"));
const rangeInputs = inputs.filter(input => input.type === 'range');
const valueInputs = inputs.filter(input => input.classList.contains('value-display'));
const bottomTypeSelect = document.getElementById('bottomType') as HTMLSelectElement;
const lipSizeContainer = document.getElementById('lipSizeContainer') as HTMLDivElement;
const lipSizeInput = document.getElementById('lipSize') as HTMLInputElement;
const widthInput = document.getElementById('width') as HTMLInputElement;
const heightInput = document.getElementById('height') as HTMLInputElement;

// Connect range inputs with their corresponding value display inputs
rangeInputs.forEach(rangeInput => {
const valueInput = document.getElementById(`${rangeInput.id}Value`) as HTMLInputElement;
if (valueInput) {
// Update value display when range changes
rangeInput.addEventListener('input', () => {
valueInput.value = rangeInput.value;
});

// Update range when value display changes
valueInput.addEventListener('input', () => {
rangeInput.value = valueInput.value;
// Trigger change event on the range input to update the model
rangeInput.dispatchEvent(new Event('input', { bubbles: true }));
});

// Also handle change event (when user finishes typing)
valueInput.addEventListener('change', () => {
rangeInput.value = valueInput.value;
// Trigger change event on the range input to update the model
rangeInput.dispatchEvent(new Event('input', { bubbles: true }));
});
}
});

// Update the lip size max value based on current dimensions
function updateLipSizeMax() {
const width = parseFloat(widthInput.value);
const height = parseFloat(heightInput.value);

// Allow the lip to cover almost the entire side, leaving a 2mm minimum opening
const minOpeningSize = 2; // minimum opening size in mm
const maxWidth = Math.max(width - minOpeningSize, 1);
const maxHeight = Math.max(height - minOpeningSize, 1);

// Set max to half the max width/height to allow for the lip on both sides
const maxLipSize = Math.min(maxWidth / 2, maxHeight / 2);

lipSizeInput.max = maxLipSize.toString();

// Ensure current value doesn't exceed new max
if (parseFloat(lipSizeInput.value) > maxLipSize) {
lipSizeInput.value = maxLipSize.toString();
}

// Update the display
const lipSizeDisplay = document.getElementById('lipSizeValue') as HTMLInputElement;
if (lipSizeDisplay) {
lipSizeDisplay.value = lipSizeInput.value;
}
}

// Show/hide lip size control based on bottom type
function updateLipSizeVisibility() {
if (bottomTypeSelect.value === 'lip') {
lipSizeContainer.style.display = 'flex';
updateLipSizeMax();
} else {
lipSizeContainer.style.display = 'none';
}
}

function parseFormData(data: FormData) {
const params: Record<string, any> = {};
for(const [key, value] of data.entries()) {
// First see if it's a checkbox
if(value === "on") {
params[key] = true;
// Handle bottomType as a string
if(key === 'bottomType') {
params[key] = value.toString();
} else {
const maybeNumber = parseFloat(value);
params[key] = isNaN(maybeNumber) ? value : maybeNumber;
// First see if it's a checkbox
if(value === "on") {
params[key] = true;
} else {
const maybeNumber = parseFloat(value.toString());
params[key] = isNaN(maybeNumber) ? value.toString() : maybeNumber;
}
}
}
return params as BracketParams;
}


function displayValues(params: BracketParams) {
for(const input of inputs) {
console.log(input);
const label = input.nextElementSibling as HTMLDivElement;
const unit = input.getAttribute("data-unit") ?? 'mm';
if(label && label.classList.contains('value-display')) {
label.value = `${input.value}`;
// Update all input fields based on params
for (const key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
const value = params[key as keyof BracketParams];
if (value !== undefined) {
const input = document.getElementById(key) as HTMLInputElement | null;
if (input) {
input.value = value.toString();

// Also update the corresponding value display
const valueDisplay = document.getElementById(`${key}Value`) as HTMLInputElement | null;
if (valueDisplay) {
valueDisplay.value = value.toString();
}
}
}
}
}

// Also pop the color on the root so we can use in css
document.documentElement.style.setProperty('--color', params.color);
}

function handleInput(e: Event) {
// If someone types into a valueDisplay, update the input
if(e.target.classList.contains('value-display')) {
const input = e.target.previousElementSibling as HTMLInputElement;
input.value = e.target.value;
if (params.color) {
document.documentElement.style.setProperty('--color', params.color);
}
const data = new FormData(controls);
const params = parseFormData(data);
displayValues(params);
updateBracket(params);
}

function updateUrl() {
// Ensure controls exists
if (!controls) return;

const data = new FormData(controls);
const url = new URLSearchParams(data);
history.pushState({}, '', `?${url.toString()}`);
const urlParams = new URLSearchParams();

// Convert FormData to URLSearchParams
for (const [key, value] of data.entries()) {
urlParams.append(key, value.toString());
}

history.pushState({}, '', `?${urlParams.toString()}`);
}


controls.addEventListener("input", handleInput);
controls.addEventListener("change", updateUrl);

// On page load, check if there is a url param and parse it
function restoreState() {

const url = new URLSearchParams(window.location.search);
const params = {
defaultParams,
...parseFormData(url)
}
// Merge in any defaults
const urlParams = Object.fromEntries(url.entries());

// Safe access helper function
const safeParseFloat = (value: string | undefined, defaultValue: number): number => {
if (value === undefined) return defaultValue;
const parsed = parseFloat(value);
return isNaN(parsed) ? defaultValue : parsed;
};

const safeParseInt = (value: string | undefined, defaultValue: number): number => {
if (value === undefined) return defaultValue;
const parsed = parseInt(value);
return isNaN(parsed) ? defaultValue : parsed;
};

const safeString = (value: string | undefined, defaultValue: string): string => {
return value !== undefined ? value : defaultValue;
};

const params: BracketParams = {
...defaultParams,
width: safeParseFloat(urlParams.width, defaultParams.width),
depth: safeParseFloat(urlParams.depth, defaultParams.depth),
height: safeParseFloat(urlParams.height, defaultParams.height),
bracketThickness: safeParseFloat(urlParams.bracketThickness, defaultParams.bracketThickness),
ribbingCount: safeParseInt(urlParams.ribbingCount, defaultParams.ribbingCount),
ribbingThickness: safeParseFloat(urlParams.ribbingThickness, defaultParams.ribbingThickness),
holeDiameter: safeParseFloat(urlParams.holeDiameter, defaultParams.holeDiameter),
earWidth: safeParseFloat(urlParams.earWidth, defaultParams.earWidth),
bottomType: (urlParams.bottomType as 'none' | 'solid' | 'lip' | undefined) || defaultParams.bottomType,
color: safeString(urlParams.color, defaultParams.color || '#44ff00'),
lipSize: safeParseFloat(urlParams.lipSize, defaultParams.lipSize || 10)
};

// Restore any params from the URL
for(const [key, value] of Object.entries(params)) {
const input = document.getElementById(key) as HTMLInputElement;
const input = document.getElementById(key) as HTMLInputElement | null;
if(input) {
input.value = value.toString();
}
}

// Update the model with initial parameters
updateBracket(params);
displayValues(params);

// trigger an input event to update the values
const event = new Event('input', { bubbles: true });
controls.dispatchEvent(event);
if (controls) {
controls.dispatchEvent(event);
}
}


// On page load, restore state
restoreState();
updateLipSizeVisibility();

// Add event listeners
function handleFormUpdate() {
if (!controls) return;

const params = parseFormData(new FormData(controls));
updateBracket(params);
displayValues(params);
updateUrl();

if (params.bottomType === 'lip') {
updateLipSizeMax();
}
}

controls.addEventListener("input", handleFormUpdate);
controls.addEventListener("change", handleFormUpdate);

// Add additional listeners for width and height changes
widthInput.addEventListener("input", updateLipSizeMax);
heightInput.addEventListener("input", updateLipSizeMax);
bottomTypeSelect.addEventListener("change", updateLipSizeVisibility);

const exportButton = document.getElementById("export-button") as HTMLButtonElement;
exportButton.addEventListener("click", async () => {
if (!exportButton) throw new Error("Could not find export button");
exportButton.addEventListener("click", async () => {
const params = parseFormData(new FormData(controls));
const model = createBracket(params);
const dimensions = `${params.width}x${params.depth}x${params.height}`;
Expand Down
31 changes: 28 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -194,17 +194,42 @@ <h1>Bracket.Engineer</h1>
/>
</div>

<div class="control-group" id="lipSizeContainer" style="display: none;">
<label for="lipSize">Lip Size</label>
<input
type="range"
id="lipSize"
name="lipSize"
min="2"
max="20"
step="0.5"
value="4"
/>
<input
type="number"
id="lipSizeValue"
class="value-display"
value="4"
/>
</div>

<div class="control-group collection">
<div>
<input type="checkbox" id="hasBottom" name="hasBottom" />
<label for="hasBottom">Has Bottom</label>
<label for="bottomType">Bottom Type</label>
<select id="bottomType" name="bottomType">
<option value="none">None</option>
<option value="solid">Solid</option>
<option value="lip">Lip</option>
</select>
</div>
<div>
<label for="hasBottom">color</label>
<label for="color">Color</label>
<input type="color" id="color" name="color" value="#44ff00" />
</div>
<button type="button" id="export-button">Export 3MF</button>
</div>


</form>
</div>

Expand Down
12 changes: 10 additions & 2 deletions preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ interface BracketParams {
depth: number;
height: number;
holeDiameter: number;
holeOffset: number;
earWidth: number;
bracketThickness: number;
ribbingThickness: number;
ribbingCount: number;
bottomType: 'none' | 'solid' | 'lip';
lipSize?: number;
color?: string;
}


Expand Down Expand Up @@ -105,7 +110,10 @@ export function setupPreview(canvas: HTMLCanvasElement, onParamsChange?: (params
bracketMesh.geometry.dispose();
}

material.color.set(params.color);
// Check if color is defined before setting it
if (params.color) {
material.color.set(params.color);
}

// Create new bracket
const bracket = createBracket(params);
Expand Down
Loading