Skip to content

Commit a96e752

Browse files
committed
feat: measurement improvements, theme swap, CI Node.js 24 migration, save/load for all annotation types
- Fix new measurements not respecting scale bar (bypass stale preference when scale source exists) - Fix distance dimension Shift+angle snap not applied on click (preview vs actual mismatch) - Fix viewport/scaleBar/measureAngle/scheduleTable not saving or loading (add saver/loader cases for all) - Fix viewport unit changes not converting pixelsPerUnit correctly - Fix formatMeasurement auto-converting mm to m (respect user's chosen unit) - Fix measurement units not respecting viewport unit in dimension text - Add viewport scale/unit editing in properties panel with recalculation - Add alignment guides and magnetic snap when reshaping polygon/dimension/angle vertices - Add drag deadzone (3px) to prevent accidental micro-moves on click-to-select - Add angle dimension hit-testing, selection, handles, move, and reshape support - Swap Default and Deep Forge theme colors; rename Deep Forge to Warm Ember (all 39 i18n files) - Migrate CI workflows to Node.js 24 (checkout v6, setup-node v6, github-script v8, upload-artifact v7, setup-android v4, trusted-signing v1 with cache disabled, replace snapcore/action-build with CLI) - Polygon/polyline selection outline follows actual edges instead of bounding rectangle - Distance dimension selection outline traces leader lines + dimension line - Measure tools section in ribbon split to 2x2 grid layout - Take-Off panel redesigned as modeless dialog matching app dialog style - Area measurement centroid label drawn on white background for readability - Default distance dimension endpoints changed from closed arrow to open circle - Version bump to 1.39.0
1 parent 37d42bf commit a96e752

79 files changed

Lines changed: 1205 additions & 492 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/auto-assign-issues.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jobs:
66
assign:
77
runs-on: ubuntu-latest
88
steps:
9-
- uses: actions/github-script@v7
9+
- uses: actions/github-script@v8
1010
with:
1111
script: |
1212
await github.rest.issues.addAssignees({

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ jobs:
2626
working-directory: open-pdf-studio
2727

2828
steps:
29-
- uses: actions/checkout@v5
29+
- uses: actions/checkout@v6
3030

3131
- name: Setup Node.js
32-
uses: actions/setup-node@v5
32+
uses: actions/setup-node@v6
3333
with:
3434
node-version: 20
3535

.github/workflows/release.yml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
version: ${{ steps.get-version.outputs.version }}
2222

2323
steps:
24-
- uses: actions/checkout@v5
24+
- uses: actions/checkout@v6
2525

2626
- name: Get version
2727
id: get-version
@@ -34,7 +34,7 @@ jobs:
3434
3535
- name: Create release
3636
id: create-release
37-
uses: actions/github-script@v7
37+
uses: actions/github-script@v8
3838
with:
3939
script: |
4040
const version = '${{ steps.get-version.outputs.version }}';
@@ -79,10 +79,10 @@ jobs:
7979
working-directory: open-pdf-studio
8080

8181
steps:
82-
- uses: actions/checkout@v5
82+
- uses: actions/checkout@v6
8383

8484
- name: Setup Node.js
85-
uses: actions/setup-node@v5
85+
uses: actions/setup-node@v6
8686
with:
8787
node-version: 20
8888

@@ -132,14 +132,15 @@ jobs:
132132

133133
- name: Authenticode sign Windows installer
134134
if: contains(matrix.target, 'windows')
135-
uses: azure/trusted-signing-action@v0.5.0
135+
uses: azure/trusted-signing-action@v1
136136
with:
137137
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
138138
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
139139
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
140140
endpoint: ${{ secrets.AZURE_ENDPOINT }}
141-
trusted-signing-account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
141+
signing-account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
142142
certificate-profile-name: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }}
143+
cache-dependencies: false
143144
files-folder: ${{ github.workspace }}/open-pdf-studio/src-tauri/target/release/bundle/nsis
144145
files-folder-filter: exe
145146
files-folder-depth: 1
@@ -239,7 +240,7 @@ jobs:
239240
working-directory: open-pdf-studio
240241

241242
steps:
242-
- uses: actions/checkout@v5
243+
- uses: actions/checkout@v6
243244

244245
- name: Setup Java JDK 17
245246
uses: actions/setup-java@v5
@@ -248,13 +249,13 @@ jobs:
248249
java-version: '17'
249250

250251
- name: Setup Android SDK
251-
uses: android-actions/setup-android@v3
252+
uses: android-actions/setup-android@v4
252253

253254
- name: Install Android NDK
254255
run: sdkmanager --install "ndk;27.0.12077973"
255256

256257
- name: Setup Node.js
257-
uses: actions/setup-node@v5
258+
uses: actions/setup-node@v6
258259
with:
259260
node-version: 20
260261
cache: 'npm'
@@ -333,7 +334,7 @@ jobs:
333334
echo "Signed APK: $SIGNED_APK"
334335
335336
- name: Upload signed APK artifact
336-
uses: actions/upload-artifact@v5
337+
uses: actions/upload-artifact@v7
337338
with:
338339
name: android-apk
339340
path: open-pdf-studio/src-tauri/gen/android/**/open-pdf-studio-release.apk
@@ -358,7 +359,7 @@ jobs:
358359
steps:
359360
- name: Publish release
360361
id: publish-release
361-
uses: actions/github-script@v7
362+
uses: actions/github-script@v8
362363
env:
363364
release_id: ${{ needs.create-release.outputs.release_id }}
364365
with:

.github/workflows/snap.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
contents: write
1919

2020
steps:
21-
- uses: actions/checkout@v5
21+
- uses: actions/checkout@v6
2222

2323
- name: Get version
2424
id: get-version
@@ -32,7 +32,7 @@ jobs:
3232
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
3333
3434
- name: Setup Node.js
35-
uses: actions/setup-node@v5
35+
uses: actions/setup-node@v6
3636
with:
3737
node-version: 20
3838

@@ -67,9 +67,16 @@ jobs:
6767
# Update version in snapcraft.yaml
6868
sed -i "s/^version: .*/version: '${{ steps.get-version.outputs.version_number }}'/" snap/snapcraft.yaml
6969
70+
- name: Install Snapcraft
71+
run: sudo snap install snapcraft --classic
72+
7073
- name: Build snap
71-
uses: snapcore/action-build@v1
7274
id: snapcraft
75+
run: |
76+
snapcraft
77+
SNAP_FILE=$(ls *.snap | head -1)
78+
echo "snap=$SNAP_FILE" >> $GITHUB_OUTPUT
79+
echo "Built snap: $SNAP_FILE"
7380
7481
- name: Upload snap to GitHub release
7582
run: |

open-pdf-studio/js/annotations/geometry.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function getAnnotationCenterAndSize(ann) {
3434
case 'stamp':
3535
case 'signature':
3636
case 'redaction':
37+
case 'viewport':
3738
case 'scaleBar':
3839
case 'scheduleTable':
3940
return {
@@ -212,6 +213,18 @@ export function findAnnotationAt(x, y) {
212213
case 'stamp':
213214
case 'signature':
214215
case 'redaction':
216+
case 'viewport': {
217+
// Viewport: hit test on boundary edges and name label
218+
const edgeTol = 6;
219+
const nearLeft = Math.abs(x - ann.x) < edgeTol && y >= ann.y - edgeTol && y <= ann.y + ann.height + edgeTol;
220+
const nearRight = Math.abs(x - (ann.x + ann.width)) < edgeTol && y >= ann.y - edgeTol && y <= ann.y + ann.height + edgeTol;
221+
const nearTop = Math.abs(y - ann.y) < edgeTol && x >= ann.x - edgeTol && x <= ann.x + ann.width + edgeTol;
222+
const nearBottom = Math.abs(y - (ann.y + ann.height)) < edgeTol && x >= ann.x - edgeTol && x <= ann.x + ann.width + edgeTol;
223+
if (nearLeft || nearRight || nearTop || nearBottom) return ann;
224+
// Label area above top-left
225+
if (x >= ann.x && x <= ann.x + 120 && y >= ann.y - 16 && y <= ann.y) return ann;
226+
break;
227+
}
215228
case 'scaleBar':
216229
case 'scheduleTable': {
217230
const imgCenter = { x: ann.x + ann.width / 2, y: ann.y + ann.height / 2 };
@@ -271,6 +284,35 @@ export function findAnnotationAt(x, y) {
271284
}
272285
}
273286
break;
287+
case 'measureAngle':
288+
if (ann.point1 && ann.vertex && ann.point2) {
289+
// Check proximity to the two rays
290+
const d1 = distanceToLine(x, y, ann.point1.x, ann.point1.y, ann.vertex.x, ann.vertex.y);
291+
if (d1 < tol) return ann;
292+
const d2 = distanceToLine(x, y, ann.vertex.x, ann.vertex.y, ann.point2.x, ann.point2.y);
293+
if (d2 < tol) return ann;
294+
// Check proximity to the arc
295+
const arcR = ann.arcRadius || 30;
296+
const dx = x - ann.vertex.x;
297+
const dy = y - ann.vertex.y;
298+
const distFromVertex = Math.sqrt(dx * dx + dy * dy);
299+
if (Math.abs(distFromVertex - arcR) < tol) {
300+
// Check the point is within the angle sweep
301+
const a1 = Math.atan2(ann.point1.y - ann.vertex.y, ann.point1.x - ann.vertex.x);
302+
const a2 = Math.atan2(ann.point2.y - ann.vertex.y, ann.point2.x - ann.vertex.x);
303+
const ap = Math.atan2(dy, dx);
304+
let sweep = a2 - a1;
305+
if (sweep < 0) sweep += 2 * Math.PI;
306+
let test = ap - a1;
307+
if (test < 0) test += 2 * Math.PI;
308+
if (sweep > Math.PI) {
309+
if (test > sweep) return ann;
310+
} else {
311+
if (test < sweep) return ann;
312+
}
313+
}
314+
}
315+
break;
274316
case 'textHighlight':
275317
case 'textStrikethrough':
276318
case 'textUnderline':
@@ -375,6 +417,7 @@ export function isPointInsideAnnotation(x, y, annotation) {
375417
}
376418
return false;
377419

420+
case 'viewport':
378421
case 'image':
379422
case 'stamp':
380423
case 'signature':

open-pdf-studio/js/annotations/handles.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ export function getAnnotationHandles(annotation, scale = 1) {
156156
}
157157
break;
158158

159+
case 'measureAngle':
160+
if (annotation.point1 && annotation.vertex && annotation.point2) {
161+
handles.push({ type: HANDLE_TYPES.POLYLINE_NODE, x: annotation.point1.x - hs/2, y: annotation.point1.y - hs/2, nodeIndex: 0 });
162+
handles.push({ type: HANDLE_TYPES.POLYLINE_NODE, x: annotation.vertex.x - hs/2, y: annotation.vertex.y - hs/2, nodeIndex: 1 });
163+
handles.push({ type: HANDLE_TYPES.POLYLINE_NODE, x: annotation.point2.x - hs/2, y: annotation.point2.y - hs/2, nodeIndex: 2 });
164+
}
165+
break;
166+
159167
case 'comment':
160168
// No resize/rotation handles — sticky note icon is fixed size, move only
161169
break;
@@ -222,22 +230,32 @@ export function getAnnotationHandles(annotation, scale = 1) {
222230
}
223231
break;
224232

233+
case 'viewport':
234+
// Viewport: corner + edge handles, no rotation
235+
handles.push({ type: HANDLE_TYPES.TOP_LEFT, x: annotation.x - hs/2, y: annotation.y - hs/2 });
236+
handles.push({ type: HANDLE_TYPES.TOP_RIGHT, x: annotation.x + annotation.width - hs/2, y: annotation.y - hs/2 });
237+
handles.push({ type: HANDLE_TYPES.BOTTOM_LEFT, x: annotation.x - hs/2, y: annotation.y + annotation.height - hs/2 });
238+
handles.push({ type: HANDLE_TYPES.BOTTOM_RIGHT, x: annotation.x + annotation.width - hs/2, y: annotation.y + annotation.height - hs/2 });
239+
handles.push({ type: HANDLE_TYPES.TOP, x: annotation.x + annotation.width/2 - hs/2, y: annotation.y - hs/2 });
240+
handles.push({ type: HANDLE_TYPES.BOTTOM, x: annotation.x + annotation.width/2 - hs/2, y: annotation.y + annotation.height - hs/2 });
241+
handles.push({ type: HANDLE_TYPES.LEFT, x: annotation.x - hs/2, y: annotation.y + annotation.height/2 - hs/2 });
242+
handles.push({ type: HANDLE_TYPES.RIGHT, x: annotation.x + annotation.width - hs/2, y: annotation.y + annotation.height/2 - hs/2 });
243+
break;
244+
225245
case 'image':
226246
case 'stamp':
227247
case 'signature':
228248
case 'scaleBar':
229249
case 'scheduleTable':
230-
// Corner handles for resize
250+
// Corner + edge + rotation handles
231251
handles.push({ type: HANDLE_TYPES.TOP_LEFT, x: annotation.x - hs/2, y: annotation.y - hs/2 });
232252
handles.push({ type: HANDLE_TYPES.TOP_RIGHT, x: annotation.x + annotation.width - hs/2, y: annotation.y - hs/2 });
233253
handles.push({ type: HANDLE_TYPES.BOTTOM_LEFT, x: annotation.x - hs/2, y: annotation.y + annotation.height - hs/2 });
234254
handles.push({ type: HANDLE_TYPES.BOTTOM_RIGHT, x: annotation.x + annotation.width - hs/2, y: annotation.y + annotation.height - hs/2 });
235-
// Edge handles
236255
handles.push({ type: HANDLE_TYPES.TOP, x: annotation.x + annotation.width/2 - hs/2, y: annotation.y - hs/2 });
237256
handles.push({ type: HANDLE_TYPES.BOTTOM, x: annotation.x + annotation.width/2 - hs/2, y: annotation.y + annotation.height - hs/2 });
238257
handles.push({ type: HANDLE_TYPES.LEFT, x: annotation.x - hs/2, y: annotation.y + annotation.height/2 - hs/2 });
239258
handles.push({ type: HANDLE_TYPES.RIGHT, x: annotation.x + annotation.width - hs/2, y: annotation.y + annotation.height/2 - hs/2 });
240-
// Rotation handle (above the image)
241259
handles.push({ type: HANDLE_TYPES.ROTATE, x: annotation.x + annotation.width/2 - hs/2, y: annotation.y - 25 / scale - hs/2 });
242260
break;
243261

@@ -266,12 +284,9 @@ export function getAnnotationHandles(annotation, scale = 1) {
266284
const center = getAnnotationCenter(annotation);
267285
if (center) {
268286
for (const handle of handles) {
269-
// Calculate handle center (add hs/2 because handle.x/y is top-left corner)
270287
const handleCenterX = handle.x + hs / 2;
271288
const handleCenterY = handle.y + hs / 2;
272-
// Rotate the handle center around the annotation center
273289
const rotated = rotatePoint(handleCenterX, handleCenterY, center.x, center.y, annotation.rotation);
274-
// Update handle position (convert back to top-left corner)
275290
handle.x = rotated.x - hs / 2;
276291
handle.y = rotated.y - hs / 2;
277292
}

open-pdf-studio/js/annotations/measurement.js

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -233,18 +233,6 @@ export function formatMeasurement(measurement) {
233233
let val = applyRounding(measurement.value, measurement.unit);
234234
let unit = measurement.unit || 'mm';
235235

236-
// Auto-convert mm distances >= 1000 to meters for readability
237-
if (unit === 'mm' && Math.abs(val) >= 1000) {
238-
val = val / 1000;
239-
unit = 'm';
240-
}
241-
242-
// Auto-convert mm² to m² for readability (1 m² = 1,000,000 mm²)
243-
if (unit === 'mm\u00B2') {
244-
val = val / 1000000;
245-
unit = 'm\u00B2';
246-
}
247-
248236
const suffix = unit === 'px' ? '' : ` ${unit}`;
249237
const rounding = state.preferences.measureRounding;
250238
if (rounding && rounding !== 'none' && unit !== 'px') {
@@ -254,9 +242,12 @@ export function formatMeasurement(measurement) {
254242
}
255243
// Default formatting per unit
256244
if (unit === 'mm') return `${Math.round(val)}${suffix}`;
245+
if (unit === 'mm\u00B2') return `${Math.round(val)}${suffix}`;
246+
if (unit === 'cm\u00B2') return `${val.toFixed(1)}${suffix}`;
257247
if (unit === 'm\u00B2') return `${val.toFixed(2)}${suffix}`;
258248
if (unit === 'cm') return `${val.toFixed(1)}${suffix}`;
259249
if (unit === 'm') return `${val.toFixed(2)}${suffix}`;
250+
if (unit === 'ft\u00B2' || unit === 'in\u00B2') return `${val.toFixed(2)}${suffix}`;
260251
if (unit === 'ft' || unit === 'in') return `${val.toFixed(2)}${suffix}`;
261252
if (unit === '°') return `${val.toFixed(1)}${suffix}`;
262253
if (val < 0.01) return `0${suffix}`;

0 commit comments

Comments
 (0)