Compare commits
4 Commits
v0.25.6
...
mike/secon
Author | SHA1 | Date | |
---|---|---|---|
828a53f215 | |||
90af99abf4 | |||
3c5bf70269 | |||
24cd1b2ea5 |
29
README.md
29
README.md
@ -128,7 +128,18 @@ Before you submit a contribution PR to this repo, please ensure that:
|
||||
|
||||
## Release a new version
|
||||
|
||||
#### 1. Bump the versions by running `./make-release.sh` and create a Cut Release PR
|
||||
#### 1. Bump the versions by running `./make-release.sh`
|
||||
|
||||
The `./make-release.sh` script has git commands to pull main but to be sure you can run the following git commands to have a fresh `main` locally.
|
||||
|
||||
```
|
||||
git branch -D main
|
||||
git checkout main
|
||||
git pull origin
|
||||
./make-release.sh
|
||||
# Copy within the back ticks and paste the stdout of the change log
|
||||
git push --set-upstream origin <branch name created from ./make-release.sh>
|
||||
```
|
||||
|
||||
That will create the branch with the updated json files for you:
|
||||
- run `./make-release.sh` or `./make-release.sh patch` for a patch update;
|
||||
@ -137,28 +148,32 @@ That will create the branch with the updated json files for you:
|
||||
|
||||
After it runs you should just need the push the branch and open a PR.
|
||||
|
||||
**Important:** It needs to be prefixed with `Cut release v` to build in release mode and a few other things to test in the best context possible, the intent would be for instance to have `Cut release v1.2.3` for the `v1.2.3` release candidate.
|
||||
#### 2. Create a Cut Release PR
|
||||
|
||||
When you open the PR copy the change log from the output of the `./make-release.sh` script into the description of the PR.
|
||||
|
||||
**Important:** Pull request title needs to be prefixed with `Cut release v` to build in release mode and a few other things to test in the best context possible, the intent would be for instance to have `Cut release v1.2.3` for the `v1.2.3` release candidate.
|
||||
|
||||
The PR may then serve as a place to discuss the human-readable changelog and extra QA. The `make-release.sh` tool suggests a changelog for you too to be used as PR description, just make sure to delete lines that are not user facing.
|
||||
|
||||
#### 2. Smoke test artifacts from the Cut Release PR
|
||||
#### 3. Manually test artifacts from the Cut Release PR
|
||||
|
||||
The release builds can be find under the `artifact` zip, at the very bottom of the `ci` action page for each commit on this branch.
|
||||
|
||||
We don't have a strict process, but click around and check for anything obvious, posting results as comments in the Cut Release PR.
|
||||
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the Cut Release PR.
|
||||
|
||||
The other `ci` output in Cut Release PRs is `updater-test`, because we don't have a way to test this fully automated, we have a semi-automated process. Download updater-test zip file, install the app, run it, expect an updater prompt to a dummy v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows).
|
||||
|
||||
#### 3. Merge the Cut Release PR
|
||||
#### 4. Merge the Cut Release PR
|
||||
|
||||
This will kick the `create-release` action, that creates a _Draft_ release out of this Cut Release PR merge after less than a minute, with the new version as title and Cut Release PR as description.
|
||||
|
||||
|
||||
#### 4. Publish the release
|
||||
#### 5. Publish the release
|
||||
|
||||
Head over to https://github.com/KittyCAD/modeling-app/releases, the draft release corresponding to the merged Cut Release PR should show up at the top as _Draft_. Click on it, verify the content, and hit _Publish_.
|
||||
|
||||
#### 5. Profit
|
||||
#### 6. Profit
|
||||
|
||||
A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter.
|
||||
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
executorInputPath,
|
||||
} from './test-utils'
|
||||
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
|
||||
import { SETTINGS_FILE_NAME } from 'lib/constants'
|
||||
import {
|
||||
TEST_SETTINGS_KEY,
|
||||
TEST_SETTINGS_CORRUPTED,
|
||||
@ -343,7 +344,7 @@ test.describe('Testing settings', () => {
|
||||
|
||||
// Selectors and constants
|
||||
const errorHeading = page.getByRole('heading', {
|
||||
name: 'An unextected error occurred',
|
||||
name: 'An unexpected error occurred',
|
||||
})
|
||||
const projectDirLink = page.getByText('Loaded from')
|
||||
|
||||
@ -372,7 +373,7 @@ test.describe('Testing settings', () => {
|
||||
|
||||
// Selectors and constants
|
||||
const errorHeading = page.getByRole('heading', {
|
||||
name: 'An unextected error occurred',
|
||||
name: 'An unexpected error occurred',
|
||||
})
|
||||
const projectDirLink = page.getByText('Loaded from')
|
||||
|
||||
@ -384,6 +385,66 @@ test.describe('Testing settings', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// It was much easier to test the logo color than the background stream color.
|
||||
test(
|
||||
'user settings reload on external change, on project and modeling view',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const {
|
||||
electronApp,
|
||||
page,
|
||||
dir: projectDirName,
|
||||
} = await setupElectron({
|
||||
testInfo,
|
||||
appSettings: {
|
||||
app: {
|
||||
// Doesn't matter what you set it to. It will
|
||||
// default to 264.5
|
||||
themeColor: '0',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
const logoLink = page.getByTestId('app-logo')
|
||||
const projectDirLink = page.getByText('Loaded from')
|
||||
|
||||
await test.step('Wait for project view', async () => {
|
||||
await expect(projectDirLink).toBeVisible()
|
||||
await expect(logoLink).toHaveCSS('--primary-hue', '264.5')
|
||||
})
|
||||
|
||||
const changeColor = async (color: string) => {
|
||||
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
|
||||
let tomlStr = await fsp.readFile(tempSettingsFilePath, 'utf-8')
|
||||
tomlStr = tomlStr.replace(/(themeColor = ")[0-9]+(")/, `$1${color}$2`)
|
||||
await fsp.writeFile(tempSettingsFilePath, tomlStr)
|
||||
}
|
||||
|
||||
await test.step('Check color of logo changed', async () => {
|
||||
await changeColor('99')
|
||||
await expect(logoLink).toHaveCSS('--primary-hue', '99')
|
||||
})
|
||||
|
||||
await test.step('Check color of logo changed when in modeling view', async () => {
|
||||
await page.getByRole('button', { name: 'New project' }).click()
|
||||
await page.getByTestId('project-link').first().click()
|
||||
await page.getByRole('button', { name: 'Dismiss' }).click()
|
||||
await changeColor('58')
|
||||
await expect(logoLink).toHaveCSS('--primary-hue', '58')
|
||||
})
|
||||
|
||||
await test.step('Check going back to projects view still changes the color', async () => {
|
||||
await logoLink.click()
|
||||
await expect(projectDirLink).toBeVisible()
|
||||
await changeColor('21')
|
||||
await expect(logoLink).toHaveCSS('--primary-hue', '21')
|
||||
})
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`Closing settings modal should go back to the original file being viewed`,
|
||||
{ tag: '@electron' },
|
||||
|
1
interface.d.ts
vendored
1
interface.d.ts
vendored
@ -23,7 +23,6 @@ export interface IElectronAPI {
|
||||
callback: (eventType: string, path: string) => void
|
||||
) => void
|
||||
watchFileOff: (path: string) => void
|
||||
watchFileObliterate: () => void
|
||||
readFile: (path: string) => ReturnType<fs.readFile>
|
||||
writeFile: (
|
||||
path: string,
|
||||
|
@ -36,6 +36,7 @@
|
||||
"@xstate/inspect": "^0.8.0",
|
||||
"@xstate/react": "^4.1.1",
|
||||
"bonjour-service": "^1.2.1",
|
||||
"chokidar": "^4.0.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
|
@ -91,7 +91,7 @@ function CommandBarSelectionInput({
|
||||
<form id="arg-form" onSubmit={handleSubmit}>
|
||||
<label
|
||||
className={
|
||||
'relative flex items-center mx-4 my-4 ' +
|
||||
'relative flex flex-col mx-4 my-4 ' +
|
||||
(!hasSubmitted || canSubmitSelection || 'text-destroy-50')
|
||||
}
|
||||
>
|
||||
@ -100,13 +100,18 @@ function CommandBarSelectionInput({
|
||||
: `Please select ${
|
||||
arg.multiple ? 'one or more ' : 'one '
|
||||
}${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`}
|
||||
{arg.warningMessage && (
|
||||
<p className="text-warn-80 bg-warn-10 px-2 py-1 rounded-sm mt-3 mr-2 -mb-2 w-full text-sm cursor-default">
|
||||
{arg.warningMessage}
|
||||
</p>
|
||||
)}
|
||||
<input
|
||||
id="selection"
|
||||
name="selection"
|
||||
ref={inputRef}
|
||||
required
|
||||
placeholder="Select an entity with your mouse"
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-default"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Backspace') {
|
||||
stepBack()
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { trap } from 'lib/trap'
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||
import withBaseUrl from '../lib/withBaseURL'
|
||||
import React, { createContext, useEffect } from 'react'
|
||||
import React, { createContext, useEffect, useState } from 'react'
|
||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { toast } from 'react-hot-toast'
|
||||
@ -15,7 +16,6 @@ import {
|
||||
} from 'lib/theme'
|
||||
import decamelize from 'decamelize'
|
||||
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||
import {
|
||||
kclManager,
|
||||
@ -33,8 +33,14 @@ import {
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { Command } from 'lib/commandTypes'
|
||||
import { BaseUnit } from 'lib/settings/settingsTypes'
|
||||
import { saveSettings } from 'lib/settings/settingsUtils'
|
||||
import {
|
||||
saveSettings,
|
||||
loadAndValidateSettings,
|
||||
} from 'lib/settings/settingsUtils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { getAppSettingsFilePath } from 'lib/desktop'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -99,6 +105,9 @@ export const SettingsAuthProviderBase = ({
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const [settingsPath, setSettingsPath] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [settingsState, settingsSend, settingsActor] = useMachine(
|
||||
settingsMachine.provide({
|
||||
@ -191,7 +200,11 @@ export const SettingsAuthProviderBase = ({
|
||||
console.error('Error executing AST after settings change', e)
|
||||
}
|
||||
},
|
||||
persistSettings: ({ context }) => {
|
||||
persistSettings: ({ context, event }) => {
|
||||
// Without this, when a user changes the file, it'd
|
||||
// create a detection loop with the file-system watcher.
|
||||
if (event.doNotPersist) return
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
saveSettings(context, loadedProject?.project?.path)
|
||||
},
|
||||
@ -201,6 +214,23 @@ export const SettingsAuthProviderBase = ({
|
||||
)
|
||||
settingsStateRef = settingsState.context
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktop()) return
|
||||
getAppSettingsFilePath().then(setSettingsPath).catch(trap)
|
||||
}, [])
|
||||
|
||||
useFileSystemWatcher(
|
||||
async () => {
|
||||
const data = await loadAndValidateSettings(loadedProject?.project?.path)
|
||||
settingsSend({
|
||||
type: 'Set all settings',
|
||||
settings: data.settings,
|
||||
doNotPersist: true,
|
||||
})
|
||||
},
|
||||
settingsPath ? [settingsPath] : []
|
||||
)
|
||||
|
||||
// Add settings commands to the command bar
|
||||
// They're treated slightly differently than other commands
|
||||
// Because their state machine doesn't have a meaningful .nextEvents,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
|
||||
type Path = string
|
||||
@ -11,13 +12,13 @@ type Path = string
|
||||
// watcher.addListener(() => { ... }).
|
||||
|
||||
export const useFileSystemWatcher = (
|
||||
callback: (path: Path) => void,
|
||||
callback: (path: Path) => Promise<void>,
|
||||
dependencyArray: Path[]
|
||||
): void => {
|
||||
// Track a ref to the callback. This is how we get the callback updated
|
||||
// across the NodeJS<->Browser boundary.
|
||||
const callbackRef = useRef<{ fn: (path: Path) => void }>({
|
||||
fn: (_path) => {},
|
||||
const callbackRef = useRef<{ fn: (path: Path) => Promise<void> }>({
|
||||
fn: async (_path) => {},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@ -35,7 +36,9 @@ export const useFileSystemWatcher = (
|
||||
if (!isDesktop()) return
|
||||
|
||||
return () => {
|
||||
window.electron.watchFileObliterate()
|
||||
for (let path of dependencyArray) {
|
||||
window.electron.watchFileOff(path)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -46,6 +49,9 @@ export const useFileSystemWatcher = (
|
||||
]
|
||||
}
|
||||
|
||||
const hasDiff =
|
||||
difference(dependencyArray, dependencyArrayTracked)[0].length !== 0
|
||||
|
||||
// Removing 1 watcher at a time is only possible because in a filesystem,
|
||||
// a path is unique (there can never be two paths with the same name).
|
||||
// Otherwise we would have to obliterate() the whole list and reconstruct it.
|
||||
@ -53,6 +59,8 @@ export const useFileSystemWatcher = (
|
||||
// The hook is useless on web.
|
||||
if (!isDesktop()) return
|
||||
|
||||
if (!hasDiff) return
|
||||
|
||||
const [pathsRemoved, pathsRemaining] = difference(
|
||||
dependencyArrayTracked,
|
||||
dependencyArray
|
||||
@ -62,10 +70,10 @@ export const useFileSystemWatcher = (
|
||||
}
|
||||
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
|
||||
for (let path of pathsAdded) {
|
||||
window.electron.watchFileOn(path, (_eventType: string, path: Path) =>
|
||||
callbackRef.current.fn(path)
|
||||
)
|
||||
window.electron.watchFileOn(path, (_eventType: string, path: Path) => {
|
||||
callbackRef.current.fn(path).catch(reportRejection)
|
||||
})
|
||||
}
|
||||
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
|
||||
}, [difference(dependencyArray, dependencyArrayTracked)[0].length !== 0])
|
||||
}, [hasDiff])
|
||||
}
|
||||
|
@ -281,6 +281,8 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
multiple: true,
|
||||
required: true,
|
||||
skip: false,
|
||||
warningMessage:
|
||||
'Fillets cannot touch other fillets yet. This is under development.',
|
||||
},
|
||||
radius: {
|
||||
inputType: 'kcl',
|
||||
|
@ -113,6 +113,7 @@ export type CommandArgumentConfig<
|
||||
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
machineContext?: C
|
||||
) => boolean)
|
||||
warningMessage?: string
|
||||
skip?: boolean
|
||||
/** For showing a summary display of the current value, such as in
|
||||
* the command bar's header
|
||||
@ -189,6 +190,7 @@ export type CommandArgument<
|
||||
) => boolean)
|
||||
skip?: boolean
|
||||
machineActor?: Actor<T>
|
||||
warningMessage?: string
|
||||
/** For showing a summary display of the current value, such as in
|
||||
* the command bar's header
|
||||
*/
|
||||
|
@ -152,6 +152,7 @@ export function buildCommandArgument<
|
||||
skip: arg.skip,
|
||||
machineActor,
|
||||
valueSummary: arg.valueSummary,
|
||||
warningMessage: arg.warningMessage ?? '',
|
||||
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||
|
||||
if (arg.inputType === 'options') {
|
||||
|
@ -379,7 +379,7 @@ const getAppFolderName = () => {
|
||||
return window.electron.packageJson.name
|
||||
}
|
||||
|
||||
const getAppSettingsFilePath = async () => {
|
||||
export const getAppSettingsFilePath = async () => {
|
||||
const isTestEnv = window.electron.process.env.IS_PLAYWRIGHT === 'true'
|
||||
const testSettingsPath = window.electron.process.env.TEST_SETTINGS_FILE_KEY
|
||||
const appConfig = await window.electron.getPath('appData')
|
||||
|
@ -177,14 +177,14 @@ export async function loadAndValidateSettings(
|
||||
|
||||
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
|
||||
|
||||
const settings = createSettings()
|
||||
let settingsNext = createSettings()
|
||||
// Because getting the default directory is async, we need to set it after
|
||||
if (onDesktop) {
|
||||
settings.app.projectDirectory.default = await getInitialDefaultDir()
|
||||
}
|
||||
|
||||
setSettingsAtLevel(
|
||||
settings,
|
||||
settingsNext = setSettingsAtLevel(
|
||||
settingsNext,
|
||||
'user',
|
||||
configurationToSettingsPayload(appSettingsPayload)
|
||||
)
|
||||
@ -199,8 +199,8 @@ export async function loadAndValidateSettings(
|
||||
return Promise.reject(new Error('Invalid project settings'))
|
||||
|
||||
const projectSettingsPayload = projectSettings
|
||||
setSettingsAtLevel(
|
||||
settings,
|
||||
settingsNext = setSettingsAtLevel(
|
||||
settingsNext,
|
||||
'project',
|
||||
projectConfigurationToSettingsPayload(projectSettingsPayload)
|
||||
)
|
||||
@ -208,7 +208,7 @@ export async function loadAndValidateSettings(
|
||||
|
||||
// Return the settings object
|
||||
return {
|
||||
settings,
|
||||
settings: settingsNext,
|
||||
configuration: appSettingsPayload,
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export const settingsMachine = setup({
|
||||
types: {
|
||||
context: {} as ReturnType<typeof createSettings>,
|
||||
input: {} as ReturnType<typeof createSettings>,
|
||||
events: {} as
|
||||
events: {} as (
|
||||
| WildcardSetEvent<SettingsPaths>
|
||||
| SetEventTypes
|
||||
| {
|
||||
@ -34,7 +34,8 @@ export const settingsMachine = setup({
|
||||
type: 'Reset settings'
|
||||
level: SettingsLevel
|
||||
}
|
||||
| { type: 'Set all settings'; settings: typeof settings },
|
||||
| { type: 'Set all settings'; settings: typeof settings }
|
||||
) & { doNotPersist?: boolean },
|
||||
},
|
||||
actions: {
|
||||
setEngineTheme: () => {},
|
||||
|
@ -5,6 +5,7 @@ import os from 'node:os'
|
||||
import fsSync from 'node:fs'
|
||||
import packageJson from '../package.json'
|
||||
import { MachinesListing } from 'lib/machineManager'
|
||||
import chokidar from 'chokidar'
|
||||
|
||||
const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args)
|
||||
const save = (args: any) => ipcRenderer.invoke('dialog.showSaveDialog', args)
|
||||
@ -23,36 +24,21 @@ const isMac = os.platform() === 'darwin'
|
||||
const isWindows = os.platform() === 'win32'
|
||||
const isLinux = os.platform() === 'linux'
|
||||
|
||||
let fsWatchListeners = new Map<
|
||||
string,
|
||||
{
|
||||
watcher: fsSync.FSWatcher
|
||||
callback: (eventType: string, path: string) => void
|
||||
}
|
||||
>()
|
||||
let fsWatchListeners = new Map<string, ReturnType<typeof chokidar.watch>>()
|
||||
|
||||
const watchFileOn = (
|
||||
path: string,
|
||||
callback: (eventType: string, path: string) => void
|
||||
) => {
|
||||
const watcher = fsSync.watch(path)
|
||||
watcher.on('change', callback)
|
||||
fsWatchListeners.set(path, { watcher, callback })
|
||||
const watchFileOn = (path: string, callback: (path: string) => void) => {
|
||||
const watcherMaybe = fsWatchListeners.get(path)
|
||||
if (watcherMaybe) return
|
||||
const watcher = chokidar.watch(path)
|
||||
watcher.on('all', callback)
|
||||
fsWatchListeners.set(path, watcher)
|
||||
}
|
||||
const watchFileOff = (path: string) => {
|
||||
const entry = fsWatchListeners.get(path)
|
||||
if (!entry) return
|
||||
const { watcher, callback } = entry
|
||||
watcher.off('change', callback)
|
||||
watcher.close()
|
||||
const watcher = fsWatchListeners.get(path)
|
||||
if (!watcher) return
|
||||
watcher.unwatch(path)
|
||||
fsWatchListeners.delete(path)
|
||||
}
|
||||
const watchFileObliterate = () => {
|
||||
for (let [pathAsKey] of fsWatchListeners) {
|
||||
watchFileOff(pathAsKey)
|
||||
}
|
||||
fsWatchListeners = new Map()
|
||||
}
|
||||
const readFile = (path: string) => fs.readFile(path, 'utf-8')
|
||||
// It seems like from the node source code this does not actually block but also
|
||||
// don't trust me on that (jess).
|
||||
@ -103,7 +89,6 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
// exported.
|
||||
watchFileOn,
|
||||
watchFileOff,
|
||||
watchFileObliterate,
|
||||
readFile,
|
||||
writeFile,
|
||||
exists,
|
||||
|
@ -176,7 +176,7 @@ const Home = () => {
|
||||
|
||||
// Re-read projects listing if the projectDir has any updates.
|
||||
useFileSystemWatcher(
|
||||
() => {
|
||||
async () => {
|
||||
setProjectsLoaderTrigger(projectsLoaderTrigger + 1)
|
||||
},
|
||||
projectsDir ? [projectsDir] : []
|
||||
|
@ -21,7 +21,7 @@ use crate::{
|
||||
},
|
||||
std::{
|
||||
utils::{
|
||||
arc_angles, arc_center_and_end, get_tangent_point_from_previous_arc, get_tangential_arc_to_info,
|
||||
arc_angles, arc_start_center_and_end, get_tangent_point_from_previous_arc, get_tangential_arc_to_info,
|
||||
get_x_component, get_y_component, intersection_with_parallel_line, TangentialArcInfoInput,
|
||||
},
|
||||
Args,
|
||||
@ -1485,6 +1485,17 @@ pub async fn arc(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
||||
Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch))
|
||||
}
|
||||
|
||||
/// Squared distance between two points
|
||||
fn distance_squared(a: Point2d, b: Point2d) -> f64 {
|
||||
let v = Point2d {
|
||||
x: b.x - a.x,
|
||||
y: b.y - a.y,
|
||||
};
|
||||
let dot = v.x * v.x + v.y * v.y;
|
||||
|
||||
dot
|
||||
}
|
||||
|
||||
/// Draw a curved line segment along an imaginary circle.
|
||||
/// The arc is constructed such that the current position of the sketch is
|
||||
/// placed along an imaginary circle of the specified radius, at angleStart
|
||||
@ -1524,9 +1535,31 @@ pub(crate) async fn inner_arc(
|
||||
angle_end,
|
||||
radius,
|
||||
} => {
|
||||
let a_start = Angle::from_degrees(*angle_start);
|
||||
let a_end = Angle::from_degrees(*angle_end);
|
||||
let (center, end) = arc_center_and_end(from, a_start, a_end, *radius);
|
||||
let mut a_start = Angle::from_degrees(*angle_start);
|
||||
let mut a_end = Angle::from_degrees(*angle_end);
|
||||
|
||||
//duplicating engine logic to make sure this is _exactly_ what engine is doing - mike
|
||||
// if a_start.to_degrees() > a_end.to_degrees() {
|
||||
// // this implies a clockwise arc, so swap the angles to a matching counter-clockwise arc
|
||||
// std::mem::swap(&mut a_start, &mut a_end);
|
||||
// }
|
||||
|
||||
let (mut start, center, mut end) = arc_start_center_and_end(from, a_start, a_end, *radius);
|
||||
let desired_start = from;
|
||||
let dist1 = distance_squared(start, desired_start);
|
||||
let dist2 = distance_squared(end, desired_start);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
web_sys::console::log_1(&format!("testing {dist1} vs {dist2}!").into());
|
||||
}
|
||||
|
||||
if !(dist2 < dist1) { //flipped from engine ????????????
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
web_sys::console::log_1(&format!("swapping!").into());
|
||||
std::mem::swap(&mut start, &mut end);
|
||||
}
|
||||
|
||||
(center, a_start, a_end, *radius, end)
|
||||
}
|
||||
ArcData::CenterToRadius { center, to, radius } => {
|
||||
@ -1671,7 +1704,7 @@ async fn inner_tangential_arc(
|
||||
// but the above logic *should* capture that behavior
|
||||
let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
|
||||
let end_angle = start_angle + offset;
|
||||
let (center, to) = arc_center_and_end(from, start_angle, end_angle, radius);
|
||||
let (_, center, to) = arc_start_center_and_end(from, start_angle, end_angle, radius);
|
||||
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
|
@ -204,21 +204,76 @@ pub fn get_x_component(angle: Angle, y: f64) -> Point2d {
|
||||
Point2d { x, y }.scale(sign)
|
||||
}
|
||||
|
||||
pub fn arc_center_and_end(from: Point2d, start_angle: Angle, end_angle: Angle, radius: f64) -> (Point2d, Point2d) {
|
||||
fn arc_center_and_end(from: Point2d, start_angle: Angle, end_angle: Angle, radius: f64) -> (Point2d, Point2d) {
|
||||
let (_, center, end) = arc_start_center_and_end(from, start_angle, end_angle, radius);
|
||||
(center, end)
|
||||
}
|
||||
|
||||
pub fn arc_start_center_and_end(from: Point2d, start_angle: Angle, end_angle: Angle, radius: f64) -> (Point2d, Point2d, Point2d) {
|
||||
let start_angle = start_angle.to_radians();
|
||||
let end_angle = end_angle.to_radians();
|
||||
|
||||
let eval = |t: f64, radius: f64, center: Point2d| { //UNUSED (this didn't work either for some reason.. - mike)
|
||||
//HACK - using this as an example to demonstrate that even something as simple as an arc can be problematic to
|
||||
//duplicate this type of logic on the frontend side because of all the little edge cases
|
||||
//we must come up with a better strategy to avoid this sort of stuff in the future.
|
||||
//having to manually port this sort of code directly into rust is error-prone, and
|
||||
//really isn't a good usage of dev time - mike
|
||||
let sin_of_pi = if ((4.0*PI).sin()).abs() > PI.sin().abs() {
|
||||
4.0*PI.sin().abs()
|
||||
} else {
|
||||
PI.sin().abs()
|
||||
};
|
||||
|
||||
let cos_of_pi_over_2 = if (4.5*PI).cos().abs() > (0.5*PI).sin().abs() {
|
||||
(4.5*PI).cos().abs()
|
||||
} else {
|
||||
(0.5*PI).sin().abs()
|
||||
};
|
||||
|
||||
let mut c = t.cos();
|
||||
let mut s = t.sin();
|
||||
|
||||
if c.abs() <= cos_of_pi_over_2 {
|
||||
c = 0.0;
|
||||
s = if s < 0.0 { -1.0 } else { 1.0 };
|
||||
} else if s.abs() <= sin_of_pi {
|
||||
s = 0.0;
|
||||
c = if c < 0.0 { -1.0 } else { 1.0 };
|
||||
}
|
||||
|
||||
Point2d {
|
||||
x: center.x + radius * c,
|
||||
y: center.y + radius * s,
|
||||
}
|
||||
};
|
||||
|
||||
let center = Point2d {
|
||||
x: -1.0 * (radius * start_angle.cos() - from.x),
|
||||
y: -1.0 * (radius * start_angle.sin() - from.y),
|
||||
};
|
||||
|
||||
//let start = eval(start_angle, radius, center);
|
||||
//let end = eval(end_angle, radius, center);
|
||||
|
||||
let start = Point2d {
|
||||
x: center.x + radius * start_angle.cos(),
|
||||
y: center.y + radius * start_angle.sin(),
|
||||
};
|
||||
|
||||
let end = Point2d {
|
||||
x: center.x + radius * end_angle.cos(),
|
||||
y: center.y + radius * end_angle.sin(),
|
||||
};
|
||||
|
||||
(center, end)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let start_deg = start_angle.to_degrees();
|
||||
let end_deg = end_angle.to_degrees();
|
||||
web_sys::console::log_1(&format!("Arc testing {start_deg:?}, {end_deg:?} -> center: {center:?}, start: {start:?} end: {end:?}").into());
|
||||
}
|
||||
|
||||
(start, center, end)
|
||||
}
|
||||
|
||||
pub fn arc_angles(
|
||||
|
43
yarn.lock
43
yarn.lock
@ -3780,6 +3780,13 @@ chokidar@^3.5.3:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chokidar@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41"
|
||||
integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==
|
||||
dependencies:
|
||||
readdirp "^4.0.1"
|
||||
|
||||
chownr@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
|
||||
@ -8155,6 +8162,11 @@ readable-stream@~2.3.6:
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readdirp@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a"
|
||||
integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==
|
||||
|
||||
readdirp@~3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
||||
@ -8773,16 +8785,7 @@ string-natural-compare@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@ -8876,14 +8879,7 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@ -9757,16 +9753,7 @@ word-wrap@^1.2.3, word-wrap@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
Reference in New Issue
Block a user