pcb-scene3d-viewer 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +67 -0
- package/COMMERCIAL-LICENSE.md +15 -0
- package/CONTRIBUTING.md +14 -0
- package/LICENSE +19 -0
- package/LICENSES/AGPL-3.0-or-later.txt +235 -0
- package/LICENSES/CC-BY-SA-4.0.txt +170 -0
- package/LICENSES/LGPL-2.1-or-later.txt +176 -0
- package/LICENSES/LicenseRef-PolyForm-Noncommercial-1.0.0.txt +131 -0
- package/NOTICE.md +36 -0
- package/README.md +128 -0
- package/REUSE.toml +16 -0
- package/docs/api.md +148 -0
- package/docs/circuitjson.md +190 -0
- package/docs/model-format.md +117 -0
- package/docs/testing.md +23 -0
- package/package.json +65 -0
- package/spec/library-scope.md +36 -0
- package/src/PcbModelArchiveExporter.mjs +320 -0
- package/src/PcbScene3dArcUtils.mjs +27 -0
- package/src/PcbScene3dBoardAssemblyPlacement.mjs +36 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +859 -0
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +537 -0
- package/src/PcbScene3dBoardMaterialPalette.mjs +40 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +895 -0
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +613 -0
- package/src/PcbScene3dCameraRig.mjs +168 -0
- package/src/PcbScene3dCircuitJsonAdapter.mjs +545 -0
- package/src/PcbScene3dController.mjs +956 -0
- package/src/PcbScene3dCopperDetailFilter.mjs +490 -0
- package/src/PcbScene3dCopperFactory.mjs +559 -0
- package/src/PcbScene3dCopperTextFactory.mjs +534 -0
- package/src/PcbScene3dCutoutGeometryFilter.mjs +873 -0
- package/src/PcbScene3dDetailCoordinateNormalizer.mjs +65 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +224 -0
- package/src/PcbScene3dDrillPathFactory.mjs +362 -0
- package/src/PcbScene3dDrillVoidFactory.mjs +268 -0
- package/src/PcbScene3dExternalModelLoadOrder.mjs +54 -0
- package/src/PcbScene3dExternalModels.mjs +968 -0
- package/src/PcbScene3dFallbackVisibility.mjs +82 -0
- package/src/PcbScene3dInteractionHints.mjs +56 -0
- package/src/PcbScene3dMountRig.mjs +53 -0
- package/src/PcbScene3dOutlineBuilder.mjs +210 -0
- package/src/PcbScene3dPadFactory.mjs +553 -0
- package/src/PcbScene3dPresetState.mjs +48 -0
- package/src/PcbScene3dRenderGroupVisibility.mjs +134 -0
- package/src/PcbScene3dRuntime.mjs +996 -0
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +99 -0
- package/src/PcbScene3dSelectionStyler.mjs +252 -0
- package/src/PcbScene3dShapePathFactory.mjs +220 -0
- package/src/PcbScene3dShellRenderer.mjs +131 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +854 -0
- package/src/PcbScene3dSilkscreenStrokeWidthResolver.mjs +81 -0
- package/src/PcbScene3dStepLoader.mjs +611 -0
- package/src/PcbScene3dStrokeFont.mjs +671 -0
- package/src/PcbScene3dStrokeGeometryBuilder.mjs +322 -0
- package/src/PcbScene3dText.mjs +99 -0
- package/src/PcbScene3dTrueTypeTextFactory.mjs +885 -0
- package/src/PcbScene3dViaFactory.mjs +176 -0
- package/src/PcbScene3dViewCompensation.mjs +109 -0
- package/src/PcbScene3dViewScale.mjs +24 -0
- package/src/PcbScene3dViewportResize.mjs +35 -0
- package/src/PcbScene3dWorkerClient.mjs +123 -0
- package/src/index.mjs +1 -0
- package/src/scene3d.mjs +44 -0
- package/src/styles/scene3d.css +295 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages when procedural fallback bodies should stay visible alongside
|
|
3
|
+
* external 3D models.
|
|
4
|
+
*/
|
|
5
|
+
export class PcbScene3dFallbackVisibility {
|
|
6
|
+
/**
|
|
7
|
+
* Registers one fallback render root under a component designator.
|
|
8
|
+
* @param {Map<string, Set<any>>} fallbackRoots
|
|
9
|
+
* @param {string} designator
|
|
10
|
+
* @param {any} rootObject
|
|
11
|
+
* @returns {void}
|
|
12
|
+
*/
|
|
13
|
+
static registerFallbackRoot(fallbackRoots, designator, rootObject) {
|
|
14
|
+
if (!(fallbackRoots instanceof Map) || !rootObject) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const normalizedDesignator = String(designator || '').trim()
|
|
19
|
+
if (!normalizedDesignator) {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!fallbackRoots.has(normalizedDesignator)) {
|
|
24
|
+
fallbackRoots.set(normalizedDesignator, new Set())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fallbackRoots.get(normalizedDesignator)?.add(rootObject)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Marks one designator as having a successfully loaded external model.
|
|
32
|
+
* @param {Set<string>} loadedDesignators
|
|
33
|
+
* @param {string} designator
|
|
34
|
+
* @returns {void}
|
|
35
|
+
*/
|
|
36
|
+
static markExternalModelLoaded(loadedDesignators, designator) {
|
|
37
|
+
if (!(loadedDesignators instanceof Set)) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const normalizedDesignator = String(designator || '').trim()
|
|
42
|
+
if (!normalizedDesignator) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
loadedDesignators.add(normalizedDesignator)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Applies the current visibility policy to all registered fallback roots.
|
|
51
|
+
* Fallbacks stay visible when their toggle is enabled and either no
|
|
52
|
+
* external model loaded for that designator or external models are hidden.
|
|
53
|
+
* @param {Map<string, Set<any>>} fallbackRoots
|
|
54
|
+
* @param {Set<string>} loadedDesignators
|
|
55
|
+
* @param {{ 'fallback-bodies'?: boolean, 'external-models'?: boolean }} toggles
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
58
|
+
static applyVisibility(fallbackRoots, loadedDesignators, toggles) {
|
|
59
|
+
if (!(fallbackRoots instanceof Map)) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const showFallbackBodies = Boolean(toggles?.['fallback-bodies'])
|
|
64
|
+
const showExternalModels = Boolean(toggles?.['external-models'])
|
|
65
|
+
|
|
66
|
+
fallbackRoots.forEach((roots, designator) => {
|
|
67
|
+
const hideForLoadedExternal =
|
|
68
|
+
showExternalModels &&
|
|
69
|
+
loadedDesignators instanceof Set &&
|
|
70
|
+
loadedDesignators.has(String(designator || '').trim())
|
|
71
|
+
|
|
72
|
+
roots?.forEach?.((rootObject) => {
|
|
73
|
+
if (!rootObject) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
rootObject.visible =
|
|
78
|
+
showFallbackBodies && !hideForLoadedExternal
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralizes pointer and touch interaction defaults for the 3D PCB viewport.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dInteractionHints {
|
|
5
|
+
/**
|
|
6
|
+
* Applies explicit OrbitControls input mappings for desktop and touch.
|
|
7
|
+
* @param {{ mouseButtons?: any, touches?: any }} controls
|
|
8
|
+
* @param {{ MOUSE?: any, TOUCH?: any }} THREE
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*/
|
|
11
|
+
static configureControls(controls, THREE) {
|
|
12
|
+
if (!controls) {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (THREE?.MOUSE) {
|
|
17
|
+
controls.mouseButtons = {
|
|
18
|
+
LEFT: THREE.MOUSE.ROTATE,
|
|
19
|
+
MIDDLE: THREE.MOUSE.DOLLY,
|
|
20
|
+
RIGHT: THREE.MOUSE.PAN
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (THREE?.TOUCH) {
|
|
25
|
+
controls.touches = {
|
|
26
|
+
ONE: THREE.TOUCH.ROTATE,
|
|
27
|
+
TWO: THREE.TOUCH.DOLLY_PAN
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolves the default interaction copy for the current pointer type.
|
|
34
|
+
* @param {{ matchMedia?: (query: string) => { matches?: boolean } }} [environment]
|
|
35
|
+
* @param {((key: string) => string) | null} [translate] Translation lookup.
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
static resolveDefaultMessage(
|
|
39
|
+
environment = globalThis.window,
|
|
40
|
+
translate = null
|
|
41
|
+
) {
|
|
42
|
+
if (environment?.matchMedia?.('(pointer: coarse)')?.matches) {
|
|
43
|
+
if (typeof translate === 'function') {
|
|
44
|
+
const value = translate('scene3d.touchHint')
|
|
45
|
+
if (value && value !== 'scene3d.touchHint') return value
|
|
46
|
+
}
|
|
47
|
+
return 'Drag with one finger to orbit, pinch to zoom, and drag with two fingers to pan.'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof translate === 'function') {
|
|
51
|
+
const value = translate('scene3d.pointerHint')
|
|
52
|
+
if (value && value !== 'scene3d.pointerHint') return value
|
|
53
|
+
}
|
|
54
|
+
return 'Drag to orbit, right-drag to pan, and use the wheel to zoom.'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a stable transform hierarchy for one PCB-mounted 3D object.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dMountRig {
|
|
5
|
+
/**
|
|
6
|
+
* Creates one transform rig that keeps the XY anchor fixed while flipping
|
|
7
|
+
* bottom-side content around the board face.
|
|
8
|
+
* @param {any} THREE
|
|
9
|
+
* @param {{ mountSide?: string, rotationDeg?: number, positionMil?: { x?: number, y?: number, z?: number } }} placement
|
|
10
|
+
* @returns {{ rootGroup: any, orientationGroup: any, sideGroup: any, faceGroup: any }}
|
|
11
|
+
*/
|
|
12
|
+
static create(THREE, placement) {
|
|
13
|
+
const rootGroup = new THREE.Group()
|
|
14
|
+
const orientationGroup = new THREE.Group()
|
|
15
|
+
const sideGroup = new THREE.Group()
|
|
16
|
+
const faceGroup = new THREE.Group()
|
|
17
|
+
const positionMil = placement?.positionMil || {}
|
|
18
|
+
|
|
19
|
+
rootGroup.position.set(
|
|
20
|
+
Number(positionMil.x || 0),
|
|
21
|
+
Number(positionMil.y || 0),
|
|
22
|
+
0
|
|
23
|
+
)
|
|
24
|
+
orientationGroup.rotation.z =
|
|
25
|
+
(Number(placement?.rotationDeg || 0) * Math.PI) / 180
|
|
26
|
+
if (PcbScene3dMountRig.#isBottomSide(placement?.mountSide)) {
|
|
27
|
+
sideGroup.rotation.y = Math.PI
|
|
28
|
+
sideGroup.rotation.z = Math.PI
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
faceGroup.position.z = Math.abs(Number(positionMil.z || 0))
|
|
32
|
+
|
|
33
|
+
sideGroup.add(faceGroup)
|
|
34
|
+
orientationGroup.add(sideGroup)
|
|
35
|
+
rootGroup.add(orientationGroup)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
rootGroup,
|
|
39
|
+
orientationGroup,
|
|
40
|
+
sideGroup,
|
|
41
|
+
faceGroup
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns true when one placement belongs on the underside of the board.
|
|
47
|
+
* @param {string | undefined} mountSide
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
static #isBottomSide(mountSide) {
|
|
51
|
+
return String(mountSide || 'top').toLowerCase() === 'bottom'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds stable board-outline commands for the 3D PCB runtime.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dOutlineBuilder {
|
|
5
|
+
static #DEGENERATE_LINE_EPSILON = 0.01
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Converts one board outline into local move/line/arc commands.
|
|
9
|
+
* @param {{ centerX?: number, centerY?: number, segments?: Array<Record<string, number | string>> }} board
|
|
10
|
+
* @returns {Array<Record<string, number | boolean>>}
|
|
11
|
+
*/
|
|
12
|
+
static buildCommands(board) {
|
|
13
|
+
const segments = Array.isArray(board?.segments) ? board.segments : []
|
|
14
|
+
if (!segments.length) {
|
|
15
|
+
return []
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const centerX = Number(board?.centerX || 0)
|
|
19
|
+
const centerY = Number(board?.centerY || 0)
|
|
20
|
+
const firstPoint = PcbScene3dOutlineBuilder.#toLocalPoint(
|
|
21
|
+
Number(segments[0].x1 || 0),
|
|
22
|
+
Number(segments[0].y1 || 0),
|
|
23
|
+
centerX,
|
|
24
|
+
centerY
|
|
25
|
+
)
|
|
26
|
+
const commands = [
|
|
27
|
+
{
|
|
28
|
+
type: 'move',
|
|
29
|
+
x: firstPoint.x,
|
|
30
|
+
y: firstPoint.y
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
for (const segment of segments) {
|
|
35
|
+
if (segment.type === 'arc') {
|
|
36
|
+
const arcCommand = PcbScene3dOutlineBuilder.#buildArcCommand(
|
|
37
|
+
segment,
|
|
38
|
+
centerX,
|
|
39
|
+
centerY
|
|
40
|
+
)
|
|
41
|
+
if (arcCommand) {
|
|
42
|
+
commands.push(arcCommand)
|
|
43
|
+
}
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lineCommand = PcbScene3dOutlineBuilder.#buildLineCommand(
|
|
48
|
+
segment,
|
|
49
|
+
centerX,
|
|
50
|
+
centerY
|
|
51
|
+
)
|
|
52
|
+
if (lineCommand) {
|
|
53
|
+
commands.push(lineCommand)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return commands
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converts one outline line segment into a local line command.
|
|
62
|
+
* @param {Record<string, number | string>} segment
|
|
63
|
+
* @param {number} centerX
|
|
64
|
+
* @param {number} centerY
|
|
65
|
+
* @returns {{ type: 'line', x: number, y: number } | null}
|
|
66
|
+
*/
|
|
67
|
+
static #buildLineCommand(segment, centerX, centerY) {
|
|
68
|
+
const startPoint = PcbScene3dOutlineBuilder.#toLocalPoint(
|
|
69
|
+
Number(segment.x1 || 0),
|
|
70
|
+
Number(segment.y1 || 0),
|
|
71
|
+
centerX,
|
|
72
|
+
centerY
|
|
73
|
+
)
|
|
74
|
+
const endPoint = PcbScene3dOutlineBuilder.#toLocalPoint(
|
|
75
|
+
Number(segment.x2 || 0),
|
|
76
|
+
Number(segment.y2 || 0),
|
|
77
|
+
centerX,
|
|
78
|
+
centerY
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
PcbScene3dOutlineBuilder.#distanceBetween(startPoint, endPoint) <
|
|
83
|
+
PcbScene3dOutlineBuilder.#DEGENERATE_LINE_EPSILON
|
|
84
|
+
) {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
type: 'line',
|
|
90
|
+
x: endPoint.x,
|
|
91
|
+
y: endPoint.y
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Converts one outline arc segment into a local arc command traced from
|
|
97
|
+
* the ordered segment endpoints instead of the serialized angle fields.
|
|
98
|
+
* @param {Record<string, number | string>} segment
|
|
99
|
+
* @param {number} centerX
|
|
100
|
+
* @param {number} centerY
|
|
101
|
+
* @returns {{ type: 'arc', cx: number, cy: number, radius: number, startX: number, startY: number, endX: number, endY: number, startAngleRad: number, endAngleRad: number, clockwise: boolean } | null}
|
|
102
|
+
*/
|
|
103
|
+
static #buildArcCommand(segment, centerX, centerY) {
|
|
104
|
+
const arcCenter = PcbScene3dOutlineBuilder.#toLocalPoint(
|
|
105
|
+
Number(segment.cx || 0),
|
|
106
|
+
Number(segment.cy || 0),
|
|
107
|
+
centerX,
|
|
108
|
+
centerY
|
|
109
|
+
)
|
|
110
|
+
const startPoint = PcbScene3dOutlineBuilder.#toLocalPoint(
|
|
111
|
+
Number(segment.x1 || 0),
|
|
112
|
+
Number(segment.y1 || 0),
|
|
113
|
+
centerX,
|
|
114
|
+
centerY
|
|
115
|
+
)
|
|
116
|
+
const endPoint = PcbScene3dOutlineBuilder.#toLocalPoint(
|
|
117
|
+
Number(segment.x2 || 0),
|
|
118
|
+
Number(segment.y2 || 0),
|
|
119
|
+
centerX,
|
|
120
|
+
centerY
|
|
121
|
+
)
|
|
122
|
+
const radius =
|
|
123
|
+
Math.max(
|
|
124
|
+
Number(segment.radius || 0),
|
|
125
|
+
PcbScene3dOutlineBuilder.#distanceBetween(
|
|
126
|
+
arcCenter,
|
|
127
|
+
startPoint
|
|
128
|
+
),
|
|
129
|
+
PcbScene3dOutlineBuilder.#distanceBetween(arcCenter, endPoint)
|
|
130
|
+
) || 0
|
|
131
|
+
|
|
132
|
+
if (!radius) {
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const startAngleRad = Math.atan2(
|
|
137
|
+
startPoint.y - arcCenter.y,
|
|
138
|
+
startPoint.x - arcCenter.x
|
|
139
|
+
)
|
|
140
|
+
const endAngleRad = Math.atan2(
|
|
141
|
+
endPoint.y - arcCenter.y,
|
|
142
|
+
endPoint.x - arcCenter.x
|
|
143
|
+
)
|
|
144
|
+
const deltaAngle = PcbScene3dOutlineBuilder.#resolveShortDeltaAngle(
|
|
145
|
+
startAngleRad,
|
|
146
|
+
endAngleRad
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
type: 'arc',
|
|
151
|
+
cx: arcCenter.x,
|
|
152
|
+
cy: arcCenter.y,
|
|
153
|
+
radius,
|
|
154
|
+
startX: startPoint.x,
|
|
155
|
+
startY: startPoint.y,
|
|
156
|
+
endX: endPoint.x,
|
|
157
|
+
endY: endPoint.y,
|
|
158
|
+
startAngleRad,
|
|
159
|
+
endAngleRad,
|
|
160
|
+
clockwise: deltaAngle < 0
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Converts one board-space point into local centered scene coordinates.
|
|
166
|
+
* @param {number} x
|
|
167
|
+
* @param {number} y
|
|
168
|
+
* @param {number} centerX
|
|
169
|
+
* @param {number} centerY
|
|
170
|
+
* @returns {{ x: number, y: number }}
|
|
171
|
+
*/
|
|
172
|
+
static #toLocalPoint(x, y, centerX, centerY) {
|
|
173
|
+
return {
|
|
174
|
+
x: Number(x || 0) - centerX,
|
|
175
|
+
y: Number(y || 0) - centerY
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Measures one point-to-point distance.
|
|
181
|
+
* @param {{ x: number, y: number }} left
|
|
182
|
+
* @param {{ x: number, y: number }} right
|
|
183
|
+
* @returns {number}
|
|
184
|
+
*/
|
|
185
|
+
static #distanceBetween(left, right) {
|
|
186
|
+
const deltaX = Number(right.x || 0) - Number(left.x || 0)
|
|
187
|
+
const deltaY = Number(right.y || 0) - Number(left.y || 0)
|
|
188
|
+
return Math.hypot(deltaX, deltaY)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolves the shortest signed delta between two radians.
|
|
193
|
+
* @param {number} startAngleRad
|
|
194
|
+
* @param {number} endAngleRad
|
|
195
|
+
* @returns {number}
|
|
196
|
+
*/
|
|
197
|
+
static #resolveShortDeltaAngle(startAngleRad, endAngleRad) {
|
|
198
|
+
let deltaAngle = endAngleRad - startAngleRad
|
|
199
|
+
|
|
200
|
+
if (deltaAngle > Math.PI) {
|
|
201
|
+
deltaAngle -= Math.PI * 2
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (deltaAngle < -Math.PI) {
|
|
205
|
+
deltaAngle += Math.PI * 2
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return deltaAngle
|
|
209
|
+
}
|
|
210
|
+
}
|