EXU: add dimensional powers and area volume units
This commit is contained in:
@@ -1698,7 +1698,7 @@ DocumentSnapshot 现在区分模型树投影与 `DocumentObjectSnapshot` 真值
|
||||
|
||||
持久化复用 schema v1 已有 `object_properties` 表:`value_json` 保存完整 Property snapshot,`property_type` 保留可查询类型;删除/重写对象时依赖外键级联清理旧属性。内存降级、autosave、Undo/Redo 和 Facade state 均使用深拷贝,避免 options/property 数组共享引用。
|
||||
|
||||
当前限制:Expression/Quantity 已完成基础词法、四则运算、长度/角度/百分比换算、对象属性引用、维度校验,以及 `abs/sin/cos/tan/atan2/min/max/clamp/round/pow` 函数的基础维度规则;locale 规则、Spreadsheet alias 和完整 FreeCAD 表达式兼容仍待 EXU。PropertyLink、Expression 和下游对象现在进入统一依赖 DAG,支持 SCC 循环诊断、拓扑重算计划和 schema v3 持久化;当前重算执行器仍是 Facade 领域切片,尚未把每个节点调度到 OCCT/Sketcher Worker。多选 mixed、批量编辑、重置默认值和属性搜索仍由 P4-02/P6-02 后续任务完成。
|
||||
当前限制:Expression/Quantity 已完成基础词法、四则运算、`^` 幂运算、长度/面积/体积/角度/百分比换算、对象属性引用、维度校验,以及 `abs/sin/cos/tan/atan2/min/max/clamp/round/pow` 函数的基础维度规则;locale 规则、Spreadsheet alias 和完整 FreeCAD 表达式兼容仍待 EXU。PropertyLink、Expression 和下游对象现在进入统一依赖 DAG,支持 SCC 循环诊断、拓扑重算计划和 schema v3 持久化;当前重算执行器仍是 Facade 领域切片,尚未把每个节点调度到 OCCT/Sketcher Worker。多选 mixed、批量编辑、重置默认值和属性搜索仍由 P4-02/P6-02 后续任务完成。
|
||||
|
||||
### 16.17 EXU/DAG/TSN 基础切片验证记录
|
||||
|
||||
|
||||
@@ -18,6 +18,12 @@ const definitions: UnitDefinition[] = [
|
||||
{ id: 'm', symbol: 'm', dimension: 'length', factor: 1000 },
|
||||
{ id: 'in', symbol: 'in', dimension: 'length', factor: 25.4 },
|
||||
{ id: 'ft', symbol: 'ft', dimension: 'length', factor: 304.8 },
|
||||
{ id: 'mm2', symbol: 'mm2', dimension: 'area', factor: 1 },
|
||||
{ id: 'cm2', symbol: 'cm2', dimension: 'area', factor: 100 },
|
||||
{ id: 'm2', symbol: 'm2', dimension: 'area', factor: 1_000_000 },
|
||||
{ id: 'mm3', symbol: 'mm3', dimension: 'volume', factor: 1 },
|
||||
{ id: 'cm3', symbol: 'cm3', dimension: 'volume', factor: 1_000 },
|
||||
{ id: 'm3', symbol: 'm3', dimension: 'volume', factor: 1_000_000_000 },
|
||||
{ id: 'deg', symbol: 'deg', dimension: 'angle', factor: 1 },
|
||||
{ id: 'rad', symbol: 'rad', dimension: 'angle', factor: 180 / Math.PI },
|
||||
{ id: '%', symbol: '%', dimension: 'percent', factor: 1 },
|
||||
@@ -63,7 +69,7 @@ const tokenize = (source: string): Token[] => {
|
||||
while (index < source.length) {
|
||||
const character = source[index]
|
||||
if (/\s/.test(character)) { index += 1; continue }
|
||||
if ('+-*/(),'.includes(character)) { tokens.push({ kind: 'operator', text: character, position: index }); index += 1; continue }
|
||||
if ('+-*/^(),'.includes(character)) { tokens.push({ kind: 'operator', text: character, position: index }); index += 1; continue }
|
||||
if (character === '%') { tokens.push({ kind: 'identifier', text: character, position: index }); index += 1; continue }
|
||||
const number = source.slice(index).match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/)
|
||||
if (number) { tokens.push({ kind: 'number', text: number[0], position: index }); index += number[0].length; continue }
|
||||
@@ -95,6 +101,15 @@ const divide = (left: Quantity, right: Quantity): Quantity => {
|
||||
throw new TypeError(`Cannot divide ${left.dimension} by ${right.dimension}.`)
|
||||
}
|
||||
|
||||
const power = (base: Quantity, exponent: Quantity): Quantity => {
|
||||
if (exponent.dimension !== 'dimensionless' || !Number.isInteger(exponent.value)) throw new TypeError('Exponent must be a dimensionless integer.')
|
||||
if (base.dimension === 'dimensionless') return { value: Math.pow(base.value, exponent.value), dimension: 'dimensionless' }
|
||||
if (exponent.value === 1) return { ...base }
|
||||
if (exponent.value === 2 && base.dimension === 'length') return { value: base.value ** 2, dimension: 'area' }
|
||||
if (exponent.value === 3 && base.dimension === 'length') return { value: base.value ** 3, dimension: 'volume' }
|
||||
throw new TypeError(`Cannot raise ${base.dimension} to the power ${exponent.value}.`)
|
||||
}
|
||||
|
||||
const evaluateFunction = (name: string, args: Quantity[]): Quantity => {
|
||||
const normalized = name.toLowerCase()
|
||||
if (normalized === 'abs' || normalized === 'round') {
|
||||
@@ -170,7 +185,13 @@ class QuantityParser {
|
||||
private parseUnary(): Quantity {
|
||||
if (this.match('+')) return this.parseUnary()
|
||||
if (this.match('-')) { const value = this.parseUnary(); return { ...value, value: -value.value } }
|
||||
return this.parsePrimary()
|
||||
return this.parsePower()
|
||||
}
|
||||
|
||||
private parsePower(): Quantity {
|
||||
const base = this.parsePrimary()
|
||||
if (!this.match('^')) return base
|
||||
return power(base, this.parseUnary())
|
||||
}
|
||||
|
||||
private parsePrimary(): Quantity {
|
||||
|
||||
@@ -129,8 +129,13 @@ test('quantity expressions convert units and reject incompatible dimensions', ()
|
||||
assert.equal(evaluateQuantityExpression('max(2 mm, 1 cm)').value.value, 10)
|
||||
assert.equal(evaluateQuantityExpression('clamp(15, 0, 10)').value.value, 10)
|
||||
assert.equal(evaluateQuantityExpression('round(2.6)').value.value, 3)
|
||||
assert.equal(evaluateQuantityExpression('2 mm ^ 2').value.dimension, 'area')
|
||||
assert.equal(evaluateQuantityExpression('2 mm ^ 3').value.dimension, 'volume')
|
||||
assert.equal(evaluateQuantityExpression('1 cm2 + 100 mm2').value.value, 200)
|
||||
assert.equal(evaluateQuantityExpression('2 ^ 3').value.value, 8)
|
||||
assert.throws(() => evaluateQuantityExpression('sin(2 mm)'), /angle or dimensionless/)
|
||||
assert.throws(() => evaluateQuantityExpression('unknown(1)'), /Unknown expression function/)
|
||||
assert.throws(() => evaluateQuantityExpression('2 mm ^ 0.5'), /dimensionless integer/)
|
||||
})
|
||||
|
||||
test('topology signatures are independent of transient face indexes and flag ambiguity', () => {
|
||||
|
||||
Reference in New Issue
Block a user